Browse Source

Merge branch 'master' into 3.next

Mark Story 7 years ago
parent
commit
439533cc2e

+ 9 - 0
src/Command/HelpCommand.php

@@ -86,6 +86,9 @@ class HelpCommand extends Command implements CommandCollectionAwareInterface
     {
         $invert = [];
         foreach ($commands as $name => $class) {
+            if (is_object($class)) {
+                $class = get_class($class);
+            }
             if (!isset($invert[$class])) {
                 $invert[$class] = [];
             }
@@ -93,6 +96,9 @@ class HelpCommand extends Command implements CommandCollectionAwareInterface
         }
 
         foreach ($commands as $name => $class) {
+            if (is_object($class)) {
+                $class = get_class($class);
+            }
             if (count($invert[$class]) == 1) {
                 $io->out('- ' . $name);
             }
@@ -125,6 +131,9 @@ class HelpCommand extends Command implements CommandCollectionAwareInterface
     {
         $shells = new SimpleXMLElement('<shells></shells>');
         foreach ($commands as $name => $class) {
+            if (is_object($class)) {
+                $class = get_class($class);
+            }
             $shell = $shells->addChild('shell');
             $shell->addAttribute('name', $name);
             $shell->addAttribute('call_as', $name);

+ 1 - 1
src/Database/Statement/PDOStatement.php

@@ -126,7 +126,7 @@ class PDOStatement extends StatementDecorator
             return $this->_statement->fetchAll(PDO::FETCH_ASSOC);
         }
         if ($type === static::FETCH_TYPE_OBJ) {
-            return $this->_statement->fetch(PDO::FETCH_OBJ);
+            return $this->_statement->fetchAll(PDO::FETCH_OBJ);
         }
 
         return $this->_statement->fetchAll($type);

+ 22 - 6
src/Datasource/EntityTrait.php

@@ -805,10 +805,10 @@ trait EntityTrait
      *
      * @param string $property the field to set or check status for
      * @param bool $isDirty true means the property was changed, false means
-     * it was not changed
+     * it was not changed. Defaults to true.
      * @return $this
      */
-    public function setDirty($property, $isDirty)
+    public function setDirty($property, $isDirty = true)
     {
         if ($isDirty === false) {
             unset($this->_dirty[$property]);
@@ -934,7 +934,7 @@ trait EntityTrait
      *
      * ```
      * // Sets the error messages for multiple fields at once
-     * $entity->setErrors(['salary' => ['message'], 'name' => ['another message']);
+     * $entity->setErrors(['salary' => ['message'], 'name' => ['another message']]);
      * ```
      *
      * @param array $fields The array of errors to set.
@@ -943,11 +943,27 @@ trait EntityTrait
      */
     public function setErrors(array $fields, $overwrite = false)
     {
+        if ($overwrite) {
+            foreach ($fields as $f => $error) {
+                $this->_errors[$f] = (array)$error;
+            }
+
+            return $this;
+        }
+
         foreach ($fields as $f => $error) {
             $this->_errors += [$f => []];
-            $this->_errors[$f] = $overwrite ?
-                (array)$error :
-                array_merge($this->_errors[$f], (array)$error);
+
+            // String messages are appended to the list,
+            // while more complex error structures need their
+            // keys perserved for nested validator.
+            if (is_string($error)) {
+                $this->_errors[$f][] = $error;
+            } else {
+                foreach ($error as $k => $v) {
+                    $this->_errors[$f][$k] = $v;
+                }
+            }
         }
 
         return $this;

+ 28 - 11
src/Routing/RouteBuilder.php

@@ -602,28 +602,45 @@ class RouteBuilder
      * the current RouteBuilder instance.
      *
      * @param string $name The plugin name
-     * @param string $file The routes file to load. Defaults to `routes.php`
+     * @param string $file The routes file to load. Defaults to `routes.php`. This parameter
+     *   is deprecated and will be removed in 4.0
      * @return void
      * @throws \Cake\Core\Exception\MissingPluginException When the plugin has not been loaded.
      * @throws \InvalidArgumentException When the plugin does not have a routes file.
      */
     public function loadPlugin($name, $file = 'routes.php')
     {
-        if (!Plugin::loaded($name)) {
+        $plugins = Plugin::getCollection();
+        if (!$plugins->has($name)) {
             throw new MissingPluginException(['plugin' => $name]);
         }
+        $plugin = $plugins->get($name);
 
-        $path = Plugin::configPath($name) . DIRECTORY_SEPARATOR . $file;
-        if (!file_exists($path)) {
-            throw new InvalidArgumentException(sprintf(
-                'Cannot load routes for the plugin named %s. The %s file does not exist.',
-                $name,
-                $path
-            ));
+        // @deprecated This block should be removed in 4.0
+        if ($file !== 'routes.php') {
+            deprecationWarning(
+                'Loading plugin routes now uses the routes() hook method on the plugin class. ' .
+                'Loading non-standard files will be removed in 4.0'
+            );
+
+            $path = $plugin->getConfigPath() . DIRECTORY_SEPARATOR . $file;
+            if (!file_exists($path)) {
+                throw new InvalidArgumentException(sprintf(
+                    'Cannot load routes for the plugin named %s. The %s file does not exist.',
+                    $name,
+                    $path
+                ));
+            }
+
+            $routes = $this;
+            include $path;
+
+            return;
         }
+        $plugin->routes($this);
 
-        $routes = $this;
-        include $path;
+        // Disable the routes hook to prevent duplicate route issues.
+        $plugin->disable('routes');
     }
 
     /**

+ 1 - 1
src/View/View.php

@@ -57,7 +57,7 @@ use RuntimeException;
  * `plugins/SuperHot/Template/Posts/index.ctp`. If a theme template
  * is not found for the current action the default app template file is used.
  *
- * @property \Cake\View\Helper\BreadCrumbsHelper $BreadCrumbs
+ * @property \Cake\View\Helper\BreadcrumbsHelper $Breadcrumbs
  * @property \Cake\View\Helper\FlashHelper $Flash
  * @property \Cake\View\Helper\FormHelper $Form
  * @property \Cake\View\Helper\HtmlHelper $Html

+ 1 - 0
tests/TestCase/Command/HelpCommandTest.php

@@ -76,6 +76,7 @@ class HelpCommandTest extends ConsoleIntegrationTestCase
         $this->assertOutputContains('- test_plugin.sample', 'Long plugin name');
         $this->assertOutputContains('- routes', 'core shell');
         $this->assertOutputContains('- example', 'short plugin name');
+        $this->assertOutputContains('- abort', 'command object');
         $this->assertOutputContains('To run a command', 'more info present');
         $this->assertOutputContains('To get help', 'more info present');
     }

+ 1 - 1
tests/TestCase/Database/QueryTest.php

@@ -3660,7 +3660,7 @@ class QueryTest extends TestCase
         $result = $query
             ->select(['d' => $query->func()->now('date')])
             ->execute();
-        $this->assertEquals([['d' => date('Y-m-d')]], $result->fetchAll('assoc'));
+        $this->assertEquals([(object)['d' => date('Y-m-d')]], $result->fetchAll('obj'));
 
         $query = new Query($this->connection);
         $result = $query

+ 11 - 1
tests/TestCase/ORM/EntityTest.php

@@ -812,7 +812,7 @@ class EntityTest extends TestCase
         ], ['markClean' => true]);
 
         $this->assertFalse($entity->isDirty());
-        $this->assertSame($entity, $entity->setDirty('title', true));
+        $this->assertSame($entity, $entity->setDirty('title'));
         $this->assertSame($entity, $entity->setDirty('id', false));
 
         $entity->setErrors(['title' => ['badness']]);
@@ -1228,6 +1228,16 @@ class EntityTest extends TestCase
         ];
         $result = $entity->getErrors();
         $this->assertEquals($expected, $result);
+
+        $indexedErrors = [2 => ['foo' => 'bar']];
+        $entity = new Entity();
+        $entity->setError('indexes', $indexedErrors);
+
+        $expectedIndexed = [
+            'indexes' => ['2' => ['foo' => 'bar']]
+        ];
+        $result = $entity->getErrors();
+        $this->assertEquals($expectedIndexed, $result);
     }
 
     /**

+ 10 - 5
tests/TestCase/Routing/RouteBuilderTest.php

@@ -1206,11 +1206,13 @@ class RouteBuilderTest extends TestCase
      */
     public function testLoadPluginBadFile()
     {
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('Cannot load routes for the plugin named TestPlugin.');
-        Plugin::load('TestPlugin');
-        $routes = new RouteBuilder($this->collection, '/');
-        $routes->loadPlugin('TestPlugin', 'nope.php');
+        $this->deprecated(function () {
+            $this->expectException(\InvalidArgumentException::class);
+            $this->expectExceptionMessage('Cannot load routes for the plugin named TestPlugin.');
+            Plugin::load('TestPlugin');
+            $routes = new RouteBuilder($this->collection, '/');
+            $routes->loadPlugin('TestPlugin', 'nope.php');
+        });
     }
 
     /**
@@ -1225,6 +1227,9 @@ class RouteBuilderTest extends TestCase
         $routes->loadPlugin('TestPlugin');
         $this->assertCount(1, $this->collection->routes());
         $this->assertNotEmpty($this->collection->parse('/test_plugin', 'GET'));
+
+        $plugin = Plugin::getCollection()->get('TestPlugin');
+        $this->assertFalse($plugin->isEnabled('routes'), 'Hook should be disabled preventing duplicate routes');
     }
 
     /**

+ 19 - 0
tests/TestCase/TestSuite/IntegrationTestTraitTest.php

@@ -15,6 +15,7 @@
 namespace Cake\Test\TestCase\TestSuite;
 
 use Cake\Core\Configure;
+use Cake\Core\Plugin;
 use Cake\Event\EventManager;
 use Cake\Http\Response;
 use Cake\Routing\DispatcherFactory;
@@ -53,6 +54,7 @@ class IntegrationTestTraitTest extends IntegrationTestCase
         Router::$initialized = true;
 
         $this->useHttpServer(true);
+        $this->configApplication(Configure::read('App.namespace') . '\Application', null);
         DispatcherFactory::clear();
     }
 
@@ -282,6 +284,23 @@ class IntegrationTestTraitTest extends IntegrationTestCase
      *
      * @return void
      */
+    public function testGetUsingApplicationWithPluginRoutes()
+    {
+        // first clean routes to have Router::$initailized === false
+        Router::reload();
+        Plugin::unload();
+
+        $this->configApplication(Configure::read('App.namespace') . '\ApplicationWithPluginRoutes', null);
+
+        $this->get('/test_plugin');
+        $this->assertResponseOk();
+    }
+
+    /**
+     * Test sending get request and using default `test_app/config/routes.php`.
+     *
+     * @return void
+     */
     public function testGetUsingApplicationWithDefaultRoutes()
     {
         // first clean routes to have Router::$initailized === false

+ 5 - 1
tests/test_app/Plugin/TestPlugin/config/routes.php

@@ -4,5 +4,9 @@ use Cake\Core\Configure;
 Configure::write('PluginTest.test_plugin.routes', 'loaded plugin routes');
 
 if (isset($routes)) {
-    $routes->get('/test_plugin', ['controller' => 'TestPlugin', 'plugin' => 'TestPlugin', 'action' => 'index']);
+    $routes->get(
+        '/test_plugin',
+        ['controller' => 'TestPlugin', 'plugin' => 'TestPlugin', 'action' => 'index'],
+        'test_plugin:index'
+    );
 }

+ 1 - 1
tests/test_app/TestApp/Application.php

@@ -29,7 +29,7 @@ class Application extends BaseApplication
     public function console($commands)
     {
         return $commands
-            ->add('abort_command', AbortCommand::class)
+            ->add('abort_command', new AbortCommand())
             ->addMany($commands->autoDiscover());
     }
 

+ 48 - 0
tests/test_app/TestApp/ApplicationWithPluginRoutes.php

@@ -0,0 +1,48 @@
+<?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.6
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace TestApp;
+
+use Cake\Http\BaseApplication;
+use Cake\Routing\Middleware\RoutingMiddleware;
+
+class ApplicationWithPluginRoutes extends BaseApplication
+{
+    public function bootstrap()
+    {
+        parent::bootstrap();
+        $this->addPlugin('TestPlugin');
+    }
+
+    public function middleware($middleware)
+    {
+        $middleware->add(new RoutingMiddleware($this));
+
+        return $middleware;
+    }
+
+    /**
+     * Routes hook, used for testing with RoutingMiddleware.
+     *
+     * @param \Cake\Routing\RouteBuilder $routes
+     * @return void
+     */
+    public function routes($routes)
+    {
+        $routes->scope('/app', function ($routes) {
+            $routes->connect('/articles', ['controller' => 'Articles']);
+        });
+        $routes->loadPlugin('TestPlugin');
+    }
+}