Browse Source

Merge branch '3.next' of github.com:cakephp/cakephp into 3.next

Mark Story 8 years ago
parent
commit
d950ce0023

+ 20 - 8
src/Collection/Collection.php

@@ -73,17 +73,29 @@ class Collection extends IteratorIterator implements CollectionInterface, Serial
     }
 
     /**
-     * Throws an exception.
+     * {@inheritDoc}
      *
-     * Issuing a count on a Collection can have many side effects, some making the
-     * Collection unusable after the count operation.
-     *
-     * @return void
-     * @throws \LogicException
+     * @return int
      */
     public function count()
     {
-        throw new LogicException('You cannot issue a count on a Collection.');
+        $traversable = $this->optimizeUnwrap();
+
+        if (is_array($traversable)) {
+            return count($traversable);
+        }
+
+        return iterator_count($traversable);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return int
+     */
+    public function countKeys()
+    {
+        return count($this->toArray());
     }
 
     /**
@@ -95,7 +107,7 @@ class Collection extends IteratorIterator implements CollectionInterface, Serial
     public function __debugInfo()
     {
         return [
-            'count' => iterator_count($this),
+            'count' => $this->count(),
         ];
     }
 }

+ 49 - 0
src/Collection/CollectionInterface.php

@@ -1049,4 +1049,53 @@ interface CollectionInterface extends Iterator, JsonSerializable
      * @return \Cake\Collection\CollectionInterface
      */
     public function transpose();
+
+    /**
+     * Returns the amount of elements in the collection.
+     *
+     * ## WARNINGS:
+     *
+     * ### Consumes all elements for NoRewindIterator collections:
+     *
+     * On certain type of collections, calling this method may render unusable afterwards.
+     * That is, you may not be able to get elements out of it, or to iterate on it anymore.
+     *
+     * Specifically any collection wrapping a Generator (a function with a yield statement)
+     * or a unbuffered database cursor will not accept any other function calls after calling
+     * `count()` on it.
+     *
+     * Create a new collection with `buffered()` method to overcome this problem.
+     *
+     * ### Can report more elements than unique keys:
+     *
+     * Any collection constructed by appending collections together, or by having internal iterators
+     * returning duplicate keys, will report a larger amount of elements using this functions than
+     * the final amount of elements when converting the collections to a keyed array. This is because
+     * duplicate keys will be collapsed into a single one in the final array, whereas this count method
+     * is only concerned by the amount of elements after converting it to a plain list.
+     *
+     * If you need the count of elements after taking the keys in consideration
+     * (the count of unique keys), you can call `countKeys()`
+     *
+     * ### Will change the current position of the iterator:
+     *
+     * Calling this method at the same time that you are iterating this collections, for example in
+     * a foreach, will result in undefined behavior. Avoid doing this.
+     *
+     *
+     * @return int
+     */
+    public function count();
+
+    /**
+     * Returns the number of unique keys in this iterator. This is, the number of
+     * elements the collection will contain after calling `toArray()`
+     *
+     * This method comes with a number of caveats. Please refer to `CollectionInterface::count()`
+     * for details.
+     *
+     * @see \Cake\Collection\CollectionInterface::count()
+     * @return int
+     */
+    public function countKeys();
 }

+ 26 - 0
src/Collection/CollectionTrait.php

@@ -812,6 +812,32 @@ trait CollectionTrait
     }
 
     /**
+     * {@inheritDoc}
+     *
+     * @return int
+     */
+    public function count()
+    {
+        $traversable = $this->optimizeUnwrap();
+
+        if (is_array($traversable)) {
+            return count($traversable);
+        }
+
+        return iterator_count($traversable);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return int
+     */
+    public function countKeys()
+    {
+        return count($this->toArray());
+    }
+
+    /**
      * Unwraps this iterator and returns the simplest
      * traversable that can be used for getting the data out
      *

+ 33 - 1
src/Console/ConsoleOptionParser.php

@@ -121,6 +121,13 @@ class ConsoleOptionParser
     protected $_subcommands = [];
 
     /**
+     * Subcommand sorting option
+     *
+     * @var bool
+     */
+    protected $_subcommandSort = true;
+
+    /**
      * Command name.
      *
      * @var string
@@ -420,6 +427,29 @@ class ConsoleOptionParser
     }
 
     /**
+     * Enables sorting of subcommands
+     *
+     * @param bool $value Whether or not to sort subcommands
+     * @return $this
+     */
+    public function enableSubcommandSort($value = true)
+    {
+        $this->_subcommandSort = (bool)$value;
+
+        return $this;
+    }
+
+    /**
+     * Checks whether or not sorting is enabled for subcommands.
+     *
+     * @return bool
+     */
+    public function isSubcommandSortEnabled()
+    {
+        return $this->_subcommandSort;
+    }
+
+    /**
      * Add an option to the option parser. Options allow you to define optional or required
      * parameters for your console application. Options are defined by the parameters they use.
      *
@@ -607,7 +637,9 @@ class ConsoleOptionParser
             $command = new ConsoleInputSubcommand($options);
         }
         $this->_subcommands[$name] = $command;
-        asort($this->_subcommands);
+        if ($this->_subcommandSort) {
+            asort($this->_subcommands);
+        }
 
         return $this;
     }

+ 1 - 0
src/Controller/Component/RequestHandlerComponent.php

@@ -332,6 +332,7 @@ class RequestHandlerComponent extends Component
 
         if ($this->ext && $isRecognized) {
             $this->renderAs($controller, $this->ext);
+            $response = $controller->response;
         } else {
             $response = $response->withCharset(Configure::read('App.encoding'));
         }

+ 3 - 0
src/Form/Form.php

@@ -223,6 +223,9 @@ class Form implements EventListenerInterface, EventDispatcherInterface, Validato
     public function validate(array $data)
     {
         $validator = $this->getValidator();
+        if (!$validator->count()) {
+            $validator = $this->validator();
+        }
         $this->_errors = $validator->errors($data);
 
         return count($this->_errors) === 0;

+ 8 - 0
src/ORM/Behavior/TimestampBehavior.php

@@ -204,6 +204,14 @@ class TimestampBehavior extends Behavior
 
         /** @var \Cake\Database\Type\DateTimeType $type */
         $type = Type::build($columnType);
+
+        if (!$type instanceof Type\DateTimeType) {
+            deprecationWarning('TimestampBehavior support for column types other than DateTimeType will be removed in 4.0.');
+            $entity->set($field, (string)$ts);
+
+            return;
+        }
+
         $class = $type->getDateTimeClassName();
 
         $entity->set($field, new $class($ts));

+ 9 - 3
src/TestSuite/IntegrationTestCase.php

@@ -22,6 +22,7 @@ if (class_exists('PHPUnit_Runner_Version', false) && !interface_exists('PHPUnit\
 
 use Cake\Core\Configure;
 use Cake\Database\Exception as DatabaseException;
+use Cake\Http\ServerRequest;
 use Cake\Http\Session;
 use Cake\Routing\Router;
 use Cake\TestSuite\Stub\TestExceptionRenderer;
@@ -658,14 +659,19 @@ abstract class IntegrationTestCase extends TestCase
      */
     protected function _url($url)
     {
-        $url = Router::url($url);
+        // re-create URL in ServerRequest's context so
+        // query strings are encoded as expected
+        $request = new ServerRequest(['url' => Router::url($url)]);
+        $url = $request->getRequestTarget();
+
         $query = '';
 
+        $path = parse_url($url, PHP_URL_PATH);
         if (strpos($url, '?') !== false) {
-            list($url, $query) = explode('?', $url, 2);
+            $query = parse_url($url, PHP_URL_QUERY);
         }
 
-        return [$url, $query];
+        return [$path, $query];
     }
 
     /**

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

@@ -1007,16 +1007,29 @@ class CollectionTest extends TestCase
     }
 
     /**
-     * Tests that issuing a count will throw an exception
+     * Tests that Count returns the number of elements
      *
+     * @dataProvider simpleProvider
      * @return void
      */
-    public function testCollectionCount()
+    public function testCollectionCount($list)
     {
-        $this->expectException(\LogicException::class);
-        $data = [1, 2, 3, 4];
-        $collection = new Collection($data);
-        $collection->count();
+        $list = (new Collection($list))->buffered();
+        $collection = new Collection($list);
+        $this->assertEquals(8, $collection->append($list)->count());
+    }
+
+    /**
+     * Tests that countKeys returns the number of unique keys
+     *
+     * @dataProvider simpleProvider
+     * @return void
+     */
+    public function testCollectionCountKeys($list)
+    {
+        $list = (new Collection($list))->buffered();
+        $collection = new Collection($list);
+        $this->assertEquals(4, $collection->append($list)->countKeys());
     }
 
     /**

+ 17 - 0
tests/TestCase/Console/ConsoleOptionParserTest.php

@@ -690,6 +690,23 @@ class ConsoleOptionParserTest extends TestCase
     }
 
     /**
+     * test addSubcommand without sorting applied.
+     */
+    public function testAddSubcommandSort()
+    {
+        $parser = new ConsoleOptionParser('test', false);
+        $this->assertEquals(true, $parser->isSubcommandSortEnabled());
+        $parser->enableSubcommandSort(false);
+        $this->assertEquals(false, $parser->isSubcommandSortEnabled());
+        $parser->addSubcommand(new ConsoleInputSubcommand('betaTest'), []);
+        $parser->addSubcommand(new ConsoleInputSubcommand('alphaTest'), []);
+        $result = $parser->subcommands();
+        $this->assertCount(2, $result);
+        $firstResult = key($result);
+        $this->assertEquals('betaTest', $firstResult);
+    }
+
+    /**
      * test removeSubcommand with an object.
      *
      * @return void

+ 20 - 0
tests/TestCase/Controller/Component/RequestHandlerComponentTest.php

@@ -46,6 +46,11 @@ class RequestHandlerComponentTest extends TestCase
     public $RequestHandler;
 
     /**
+     * @var ServerRequest
+     */
+    public $request;
+
+    /**
      * Backup of $_SERVER
      *
      * @var array
@@ -1432,4 +1437,19 @@ XML;
         $this->RequestHandler->beforeRender($event);
         $this->assertEquals('text/plain', $this->Controller->response->getType());
     }
+
+    /**
+     * tests beforeRender automatically uses renderAs when a supported extension is found
+     *
+     * @return void
+     */
+    public function testBeforeRenderAutoRenderAs()
+    {
+        $this->Controller->setRequest($this->request->withParam('_ext', 'csv'));
+        $this->RequestHandler->startup(new Event('Controller.startup', $this->Controller));
+
+        $event = new Event('Controller.beforeRender', $this->Controller);
+        $this->RequestHandler->beforeRender($event);
+        $this->assertEquals('text/csv', $this->Controller->response->getType());
+    }
 }

+ 1 - 1
tests/TestCase/Core/BasePluginTest.php

@@ -148,7 +148,7 @@ class BasePluginTest extends TestCase
     public function testGetPathSubclass()
     {
         $plugin = new TestPlugin();
-        $expected = TEST_APP . 'Plugin/TestPlugin' . DS;
+        $expected = TEST_APP . 'Plugin' . DS . 'TestPlugin' . DS;
         $this->assertSame($expected, $plugin->getPath());
         $this->assertSame($expected . 'config' . DS, $plugin->getConfigPath());
         $this->assertSame($expected . 'src' . DS, $plugin->getClassPath());

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

@@ -126,7 +126,7 @@ class SqlserverTest extends TestCase
             'settings' => ['config1' => 'value1', 'config2' => 'value2'],
         ];
         $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
-            ->setMethods(['_connect', 'connection'])
+            ->setMethods(['_connect', 'setConnection', 'getConnection'])
             ->setConstructorArgs([$config])
             ->getMock();
         $dsn = 'sqlsrv:Server=foo;Database=bar;MultipleActiveResultSets=false';
@@ -165,7 +165,7 @@ class SqlserverTest extends TestCase
         $driver->expects($this->once())->method('_connect')
             ->with($dsn, $expected);
 
-        $driver->expects($this->any())->method('connection')
+        $driver->expects($this->any())->method('getConnection')
             ->will($this->returnValue($connection));
 
         $driver->connect();

+ 16 - 0
tests/TestCase/Form/FormTest.php

@@ -19,6 +19,7 @@ use Cake\TestSuite\TestCase;
 use Cake\Validation\Validator;
 use TestApp\Form\AppForm;
 use TestApp\Form\FormSchema;
+use TestApp\Form\ValidateForm;
 
 /**
  * Form test case.
@@ -127,6 +128,21 @@ class FormTest extends TestCase
     }
 
     /**
+     * tests validate using deprecated validate() method
+     *
+     * @return void
+     */
+    public function testValidateDeprected()
+    {
+        $this->deprecated(function () {
+            $form = new ValidateForm();
+            $data = [];
+            $this->assertFalse($form->validate($data));
+            $this->assertCount(1, $form->errors());
+        });
+    }
+
+    /**
      * Test the errors methods.
      *
      * @return void

+ 27 - 0
tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php

@@ -21,6 +21,7 @@ use Cake\ORM\Behavior\TimestampBehavior;
 use Cake\ORM\Entity;
 use Cake\ORM\Table;
 use Cake\TestSuite\TestCase;
+use PHPUnit\Framework\Error\Deprecated;
 
 /**
  * Behavior test case
@@ -242,6 +243,31 @@ class TimestampBehaviorTest extends TestCase
     }
 
     /**
+     * tests using non-DateTimeType throws deprecation warning
+     *
+     * @return void
+     */
+    public function testNonDateTimeTypeDeprecated()
+    {
+        $this->expectException(Deprecated::class);
+        $this->expectExceptionMessage('TimestampBehavior support for column types other than DateTimeType will be removed in 4.0.');
+
+        $table = $this->getTable();
+        $this->Behavior = new TimestampBehavior($table, [
+            'events' => [
+                'Model.beforeSave' => [
+                    'timestamp_str' => 'always',
+                ]
+            ],
+        ]);
+
+        $entity = new Entity();
+        $event = new Event('Model.beforeSave');
+        $this->Behavior->handleEvent($event, $entity);
+        $this->assertInternalType('string', $entity->timestamp_str);
+    }
+
+    /**
      * testInvalidEventConfig
      *
      * @return void
@@ -457,6 +483,7 @@ class TimestampBehaviorTest extends TestCase
             'created' => ['type' => 'datetime'],
             'modified' => ['type' => 'timestamp'],
             'date_specialed' => ['type' => 'datetime'],
+            'timestamp_str' => ['type' => 'string'],
         ];
         $table = new Table(['schema' => $schema]);
 

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

@@ -1094,4 +1094,33 @@ class IntegrationTestCaseTest extends IntegrationTestCase
         $this->disableErrorHandlerMiddleware();
         $this->get('/foo');
     }
+
+    /**
+     * tests getting a secure action while passing a query string
+     *
+     * @return void
+     * @dataProvider methodsProvider
+     */
+    public function testSecureWithQueryString($method)
+    {
+        $this->enableSecurityToken();
+        $this->{$method}('/posts/securePost/?ids[]=1&ids[]=2');
+        $this->assertResponseOk();
+    }
+
+    /**
+     * data provider for HTTP methods
+     *
+     * @return array
+     */
+    public function methodsProvider()
+    {
+        return [
+            'GET' => ['get'],
+            'POST' => ['post'],
+            'PATCH' => ['patch'],
+            'PUT' => ['put'],
+            'DELETE' => ['delete'],
+        ];
+    }
 }

+ 3 - 7
tests/TestCase/View/Form/FormContextTest.php

@@ -259,13 +259,9 @@ class FormContextTest extends TestCase
         $this->assertEquals([], $context->error('Alias.name'));
         $this->assertEquals([], $context->error('nope.nope'));
 
-        $mock = $this->getMockBuilder('Cake\Validation\Validator')
-            ->setMethods(['errors'])
-            ->getMock();
-        $mock->expects($this->once())
-            ->method('errors')
-            ->willReturn(['key' => 'should be an array, not a string']);
-        $form->setValidator('default', $mock);
+        $validator = new Validator();
+        $validator->requirePresence('key', true, 'should be an array, not a string');
+        $form->setValidator('default', $validator);
         $form->validate([]);
         $context = new FormContext($this->request, ['entity' => $form]);
         $this->assertEquals(

+ 27 - 0
tests/test_app/TestApp/Form/ValidateForm.php

@@ -0,0 +1,27 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.6.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace TestApp\Form;
+
+use Cake\Form\Form;
+
+class ValidateForm extends Form
+{
+
+    public function validator(\Cake\Validation\Validator $validator = null)
+    {
+        return parent::validator($validator)
+            ->requirePresence('title');
+    }
+}