1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Contract\Core\ArrayMapperFlag;
6: use Salient\Contract\Core\ListConformity;
7: use Salient\Utility\Arr;
8: use InvalidArgumentException;
9:
10: /**
11: * Moves array values from one set of keys to another
12: *
13: * @api
14: */
15: final class ArrayMapper
16: {
17: /**
18: * Output key => input key
19: *
20: * @var array<array-key,array-key>
21: */
22: private array $OutputMap = [];
23:
24: /**
25: * Output keys
26: *
27: * @var array-key[]|null
28: */
29: private ?array $OutputKeys = null;
30:
31: /**
32: * Input key => output key
33: *
34: * @var array<array-key,array-key>
35: */
36: private array $KeyMap;
37:
38: private bool $RemoveNull;
39: private bool $AddUnmapped;
40: private bool $AddMissing;
41: private bool $RequireMapped;
42:
43: /**
44: * Creates a new ArrayMapper object
45: *
46: * By default, an array mapper:
47: *
48: * - populates an "output" array with values mapped from an "input" array
49: * - ignores missing values (maps for which there are no input values)
50: * - preserves unmapped values (input values for which there are no maps)
51: * - keeps `null` values in the output array
52: *
53: * Provide a bitmask of {@see ArrayMapperFlag} values to modify this
54: * behaviour.
55: *
56: * @param array<array-key,array-key|array-key[]> $keyMap An array that maps
57: * input keys to one or more output keys.
58: * @param ListConformity::* $conformity Use {@see ListConformity::COMPLETE}
59: * wherever possible to improve performance.
60: * @param int-mask-of<ArrayMapperFlag::*> $flags
61: */
62: public function __construct(
63: array $keyMap,
64: $conformity = ListConformity::NONE,
65: int $flags = ArrayMapperFlag::ADD_UNMAPPED
66: ) {
67: foreach ($keyMap as $inKey => $outKey) {
68: foreach ((array) $outKey as $outKey) {
69: $this->OutputMap[$outKey] = $inKey;
70: }
71: }
72:
73: $this->RemoveNull = (bool) ($flags & ArrayMapperFlag::REMOVE_NULL);
74:
75: if (
76: count($keyMap) === count($this->OutputMap)
77: && $conformity === ListConformity::COMPLETE
78: ) {
79: $this->OutputKeys = array_keys($this->OutputMap);
80: return;
81: }
82:
83: $this->KeyMap = $keyMap;
84: $this->AddUnmapped = (bool) ($flags & ArrayMapperFlag::ADD_UNMAPPED);
85: $this->AddMissing = (bool) ($flags & ArrayMapperFlag::ADD_MISSING);
86: $this->RequireMapped = (bool) ($flags & ArrayMapperFlag::REQUIRE_MAPPED);
87: }
88:
89: /**
90: * Map an input array to an output array
91: *
92: * @param array<array-key,mixed> $in
93: * @return array<array-key,mixed>
94: */
95: public function map(array $in): array
96: {
97: if ($this->OutputKeys !== null) {
98: $out = @array_combine($this->OutputKeys, $in);
99:
100: if ($out === false) {
101: throw new InvalidArgumentException('Invalid input array');
102: }
103:
104: return $this->RemoveNull
105: ? Arr::whereNotNull($out)
106: : $out;
107: }
108:
109: $out = [];
110: foreach ($this->OutputMap as $outKey => $inKey) {
111: if ($this->AddMissing || array_key_exists($inKey, $in)) {
112: $out[$outKey] = $in[$inKey] ?? null;
113: continue;
114: }
115: if ($this->RequireMapped) {
116: throw new InvalidArgumentException(sprintf('No data at input key: %s', $inKey));
117: }
118: }
119:
120: // Add unmapped values that don't conflict with output array keys
121: if ($this->AddUnmapped) {
122: $out = array_merge(
123: $out,
124: array_diff_key($in, $this->KeyMap, $this->OutputMap)
125: );
126: }
127:
128: return $this->RemoveNull
129: ? Arr::whereNotNull($out)
130: : $out;
131: }
132: }
133: