Skip to content

Instantly share code, notes, and snippets.

@D1360-64RC14
Last active February 10, 2026 16:59
Show Gist options
  • Select an option

  • Save D1360-64RC14/fee5caad4bb9033438f8385157b1a832 to your computer and use it in GitHub Desktop.

Select an option

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+.
<?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
// )
// )
// )
<?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