diff --git a/README.md b/README.md index 9088e63b0..287e838aa 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ the methods always return the same values for the same inputs. | `nth` | new Collection object | [Nth.php](./src/Operation/Append.php) | `only` | new Collection object | [Only.php](./src/Operation/Append.php) | `pad` | new Collection object | [Pad.php](./src/Operation/Append.php) +| `pluck` | new Collection object | [Pluck.php](./src/Operation/Pluck.php) | `prepend` | new Collection object | [Prepend.php](./src/Operation/Append.php) | `reduce` | mixed | [Collection.php](./src/Collection.php) | `run` | new Collection object | [Collection.php](./src/Collection.php) diff --git a/spec/drupol/collection/CollectionSpec.php b/spec/drupol/collection/CollectionSpec.php index 2a3576ec1..5423b3676 100644 --- a/spec/drupol/collection/CollectionSpec.php +++ b/spec/drupol/collection/CollectionSpec.php @@ -498,6 +498,75 @@ public function it_can_pad(): void ->shouldReturn(['A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E', 0 => 'foo', 1 => 'foo', 2 => 'foo', 3 => 'foo', 4 => 'foo']); } + public function it_can_pluck(): void + { + $six = new class() { + public $foo = [ + 'bar' => 5, + ]; + }; + + $input = [ + [ + 'foo' => [ + 'bar' => 0, + ], + ], + [ + 'foo' => [ + 'bar' => 1, + ], + ], + [ + 'foo' => [ + 'bar' => 2, + ], + ], + Collection::withArray( + [ + 'foo' => [ + 'bar' => 3, + ], + ] + ), + new \ArrayObject([ + 'foo' => [ + 'bar' => 4, + ], + ]), + new class() { + public $foo = [ + 'bar' => 5, + ]; + }, + [ + 'foo' => [ + 'bar' => $six, + ], + ], + ]; + + $this::with($input) + ->pluck('foo') + ->shouldIterateAs([0 => ['bar' => 0], 1 => ['bar' => 1], 2 => ['bar' => 2], 3 => ['bar' => 3], 4 => ['bar' => 4], 5 => ['bar' => 5], 6 => ['bar' => $six]]); + + $this::with($input) + ->pluck('foo.*') + ->shouldIterateAs([0 => [0 => 0], 1 => [0 => 1], 2 => [0 => 2], 3 => [0 => 3], 4 => [0 => 4], 5 => [0 => 5], 6 => [0 => $six]]); + + $this::with($input) + ->pluck('foo.bar') + ->shouldIterateAs([0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => $six]); + + $this::with($input) + ->pluck('foo.bar.*', 'taz') + ->shouldIterateAs([0 => 'taz', 1 => 'taz', 2 => 'taz', 3 => 'taz', 4 => 'taz', 5 => 'taz', 6 => 'taz']); + + $this::with($input) + ->pluck('azerty', 'taz') + ->shouldIterateAs([0 => 'taz', 1 => 'taz', 2 => 'taz', 3 => 'taz', 4 => 'taz', 5 => 'taz', 6 => 'taz']); + } + public function it_can_prepend(): void { $this diff --git a/src/Collection.php b/src/Collection.php index ea3604e60..b8ee0a305 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -23,6 +23,7 @@ use drupol\collection\Operation\Nth; use drupol\collection\Operation\Only; use drupol\collection\Operation\Pad; +use drupol\collection\Operation\Pluck; use drupol\collection\Operation\Prepend; use drupol\collection\Operation\Range; use drupol\collection\Operation\Skip; @@ -256,6 +257,14 @@ public function pad(int $size, $value): CollectionInterface return $this->run(Pad::with($size, $value)); } + /** + * {@inheritdoc} + */ + public function pluck($pluck, $default = null): CollectionInterface + { + return $this->run(Pluck::with($pluck, $default)); + } + /** * {@inheritdoc} */ diff --git a/src/Contract/Collection.php b/src/Contract/Collection.php index 58f4cd718..538d3b6e8 100644 --- a/src/Contract/Collection.php +++ b/src/Contract/Collection.php @@ -196,6 +196,14 @@ public function only(...$keys): self; */ public function pad(int $size, $value): self; + /** + * @param array|string $pluck + * @param null|mixed $default + * + * @return \drupol\collection\Contract\Collection + */ + public function pluck($pluck, $default = null): self; + /** * Push an item onto the beginning of the collection. * diff --git a/src/Operation/Pluck.php b/src/Operation/Pluck.php new file mode 100644 index 000000000..d1187d896 --- /dev/null +++ b/src/Operation/Pluck.php @@ -0,0 +1,78 @@ +parameters; + $operation = $this; + + return Collection::withClosure( + static function () use ($key, $default, $collection, $operation) { + $key = \is_string($key) ? \explode('.', \trim($key, '.')) : $key; + + foreach ($collection as $item) { + yield $operation->pick($item, $key, $default); + } + } + ); + } + + /** + * Get an item from an array or object using "dot" notation. + * + * @param mixed $target + * @param array $key + * @param mixed $default + * + * @throws \ReflectionException + * + * @return mixed + */ + private function pick($target, array $key, $default = null) + { + while (null !== $segment = \array_shift($key)) { + if ('*' === $segment) { + if (!\is_array($target)) { + return $default; + } + + $result = []; + + foreach ($target as $item) { + $result[] = $this->pick($item, $key); + } + + return \in_array('*', $key, true) ? Collection::withArray($result)->collapse() : $result; + } + + if ((true === \is_array($target)) && (true === \array_key_exists($segment, $target))) { + $target = $target[$segment]; + } elseif (($target instanceof ArrayAccess) && (true === $target->offsetExists($segment))) { + $target = $target[$segment]; + } elseif ($target instanceof CollectionInterface) { + $target = $target->get($segment, $default); + } elseif ((true === \is_object($target)) && (true === \property_exists($target, $segment))) { + $target = (new \ReflectionClass($target))->getProperty($segment)->getValue($target); + } else { + $target = $default; + } + } + + return $target; + } +}