浏览代码

Merge branch 'master' into 3.1

Mark Story 11 年之前
父节点
当前提交
750511a5aa

+ 3 - 3
.travis.yml

@@ -30,16 +30,16 @@ matrix:
     - php: 5.4
       env: PHPCS=1 DEFAULT=0
 
-    - php: hhvm-nightly
+    - php: hhvm
       env: HHVM=1 DB=sqlite db_dsn='sqlite:///:memory:'
 
-    - php: hhvm-nightly
+    - php: hhvm
       env: HHVM=1 DB=mysql db_dsn='mysql://travis@0.0.0.0/cakephp_test'
 
   allow_failures:
     - php: 7.0
 
-    - php: hhvm-nightly
+    - php: hhvm
 
 before_script:
   - composer self-update

+ 4 - 8
Makefile

@@ -93,7 +93,7 @@ tag-release: guard-VERSION bump-version
 
 # Tasks for tagging the app skeleton and
 # creating a zipball of a fully built app skeleton.
-.PHONY: clean tag-app build-app package
+.PHONY: clean package
 
 clean:
 	rm -rf build
@@ -108,12 +108,6 @@ build/app: build
 build/cakephp: build
 	git checkout-index -a -f --prefix=build/cakephp/
 
-tag-app: guard-VERSION build/app
-	@echo "Tagging new version of application skeleton"
-	cd build/app && git tag -s $(VERSION) -m "CakePHP App $(VERSION)"
-	cd build/app && git push $(REMOTE)
-	cd build/app && git push $(REMOTE) --tags
-
 dist/cakephp-$(DASH_VERSION).zip: build/app build/cakephp composer.phar
 	mkdir -p dist
 	@echo "Installing app dependencies with composer"
@@ -130,7 +124,7 @@ dist/cakephp-$(DASH_VERSION).zip: build/app build/cakephp composer.phar
 	cd build/app && find . -not -path '*.git*' | zip ../../dist/cakephp-$(DASH_VERSION).zip -@
 
 # Easier to type alias for zip balls
-package: tag-app dist/cakephp-$(DASH_VERSION).zip
+package: dist/cakephp-$(DASH_VERSION).zip
 
 
 
@@ -181,6 +175,8 @@ tag-component-%: component-% guard-VERSION guard-GITHUB_USER
 		"sha": "$(shell git rev-parse $*)" \
 	}'
 	git checkout $(CURRENT_BRANCH) > /dev/null
+	git branch -D $*
+	git remote rm $*
 
 # Top level alias for doing a release.
 release: guard-VERSION guard-GITHUB_USER tag-release package publish components-tag

+ 17 - 0
src/Collection/CollectionInterface.php

@@ -830,4 +830,21 @@ interface CollectionInterface extends Iterator, JsonSerializable
      * @return \Cake\Collection\CollectionInterface
      */
     public function through(callable $handler);
+
+    /**
+     * Returns whether or not there are elements in this collection
+     *
+     * ### Example:
+     *
+     * ```
+     * $items [1, 2, 3];
+     * (new Collection($items))->isEmpty(); // false
+     * ```
+     * ```
+     * (new Collection([]))->isEmpty(); // true
+     * ```
+     *
+     * @return bool
+     */
+    public function isEmpty();
 }

+ 9 - 0
src/Collection/CollectionTrait.php

@@ -532,6 +532,15 @@ trait CollectionTrait
     }
 
     /**
+     * {@inheritDoc}
+     *
+     */
+    public function isEmpty()
+    {
+        return iterator_count($this->take(1)) === 0;
+    }
+
+    /**
      * Returns the closest nested iterator that can be safely traversed without
      * losing any possible transformations.
      *

+ 1 - 1
src/Controller/Component/AuthComponent.php

@@ -211,7 +211,7 @@ class AuthComponent extends Component
     /**
      * Instance of the Session object
      *
-     * @return void
+     * @var \Cake\Network\Session
      */
     public $session;
 

+ 26 - 1
src/Datasource/EntityTrait.php

@@ -526,7 +526,10 @@ trait EntityTrait
 
     /**
      * Returns an array with the requested original properties
-     * stored in this entity, indexed by property name
+     * stored in this entity, indexed by property name.
+     *
+     * Properties that are unchanged from their original value will be included in the
+     * return of this method.
      *
      * @param array $properties List of properties to be returned
      * @return array
@@ -536,6 +539,28 @@ trait EntityTrait
         $result = [];
         foreach ($properties as $property) {
             $original = $this->getOriginal($property);
+            if ($original !== null) {
+                $result[$property] = $original;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Returns an array with only the original properties
+     * stored in this entity, indexed by property name.
+     *
+     * This method will only return properties that have been modified since
+     * the entity was built. Unchanged properties will be omitted.
+     *
+     * @param array $properties List of properties to be returned
+     * @return array
+     */
+    public function extractOriginalChanged(array $properties)
+    {
+        $result = [];
+        foreach ($properties as $property) {
+            $original = $this->getOriginal($property);
             if ($original !== null && $original !== $this->get($property)) {
                 $result[$property] = $original;
             }

+ 5 - 5
src/ORM/Association/BelongsToMany.php

@@ -1002,12 +1002,12 @@ class BelongsToMany extends Association
     {
         if ($name === null) {
             if (empty($this->_junctionTableName)) {
-                $aliases = array_map('\Cake\Utility\Inflector::underscore', [
-                    $this->source()->alias(),
-                    $this->target()->alias()
+                $tablesNames = array_map('\Cake\Utility\Inflector::underscore', [
+                    $this->source()->table(),
+                    $this->target()->table()
                 ]);
-                sort($aliases);
-                $this->_junctionTableName = implode('_', $aliases);
+                sort($tablesNames);
+                $this->_junctionTableName = implode('_', $tablesNames);
             }
             return $this->_junctionTableName;
         }

+ 1 - 1
src/ORM/Association/ExternalAssociationTrait.php

@@ -57,7 +57,7 @@ trait ExternalAssociationTrait
     {
         if ($key === null) {
             if ($this->_foreignKey === null) {
-                $this->_foreignKey = $this->_modelKey($this->source()->alias());
+                $this->_foreignKey = $this->_modelKey($this->source()->table());
             }
             return $this->_foreignKey;
         }

+ 1 - 1
src/ORM/Behavior/CounterCacheBehavior.php

@@ -138,7 +138,7 @@ class CounterCacheBehavior extends Behavior
         $countConditions = $entity->extract($foreignKeys);
         $updateConditions = array_combine($primaryKeys, $countConditions);
 
-        $countOriginalConditions = $entity->extractOriginal($foreignKeys);
+        $countOriginalConditions = $entity->extractOriginalChanged($foreignKeys);
         if ($countOriginalConditions !== []) {
             $updateOriginalConditions = array_combine($primaryKeys, $countOriginalConditions);
         }

+ 22 - 1
src/TestSuite/IntegrationTestCase.php

@@ -350,11 +350,13 @@ abstract class IntegrationTestCase extends TestCase
         $session = Session::create($sessionConfig);
         $session->write($this->_session);
 
+        list ($url, $query) = $this->_url($url);
         $props = [
-            'url' => Router::url($url),
+            'url' => $url,
             'post' => $data,
             'cookies' => $this->_cookie,
             'session' => $session,
+            'query' => $query
         ];
         $env = [];
         if (isset($this->_request['headers'])) {
@@ -370,6 +372,25 @@ abstract class IntegrationTestCase extends TestCase
     }
 
     /**
+     * Creates a valid request url and parameter array more like Request::_url()
+     *
+     * @param string|array $url The URL
+     * @return array Qualified URL and the query parameters
+     */
+    protected function _url($url)
+    {
+        $url = Router::url($url);
+        $query = [];
+
+        if (strpos($url, '?') !== false) {
+            list($url, $parameters) = explode('?', $url, 2);
+            parse_str($parameters, $query);
+        }
+
+        return [$url, $query];
+    }
+
+    /**
      * Fetches a view variable by name.
      *
      * If the view variable does not exist, null will be returned.

+ 3 - 3
src/Utility/Xml.php

@@ -160,7 +160,7 @@ class Xml
      *
      * ### Options
      *
-     * - `format` If create childs ('tags') or attributes ('attribute').
+     * - `format` If create childs ('tags') or attributes ('attributes').
      * - `pretty` Returns formatted Xml when set to `true`. Defaults to `false`
      * - `version` Version of XML document. Default is 1.0.
      * - `encoding` Encoding of XML document. If null remove from XML header. Default is the some of application.
@@ -184,7 +184,7 @@ class Xml
      *
      * `<root><tag><id>1</id><value>defect</value>description</tag></root>`
      *
-     * And calling `Xml::fromArray($value, 'attribute');` Will generate:
+     * And calling `Xml::fromArray($value, 'attributes');` Will generate:
      *
      * `<root><tag id="1" value="defect">description</tag></root>`
      *
@@ -237,7 +237,7 @@ class Xml
      * @param \DOMDocument $dom Handler to DOMDocument
      * @param \DOMElement $node Handler to DOMElement (child)
      * @param array $data Array of data to append to the $node.
-     * @param string $format Either 'attribute' or 'tags'. This determines where nested keys go.
+     * @param string $format Either 'attributes' or 'tags'. This determines where nested keys go.
      * @return void
      * @throws \Cake\Utility\Exception\XmlException
      */

+ 4 - 1
src/Validation/ValidatorAwareTrait.php

@@ -84,8 +84,11 @@ trait ValidatorAwareTrait
      *   use null to get a validator.
      * @return \Cake\Validation\Validator
      */
-    public function validator($name = self::DEFAULT_VALIDATOR, Validator $validator = null)
+    public function validator($name = null, Validator $validator = null)
     {
+        if ($name === null) {
+            $name = self::DEFAULT_VALIDATOR;
+        }
         if ($validator === null && isset($this->_validators[$name])) {
             return $this->_validators[$name];
         }

+ 10 - 5
src/View/Helper/FormHelper.php

@@ -367,11 +367,16 @@ class FormHelper extends Helper
         }
         unset($options['templates']);
 
-        $url = $this->_formUrl($context, $options);
-        $action = $this->Url->build($url);
-        unset($options['url'], $options['action'], $options['idPrefix']);
+        if ($options['action'] === false || $options['url'] === false) {
+            $url = $this->request->here(false);
+            $action = null;
+        } else {
+            $url = $this->_formUrl($context, $options);
+            $action = $this->Url->build($url);
+        }
 
         $this->_lastAction($url);
+        unset($options['url'], $options['action'], $options['idPrefix']);
 
         $htmlAttributes = [];
         switch (strtolower($options['type'])) {
@@ -545,7 +550,7 @@ class FormHelper extends Helper
      */
     public function secure(array $fields = [], array $secureAttributes = [])
     {
-        if (!isset($this->request['_Token']) || empty($this->request['_Token'])) {
+        if (empty($this->request['_Token'])) {
             return;
         }
         $locked = [];
@@ -1425,7 +1430,7 @@ class FormHelper extends Helper
      *
      * @param string $fieldName Name of a field, like this "modelname.fieldname"
      * @param array|\Traversable $options Radio button options array.
-     * @param array $attributes Array of HTML attributes, and special attributes above.
+     * @param array $attributes Array of attributes.
      * @return string Completed radio widget set.
      * @link http://book.cakephp.org/3.0/en/views/helpers/form.html#creating-radio-buttons
      */

+ 4 - 0
src/View/JsonView.php

@@ -147,6 +147,10 @@ class JsonView extends View
     /**
      * Serialize view vars
      *
+     * ### Special parameters
+     * `_jsonOptions` You can set custom options for json_encode() this way,
+     *   e.g. `JSON_HEX_TAG | JSON_HEX_APOS`.
+     *
      * @param array|string $serialize The name(s) of the view variable(s) that need(s) to be serialized
      * @return string The serialized data
      */

+ 7 - 0
src/View/XmlView.php

@@ -131,6 +131,10 @@ class XmlView extends View
     /**
      * Serialize view vars.
      *
+     * ### Special parameters
+     * `_xmlOptions` You can set an array of custom options for Xml::fromArray() this way, e.g.
+     *   'format' as 'attributes' instead of 'tags'.
+     *
      * @param array|string $serialize The name(s) of the view variable(s) that need(s) to be serialized
      * @return string The serialized data
      */
@@ -154,6 +158,9 @@ class XmlView extends View
         }
 
         $options = [];
+        if (isset($this->viewVars['_xmlOptions'])) {
+            $options = $this->viewVars['_xmlOptions'];
+        }
         if (Configure::read('debug')) {
             $options['pretty'] = true;
         }

+ 0 - 1
src/basics.php

@@ -108,7 +108,6 @@ if (!function_exists('stackTrace')) {
      *
      * @param array $options Format for outputting stack trace
      * @return mixed Formatted stack trace
-     * @see Debugger::trace()
      */
     function stackTrace(array $options = [])
     {

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

@@ -1226,4 +1226,23 @@ class CollectionTest extends TestCase
         ];
         $this->assertSame($expected, $result);
     }
+
+    /**
+     * Tests the isEmpty() method
+     *
+     * @return void
+     */
+    public function testIsEmpty()
+    {
+        $collection = new Collection([1, 2, 3]);
+        $this->assertFalse($collection->isEmpty());
+
+        $collection = $collection->map(function () {
+            return null;
+        });
+        $this->assertFalse($collection->isEmpty());
+
+        $collection = $collection->filter();
+        $this->assertTrue($collection->isEmpty());
+    }
 }

+ 0 - 8
tests/TestCase/Core/StaticConfigTraitTest.php

@@ -413,14 +413,6 @@ class StaticConfigTraitTest extends TestCase
      */
     public function testParseDsnPathSetting()
     {
-        $dsn = 'file:///';
-        $expected = [
-            'className' => 'Cake\Log\Engine\FileLog',
-            'path' => '/',
-            'scheme' => 'file',
-        ];
-        $this->assertEquals($expected, TestLogStaticConfig::parseDsn($dsn));
-
         $dsn = 'file:///?path=/tmp/persistent/';
         $expected = [
             'className' => 'Cake\Log\Engine\FileLog',

+ 28 - 0
tests/TestCase/ORM/EntityTest.php

@@ -72,6 +72,34 @@ class EntityTest extends TestCase
     }
 
     /**
+     * Test extractOriginal()
+     *
+     * @return void
+     */
+    public function testExtractOriginal()
+    {
+        $entity = new Entity([
+            'id' => 1,
+            'title' => 'original',
+            'body' => 'no'
+        ], ['markNew' => true]);
+        $entity->set('body', 'updated body');
+        $result = $entity->extractOriginal(['id', 'title', 'body']);
+        $expected = [
+            'id' => 1,
+            'title' => 'original',
+            'body' => 'no'
+        ];
+        $this->assertEquals($expected, $result);
+
+        $result = $entity->extractOriginalChanged(['id', 'title', 'body']);
+        $expected = [
+            'body' => 'no',
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
      * Tests setting a single property using a setter function
      *
      * @return void

+ 12 - 0
tests/TestCase/ORM/QueryTest.php

@@ -2630,4 +2630,16 @@ class QueryTest extends TestCase
         $this->assertCount(2, $result->tags);
         $this->assertEquals(2, $result->_matchingData['tags']->id);
     }
+
+    /**
+     * Tests that isEmpty() can be called on a query
+     *
+     * @return void
+     */
+    public function testIsEmpty()
+    {
+        $table = TableRegistry::get('articles');
+        $this->assertFalse($table->find()->isEmpty());
+        $this->assertTrue($table->find()->where(['id' => -1])->isEmpty());
+    }
 }

+ 26 - 0
tests/TestCase/ORM/TableTest.php

@@ -2577,6 +2577,32 @@ class TableTest extends TestCase
     }
 
     /**
+     * Test delete with dependent records belonging to an aliased
+     * belongsToMany association.
+     *
+     * @return void
+     */
+    public function testDeleteDependentAliased()
+    {
+        $Authors = TableRegistry::get('authors');
+        $Authors->associations()->removeAll();
+        $Articles = TableRegistry::get('articles');
+        $Articles->associations()->removeAll();
+
+        $Authors->hasMany('AliasedArticles', [
+            'className' => 'articles',
+            'dependent' => true,
+            'cascadeCallbacks' => true
+        ]);
+        $Articles->belongsToMany('Tags');
+
+        $author = $Authors->get(1);
+        $result = $Authors->delete($author);
+
+        $this->assertTrue($result);
+    }
+
+    /**
      * Test that cascading associations are deleted first.
      *
      * @return void

+ 13 - 0
tests/TestCase/TestSuite/IntegrationTestCaseTest.php

@@ -73,6 +73,19 @@ class IntegrationTestCaseTest extends IntegrationTestCase
     }
 
     /**
+     * Test building a request, with query parameters
+     *
+     * @return void
+     */
+    public function testRequestBuildingQueryParameters()
+    {
+        $request = $this->_buildRequest('/tasks/view?archived=yes', 'GET', []);
+
+        $this->assertEquals('/tasks/view?archived=yes', $request->here());
+        $this->assertEquals('yes', $request->query('archived'));
+    }
+
+    /**
      * Test sending get requests.
      *
      * @return void

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

@@ -674,6 +674,29 @@ class FormHelperTest extends TestCase
     }
 
     /**
+     * Test create() with no URL (no "action" attribute for <form> tag)
+     *
+     * @return void
+     */
+    public function testCreateNoUrl()
+    {
+        $result = $this->Form->create(false, ['url' => false]);
+        $expected = [
+            'form' => [
+                'method' => 'post',
+                'accept-charset' => strtolower(Configure::read('App.encoding'))
+            ],
+            'div' => ['style' => 'display:none;'],
+            'input' => ['type' => 'hidden', 'name' => '_method', 'value' => 'POST'],
+            '/div'
+        ];
+        $this->assertHtml($expected, $result);
+
+        $result = $this->Form->create(false, ['action' => false]);
+        $this->assertHtml($expected, $result);
+    }
+
+    /**
      * test create() with a custom route
      *
      * @return void
@@ -3723,6 +3746,28 @@ class FormHelperTest extends TestCase
             '/label',
         ];
         $this->assertHtml($expected, $result);
+
+        $result = $this->Form->radio(
+            'Employee.gender',
+            [
+                ['value' => 'male', 'text' => 'Male', 'style' => 'width:20px'],
+                ['value' => 'female', 'text' => 'Female', 'style' => 'width:20px'],
+            ]
+        );
+        $expected = [
+            'input' => ['type' => 'hidden', 'name' => 'Employee[gender]', 'value' => ''],
+            ['label' => ['for' => 'employee-gender-male']],
+            ['input' => ['type' => 'radio', 'name' => 'Employee[gender]', 'value' => 'male',
+                'id' => 'employee-gender-male', 'style' => 'width:20px']],
+            'Male',
+            '/label',
+            ['label' => ['for' => 'employee-gender-female']],
+            ['input' => ['type' => 'radio', 'name' => 'Employee[gender]', 'value' => 'female',
+                'id' => 'employee-gender-female', 'style' => 'width:20px']],
+            'Female',
+            '/label',
+        ];
+        $this->assertHtml($expected, $result);
     }
 
     /**

+ 72 - 0
tests/TestCase/View/XmlViewTest.php

@@ -108,6 +108,78 @@ class XmlViewTest extends TestCase
     }
 
     /**
+     * Test that rendering with _serialize respects XML options.
+     *
+     * @return void
+     */
+    public function testRenderSerializeWithOptions()
+    {
+        $Request = new Request();
+        $Response = new Response();
+        $Controller = new Controller($Request, $Response);
+        $data = [
+            '_serialize' => ['tags'],
+            '_xmlOptions' => ['format' => 'attributes'],
+            'tags' => [
+                    'tag' => [
+                        [
+                            'id' => '1',
+                            'name' => 'defect'
+                        ],
+                        [
+                            'id' => '2',
+                            'name' => 'enhancement'
+                        ]
+                    ]
+            ]
+        ];
+        $Controller->set($data);
+        $Controller->viewClass = 'Xml';
+        $View = $Controller->createView();
+        $result = $View->render();
+
+        $expected = Xml::build(['response' => ['tags' => $data['tags']]], $data['_xmlOptions'])->asXML();
+        $this->assertSame($expected, $result);
+    }
+
+    /**
+     * Test that rendering with _serialize can work with string setting.
+     *
+     * @return void
+     */
+    public function testRenderSerializeWithString()
+    {
+        $Request = new Request();
+        $Response = new Response();
+        $Controller = new Controller($Request, $Response);
+        $data = [
+            '_serialize' => 'tags',
+            '_xmlOptions' => ['format' => 'attributes'],
+            'tags' => [
+                'tags' => [
+                    'tag' => [
+                        [
+                            'id' => '1',
+                            'name' => 'defect'
+                        ],
+                        [
+                            'id' => '2',
+                            'name' => 'enhancement'
+                        ]
+                    ]
+                ]
+            ]
+        ];
+        $Controller->set($data);
+        $Controller->viewClass = 'Xml';
+        $View = $Controller->createView();
+        $result = $View->render();
+
+        $expected = Xml::build($data['tags'], $data['_xmlOptions'])->asXML();
+        $this->assertSame($expected, $result);
+    }
+
+    /**
      * Test render with an array in _serialize
      *
      * @return void