ソースを参照

Merge branch 'master' into 3.next

mark_story 8 年 前
コミット
13c2acd20f

+ 1 - 1
src/Console/Command.php

@@ -73,7 +73,7 @@ class Command
      * command can be calculated.
      *
      * @param string $name The name the command uses in the collection.
-     * @return $this;
+     * @return $this
      * @throws \InvalidArgumentException
      */
     public function setName($name)

+ 22 - 0
src/Console/CommandRunner.php

@@ -22,10 +22,12 @@ use Cake\Console\ConsoleIo;
 use Cake\Console\Exception\StopException;
 use Cake\Console\Shell;
 use Cake\Core\ConsoleApplicationInterface;
+use Cake\Core\HttpApplicationInterface;
 use Cake\Core\PluginApplicationInterface;
 use Cake\Event\EventDispatcherInterface;
 use Cake\Event\EventDispatcherTrait;
 use Cake\Event\EventManager;
+use Cake\Routing\Router;
 use Cake\Utility\Inflector;
 use InvalidArgumentException;
 use RuntimeException;
@@ -145,6 +147,7 @@ class CommandRunner implements EventDispatcherInterface
         }
         $this->checkCollection($commands, 'pluginConsole');
         $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);
+        $this->loadRoutes();
 
         if (empty($argv)) {
             throw new RuntimeException("Cannot run any commands. No arguments received.");
@@ -358,4 +361,23 @@ class CommandRunner implements EventDispatcherInterface
 
         return $shell;
     }
+
+    /**
+     * Ensure that the application's routes are loaded.
+     *
+     * Console commands and shells often need to generate URLs.
+     *
+     * @return void
+     */
+    protected function loadRoutes()
+    {
+        $builder = Router::createRouteBuilder('/');
+
+        if ($this->app instanceof HttpApplicationInterface) {
+            $this->app->routes($builder);
+        }
+        if ($this->app instanceof PluginApplicationInterface) {
+            $this->app->pluginRoutes($builder);
+        }
+    }
 }

+ 12 - 12
src/Controller/Controller.php

@@ -252,7 +252,7 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
         }
 
         $this->setRequest($request ?: new ServerRequest());
-        $this->setResponse($response ?: new Response());
+        $this->response = $response ?: new Response();
 
         if ($eventManager !== null) {
             $this->setEventManager($eventManager);
@@ -734,7 +734,7 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
      */
     public function setAction($action, ...$args)
     {
-        $this->setRequest($this->getRequest()->withParam('action', $action));
+        $this->setRequest($this->request->withParam('action', $action));
 
         return $this->$action(...$args);
     }
@@ -754,7 +754,7 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
             $builder->setTemplatePath($this->_viewPath());
         }
 
-        if ($this->getRequest()->getParam('bare')) {
+        if ($this->request->getParam('bare')) {
             $builder->enableAutoLayout(false);
         }
         $this->autoRender = false;
@@ -764,18 +764,18 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
             return $event->getResult();
         }
         if ($event->isStopped()) {
-            return $this->getResponse();
+            return $this->response;
         }
 
-        if ($builder->getTemplate() === null && $this->getRequest()->getParam('action')) {
-            $builder->setTemplate($this->getRequest()->getParam('action'));
+        if ($builder->getTemplate() === null && $this->request->getParam('action')) {
+            $builder->setTemplate($this->request->getParam('action'));
         }
 
         $this->View = $this->createView();
         $contents = $this->View->render($view, $layout);
         $this->setResponse($this->View->getResponse()->withStringBody($contents));
 
-        return $this->getResponse();
+        return $this->response;
     }
 
     /**
@@ -786,10 +786,10 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
     protected function _viewPath()
     {
         $viewPath = $this->name;
-        if ($this->getRequest()->getParam('prefix')) {
+        if ($this->request->getParam('prefix')) {
             $prefixes = array_map(
                 'Cake\Utility\Inflector::camelize',
-                explode('/', $this->getRequest()->getParam('prefix'))
+                explode('/', $this->request->getParam('prefix'))
             );
             $viewPath = implode(DIRECTORY_SEPARATOR, $prefixes) . DIRECTORY_SEPARATOR . $viewPath;
         }
@@ -806,14 +806,14 @@ class Controller implements EventListenerInterface, EventDispatcherInterface
      */
     public function referer($default = null, $local = false)
     {
-        if (!$this->getRequest()) {
+        if (!$this->request) {
             return Router::url($default, !$local);
         }
 
-        $referer = $this->getRequest()->referer($local);
+        $referer = $this->request->referer($local);
         if ($referer === '/' && $default && $default !== $referer) {
             $url = Router::url($default, !$local);
-            $base = $this->getRequest()->getAttribute('base');
+            $base = $this->request->getAttribute('base');
             if ($local && $base && strpos($url, $base) === 0) {
                 $url = substr($url, strlen($base));
                 if ($url[0] !== '/') {

+ 11 - 4
src/Database/SqlDialectTrait.php

@@ -46,28 +46,35 @@ trait SqlDialectTrait
             return $this->_startQuote . $identifier . $this->_endQuote;
         }
 
+        // string.string
         if (preg_match('/^[\w-]+\.[^ \*]*$/', $identifier)) {
-// string.string
             $items = explode('.', $identifier);
 
             return $this->_startQuote . implode($this->_endQuote . '.' . $this->_startQuote, $items) . $this->_endQuote;
         }
 
+        // string.*
         if (preg_match('/^[\w-]+\.\*$/', $identifier)) {
-// string.*
             return $this->_startQuote . str_replace('.*', $this->_endQuote . '.*', $identifier);
         }
 
         if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) {
-// Functions
+            // Functions
             return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')';
         }
 
         // Alias.field AS thing
-        if (preg_match('/^([\w-]+(\.[\w-]+|\(.*\))*)\s+AS\s*([\w-]+)$/i', $identifier, $matches)) {
+        if (preg_match('/^([\w-]+(\.[\w-\s]+|\(.*\))*)\s+AS\s*([\w-]+)$/i', $identifier, $matches)) {
             return $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3]);
         }
 
+        // string.string with spaces
+        if (preg_match('/^[\w-_]+\.[\w-_\s]+[\w_]*/', $identifier)) {
+            $items = explode('.', $identifier);
+
+            return $this->_startQuote . implode($this->_endQuote . '.' . $this->_startQuote, $items) . $this->_endQuote;
+        }
+
         if (preg_match('/^[\w-_\s]*[\w-_]+/', $identifier)) {
             return $this->_startQuote . $identifier . $this->_endQuote;
         }

+ 3 - 4
src/ORM/Locator/TableLocator.php

@@ -191,14 +191,13 @@ class TableLocator implements LocatorInterface
             $options += $this->_config[$alias];
         }
 
-        if (empty($options['className'])) {
-            $options['className'] = Inflector::camelize($alias);
-        }
-
         $className = $this->_getClassName($alias, $options);
         if ($className) {
             $options['className'] = $className;
         } else {
+            if (empty($options['className'])) {
+                $options['className'] = Inflector::camelize($alias);
+            }
             if (!isset($options['table']) && strpos($options['className'], '\\') === false) {
                 list(, $table) = pluginSplit($options['className']);
                 $options['table'] = Inflector::underscore($table);

+ 8 - 2
src/ORM/Table.php

@@ -1677,12 +1677,18 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     {
         $options += [
             'atomic' => true,
-            'defaults' => true
+            'defaults' => true,
         ];
 
-        return $this->_executeTransaction(function () use ($search, $callback, $options) {
+        $entity = $this->_executeTransaction(function () use ($search, $callback, $options) {
             return $this->_processFindOrCreate($search, $callback, $options);
         }, $options['atomic']);
+
+        if ($entity && $this->_transactionCommitted($options['atomic'], true)) {
+            $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
+        }
+
+        return $entity;
     }
 
     /**

+ 17 - 26
src/Routing/RouteBuilder.php

@@ -743,33 +743,24 @@ class RouteBuilder
             return $defaults;
         }
 
-        $regex = '/(?:([a-zA-Z0-9\/]*)\.)?([a-zA-Z0-9\/]*?)(?:\/)?([a-zA-Z0-9]*):{2}([a-zA-Z0-9_]*)/i';
+        $regex = '/(?:(?<plugin>[a-zA-Z0-9\/]*)\.)?(?<prefix>[a-zA-Z0-9\/]*?)' .
+            '(?:\/)?(?<controller>[a-zA-Z0-9]*):{2}(?<action>[a-zA-Z0-9_]*)/i';
+
         if (preg_match($regex, $defaults, $matches)) {
-            unset($matches[0]);
-            $matches = array_filter($matches, function ($value) {
-                return $value !== '' && $value !== '::';
-            });
-
-            // Intentionally incomplete switch
-            switch (count($matches)) {
-                case 2:
-                    return [
-                        'controller' => $matches[3],
-                        'action' => $matches[4]
-                    ];
-                case 3:
-                    return [
-                        'prefix' => strtolower($matches[2]),
-                        'controller' => $matches[3],
-                        'action' => $matches[4]
-                    ];
-                case 4:
-                    return [
-                        'plugin' => $matches[1],
-                        'prefix' => strtolower($matches[2]),
-                        'controller' => $matches[3],
-                        'action' => $matches[4]
-                    ];
+            foreach ($matches as $key => $value) {
+                // Remove numeric keys and empty values.
+                if (is_int($key) || $value === '' || $value === '::') {
+                    unset($matches[$key]);
+                }
+            }
+            $length = count($matches);
+
+            if (isset($matches['prefix'])) {
+                $matches['prefix'] = strtolower($matches['prefix']);
+            }
+
+            if ($length >= 2 || $length <= 4) {
+                return $matches;
             }
         }
         throw new RuntimeException("Could not parse `{$defaults}` route destination string.");

+ 1 - 1
src/Routing/Router.php

@@ -825,7 +825,7 @@ class Router
      * are used for CakePHP internals and should not normally be part of an output URL.
      *
      * @param \Cake\Http\ServerRequest|array $params The params array or
-     *     Cake\Network\Request object that needs to be reversed.
+     *     Cake\Http\ServerRequest object that needs to be reversed.
      * @param bool $full Set to true to include the full URL including the
      *     protocol when reversing the URL.
      * @return string The string that is the reversed result of the array

+ 3 - 1
src/TestSuite/ConsoleIntegrationTestCase.php

@@ -252,8 +252,10 @@ abstract class ConsoleIntegrationTestCase extends TestCase
     {
         if ($this->_useCommandRunner) {
             $applicationClassName = Configure::read('App.namespace') . '\Application';
+            /** @var \Cake\Http\BaseApplication $applicationClass */
+            $applicationClass = new $applicationClassName(CONFIG);
 
-            return new CommandRunner(new $applicationClassName([CONFIG]));
+            return new CommandRunner($applicationClass);
         }
 
         return new LegacyCommandRunner();

+ 6 - 1
src/Utility/Security.php

@@ -208,7 +208,12 @@ class Security
      * @param string $key Key to use as the encryption key for encrypted data.
      * @param string $operation Operation to perform, encrypt or decrypt
      * @throws \InvalidArgumentException When there are errors.
-     * @return string Encrypted/Decrypted string
+     * @return string Encrypted/Decrypted string.
+     * @deprecated 3.6.3 This method relies on functions provided by mcrypt
+     *   extension which has been deprecated in PHP 7.1 and removed in PHP 7.2.
+     *   There's no 1:1 replacement for this method.
+     *   Upgrade your code to use Security::encrypt()/Security::decrypt() with
+     *   OpenSsl engine instead.
      */
     public static function rijndael($text, $key, $operation)
     {

+ 35 - 11
tests/TestCase/Console/CommandRunnerTest.php

@@ -24,6 +24,7 @@ use Cake\Core\ConsoleApplicationInterface;
 use Cake\Event\EventList;
 use Cake\Event\EventManager;
 use Cake\Http\BaseApplication;
+use Cake\Routing\Router;
 use Cake\TestSuite\Stub\ConsoleOutput;
 use Cake\TestSuite\TestCase;
 use InvalidArgumentException;
@@ -160,7 +161,7 @@ class CommandRunnerTest extends TestCase
         $this->expectException(\RuntimeException::class);
         $this->expectExceptionMessage('Cannot run any commands. No arguments received.');
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -178,7 +179,7 @@ class CommandRunnerTest extends TestCase
         $this->expectException(\RuntimeException::class);
         $this->expectExceptionMessage('Unknown command `cake nope`. Run `cake --help` to get the list of valid commands.');
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -194,7 +195,7 @@ class CommandRunnerTest extends TestCase
     public function testRunHelpLongOption()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -216,7 +217,7 @@ class CommandRunnerTest extends TestCase
     public function testRunHelpShortOption()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -237,7 +238,7 @@ class CommandRunnerTest extends TestCase
     public function testRunNoCommand()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -260,7 +261,7 @@ class CommandRunnerTest extends TestCase
     public function testRunVersionAlias()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -278,7 +279,7 @@ class CommandRunnerTest extends TestCase
     public function testRunValidCommand()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -301,7 +302,7 @@ class CommandRunnerTest extends TestCase
     public function testRunValidCommandInflection()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -432,7 +433,7 @@ class CommandRunnerTest extends TestCase
     public function testRunTriggersBuildCommandsEvent()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap'])
+            ->setMethods(['middleware', 'bootstrap', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -454,7 +455,10 @@ class CommandRunnerTest extends TestCase
     public function testRunCallsPluginHookMethods()
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap', 'pluginBootstrap', 'pluginEvents', 'pluginConsole'])
+            ->setMethods([
+                'middleware', 'bootstrap', 'routes',
+                'pluginBootstrap', 'pluginConsole', 'pluginRoutes'
+            ])
             ->setConstructorArgs([$this->config])
             ->getMock();
 
@@ -468,6 +472,8 @@ class CommandRunnerTest extends TestCase
             ->will($this->returnCallback(function ($commands) {
                 return $commands;
             }));
+        $app->expects($this->at(3))->method('routes');
+        $app->expects($this->at(4))->method('pluginRoutes');
 
         $output = new ConsoleOutput();
         $runner = new CommandRunner($app, 'cake');
@@ -475,10 +481,28 @@ class CommandRunnerTest extends TestCase
         $this->assertContains(Configure::version(), $output->messages()[0]);
     }
 
+    /**
+     * Test that run() loads routing.
+     *
+     * @return void
+     */
+    public function testRunLoadsRoutes()
+    {
+        $app = $this->getMockBuilder(BaseApplication::class)
+            ->setMethods(['middleware', 'bootstrap'])
+            ->setConstructorArgs([TEST_APP . 'config' . DS])
+            ->getMock();
+
+        $output = new ConsoleOutput();
+        $runner = new CommandRunner($app, 'cake');
+        $runner->run(['cake', '--version'], $this->getMockIo($output));
+        $this->assertGreaterThan(2, count(Router::getRouteCollection()->routes()));
+    }
+
     protected function makeAppWithCommands($commands)
     {
         $app = $this->getMockBuilder(BaseApplication::class)
-            ->setMethods(['middleware', 'bootstrap', 'console'])
+            ->setMethods(['middleware', 'bootstrap', 'console', 'routes'])
             ->setConstructorArgs([$this->config])
             ->getMock();
         $collection = new CommandCollection($commands);

+ 12 - 0
tests/TestCase/Database/ConnectionTest.php

@@ -765,6 +765,18 @@ class ConnectionTest extends TestCase
         $expected = '"Model".*';
         $this->assertEquals($expected, $result);
 
+        $result = $connection->quoteIdentifier('Items.No_ 2');
+        $expected = '"Items"."No_ 2"';
+        $this->assertEquals($expected, $result);
+
+        $result = $connection->quoteIdentifier('Items.No_ 2 thing');
+        $expected = '"Items"."No_ 2 thing"';
+        $this->assertEquals($expected, $result);
+
+        $result = $connection->quoteIdentifier('Items.No_ 2 thing AS thing');
+        $expected = '"Items"."No_ 2 thing" AS "thing"';
+        $this->assertEquals($expected, $result);
+
         $result = $connection->quoteIdentifier('MTD()');
         $expected = 'MTD()';
         $this->assertEquals($expected, $result);

+ 5 - 1
tests/TestCase/ORM/TableTest.php

@@ -6150,6 +6150,9 @@ class TableTest extends TestCase
     public function testFindOrCreateTransactions()
     {
         $articles = $this->getTableLocator()->get('Articles');
+        $articles->getEventManager()->on('Model.afterSaveCommit', function ($event, $entity) {
+            $entity->afterSaveCommit = true;
+        });
 
         $article = $articles->findOrCreate(function ($query) {
             $this->assertInstanceOf('Cake\ORM\Query', $query);
@@ -6157,12 +6160,13 @@ class TableTest extends TestCase
             $this->assertTrue($this->connection->inTransaction());
         }, function ($article) {
             $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
-            $this->assertTrue($this->connection->inTransaction());
             $article->title = 'Success';
+            $this->assertTrue($this->connection->inTransaction());
         });
         $this->assertFalse($article->isNew());
         $this->assertNotNull($article->id);
         $this->assertEquals('Success', $article->title);
+        $this->assertTrue($article->afterSaveCommit);
     }
 
     /**

+ 47 - 0
tests/TestCase/Routing/RouteBuilderTest.php

@@ -225,6 +225,53 @@ class RouteBuilderTest extends TestCase
      *
      * @return void
      */
+    public function testConnectShortStringPrefix()
+    {
+        $routes = new RouteBuilder($this->collection, '/');
+        $routes->connect('/admin/bookmarks', 'Admin/Bookmarks::index');
+        $expected = [
+            'pass' => [],
+            'plugin' => null,
+            'prefix' => 'admin',
+            'controller' => 'Bookmarks',
+            'action' => 'index',
+            '_matchedRoute' => '/admin/bookmarks'
+        ];
+        $this->assertEquals($expected, $this->collection->parse('/admin/bookmarks'));
+
+        $url = $expected['_matchedRoute'];
+        unset($expected['_matchedRoute']);
+        $this->assertEquals($url, '/' . $this->collection->match($expected, []));
+    }
+
+    /**
+     * Test connect() with short string syntax
+     *
+     * @return void
+     */
+    public function testConnectShortStringPlugin()
+    {
+        $routes = new RouteBuilder($this->collection, '/');
+        $routes->connect('/blog/articles/view', 'Blog.Articles::view');
+        $expected = [
+            'pass' => [],
+            'plugin' => 'Blog',
+            'controller' => 'Articles',
+            'action' => 'view',
+            '_matchedRoute' => '/blog/articles/view'
+        ];
+        $this->assertEquals($expected, $this->collection->parse('/blog/articles/view'));
+
+        $url = $expected['_matchedRoute'];
+        unset($expected['_matchedRoute']);
+        $this->assertEquals($url, '/' . $this->collection->match($expected, []));
+    }
+
+    /**
+     * Test connect() with short string syntax
+     *
+     * @return void
+     */
     public function testConnectShortStringPluginPrefix()
     {
         $routes = new RouteBuilder($this->collection, '/');

+ 2 - 8
tests/test_app/TestApp/Application.php

@@ -19,16 +19,10 @@ use Cake\Routing\Middleware\RoutingMiddleware;
 
 class Application extends BaseApplication
 {
-    /**
-     * Bootstrap hook.
-     *
-     * Nerfed as this is for IntegrationTestCase testing.
-     *
-     * @return void
-     */
+
     public function bootstrap()
     {
-        // Do nothing.
+        parent::bootstrap();
     }
 
     public function middleware($middleware)

+ 1 - 1
tests/test_app/TestApp/Controller/PostsController.php

@@ -63,7 +63,7 @@ class PostsController extends AppController
     /**
      * Sets a flash message and redirects (no rendering)
      *
-     * @return \Cake\Network\Response
+     * @return \Cake\Http\Response
      */
     public function flashNoRender()
     {

+ 2 - 0
tests/test_app/config/bootstrap.php

@@ -0,0 +1,2 @@
+<?php
+// Do nothing