ソースを参照

Merge branch '3.next' into 4.x

ADmad 7 年 前
コミット
545e2bdc6a

+ 5 - 0
.gitattributes

@@ -55,3 +55,8 @@ phpunit.xml.dist export-ignore
 tests/test_app export-ignore
 tests/TestCase export-ignore
 .github export-ignore
+.mailmap export-ignore
+.varci.yml export-ignore
+phpcs.xml.dist export-ignore
+phpstan.neon export-ignore
+contrib export-ignore

+ 2 - 0
phpstan.neon

@@ -21,6 +21,8 @@ parameters:
         - '#Variable \$_SESSION in isset\(\) always exists and is not nullable#'
         - '#Access to an undefined static property Cake\\Mailer\\Email::\$_dsnClassMap#'
         - '#PHPDoc tag @throws with type PHPUnit\\Exception|Throwable is not subtype of Throwable#'
+        - '#PHPDoc tag @throws with type PHPUnit\\Exception is not subtype of Throwable#'
+        - '#Call to an undefined method DOMNode::setAttribute\(\)#'
         - '#Binary operation "\+" between array|false and array results in an error#'
         - '#Static property Cake\\Chronos\\(Date|Chronos|MutableDate|MutableDateTime)::\$diffFormatter \(Cake\\Chronos\\DifferenceFormatter\) does not accept Cake\\I18n\\RelativeTimeFormatter#'
         - '#Return type \(void\) of method Cake\\Shell\\[A-Za-z]+Shell::main\(\) should be compatible with return type \(bool|int|null\) of method Cake\\Console\\Shell::main\(\)#'

+ 2 - 1
src/Cache/composer.json

@@ -23,7 +23,8 @@
     },
     "require": {
         "php": ">=7.1.0,<7.3.0",
-        "cakephp/core": "^4.0.0"
+        "cakephp/core": "^4.0.0",
+        "psr/simple-cache": "^1.0.0"
     },
     "autoload": {
         "psr-4": {

+ 38 - 24
src/Collection/CollectionTrait.php

@@ -37,13 +37,27 @@ use RecursiveIteratorIterator;
 use Traversable;
 
 /**
- * Offers a handful of method to manipulate iterators
+ * Offers a handful of methods to manipulate iterators
  */
 trait CollectionTrait
 {
     use ExtractTrait;
 
     /**
+     * Returns a new collection.
+     *
+     * Allows classes which use this trait to determine their own
+     * type of returned collection interface
+     *
+     * @param mixed ...$args Constructor arguments.
+     * @return \Cake\Collection\CollectionInterface
+     */
+    protected function newCollection(...$args)
+    {
+        return new Collection(...$args);
+    }
+
+    /**
      * @inheritDoc
      */
     public function each(callable $c): CollectionInterface
@@ -259,7 +273,7 @@ trait CollectionTrait
             $group[$callback($value)][] = $value;
         }
 
-        return new Collection($group);
+        return $this->newCollection($group);
     }
 
     /**
@@ -273,7 +287,7 @@ trait CollectionTrait
             $group[$callback($value)] = $value;
         }
 
-        return new Collection($group);
+        return $this->newCollection($group);
     }
 
     /**
@@ -293,7 +307,7 @@ trait CollectionTrait
             $mr->emit(count($values), $key);
         };
 
-        return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer));
+        return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer));
     }
 
     /**
@@ -322,7 +336,7 @@ trait CollectionTrait
         $elements = $this->toArray();
         shuffle($elements);
 
-        return new Collection($elements);
+        return $this->newCollection($elements);
     }
 
     /**
@@ -330,7 +344,7 @@ trait CollectionTrait
      */
     public function sample(int $size = 10): CollectionInterface
     {
-        return new Collection(new LimitIterator($this->shuffle(), 0, $size));
+        return $this->newCollection(new LimitIterator($this->shuffle(), 0, $size));
     }
 
     /**
@@ -338,7 +352,7 @@ trait CollectionTrait
      */
     public function take(int $size = 1, int $from = 0): CollectionInterface
     {
-        return new Collection(new LimitIterator($this, $from, $size));
+        return $this->newCollection(new LimitIterator($this, $from, $size));
     }
 
     /**
@@ -346,7 +360,7 @@ trait CollectionTrait
      */
     public function skip(int $howMany): CollectionInterface
     {
-        return new Collection(new LimitIterator($this, $howMany));
+        return $this->newCollection(new LimitIterator($this, $howMany));
     }
 
     /**
@@ -415,19 +429,19 @@ trait CollectionTrait
 
         $iterator = $this->optimizeUnwrap();
         if (is_array($iterator)) {
-            return new Collection(array_slice($iterator, $howMany * -1));
+            return $this->newCollection(array_slice($iterator, $howMany * -1));
         }
 
         if ($iterator instanceof Countable) {
             $count = count($iterator);
 
             if ($count === 0) {
-                return new Collection([]);
+                return $this->newCollection([]);
             }
 
             $iterator = new LimitIterator($iterator, max(0, $count - $howMany), $howMany);
 
-            return new Collection($iterator);
+            return $this->newCollection($iterator);
         }
 
         $generator = function ($iterator, $howMany) {
@@ -500,7 +514,7 @@ trait CollectionTrait
             }
         };
 
-        return new Collection($generator($iterator, $howMany));
+        return $this->newCollection($generator($iterator, $howMany));
     }
 
     /**
@@ -510,9 +524,9 @@ trait CollectionTrait
     {
         $list = new AppendIterator();
         $list->append($this->unwrap());
-        $list->append((new Collection($items))->unwrap());
+        $list->append($this->newCollection($items)->unwrap());
 
-        return new Collection($list);
+        return $this->newCollection($list);
     }
 
     /**
@@ -534,7 +548,7 @@ trait CollectionTrait
      */
     public function prepend($items): CollectionInterface
     {
-        return (new Collection($items))->append($this);
+        return $this->newCollection($items)->append($this);
     }
 
     /**
@@ -589,7 +603,7 @@ trait CollectionTrait
             $mapReduce->emit($result, $key);
         };
 
-        return new Collection(new MapReduce($this->unwrap(), $mapper, $reducer));
+        return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer));
     }
 
     /**
@@ -634,7 +648,7 @@ trait CollectionTrait
             $parents[$key][$nestingKey] = $children;
         };
 
-        return (new Collection(new MapReduce($this->unwrap(), $mapper, $reducer)))
+        return $this->newCollection(new MapReduce($this->unwrap(), $mapper, $reducer))
             ->map(function ($value) use (&$isObject) {
                 /** @var \ArrayIterator $value */
                 return $isObject ? $value : $value->getArrayCopy();
@@ -694,7 +708,7 @@ trait CollectionTrait
      */
     public function compile(bool $preserveKeys = true): CollectionInterface
     {
-        return new Collection($this->toArray($preserveKeys));
+        return $this->newCollection($this->toArray($preserveKeys));
     }
 
     /**
@@ -708,7 +722,7 @@ trait CollectionTrait
             }
         };
 
-        return new Collection($generator());
+        return $this->newCollection($generator());
     }
 
     /**
@@ -766,7 +780,7 @@ trait CollectionTrait
             };
         }
 
-        return new Collection(
+        return $this->newCollection(
             new RecursiveIteratorIterator(
                 new UnfoldIterator($this->unwrap(), $transformer),
                 RecursiveIteratorIterator::LEAVES_ONLY
@@ -781,7 +795,7 @@ trait CollectionTrait
     {
         $result = $handler($this);
 
-        return $result instanceof CollectionInterface ? $result : new Collection($result);
+        return $result instanceof CollectionInterface ? $result : $this->newCollection($result);
     }
 
     /**
@@ -892,7 +906,7 @@ trait CollectionTrait
     public function cartesianProduct(?callable $operation = null, ?callable $filter = null): CollectionInterface
     {
         if ($this->isEmpty()) {
-            return new Collection([]);
+            return $this->newCollection([]);
         }
 
         $collectionArrays = [];
@@ -936,7 +950,7 @@ trait CollectionTrait
             }
         }
 
-        return new Collection($result);
+        return $this->newCollection($result);
     }
 
     /**
@@ -960,7 +974,7 @@ trait CollectionTrait
             $result[] = array_column($arrayValue, $column);
         }
 
-        return new Collection($result);
+        return $this->newCollection($result);
     }
 
     /**

+ 4 - 0
src/Console/Command.php

@@ -63,6 +63,10 @@ class Command
         $this->modelFactory('Table', function ($alias) {
             return $this->getTableLocator()->get($alias);
         });
+
+        if (isset($this->modelClass)) {
+            $this->loadModel();
+        }
     }
 
     /**

+ 16 - 3
src/Datasource/ModelAwareTrait.php

@@ -76,7 +76,8 @@ trait ModelAwareTrait
      * If a repository provider does not return an object a MissingModelException will
      * be thrown.
      *
-     * @param string|null $modelClass Name of model class to load. Defaults to $this->modelClass
+     * @param string|null $modelClass Name of model class to load. Defaults to $this->modelClass.
+     *  The name can be an alias like `'Post'` or FQCN like `App\Model\Table\PostsTable::class`.
      * @param string|null $modelType The type of repository to load. Defaults to the modelType() value.
      * @return \Cake\Datasource\RepositoryInterface The model instance created.
      * @throws \Cake\Datasource\Exception\MissingModelException If the model class cannot be found.
@@ -92,7 +93,19 @@ trait ModelAwareTrait
             $modelType = $this->getModelType();
         }
 
-        [, $alias] = pluginSplit($modelClass, true);
+        $alias = null;
+        $options = [];
+        if (strpos($modelClass, '\\') === false) {
+            [, $alias] = pluginSplit($modelClass, true);
+        } else {
+            $options['className'] = $modelClass;
+            $alias = substr(
+                $modelClass,
+                strrpos($modelClass, '\\') + 1,
+                -strlen($modelType)
+            );
+            $modelClass = $alias;
+        }
 
         if (isset($this->{$alias})) {
             return $this->{$alias};
@@ -104,7 +117,7 @@ trait ModelAwareTrait
         if (!isset($factory)) {
             $factory = FactoryLocator::get($modelType);
         }
-        $this->{$alias} = $factory($modelClass);
+        $this->{$alias} = $factory($modelClass, $options);
         if (!$this->{$alias}) {
             throw new MissingModelException([$modelClass, $modelType]);
         }

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

@@ -469,7 +469,7 @@ class HasMany extends Association
                 return !in_array(null, $v, true);
             }
         )
-        ->toArray();
+        ->toList();
 
         $conditions = $foreignKeyReference;
 

+ 1 - 1
src/ORM/Table.php

@@ -1409,7 +1409,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
             throw new InvalidPrimaryKeyException(sprintf(
                 'Record not found in table "%s" with primary key [%s]',
                 $this->getTable(),
-                implode($primaryKey, ', ')
+                implode(', ', $primaryKey)
             ));
         }
         $conditions = array_combine($key, $primaryKey);

+ 24 - 2
src/Shell/Helper/TableHelper.php

@@ -44,7 +44,7 @@ class TableHelper extends Helper
         $widths = [];
         foreach ($rows as $line) {
             foreach (array_values($line) as $k => $v) {
-                $columnLength = mb_strwidth((string)$v);
+                $columnLength = $this->_cellWidth($v);
                 if ($columnLength >= ($widths[$k] ?? 0)) {
                     $widths[$k] = $columnLength;
                 }
@@ -55,6 +55,28 @@ class TableHelper extends Helper
     }
 
     /**
+     * Get the width of a cell exclusive of style tags.
+     *
+     * @param string|null $text The text to calculate a width for.
+     * @return int The width of the textual content in visible characters.
+     */
+    protected function _cellWidth(?string $text): int
+    {
+        if ($text === null) {
+            return 0;
+        }
+
+        if (strpos($text, '<') === false && strpos($text, '>') === false) {
+            return mb_strwidth($text);
+        }
+        $styles = array_keys($this->_io->styles());
+        $tags = implode('|', $styles);
+        $text = preg_replace('#</?(?:' . $tags . ')>#', '', $text);
+
+        return mb_strwidth($text);
+    }
+
+    /**
      * Output a row separator.
      *
      * @param array $widths The widths of each column to output.
@@ -86,7 +108,7 @@ class TableHelper extends Helper
 
         $out = '';
         foreach (array_values($row) as $i => $column) {
-            $pad = $widths[$i] - mb_strwidth((string)$column);
+            $pad = $widths[$i] - $this->_cellWidth($column);
             if (!empty($options['style'])) {
                 $column = $this->_addStyle($column, $options['style']);
             }

+ 4 - 4
src/TestSuite/EmailTrait.php

@@ -206,15 +206,15 @@ trait EmailTrait
     }
 
     /**
-     * Asserts an email contains expected text contents
+     * Asserts an email contains an expected text content
      *
-     * @param string $contents Contents
-     * @param string $message Message
+     * @param string $contents Expected text.
+     * @param string $message Message to display if assertion fails.
      * @return void
      */
     public function assertMailContainsText(string $contents, string $message = ''): void
     {
-        $this->assertThat($contents, new MailContainsText(), $message);
+        $this->assertThat($expectedText, new MailContainsText(), $message);
     }
 
     /**

+ 5 - 5
src/TestSuite/TestCase.php

@@ -197,7 +197,7 @@ abstract class TestCase extends BaseTestCase
      * Useful to test how plugins being loaded/not loaded interact with other
      * elements in CakePHP or applications.
      *
-     * @param array $plugins list of Plugins to load
+     * @param array $plugins List of Plugins to load.
      * @return \Cake\Http\BaseApplication
      */
     public function loadPlugins(array $plugins = []): BaseApplication
@@ -208,11 +208,11 @@ abstract class TestCase extends BaseTestCase
             ['']
         );
 
-        foreach ($plugins as $k => $opts) {
-            if (is_array($opts)) {
-                $app->addPlugin($k, $opts);
+        foreach ($plugins as $pluginName => $config) {
+            if (is_array($config)) {
+                $app->addPlugin($pluginName, $config);
             } else {
-                $app->addPlugin($opts);
+                $app->addPlugin($config);
             }
         }
         $app->pluginBootstrap();

+ 12 - 0
tests/TestCase/Console/CommandTest.php

@@ -22,6 +22,7 @@ use Cake\ORM\Locator\TableLocator;
 use Cake\ORM\Table;
 use Cake\TestSuite\Stub\ConsoleOutput;
 use Cake\TestSuite\TestCase;
+use TestApp\Command\AutoLoadModelCommand;
 use TestApp\Command\DemoCommand;
 
 /**
@@ -54,6 +55,17 @@ class CommandTest extends TestCase
     }
 
     /**
+     * test loadModel is configured properly
+     *
+     * @return void
+     */
+    public function testConstructorAutoLoadModel()
+    {
+        $command = new AutoLoadModelCommand();
+        $this->assertInstanceOf(Table::class, $command->Posts);
+    }
+
+    /**
      * Test name
      *
      * @return void

+ 6 - 0
tests/TestCase/Datasource/ModelAwareTraitTest.php

@@ -16,6 +16,7 @@ namespace Cake\Test\TestCase\Datasource;
 
 use Cake\Datasource\FactoryLocator;
 use Cake\TestSuite\TestCase;
+use TestApp\Model\Table\PaginatorPostsTable;
 use TestApp\Stub\Stub;
 
 /**
@@ -55,6 +56,11 @@ class ModelAwareTraitTest extends TestCase
         $result = $stub->loadModel('Comments');
         $this->assertInstanceOf('Cake\ORM\Table', $result);
         $this->assertInstanceOf('Cake\ORM\Table', $stub->Comments);
+
+        $result = $stub->loadModel(PaginatorPostsTable::class);
+        $this->assertInstanceOf(PaginatorPostsTable::class, $result);
+        $this->assertInstanceOf(PaginatorPostsTable::class, $stub->PaginatorPosts);
+        $this->assertSame('PaginatorPosts', $result->getAlias());
     }
 
     /**

+ 36 - 0
tests/TestCase/ORM/Association/HasManyTest.php

@@ -1110,6 +1110,42 @@ class HasManyTest extends TestCase
 
     /**
      * Test that the associated entities are unlinked and deleted when they are dependent
+     * when associated entities array is indexed by string keys
+     *
+     * @return void
+     */
+    public function testSaveReplaceSaveStrategyDependentWithStringKeys()
+    {
+        $authors = $this->getTableLocator()->get('Authors');
+        $authors->hasMany('Articles', ['saveStrategy' => HasMany::SAVE_REPLACE, 'dependent' => true]);
+
+        $entity = $authors->newEntity([
+            'name' => 'mylux',
+            'articles' => [
+                ['title' => 'One Random Post', 'body' => 'The cake is not a lie'],
+                ['title' => 'Another Random Post', 'body' => 'The cake is nice'],
+                ['title' => 'One more random post', 'body' => 'The cake is forever'],
+            ],
+        ], ['associated' => ['Articles']]);
+
+        $entity = $authors->saveOrFail($entity, ['associated' => ['Articles']]);
+        $sizeArticles = count($entity->articles);
+        $this->assertSame($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
+
+        $articleId = $entity->articles[0]->id;
+        $entity->articles = [
+            'one' => $entity->articles[1],
+            'two' => $entity->articles[2],
+        ];
+
+        $authors->saveOrFail($entity, ['associated' => ['Articles']]);
+
+        $this->assertSame($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
+        $this->assertFalse($authors->Articles->exists(['id' => $articleId]));
+    }
+
+    /**
+     * Test that the associated entities are unlinked and deleted when they are dependent
      *
      * In the future this should change and apply the finder.
      *

+ 2 - 2
tests/TestCase/Shell/CompletionShellTest.php

@@ -101,8 +101,8 @@ class CompletionShellTest extends TestCase
         $output = $this->out->output();
 
         $expected = 'TestPlugin.example TestPlugin.sample TestPluginTwo.example unique welcome ' .
-            'cache help i18n plugin routes schema_cache server upgrade version ' .
-            "abort demo i18m integration merge sample shell_test testing_dispatch";
+            'cache help plugin routes schema_cache server upgrade version ' .
+            "abort auto_load_model demo i18n integration merge sample shell_test testing_dispatch";
         $this->assertTextEquals($expected, $output);
     }
 

+ 22 - 0
tests/TestCase/Shell/Helper/TableHelperTest.php

@@ -272,6 +272,28 @@ class TableHelperTest extends TestCase
     }
 
     /**
+     * Test output with formatted cells
+     *
+     * @return void
+     */
+    public function testOutputWithFormattedCells()
+    {
+        $data = [
+            ['short', 'Longish thing', '<info>short</info>'],
+            ['Longer thing', 'short', '<warning>Longest</warning> <error>Value</error>'],
+        ];
+        $this->helper->setConfig(['headers' => false]);
+        $this->helper->output($data);
+        $expected = [
+            '+--------------+---------------+---------------+',
+            '| short        | Longish thing | <info>short</info>         |',
+            '| Longer thing | short         | <warning>Longest</warning> <error>Value</error> |',
+            '+--------------+---------------+---------------+',
+        ];
+        $this->assertEquals($expected, $this->stub->messages());
+    }
+
+    /**
      * Test output with row separator
      *
      * @return void

+ 10 - 0
tests/test_app/TestApp/Command/AutoLoadModelCommand.php

@@ -0,0 +1,10 @@
+<?php
+declare(strict_types=1);
+namespace TestApp\Command;
+
+use Cake\Console\Command;
+
+class AutoLoadModelCommand extends Command
+{
+    public $modelClass = 'Posts';
+}

+ 0 - 37
tests/test_app/TestApp/Shell/I18mShell.php

@@ -1,37 +0,0 @@
-<?php
-declare(strict_types=1);
-/**
- * I18mShell file
- *
- * 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.0.8
- * @license       https://opensource.org/licenses/mit-license.php MIT License
- */
-
-/**
- * SampleShell
- */
-namespace TestApp\Shell;
-
-use Cake\Console\Shell;
-
-class I18mShell extends Shell
-{
-    /**
-     * main method
-     *
-     * @return void
-     */
-    public function main()
-    {
-        $this->out('This is the main method called from I18mShell');
-    }
-}