Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/arrayable filters #7

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
46 changes: 26 additions & 20 deletions src/ArrayableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ public function extraFields(): array
* Specify an 'extra' to include additional fields if implementing the
* ArrayableFields interface. This will add properties that were excluded
* from `fields()` and includes any virtual fields defined in `extraFields()`.
* included in `fields()` and any virtual fields defined in `extraFields()`.
*
* @param string[]|null $filter
* @param string[]|null $extra
Expand All @@ -124,31 +123,31 @@ public function toArray(array $filter = null, array $extra = null): array
$fields = array_merge($fields, $this->fields());
}

// Add extra fields.
if ($extra) {
$extra_keys = array_fill_keys($extra, true);
$extra_fields = [];
// Apply filters.
if ($filter) {
$filter_keys = Arrays::keyRoots($filter);
$filter_keys = array_fill_keys($filter_keys, true);

// Extract any virtual fields.
if ($this instanceof ArrayableFields) {
$extra_fields = $this->extraFields();
$extra_fields = array_intersect_key($extra_fields, $extra_keys);
}
$fields = array_intersect_key($fields, $filter_keys);

// The 'extras' field can also reference natural fields or those
// in fields(), so we layer them all together.
$fields = array_merge($fields, $extra_keys, $extra_fields);
// Re-apply any in 'filter' not already in fields().
$fields = array_merge($filter_keys, $fields);
}

// Apply filters.
if ($filter) {
$filter = array_fill_keys($filter, true);
// Add extra fields.
if ($extra) {
$extra_keys = Arrays::keyRoots($extra, true);
$extra_keys = array_fill_keys($extra_keys, true);

// Extract any virtual fields.
$fields = array_intersect_key($fields, $filter);
$fields = array_merge($fields, $extra_keys);

// Re-apply any in 'filter' not already in fields().
$fields = array_merge($filter, $fields);
if ($this instanceof ArrayableFields) {
$extra_fields = $this->extraFields();
$extra_fields = array_intersect_key($extra_fields, $extra_keys);

$fields = array_merge($fields, $extra_fields);
}
}

$array = [];
Expand Down Expand Up @@ -199,7 +198,14 @@ public function toArray(array $filter = null, array $extra = null): array
or $item instanceof Arrayable
or $item instanceof Traversable
) {
$item = Arrays::toArray($item);
$next_filter = $filter ? Arrays::keyChildren($key, $filter) : null;
$next_extra = $extra ? Arrays::keyChildren($key, $extra, true) : null;

$item = Arrays::toArray($item, $next_filter, $next_extra);

if (empty($item)) {
continue;
}
}

$array[$key] = $item;
Expand Down
289 changes: 277 additions & 12 deletions src/Arrays.php
Original file line number Diff line number Diff line change
Expand Up @@ -799,39 +799,100 @@ public static function explodeKeys(array $array, string $glue = '.', $index = ''
* This converts any nested arrayables to arrays.
*
* @param Arrayable|Traversable|array $array
* @param array|null $filter
* @param array|null $extra
* @return array
*/
public static function toArray($array): array
public static function toArray($array, array $filter = null, array $extra = null): array
{
// This performs it's own filtering.
if ($array instanceof ArrayableFields) {
return $array->toArray($filter, $extra);
}

if ($array instanceof Arrayable) {
return $array->toArray();
$array = $array->toArray();
}

$filter_roots = null;

// Extract filter roots.
if ($filter) {
$filter_roots = self::keyRoots($filter);
$filter_roots = array_fill_keys($filter_roots, true);
}

foreach ($array as &$item) {
$items = [];

foreach ($array as $key => $item) {

// Do filtering.
if (
$filter_roots !== null
and !is_numeric($key)
and !array_key_exists($key, $filter_roots)
) {
continue;
}

// Like, what else would we do here?
if (is_resource($item)) {
$item[$key] = '(resource)';
continue;
}

if ($item instanceof ArrayableFields) {
$next_filter = self::keyChildren($key, $filter ?? []);
$next_extra = self::keyChildren($key, $extra ?? [], true);

$item = $item->toArray($next_filter, $next_extra);

if (empty($item)) {
continue;
}

// This performs it's own filtering so we can skip the rest.
$items[$key] = $item;
continue;
}

if ($item instanceof Arrayable) {
$item = $item->toArray();
continue;
}

if ($item instanceof Traversable) {
$item = iterator_to_array($item);
$next_filter = self::keyChildren($key, $filter ?? []);
$next_extra = self::keyChildren($key, $extra ?? [], true);

$item = self::toArray($item, $next_filter, $next_extra);

if (empty($item)) {
continue;
}

$items[$key] = $item;
continue;
}

if (is_object($item)) {
$item = (array) $item;
continue;
}

// Like, what else would we do here?
if (is_resource($item)) {
$item = '(resource)';
continue;
if (is_array($item) and ($filter or $extra)) {
$next_filter = self::keyChildren($key, $filter ?? []);
$next_extra = self::keyChildren($key, $extra ?? [], true);

$item = self::toArray($item, $next_filter, $next_extra);

if (empty($item)) {
continue;
}
}

$items[$key] = $item;
}
unset($item);

return $array;
return $items;
}


Expand Down Expand Up @@ -968,6 +1029,210 @@ public static function value($array, string $query)
}


/**
* Shift all the root elements from a list of array queries.
*
* ```
* $keys = [
* 'one.two.three',
* 'def.ghi',
* '**.deep',
* ];
*
* $shift = Arrays::shiftKeys($keys);
* // $shift == [ 'one', 'def', 'deep' ]
* // $keys == [ 'two.three', 'ghi', '**.deep' ]
*
* $shift = Arrays::shiftKeys($keys);
* // $shift == [ 'two', 'ghi', 'deep' ]
* // $keys == [ 'three', '**.deep' ]
*
* $shift = Arrays::shiftKeys($keys);
* // $shift == [ 'three', 'deep' ]
* // $keys == [ '**.deep' ]
* ```
*
* @param string[] $keys modified by ref, leaving only children
* @return string[] the key roots
*/
public static function shiftKeys(array &$keys): array
{
$first = [];
$numeric = true;
$i = 0;

foreach ($keys as $index => &$key) {
if ($index !== $i++) {
$numeric = false;
}

$parts = explode('.', $key, 2);
$part = array_shift($parts);

if (empty($parts)) {
$first[$index] = $part;
unset($keys[$index]);
}
else {
$first[$index] = $part;
$key = $parts[0];
}
}

unset($key);

if ($numeric) {
$keys = array_values($keys);
$first = array_values($first);
}

return $first;
}


/**
* Get root keys from a list of array queries.
*
* Wildcards will replace root `*` (star) key with the first child.
*
* ```
* $keys = [
* 'root1.child1',
* 'root2.child2.deep',
* '*.child3',
* ];
*
* $roots = Arrays::keyRoots($keys);
* // => [ 'root1', 'root2' ]
*
* $roots = Arrays::keyRoots($keys, true);
* // => [ 'root1', 'root2', 'child3' ]
* ```
*
* @param string[] $keys
* @param bool $wildcard
* @return string[]
*/
public static function keyRoots(array $keys, bool $wildcard = false): array
{
// Nothing to see here.
if (empty($keys)) {
return [];
}

$roots = [];

foreach ($keys as $key) {
$parts = explode('.', $key, 3);
$part = array_shift($parts);

if ($part === '*') {
// Drop wildcard keys if not enabled.
if (!$wildcard) {
continue;
}

// End of the line.
if (empty($parts)) {
continue;
}

// Grab the first child.
$part = array_shift($parts);
}

$roots[$part] = $part;
}

$roots = array_keys($roots);
return $roots;
}


/**
* Get child keys from a list of array queries.
*
* Wildcards permit a root `*` (star) key to exists no matter the target.
*
* ```
* $keys = [
* 'root1.child1',
* 'root2.child2',
* 'root2.child3.deep',
* '*.child4',
* ];
*
* // No wildcards
* $children = Arrays::keyChildren('root2', $keys);
* // => [ 'root2.child2', 'root2.child3.deep' ]
*
* // Wildcards
* $children = Arrays::keyChildren('root1', $keys, true);
* // => [ 'child1', 'child4' ]
*
* // Also wildcards:
* $children = Arrays::keyChildren('other', $keys, true);
* // => [ 'child2' ]
* ```
*
* @param string $root
* @param array $keys
* @param bool $wildcard
* @return array
*/
public static function keyChildren(string $root, array $keys, bool $wildcard = false): array
{
// Empty keys? boring.
// Numeric? can't help.
// wildcard root? - you get everything.
if (
empty($keys)
or is_numeric($root)
or $root === '*'
) {
return $keys;
}

$children = [];

foreach ($keys as $key) {
$parts = explode('.', $key, 3);
$part = array_shift($parts);

if (empty($parts)) {
continue;
}

if ($part === $root) {
$child = implode('.', $parts);
$children[$child] = $child;
continue;
}

if ($wildcard and $part === '*') {
$child = implode('.', $parts);
$children[$child] = $child;

// If the first child is the root then flatten it.
// wild + *.wild.card => card
if ($parts[0] === $root) {
$child = $parts[1] ?? false;
$children[$child] = $child;
}

// The wildcard lives forever so it can recurse.
$children[$key] = $key;

continue;
}
}

// Don't forget to dedupe.
$children = array_keys($children);
return $children;
}


/**
* Shorthand for putting together [key => value] maps.
*
Expand Down
Loading