Last active
February 10, 2026 16:59
-
-
Save D1360-64RC14/fee5caad4bb9033438f8385157b1a832 to your computer and use it in GitHub Desktop.
PHP class to filter entire structs using dot array accessors (array->get('some.list.*.name')) and fluent API. PHP 8.0+.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php /* Example */ | |
| $data = [ | |
| 'hello' => 1, | |
| 'my' => 2, | |
| 'ducking' => 3, | |
| 'beautiful' => 4, | |
| 'place' => 5, | |
| 'another' => [ | |
| 'inner' => [ | |
| 'content' => 10 | |
| ], | |
| ], | |
| ]; | |
| $struct = new StructMap() | |
| ->hide('my') | |
| ->hide('ducking') | |
| ->rename('place', 'world') | |
| ->on('another.inner', static fn($m) => $m | |
| ->hide('content') | |
| ->add('array', 'also works')); | |
| $array = $struct->withStruct($data)->toArray(); | |
| print_r($array); | |
| // Array | |
| // ( | |
| // [hello] => 1 | |
| // [beautiful] => 4 | |
| // [world] => 5 | |
| // [another] => Array ( | |
| // [inner] => Array ( | |
| // [array] => also works | |
| // ) | |
| // ) | |
| // ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // Copyright 2026 Diego Garcia | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| use Closure; | |
| use Generator; | |
| use function count; | |
| use function is_int; | |
| use function is_array; | |
| /** | |
| * StructMap allows you to perform various transform tasks on array structures. | |
| * | |
| * Supported tasks: | |
| * - Allowing only a subset of keys | |
| * - Hiding a subset of keys | |
| * - Adding new keys, with values | |
| * - Renaming keys | |
| * - Transforming values | |
| * | |
| * The same StructMap instance can be applied to multiple structures using the | |
| * `withStruct()` method. | |
| */ | |
| final class StructMap | |
| { | |
| private array $workingPath = []; | |
| private array $actionMapping = [ | |
| 'allow' => [], | |
| 'hide' => [], | |
| 'add' => [], | |
| 'transform' => [], | |
| 'rename' => [], | |
| ]; | |
| /** | |
| * Null when is template | |
| */ | |
| private ?array $struct; | |
| public function __construct(?array $struct = null) | |
| { | |
| $this->struct = $struct; | |
| } | |
| /** | |
| * Points the current path to a specific location in the structure. | |
| * | |
| * Example: | |
| * | |
| * ```php | |
| * <?php | |
| * new StructMap()->on('payment.*.user', static fn($s) => ...); | |
| * ``` | |
| * | |
| * @param string $path | |
| * @param callable(self $map):void $mapping | |
| * @return self | |
| */ | |
| public function on(string $path, callable $mapping): self | |
| { | |
| $previousWorkingPath = $this->workingPath; | |
| $path = $this->splitPath($path); | |
| $this->workingPath = [...$this->workingPath, ...$path]; | |
| $mapping($this); | |
| $this->workingPath = $previousWorkingPath; | |
| return $this; | |
| } | |
| /** | |
| * Allows only specified keys in the object. | |
| * | |
| * All keys different from the specified ones are excluded. | |
| * | |
| * Example: | |
| * | |
| * ```php | |
| * <?php | |
| * new StructMap() | |
| * ->allow('id') | |
| * ->allow('name') | |
| * ->allow('phone') | |
| * ->allow('created_at'); | |
| * ``` | |
| * | |
| * @param string|Closure(string $key, mixed $value):bool $key | |
| * @return self | |
| */ | |
| public function allow(string|Closure $key): self | |
| { | |
| $this->actionMapping['allow'][] = [$this->workingPath, [$key]]; | |
| return $this; | |
| } | |
| /** | |
| * Hides the specified keys (excludes) from the object. | |
| * | |
| * All specified keys are excluded, keeping the other ones. | |
| * | |
| * Example: | |
| * | |
| * ```php | |
| * <?php | |
| * new StructMap() | |
| * ->hide('updated_at') | |
| * ->hide('deleted_at'); | |
| * ``` | |
| * | |
| * @param string|Closure(string $key, mixed $value):bool $key | |
| * @return self | |
| */ | |
| public function hide(string|Closure $key): self | |
| { | |
| $this->actionMapping['hide'][] = [$this->workingPath, [$key]]; | |
| return $this; | |
| } | |
| /** | |
| * Adds a key to the object with the specified value. | |
| * | |
| * Example: | |
| * | |
| * ```php | |
| * <?php | |
| * new StructMap() | |
| * ->add('tipo', UserType::Consumer); | |
| * ``` | |
| * | |
| * @param string $key | |
| * @param mixed $value | |
| * @return self | |
| */ | |
| public function add(string $key, mixed $value): self | |
| { | |
| $this->actionMapping['add'][] = [$this->workingPath, [$key, $value]]; | |
| return $this; | |
| } | |
| /** | |
| * Transforms the value of a key in the object using a function. | |
| * | |
| * Example: | |
| * | |
| * ```php | |
| * <?php | |
| * new StructMap() | |
| * ->transform('value', static fn($value) => 'R$ ' . number_format($value ?? 0, 2, ',', '.')); | |
| * | |
| * new StructMap() | |
| * ->transform('value', static fn($value, $struct) => "{$struct['currency']} " . number_format($value ?? 0, 2, ',', '.')); | |
| * ``` | |
| * | |
| * @param string $key | |
| * @param callable(mixed $value, mixed $struct):mixed $transformer | |
| * @return self | |
| */ | |
| public function transform(string $key, callable $transformer): self | |
| { | |
| $this->actionMapping['transform'][] = [$this->workingPath, [$key, $transformer]]; | |
| return $this; | |
| } | |
| /** | |
| * Renames a key in the object. | |
| * | |
| * Example: | |
| * | |
| * ```php | |
| * <?php | |
| * new StructMap() | |
| * ->rename('id_user', 'id'); | |
| * ``` | |
| * | |
| * @param string $oldKey | |
| * @param string $newKey | |
| * @return StructMap | |
| */ | |
| public function rename(string $oldKey, string $newKey): self | |
| { | |
| $this->actionMapping['rename'][] = [$this->workingPath, [$oldKey, $newKey]]; | |
| return $this; | |
| } | |
| /** | |
| * Defines the structure to be restructured. | |
| * | |
| * Allows the same mapping to be applied to other structures. | |
| * | |
| * @param array $struct | |
| * @return self | |
| */ | |
| public function withStruct(array $struct): self | |
| { | |
| $this->struct = $struct; | |
| return $this; | |
| } | |
| /** | |
| * Returns the restructured structure with the mappings applied. | |
| * | |
| * @throws \InvalidArgumentException | |
| * @return array | |
| */ | |
| public function toArray(): array | |
| { | |
| if (!isset($this->struct)) { | |
| throw new \InvalidArgumentException('StructMap must have a struct. Use withStruct() before toArray()'); | |
| } | |
| $this->sortActions(); | |
| return $this->restruct($this->struct, []); | |
| } | |
| private function restruct(array $struct, array $path) | |
| { | |
| $actions = $this->actionsForPath($path); | |
| $result = []; | |
| foreach ($this->restructLayer($struct, $actions) as $key => $value) { | |
| $entry = is_array($value) | |
| ? $this->restruct($value, [...$path, $key]) | |
| : $value; | |
| if (is_int($key)) { | |
| $result[] = $entry; | |
| } else { | |
| $result[$key] = $entry; | |
| } | |
| } | |
| return $result; | |
| } | |
| private function restructLayer(array $struct, array $actions) | |
| { | |
| $result = processAllow($struct, $actions['allow']); | |
| $result = processHide($result, $actions['hide']); | |
| $result = processAdd($result, $actions['add']); | |
| $result = processTransform($result, $actions['transform']); | |
| $result = processRename($result, $actions['rename']); | |
| return iterator_to_array($result); | |
| } | |
| private function actionsForPath(array $path): array | |
| { | |
| $path = array_map(static fn(string|int $p): string => is_int($p) ? '*' : $p, $path); | |
| $result = []; | |
| foreach ($this->actionMapping as $name => $actions) { | |
| $result[$name] = []; | |
| foreach ($actions as [$actPath, $params]) { | |
| if ($actPath === $path) { | |
| $result[$name][] = [$actPath, $params]; | |
| } | |
| } | |
| } | |
| return $result; | |
| } | |
| private function sortActions() | |
| { | |
| foreach ($this->actionMapping as $name => &$actions) { | |
| usort($actions, static fn(array $a, array $b): int => count($a[0]) - count($b[0])); | |
| } | |
| } | |
| private function splitPath(string $path): array | |
| { | |
| return explode('.', $path); | |
| } | |
| } | |
| function processAllow(iterable $struct, array $actions): Generator | |
| { | |
| if (count($actions) <= 0) | |
| return yield from $struct; | |
| foreach ($struct as $key => $value) { | |
| foreach ($actions as [$actPath, [$actionKeyOrFn]]) { | |
| $matches = $actionKeyOrFn instanceof Closure | |
| ? $actionKeyOrFn($key, $value) | |
| : $actionKeyOrFn === $key; | |
| if ($matches) { | |
| yield $key => $value; | |
| goto ignoreLoopTail; | |
| } | |
| } | |
| ignoreLoopTail: | |
| } | |
| } | |
| function processHide(iterable $struct, array $actions): Generator | |
| { | |
| if (count($actions) <= 0) | |
| return yield from $struct; | |
| foreach ($struct as $key => $value) { | |
| foreach ($actions as [$actPath, [$actionKeyOrFn]]) { | |
| $matches = $actionKeyOrFn instanceof Closure | |
| ? $actionKeyOrFn($key, $value) | |
| : $actionKeyOrFn === $key; | |
| if ($matches) { | |
| goto ignoreKeyValue; | |
| } | |
| } | |
| yield $key => $value; | |
| ignoreKeyValue: | |
| } | |
| } | |
| function processAdd(iterable $struct, array $actions): Generator | |
| { | |
| yield from $struct; | |
| foreach ($actions as [$actPath, [$key, $value]]) { | |
| yield $key => $value; | |
| } | |
| } | |
| function processTransform(iterable $struct, array $actions): Generator | |
| { | |
| if (count($actions) <= 0) | |
| return yield from $struct; | |
| foreach ($struct as $key => $value) { | |
| foreach ($actions as [$actPath, [$actKey, $transformer]]) { | |
| if ($actKey === $key) { | |
| yield $actKey => $transformer($value, $struct); | |
| goto breakFromInner; | |
| } | |
| } | |
| yield $key => $value; | |
| breakFromInner: | |
| } | |
| } | |
| function processRename(iterable $struct, array $actions): Generator | |
| { | |
| if (count($actions) <= 0) | |
| return yield from $struct; | |
| foreach ($struct as $key => $value) { | |
| foreach ($actions as [$actPath, [$oldKey, $newKey]]) { | |
| if ($oldKey === $key) { | |
| yield $newKey => $value; | |
| goto breakFromInner; | |
| } | |
| } | |
| yield $key => $value; | |
| breakFromInner: | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment