Browse Source

Merge pull request #6702 from cakephp/collection-improvements

Implemented {n} notation for Collection::extract()
Mark Story 10 years ago
parent
commit
925717ef9b

+ 13 - 0
src/Collection/CollectionInterface.php

@@ -213,6 +213,19 @@ interface CollectionInterface extends Iterator, JsonSerializable
      * ['Mark', 'Renan']
      * ```
      *
+     * It is also possible to extract a flattened collection out of nested properties
+     *
+     * ```
+     *  $items = [
+     *      ['comment' => ['votes' => [['value' => 1], ['value' => 2], ['value' => 3]]],
+     *      ['comment' => ['votes' => [['value' => 4]]
+     * ];
+     * $extracted = (new Collection($items))->extract('comment.votes.{*}.value');
+     *
+     * // Result will contain
+     * [1, 2, 3, 4]
+     * ```
+     *
      * @param string $matcher a dot separated string symbolizing the path to follow
      * inside the hierarchy of each value so that the column can be extracted.
      * @return \Cake\Collection\CollectionInterface

+ 10 - 2
src/Collection/CollectionTrait.php

@@ -158,11 +158,19 @@ trait CollectionTrait
     /**
      * {@inheritDoc}
      *
-     * @return \Cake\Collection\Iterator\ExtractIterator
      */
     public function extract($matcher)
     {
-        return new ExtractIterator($this->unwrap(), $matcher);
+        $extractor = new ExtractIterator($this->unwrap(), $matcher);
+        if (is_string($matcher) && strpos($matcher, '{*}') !== false) {
+            $extractor = $extractor
+                ->filter(function ($data) {
+                    return $data !== null && ($data instanceof \Traversable || is_array($data));
+                })
+                ->unfold();
+        }
+
+        return $extractor;
     }
 
     /**

+ 52 - 5
src/Collection/ExtractTrait.php

@@ -32,19 +32,27 @@ trait ExtractTrait
      */
     protected function _propertyExtractor($callback)
     {
-        if (is_string($callback)) {
-            $path = explode('.', $callback);
-            $callback = function ($element) use ($path) {
+        if (!is_string($callback)) {
+            return $callback;
+        }
+
+        $path = explode('.', $callback);
+
+        if (strpos($callback, '{*}') !== false) {
+            return function ($element) use ($path) {
                 return $this->_extract($element, $path);
             };
         }
 
-        return $callback;
+        return function ($element) use ($path) {
+            return $this->_simpleExtract($element, $path);
+        };
     }
 
     /**
      * Returns a column from $data that can be extracted
-     * by iterating over the column names contained in $path
+     * by iterating over the column names contained in $path.
+     * It will return arrays for elements in represented with `{*}`
      *
      * @param array|\ArrayAccess $data Data.
      * @param array $path Path to extract from.
@@ -53,6 +61,45 @@ trait ExtractTrait
     protected function _extract($data, $path)
     {
         $value = null;
+        $collectionTransform = false;
+
+        foreach ($path as $i => $column) {
+            if ($column === '{*}') {
+                $collectionTransform = true;
+                continue;
+            }
+
+            if ($collectionTransform &&
+                !($data instanceof \Traversable || is_array($data))) {
+                return null;
+            }
+
+            if ($collectionTransform) {
+                $rest = implode('.', array_slice($path, $i));
+                return (new Collection($data))->extract($rest);
+            }
+
+            if (!isset($data[$column])) {
+                return null;
+            }
+
+            $value = $data[$column];
+            $data = $value;
+        }
+        return $value;
+    }
+
+    /**
+     * Returns a column from $data that can be extracted
+     * by iterating over the column names contained in $path
+     *
+     * @param array|\ArrayAccess $data Data.
+     * @param array $path Path to extract from.
+     * @return mixed
+     */
+    protected function _simpleExtract($data, $path)
+    {
+        $value = null;
         foreach ($path as $column) {
             if (!isset($data[$column])) {
                 return null;

+ 52 - 0
tests/TestCase/Collection/CollectionTest.php

@@ -1381,4 +1381,56 @@ class CollectionTest extends TestCase
         $collection = new Collection(['a' => 1, 'b' => 4, 'c' => 6]);
         $this->assertEquals(11, $collection->sumOf());
     }
+
+    /**
+     * Tests using extract with the {*} notation
+     *
+     * @return void
+     */
+    public function testUnfoldedExtract()
+    {
+        $items = [
+            ['comments' => [['id' => 1], ['id' => 2]]],
+            ['comments' => [['id' => 3], ['id' => 4]]],
+            ['comments' => [['id' => 7], ['nope' => 8]]],
+        ];
+
+        $extracted = (new Collection($items))->extract('comments.{*}.id');
+        $this->assertEquals([1, 2, 3, 4, 7, null], $extracted->toList());
+
+        $items = [
+            [
+                'comments' => [
+                    [
+                        'voters' => [['id' => 1], ['id' => 2]]
+                    ]
+                ]
+            ],
+            [
+                'comments' => [
+                    [
+                        'voters' => [['id' => 3], ['id' => 4]]
+                    ]
+                ]
+            ],
+            [
+                'comments' => [
+                    [
+                        'voters' => [['id' => 5], ['nope' => 'fail'], ['id' => 6]]
+                    ]
+                ]
+            ],
+            [
+                'comments' => [
+                    [
+                        'not_voters' => [['id' => 5]]
+                    ]
+                ]
+            ],
+            ['not_comments' => []]
+        ];
+        $extracted = (new Collection($items))->extract('comments.{*}.voters.{*}.id');
+        $expected = [1, 2, 3, 4, 5, null, 6];
+        $this->assertEquals($expected, $extracted->toList());
+    }
 }