1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Contract\Core\ArrayMapperInterface;
6: use Salient\Contract\Core\ListConformity;
7: use Salient\Utility\Arr;
8: use InvalidArgumentException;
9: use ValueError;
10:
11: /**
12: * Moves array values from one set of keys to another
13: *
14: * @api
15: */
16: final class ArrayMapper implements ArrayMapperInterface
17: {
18: /** @var array-key[]|null */
19: private ?array $OutputKeys;
20: /** @var array<array-key,array-key> */
21: private array $OutputMap;
22: private bool $RemoveNull;
23: private bool $AddUnmapped;
24: private bool $RequireMapped;
25: private bool $AddMissing;
26: /** @var array<array-key,true> */
27: private array $InputKeyIndex;
28:
29: /**
30: * Creates a new ArrayMapper object
31: *
32: * By default, an array mapper:
33: *
34: * - ignores missing values (maps where the input array has no data)
35: * - preserves unmapped values (input that isn't mapped to a different key)
36: * - does not remove `null` values from the output array
37: *
38: * @param array<array-key,array-key|array-key[]> $keyMap An array that maps
39: * input keys to one or more output keys.
40: * @param ListConformity::* $conformity Use {@see ListConformity::COMPLETE}
41: * wherever possible to improve performance.
42: * @param int-mask-of<ArrayMapper::*> $flags
43: */
44: public function __construct(
45: array $keyMap,
46: int $conformity = ListConformity::NONE,
47: int $flags = ArrayMapper::ADD_UNMAPPED
48: ) {
49: $outKeyMap = [];
50: foreach ($keyMap as $inKey => $outKeys) {
51: foreach ((array) $outKeys as $outKey) {
52: $outKeyMap[$outKey] = $inKey;
53: }
54: }
55:
56: $this->RemoveNull = (bool) ($flags & self::REMOVE_NULL);
57:
58: if (
59: $conformity === ListConformity::COMPLETE
60: && count($keyMap) === count($outKeyMap)
61: ) {
62: $this->OutputKeys = array_keys($outKeyMap);
63: return;
64: }
65:
66: $this->OutputKeys = null;
67: $this->OutputMap = $outKeyMap;
68: $this->AddUnmapped = (bool) ($flags & self::ADD_UNMAPPED);
69: $this->RequireMapped = (bool) ($flags & self::REQUIRE_MAPPED);
70: $this->AddMissing = !$this->RequireMapped && $flags & self::ADD_MISSING;
71:
72: if ($this->AddUnmapped) {
73: $this->InputKeyIndex = array_fill_keys(array_keys($keyMap), true);
74: }
75: }
76:
77: /**
78: * @inheritDoc
79: */
80: public function map(array $in): array
81: {
82: if ($this->OutputKeys !== null) {
83: try {
84: $out = Arr::combine($this->OutputKeys, $in);
85: } catch (ValueError $ex) {
86: throw new InvalidArgumentException('Invalid input array', 0, $ex);
87: }
88:
89: return $this->RemoveNull ? Arr::whereNotNull($out) : $out;
90: }
91:
92: $out = [];
93: foreach ($this->OutputMap as $outKey => $inKey) {
94: if (array_key_exists($inKey, $in)) {
95: $out[$outKey] = $in[$inKey];
96: } elseif ($this->AddMissing) {
97: $out[$outKey] = null;
98: } elseif ($this->RequireMapped) {
99: throw new InvalidArgumentException(sprintf(
100: 'Input key not found: %s',
101: $inKey,
102: ));
103: }
104: }
105:
106: if ($this->AddUnmapped) {
107: $out += array_diff_key($in, $this->InputKeyIndex);
108: }
109:
110: return $this->RemoveNull ? Arr::whereNotNull($out) : $out;
111: }
112: }
113: