Browse Source

Merge remote-tracking branch 'origin/master' into 3.1

Jose Lorenzo Rodriguez 10 years ago
parent
commit
57bd9c2ffb

+ 0 - 1
composer.json

@@ -28,7 +28,6 @@
     },
     "require-dev": {
         "phpunit/phpunit": "*",
-        "phpunit/phpunit-mock-objects": "2.3.1",
         "cakephp/cakephp-codesniffer": "dev-master"
     },
     "autoload": {

+ 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;

+ 10 - 6
src/Datasource/RulesAwareTrait.php

@@ -80,10 +80,13 @@ trait RulesAwareTrait
     }
 
     /**
-     * Returns the rule checker for this table. A rules checker object is used to
-     * test an entity for validity on rules that may involve complex logic or data that
-     * needs to be fetched from the database or other sources.
+     * Returns the RulesChecker for this instance.
      *
+     * A RulesChecker object is used to test an entity for validity
+     * on rules that may involve complex logic or data that
+     * needs to be fetched from relevant datasources.
+     *
+     * @see \Cake\Datasource\RulesChecker
      * @return \Cake\Datasource\RulesChecker
      */
     public function rulesChecker()
@@ -98,9 +101,10 @@ trait RulesAwareTrait
     }
 
     /**
-     * Returns rules checker object after modifying the one that was passed. Subclasses
-     * can override this method in order to initialize the rules to be applied to
-     * entities saved by this table.
+     * Returns a RulesChecker object after modifying the one that was supplied.
+     *
+     * Subclasses should override this method in order to initialize the rules to be applied to
+     * entities saved by this instance.
      *
      * @param \Cake\Datasource\RulesChecker $rules The rules object to be modified.
      * @return \Cake\Datasource\RulesChecker

+ 81 - 15
src/ORM/Table.php

@@ -626,7 +626,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * Returns the behavior registry for this table.
      *
-     * @return \Cake\ORM\BehaviorRegistry
+     * @return \Cake\ORM\BehaviorRegistry The BehaviorRegistry instance.
      */
     public function behaviors()
     {
@@ -637,7 +637,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * Check if a behavior with the given alias has been loaded.
      *
      * @param string $name The behavior alias to check.
-     * @return bool
+     * @return bool Whether or not the behavior exists.
      */
     public function hasBehavior($name)
     {
@@ -647,8 +647,8 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * Returns an association object configured for the specified alias if any
      *
-     * @param string $name the alias used for the association
-     * @return \Cake\ORM\Association
+     * @param string $name the alias used for the association.
+     * @return \Cake\ORM\Association|null Either the association or null.
      */
     public function association($name)
     {
@@ -658,7 +658,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * Get the associations collection for this table.
      *
-     * @return \Cake\ORM\AssociationCollection
+     * @return \Cake\ORM\AssociationCollection The collection of association objects.
      */
     public function associations()
     {
@@ -867,6 +867,11 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * {@inheritDoc}
      *
+     * ### Model.beforeFind event
+     *
+     * Each find() will trigger a `Model.beforeFind` event for all attached
+     * listeners. Any listener can set a valid result set using $query
+     *
      * By default, `$options` will recognize the following keys:
      *
      * - fields
@@ -879,7 +884,40 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * - having
      * - contain
      * - join
-     * @return \Cake\ORM\Query
+     *
+     * ### Usage
+     *
+     * Using the options array:
+     *
+     * ```
+     * $query = $articles->find('all', [
+     *   'conditions' => ['published' => 1],
+     *   'limit' => 10,
+     *   'contain' => ['Users', 'Comments']
+     * ]);
+     * ```
+     *
+     * Using the builder interface:
+     *
+     * ```
+     * $query = $articles->find()
+     *   ->where(['published' => 1])
+     *   ->limit(10)
+     *   ->contain(['Users', 'Comments']);
+     * ```
+     *
+     * ### Calling finders
+     *
+     * The find() method is the entry point for custom finder methods.
+     * You can invoke a finder by specifying the type:
+     *
+     * ```
+     * $query = $articles->find('published');
+     * ```
+     *
+     * Would invoke the `findPublished` method.
+     *
+     * @return \Cake\ORM\Query The query builder
      */
     public function find($type = 'all', $options = [])
     {
@@ -889,11 +927,14 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     }
 
     /**
-     * Returns the query as passed
+     * Returns the query as passed.
+     *
+     * By default findAll() applies no conditions, you
+     * can override this method in subclasses to modify how `find('all')` works.
      *
      * @param \Cake\ORM\Query $query The query to find with
      * @param array $options The options to use for the find
-     * @return \Cake\ORM\Query
+     * @return \Cake\ORM\Query The query builder
      */
     public function findAll(Query $query, array $options)
     {
@@ -955,7 +996,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      *
      * @param \Cake\ORM\Query $query The query to find with
      * @param array $options The options for the find
-     * @return \Cake\ORM\Query
+     * @return \Cake\ORM\Query The query builder
      */
     public function findList(Query $query, array $options)
     {
@@ -1022,7 +1063,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      *
      * @param \Cake\ORM\Query $query The query to find with
      * @param array $options The options to find with
-     * @return \Cake\ORM\Query
+     * @return \Cake\ORM\Query The query builder
      */
     public function findThreaded(Query $query, array $options)
     {
@@ -1085,6 +1126,14 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     /**
      * {@inheritDoc}
      *
+     * ### Usage
+     *
+     * Get an article and some relationships:
+     *
+     * ```
+     * $article = $articles->get(1, ['contain' => ['Users', 'Comments']]);
+     * ```
+     *
      * @throws \Cake\Datasource\Exception\InvalidPrimaryKeyException When $primaryKey has an
      *      incorrect number of elements.
      */
@@ -1134,9 +1183,13 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * Finds an existing record or creates a new one.
      *
      * Using the attributes defined in $search a find() will be done to locate
-     * an existing record. If that record exists it will be returned. If it does
-     * not exist, a new entity will be created with the $search properties, and
-     * the $defaults. When a new entity is created, it will be saved.
+     * an existing record. If records matches the conditions, the first record
+     * will be returned.
+     *
+     * If no record can be found, a new entity will be created
+     * with the $search properties. If a callback is provided, it will be
+     * called allowing you to define additional default values. The new
+     * entity will be saved and returned.
      *
      * @param array $search The criteria to find existing records by.
      * @param callable|null $callback A callback that will be invoked for newly
@@ -1214,7 +1267,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      *
      * ### Options
      *
-     * The options array can receive the following keys:
+     * The options array accepts the following keys:
      *
      * - atomic: Whether to execute the save and callbacks inside a database
      *   transaction (default: true)
@@ -1237,7 +1290,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * - Model.beforeRules: Will be triggered right before any rule checking is done
      *   for the passed entity if the `checkRules` key in $options is not set to false.
      *   Listeners will receive as arguments the entity, options array and the operation type.
-     *   If the event is stopped the checking result will be set to the result of the event itself.
+     *   If the event is stopped the rules check result will be set to the result of the event itself.
      * - Model.afterRules: Will be triggered right after the `checkRules()` method is
      *   called for the entity. Listeners will receive as arguments the entity,
      *   options array, the result of checking the rules and the operation type.
@@ -2079,6 +2132,19 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * Override this method if you need to add non-conventional event listeners.
      * Or if you want you table to listen to non-standard events.
      *
+     * The conventional method map is:
+     *
+     * - Model.beforeMarshal => beforeMarshal
+     * - Model.beforeFind => beforeFind
+     * - Model.beforeSave => beforeSave
+     * - Model.afterSave => afterSave
+     * - Model.afterSaveCommit => afterSaveCommit
+     * - Model.beforeDelete => beforeDelete
+     * - Model.afterDelete => afterDelete
+     * - Model.afterDeleteCommit => afterDeleteCommit
+     * - Model.beforeRules => beforeRules
+     * - Model.afterRules => afterRules
+     *
      * @return array
      */
     public function implementedEvents()

+ 22 - 1
src/Routing/Filter/AssetFilter.php

@@ -38,6 +38,27 @@ class AssetFilter extends DispatcherFilter
     protected $_priority = 9;
 
     /**
+     * The amount of time to cache the asset.
+     *
+     * @var string
+     */
+    protected $_cacheTime = '+1 day';
+
+    /**
+     *
+     * Constructor.
+     *
+     * @param array $config Array of config.
+     */
+    public function __construct($config = [])
+    {
+        if (!empty($config['cacheTime'])) {
+            $this->_cacheTime = $config['cacheTime'];
+        }
+        parent::__construct($config);
+    }
+
+    /**
      * Checks if a requested asset exists and sends it to the browser
      *
      * @param \Cake\Event\Event $event containing the request and response object
@@ -119,7 +140,7 @@ class AssetFilter extends DispatcherFilter
         if (!$compressionEnabled) {
             $response->header('Content-Length', filesize($assetFile));
         }
-        $response->cache(filemtime($assetFile));
+        $response->cache(filemtime($assetFile), $this->_cacheTime);
         $response->sendHeaders();
         readfile($assetFile);
         if ($compressionEnabled) {

+ 2 - 2
src/Utility/Inflector.php

@@ -511,7 +511,7 @@ class Inflector
             static::$_cache['irregular']['pluralize'] = '(?:' . implode('|', array_keys(static::$_irregular)) . ')';
         }
 
-        if (preg_match('/(.*(?:\\b|_))(' . static::$_cache['irregular']['pluralize'] . ')$/i', $word, $regs)) {
+        if (preg_match('/(.*?(?:\\b|_))(' . static::$_cache['irregular']['pluralize'] . ')$/i', $word, $regs)) {
             static::$_cache['pluralize'][$word] = $regs[1] . substr($regs[2], 0, 1) .
                 substr(static::$_irregular[strtolower($regs[2])], 1);
             return static::$_cache['pluralize'][$word];
@@ -551,7 +551,7 @@ class Inflector
             static::$_cache['irregular']['singular'] = '(?:' . implode('|', static::$_irregular) . ')';
         }
 
-        if (preg_match('/(.*(?:\\b|_))(' . static::$_cache['irregular']['singular'] . ')$/i', $word, $regs)) {
+        if (preg_match('/(.*?(?:\\b|_))(' . static::$_cache['irregular']['singular'] . ')$/i', $word, $regs)) {
             static::$_cache['singularize'][$word] = $regs[1] . substr($regs[2], 0, 1) .
                 substr(array_search(strtolower($regs[2]), static::$_irregular), 1);
             return static::$_cache['singularize'][$word];

+ 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());
+    }
 }

+ 2 - 1
tests/TestCase/Database/Driver/SqlserverTest.php

@@ -35,7 +35,7 @@ class SqlserverTest extends TestCase
     public function setUp()
     {
         parent::setUp();
-        $this->skipUnless(defined('PDO::SQLSRV_ENCODING_UTF8'), 'SQL Server extension not present');
+        $this->missingExtension = !defined('PDO::SQLSRV_ENCODING_UTF8');
     }
 
     /**
@@ -45,6 +45,7 @@ class SqlserverTest extends TestCase
      */
     public function testConnectionConfigCustom()
     {
+        $this->skipIf($this->missingExtension, 'pdo_sqlsrv is not installed.');
         $config = [
             'persistent' => false,
             'host' => 'foo',

+ 0 - 12
tests/TestCase/Database/Schema/SqlserverSchemaTest.php

@@ -26,18 +26,6 @@ use Cake\TestSuite\TestCase;
  */
 class SqlserverSchemaTest extends TestCase
 {
-
-    /**
-     * Set up
-     *
-     * @return void
-     */
-    public function setUp()
-    {
-        parent::setUp();
-        $this->skipUnless(defined('PDO::SQLSRV_ENCODING_UTF8'), 'SQL Server extension not present');
-    }
-
     /**
      * Helper method for skipping tests that need a real connection.
      *

+ 44 - 0
tests/TestCase/Utility/InflectorTest.php

@@ -180,6 +180,28 @@ class InflectorTest extends TestCase
     }
 
     /**
+     * Test that overlapping irregulars don't collide.
+     *
+     * @return void
+     */
+    public function testSingularizeMultiWordIrregular()
+    {
+        Inflector::rules('irregular', [
+            'pregunta_frecuente' => 'preguntas_frecuentes',
+            'categoria_pregunta_frecuente' => 'categorias_preguntas_frecuentes',
+        ]);
+        $this->assertEquals('pregunta_frecuente', Inflector::singularize('preguntas_frecuentes'));
+        $this->assertEquals(
+            'categoria_pregunta_frecuente',
+            Inflector::singularize('categorias_preguntas_frecuentes')
+        );
+        $this->assertEquals(
+            'faq_categoria_pregunta_frecuente',
+            Inflector::singularize('faq_categorias_preguntas_frecuentes')
+        );
+    }
+
+    /**
      * testInflectingPlurals method
      *
      * @return void
@@ -259,6 +281,28 @@ class InflectorTest extends TestCase
     }
 
     /**
+     * Test that overlapping irregulars don't collide.
+     *
+     * @return void
+     */
+    public function testPluralizeMultiWordIrregular()
+    {
+        Inflector::rules('irregular', [
+            'pregunta_frecuente' => 'preguntas_frecuentes',
+            'categoria_pregunta_frecuente' => 'categorias_preguntas_frecuentes',
+        ]);
+        $this->assertEquals('preguntas_frecuentes', Inflector::pluralize('pregunta_frecuente'));
+        $this->assertEquals(
+            'categorias_preguntas_frecuentes',
+            Inflector::pluralize('categoria_pregunta_frecuente')
+        );
+        $this->assertEquals(
+            'faq_categorias_preguntas_frecuentes',
+            Inflector::pluralize('faq_categoria_pregunta_frecuente')
+        );
+    }
+
+    /**
      * testInflectingMultiWordIrregulars
      *
      * @return void

+ 25 - 0
tests/TestCase/View/Helper/FormHelperTest.php

@@ -5830,6 +5830,28 @@ class FormHelperTest extends TestCase
     }
 
     /**
+     * Test minYear being prior to the unix epoch
+     *
+     * @return void
+     */
+    public function testInputDatetimePreEpoch()
+    {
+        $start = date('Y') - 80;
+        $end = date('Y') - 18;
+        $result = $this->Form->input('birth_year', [
+            'type' => 'date',
+            'label' => 'Birth Year',
+            'minYear' => $start,
+            'maxYear' => $end,
+            'month' => false,
+            'day' => false,
+        ]);
+        $this->assertContains('value="' . $start . '">' . $start, $result);
+        $this->assertContains('value="' . $end . '" selected="selected">' . $end, $result);
+        $this->assertNotContains('value="00">00', $result);
+    }
+
+    /**
      * testYearAutoExpandRange method
      *
      * @return void
@@ -5922,6 +5944,9 @@ class FormHelperTest extends TestCase
             'default' => true
         ]);
         $this->assertContains('value="2008" selected="selected"', $result);
+        $this->assertContains('value="2006"', $result);
+        $this->assertNotContains('value="2005"', $result);
+        $this->assertNotContains('value="2009"', $result);
     }
 
     /**