Browse Source

Merge branch 'master' into 3.2

Mark Story 10 years ago
parent
commit
5d9b4cbff0

+ 3 - 3
phpunit.xml.dist

@@ -45,13 +45,13 @@
         <env name="db_dsn" value="sqlite:///:memory:"/>
         -->
         <!-- Postgres
-        <env name="db_dsn" value="postgres://localhost/cake_test"/>
+        <env name="db_dsn" value="postgres://localhost/cake_test?timezone=UTC"/>
         -->
         <!-- Mysql
-        <env name="db_dsn" value="mysql://localhost/cake_test"/>
+        <env name="db_dsn" value="mysql://localhost/cake_test?timezone=UTC"/>
         -->
         <!-- SQL Server
-        <env name="db_dsn" value="sqlserver://localhost/cake_test"/>
+        <env name="db_dsn" value="sqlserver://localhost/cake_test?timezone=UTC"/>
         -->
     </php>
 </phpunit>

+ 19 - 3
src/Collection/CollectionInterface.php

@@ -812,13 +812,13 @@ interface CollectionInterface extends Iterator, JsonSerializable
     public function stopWhen($condition);
 
     /**
-     * Creates a new collection where the items that it will contain are the
+     * Creates a new collection where the items are the
      * concatenation of the lists of items generated by the transformer function
-     * after passing each of the items form the original collection.
+     * applied to each item in the original collection.
      *
      * The transformer function will receive the value and the key for each of the
      * items in the collection, in that order, and it must return an array or a
-     * Traversable object so that it can be concatenated to the final result.
+     * Traversable object that can be concatenated to the final result.
      *
      * If no transformer function is passed, an "identity" function will be used.
      * This is useful when each of the elements in the source collection are
@@ -904,6 +904,22 @@ interface CollectionInterface extends Iterator, JsonSerializable
     public function zipWith($items, $callable);
 
     /**
+     * Breaks the collection into smaller arrays of the given size.
+     *
+     * ### Example:
+     *
+     * ```
+     * $items [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
+     * $chunked = (new Collection($items))->chunk(3)->toList();
+     * // Returns [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11]]
+     * ```
+     *
+     * @param int $chunkSize The maximum size for each chunk
+     * @return \Cake\Collection\CollectionInterface
+     */
+    public function chunk($chunkSize);
+
+    /**
      * Returns whether or not there are elements in this collection
      *
      * ### Example:

+ 20 - 0
src/Collection/CollectionTrait.php

@@ -605,6 +605,26 @@ trait CollectionTrait
      * {@inheritDoc}
      *
      */
+    public function chunk($chunkSize)
+    {
+        return $this->map(function ($v, $k, $iterator) use ($chunkSize) {
+            $values = [$v];
+            for ($i = 1; $i < $chunkSize; $i++) {
+                $iterator->next();
+                if (!$iterator->valid()) {
+                    break;
+                }
+                $values[] = $iterator->current();
+            }
+
+            return $values;
+        });
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     */
     public function isEmpty()
     {
         foreach ($this->unwrap() as $el) {

+ 3 - 2
src/Collection/Iterator/NoChildrenIterator.php

@@ -18,8 +18,9 @@ use Cake\Collection\Collection;
 use RecursiveIterator;
 
 /**
- * An iterator that can be used as argument for other iterators that require
- * a RecursiveIterator, but that will always report as having no nested items.
+ * An iterator that can be used as an argument for other iterators that require
+ * a RecursiveIterator but do not want children. This iterator will
+ * always behave as having no nested items.
  */
 class NoChildrenIterator extends Collection implements RecursiveIterator
 {

+ 5 - 5
src/Collection/Iterator/UnfoldIterator.php

@@ -18,8 +18,8 @@ use IteratorIterator;
 use RecursiveIterator;
 
 /**
- * An iterator that can be used to generate nested iterators out of each of
- * applying an function to each of the elements in this iterator.
+ * An iterator that can be used to generate nested iterators out of a collection
+ * of items by applying an function to each of the elements in this iterator.
  *
  * @internal
  * @see Collection::unfold()
@@ -28,8 +28,8 @@ class UnfoldIterator extends IteratorIterator implements RecursiveIterator
 {
 
     /**
-     * A functions that gets passed each of the elements of this iterator and
-     * that must return an array or Traversable object.
+     * A function that is passed each element in this iterator and
+     * must return an array or Traversable object.
      *
      * @var callable
      */
@@ -70,7 +70,7 @@ class UnfoldIterator extends IteratorIterator implements RecursiveIterator
     }
 
     /**
-     * Returns an iterator containing the items generated out of transforming
+     * Returns an iterator containing the items generated by transforming
      * the current value with the callable function.
      *
      * @return \RecursiveIterator

+ 3 - 3
src/Console/ConsoleOptionParser.php

@@ -169,7 +169,7 @@ class ConsoleOptionParser
      *
      * @param string|null $command The command name this parser is for. The command name is used for generating help.
      * @param bool $defaultOptions Whether you want the verbose and quiet options set.
-     * @return ConsoleOptionParser
+     * @return $this
      */
     public static function create($command, $defaultOptions = true)
     {
@@ -197,7 +197,7 @@ class ConsoleOptionParser
      *
      * @param array $spec The spec to build the OptionParser with.
      * @param bool $defaultOptions Whether you want the verbose and quiet options set.
-     * @return ConsoleOptionParser
+     * @return $this
      */
     public static function buildFromArray($spec, $defaultOptions = true)
     {
@@ -371,7 +371,7 @@ class ConsoleOptionParser
      * Remove an option from the option parser.
      *
      * @param string $name The option name to remove.
-     * @return ConsoleOptionParser this
+     * @return $this
      */
     public function removeOption($name)
     {

+ 14 - 4
src/Console/Shell.php

@@ -41,6 +41,13 @@ class Shell
     use ModelAwareTrait;
 
     /**
+     * Default error code
+     *
+     * @var int
+     */
+    const CODE_ERROR = 1;
+
+    /**
      * Output constant making verbose shells.
      *
      * @var int
@@ -496,7 +503,7 @@ class Shell
      *
      * By overriding this method you can configure the ConsoleOptionParser before returning it.
      *
-     * @return ConsoleOptionParser
+     * @return \Cake\Console\ConsoleOptionParser
      * @link http://book.cakephp.org/3.0/en/console-and-shells.html#configuring-options-and-generating-help
      */
     public function getOptionParser()
@@ -711,7 +718,7 @@ class Shell
      *
      * @param string $title Title of the error
      * @param string|null $message An optional error message
-     * @return void
+     * @return int Error code
      * @link http://book.cakephp.org/3.0/en/console-and-shells.html#styling-output
      */
     public function error($title, $message = null)
@@ -721,7 +728,9 @@ class Shell
         if (!empty($message)) {
             $this->_io->err($message);
         }
-        $this->_stop(1);
+        $this->_stop(self::CODE_ERROR);
+
+        return self::CODE_ERROR;
     }
 
     /**
@@ -761,7 +770,8 @@ class Shell
 
             if (strtolower($key) === 'q') {
                 $this->_io->out('<error>Quitting</error>.', 2);
-                return $this->_stop();
+                $this->_stop();
+                return false;
             }
             if (strtolower($key) === 'a') {
                 $this->params['force'] = true;

+ 17 - 13
src/Datasource/RulesChecker.php

@@ -248,12 +248,7 @@ class RulesChecker
      */
     public function checkCreate(EntityInterface $entity, array $options = [])
     {
-        $success = true;
-        $options = $options + $this->_options;
-        foreach (array_merge($this->_rules, $this->_createRules) as $rule) {
-            $success = $rule($entity, $options) && $success;
-        }
-        return $success;
+        return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_createRules));
     }
 
     /**
@@ -266,12 +261,7 @@ class RulesChecker
      */
     public function checkUpdate(EntityInterface $entity, array $options = [])
     {
-        $success = true;
-        $options = $options + $this->_options;
-        foreach (array_merge($this->_rules, $this->_updateRules) as $rule) {
-            $success = $rule($entity, $options) && $success;
-        }
-        return $success;
+        return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_updateRules));
     }
 
     /**
@@ -284,9 +274,23 @@ class RulesChecker
      */
     public function checkDelete(EntityInterface $entity, array $options = [])
     {
+        return $this->_checkRules($entity, $options, $this->_deleteRules);
+    }
+
+    /**
+     * Used by top level functions checkDelete, checkCreate and checkUpdate, this function
+     * iterates an array containing the rules to be checked and check them all.
+     *
+     * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
+     * @param array $options Extra options to pass to checker functions.
+     * @param array $rules the list of rules that must be checked
+     * @return bool
+     */
+    protected function _checkRules(EntityInterface $entity, array $options = [], array $rules = [])
+    {
         $success = true;
         $options = $options + $this->_options;
-        foreach ($this->_deleteRules as $rule) {
+        foreach ($rules as $rule) {
             $success = $rule($entity, $options) && $success;
         }
         return $success;

+ 42 - 17
src/ORM/Rule/ExistsIn.php

@@ -16,6 +16,7 @@ namespace Cake\ORM\Rule;
 
 use Cake\Datasource\EntityInterface;
 use Cake\ORM\Association;
+use RuntimeException;
 
 /**
  * Checks that the value provided in a field exists as the primary key of another
@@ -57,22 +58,34 @@ class ExistsIn
      * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields
      * @param array $options Options passed to the check,
      * where the `repository` key is required.
+     * @throws \RuntimeException When the rule refers to an undefined association.
      * @return bool
      */
     public function __invoke(EntityInterface $entity, array $options)
     {
         if (is_string($this->_repository)) {
-            $this->_repository = $options['repository']->association($this->_repository);
-        }
-
-        $source = !empty($options['repository']) ? $options['repository'] : $this->_repository;
+            $alias = $this->_repository;
+            $this->_repository = $options['repository']->association($alias);
 
-        $source = $source instanceof Association ? $source->source() : $source;
-        $target = $this->_repository;
+            if (empty($this->_repository)) {
+                throw new RuntimeException(sprintf(
+                    "ExistsIn rule for '%s' is invalid. The '%s' association is not defined.",
+                    implode(', ', $this->_fields),
+                    $alias
+                ));
+            }
+        }
 
+        $source = $target = $this->_repository;
+        if (!empty($options['repository'])) {
+            $source = $options['repository'];
+        }
+        if ($source instanceof Association) {
+            $source = $source->source();
+        }
         if ($target instanceof Association) {
             $bindingKey = (array)$target->bindingKey();
-            $target = $this->_repository->target();
+            $target = $target->target();
         } else {
             $bindingKey = (array)$target->primaryKey();
         }
@@ -85,25 +98,37 @@ class ExistsIn
             return true;
         }
 
-        $nulls = 0;
-        $schema = $source->schema();
-        foreach ($this->_fields as $field) {
-            if ($schema->column($field) && $schema->isNullable($field) && $entity->get($field) === null) {
-                $nulls++;
-            }
-        }
-        if ($nulls === count($this->_fields)) {
+        if ($this->_fieldsAreNull($entity, $source)) {
             return true;
         }
 
         $primary = array_map(
-            [$this->_repository, 'aliasField'],
+            [$target, 'aliasField'],
             $bindingKey
         );
         $conditions = array_combine(
             $primary,
             $entity->extract($this->_fields)
         );
-        return $this->_repository->exists($conditions);
+        return $target->exists($conditions);
+    }
+
+    /**
+     * Check whether or not the entity fields are nullable and null.
+     *
+     * @param \Cake\ORM\EntityInterface $entity The entity to check.
+     * @param \Cake\ORM\Table $source The table to use schema from.
+     * @return bool
+     */
+    protected function _fieldsAreNull($entity, $source)
+    {
+        $nulls = 0;
+        $schema = $source->schema();
+        foreach ($this->_fields as $field) {
+            if ($schema->column($field) && $schema->isNullable($field) && $entity->get($field) === null) {
+                $nulls++;
+            }
+        }
+        return $nulls === count($this->_fields);
     }
 }

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

@@ -1527,4 +1527,43 @@ class CollectionTest extends TestCase
         $unserialized = unserialize($selialized);
         $this->assertEquals($collection->toList(), $unserialized->toList());
     }
+
+    /**
+     * Tests the chunk method with exact chunks
+     *
+     * @return void
+     */
+    public function testChunk()
+    {
+        $collection = new Collection(range(1, 10));
+        $chunked = $collection->chunk(2)->toList();
+        $expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]];
+        $this->assertEquals($expected, $chunked);
+    }
+
+    /**
+     * Tests the chunk method with overflowing chunk size
+     *
+     * @return void
+     */
+    public function testChunkOverflow()
+    {
+        $collection = new Collection(range(1, 11));
+        $chunked = $collection->chunk(2)->toList();
+        $expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]];
+        $this->assertEquals($expected, $chunked);
+    }
+
+    /**
+     * Tests the chunk method with non-scalar items
+     *
+     * @return void
+     */
+    public function testChunkNested()
+    {
+        $collection = new Collection([1, 2, 3, [4, 5], 6, [7, [8, 9], 10], 11]);
+        $chunked = $collection->chunk(2)->toList();
+        $expected = [[1, 2], [3, [4, 5]], [6, [7, [8, 9], 10]], [11]];
+        $this->assertEquals($expected, $chunked);
+    }
 }

+ 23 - 0
tests/TestCase/ORM/RulesCheckerIntegrationTest.php

@@ -461,6 +461,29 @@ class RulesCheckerIntegrationTest extends TestCase
     }
 
     /**
+     * Tests existsIn with invalid associations
+     *
+     * @group save
+     * @expectedException RuntimeException
+     * @expectedExceptionMessage ExistsIn rule for 'author_id' is invalid. The 'NotValid' association is not defined.
+     * @return void
+     */
+    public function testExistsInInvalidAssociation()
+    {
+        $entity = new Entity([
+            'title' => 'An Article',
+            'author_id' => 500
+        ]);
+
+        $table = TableRegistry::get('Articles');
+        $table->belongsTo('Authors');
+        $rules = $table->rulesChecker();
+        $rules->add($rules->existsIn('author_id', 'NotValid'));
+
+        $table->save($entity);
+    }
+
+    /**
      * Tests the checkRules save option
      *
      * @group save