浏览代码

Merge branch '3.next' into 3.x

Mark Story 4 年之前
父节点
当前提交
08439ed3f4

+ 1 - 1
VERSION.txt

@@ -16,4 +16,4 @@
 // @license       https://opensource.org/licenses/mit-license.php MIT License
 // +--------------------------------------------------------------------------------------------+ //
 ////////////////////////////////////////////////////////////////////////////////////////////////////
-3.9.10
+3.10.0-RC1

+ 19 - 1
src/Cache/Engine/MemcachedEngine.php

@@ -145,7 +145,25 @@ class MemcachedEngine extends CacheEngine
         }
         $this->_setOptions();
 
-        if (count($this->_Memcached->getServerList())) {
+        $serverList = $this->_Memcached->getServerList();
+        if (count($serverList)) {
+            if ($this->_config['persistent'] !== false) {
+                $actualServers = [];
+                foreach ($serverList as $server) {
+                    $actualServers[] = $server['host'] . ':' . $server['port'];
+                }
+                unset($server);
+                sort($actualServers);
+                $configuredServers = $this->_config['servers'];
+                sort($configuredServers);
+                if ($actualServers !== $configuredServers) {
+                    $message = "Invalid cache configuration. Multiple persistent cache configurations are detected" .
+                        " with different 'servers' values. 'servers' values for persistent cache configurations" .
+                        " must be the same when using the same persistence id.";
+                    throw new InvalidArgumentException($message);
+                }
+            }
+
             return true;
         }
 

+ 2 - 0
src/Core/StaticConfigTrait.php

@@ -24,6 +24,8 @@ use LogicException;
  * configuration data registered and manipulated.
  *
  * Implementing objects are expected to declare a static `$_dsnClassMap` property.
+ *
+ * @property \Cake\Core\ObjectRegistry $_registry
  */
 trait StaticConfigTrait
 {

+ 21 - 27
src/ORM/Behavior/TreeBehavior.php

@@ -819,47 +819,41 @@ class TreeBehavior extends Behavior
     /**
      * Recursive method used to recover a single level of the tree
      *
-     * @param int $counter The Last left column value that was assigned
+     * @param int $lftRght The starting lft/rght value
      * @param mixed $parentId the parent id of the level to be recovered
      * @param int $level Node level
-     * @return int The next value to use for the left column
+     * @return int The next lftRght value
      */
-    protected function _recoverTree($counter = 0, $parentId = null, $level = -1)
+    protected function _recoverTree($lftRght = 1, $parentId = null, $level = 0)
     {
         $config = $this->getConfig();
         list($parent, $left, $right) = [$config['parent'], $config['left'], $config['right']];
         $primaryKey = $this->_getPrimaryKey();
-        $aliasedPrimaryKey = $this->_table->aliasField($primaryKey);
-        $order = $config['recoverOrder'] ?: $aliasedPrimaryKey;
+        $order = $config['recoverOrder'] ?: $primaryKey;
 
-        $query = $this->_scope($this->_table->query())
-            ->select([$aliasedPrimaryKey])
-            ->where([$this->_table->aliasField($parent) . ' IS' => $parentId])
+        $nodes = $this->_scope($this->_table->query())
+            ->select($primaryKey)
+            ->where([$parent . ' IS' => $parentId])
             ->order($order)
-            ->disableHydration();
+            ->disableHydration()
+            ->all();
 
-        $leftCounter = $counter;
-        $nextLevel = $level + 1;
-        foreach ($query as $row) {
-            $counter++;
-            $counter = $this->_recoverTree($counter, $row[$primaryKey], $nextLevel);
-        }
+        foreach ($nodes as $node) {
+            $nodeLft = $lftRght++;
+            $lftRght = $this->_recoverTree($lftRght, $node[$primaryKey], $level + 1);
 
-        if ($parentId === null) {
-            return $counter;
-        }
+            $fields = [$left => $nodeLft, $right => $lftRght++];
+            if ($config['level']) {
+                $fields[$config['level']] = $level;
+            }
 
-        $fields = [$left => $leftCounter, $right => $counter + 1];
-        if ($config['level']) {
-            $fields[$config['level']] = $level;
+            $this->_table->updateAll(
+                $fields,
+                [$primaryKey => $node[$primaryKey]]
+            );
         }
 
-        $this->_table->updateAll(
-            $fields,
-            [$primaryKey => $parentId]
-        );
-
-        return $counter + 1;
+        return $lftRght;
     }
 
     /**

+ 25 - 1
src/ORM/Table.php

@@ -1135,6 +1135,12 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * - finder: The finder method to use when loading records from this association.
      *   Defaults to 'all'. When the strategy is 'join', only the fields, containments,
      *   and where conditions will be used from the finder.
+     * - propertyName: The property name that should be filled with data from the
+     *   associated table into the source table results. By default this is the underscored
+     *   & singular name of the association. For an association of ProductCategories it would
+     *   be product_category.
+     * - bindingKey: The name of the column in the other table used to match 'foreignKey'.
+     *   The default value is the primary key of the other table.
      *
      * This method will return the association object that was built.
      *
@@ -1181,6 +1187,12 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * - finder: The finder method to use when loading records from this association.
      *   Defaults to 'all'. When the strategy is 'join', only the fields, containments,
      *   and where conditions will be used from the finder.
+     * - propertyName: The property name that should be filled with data from the
+     *   associated table into the source table results. By default this is the underscored
+     *   & singular name of the association. For an association of ProductCategories it would
+     *   be product_category.
+     * - bindingKey: The name of the column in the other table used to match 'foreignKey'.
+     *   The default value is the primary key of the other table.
      *
      * This method will return the association object that was built.
      *
@@ -1233,6 +1245,12 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      *   target table.
      * - finder: The finder method to use when loading records from this association.
      *   Defaults to 'all'.
+     * - propertyName: The property name that should be filled with data from the
+     *   associated table into the source table results. By default this is the underscored
+     *   & singular name of the association. For an association of ProductCategories it would
+     *   be product_category.
+     * - bindingKey: The name of the column in the other table used to match 'foreignKey'.
+     *   The default value is the primary key of the other table.
      *
      * This method will return the association object that was built.
      *
@@ -1287,6 +1305,12 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * - strategy: The loading strategy to use. 'select' and 'subquery' are supported.
      * - finder: The finder method to use when loading records from this association.
      *   Defaults to 'all'.
+     * - propertyName: The property name that should be filled with data from the
+     *   associated table into the source table results. By default this is the underscored
+     *   & singular name of the association. For an association of ProductCategories it would
+     *   be product_category.
+     * - bindingKey: The name of the column in the other table used to match 'foreignKey'.
+     *   The default value is the primary key of the other table.
      *
      * This method will return the association object that was built.
      *
@@ -1911,7 +1935,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
      * ```
      *
      * @param \Cake\Datasource\EntityInterface $entity
-     * @param array $options
+     * @param \Cake\ORM\SaveOptionsBuilder|array $options
      * @return \Cake\Datasource\EntityInterface|false
      * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction is aborted in the afterSave event.
      */

+ 1 - 1
src/TestSuite/Constraint/Email/MailConstraintBase.php

@@ -50,7 +50,7 @@ abstract class MailConstraintBase extends Constraint
     {
         $emails = TestEmailTransport::getEmails();
 
-        if ($this->at) {
+        if ($this->at !== null) {
             if (!isset($emails[$this->at])) {
                 return [];
             }

+ 2 - 3
src/Validation/Validation.php

@@ -701,8 +701,7 @@ class Validation
 
     /**
      * Time validation, determines if the string passed is a valid time.
-     * Validates time as 24hr (HH:MM) or am/pm ([H]H:MM[a|p]m)
-     * Does not allow/validate seconds.
+     * Validates time as 24hr (HH:MM[:SS]) or am/pm ([H]H[:MM][:SS][a|p]m)
      *
      * @param string|\DateTimeInterface $check a valid time string/object
      * @return bool Success
@@ -716,7 +715,7 @@ class Validation
             $check = static::_getDateString($check);
         }
 
-        return static::_check($check, '%^((0?[1-9]|1[012])(:[0-5]\d){0,2} ?([AP]M|[ap]m))$|^([01]\d|2[0-3])(:[0-5]\d){0,2}$%');
+        return static::_check($check, '%^((0?[1-9]|1[012])(:[0-5]\d){0,2} ?([AP]M|[ap]m))$|^([01]\d|2[0-3])(:[0-5]\d){1,2}$%');
     }
 
     /**

+ 26 - 0
tests/TestCase/Cache/Engine/MemcachedEngineTest.php

@@ -361,6 +361,32 @@ class MemcachedEngineTest extends TestCase
     }
 
     /**
+     * testConfigDifferentPorts method
+     *
+     * @expectedException InvalidArgumentException
+     * @expectedExceptionMessage Invalid cache configuration. Multiple persistent cache configurations are detected with different 'servers' values. 'servers' values for persistent cache configurations must be the same when using the same persistence id.
+     * @return void
+     */
+    public function testConfigDifferentPorts()
+    {
+        $Memcached1 = new MemcachedEngine();
+        $config1 = [
+            'className' => 'Memcached',
+            'servers' => ['127.0.0.1:11211'],
+            'persistent' => true,
+        ];
+        $Memcached1->init($config1);
+
+        $Memcached2 = new MemcachedEngine();
+        $config2 = [
+            'className' => 'Memcached',
+            'servers' => ['127.0.0.1:11212'],
+            'persistent' => true,
+        ];
+        $Memcached2->init($config2);
+    }
+
+    /**
      * testConfig method
      *
      * @return void

+ 2 - 2
tests/TestCase/Http/Cookie/CookieCollectionTest.php

@@ -206,7 +206,7 @@ class CookieCollectionTest extends TestCase
         ]);
         $response = (new Response())
             ->withAddedHeader('Set-Cookie', 'test=value')
-            ->withAddedHeader('Set-Cookie', 'expiring=soon; Expires=Wed, 09-Jun-2021 10:18:14 GMT; Path=/; HttpOnly; Secure;')
+            ->withAddedHeader('Set-Cookie', 'expiring=soon; Expires=Wed, 14-Jun-2023 10:18:14 GMT; Path=/; HttpOnly; Secure;')
             ->withAddedHeader('Set-Cookie', 'session=123abc; Domain=www.example.com')
             ->withAddedHeader('Set-Cookie', 'maxage=value; Max-Age=60; Expires=Wed, 09-Jun-2021 10:18:14 GMT;');
         $new = $collection->addFromResponse($response, $request);
@@ -225,7 +225,7 @@ class CookieCollectionTest extends TestCase
 
         $this->assertNull($new->get('test')->getExpiry(), 'No expiry');
         $this->assertSame(
-            '2021-06-09 10:18:14',
+            '2023-06-14 10:18:14',
             $new->get('expiring')->getExpiry()->format('Y-m-d H:i:s'),
             'Has expiry'
         );

+ 20 - 0
tests/TestCase/TestSuite/EmailTraitTest.php

@@ -16,10 +16,12 @@ namespace Cake\Test\TestCase\TestSuite;
 
 use Cake\Mailer\Email;
 use Cake\Mailer\TransportFactory;
+use Cake\TestSuite\Constraint\Email\MailSentFrom;
 use Cake\TestSuite\EmailTrait;
 use Cake\TestSuite\TestCase;
 use Cake\TestSuite\TestEmailTransport;
 use PHPUnit\Framework\AssertionFailedError;
+use PHPUnit\Framework\Constraint\LogicalNot;
 
 /**
  * Tests EmailTrait assertions
@@ -125,6 +127,24 @@ class EmailTraitTest extends TestCase
     }
 
     /**
+     * confirm that "at 0" is really testing email 0, not all the emails
+     *
+     * @return void
+     */
+    public function testAt0()
+    {
+        $this->skipIf(!class_exists('PHPUnit\Framework\Constraint\LogicalNot'), 'LogicalNot class not supported on PHP5.6');
+
+        $this->assertNoMailSent();
+
+        $this->sendEmails();
+
+        $this->assertMailCount(3);
+
+        $this->assertThat('alternate@example.com', new LogicalNot(new MailSentFrom(0)));
+    }
+
+    /**
      * tests assertNoMailSent fails when no mail is sent
      *
      * @return void

+ 6 - 0
tests/TestCase/Validation/ValidationTest.php

@@ -1646,6 +1646,12 @@ class ValidationTest extends TestCase
         $this->assertTrue(Validation::time('1:00pm'));
         $this->assertFalse(Validation::time('13:00pm'));
         $this->assertFalse(Validation::time('9:00'));
+        $this->assertFalse(Validation::time('00'));
+        $this->assertFalse(Validation::time('0'));
+        $this->assertFalse(Validation::time('09'));
+        $this->assertFalse(Validation::time('9'));
+        $this->assertFalse(Validation::time('10'));
+        $this->assertFalse(Validation::time('23'));
     }
 
     /**