浏览代码

Merge branch 'master' into 3.next

Mark Story 10 年之前
父节点
当前提交
4697b8e303

文件差异内容过多而无法显示
+ 1165 - 1169
config/cacert.pem


+ 3 - 1
src/Collection/CollectionTrait.php

@@ -86,12 +86,14 @@ trait CollectionTrait
      */
     public function every(callable $c)
     {
+        $return = false;
         foreach ($this->unwrap() as $key => $value) {
+            $return = true;
             if (!$c($value, $key)) {
                 return false;
             }
         }
-        return true;
+        return $return;
     }
 
     /**

+ 3 - 0
src/Console/ConsoleOptionParser.php

@@ -361,8 +361,10 @@ class ConsoleOptionParser
             $option = new ConsoleInputOption($options);
         }
         $this->_options[$name] = $option;
+        asort($this->_options);
         if ($option->short() !== null) {
             $this->_shortOptions[$option->short()] = $name;
+            asort($this->_shortOptions);
         }
         return $this;
     }
@@ -495,6 +497,7 @@ class ConsoleOptionParser
             $command = new ConsoleInputSubcommand($options);
         }
         $this->_subcommands[$name] = $command;
+        asort($this->_subcommands);
         return $this;
     }
 

+ 8 - 2
src/Database/Query.php

@@ -1685,8 +1685,14 @@ class Query implements ExpressionInterface, IteratorAggregate
      *
      * If type is expressed as "atype[]" (note braces) then it will cause the
      * placeholder to be re-written dynamically so if the value is an array, it
-     * will create as many placeholders as values are in it. For example "string[]"
-     * will create several placeholders of type string.
+     * will create as many placeholders as values are in it. For example:
+     *
+     * ```
+     * $query->bind(':id', [1, 2, 3], 'int[]');
+     * ```
+     *
+     * Will create 3 int placeholders. When using named placeholders, this method
+     * requires that the placeholders include `:` e.g. `:value`.
      *
      * @param string|int $param placeholder to be replaced with quoted version
      *   of $value

+ 2 - 0
src/Datasource/EntityInterface.php

@@ -20,6 +20,8 @@ use JsonSerializable;
 /**
  * Describes the methods that any class representing a data storage should
  * comply with.
+ *
+ * @property mixed $id Alias for commonly used primary key.
  */
 interface EntityInterface extends ArrayAccess, JsonSerializable
 {

+ 2 - 7
src/Network/Http/Response.php

@@ -380,19 +380,14 @@ class Response extends Message
     /**
      * Get the response body as JSON decoded data.
      *
-     * @return null|array
+     * @return mixed
      */
     protected function _getJson()
     {
         if (!empty($this->_json)) {
             return $this->_json;
         }
-        $data = json_decode($this->_body, true);
-        if ($data) {
-            $this->_json = $data;
-            return $this->_json;
-        }
-        return null;
+        return $this->_json = json_decode($this->_body, true);
     }
 
     /**

+ 4 - 1
src/ORM/README.md

@@ -7,7 +7,7 @@ The CakePHP ORM provides a powerful and flexible way to work with relational
 databases. Using a datamapper pattern the ORM allows you to manipulate data as
 entities allowing you to create expressive domain layers in your applications.
 
-## Connecting to the Database
+## Database engines supported
 
 The CakePHP ORM is compatible with:
 
@@ -15,6 +15,9 @@ The CakePHP ORM is compatible with:
 * Postgres 8+
 * SQLite3
 * SQLServer 2008+
+* Oracle (through a [community plugin](https://github.com/CakeDC/cakephp-oracle-driver))
+
+## Connecting to the Database
 
 The first thing you need to do when using this library is register a connection
 object.  Before performing any operations with the connection, you need to

+ 39 - 0
src/ORM/Table.php

@@ -1619,6 +1619,45 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc
     }
 
     /**
+     * Persists multiple entities of a table.
+     *
+     * The records will be saved in a transaction which will be rolled back if
+     * any one of the records fails to save due to failed validation or database
+     * error.
+     *
+     * @param array|\Cake\ORM\ResultSet $entities Entities to save.
+     * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity.
+     * @return bool|array|\Cake\ORM\ResultSet False on failure, entities list on succcess.
+     */
+    public function saveMany($entities, $options = [])
+    {
+        $isNew = [];
+
+        $return = $this->connection()->transactional(
+            function () use ($entities, $options, &$isNew) {
+                foreach ($entities as $key => $entity) {
+                    $isNew[$key] = $entity->isNew();
+                    if ($this->save($entity, $options) === false) {
+                        return false;
+                    }
+                }
+            }
+        );
+
+        if ($return === false) {
+            foreach ($entities as $key => $entity) {
+                if (isset($isNew[$key]) && $isNew[$key]) {
+                    $entity->unsetProperty($this->primaryKey());
+                    $entity->isNew(true);
+                }
+            }
+            return false;
+        }
+
+        return $entities;
+    }
+
+    /**
      * {@inheritDoc}
      *
      * For HasMany and HasOne associations records will be removed based on

+ 5 - 5
src/ORM/composer.json

@@ -4,9 +4,9 @@
     "license": "MIT",
     "authors": [
         {
-        "name": "CakePHP Community",
-        "homepage": "http://cakephp.org"
-    }
+            "name": "CakePHP Community",
+            "homepage": "http://cakephp.org"
+        }
     ],
     "autoload": {
         "psr-4": {
@@ -16,8 +16,8 @@
     "require": {
         "cakephp/collection": "~3.0",
         "cakephp/core": "~3.0",
-        "cakephp/datasource": "~3.0",
-        "cakephp/database": "~3.0",
+        "cakephp/datasource": "^3.1.2",
+        "cakephp/database": "^3.1.4",
         "cakephp/event": "~3.0",
         "cakephp/utility": "~3.0",
         "cakephp/validation": "~3.0"

+ 10 - 1
src/Utility/Security.php

@@ -104,7 +104,16 @@ class Security
             return random_bytes($length);
         }
         if (function_exists('openssl_random_pseudo_bytes')) {
-            return openssl_random_pseudo_bytes($length);
+            $bytes = openssl_random_pseudo_bytes($length, $strongSource);
+            if (!$strongSource) {
+                trigger_error(
+                    'openssl was unable to use a strong source of entropy. ' .
+                    'Consider updating your system libraries, or ensuring ' .
+                    'you have more available entropy.',
+                    E_USER_WARNING
+                );
+            }
+            return $bytes;
         }
         trigger_error(
             'You do not have a safe source of random data available. ' .

+ 56 - 43
src/Utility/Text.php

@@ -570,13 +570,12 @@ class Text
             $default['ellipsis'] = "\xe2\x80\xa6";
         }
         $options += $default;
-        extract($options);
 
-        if ($html) {
+        if ($options['html']) {
             if (mb_strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
                 return $text;
             }
-            $totalLength = mb_strlen(strip_tags($ellipsis));
+            $totalLength = mb_strlen(strip_tags($options['ellipsis']));
             $openTags = [];
             $truncate = '';
 
@@ -609,49 +608,70 @@ class Text
                         }
                     }
 
-                    $truncate .= mb_substr($tag[3], 0, $left + $entitiesLength);
+                    if (!$options['exact']) {
+                        $words = explode(' ', $tag[3]);
+                        // Keep at least one word.
+                        if (count($words) === 1) {
+                            $truncate .= mb_substr($tag[3], 0, $left + $entitiesLength);
+                        } else {
+                            $wordLength = 0;
+                            $addWords = [];
+                            // Append words until the length is crossed.
+                            foreach ($words as $word) {
+                                // Add words until we have enough letters.
+                                if ($wordLength < $left + $entitiesLength) {
+                                    $addWords[] = $word;
+                                }
+                                // Include inter-word space.
+                                $wordLength += mb_strlen($word) + 1;
+                            }
+                            $truncate .= implode(' ', $addWords);
+
+                            // If the string is longer than requested, find the last space and cut there.
+                            $lastSpace = mb_strrpos($truncate, ' ');
+                            if (mb_strlen($truncate) > $totalLength && $lastSpace !== false) {
+                                $remainder = mb_substr($truncate, $lastSpace);
+                                $truncate = mb_substr($truncate, 0, $lastSpace);
+
+                                // Re-add close tags that were cut off.
+                                preg_match_all('/<\/([a-z]+)>/', $remainder, $droppedTags, PREG_SET_ORDER);
+                                if ($droppedTags) {
+                                    foreach ($droppedTags as $closingTag) {
+                                        if (!in_array($closingTag[1], $openTags)) {
+                                            array_unshift($openTags, $closingTag[1]);
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        $truncate .= mb_substr($tag[3], 0, $left + $entitiesLength);
+                    }
                     break;
                 }
-
                 $truncate .= $tag[3];
+
                 $totalLength += $contentLength;
                 if ($totalLength >= $length) {
                     break;
                 }
             }
-        } else {
-            if (mb_strlen($text) <= $length) {
-                return $text;
+
+            $truncate .= $options['ellipsis'];
+
+            foreach ($openTags as $tag) {
+                $truncate .= '</' . $tag . '>';
             }
-            $truncate = mb_substr($text, 0, $length - mb_strlen($ellipsis));
+            return $truncate;
         }
-        if (!$exact) {
+
+        if (mb_strlen($text) <= $length) {
+            return $text;
+        }
+        $truncate = mb_substr($text, 0, $length - mb_strlen($options['ellipsis']));
+
+        if (!$options['exact']) {
             $spacepos = mb_strrpos($truncate, ' ');
-            if ($html) {
-                $truncateCheck = mb_substr($truncate, 0, $spacepos);
-                $lastOpenTag = mb_strrpos($truncateCheck, '<');
-                $lastCloseTag = mb_strrpos($truncateCheck, '>');
-                if ($lastOpenTag > $lastCloseTag) {
-                    preg_match_all('/<[\w]+[^>]*>/s', $truncate, $lastTagMatches);
-                    $lastTag = array_pop($lastTagMatches[0]);
-                    $spacepos = mb_strrpos($truncate, $lastTag) + mb_strlen($lastTag);
-                }
-                $bits = mb_substr($truncate, $spacepos);
-                preg_match_all('/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER);
-                if (!empty($droppedTags)) {
-                    if (!empty($openTags)) {
-                        foreach ($droppedTags as $closingTag) {
-                            if (!in_array($closingTag[1], $openTags)) {
-                                array_unshift($openTags, $closingTag[1]);
-                            }
-                        }
-                    } else {
-                        foreach ($droppedTags as $closingTag) {
-                            $openTags[] = $closingTag[1];
-                        }
-                    }
-                }
-            }
             $truncate = mb_substr($truncate, 0, $spacepos);
 
             // If truncate still empty, then we don't need to count ellipsis in the cut.
@@ -660,14 +680,7 @@ class Text
             }
         }
 
-        $truncate .= $ellipsis;
-
-        if ($html) {
-            foreach ($openTags as $tag) {
-                $truncate .= '</' . $tag . '>';
-            }
-        }
-
+        $truncate .= $options['ellipsis'];
         return $truncate;
     }
 

+ 1 - 1
src/View/Helper/HtmlHelper.php

@@ -148,7 +148,7 @@ class HtmlHelper extends Helper
      * Adds a link to the breadcrumbs array.
      *
      * @param string $name Text for link
-     * @param string|null $link URL for link (if empty it won't be a link)
+     * @param string|array|null $link URL for link (if empty it won't be a link)
      * @param string|array $options Link attributes e.g. ['id' => 'selected']
      * @return $this
      * @see \Cake\View\Helper\HtmlHelper::link() for details on $options that can be used.

+ 7 - 0
tests/TestCase/Collection/CollectionTest.php

@@ -192,6 +192,13 @@ class CollectionTest extends TestCase
             ->will($this->returnValue(false));
         $callable->expects($this->exactly(2))->method('__invoke');
         $this->assertFalse($collection->every($callable));
+
+        $items = [];
+        $collection = new Collection($items);
+        $callable = $this->getMock('stdClass', ['__invoke']);
+        $callable->expects($this->never())
+            ->method('__invoke');
+        $this->assertFalse($collection->every($callable));
     }
 
     /**

+ 2 - 2
tests/TestCase/Console/ConsoleOptionParserTest.php

@@ -591,12 +591,12 @@ class ConsoleOptionParserTest extends TestCase
         $result = $parser->help('method');
         $expected = <<<TEXT
 <info>Usage:</info>
-cake mycommand method [-h] [--connection]
+cake mycommand method [--connection] [-h]
 
 <info>Options:</info>
 
---help, -h        Display this help.
 --connection      Db connection.
+--help, -h        Display this help.
 
 TEXT;
         $this->assertTextEquals($expected, $result, 'Help is not correct.');

+ 5 - 5
tests/TestCase/Console/HelpFormatterTest.php

@@ -47,7 +47,7 @@ This is fifteen This is
 fifteen This is fifteen
 
 <info>Usage:</info>
-cake test [subcommand] [-h] [--four] [<four>]
+cake test [subcommand] [--four] [-h] [<four>]
 
 <info>Subcommands:</info>
 
@@ -58,9 +58,9 @@ To see help on a subcommand use <info>`cake test [subcommand] --help`</info>
 
 <info>Options:</info>
 
---help, -h  Display this help.
 --four      this is help text
             this is help text
+--help, -h  Display this help.
 
 <info>Arguments:</info>
 
@@ -199,14 +199,14 @@ txt;
         $result = $formatter->text();
         $expected = <<<txt
 <info>Usage:</info>
-cake mycommand [-h] [--test] [-c default]
+cake mycommand [-c default] [-h] [--test]
 
 <info>Options:</info>
 
---help, -h        Display this help.
---test            A test option.
 --connection, -c  The connection to use. <comment>(default:
                   default)</comment>
+--help, -h        Display this help.
+--test            A test option.
 
 txt;
         $this->assertTextEquals($expected, $result, 'Help does not match');

+ 17 - 1
tests/TestCase/Network/Http/ResponseTest.php

@@ -100,7 +100,23 @@ class ResponseTest extends TestCase
 
         $data = '';
         $response = new Response([], $data);
-        $this->assertFalse(isset($response->json));
+        $this->assertNull($response->json);
+
+        $data = json_encode([]);
+        $response = new Response([], $data);
+        $this->assertTrue(is_array($response->json));
+
+        $data = json_encode(null);
+        $response = new Response([], $data);
+        $this->assertNull($response->json);
+
+        $data = json_encode(false);
+        $response = new Response([], $data);
+        $this->assertFalse($response->json);
+
+        $data = json_encode('');
+        $response = new Response([], $data);
+        $this->assertSame('', $response->json);
     }
 
     /**

+ 66 - 0
tests/TestCase/ORM/TableTest.php

@@ -2778,6 +2778,72 @@ class TableTest extends TestCase
     }
 
     /**
+     * Test saveMany() with entities array
+     *
+     * @return void
+     */
+    public function testSaveManyArray()
+    {
+        $entities = [
+            new Entity(['name' => 'admad']),
+            new Entity(['name' => 'dakota'])
+        ];
+
+        $table = TableRegistry::get('authors');
+        $result = $table->saveMany($entities);
+
+        $this->assertSame($entities, $result);
+        $this->assertTrue(isset($result[0]->id));
+        foreach ($entities as $entity) {
+            $this->assertFalse($entity->isNew());
+        }
+    }
+
+    /**
+     * Test saveMany() with ResultSet instance
+     *
+     * @return void
+     */
+    public function testSaveManyResultSet()
+    {
+        $table = TableRegistry::get('authors');
+
+        $entities = $table->find()
+            ->order(['id' => 'ASC'])
+            ->all();
+        $entities->first()->name = 'admad';
+
+        $result = $table->saveMany($entities);
+        $this->assertSame($entities, $result);
+
+        $first = $table->find()
+            ->order(['id' => 'ASC'])
+            ->first();
+        $this->assertSame('admad', $first->name);
+    }
+
+    /**
+     * Test saveMany() with failed save
+     *
+     * @return void
+     */
+    public function testSaveManyFailed()
+    {
+        $table = TableRegistry::get('authors');
+        $entities = [
+            new Entity(['name' => 'mark']),
+            new Entity(['name' => 'jose'])
+        ];
+        $entities[1]->errors(['name' => ['message']]);
+        $result = $table->saveMany($entities);
+
+        $this->assertFalse($result);
+        foreach ($entities as $entity) {
+            $this->assertTrue($entity->isNew());
+        }
+    }
+
+    /**
      * Test simple delete.
      *
      * @return void

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

@@ -161,7 +161,7 @@ class CompletionShellTest extends TestCase
         $this->Shell->runCommand(['options', 'orm_cache']);
         $output = $this->out->output;
 
-        $expected = "--help -h --verbose -v --quiet -q --connection -c\n";
+        $expected = "--connection -c --help -h --quiet -q --verbose -v\n";
         $this->assertTextEquals($expected, $output);
     }
 
@@ -175,7 +175,7 @@ class CompletionShellTest extends TestCase
         $this->Shell->runCommand(['options', 'sample', 'sample']);
         $output = $this->out->output;
 
-        $expected = "--help -h --verbose -v --quiet -q --sample -s\n";
+        $expected = "--help -h --quiet -q --sample -s --verbose -v\n";
         $this->assertTextEquals($expected, $output);
     }
 

+ 23 - 35
tests/TestCase/Utility/TextTest.php

@@ -556,7 +556,7 @@ TEXT;
         $this->assertSame($this->Text->truncate($text1, 15, ['html' => true]), "The quick brow\xe2\x80\xa6");
         $this->assertSame($this->Text->truncate($text1, 15, ['exact' => false, 'html' => true]), "The quick\xe2\x80\xa6");
         $this->assertSame($this->Text->truncate($text2, 10, ['html' => true]), "Heiz&ouml;lr&uuml;c\xe2\x80\xa6");
-        $this->assertSame($this->Text->truncate($text2, 10, ['exact' => false, 'html' => true]), "Heiz&ouml;\xe2\x80\xa6");
+        $this->assertSame($this->Text->truncate($text2, 10, ['exact' => false, 'html' => true]), "Heiz&ouml;lr&uuml;c\xe2\x80\xa6");
         $this->assertSame($this->Text->truncate($text3, 20, ['html' => true]), "<b>&copy; 2005-2007, Cake S\xe2\x80\xa6</b>");
         $this->assertSame($this->Text->truncate($text4, 15, ['html' => true]), "<img src=\"mypic.jpg\"> This image ta\xe2\x80\xa6");
         $this->assertSame($this->Text->truncate($text4, 45, ['html' => true]), "<img src=\"mypic.jpg\"> This image tag is not XHTML conform!<br><hr/><b>But the\xe2\x80\xa6</b>");
@@ -576,43 +576,31 @@ TEXT;
             'exact' => false,
             'html' => true
         ]);
-        $expected = '<p><span style="font-size: medium;"><a>...</a></span></p>';
-        $this->assertEquals($expected, $result);
-
-        $text = '<p><span style="font-size: medium;">El biógrafo de Steve Jobs, Walter
-Isaacson, explica porqué Jobs le pidió que le hiciera su biografía en
-este artículo de El País.</span></p>
-<p><span style="font-size: medium;"><span style="font-size:
-large;">Por qué Steve era distinto.</span></span></p>
-<p><span style="font-size: medium;"><a href="http://www.elpais.com/
-articulo/primer/plano/Steve/era/distinto/elpepueconeg/
-20111009elpneglse_4/Tes">http://www.elpais.com/articulo/primer/plano/
-Steve/era/distinto/elpepueconeg/20111009elpneglse_4/Tes</a></span></p>
-<p><span style="font-size: medium;">Ya se ha publicado la biografía de
-Steve Jobs escrita por Walter Isaacson  "<strong>Steve Jobs by Walter
-Isaacson</strong>", aquí os dejamos la dirección de amazon donde
-podeís adquirirla.</span></p>
-<p><span style="font-size: medium;"><a>http://www.amazon.com/Steve-
-Jobs-Walter-Isaacson/dp/1451648537</a></span></p>';
-        $result = $this->Text->truncate($text, 500, [
-            'ellipsis' => '... ',
+        $expected = '<p><span style="font-size: medium;"><a>Iamates...</a></span></p>';
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test truncate() method with both exact and html.
+     * @return void
+     */
+    public function testTruncateExactHtml()
+    {
+        $text = '<a href="http://example.org">hello</a> world';
+        $expected = '<a href="http://example.org">hell..</a>';
+        $result = Text::truncate($text, 6, [
+            'ellipsis' => '..',
+            'exact' => true,
+            'html' => true
+        ]);
+        $this->assertEquals($expected, $result);
+
+        $expected = '<a href="http://example.org">hell..</a>';
+        $result = Text::truncate($text, 6, [
+            'ellipsis' => '..',
             'exact' => false,
             'html' => true
         ]);
-        $expected = '<p><span style="font-size: medium;">El biógrafo de Steve Jobs, Walter
-Isaacson, explica porqué Jobs le pidió que le hiciera su biografía en
-este artículo de El País.</span></p>
-<p><span style="font-size: medium;"><span style="font-size:
-large;">Por qué Steve era distinto.</span></span></p>
-<p><span style="font-size: medium;"><a href="http://www.elpais.com/
-articulo/primer/plano/Steve/era/distinto/elpepueconeg/
-20111009elpneglse_4/Tes">http://www.elpais.com/articulo/primer/plano/
-Steve/era/distinto/elpepueconeg/20111009elpneglse_4/Tes</a></span></p>
-<p><span style="font-size: medium;">Ya se ha publicado la biografía de
-Steve Jobs escrita por Walter Isaacson  "<strong>Steve Jobs by Walter
-Isaacson</strong>", aquí os dejamos la dirección de amazon donde
-podeís adquirirla.</span></p>
-<p><span style="font-size: medium;"><a>... </a></span></p>';
         $this->assertEquals($expected, $result);
     }
 

+ 10 - 1
tests/TestCase/View/Helper/HtmlHelperTest.php

@@ -1280,6 +1280,11 @@ class HtmlHelperTest extends TestCase
         ];
         $this->assertHtml($expected, $result);
 
+        $this->Html->addCrumb('Fifth', [
+            'plugin' => false,
+            'controller' => 'controller',
+            'action' => 'action',
+        ]);
         $result = $this->Html->getCrumbs('-', 'Start');
         $expected = [
             ['a' => ['href' => '/']],
@@ -1298,7 +1303,11 @@ class HtmlHelperTest extends TestCase
             'Third',
             '/a',
             '-',
-            'Fourth'
+            'Fourth',
+            '-',
+            ['a' => ['href' => '/controller/action']],
+            'Fifth',
+            '/a',
         ];
         $this->assertHtml($expected, $result);
     }