From 9e50e93861c0da5ccbb5c55534c6a50a91efb63c Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 25 Sep 2015 22:37:34 +0530 Subject: [PATCH 001/128] Invoke cell action just before rendering. This is a step towards avoiding running the cell action when cached cell is available. --- src/View/Cell.php | 29 +++++++++++++++++++++++++++++ src/View/CellTrait.php | 15 +++------------ tests/TestCase/View/CellTest.php | 3 ++- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/View/Cell.php b/src/View/Cell.php index a89c8c7745a..2af07c8512f 100644 --- a/src/View/Cell.php +++ b/src/View/Cell.php @@ -14,6 +14,7 @@ */ namespace Cake\View; +use BadMethodCallException; use Cake\Datasource\ModelAwareTrait; use Cake\Event\EventDispatcherTrait; use Cake\Event\EventManager; @@ -24,6 +25,8 @@ use Cake\View\Exception\MissingCellViewException; use Cake\View\Exception\MissingTemplateException; use Exception; +use ReflectionException; +use ReflectionMethod; /** * Cell base. @@ -86,6 +89,20 @@ abstract class Cell */ public $helpers = []; + /** + * The cell's action to invoke. + * + * @var string + */ + public $action; + + /** + * Arguments to pass to cell's action. + * + * @var array + */ + public $args = []; + /** * These properties can be set directly on Cell and passed to the View as options. * @@ -131,6 +148,7 @@ public function __construct( $this->response = $response; $this->modelFactory('Table', [$this->tableLocator(), 'get']); + $this->_validCellOptions = array_merge(['action', 'args'], $this->_validCellOptions); foreach ($this->_validCellOptions as $var) { if (isset($cellOptions[$var])) { $this->{$var} = $cellOptions[$var]; @@ -151,6 +169,17 @@ public function __construct( */ public function render($template = null) { + try { + $reflect = new ReflectionMethod($this, $this->action); + $reflect->invokeArgs($this, $this->args); + } catch (ReflectionException $e) { + throw new BadMethodCallException(sprintf( + 'Class %s does not have a "%s" method.', + get_class($this), + $this->action + )); + } + if ($template !== null && strpos($template, '/') === false && strpos($template, '.') === false diff --git a/src/View/CellTrait.php b/src/View/CellTrait.php index 3aa75119584..1d316d44678 100644 --- a/src/View/CellTrait.php +++ b/src/View/CellTrait.php @@ -75,22 +75,13 @@ public function cell($cell, array $data = [], array $options = []) throw new Exception\MissingCellException(['className' => $pluginAndCell . 'Cell']); } - $cell = $this->_createCell($className, $action, $plugin, $options); if (!empty($data)) { $data = array_values($data); } + $options = ['action' => $action, 'args' => $data] + $options; + $cell = $this->_createCell($className, $action, $plugin, $options); - try { - $reflect = new ReflectionMethod($cell, $action); - $reflect->invokeArgs($cell, $data); - return $cell; - } catch (ReflectionException $e) { - throw new BadMethodCallException(sprintf( - 'Class %s does not have a "%s" method.', - $className, - $action - )); - } + return $cell; } /** diff --git a/tests/TestCase/View/CellTest.php b/tests/TestCase/View/CellTest.php index c0cef51102c..76d335a532f 100644 --- a/tests/TestCase/View/CellTest.php +++ b/tests/TestCase/View/CellTest.php @@ -250,7 +250,8 @@ public function testUnexistingCell() */ public function testCellMissingMethod() { - $this->View->cell('Articles::nope'); + $cell = $this->View->cell('Articles::nope'); + $cell->render(); } /** From 515c42e96f8006abc9257f93833c39ae40b7a241 Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 25 Sep 2015 23:07:20 +0530 Subject: [PATCH 002/128] Avoid running cell action if cached cell is available --- src/View/Cell.php | 33 +++++++++++++++++++------------- tests/TestCase/View/CellTest.php | 22 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/View/Cell.php b/src/View/Cell.php index 2af07c8512f..9e206d01f10 100644 --- a/src/View/Cell.php +++ b/src/View/Cell.php @@ -15,6 +15,7 @@ namespace Cake\View; use BadMethodCallException; +use Cake\Cache\Cache; use Cake\Datasource\ModelAwareTrait; use Cake\Event\EventDispatcherTrait; use Cake\Event\EventManager; @@ -169,6 +170,25 @@ public function __construct( */ public function render($template = null) { + if ($template !== null && + strpos($template, '/') === false && + strpos($template, '.') === false + ) { + $template = Inflector::underscore($template); + } + if ($template === null) { + $template = $this->template; + } + + $cache = []; + if ($this->_cache) { + $cache = $this->_cacheConfig($template); + $result = Cache::read($cache['key'], $cache['config']); + if ($result !== false) { + return $result; + } + } + try { $reflect = new ReflectionMethod($this, $this->action); $reflect->invokeArgs($this, $this->args); @@ -180,23 +200,10 @@ public function render($template = null) )); } - if ($template !== null && - strpos($template, '/') === false && - strpos($template, '.') === false - ) { - $template = Inflector::underscore($template); - } - if ($template === null) { - $template = $this->template; - } $builder = $this->viewBuilder(); $builder->layout(false); $builder->template($template); - $cache = []; - if ($this->_cache) { - $cache = $this->_cacheConfig($template); - } $this->View = $this->createView(); $render = function () use ($template) { diff --git a/tests/TestCase/View/CellTest.php b/tests/TestCase/View/CellTest.php index 76d335a532f..8ea68e662c4 100644 --- a/tests/TestCase/View/CellTest.php +++ b/tests/TestCase/View/CellTest.php @@ -315,6 +315,28 @@ public function testCachedRenderSimple() Cache::drop('default'); } + /** + * Test read cached cell. + * + * @return void + */ + public function testReadCachedCell() + { + $mock = $this->getMock('Cake\Cache\CacheEngine'); + $mock->method('init') + ->will($this->returnValue(true)); + $mock->method('read') + ->will($this->returnValue("dummy\n")); + $mock->expects($this->never()) + ->method('write'); + Cache::config('default', $mock); + + $cell = $this->View->cell('Articles', [], ['cache' => true]); + $result = $cell->render(); + $this->assertEquals("dummy\n", $result); + Cache::drop('default'); + } + /** * Test cached render array config * From c28af27a7d231cbaf5afd79673c7d26439df727b Mon Sep 17 00:00:00 2001 From: ADmad Date: Sat, 26 Sep 2015 12:52:07 +0530 Subject: [PATCH 003/128] Use action name instead of template to generate default cache key --- src/View/Cell.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/View/Cell.php b/src/View/Cell.php index 9e206d01f10..4dd4250eb6c 100644 --- a/src/View/Cell.php +++ b/src/View/Cell.php @@ -182,7 +182,7 @@ public function render($template = null) $cache = []; if ($this->_cache) { - $cache = $this->_cacheConfig($template); + $cache = $this->_cacheConfig($this->action); $result = Cache::read($cache['key'], $cache['config']); if ($result !== false) { return $result; @@ -229,17 +229,17 @@ public function render($template = null) /** * Generate the cache key to use for this cell. * - * If the key is undefined, the cell class and template will be used. + * If the key is undefined, the cell class and action name will be used. * - * @param string $template The template being rendered. + * @param string $action The action invoked. * @return array The cache configuration. */ - protected function _cacheConfig($template) + protected function _cacheConfig($action) { if (empty($this->_cache)) { return []; } - $key = 'cell_' . Inflector::underscore(get_class($this)) . '_' . $template; + $key = 'cell_' . Inflector::underscore(get_class($this)) . '_' . $action; $key = str_replace('\\', '_', $key); $default = [ 'config' => 'default', From 7a8a70a2f4efa39d50d293541f78484b8fe24fa7 Mon Sep 17 00:00:00 2001 From: ADmad Date: Sat, 26 Sep 2015 12:54:33 +0530 Subject: [PATCH 004/128] Include action and args in debug info. --- src/View/Cell.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/View/Cell.php b/src/View/Cell.php index 4dd4250eb6c..e0b0ec09599 100644 --- a/src/View/Cell.php +++ b/src/View/Cell.php @@ -280,6 +280,8 @@ public function __debugInfo() { return [ 'plugin' => $this->plugin, + 'action' => $this->action, + 'arg' => $this->args, 'template' => $this->template, 'viewClass' => $this->viewClass, 'request' => $this->request, From 546b5a4796ef44b3eced26e155dadee97851aa9a Mon Sep 17 00:00:00 2001 From: ADmad Date: Sun, 27 Sep 2015 00:57:29 +0530 Subject: [PATCH 005/128] Fix typo --- src/View/Cell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/Cell.php b/src/View/Cell.php index e0b0ec09599..a371c86d70f 100644 --- a/src/View/Cell.php +++ b/src/View/Cell.php @@ -281,7 +281,7 @@ public function __debugInfo() return [ 'plugin' => $this->plugin, 'action' => $this->action, - 'arg' => $this->args, + 'args' => $this->args, 'template' => $this->template, 'viewClass' => $this->viewClass, 'request' => $this->request, From 60c0ab36ac4034d0545a08ea0ad485a6193f56c9 Mon Sep 17 00:00:00 2001 From: ADmad Date: Sun, 27 Sep 2015 01:34:19 +0530 Subject: [PATCH 006/128] Refactor to use Cache::remember(). This avoids checking for cached data twice. --- src/View/Cell.php | 61 +++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/src/View/Cell.php b/src/View/Cell.php index a371c86d70f..7f91032689b 100644 --- a/src/View/Cell.php +++ b/src/View/Cell.php @@ -170,47 +170,42 @@ public function __construct( */ public function render($template = null) { - if ($template !== null && - strpos($template, '/') === false && - strpos($template, '.') === false - ) { - $template = Inflector::underscore($template); - } - if ($template === null) { - $template = $this->template; - } - $cache = []; if ($this->_cache) { $cache = $this->_cacheConfig($this->action); - $result = Cache::read($cache['key'], $cache['config']); - if ($result !== false) { - return $result; - } - } - - try { - $reflect = new ReflectionMethod($this, $this->action); - $reflect->invokeArgs($this, $this->args); - } catch (ReflectionException $e) { - throw new BadMethodCallException(sprintf( - 'Class %s does not have a "%s" method.', - get_class($this), - $this->action - )); } - $builder = $this->viewBuilder(); - $builder->layout(false); - $builder->template($template); + $render = function () use ($template) { + if ($template !== null && + strpos($template, '/') === false && + strpos($template, '.') === false + ) { + $template = Inflector::underscore($template); + } + if ($template === null) { + $template = $this->template; + } - $this->View = $this->createView(); + $builder = $this->viewBuilder(); + $builder->layout(false); + $builder->template($template); - $render = function () use ($template) { $className = substr(strrchr(get_class($this), "\\"), 1); $name = substr($className, 0, -4); - $this->View->templatePath('Cell' . DS . $name); + $builder->templatePath('Cell' . DS . $name); + + try { + $reflect = new ReflectionMethod($this, $this->action); + $reflect->invokeArgs($this, $this->args); + } catch (ReflectionException $e) { + throw new BadMethodCallException(sprintf( + 'Class %s does not have a "%s" method.', + get_class($this), + $this->action + )); + } + $this->View = $this->createView(); try { return $this->View->render($template); } catch (MissingTemplateException $e) { @@ -219,9 +214,7 @@ public function render($template = null) }; if ($cache) { - return $this->View->cache(function () use ($render) { - echo $render(); - }, $cache); + return Cache::remember($cache['key'], $render, $cache['config']); } return $render(); } From 69febb1c0e53452f3d5ea2199d24d8053026d660 Mon Sep 17 00:00:00 2001 From: ADmad Date: Sat, 24 Oct 2015 11:44:44 +0530 Subject: [PATCH 007/128] Update minimum PHP version requirement to 5.5 --- .travis.yml | 3 +-- composer.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index e784955f676..05760554f0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 5.4 - 5.5 - 5.6 - 7.0 @@ -24,7 +23,7 @@ matrix: fast_finish: true include: - - php: 5.4 + - php: 5.5 env: COVERALLS=1 DEFAULT=0 - php: 7.0 diff --git a/composer.json b/composer.json index 489465d5cd4..f1c0b2f4d50 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,10 @@ "source": "https://github.com/cakephp/cakephp" }, "require": { - "php": ">=5.4.16", + "php": ">=5.5.0", "ext-intl": "*", "ext-mbstring": "*", "nesbot/Carbon": "1.13.*", - "ircmaxell/password-compat": "1.0.*", "aura/intl": "1.1.*", "psr/log": "1.0" }, From cd00fa42c1a22dc7132ed305bb7be23e954b1f3e Mon Sep 17 00:00:00 2001 From: Dustin Haggard Date: Wed, 4 Nov 2015 14:01:07 -0500 Subject: [PATCH 008/128] Added initialize hook to base helper. Ref #7645 --- src/View/Helper.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/View/Helper.php b/src/View/Helper.php index 4cd9358ae12..ac2cffaafa9 100644 --- a/src/View/Helper.php +++ b/src/View/Helper.php @@ -124,6 +124,8 @@ public function __construct(View $View, array $config = []) if (!empty($this->helpers)) { $this->_helperMap = $View->helpers()->normalizeArray($this->helpers); } + + $this->initialize($config); } /** @@ -220,6 +222,18 @@ public function implementedEvents() return $events; } + /** + * Constructor hook method. + * + * Implement this method to avoid having to overwrite the constructor and call parent. + * + * @param array $config The configuration settings provided to this helper. + * @return void + */ + public function initialize(array $config) + { + } + /** * Returns an array that can be used to describe the internal state of this * object. From a60c4f06eece86fe3a962ce11f93ee9a4aa5a724 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 5 Nov 2015 22:53:40 -0500 Subject: [PATCH 009/128] Add temporary force to makefile. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 846a9afb5e3..a3ea0d4e123 100644 --- a/Makefile +++ b/Makefile @@ -164,7 +164,7 @@ component-%: - (git branch -D $* 2> /dev/null) git checkout -b $* git filter-branch --prune-empty --subdirectory-filter src/$(shell php -r "echo ucfirst('$*');") -f $* - git push $* $*:$(CURRENT_BRANCH) + git push -f $* $*:$(CURRENT_BRANCH) git checkout $(CURRENT_BRANCH) > /dev/null tag-component-%: component-% guard-VERSION guard-GITHUB_USER From a4076bc8ba2debcb36069e8125e90be6ca300437 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 6 Nov 2015 09:47:54 -0500 Subject: [PATCH 010/128] Re-update the VERSION file. --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index fa7aa9b6445..fe1576234e6 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -16,4 +16,4 @@ // @license http://www.opensource.org/licenses/mit-license.php MIT License // +--------------------------------------------------------------------------------------------+ // //////////////////////////////////////////////////////////////////////////////////////////////////// -3.1.4 +3.2.0-dev From 637eb9e9441eff7cee8a63d12890037e889a9280 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 26 Oct 2015 16:10:09 -0400 Subject: [PATCH 011/128] Start integrating chronos. * Update composer.json * Remove methods defined in chronos. --- composer.json | 2 +- src/I18n/Time.php | 34 ++-------------------------------- 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/composer.json b/composer.json index f1c0b2f4d50..79f7b2b9913 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "php": ">=5.5.0", "ext-intl": "*", "ext-mbstring": "*", - "nesbot/Carbon": "1.13.*", + "cakephp/chronos": "~1.0", "aura/intl": "1.1.*", "psr/log": "1.0" }, diff --git a/src/I18n/Time.php b/src/I18n/Time.php index 3f4935ebfd6..26afa095213 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -14,7 +14,7 @@ */ namespace Cake\I18n; -use Carbon\Carbon; +use Cake\Chronos\Chronos; use DateTime; use DateTimeZone; use IntlDateFormatter; @@ -25,7 +25,7 @@ * formatting helpers * */ -class Time extends Carbon implements JsonSerializable +class Time extends Chronos implements JsonSerializable { /** @@ -158,36 +158,6 @@ public function nice($timezone = null, $locale = null) return $this->i18nFormat(static::$niceFormat, $timezone, $locale); } - /** - * Returns true if this object represents a date within the current week - * - * @return bool - */ - public function isThisWeek() - { - return static::now($this->getTimezone())->format('W o') == $this->format('W o'); - } - - /** - * Returns true if this object represents a date within the current month - * - * @return bool - */ - public function isThisMonth() - { - return static::now($this->getTimezone())->format('m Y') == $this->format('m Y'); - } - - /** - * Returns true if this object represents a date within the current year - * - * @return bool - */ - public function isThisYear() - { - return static::now($this->getTimezone())->format('Y') == $this->format('Y'); - } - /** * Returns the quarter * From fb625162844245e9eb2389b00261513e8b58960f Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 27 Oct 2015 22:53:24 -0400 Subject: [PATCH 012/128] Start to integrate Chronos. Extract a DateFormatTrait as soon there will be an Cake\I18n\Date class to provide Time improvements for Date columns. --- composer.json | 2 +- src/I18n/DateFormatTrait.php | 351 ++++++++++++++++++++++++++ src/I18n/Time.php | 421 +------------------------------ tests/TestCase/I18n/TimeTest.php | 166 ------------ tests/bootstrap.php | 8 +- 5 files changed, 363 insertions(+), 585 deletions(-) create mode 100644 src/I18n/DateFormatTrait.php diff --git a/composer.json b/composer.json index 79f7b2b9913..5aa3ac584b8 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "php": ">=5.5.0", "ext-intl": "*", "ext-mbstring": "*", - "cakephp/chronos": "~1.0", + "cakephp/chronos": "*", "aura/intl": "1.1.*", "psr/log": "1.0" }, diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php new file mode 100644 index 00000000000..3575ed2c15c --- /dev/null +++ b/src/I18n/DateFormatTrait.php @@ -0,0 +1,351 @@ +i18nFormat(static::$niceFormat, $timezone, $locale); + } + + /** + * Returns a formatted string for this time object using the preferred format and + * language for the specified locale. + * + * It is possible to specify the desired format for the string to be displayed. + * You can either pass `IntlDateFormatter` constants as the first argument of this + * function, or pass a full ICU date formatting string as specified in the following + * resource: http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details. + * + * ### Examples + * + * ``` + * $time = new Time('2014-04-20 22:10'); + * $time->i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale + * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format + * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format + * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10' + * ``` + * + * If you wish to control the default format to be used for this method, you can alter + * the value of the static `Time::$defaultLocale` variable and set it to one of the + * possible formats accepted by this function. + * + * You can read about the available IntlDateFormatter constants at + * http://www.php.net/manual/en/class.intldateformatter.php + * + * If you need to display the date in a different timezone than the one being used for + * this Time object without altering its internal state, you can pass a timezone + * string or object as the second parameter. + * + * Finally, should you need to use a different locale for displaying this time object, + * pass a locale string as the third parameter to this function. + * + * ### Examples + * + * ``` + * $time = new Time('2014-04-20 22:10'); + * $time->i18nFormat(null, null, 'de-DE'); + * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE'); + * ``` + * + * You can control the default locale to be used by setting the static variable + * `Time::$defaultLocale` to a valid locale string. If empty, the default will be + * taken from the `intl.default_locale` ini config. + * + * @param string|int|null $format Format string. + * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object + * in which the date will be displayed. The timezone stored for this object will not + * be changed. + * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR) + * @return string Formatted and translated date string + */ + public function i18nFormat($format = null, $timezone = null, $locale = null) + { + $time = $this; + + if ($timezone) { + $time = $time->timezone($timezone); + } + + $format = $format !== null ? $format : static::$_toStringFormat; + $locale = $locale ?: static::$defaultLocale; + return $this->_formatObject($time, $format, $locale); + } + + /** + * Returns a translated and localized date string. + * Implements what IntlDateFormatter::formatObject() is in PHP 5.5+ + * + * @param \DateTime $date Date. + * @param string|int|array $format Format. + * @param string $locale The locale name in which the date should be displayed. + * @return string + */ + protected function _formatObject($date, $format, $locale) + { + $pattern = $dateFormat = $timeFormat = $calendar = null; + + if (is_array($format)) { + list($dateFormat, $timeFormat) = $format; + } elseif (is_numeric($format)) { + $dateFormat = $format; + } else { + $dateFormat = $timeFormat = IntlDateFormatter::FULL; + $pattern = $format; + } + + if (preg_match('/@calendar=(japanese|buddhist|chinese|persian|indian|islamic|hebrew|coptic|ethiopic)/', $locale)) { + $calendar = IntlDateFormatter::TRADITIONAL; + } else { + $calendar = IntlDateFormatter::GREGORIAN; + } + + $timezone = $date->getTimezone()->getName(); + $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}"; + + if (!isset(static::$_formatters[$key])) { + if ($timezone === '+00:00') { + $timezone = 'UTC'; + } elseif ($timezone[0] === '+' || $timezone[0] === '-') { + $timezone = 'GMT' . $timezone; + } + static::$_formatters[$key] = datefmt_create( + $locale, + $dateFormat, + $timeFormat, + $timezone, + $calendar, + $pattern + ); + } + + return static::$_formatters[$key]->format($date); + } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->i18nFormat(); + } + + /** + * Resets the format used to the default when converting an instance of this type to + * a string + * + * @return void + */ + public static function resetToStringFormat() + { + static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]); + } + + /** + * Sets the default format used when type converting instances of this type to string + * + * @param string|array|int $format Format. + * @return void + */ + public static function setToStringFormat($format) + { + static::$_toStringFormat = $format; + } + + /** + * Sets the default format used when converting this object to json + * + * @param string|array|int $format Format. + * @return void + */ + public static function setJsonEncodeFormat($format) + { + static::$_jsonEncodeFormat = $format; + } + + /** + * Returns a new Time object after parsing the provided time string based on + * the passed or configured date time format. This method is locale dependent, + * Any string that is passed to this function will be interpreted as a locale + * dependent string. + * + * When no $format is provided, the `toString` format will be used. + * + * If it was impossible to parse the provided time, null will be returned. + * + * Example: + * + * ``` + * $time = Time::parseDateTime('10/13/2013 12:54am'); + * $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm'); + * $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, -1]); + * ``` + * + * @param string $time The time string to parse. + * @param string|array $format Any format accepted by IntlDateFormatter. + * @return static|null + */ + public static function parseDateTime($time, $format = null) + { + $dateFormat = $format ?: static::$_toStringFormat; + $timeFormat = $pattern = null; + + if (is_array($dateFormat)) { + list($newDateFormat, $timeFormat) = $dateFormat; + $dateFormat = $newDateFormat; + } else { + $pattern = $dateFormat; + $dateFormat = null; + } + + $formatter = datefmt_create( + static::$defaultLocale, + $dateFormat, + $timeFormat, + date_default_timezone_get(), + null, + $pattern + ); + $time = $formatter->parse($time); + if ($time) { + $result = new static('@' . $time); + $result->setTimezone(date_default_timezone_get()); + return $result; + } + return null; + } + + /** + * Returns a new Time object after parsing the provided $date string based on + * the passed or configured date time format. This method is locale dependent, + * Any string that is passed to this function will be interpreted as a locale + * dependent string. + * + * When no $format is provided, the `wordFormat` format will be used. + * + * If it was impossible to parse the provided time, null will be returned. + * + * Example: + * + * ``` + * $time = Time::parseDate('10/13/2013'); + * $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y'); + * $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT); + * ``` + * + * @param string $date The date string to parse. + * @param string|int $format Any format accepted by IntlDateFormatter. + * @return static|null + */ + public static function parseDate($date, $format = null) + { + if (is_int($format)) { + $format = [$format, -1]; + } + $format = $format ?: static::$wordFormat; + return static::parseDateTime($date, $format); + } + + /** + * Returns a new Time object after parsing the provided $time string based on + * the passed or configured date time format. This method is locale dependent, + * Any string that is passed to this function will be interpreted as a locale + * dependent string. + * + * When no $format is provided, the IntlDateFormatter::SHORT format will be used. + * + * If it was impossible to parse the provided time, null will be returned. + * + * Example: + * + * ``` + * $time = Time::parseTime('11:23pm'); + * ``` + * + * @param string $time The time string to parse. + * @param string|int $format Any format accepted by IntlDateFormatter. + * @return static|null + */ + public static function parseTime($time, $format = null) + { + if (is_int($format)) { + $format = [-1, $format]; + } + $format = $format ?: [-1, IntlDateFormatter::SHORT]; + return static::parseDateTime($time, $format); + } + + /** + * Returns a string that should be serialized when converting this object to json + * + * @return string + */ + public function jsonSerialize() + { + return $this->i18nFormat(static::$_jsonEncodeFormat); + } + + /** + * Returns the data that should be displayed when debugging this object + * + * @return array + */ + public function __debugInfo() + { + return [ + 'time' => $this->format(DateTime::ISO8601), + 'timezone' => $this->getTimezone()->getName(), + 'fixedNowTime' => $this->hasTestNow() ? $this->getTestNow()->format(DateTime::ISO8601) : false + ]; + } +} diff --git a/src/I18n/Time.php b/src/I18n/Time.php index 26afa095213..5036fd5aea3 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -27,6 +27,7 @@ */ class Time extends Chronos implements JsonSerializable { + use DateFormatTrait; /** * The format to use when formatting a time using `Cake\I18n\Time::i18nFormat()` @@ -45,22 +46,6 @@ class Time extends Chronos implements JsonSerializable */ protected static $_toStringFormat = [IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]; - /** - * The format to use when when converting this object to json - * - * The format should be either the formatting constants from IntlDateFormatter as - * described in (http://www.php.net/manual/en/class.intldateformatter.php) or a pattern - * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details) - * - * It is possible to provide an array of 2 constants. In this case, the first position - * will be used for formatting the date part of the object and the second position - * will be used to format the time part. - * - * @var string|array|int - * @see \Cake\I18n\Time::i18nFormat() - */ - protected static $_jsonEncodeFormat = "yyyy-MM-dd'T'HH:mm:ssZ"; - /** * The format to use when formatting a time using `Cake\I18n\Time::nice()` * @@ -142,58 +127,6 @@ public function __construct($time = null, $tz = null) parent::__construct($time, $tz); } - /** - * Returns a nicely formatted date string for this object. - * - * The format to be used is stored in the static property `Time::niceFormat`. - * - * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object - * in which the date will be displayed. The timezone stored for this object will not - * be changed. - * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR) - * @return string Formatted date string - */ - public function nice($timezone = null, $locale = null) - { - return $this->i18nFormat(static::$niceFormat, $timezone, $locale); - } - - /** - * Returns the quarter - * - * @param bool $range Range. - * @return mixed 1, 2, 3, or 4 quarter of year or array if $range true - */ - public function toQuarter($range = false) - { - $quarter = ceil($this->format('m') / 3); - if ($range === false) { - return $quarter; - } - - $year = $this->format('Y'); - switch ($quarter) { - case 1: - return [$year . '-01-01', $year . '-03-31']; - case 2: - return [$year . '-04-01', $year . '-06-30']; - case 3: - return [$year . '-07-01', $year . '-09-30']; - case 4: - return [$year . '-10-01', $year . '-12-31']; - } - } - - /** - * Returns a UNIX timestamp. - * - * @return string UNIX timestamp - */ - public function toUnixString() - { - return $this->format('U'); - } - /** * Returns either a relative or a formatted absolute date depending * on the difference between the current time and this object. @@ -262,8 +195,7 @@ public function timeAgoInWords(array $options = []) } if ($timezone) { - $time = clone $this; - $time->timezone($timezone); + $time = $time->timezone($timezone); } $now = $from->format('U'); @@ -430,181 +362,17 @@ public function timeAgoInWords(array $options = []) * See `Time::timeAgoInWords()` for a full list of options that can be passed * to this method. * - * @param \Carbon\Carbon|null $other the date to diff with + * @param \Cake\Chronos\Chronos|null $other the date to diff with * @param array $options options accepted by timeAgoInWords * @return string * @see Time::timeAgoInWords() */ - public function diffForHumans(Carbon $other = null, array $options = []) + public function diffForHumans(Chronos $other = null, array $options = []) { $options = ['from' => $other] + $options; return $this->timeAgoInWords($options); } - /** - * Returns true this instance happened within the specified interval - * - * @param string|int $timeInterval the numeric value with space then time type. - * Example of valid types: 6 hours, 2 days, 1 minute. - * @return bool - */ - public function wasWithinLast($timeInterval) - { - $tmp = str_replace(' ', '', $timeInterval); - if (is_numeric($tmp)) { - $timeInterval = $tmp . ' days'; - } - - $interval = new static('-' . $timeInterval); - $now = new static(); - - return $this >= $interval && $this <= $now; - } - - /** - * Returns true this instance will happen within the specified interval - * - * @param string|int $timeInterval the numeric value with space then time type. - * Example of valid types: 6 hours, 2 days, 1 minute. - * @return bool - */ - public function isWithinNext($timeInterval) - { - $tmp = str_replace(' ', '', $timeInterval); - if (is_numeric($tmp)) { - $timeInterval = $tmp . ' days'; - } - - $interval = new static('+' . $timeInterval); - $now = new static(); - - return $this <= $interval && $this >= $now; - } - - /** - * Returns a formatted string for this time object using the preferred format and - * language for the specified locale. - * - * It is possible to specify the desired format for the string to be displayed. - * You can either pass `IntlDateFormatter` constants as the first argument of this - * function, or pass a full ICU date formatting string as specified in the following - * resource: http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details. - * - * ### Examples - * - * ``` - * $time = new Time('2014-04-20 22:10'); - * $time->i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale - * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format - * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format - * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10' - * ``` - * - * If you wish to control the default format to be used for this method, you can alter - * the value of the static `Time::$defaultLocale` variable and set it to one of the - * possible formats accepted by this function. - * - * You can read about the available IntlDateFormatter constants at - * http://www.php.net/manual/en/class.intldateformatter.php - * - * If you need to display the date in a different timezone than the one being used for - * this Time object without altering its internal state, you can pass a timezone - * string or object as the second parameter. - * - * Finally, should you need to use a different locale for displaying this time object, - * pass a locale string as the third parameter to this function. - * - * ### Examples - * - * ``` - * $time = new Time('2014-04-20 22:10'); - * $time->i18nFormat(null, null, 'de-DE'); - * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE'); - * ``` - * - * You can control the default locale to be used by setting the static variable - * `Time::$defaultLocale` to a valid locale string. If empty, the default will be - * taken from the `intl.default_locale` ini config. - * - * @param string|int|null $format Format string. - * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object - * in which the date will be displayed. The timezone stored for this object will not - * be changed. - * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR) - * @return string Formatted and translated date string - */ - public function i18nFormat($format = null, $timezone = null, $locale = null) - { - $time = $this; - - if ($timezone) { - $time = clone $this; - $time->timezone($timezone); - } - - $format = $format !== null ? $format : static::$_toStringFormat; - $locale = $locale ?: static::$defaultLocale; - return $this->_formatObject($time, $format, $locale); - } - - /** - * Returns a translated and localized date string. - * Implements what IntlDateFormatter::formatObject() is in PHP 5.5+ - * - * @param \DateTime $date Date. - * @param string|int|array $format Format. - * @param string $locale The locale name in which the date should be displayed. - * @return string - */ - protected function _formatObject($date, $format, $locale) - { - $pattern = $dateFormat = $timeFormat = $calendar = null; - - if (is_array($format)) { - list($dateFormat, $timeFormat) = $format; - } elseif (is_numeric($format)) { - $dateFormat = $format; - } else { - $dateFormat = $timeFormat = IntlDateFormatter::FULL; - $pattern = $format; - } - - if (preg_match('/@calendar=(japanese|buddhist|chinese|persian|indian|islamic|hebrew|coptic|ethiopic)/', $locale)) { - $calendar = IntlDateFormatter::TRADITIONAL; - } else { - $calendar = IntlDateFormatter::GREGORIAN; - } - - $timezone = $date->getTimezone()->getName(); - $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}"; - - if (!isset(static::$_formatters[$key])) { - if ($timezone === '+00:00') { - $timezone = 'UTC'; - } elseif ($timezone[0] === '+' || $timezone[0] === '-') { - $timezone = 'GMT' . $timezone; - } - static::$_formatters[$key] = datefmt_create( - $locale, - $dateFormat, - $timeFormat, - $timezone, - $calendar, - $pattern - ); - } - - return static::$_formatters[$key]->format($date); - } - - /** - * {@inheritDoc} - */ - public function __toString() - { - return $this->i18nFormat(); - } - /** * Get list of timezone identifiers * @@ -679,185 +447,4 @@ public static function listTimezones($filter = null, $country = null, $options = } return array_combine($identifiers, $identifiers); } - - /** - * Resets the format used to the default when converting an instance of this type to - * a string - * - * @return void - */ - public static function resetToStringFormat() - { - static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]); - } - - /** - * Sets the default format used when type converting instances of this type to string - * - * @param string|array|int $format Format. - * @return void - */ - public static function setToStringFormat($format) - { - static::$_toStringFormat = $format; - } - - /** - * Sets the default format used when converting this object to json - * - * @param string|array|int $format Format. - * @return void - */ - public static function setJsonEncodeFormat($format) - { - static::$_jsonEncodeFormat = $format; - } - - /** - * Returns a new Time object after parsing the provided time string based on - * the passed or configured date time format. This method is locale dependent, - * Any string that is passed to this function will be interpreted as a locale - * dependent string. - * - * When no $format is provided, the `toString` format will be used. - * - * If it was impossible to parse the provided time, null will be returned. - * - * Example: - * - * ``` - * $time = Time::parseDateTime('10/13/2013 12:54am'); - * $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm'); - * $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, -1]); - * ``` - * - * @param string $time The time string to parse. - * @param string|array $format Any format accepted by IntlDateFormatter. - * @return static|null - */ - public static function parseDateTime($time, $format = null) - { - $dateFormat = $format ?: static::$_toStringFormat; - $timeFormat = $pattern = null; - - if (is_array($dateFormat)) { - list($newDateFormat, $timeFormat) = $dateFormat; - $dateFormat = $newDateFormat; - } else { - $pattern = $dateFormat; - $dateFormat = null; - } - - $formatter = datefmt_create( - static::$defaultLocale, - $dateFormat, - $timeFormat, - date_default_timezone_get(), - null, - $pattern - ); - $time = $formatter->parse($time); - if ($time) { - $result = new static('@' . $time); - $result->setTimezone(date_default_timezone_get()); - return $result; - } - return null; - } - - /** - * Returns a new Time object after parsing the provided $date string based on - * the passed or configured date time format. This method is locale dependent, - * Any string that is passed to this function will be interpreted as a locale - * dependent string. - * - * When no $format is provided, the `wordFormat` format will be used. - * - * If it was impossible to parse the provided time, null will be returned. - * - * Example: - * - * ``` - * $time = Time::parseDate('10/13/2013'); - * $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y'); - * $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT); - * ``` - * - * @param string $date The date string to parse. - * @param string|int $format Any format accepted by IntlDateFormatter. - * @return static|null - */ - public static function parseDate($date, $format = null) - { - if (is_int($format)) { - $format = [$format, -1]; - } - $format = $format ?: static::$wordFormat; - return static::parseDateTime($date, $format); - } - - /** - * Returns a new Time object after parsing the provided $time string based on - * the passed or configured date time format. This method is locale dependent, - * Any string that is passed to this function will be interpreted as a locale - * dependent string. - * - * When no $format is provided, the IntlDateFormatter::SHORT format will be used. - * - * If it was impossible to parse the provided time, null will be returned. - * - * Example: - * - * ``` - * $time = Time::parseDate('11:23pm'); - * ``` - * - * @param string $time The time string to parse. - * @param string|int $format Any format accepted by IntlDateFormatter. - * @return static|null - */ - public static function parseTime($time, $format = null) - { - if (is_int($format)) { - $format = [-1, $format]; - } - $format = $format ?: [-1, IntlDateFormatter::SHORT]; - return static::parseDateTime($time, $format); - } - - /** - * Convenience method for getting the remaining time from a given time. - * - * @param \DateTime|\DateTimeImmutable $datetime The date to get the remaining time from. - * @return \DateInterval|bool The DateInterval object representing the difference between the two dates or FALSE on failure. - */ - public static function fromNow($datetime) - { - $timeNow = new Time(); - return $timeNow->diff($datetime); - } - - /** - * Returns a string that should be serialized when converting this object to json - * - * @return string - */ - public function jsonSerialize() - { - return $this->i18nFormat(static::$_jsonEncodeFormat); - } - - /** - * Returns the data that should be displayed when debugging this object - * - * @return array - */ - public function __debugInfo() - { - return [ - 'time' => $this->format(static::ISO8601), - 'timezone' => $this->getTimezone()->getName(), - 'fixedNowTime' => $this->hasTestNow() ? $this->getTestNow()->format(static::ISO8601) : false - ]; - } } diff --git a/tests/TestCase/I18n/TimeTest.php b/tests/TestCase/I18n/TimeTest.php index ceea72299fc..3b6d8b214a8 100644 --- a/tests/TestCase/I18n/TimeTest.php +++ b/tests/TestCase/I18n/TimeTest.php @@ -1,7 +1,5 @@ _systemTimezoneIdentifier); } - /** - * Provides values and expectations for the toQuarter method - * - * @return array - */ - public function toQuarterProvider() - { - return [ - ['2007-12-25', 4], - ['2007-9-25', 3], - ['2007-3-25', 1], - ['2007-3-25', ['2007-01-01', '2007-03-31'], true], - ['2007-5-25', ['2007-04-01', '2007-06-30'], true], - ['2007-8-25', ['2007-07-01', '2007-09-30'], true], - ['2007-12-25', ['2007-10-01', '2007-12-31'], true], - ]; - } - - /** - * testToQuarter method - * - * @dataProvider toQuarterProvider - * @return void - */ - public function testToQuarter($date, $expected, $range = false) - { - $this->assertEquals($expected, (new Time($date))->toQuarter($range)); - } - /** * provider for timeAgoInWords() tests * @@ -393,124 +361,6 @@ public function testNice() $this->assertTimeFormat('20 avr. 2014 16:00', $time->nice('America/New_York', 'fr-FR')); } - /** - * testToUnix method - * - * @return void - */ - public function testToUnix() - { - $time = new Time('2014-04-20 08:00:00'); - $this->assertEquals('1397980800', $time->toUnixString()); - - $time = new Time('2021-12-11 07:00:01'); - $this->assertEquals('1639206001', $time->toUnixString()); - } - - /** - * testIsThisWeek method - * - * @return void - */ - public function testIsThisWeek() - { - $time = new Time('this sunday'); - $this->assertTrue($time->isThisWeek()); - - $this->assertTrue($time->modify('-1 day')->isThisWeek()); - $this->assertFalse($time->modify('-6 days')->isThisWeek()); - - $time = new Time(); - $time->year = $time->year - 1; - $this->assertFalse($time->isThisWeek()); - } - - /** - * testIsThisMonth method - * - * @return void - */ - public function testIsThisMonth() - { - $time = new Time(); - $this->assertTrue($time->isThisMonth()); - - $time->year = $time->year + 1; - $this->assertFalse($time->isThisMonth()); - - $time = new Time(); - $this->assertFalse($time->modify('next month')->isThisMonth()); - } - - /** - * testIsThisYear method - * - * @return void - */ - public function testIsThisYear() - { - $time = new Time(); - $this->assertTrue($time->isThisYear()); - - $time->year = $time->year + 1; - $this->assertFalse($time->isThisYear()); - - $thisYear = date('Y'); - $time = new Time("$thisYear-01-01 00:00", 'Australia/Sydney'); - - $now = clone $time; - $now->timezone('UTC'); - Time::setTestNow($now); - $this->assertFalse($time->isThisYear()); - } - - /** - * testWasWithinLast method - * - * @return void - */ - public function testWasWithinLast() - { - $this->assertTrue((new Time('-1 day'))->wasWithinLast('1 day')); - $this->assertTrue((new Time('-1 week'))->wasWithinLast('1 week')); - $this->assertTrue((new Time('-1 year'))->wasWithinLast('1 year')); - $this->assertTrue((new Time('-1 second'))->wasWithinLast('1 second')); - $this->assertTrue((new Time('-1 day'))->wasWithinLast('1 week')); - $this->assertTrue((new Time('-1 week'))->wasWithinLast('2 week')); - $this->assertTrue((new Time('-1 second'))->wasWithinLast('10 minutes')); - $this->assertTrue((new Time('-1 month'))->wasWithinLast('13 month')); - $this->assertTrue((new Time('-1 seconds'))->wasWithinLast('1 hour')); - - $this->assertFalse((new Time('-1 year'))->wasWithinLast('1 second')); - $this->assertFalse((new Time('-1 year'))->wasWithinLast('0 year')); - $this->assertFalse((new Time('-1 weeks'))->wasWithinLast('1 day')); - - $this->assertTrue((new Time('-3 days'))->wasWithinLast('5')); - } - - /** - * testWasWithinLast method - * - * @return void - */ - public function testIsWithinNext() - { - $this->assertFalse((new Time('-1 day'))->isWithinNext('1 day')); - $this->assertFalse((new Time('-1 week'))->isWithinNext('1 week')); - $this->assertFalse((new Time('-1 year'))->isWithinNext('1 year')); - $this->assertFalse((new Time('-1 second'))->isWithinNext('1 second')); - $this->assertFalse((new Time('-1 day'))->isWithinNext('1 week')); - $this->assertFalse((new Time('-1 week'))->isWithinNext('2 week')); - $this->assertFalse((new Time('-1 second'))->isWithinNext('10 minutes')); - $this->assertFalse((new Time('-1 month'))->isWithinNext('13 month')); - $this->assertFalse((new Time('-1 seconds'))->isWithinNext('1 hour')); - - $this->assertTrue((new Time('+1 day'))->isWithinNext('1 day')); - $this->assertTrue((new Time('+1 week'))->isWithinNext('7 day')); - $this->assertTrue((new Time('+1 second'))->isWithinNext('1 minute')); - $this->assertTrue((new Time('+1 month'))->isWithinNext('1 month')); - } - /** * test formatting dates taking in account preferred i18n locale file * @@ -843,22 +693,6 @@ public function testParseDateDifferentTimezone() $this->assertEquals(new \DateTimeZone('Europe/Paris'), $result->tz); } - /** - * Tests the "from now" time calculation. - * - * @return void - */ - public function testFromNow() - { - $date = clone $this->now; - $date->modify('-1 year'); - $date->modify('-6 days'); - $date->modify('-51 seconds'); - $interval = Time::fromNow($date); - $result = $interval->format("%y %m %d %H %i %s"); - $this->assertEquals($result, '1 0 6 00 0 51'); - } - /** * Custom assert to allow for variation in the version of the intl library, where * some translations contain a few extra commas. diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3a1b5bbe6d9..a686f5ec32a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -101,6 +101,11 @@ ]); Log::config([ + // 'queries' => [ + // 'className' => 'Console', + // 'stream' => 'php://stderr', + // 'scopes' => ['queriesLog'] + // ], 'debug' => [ 'engine' => 'Cake\Log\Engine\FileLog', 'levels' => ['notice', 'info', 'debug'], @@ -113,6 +118,7 @@ ] ]); -Carbon\Carbon::setTestNow(Carbon\Carbon::now()); +Cake\Chronos\Chronos::setTestNow(Cake\Chronos\Chronos::now()); +Cake\Chronos\Date::setTestNow(Cake\Chronos\Date::now()); ini_set('intl.default_locale', 'en_US'); From 0eca8416955206022915076c5ec1ac456bf636c0 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 27 Oct 2015 23:28:25 -0400 Subject: [PATCH 013/128] Move additional code into the shared trait. --- src/I18n/DateFormatTrait.php | 18 +++++++++++++++--- src/I18n/Time.php | 15 --------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index 3575ed2c15c..d89db14d287 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -24,6 +24,19 @@ */ trait DateFormatTrait { + /** + * The default locale to be used for displaying formatted date strings. + * + * @var string + */ + public static $defaultLocale; + + /** + * In-memory cache of date formatters + * + * @var array + */ + protected static $_formatters = []; /** * The format to use when when converting this object to json @@ -169,7 +182,7 @@ protected function _formatObject($date, $format, $locale) ); } - return static::$_formatters[$key]->format($date); + return static::$_formatters[$key]->format($date->format('U')); } /** @@ -259,8 +272,7 @@ public static function parseDateTime($time, $format = null) $time = $formatter->parse($time); if ($time) { $result = new static('@' . $time); - $result->setTimezone(date_default_timezone_get()); - return $result; + return $result->setTimezone(date_default_timezone_get()); } return null; } diff --git a/src/I18n/Time.php b/src/I18n/Time.php index 5036fd5aea3..19c16f804cc 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -61,14 +61,6 @@ class Time extends Chronos implements JsonSerializable * @see \Cake\I18n\Time::nice() */ public static $niceFormat = [IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT]; - - /** - * The default locale to be used for displaying formatted date strings. - * - * @var string - */ - public static $defaultLocale; - /** * The format to use when formatting a time using `Cake\I18n\Time::timeAgoInWords()` * and the difference is more than `Cake\I18n\Time::$wordEnd` @@ -103,13 +95,6 @@ class Time extends Chronos implements JsonSerializable */ public static $wordEnd = '+1 month'; - /** - * In-memory cache of date formatters - * - * @var array - */ - protected static $_formatters = []; - /** * {@inheritDoc} */ From 77294ba789d1ea3a695f80580d2e89ee175687cf Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 27 Oct 2015 23:30:05 -0400 Subject: [PATCH 014/128] Start adding the Date wrapper. Add the I18n\Date class which adds the same i18n features to Dates that we offer on Time instances. The timeAgoInWords() method has not been implemented yet. --- src/I18n/Date.php | 62 +++++++++++++++++++++++++ tests/TestCase/I18n/DateTest.php | 79 ++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/I18n/Date.php create mode 100644 tests/TestCase/I18n/DateTest.php diff --git a/src/I18n/Date.php b/src/I18n/Date.php new file mode 100644 index 00000000000..2e4ffd275b0 --- /dev/null +++ b/src/I18n/Date.php @@ -0,0 +1,62 @@ +i18nFormat(); + $expected = '1/14/10'; + $this->assertEquals($expected, $result); + + $result = $time->i18nFormat(\IntlDateFormatter::FULL, null, 'es-ES'); + $expected = 'jueves, 14 de enero de 2010, 0:00:00 (GMT)'; + $this->assertEquals($expected, $result); + + $format = [\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT]; + $result = $time->i18nFormat($format); + $expected = '12:00 AM'; + $this->assertEquals($expected, $result); + + $result = $time->i18nFormat('HH:mm:ss', 'Australia/Sydney'); + $expected = '00:00:00'; + $this->assertEquals($expected, $result); + + Date::$defaultLocale = 'fr-FR'; + $result = $time->i18nFormat(\IntlDateFormatter::FULL); + $expected = 'jeudi 14 janvier 2010 00:00:00 UTC'; + $this->assertEquals($expected, $result); + + $result = $time->i18nFormat(\IntlDateFormatter::FULL, null, 'es-ES'); + $expected = 'jueves, 14 de enero de 2010, 0:00:00 (GMT)'; + $this->assertEquals($expected, $result, 'Default locale should not be used'); + } + + public function testToString() + { + $this->markTestIncomplete(); + } + + public function testJsonSerialize() + { + $this->markTestIncomplete(); + } + + public function testParseDate() + { + $this->markTestIncomplete(); + } + + public function testParseDateTime() + { + $this->markTestIncomplete(); + } +} From c6d973ff62900ef7e92007e6197d3c463d69df98 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 30 Oct 2015 22:24:35 -0400 Subject: [PATCH 015/128] Further integrate chronos into cakephp. Finish integration with the database abstraction layer, and database type mappers. The change to immutable datetimes may end up breaking some applications, but hopefully not many. --- src/Database/Type/DateTimeType.php | 9 ++++---- src/Database/Type/DateType.php | 8 ++++++- src/I18n/Date.php | 13 +++++++++-- src/I18n/Time.php | 1 + tests/TestCase/Database/Type/DateTypeTest.php | 22 +++++++++---------- tests/TestCase/Database/Type/TimeTypeTest.php | 2 +- .../ORM/Behavior/TimestampBehaviorTest.php | 2 +- tests/TestCase/ORM/MarshallerTest.php | 2 +- 8 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Database/Type/DateTimeType.php b/src/Database/Type/DateTimeType.php index 2eb8b155788..efee3636036 100644 --- a/src/Database/Type/DateTimeType.php +++ b/src/Database/Type/DateTimeType.php @@ -16,7 +16,8 @@ use Cake\Database\Driver; use Cake\Database\Type; -use DateTime; +use DateTimeInterface; +use DateTimeImmutable; use Exception; use RuntimeException; @@ -126,7 +127,7 @@ public function toPHP($value, Driver $driver) */ public function marshal($value) { - if ($value instanceof DateTime) { + if ($value instanceof DateTime || $value instanceof DateTimeImmutable) { return $value; } @@ -196,9 +197,7 @@ public function useLocaleParser($enable = true) $this->_useLocaleParser = $enable; return $this; } - if (static::$dateTimeClass === 'Cake\I18n\Time' || - is_subclass_of(static::$dateTimeClass, 'Cake\I18n\Time') - ) { + if (method_exists(static::$dateTimeClass, 'parseDateTime')) { $this->_useLocaleParser = $enable; return $this; } diff --git a/src/Database/Type/DateType.php b/src/Database/Type/DateType.php index ae53e7c0ee3..d008cd448c9 100644 --- a/src/Database/Type/DateType.php +++ b/src/Database/Type/DateType.php @@ -15,10 +15,16 @@ namespace Cake\Database\Type; use Cake\Database\Driver; -use DateTime; +use DateTimeImmutable; class DateType extends DateTimeType { + /** + * The class to use for representing date objects + * + * @var string + */ + public static $dateTimeClass = 'Cake\I18n\Date'; /** * Date format for DateTime object diff --git a/src/I18n/Date.php b/src/I18n/Date.php index 2e4ffd275b0..0b486a32ca7 100644 --- a/src/I18n/Date.php +++ b/src/I18n/Date.php @@ -40,10 +40,19 @@ class Date extends BaseDate implements JsonSerializable * will be used to format the time part. * * @var string|array|int - * @see \Cake\I18n\Time::i18nFormat() + * @see \Cake\I18n\DateFormatTrait::i18nFormat() */ protected static $_toStringFormat = [IntlDateFormatter::SHORT, -1]; + /** + * The format to use when formatting a time using `Cake\I18n\Time::timeAgoInWords()` + * and the difference is more than `Cake\I18n\Time::$wordEnd` + * + * @var string + * @see \Cake\I18n\DateFormatTrait::parseDate() + */ + public static $wordFormat = [IntlDateFormatter::SHORT, -1]; + /** * The format to use when formatting a time using `Cake\I18n\Time::nice()` * @@ -56,7 +65,7 @@ class Date extends BaseDate implements JsonSerializable * will be used to format the time part. * * @var string|array|int - * @see \Cake\I18n\Time::nice() + * @see \Cake\I18n\DateFormatTrait::nice() */ public static $niceFormat = [IntlDateFormatter::MEDIUM, -1]; } diff --git a/src/I18n/Time.php b/src/I18n/Time.php index 19c16f804cc..d61b7c45aeb 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -61,6 +61,7 @@ class Time extends Chronos implements JsonSerializable * @see \Cake\I18n\Time::nice() */ public static $niceFormat = [IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT]; + /** * The format to use when formatting a time using `Cake\I18n\Time::timeAgoInWords()` * and the difference is more than `Cake\I18n\Time::$wordEnd` diff --git a/tests/TestCase/Database/Type/DateTypeTest.php b/tests/TestCase/Database/Type/DateTypeTest.php index bcb42684ebf..1c3ef4b0d37 100644 --- a/tests/TestCase/Database/Type/DateTypeTest.php +++ b/tests/TestCase/Database/Type/DateTypeTest.php @@ -14,6 +14,7 @@ */ namespace Cake\Test\TestCase\Database\Type; +use Cake\Chronos\Date; use Cake\Database\Type; use Cake\Database\Type\DateType; use Cake\I18n\Time; @@ -48,7 +49,7 @@ public function testToPHP() $this->assertNull($this->type->toPHP('0000-00-00', $this->driver)); $result = $this->type->toPHP('2001-01-04', $this->driver); - $this->assertInstanceOf('DateTime', $result); + $this->assertInstanceOf('DateTimeImmutable', $result); $this->assertEquals('2001', $result->format('Y')); $this->assertEquals('01', $result->format('m')); $this->assertEquals('04', $result->format('d')); @@ -81,8 +82,7 @@ public function testToDatabase() */ public function marshalProvider() { - $date = new Time('@1392387900'); - $date->setTime(0, 0, 0); + $date = new Date('@1392387900'); return [ // invalid types. @@ -98,7 +98,7 @@ public function marshalProvider() // valid string types ['1392387900', $date], [1392387900, $date], - ['2014-02-14', new Time('2014-02-14')], + ['2014-02-14', new Date('2014-02-14')], // valid array types [ @@ -107,7 +107,7 @@ public function marshalProvider() ], [ ['year' => 2014, 'month' => 2, 'day' => 14, 'hour' => 13, 'minute' => 14, 'second' => 15], - new Time('2014-02-14 00:00:00') + new Date('2014-02-14') ], [ [ @@ -115,7 +115,7 @@ public function marshalProvider() 'hour' => 1, 'minute' => 14, 'second' => 15, 'meridian' => 'am' ], - new Time('2014-02-14 00:00:00') + new Date('2014-02-14') ], [ [ @@ -123,30 +123,30 @@ public function marshalProvider() 'hour' => 1, 'minute' => 14, 'second' => 15, 'meridian' => 'pm' ], - new Time('2014-02-14 00:00:00') + new Date('2014-02-14') ], [ [ 'year' => 2014, 'month' => 2, 'day' => 14, ], - new Time('2014-02-14 00:00:00') + new Date('2014-02-14') ], // Invalid array types [ ['year' => 'farts', 'month' => 'derp'], - new Time(date('Y-m-d 00:00:00')) + new Date(date('Y-m-d')) ], [ ['year' => 'farts', 'month' => 'derp', 'day' => 'farts'], - new Time(date('Y-m-d 00:00:00')) + new Date(date('Y-m-d')) ], [ [ 'year' => '2014', 'month' => '02', 'day' => '14', 'hour' => 'farts', 'minute' => 'farts' ], - new Time('2014-02-14 00:00:00') + new Date('2014-02-14') ], ]; } diff --git a/tests/TestCase/Database/Type/TimeTypeTest.php b/tests/TestCase/Database/Type/TimeTypeTest.php index eb28f65919c..b02c2d7f90a 100644 --- a/tests/TestCase/Database/Type/TimeTypeTest.php +++ b/tests/TestCase/Database/Type/TimeTypeTest.php @@ -53,7 +53,7 @@ public function testToPHP() $this->assertEquals('15', $result->format('s')); $result = $this->type->toPHP('16:30:15', $this->driver); - $this->assertInstanceOf('DateTime', $result); + $this->assertInstanceOf('DateTimeImmutable', $result); $this->assertEquals('16', $result->format('H')); $this->assertEquals('30', $result->format('i')); $this->assertEquals('15', $result->format('s')); diff --git a/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php b/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php index 91f3e02a1e0..db35e01cff6 100644 --- a/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php +++ b/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php @@ -225,7 +225,7 @@ public function testGetTimestamp() $return = $this->Behavior->timestamp(); $this->assertInstanceOf( - 'DateTime', + 'DateTimeImmutable', $return, 'Should return a timestamp object' ); diff --git a/tests/TestCase/ORM/MarshallerTest.php b/tests/TestCase/ORM/MarshallerTest.php index 58c237655df..f47b8550e9d 100644 --- a/tests/TestCase/ORM/MarshallerTest.php +++ b/tests/TestCase/ORM/MarshallerTest.php @@ -2087,7 +2087,7 @@ public function testMergeComplexType() ]; $marshall = new Marshaller($this->comments); $result = $marshall->merge($entity, $data); - $this->assertInstanceOf('DateTime', $entity->created); + $this->assertInstanceOf('DateTimeImmutable', $entity->created); $this->assertEquals('2014-02-14', $entity->created->format('Y-m-d')); } From 8deb66f03c2151c78727a221e6f30bc16c77be28 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 1 Nov 2015 21:58:42 -0500 Subject: [PATCH 016/128] Accomodate immutable dates and use named formats. Use the proper ISO-8601 formats. --- src/I18n/DateFormatTrait.php | 4 ++-- tests/TestCase/Controller/Component/CookieComponentTest.php | 2 +- tests/TestCase/I18n/TimeTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index d89db14d287..2edf37e4670 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -355,9 +355,9 @@ public function jsonSerialize() public function __debugInfo() { return [ - 'time' => $this->format(DateTime::ISO8601), + 'time' => $this->toIso8601String(), 'timezone' => $this->getTimezone()->getName(), - 'fixedNowTime' => $this->hasTestNow() ? $this->getTestNow()->format(DateTime::ISO8601) : false + 'fixedNowTime' => $this->hasTestNow() ? $this->getTestNow()->toIso8601String() : false ]; } } diff --git a/tests/TestCase/Controller/Component/CookieComponentTest.php b/tests/TestCase/Controller/Component/CookieComponentTest.php index 9db61efd430..469191af711 100644 --- a/tests/TestCase/Controller/Component/CookieComponentTest.php +++ b/tests/TestCase/Controller/Component/CookieComponentTest.php @@ -294,7 +294,7 @@ public function testWriteFarFuture() $this->Cookie->configKey('Testing', 'expires', '+90 years'); $this->Cookie->write('Testing', 'value'); $future = new Time('now'); - $future->modify('+90 years'); + $future = $future->modify('+90 years'); $expected = [ 'name' => 'Testing', diff --git a/tests/TestCase/I18n/TimeTest.php b/tests/TestCase/I18n/TimeTest.php index 3b6d8b214a8..0aba4540ac5 100644 --- a/tests/TestCase/I18n/TimeTest.php +++ b/tests/TestCase/I18n/TimeTest.php @@ -604,9 +604,9 @@ public function testDebugInfo() { $time = new Time('2014-04-20 10:10:10'); $expected = [ - 'time' => '2014-04-20T10:10:10+0000', + 'time' => '2014-04-20T10:10:10+00:00', 'timezone' => 'UTC', - 'fixedNowTime' => Time::getTestNow()->toISO8601String() + 'fixedNowTime' => Time::getTestNow()->toIso8601String() ]; $this->assertEquals($expected, $time->__debugInfo()); } From 89ae92d5201130ec3fed665b802913a0a332141d Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 2 Nov 2015 20:49:38 -0500 Subject: [PATCH 017/128] Add backwards compatibility shims These methods are gross, but are required for backwards compatibility with previous implementations of TimeHelper. This behavior is now deprecated and will be removed in the next major release. --- src/I18n/Time.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/I18n/Time.php b/src/I18n/Time.php index d61b7c45aeb..0895e8a1058 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -433,4 +433,44 @@ public static function listTimezones($filter = null, $country = null, $options = } return array_combine($identifiers, $identifiers); } + + /** + * Returns true this instance will happen within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function wasWithinLast($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + $timeInterval = $tmp . ' days'; + } + return parent::wasWithinLast($timeInterval); + } + + /** + * Returns true this instance happened within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function isWithinNext($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + $timeInterval = $tmp . ' days'; + } + return parent::isWithinNext($timeInterval); + } } From 2ea2cf39e72d243b97f0fba42f85e55fb65e8f73 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 4 Nov 2015 21:50:18 -0500 Subject: [PATCH 018/128] Backup the locale property. Don't leave global state in a worse place than when we found it. --- tests/TestCase/Database/Type/DateTypeTest.php | 4 +-- tests/TestCase/I18n/DateTest.php | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/TestCase/Database/Type/DateTypeTest.php b/tests/TestCase/Database/Type/DateTypeTest.php index 1c3ef4b0d37..deb155e5e2a 100644 --- a/tests/TestCase/Database/Type/DateTypeTest.php +++ b/tests/TestCase/Database/Type/DateTypeTest.php @@ -175,7 +175,7 @@ public function testMarshal($value, $expected) public function testMarshalWithLocaleParsing() { $this->type->useLocaleParser(); - $expected = new Time('13-10-2013'); + $expected = new Date('13-10-2013'); $result = $this->type->marshal('10/13/2013'); $this->assertEquals($expected->format('Y-m-d'), $result->format('Y-m-d')); @@ -190,7 +190,7 @@ public function testMarshalWithLocaleParsing() public function testMarshalWithLocaleParsingWithFormat() { $this->type->useLocaleParser()->setLocaleFormat('dd MMM, y'); - $expected = new Time('13-10-2013'); + $expected = new Date('13-10-2013'); $result = $this->type->marshal('13 Oct, 2013'); $this->assertEquals($expected->format('Y-m-d'), $result->format('Y-m-d')); } diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index 6c3eef5775a..4eafd77fff3 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -22,6 +22,35 @@ */ class DateTest extends TestCase { + /** + * Backup the locale property + * + * @var string + */ + protected $locale; + + /** + * setup + * + * @return void + */ + public function setUp() + { + parent::setUp(); + $this->locale = Date::$defaultLocale; + } + + /** + * Teardown + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + Date::$defaultLocale = $this->locale; + } + /** * test formatting dates taking in account preferred i18n locale file * From cf1a848ad2590aac13e5bc4ac9350c10c34b2bbc Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 6 Nov 2015 10:04:17 -0500 Subject: [PATCH 019/128] Implement incomplete tests for I18n\Date. --- tests/TestCase/I18n/DateTest.php | 55 +++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index 4eafd77fff3..9fe0c699166 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -16,6 +16,7 @@ use Cake\I18n\Date; use Cake\TestSuite\TestCase; +use DateTimeZone; /** * DateTest class @@ -86,23 +87,69 @@ public function testI18nFormat() $this->assertEquals($expected, $result, 'Default locale should not be used'); } + /** + * test __toString + * + * @return void + */ public function testToString() { - $this->markTestIncomplete(); + $date = new Date('2015-11-06 11:32:45'); + $this->assertEquals('11/6/15', (string)$date); + } + + /** + * test nice() + * + * @return void + */ + public function testNice() + { + $date = new Date('2015-11-06 11:32:45'); + + $this->assertEquals('Nov 6, 2015', $date->nice()); + $this->assertEquals('Nov 6, 2015', $date->nice(new DateTimeZone('America/New_York'))); + $this->assertEquals('6 nov. 2015', $date->nice(null, 'fr-FR')); } + /** + * test jsonSerialize() + * + * @return void + */ public function testJsonSerialize() { - $this->markTestIncomplete(); + $date = new Date('2015-11-06 11:32:45'); + $this->assertEquals('"2015-11-06T00:00:00+0000"', json_encode($date)); } + /** + * test parseDate() + * + * @return void + */ public function testParseDate() { - $this->markTestIncomplete(); + $date = Date::parseDate('11/6/15'); + $this->assertEquals('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s')); + + Date::$defaultLocale = 'fr-FR'; + $date = Date::parseDate('13 10, 2015'); + $this->assertEquals('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s')); } + /** + * test parseDateTime() + * + * @return void + */ public function testParseDateTime() { - $this->markTestIncomplete(); + $date = Date::parseDate('11/6/15 12:33:12'); + $this->assertEquals('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s')); + + Date::$defaultLocale = 'fr-FR'; + $date = Date::parseDate('13 10, 2015 12:54:12'); + $this->assertEquals('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s')); } } From 1619c46cc3dffc52bc4facd9af3b9254cd7a89f1 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 6 Nov 2015 15:47:45 -0500 Subject: [PATCH 020/128] Implement timeAgoInWords() for Date. Duplicate many of the tests from Time, and remove tests related to time components. --- src/I18n/Date.php | 223 +++++++++++++++++++++++++ tests/TestCase/I18n/DateTest.php | 274 +++++++++++++++++++++++++++++++ 2 files changed, 497 insertions(+) diff --git a/src/I18n/Date.php b/src/I18n/Date.php index 0b486a32ca7..9fe9bbad069 100644 --- a/src/I18n/Date.php +++ b/src/I18n/Date.php @@ -68,4 +68,227 @@ class Date extends BaseDate implements JsonSerializable * @see \Cake\I18n\DateFormatTrait::nice() */ public static $niceFormat = [IntlDateFormatter::MEDIUM, -1]; + + /** + * The format to use when formatting a time using `Time::timeAgoInWords()` + * and the difference is less than `Time::$wordEnd` + * + * @var array + * @see \Cake\I18n\Date::timeAgoInWords() + */ + public static $wordAccuracy = [ + 'year' => "day", + 'month' => "day", + 'week' => "day", + 'day' => "day", + 'hour' => "day", + 'minute' => "day", + 'second' => "day", + ]; + + /** + * The end of relative time telling + * + * @var string + * @see \Cake\I18n\Date::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current date and this object. + * + * ### Options: + * + * - `from` => another Date object representing the "now" date + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "day") + * - `end` => The end of relative date telling + * - `relativeString` => The printf compatible string when outputting relative date + * - `absoluteString` => The printf compatible string when outputting absolute date + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 1 day ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings. + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day. + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + $date = $this; + + $options += [ + 'from' => static::now(), + 'timezone' => null, + 'format' => static::$wordFormat, + 'accuracy' => static::$wordAccuracy, + 'end' => static::$wordEnd, + 'relativeString' => __d('cake', '%s ago'), + 'absoluteString' => __d('cake', 'on %s'), + ]; + if (is_string($options['accuracy'])) { + foreach (static::$wordAccuracy as $key => $level) { + $options[$key] = $options['accuracy']; + } + } else { + $options['accuracy'] += static::$wordAccuracy; + } + if ($options['timezone']) { + $date = $date->timezone($options['timezone']); + } + + $now = $options['from']->format('U'); + $inSeconds = $date->format('U'); + $backwards = ($inSeconds > $now); + + $futureTime = $now; + $pastTime = $inSeconds; + if ($backwards) { + $futureTime = $inSeconds; + $pastTime = $now; + } + $diff = $futureTime - $pastTime; + + if (!$diff) { + return __d('cake', 'today'); + } + + if ($diff > abs($now - (new static($options['end']))->format('U'))) { + return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); + } + + // If more than a week, then take into account the length of months + if ($diff >= 604800) { + list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); + + list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime)); + $weeks = $days = $hours = $minutes = $seconds = 0; + + $years = $future['Y'] - $past['Y']; + $months = $future['m'] + ((12 * $years) - $past['m']); + + if ($months >= 12) { + $years = floor($months / 12); + $months = $months - ($years * 12); + } + if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { + $years--; + } + + if ($future['d'] >= $past['d']) { + $days = $future['d'] - $past['d']; + } else { + $daysInPastMonth = date('t', $pastTime); + $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); + + if (!$backwards) { + $days = ($daysInPastMonth - $past['d']) + $future['d']; + } else { + $days = ($daysInFutureMonth - $past['d']) + $future['d']; + } + + if ($future['m'] != $past['m']) { + $months--; + } + } + + if (!$months && $years >= 1 && $diff < ($years * 31536000)) { + $months = 11; + $years--; + } + + if ($months >= 12) { + $years = $years + 1; + $months = $months - 12; + } + + if ($days >= 7) { + $weeks = floor($days / 7); + $days = $days - ($weeks * 7); + } + } else { + $years = $months = $weeks = 0; + $days = floor($diff / 86400); + + $diff = $diff - ($days * 86400); + + $hours = floor($diff / 3600); + $diff = $diff - ($hours * 3600); + + $minutes = floor($diff / 60); + $diff = $diff - ($minutes * 60); + $seconds = $diff; + } + + $fWord = $options['accuracy']['day']; + if ($years > 0) { + $fWord = $options['accuracy']['year']; + } elseif (abs($months) > 0) { + $fWord = $options['accuracy']['month']; + } elseif (abs($weeks) > 0) { + $fWord = $options['accuracy']['week']; + } elseif (abs($days) > 0) { + $fWord = $options['accuracy']['day']; + } + + $fNum = str_replace(['year', 'month', 'week', 'day'], [1, 2, 3, 4], $fWord); + + $relativeDate = ''; + if ($fNum >= 1 && $years > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} year', '{0} years', $years, $years); + } + if ($fNum >= 2 && $months > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} month', '{0} months', $months, $months); + } + if ($fNum >= 3 && $weeks > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); + } + if ($fNum >= 4 && $days > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} day', '{0} days', $days, $days); + } + + // When time has passed + if (!$backwards && $relativeDate) { + return sprintf($options['relativeString'], $relativeDate); + } + if (!$backwards) { + $aboutAgo = [ + 'day' => __d('cake', 'about a day ago'), + 'week' => __d('cake', 'about a week ago'), + 'month' => __d('cake', 'about a month ago'), + 'year' => __d('cake', 'about a year ago') + ]; + + return $aboutAgo[$fWord]; + } + + // When time is to come + if (!$relativeDate) { + $aboutIn = [ + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'month' => __d('cake', 'in about a month'), + 'year' => __d('cake', 'in about a year') + ]; + + return $aboutIn[$fWord]; + } + + return $relativeDate; + } } diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index 9fe0c699166..4f26093ba49 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -152,4 +152,278 @@ public function testParseDateTime() $date = Date::parseDate('13 10, 2015 12:54:12'); $this->assertEquals('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s')); } + + /** + * provider for timeAgoInWords() tests + * + * @return array + */ + public static function timeAgoProvider() + { + return [ + ['-12 seconds', 'today'], + ['-12 minutes', 'today'], + ['-2 hours', 'today'], + ['-1 day', '1 day ago'], + ['-2 days', '2 days ago'], + ['-1 week', '1 week ago'], + ['-2 weeks -2 days', '2 weeks, 2 days ago'], + ['+1 second', 'today'], + ['+1 minute, +10 seconds', 'today'], + ['+1 week', '1 week'], + ['+1 week 1 day', '1 week, 1 day'], + ['+2 weeks 2 day', '2 weeks, 2 days'], + ['2007-9-24', 'on 9/24/07'], + ['now', 'today'], + ]; + } + + /** + * testTimeAgoInWords method + * + * @dataProvider timeAgoProvider + * @return void + */ + public function testTimeAgoInWords($input, $expected) + { + $date = new Date($input); + $result = $date->timeAgoInWords(); + $this->assertEquals($expected, $result); + } + + /** + * test the timezone option for timeAgoInWords + * + * @return void + */ + public function testTimeAgoInWordsTimezone() + { + $date = new Date('1990-07-31 20:33:00 UTC'); + $result = $date->timeAgoInWords( + [ + 'timezone' => 'America/Vancouver', + 'end' => '+1month', + 'format' => 'dd-MM-YYYY' + ] + ); + $this->assertEquals('on 31-07-1990', $result); + } + + /** + * provider for timeAgo with an end date. + * + * @return void + */ + public function timeAgoEndProvider() + { + return [ + [ + '+4 months +2 weeks +3 days', + '4 months, 2 weeks, 3 days', + '8 years' + ], + [ + '+4 months +2 weeks +1 day', + '4 months, 2 weeks, 1 day', + '8 years' + ], + [ + '+3 months +2 weeks', + '3 months, 2 weeks', + '8 years' + ], + [ + '+3 months +2 weeks +1 day', + '3 months, 2 weeks, 1 day', + '8 years' + ], + [ + '+1 months +1 week +1 day', + '1 month, 1 week, 1 day', + '8 years' + ], + [ + '+2 months +2 days', + '2 months, 2 days', + '+2 months +2 days' + ], + [ + '+2 months +12 days', + '2 months, 1 week, 5 days', + '3 months' + ], + ]; + } + + /** + * test the end option for timeAgoInWords + * + * @dataProvider timeAgoEndProvider + * @return void + */ + public function testTimeAgoInWordsEnd($input, $expected, $end) + { + $time = new Date($input); + $result = $time->timeAgoInWords(['end' => $end]); + $this->assertEquals($expected, $result); + } + + /** + * test the custom string options for timeAgoInWords + * + * @return void + */ + public function testTimeAgoInWordsCustomStrings() + { + $date = new Date('-8 years -4 months -2 weeks -3 days'); + $result = $date->timeAgoInWords([ + 'relativeString' => 'at least %s ago', + 'accuracy' => ['year' => 'year'], + 'end' => '+10 years' + ]); + $expected = 'at least 8 years ago'; + $this->assertEquals($expected, $result); + + $date = new Date('+4 months +2 weeks +3 days'); + $result = $date->timeAgoInWords([ + 'absoluteString' => 'exactly on %s', + 'accuracy' => ['year' => 'year'], + 'end' => '+2 months' + ]); + $expected = 'exactly on ' . date('n/j/y', strtotime('+4 months +2 weeks +3 days')); + $this->assertEquals($expected, $result); + } + + /** + * Test the accuracy option for timeAgoInWords() + * + * @return void + */ + public function testDateAgoInWordsAccuracy() + { + $date = new Date('+8 years +4 months +2 weeks +3 days'); + $result = $date->timeAgoInWords([ + 'accuracy' => ['year' => 'year'], + 'end' => '+10 years' + ]); + $expected = '8 years'; + $this->assertEquals($expected, $result); + + $date = new Date('+8 years +4 months +2 weeks +3 days'); + $result = $date->timeAgoInWords([ + 'accuracy' => ['year' => 'month'], + 'end' => '+10 years' + ]); + $expected = '8 years, 4 months'; + $this->assertEquals($expected, $result); + + $date = new Date('+8 years +4 months +2 weeks +3 days'); + $result = $date->timeAgoInWords([ + 'accuracy' => ['year' => 'week'], + 'end' => '+10 years' + ]); + $expected = '8 years, 4 months, 2 weeks'; + $this->assertEquals($expected, $result); + + $date = new Date('+8 years +4 months +2 weeks +3 days'); + $result = $date->timeAgoInWords([ + 'accuracy' => ['year' => 'day'], + 'end' => '+10 years' + ]); + $expected = '8 years, 4 months, 2 weeks, 3 days'; + $this->assertEquals($expected, $result); + + $date = new Date('+1 years +5 weeks'); + $result = $date->timeAgoInWords([ + 'accuracy' => ['year' => 'year'], + 'end' => '+10 years' + ]); + $expected = '1 year'; + $this->assertEquals($expected, $result); + + $date = new Date('+23 hours'); + $result = $date->timeAgoInWords([ + 'accuracy' => 'day' + ]); + $expected = 'today'; + $this->assertEquals($expected, $result); + } + + /** + * Test the format option of timeAgoInWords() + * + * @return void + */ + public function testDateAgoInWordsWithFormat() + { + $date = new Date('2007-9-25'); + $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); + $this->assertEquals('on 2007-09-25', $result); + + $date = new Date('2007-9-25'); + $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); + $this->assertEquals('on 2007-09-25', $result); + + $date = new Date('+2 weeks +2 days'); + $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); + $this->assertRegExp('/^2 weeks, [1|2] day(s)?$/', $result); + + $date = new Date('+2 months +2 days'); + $result = $date->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']); + $this->assertEquals('on ' . date('Y-m-d', strtotime('+2 months +2 days')), $result); + } + + /** + * test timeAgoInWords() with negative values. + * + * @return void + */ + public function testDateAgoInWordsNegativeValues() + { + $date = new Date('-2 months -2 days'); + $result = $date->timeAgoInWords(['end' => '3 month']); + $this->assertEquals('2 months, 2 days ago', $result); + + $date = new Date('-2 months -2 days'); + $result = $date->timeAgoInWords(['end' => '3 month']); + $this->assertEquals('2 months, 2 days ago', $result); + + $date = new Date('-2 months -2 days'); + $result = $date->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']); + $this->assertEquals('on ' . date('Y-m-d', strtotime('-2 months -2 days')), $result); + + $date = new Date('-2 years -5 months -2 days'); + $result = $date->timeAgoInWords(['end' => '3 years']); + $this->assertEquals('2 years, 5 months, 2 days ago', $result); + + $date = new Date('-2 weeks -2 days'); + $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); + $this->assertEquals('2 weeks, 2 days ago', $result); + + $date = new Date('-3 years -12 months'); + $result = $date->timeAgoInWords(); + $expected = 'on ' . $date->format('n/j/y'); + $this->assertEquals($expected, $result); + + $date = new Date('-1 month -1 week -6 days'); + $result = $date->timeAgoInWords( + ['end' => '1 year', 'accuracy' => ['month' => 'month']] + ); + $this->assertEquals('1 month ago', $result); + + $date = new Date('-1 years -2 weeks -3 days'); + $result = $date->timeAgoInWords( + ['accuracy' => ['year' => 'year']] + ); + $expected = 'on ' . $date->format('n/j/y'); + $this->assertEquals($expected, $result); + + $date = new Date('-13 months -5 days'); + $result = $date->timeAgoInWords(['end' => '2 years']); + $this->assertEquals('1 year, 1 month, 5 days ago', $result); + + $date = new Date('-23 hours'); + $result = $date->timeAgoInWords(['accuracy' => 'day']); + $this->assertEquals('today', $result); + } } From 8e51f90ac428d47821dfb7d1b1ed6b2b86541acc Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 6 Nov 2015 18:07:48 -0500 Subject: [PATCH 021/128] Remove duplicate test. --- tests/TestCase/I18n/TimeTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/TestCase/I18n/TimeTest.php b/tests/TestCase/I18n/TimeTest.php index 0aba4540ac5..8959c83af5a 100644 --- a/tests/TestCase/I18n/TimeTest.php +++ b/tests/TestCase/I18n/TimeTest.php @@ -272,10 +272,6 @@ public function testTimeAgoInWordsWithFormat() $result = $time->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertEquals('on 2007-09-25', $result); - $time = new Time('2007-9-25'); - $result = $time->timeAgoInWords(['format' => 'yyyy-MM-dd']); - $this->assertEquals('on 2007-09-25', $result); - $time = new Time('+2 weeks +2 days'); $result = $time->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertRegExp('/^2 weeks, [1|2] day(s)?$/', $result); From 64a0b117754788a6196fd17a22e97f0a677b3e1f Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 8 Nov 2015 23:45:29 +0100 Subject: [PATCH 022/128] Replacing QueryExpression::type() with tieWith() The old method name was confusing, and would become a barrier for understanding how the new data type conversions for expressions would work --- src/Database/Dialect/PostgresDialectTrait.php | 12 ++++++------ src/Database/Dialect/SqliteDialectTrait.php | 10 +++++----- src/Database/Dialect/SqlserverDialectTrait.php | 12 ++++++------ src/Database/Expression/QueryExpression.php | 17 +++++++++++++++-- src/Database/FunctionsBuilder.php | 4 ++-- src/Database/Query.php | 2 +- src/Datasource/QueryInterface.php | 2 +- 7 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Database/Dialect/PostgresDialectTrait.php b/src/Database/Dialect/PostgresDialectTrait.php index 4c501cca64d..813c8f5d8fa 100644 --- a/src/Database/Dialect/PostgresDialectTrait.php +++ b/src/Database/Dialect/PostgresDialectTrait.php @@ -103,12 +103,12 @@ protected function _transformFunctionExpression(FunctionExpression $expression) switch ($expression->name()) { case 'CONCAT': // CONCAT function is expressed as exp1 || exp2 - $expression->name('')->type(' ||'); + $expression->name('')->tieWith(' ||'); break; case 'DATEDIFF': $expression ->name('') - ->type('-') + ->tieWith('-') ->iterateParts(function ($p) { if (is_string($p)) { $p = ['value' => [$p => 'literal'], 'type' => null]; @@ -121,11 +121,11 @@ protected function _transformFunctionExpression(FunctionExpression $expression) break; case 'CURRENT_DATE': $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); - $expression->name('CAST')->type(' AS ')->add([$time, 'date' => 'literal']); + $expression->name('CAST')->tieWith(' AS ')->add([$time, 'date' => 'literal']); break; case 'CURRENT_TIME': $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']); - $expression->name('CAST')->type(' AS ')->add([$time, 'time' => 'literal']); + $expression->name('CAST')->tieWith(' AS ')->add([$time, 'time' => 'literal']); break; case 'NOW': $expression->name('LOCALTIMESTAMP')->add([' 0 ' => 'literal']); @@ -133,7 +133,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) case 'DATE_ADD': $expression ->name('') - ->type(' + INTERVAL') + ->tieWith(' + INTERVAL') ->iterateParts(function ($p, $key) { if ($key === 1) { $p = sprintf("'%s'", $p); @@ -144,7 +144,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) case 'DAYOFWEEK': $expression ->name('EXTRACT') - ->type(' ') + ->tieWith(' ') ->add(['DOW FROM' => 'literal'], [], true) ->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1 break; diff --git a/src/Database/Dialect/SqliteDialectTrait.php b/src/Database/Dialect/SqliteDialectTrait.php index 7db928e92a1..2376d618e65 100644 --- a/src/Database/Dialect/SqliteDialectTrait.php +++ b/src/Database/Dialect/SqliteDialectTrait.php @@ -95,12 +95,12 @@ protected function _transformFunctionExpression(FunctionExpression $expression) switch ($expression->name()) { case 'CONCAT': // CONCAT function is expressed as exp1 || exp2 - $expression->name('')->type(' ||'); + $expression->name('')->tieWith(' ||'); break; case 'DATEDIFF': $expression ->name('ROUND') - ->type('-') + ->tieWith('-') ->iterateParts(function ($p) { return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]); }); @@ -117,7 +117,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) case 'EXTRACT': $expression ->name('STRFTIME') - ->type(' ,') + ->tieWith(' ,') ->iterateParts(function ($p, $key) { if ($key === 0) { $value = rtrim(strtolower($p), 's'); @@ -131,7 +131,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) case 'DATE_ADD': $expression ->name('DATE') - ->type(',') + ->tieWith(',') ->iterateParts(function ($p, $key) { if ($key === 1) { $p = ['value' => $p, 'type' => null]; @@ -142,7 +142,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) case 'DAYOFWEEK': $expression ->name('STRFTIME') - ->type(' ') + ->tieWith(' ') ->add(["'%w', " => 'literal'], [], true) ->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1 break; diff --git a/src/Database/Dialect/SqlserverDialectTrait.php b/src/Database/Dialect/SqlserverDialectTrait.php index 94778c7f8bf..e842af4860a 100644 --- a/src/Database/Dialect/SqlserverDialectTrait.php +++ b/src/Database/Dialect/SqlserverDialectTrait.php @@ -154,10 +154,10 @@ protected function _transformDistinct($original) ->select(function ($q) use ($distinct, $order) { $over = $q->newExpr('ROW_NUMBER() OVER') ->add('(PARTITION BY') - ->add($q->newExpr()->add($distinct)->type(',')) + ->add($q->newExpr()->add($distinct)->tieWith(',')) ->add($order) ->add(')') - ->type(' '); + ->tieWith(' '); return [ '_cake_distinct_pivot_' => $over ]; @@ -210,7 +210,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) switch ($expression->name()) { case 'CONCAT': // CONCAT function is expressed as exp1 + exp2 - $expression->name('')->type(' +'); + $expression->name('')->tieWith(' +'); break; case 'DATEDIFF': $hasDay = false; @@ -238,7 +238,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) $expression->name('GETUTCDATE'); break; case 'EXTRACT': - $expression->name('DATEPART')->type(' ,'); + $expression->name('DATEPART')->tieWith(' ,'); break; case 'DATE_ADD': $params = []; @@ -258,7 +258,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) $expression ->name('DATEADD') - ->type(',') + ->tieWith(',') ->iterateParts($visitor) ->iterateParts($manipulator) ->add([$params[2] => 'literal']); @@ -266,7 +266,7 @@ protected function _transformFunctionExpression(FunctionExpression $expression) case 'DAYOFWEEK': $expression ->name('DATEPART') - ->type(' ') + ->tieWith(' ') ->add(['weekday, ' => 'literal'], [], true); break; } diff --git a/src/Database/Expression/QueryExpression.php b/src/Database/Expression/QueryExpression.php index a0d71adeed0..73421319434 100644 --- a/src/Database/Expression/QueryExpression.php +++ b/src/Database/Expression/QueryExpression.php @@ -65,7 +65,7 @@ class QueryExpression implements ExpressionInterface, Countable public function __construct($conditions = [], $types = [], $conjunction = 'AND') { $this->typeMap($types); - $this->type(strtoupper($conjunction)); + $this->tieWith(strtoupper($conjunction)); if (!empty($conditions)) { $this->add($conditions, $this->typeMap()->types()); } @@ -79,7 +79,7 @@ public function __construct($conditions = [], $types = [], $conjunction = 'AND') * will not set any value, but return the currently stored one * @return string|$this */ - public function type($conjunction = null) + public function tieWith($conjunction = null) { if ($conjunction === null) { return $this->_conjunction; @@ -89,6 +89,19 @@ public function type($conjunction = null) return $this; } + /** + * Backwards compatible wrapper for tieWith() + * + * @param string $conjunction value to be used for joining conditions. If null it + * will not set any value, but return the currently stored one + * @return string|$this + * @deprecated 3.2.0 Use tieWith() instead + */ + public function type($conjunction = null) + { + return $this->tieWith($conjunction); + } + /** * Adds one or more conditions to this expression object. Conditions can be * expressed in a one dimensional array, that will cause all conditions to diff --git a/src/Database/FunctionsBuilder.php b/src/Database/FunctionsBuilder.php index f2cb49e12bc..18cd01a09a3 100644 --- a/src/Database/FunctionsBuilder.php +++ b/src/Database/FunctionsBuilder.php @@ -178,7 +178,7 @@ public function datePart($part, $expression, $types = []) public function extract($part, $expression, $types = []) { $expression = $this->_literalArgumentFunction('EXTRACT', $expression, $types); - $expression->type(' FROM')->add([$part => 'literal'], [], true); + $expression->tieWith(' FROM')->add([$part => 'literal'], [], true); return $expression; } @@ -198,7 +198,7 @@ public function dateAdd($expression, $value, $unit, $types = []) } $interval = $value . ' ' . $unit; $expression = $this->_literalArgumentFunction('DATE_ADD', $expression, $types); - $expression->type(', INTERVAL')->add([$interval => 'literal']); + $expression->tieWith(', INTERVAL')->add([$interval => 'literal']); return $expression; } diff --git a/src/Database/Query.php b/src/Database/Query.php index b657833cb77..5896a075759 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -704,7 +704,7 @@ protected function _makeJoin($table, $conditions, $type) * ### Using expressions objects: * * ``` - * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->type('OR'); + * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR'); * $query->where(['published' => true], ['published' => 'boolean'])->where($exp); * ``` * diff --git a/src/Datasource/QueryInterface.php b/src/Datasource/QueryInterface.php index 31373862181..999f80e72a6 100644 --- a/src/Datasource/QueryInterface.php +++ b/src/Datasource/QueryInterface.php @@ -319,7 +319,7 @@ public function repository(RepositoryInterface $repository = null); * ### Using expressions objects: * * ``` - * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->type('OR'); + * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR'); * $query->where(['published' => true], ['published' => 'boolean'])->where($exp); * ``` * From b160b2c72410c67d6ddc88ce100f61282f7f7c1d Mon Sep 17 00:00:00 2001 From: mark_story Date: Tue, 10 Nov 2015 12:53:59 -0500 Subject: [PATCH 023/128] Incorporate feedback thus far. --- src/Database/Type/DateTimeType.php | 3 +-- src/I18n/Date.php | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Database/Type/DateTimeType.php b/src/Database/Type/DateTimeType.php index efee3636036..8f64e45534f 100644 --- a/src/Database/Type/DateTimeType.php +++ b/src/Database/Type/DateTimeType.php @@ -17,7 +17,6 @@ use Cake\Database\Driver; use Cake\Database\Type; use DateTimeInterface; -use DateTimeImmutable; use Exception; use RuntimeException; @@ -127,7 +126,7 @@ public function toPHP($value, Driver $driver) */ public function marshal($value) { - if ($value instanceof DateTime || $value instanceof DateTimeImmutable) { + if ($value instanceof DateTimeInterface) { return $value; } diff --git a/src/I18n/Date.php b/src/I18n/Date.php index 9fe9bbad069..f520e7b22b3 100644 --- a/src/I18n/Date.php +++ b/src/I18n/Date.php @@ -45,8 +45,8 @@ class Date extends BaseDate implements JsonSerializable protected static $_toStringFormat = [IntlDateFormatter::SHORT, -1]; /** - * The format to use when formatting a time using `Cake\I18n\Time::timeAgoInWords()` - * and the difference is more than `Cake\I18n\Time::$wordEnd` + * The format to use when formatting a time using `Cake\I18n\Date::timeAgoInWords()` + * and the difference is more than `Cake\I18n\Date::$wordEnd` * * @var string * @see \Cake\I18n\DateFormatTrait::parseDate() @@ -54,7 +54,7 @@ class Date extends BaseDate implements JsonSerializable public static $wordFormat = [IntlDateFormatter::SHORT, -1]; /** - * The format to use when formatting a time using `Cake\I18n\Time::nice()` + * The format to use when formatting a time using `Cake\I18n\Date::nice()` * * The format should be either the formatting constants from IntlDateFormatter as * described in (http://www.php.net/manual/en/class.intldateformatter.php) or a pattern @@ -70,8 +70,8 @@ class Date extends BaseDate implements JsonSerializable public static $niceFormat = [IntlDateFormatter::MEDIUM, -1]; /** - * The format to use when formatting a time using `Time::timeAgoInWords()` - * and the difference is less than `Time::$wordEnd` + * The format to use when formatting a time using `Date::timeAgoInWords()` + * and the difference is less than `Date::$wordEnd` * * @var array * @see \Cake\I18n\Date::timeAgoInWords() From d6c4d0260cd062b2d5d57833bde567d3d604fceb Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 10 Nov 2015 21:01:57 -0500 Subject: [PATCH 024/128] See if german dates don't vary with intl versions. --- tests/TestCase/I18n/DateTest.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index 4f26093ba49..9c2507fdc8d 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -64,10 +64,6 @@ public function testI18nFormat() $expected = '1/14/10'; $this->assertEquals($expected, $result); - $result = $time->i18nFormat(\IntlDateFormatter::FULL, null, 'es-ES'); - $expected = 'jueves, 14 de enero de 2010, 0:00:00 (GMT)'; - $this->assertEquals($expected, $result); - $format = [\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT]; $result = $time->i18nFormat($format); $expected = '12:00 AM'; @@ -82,8 +78,8 @@ public function testI18nFormat() $expected = 'jeudi 14 janvier 2010 00:00:00 UTC'; $this->assertEquals($expected, $result); - $result = $time->i18nFormat(\IntlDateFormatter::FULL, null, 'es-ES'); - $expected = 'jueves, 14 de enero de 2010, 0:00:00 (GMT)'; + $result = $time->i18nFormat(\IntlDateFormatter::FULL, null, 'de-DE'); + $expected = 'Donnerstag, 14. Januar 2010 um 00:00:00 GMT'; $this->assertEquals($expected, $result, 'Default locale should not be used'); } From ad8ced1bed1cbe1f80e23dca7b5e261577e6ee63 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 11 Nov 2015 21:51:18 -0500 Subject: [PATCH 025/128] Default to mutable datetimes. While I'd much prefer to use immutable date time objects. Historically, we've provided userland code mutable instances. To keep compatibility with existing use cases using mutable objects will be be safer. --- src/I18n/DateFormatTrait.php | 3 ++- src/I18n/Time.php | 9 +++++---- tests/TestCase/Database/Type/DateTypeTest.php | 2 +- tests/TestCase/Database/Type/TimeTypeTest.php | 2 +- tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php | 2 +- tests/TestCase/ORM/MarshallerTest.php | 2 +- tests/bootstrap.php | 1 + 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index 2edf37e4670..471a8bf751b 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -127,7 +127,8 @@ public function i18nFormat($format = null, $timezone = null, $locale = null) $time = $this; if ($timezone) { - $time = $time->timezone($timezone); + $time = clone $this; + $time->timezone($timezone); } $format = $format !== null ? $format : static::$_toStringFormat; diff --git a/src/I18n/Time.php b/src/I18n/Time.php index 0895e8a1058..a1d7798bf69 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -14,7 +14,8 @@ */ namespace Cake\I18n; -use Cake\Chronos\Chronos; +use Cake\Chronos\ChronosInterface; +use Cake\Chronos\MutableDateTime; use DateTime; use DateTimeZone; use IntlDateFormatter; @@ -25,7 +26,7 @@ * formatting helpers * */ -class Time extends Chronos implements JsonSerializable +class Time extends MutableDateTime implements JsonSerializable { use DateFormatTrait; @@ -348,12 +349,12 @@ public function timeAgoInWords(array $options = []) * See `Time::timeAgoInWords()` for a full list of options that can be passed * to this method. * - * @param \Cake\Chronos\Chronos|null $other the date to diff with + * @param \Cake\Chronos\ChronosInterface|null $other the date to diff with * @param array $options options accepted by timeAgoInWords * @return string * @see Time::timeAgoInWords() */ - public function diffForHumans(Chronos $other = null, array $options = []) + public function diffForHumans(ChronosInterface $other = null, array $options = []) { $options = ['from' => $other] + $options; return $this->timeAgoInWords($options); diff --git a/tests/TestCase/Database/Type/DateTypeTest.php b/tests/TestCase/Database/Type/DateTypeTest.php index deb155e5e2a..294e21f099e 100644 --- a/tests/TestCase/Database/Type/DateTypeTest.php +++ b/tests/TestCase/Database/Type/DateTypeTest.php @@ -49,7 +49,7 @@ public function testToPHP() $this->assertNull($this->type->toPHP('0000-00-00', $this->driver)); $result = $this->type->toPHP('2001-01-04', $this->driver); - $this->assertInstanceOf('DateTimeImmutable', $result); + $this->assertInstanceOf('DateTime', $result); $this->assertEquals('2001', $result->format('Y')); $this->assertEquals('01', $result->format('m')); $this->assertEquals('04', $result->format('d')); diff --git a/tests/TestCase/Database/Type/TimeTypeTest.php b/tests/TestCase/Database/Type/TimeTypeTest.php index b02c2d7f90a..eb28f65919c 100644 --- a/tests/TestCase/Database/Type/TimeTypeTest.php +++ b/tests/TestCase/Database/Type/TimeTypeTest.php @@ -53,7 +53,7 @@ public function testToPHP() $this->assertEquals('15', $result->format('s')); $result = $this->type->toPHP('16:30:15', $this->driver); - $this->assertInstanceOf('DateTimeImmutable', $result); + $this->assertInstanceOf('DateTime', $result); $this->assertEquals('16', $result->format('H')); $this->assertEquals('30', $result->format('i')); $this->assertEquals('15', $result->format('s')); diff --git a/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php b/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php index db35e01cff6..91f3e02a1e0 100644 --- a/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php +++ b/tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php @@ -225,7 +225,7 @@ public function testGetTimestamp() $return = $this->Behavior->timestamp(); $this->assertInstanceOf( - 'DateTimeImmutable', + 'DateTime', $return, 'Should return a timestamp object' ); diff --git a/tests/TestCase/ORM/MarshallerTest.php b/tests/TestCase/ORM/MarshallerTest.php index f47b8550e9d..58c237655df 100644 --- a/tests/TestCase/ORM/MarshallerTest.php +++ b/tests/TestCase/ORM/MarshallerTest.php @@ -2087,7 +2087,7 @@ public function testMergeComplexType() ]; $marshall = new Marshaller($this->comments); $result = $marshall->merge($entity, $data); - $this->assertInstanceOf('DateTimeImmutable', $entity->created); + $this->assertInstanceOf('DateTime', $entity->created); $this->assertEquals('2014-02-14', $entity->created->format('Y-m-d')); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a686f5ec32a..b4a7d788348 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -119,6 +119,7 @@ ]); Cake\Chronos\Chronos::setTestNow(Cake\Chronos\Chronos::now()); +Cake\Chronos\MutableDateTime::setTestNow(Cake\Chronos\MutableDateTime::now()); Cake\Chronos\Date::setTestNow(Cake\Chronos\Date::now()); ini_set('intl.default_locale', 'en_US'); From 037e1d63969b547fa5fc186a56b26fbf2b927cd9 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 12 Nov 2015 21:44:42 -0500 Subject: [PATCH 026/128] Use mutable date for Date base class. Historically DateTimes from date columns were mutable. Make that the default now as well. --- src/Database/Type/DateType.php | 2 +- src/I18n/Date.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Type/DateType.php b/src/Database/Type/DateType.php index d008cd448c9..bae140a78f2 100644 --- a/src/Database/Type/DateType.php +++ b/src/Database/Type/DateType.php @@ -15,7 +15,7 @@ namespace Cake\Database\Type; use Cake\Database\Driver; -use DateTimeImmutable; +use DateTime; class DateType extends DateTimeType { diff --git a/src/I18n/Date.php b/src/I18n/Date.php index f520e7b22b3..8cb2693967b 100644 --- a/src/I18n/Date.php +++ b/src/I18n/Date.php @@ -14,7 +14,7 @@ */ namespace Cake\I18n; -use Cake\Chronos\Date as BaseDate; +use Cake\Chronos\MutableDate; use IntlDateFormatter; use JsonSerializable; @@ -23,7 +23,7 @@ * * Adds handy methods and locale-aware formatting helpers */ -class Date extends BaseDate implements JsonSerializable +class Date extends MutableDate implements JsonSerializable { use DateFormatTrait; From cd73d0751ce09b5b67a529d111b6d51bcb9da6cc Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 16 Nov 2015 22:08:15 -0500 Subject: [PATCH 027/128] Set testNow() on MutableDate too. --- tests/bootstrap.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b4a7d788348..b85d4d12bfa 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,6 +12,10 @@ */ use Cake\Cache\Cache; +use Cake\Chronos\Chronos; +use Cake\Chronos\Date; +use Cake\Chronos\MutableDate; +use Cake\Chronos\MutableDateTime; use Cake\Core\Configure; use Cake\Datasource\ConnectionManager; use Cake\I18n\I18n; @@ -118,8 +122,9 @@ ] ]); -Cake\Chronos\Chronos::setTestNow(Cake\Chronos\Chronos::now()); -Cake\Chronos\MutableDateTime::setTestNow(Cake\Chronos\MutableDateTime::now()); -Cake\Chronos\Date::setTestNow(Cake\Chronos\Date::now()); +Chronos::setTestNow(Chronos::now()); +MutableDateTime::setTestNow(MutableDateTime::now()); +Date::setTestNow(Date::now()); +MutableDate::setTestNow(MutableDate::now()); ini_set('intl.default_locale', 'en_US'); From 3f4a0d87706e2fff69a79e4e986cfa4409a11632 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 16 Nov 2015 22:32:58 -0500 Subject: [PATCH 028/128] Lets see if es-ES is more suitable for use in travis.ci --- tests/TestCase/I18n/DateTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index 9c2507fdc8d..1ecf24454ab 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -78,9 +78,8 @@ public function testI18nFormat() $expected = 'jeudi 14 janvier 2010 00:00:00 UTC'; $this->assertEquals($expected, $result); - $result = $time->i18nFormat(\IntlDateFormatter::FULL, null, 'de-DE'); - $expected = 'Donnerstag, 14. Januar 2010 um 00:00:00 GMT'; - $this->assertEquals($expected, $result, 'Default locale should not be used'); + $result = $time->i18nFormat(\IntlDateFormatter::FULL, null, 'es-ES'); + $this->assertContains('14 de enero de 2010', $result, 'Default locale should not be used'); } /** From 085bee8f28626b254ca2daceff8eae1b5d50df4e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 21 Nov 2015 12:19:41 -0500 Subject: [PATCH 029/128] Start building out the CorsBuilder. This class will provide a fluent interface for defining all the Access-Control-* headers. The current implementation only adds headers if there is an origin match. I'm going to address that issue once more of the methods are in place. --- src/Network/CorsBuilder.php | 85 ++++++++++++++++++++++ tests/TestCase/Network/CorsBuilderTest.php | 49 +++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/Network/CorsBuilder.php create mode 100644 tests/TestCase/Network/CorsBuilderTest.php diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php new file mode 100644 index 00000000000..b47b7a8cc87 --- /dev/null +++ b/src/Network/CorsBuilder.php @@ -0,0 +1,85 @@ +_origin = $origin; + $this->_isSsl = $isSsl; + $this->_response = $response; + } + + public function allowOrigin($domain) + { + if (empty($this->_origin)) { + return $this; + } + $allowed = $this->_normalizeDomains((array)$domain); + foreach ($allowed as $domain) { + if (!preg_match($domain['preg'], $this->_origin)) { + continue; + } + $this->_response->header('Access-Control-Origin', $this->_origin); + break; + } + return $this; + } + + /** + * Normalize the origin to regular expressions and put in an array format + * + * @param array $domains Domain names to normalize. + * @return array + */ + protected function _normalizeDomains($domains) + { + $result = []; + foreach ($domains as $domain) { + if ($domain === '*') { + $result[] = ['preg' => '@.@', 'original' => '*']; + continue; + } + + $original = $preg = $domain; + if (strpos($domain, '://') === false) { + $preg = ($this->_isSsl ? 'https://' : 'http://') . $domain; + } + $preg = '@' . str_replace('*', '.*', $domain) . '@'; + $result[] = compact('original', 'preg'); + } + return $result; + } + + public function allowMethods($methods) + { + return $this; + } + + public function allowCredentials() + { + return $this; + } + + public function allowHeaders($headers) + { + return $this; + } + + public function exposeHeaders($headers) + { + return $this; + } + + public function maxAge($headers) + { + return $this; + } +} diff --git a/tests/TestCase/Network/CorsBuilderTest.php b/tests/TestCase/Network/CorsBuilderTest.php new file mode 100644 index 00000000000..bb3be6b16dd --- /dev/null +++ b/tests/TestCase/Network/CorsBuilderTest.php @@ -0,0 +1,49 @@ +assertSame($builder, $builder->allowOrigin(['*.example.com', '*.foo.com'])); + $this->assertHeader('http://www.example.com', $response, 'Access-Control-Origin'); + } + + /** + * test allowOrigin() setting allow-origin + * + * @return void + */ + public function testOriginString() + { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://www.example.com'); + $this->assertSame($builder, $builder->allowOrigin('*.example.com')); + $this->assertHeader('http://www.example.com', $response, 'Access-Control-Origin'); + } + + /** + * Helper for checking header values. + * + * @param string $expected The expected value + * @param \Cake\Network\Response $response The Response object. + * @params string $header The header key to check + */ + protected function assertHeader($expected, Response $response, $header) + { + $headers = $response->header(); + $this->assertArrayHasKey($header, $headers, 'Header key not found.'); + $this->assertEquals($expected, $headers[$header], 'Header value not found.'); + } +} From 0584a89d8fb6603a700395128efda2c1a162716c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 21 Nov 2015 12:34:11 -0500 Subject: [PATCH 030/128] Implement more methods on CorsBuilder. --- src/Network/CorsBuilder.php | 16 +++- tests/TestCase/Network/CorsBuilderTest.php | 96 +++++++++++++++++++++- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php index b47b7a8cc87..fbb4056dbae 100644 --- a/src/Network/CorsBuilder.php +++ b/src/Network/CorsBuilder.php @@ -58,18 +58,30 @@ protected function _normalizeDomains($domains) return $result; } - public function allowMethods($methods) + public function allowMethods(array $methods) { + if (empty($this->_origin)) { + return $this; + } + $this->_response->header('Access-Control-Allow-Methods', implode(', ', $methods)); return $this; } public function allowCredentials() { + if (empty($this->_origin)) { + return $this; + } + $this->_response->header('Access-Control-Allow-Credentials', 'true'); return $this; } - public function allowHeaders($headers) + public function allowHeaders(array $headers) { + if (empty($this->_origin)) { + return $this; + } + $this->_response->header('Access-Control-Allow-Headers', implode(', ', $headers)); return $this; } diff --git a/tests/TestCase/Network/CorsBuilderTest.php b/tests/TestCase/Network/CorsBuilderTest.php index bb3be6b16dd..30537dabd89 100644 --- a/tests/TestCase/Network/CorsBuilderTest.php +++ b/tests/TestCase/Network/CorsBuilderTest.php @@ -12,12 +12,12 @@ class CorsBuilderTest extends TestCase * * @return void */ - public function testAllowOriginArray() + public function testAllowOriginNoOrigin() { $response = new Response(); - $builder = new CorsBuilder($response, 'http://www.example.com'); + $builder = new CorsBuilder($response, ''); $this->assertSame($builder, $builder->allowOrigin(['*.example.com', '*.foo.com'])); - $this->assertHeader('http://www.example.com', $response, 'Access-Control-Origin'); + $this->assertNoHeader($response, 'Access-Control-Origin'); } /** @@ -25,14 +25,90 @@ public function testAllowOriginArray() * * @return void */ - public function testOriginString() + public function testAllowOrigin() { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://www.example.com'); + $this->assertSame($builder, $builder->allowOrigin(['*.example.com', '*.foo.com'])); + $this->assertHeader('http://www.example.com', $response, 'Access-Control-Origin'); + $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin('*.example.com')); $this->assertHeader('http://www.example.com', $response, 'Access-Control-Origin'); } + /** + * test allowOrigin() with SSL + * + * @return void + */ + public function testAllowOriginSsl() + { + $response = new Response(); + $builder = new CorsBuilder($response, 'https://www.example.com', true); + $this->assertSame($builder, $builder->allowOrigin('http://example.com')); + $this->assertNoHeader($response, 'Access-Control-Origin'); + + $response = new Response(); + $builder = new CorsBuilder($response, 'http://www.example.com', true); + $this->assertSame($builder, $builder->allowOrigin('https://example.com')); + $this->assertNoHeader($response, 'Access-Control-Origin'); + + $response = new Response(); + $builder = new CorsBuilder($response, 'http://www.example.com'); + $this->assertSame($builder, $builder->allowOrigin('https://example.com')); + $this->assertNoHeader($response, 'Access-Control-Origin'); + } + + public function testAllowMethodsNoOrigin() + { + $response = new Response(); + $builder = new CorsBuilder($response, ''); + $this->assertSame($builder, $builder->allowMethods(['GET', 'POST'])); + $this->assertNoHeader($response, 'Access-Control-Allow-Methods'); + } + + public function testAllowMethods() + { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://example.com'); + $this->assertSame($builder, $builder->allowMethods(['GET', 'POST'])); + $this->assertHeader('GET, POST', $response, 'Access-Control-Allow-Methods'); + } + + public function testAllowCredentialsNoOrigin() + { + $response = new Response(); + $builder = new CorsBuilder($response, ''); + $this->assertSame($builder, $builder->allowCredentials()); + $this->assertNoHeader($response, 'Access-Control-Allow-Credentials'); + } + + public function testAllowCredentials() + { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://example.com'); + $this->assertSame($builder, $builder->allowCredentials()); + $this->assertHeader('true', $response, 'Access-Control-Allow-Credentials'); + } + + public function testAllowHeadersNoOrigin() + { + $response = new Response(); + $builder = new CorsBuilder($response, ''); + $this->assertSame($builder, $builder->allowHeaders(['X-THING'])); + $this->assertNoHeader($response, 'Access-Control-Allow-Headers'); + } + + public function testAllowHeaders() + { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://example.com'); + $this->assertSame($builder, $builder->allowHeaders(['Content-Type', 'Accept'])); + $this->assertHeader('Content-Type, Accept', $response, 'Access-Control-Allow-Headers'); + } + /** * Helper for checking header values. * @@ -46,4 +122,16 @@ protected function assertHeader($expected, Response $response, $header) $this->assertArrayHasKey($header, $headers, 'Header key not found.'); $this->assertEquals($expected, $headers[$header], 'Header value not found.'); } + + /** + * Helper for checking header values. + * + * @param \Cake\Network\Response $response The Response object. + * @params string $header The header key to check + */ + protected function assertNoHeader(Response $response, $header) + { + $headers = $response->header(); + $this->assertArrayNotHasKey($header, $headers, 'Header key was found.'); + } } From 423f2e929121c744929625d4a3057e32aa4fd72b Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 21 Nov 2015 12:42:41 -0500 Subject: [PATCH 031/128] Finish building out methods for CorsBuilder. --- src/Network/CorsBuilder.php | 14 ++++++-- tests/TestCase/Network/CorsBuilderTest.php | 42 +++++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php index fbb4056dbae..792f3219f6d 100644 --- a/src/Network/CorsBuilder.php +++ b/src/Network/CorsBuilder.php @@ -27,7 +27,7 @@ public function allowOrigin($domain) if (!preg_match($domain['preg'], $this->_origin)) { continue; } - $this->_response->header('Access-Control-Origin', $this->_origin); + $this->_response->header('Access-Control-Allow-Origin', $this->_origin); break; } return $this; @@ -85,13 +85,21 @@ public function allowHeaders(array $headers) return $this; } - public function exposeHeaders($headers) + public function exposeHeaders(array $headers) { + if (empty($this->_origin)) { + return $this; + } + $this->_response->header('Access-Control-Expose-Headers', implode(', ', $headers)); return $this; } - public function maxAge($headers) + public function maxAge($age) { + if (empty($this->_origin)) { + return $this; + } + $this->_response->header('Access-Control-Max-Age', $age); return $this; } } diff --git a/tests/TestCase/Network/CorsBuilderTest.php b/tests/TestCase/Network/CorsBuilderTest.php index 30537dabd89..ba863e0d6e3 100644 --- a/tests/TestCase/Network/CorsBuilderTest.php +++ b/tests/TestCase/Network/CorsBuilderTest.php @@ -30,12 +30,12 @@ public function testAllowOrigin() $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin(['*.example.com', '*.foo.com'])); - $this->assertHeader('http://www.example.com', $response, 'Access-Control-Origin'); + $this->assertHeader('http://www.example.com', $response, 'Access-Control-Allow-Origin'); $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin('*.example.com')); - $this->assertHeader('http://www.example.com', $response, 'Access-Control-Origin'); + $this->assertHeader('http://www.example.com', $response, 'Access-Control-Allow-Origin'); } /** @@ -48,17 +48,17 @@ public function testAllowOriginSsl() $response = new Response(); $builder = new CorsBuilder($response, 'https://www.example.com', true); $this->assertSame($builder, $builder->allowOrigin('http://example.com')); - $this->assertNoHeader($response, 'Access-Control-Origin'); + $this->assertNoHeader($response, 'Access-Control-Allow-Origin'); $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com', true); $this->assertSame($builder, $builder->allowOrigin('https://example.com')); - $this->assertNoHeader($response, 'Access-Control-Origin'); + $this->assertNoHeader($response, 'Access-Control-Allow-Origin'); $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin('https://example.com')); - $this->assertNoHeader($response, 'Access-Control-Origin'); + $this->assertNoHeader($response, 'Access-Control-Allow-Origin'); } public function testAllowMethodsNoOrigin() @@ -109,6 +109,38 @@ public function testAllowHeaders() $this->assertHeader('Content-Type, Accept', $response, 'Access-Control-Allow-Headers'); } + public function testExposeHeadersNoOrigin() + { + $response = new Response(); + $builder = new CorsBuilder($response, ''); + $this->assertSame($builder, $builder->exposeHeaders(['X-THING'])); + $this->assertNoHeader($response, 'Access-Control-Expose-Headers'); + } + + public function testExposeHeaders() + { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://example.com'); + $this->assertSame($builder, $builder->exposeHeaders(['Content-Type', 'Accept'])); + $this->assertHeader('Content-Type, Accept', $response, 'Access-Control-Expose-Headers'); + } + + public function testMaxAgeNoOrigin() + { + $response = new Response(); + $builder = new CorsBuilder($response, ''); + $this->assertSame($builder, $builder->maxAge(300)); + $this->assertNoHeader($response, 'Access-Control-Max-Age'); + } + + public function testMaxAge() + { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://example.com'); + $this->assertSame($builder, $builder->maxAge(300)); + $this->assertHeader('300', $response, 'Access-Control-Max-Age'); + } + /** * Helper for checking header values. * From 719e9da7cae0460a863bc4817825809a17ec5d11 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 21 Nov 2015 12:52:04 -0500 Subject: [PATCH 032/128] Integrate CorsBuilder into Response. This maintains compatiblity with previous releases, and also allows the builder to be used for addtional options. --- src/Network/CorsBuilder.php | 3 +- src/Network/Response.php | 48 +++++----------------- tests/TestCase/Network/CorsBuilderTest.php | 5 +++ 3 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php index 792f3219f6d..934a48310da 100644 --- a/src/Network/CorsBuilder.php +++ b/src/Network/CorsBuilder.php @@ -27,7 +27,8 @@ public function allowOrigin($domain) if (!preg_match($domain['preg'], $this->_origin)) { continue; } - $this->_response->header('Access-Control-Allow-Origin', $this->_origin); + $value = $domain['original'] === '*' ? '*' : $this->_origin; + $this->_response->header('Access-Control-Allow-Origin', $value); break; } return $this; diff --git a/src/Network/Response.php b/src/Network/Response.php index f9ca735b4c9..5f437473179 100644 --- a/src/Network/Response.php +++ b/src/Network/Response.php @@ -1351,51 +1351,25 @@ public function cookie($options = null) * @param string|array $allowedDomains List of allowed domains, see method description for more details * @param string|array $allowedMethods List of HTTP verbs allowed * @param string|array $allowedHeaders List of HTTP headers allowed - * @return void + * @return \Cake\Network\CorsBuilder A builder object the provides a fluent interface for defining + * additional CORS headers. */ public function cors(Request $request, $allowedDomains, $allowedMethods = [], $allowedHeaders = []) { $origin = $request->header('Origin'); + $ssl = $request->is('ssl'); + $builder = new CorsBuilder($this, $origin, $ssl); if (!$origin) { - return; + return $builder; } - - $allowedDomains = $this->_normalizeCorsDomains((array)$allowedDomains, $request->is('ssl')); - foreach ($allowedDomains as $domain) { - if (!preg_match($domain['preg'], $origin)) { - continue; - } - $this->header('Access-Control-Allow-Origin', $domain['original'] === '*' ? '*' : $origin); - $allowedMethods && $this->header('Access-Control-Allow-Methods', implode(', ', (array)$allowedMethods)); - $allowedHeaders && $this->header('Access-Control-Allow-Headers', implode(', ', (array)$allowedHeaders)); - break; + $builder->allowOrigin($allowedDomains); + if ($allowedMethods) { + $builder->allowMethods((array)$allowedMethods); } - } - - /** - * Normalize the origin to regular expressions and put in an array format - * - * @param array $domains Domain names to normalize. - * @param bool $requestIsSSL Whether it's a SSL request. - * @return array - */ - protected function _normalizeCorsDomains($domains, $requestIsSSL = false) - { - $result = []; - foreach ($domains as $domain) { - if ($domain === '*') { - $result[] = ['preg' => '@.@', 'original' => '*']; - continue; - } - - $original = $preg = $domain; - if (strpos($domain, '://') === false) { - $preg = ($requestIsSSL ? 'https://' : 'http://') . $domain; - } - $preg = '@' . str_replace('*', '.*', $domain) . '@'; - $result[] = compact('original', 'preg'); + if ($allowedHeaders) { + $builder->allowHeaders((array)$allowedHeaders); } - return $result; + return $builder; } /** diff --git a/tests/TestCase/Network/CorsBuilderTest.php b/tests/TestCase/Network/CorsBuilderTest.php index ba863e0d6e3..d31d8f8d40b 100644 --- a/tests/TestCase/Network/CorsBuilderTest.php +++ b/tests/TestCase/Network/CorsBuilderTest.php @@ -27,6 +27,11 @@ public function testAllowOriginNoOrigin() */ public function testAllowOrigin() { + $response = new Response(); + $builder = new CorsBuilder($response, 'http://www.example.com'); + $this->assertSame($builder, $builder->allowOrigin('*')); + $this->assertHeader('*', $response, 'Access-Control-Allow-Origin'); + $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin(['*.example.com', '*.foo.com'])); From 7aa662e07d71d5975a90a5d29fa1c423a7496222 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 21 Nov 2015 12:54:01 -0500 Subject: [PATCH 033/128] Deprecate the parameters for Response::cors(). The builder gives a more useful and expressive interface for setting cors options. The other parameters will be removed in 4.x --- src/Network/Response.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Network/Response.php b/src/Network/Response.php index 5f437473179..f94e2b32ff7 100644 --- a/src/Network/Response.php +++ b/src/Network/Response.php @@ -1347,6 +1347,9 @@ public function cookie($options = null) * cors($request, ['http://www.cakephp.org', '*.google.com', 'https://myproject.github.io']); * ``` * + * *Note* The `$allowedDomains`, `$allowedMethods`, `$allowedHeaders` parameters are deprectated. + * Instead the builder object should be used. + * * @param \Cake\Network\Request $request Request object * @param string|array $allowedDomains List of allowed domains, see method description for more details * @param string|array $allowedMethods List of HTTP verbs allowed @@ -1354,7 +1357,7 @@ public function cookie($options = null) * @return \Cake\Network\CorsBuilder A builder object the provides a fluent interface for defining * additional CORS headers. */ - public function cors(Request $request, $allowedDomains, $allowedMethods = [], $allowedHeaders = []) + public function cors(Request $request, $allowedDomains = [], $allowedMethods = [], $allowedHeaders = []) { $origin = $request->header('Origin'); $ssl = $request->is('ssl'); @@ -1362,7 +1365,9 @@ public function cors(Request $request, $allowedDomains, $allowedMethods = [], $a if (!$origin) { return $builder; } - $builder->allowOrigin($allowedDomains); + if ($allowedDomains) { + $builder->allowOrigin($allowedDomains); + } if ($allowedMethods) { $builder->allowMethods((array)$allowedMethods); } From 8477fa8bb89ea6c33bd8d9f62870aaa030d5afca Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 22 Nov 2015 13:54:44 +0100 Subject: [PATCH 034/128] Moving result type conversion to the statements layer This is part of the work needed for automatically converting SQL functions to their correspondiny PHP types. I also cleans up quite a bit the ResultSet class --- src/Database/FieldTypeConverter.php | 74 +++++++++++++++++++ src/Database/Query.php | 38 ++++++++++ src/Database/TypeMap.php | 10 +++ src/ORM/Query.php | 41 ++++++++-- src/ORM/ResultSet.php | 48 ++---------- tests/TestCase/Database/QueryTest.php | 35 +++++++++ .../ORM/Association/BelongsToTest.php | 2 + tests/TestCase/ORM/Association/HasOneTest.php | 3 + tests/TestCase/ORM/EagerLoaderTest.php | 11 +++ tests/TestCase/ORM/QueryTest.php | 10 ++- tests/TestCase/ORM/TableTest.php | 49 ++++++------ 11 files changed, 244 insertions(+), 77 deletions(-) create mode 100644 src/Database/FieldTypeConverter.php diff --git a/src/Database/FieldTypeConverter.php b/src/Database/FieldTypeConverter.php new file mode 100644 index 00000000000..afdb17333ae --- /dev/null +++ b/src/Database/FieldTypeConverter.php @@ -0,0 +1,74 @@ +_driver = $driver; + $map = $typeMap->toArray(); + $types = array_keys(Type::map()); + $types = array_map(['Cake\Database\Type', 'build'], array_combine($types, $types)); + $result = []; + + foreach ($map as $field => $type) { + if (isset($types[$type])) { + $result[$field] = $types[$type]; + } + } + $this->_typeMap = $result; + } + + /** + * Converts each of the fields in the array that are present in the type map + * using the corresponding Type class. + * + * @param array $row The array with the fields to be casted + * @return array + */ + public function __invoke($row) + { + foreach ($this->_typeMap as $field => $type) { + $row[$field] = $type->toPHP($row[$field], $this->_driver); + } + return $row; + } +} diff --git a/src/Database/Query.php b/src/Database/Query.php index 5896a075759..e45562471df 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -19,6 +19,7 @@ use Cake\Database\Expression\QueryExpression; use Cake\Database\Expression\ValuesExpression; use Cake\Database\Statement\CallbackStatement; +use Cake\Database\TypeMap; use IteratorAggregate; use RuntimeException; @@ -121,6 +122,13 @@ class Query implements ExpressionInterface, IteratorAggregate */ protected $_useBufferedResults = true; + /** + * The Type map for fields in the select clause + * + * @var \Cake\Database\TypeMap + */ + protected $_selectTypeMap; + /** * Constructor. * @@ -172,6 +180,13 @@ public function connection($connection = null) public function execute() { $statement = $this->_connection->run($this); + $driver = $this->_connection->driver(); + $typeMap = $this->selectTypeMap(); + + if ($typeMap->toArray()) { + $this->decorateResults(new FieldTypeConverter($typeMap, $driver)); + } + $this->_iterator = $this->_decorateStatement($statement); $this->_dirty = false; return $this->_iterator; @@ -1677,6 +1692,29 @@ public function bufferResults($enable = null) return $this; } + /** + * Sets the TypeMap class where the types for each of the fields in the + * select clause are stored. + * + * When called with no arguments, the current TypeMap object is returned. + * + * @param \Cake\Database\TypeMap $typeMap The map object to use + * @return $this|\Cake\Database\TypeMap + */ + public function selectTypeMap(TypeMap $typeMap = null) + { + if ($typeMap === null && $this->_selectTypeMap === null) { + $this->_selectTypeMap = new TypeMap(); + } + + if ($typeMap === null) { + return $this->_selectTypeMap; + } + + $this->_selectTypeMap = $typeMap; + return $this; + } + /** * Auxiliary function used to wrap the original statement from the driver with * any registered callbacks. diff --git a/src/Database/TypeMap.php b/src/Database/TypeMap.php index 670ee47eaa2..b6ad0e1cd47 100644 --- a/src/Database/TypeMap.php +++ b/src/Database/TypeMap.php @@ -136,4 +136,14 @@ public function type($column) } return null; } + + /** + * Returns an array of all types mapped types + * + * @return array + */ + public function toArray() + { + return $this->_types + $this->_defaults; + } } diff --git a/src/ORM/Query.php b/src/ORM/Query.php index 1a8aa730ed8..dfd0ecc47fb 100644 --- a/src/ORM/Query.php +++ b/src/ORM/Query.php @@ -17,6 +17,7 @@ use ArrayObject; use Cake\Database\ExpressionInterface; use Cake\Database\Query as DatabaseQuery; +use Cake\Database\TypeMap; use Cake\Database\ValueBinder; use Cake\Datasource\QueryInterface; use Cake\Datasource\QueryTrait; @@ -173,7 +174,7 @@ public function addDefaultTypes(Table $table) $map = $table->schema()->typeMap(); $fields = []; foreach ($map as $f => $type) { - $fields[$f] = $fields[$alias . '.' . $f] = $type; + $fields[$f] = $fields[$alias . '.' . $f] = $fields[$alias . '__' . $f] = $type; } $this->typeMap()->addDefaults($fields); @@ -669,6 +670,8 @@ public function cleanCopy() $clone->offset(null); $clone->mapReduce(null, null, true); $clone->formatResults(null, true); + $clone->selectTypeMap(new TypeMap()); + $clone->decorateResults(null, true); return $clone; } @@ -869,6 +872,7 @@ protected function _execute() $decorator = $this->_decoratorClass(); return new $decorator($this->_results); } + $statement = $this->eagerLoader()->loadExternal($this, $this->execute()); return new ResultSet($this, $statement); } @@ -880,22 +884,23 @@ protected function _execute() * specified and applies the joins required to eager load associations defined * using `contain` * + * It also sets the default types for the columns in the select clause + * * @see \Cake\Database\Query::execute() * @return void */ protected function _transformQuery() { - if (!$this->_dirty) { + if (!$this->_dirty || $this->_type !== 'select') { return; } - if ($this->_type === 'select') { - if (empty($this->_parts['from'])) { - $this->from([$this->_repository->alias() => $this->_repository->table()]); - } - $this->_addDefaultFields(); - $this->eagerLoader()->attachAssociations($this, $this->_repository, !$this->_hasFields); + if (empty($this->_parts['from'])) { + $this->from([$this->_repository->alias() => $this->_repository->table()]); } + $this->_addDefaultFields(); + $this->eagerLoader()->attachAssociations($this, $this->_repository, !$this->_hasFields); + $this->_addDefaultSelectTypes(); } /** @@ -919,6 +924,26 @@ protected function _addDefaultFields() $this->select($aliased, true); } + /** + * Sets the default types for converting the fields in the select clause + * + * @return void + */ + protected function _addDefaultSelectTypes() + { + $typeMap = $this->typeMap()->defaults(); + $selectTypeMap = $this->selectTypeMap(); + $select = array_keys($this->clause('select')); + $types = []; + + foreach ($select as $alias) { + if (isset($typeMap[$alias])) { + $types[$alias] = $typeMap[$alias]; + } + } + $this->selectTypeMap()->addDefaults($types); + } + /** * {@inheritDoc} * diff --git a/src/ORM/ResultSet.php b/src/ORM/ResultSet.php index 7c734cc6fed..97b47ae3f98 100644 --- a/src/ORM/ResultSet.php +++ b/src/ORM/ResultSet.php @@ -179,7 +179,6 @@ public function __construct($query, $statement) $this->_useBuffering = $query->bufferResults(); $this->_defaultAlias = $this->_defaultTable->alias(); $this->_calculateColumnMap(); - $this->_calculateTypeMap(); if ($this->_useBuffering) { $count = $this->count(); @@ -397,34 +396,11 @@ protected function _calculateColumnMap() * Creates a map of Type converter classes for each of the columns that should * be fetched by this object. * + * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level * @return void */ protected function _calculateTypeMap() { - if (isset($this->_map[$this->_defaultAlias])) { - $this->_types[$this->_defaultAlias] = $this->_getTypes( - $this->_defaultTable, - $this->_map[$this->_defaultAlias] - ); - } - - foreach ($this->_matchingMapColumns as $alias => $keys) { - $this->_types[$alias] = $this->_getTypes( - $this->_matchingMap[$alias]['instance']->target(), - $keys - ); - } - - foreach ($this->_containMap as $assoc) { - $alias = $assoc['alias']; - if (isset($this->_types[$alias]) || !$assoc['canBeJoined'] || !isset($this->_map[$alias])) { - continue; - } - $this->_types[$alias] = $this->_getTypes( - $assoc['instance']->target(), - $this->_map[$alias] - ); - } } /** @@ -499,12 +475,9 @@ protected function _groupResult($row) foreach ($this->_matchingMapColumns as $alias => $keys) { $matching = $this->_matchingMap[$alias]; - $results['_matchingData'][$alias] = $this->_castValues( - $alias, - array_combine( - $keys, - array_intersect_key($row, $keys) - ) + $results['_matchingData'][$alias] = array_combine( + $keys, + array_intersect_key($row, $keys) ); if ($this->_hydrate) { $options['source'] = $matching['instance']->registryAlias(); @@ -519,12 +492,6 @@ protected function _groupResult($row) $presentAliases[$table] = true; } - if (isset($presentAliases[$defaultAlias])) { - $results[$defaultAlias] = $this->_castValues( - $defaultAlias, - $results[$defaultAlias] - ); - } unset($presentAliases[$defaultAlias]); foreach ($this->_containMap as $assoc) { @@ -550,8 +517,6 @@ protected function _groupResult($row) unset($presentAliases[$alias]); if ($assoc['canBeJoined']) { - $results[$alias] = $this->_castValues($assoc['alias'], $results[$alias]); - $hasData = false; foreach ($results[$alias] as $v) { if ($v !== null && $v !== []) { @@ -602,14 +567,11 @@ protected function _groupResult($row) * * @param string $alias The table object alias * @param array $values The values to cast + * @deprecated 3.2.0 Not used anymore. Type casting is done at the statement level * @return array */ protected function _castValues($alias, $values) { - foreach ($this->_types[$alias] as $field => $type) { - $values[$field] = $type->toPHP($values[$field], $this->_driver); - } - return $values; } diff --git a/tests/TestCase/Database/QueryTest.php b/tests/TestCase/Database/QueryTest.php index f43e5a83fda..bf108d0c738 100644 --- a/tests/TestCase/Database/QueryTest.php +++ b/tests/TestCase/Database/QueryTest.php @@ -17,6 +17,7 @@ use Cake\Core\Configure; use Cake\Database\Expression\IdentifierExpression; use Cake\Database\Query; +use Cake\Database\TypeMap; use Cake\Datasource\ConnectionManager; use Cake\TestSuite\TestCase; @@ -3488,6 +3489,40 @@ public function testDeepClone() $this->assertNotEquals($query->clause('order'), $dupe->clause('order')); } + /** + * Tests the selectTypeMap method + * + * @return void + */ + public function testSelectTypeMap() + { + $query = new Query($this->connection); + $typeMap = $query->selectTypeMap(); + $this->assertInstanceOf(TypeMap::class, $typeMap); + $another = clone $typeMap; + $query->selectTypeMap($another); + $this->assertSame($another, $query->selectTypeMap()); + } + + /** + * Tests the automatic type conversion for the fields in the result + * + * @return void + */ + public function testSelectTypeConversion() + { + $query = new Query($this->connection); + $time = new \DateTime('2007-03-18 10:50:00'); + $query + ->select(['id', 'comment', 'the_date' => 'created']) + ->from('comments') + ->limit(1) + ->selectTypeMap()->types(['id' => 'integer', 'the_date' => 'datetime']); + $result = $query->execute()->fetchAll('assoc'); + $this->assertInternalType('integer', $result[0]['id']); + $this->assertInstanceOf('DateTime', $result[0]['the_date']); + } + /** * Assertion for comparing a table's contents with what is in it. * diff --git a/tests/TestCase/ORM/Association/BelongsToTest.php b/tests/TestCase/ORM/Association/BelongsToTest.php index 1cbc6de407f..4045b844868 100644 --- a/tests/TestCase/ORM/Association/BelongsToTest.php +++ b/tests/TestCase/ORM/Association/BelongsToTest.php @@ -77,6 +77,8 @@ public function setUp() 'id' => 'integer', 'Companies.company_name' => 'string', 'company_name' => 'string', + 'Companies__id' => 'integer', + 'Companies__company_name' => 'string' ]); } diff --git a/tests/TestCase/ORM/Association/HasOneTest.php b/tests/TestCase/ORM/Association/HasOneTest.php index b514a8f5824..d5ad6e4647c 100644 --- a/tests/TestCase/ORM/Association/HasOneTest.php +++ b/tests/TestCase/ORM/Association/HasOneTest.php @@ -65,6 +65,9 @@ public function setUp() 'first_name' => 'string', 'Profiles.user_id' => 'integer', 'user_id' => 'integer', + 'Profiles__first_name' => 'string', + 'Profiles__user_id' => 'integer', + 'Profiles__id' => 'integer', ]); } diff --git a/tests/TestCase/ORM/EagerLoaderTest.php b/tests/TestCase/ORM/EagerLoaderTest.php index 30ee6a0b70a..ced2cc2353f 100644 --- a/tests/TestCase/ORM/EagerLoaderTest.php +++ b/tests/TestCase/ORM/EagerLoaderTest.php @@ -87,6 +87,9 @@ public function setUp() 'name' => 'string', 'clients.phone' => 'string', 'phone' => 'string', + 'clients__id' => 'integer', + 'clients__name' => 'string', + 'clients__phone' => 'string', ]); $this->ordersTypeMap = new TypeMap([ 'orders.id' => 'integer', @@ -95,26 +98,34 @@ public function setUp() 'total' => 'string', 'orders.placed' => 'datetime', 'placed' => 'datetime', + 'orders__id' => 'integer', + 'orders__total' => 'string', + 'orders__placed' => 'datetime', ]); $this->orderTypesTypeMap = new TypeMap([ 'orderTypes.id' => 'integer', 'id' => 'integer', + 'orderTypes__id' => 'integer', ]); $this->stuffTypeMap = new TypeMap([ 'stuff.id' => 'integer', 'id' => 'integer', + 'stuff__id' => 'integer', ]); $this->stuffTypesTypeMap = new TypeMap([ 'stuffTypes.id' => 'integer', 'id' => 'integer', + 'stuffTypes__id' => 'integer', ]); $this->companiesTypeMap = new TypeMap([ 'companies.id' => 'integer', 'id' => 'integer', + 'companies__id' => 'integer', ]); $this->categoriesTypeMap = new TypeMap([ 'categories.id' => 'integer', 'id' => 'integer', + 'categories__id' => 'integer', ]); } diff --git a/tests/TestCase/ORM/QueryTest.php b/tests/TestCase/ORM/QueryTest.php index 7b815128e42..3ce40da976a 100644 --- a/tests/TestCase/ORM/QueryTest.php +++ b/tests/TestCase/ORM/QueryTest.php @@ -824,14 +824,20 @@ public function testApplyOptions() $typeMap = new TypeMap([ 'foo.id' => 'integer', 'id' => 'integer', + 'foo__id' => 'integer', 'articles.id' => 'integer', + 'articles__id' => 'integer', 'articles.author_id' => 'integer', + 'articles__author_id' => 'integer', 'author_id' => 'integer', 'articles.title' => 'string', + 'articles__title' => 'string', 'title' => 'string', 'articles.body' => 'text', + 'articles__body' => 'text', 'body' => 'text', 'articles.published' => 'string', + 'articles__published' => 'string', 'published' => 'string', ]); @@ -2372,10 +2378,12 @@ public function testDebugInfo() 'sql' => $query->sql(), 'params' => $query->valueBinder()->bindings(), 'defaultTypes' => [ + 'authors__id' => 'integer', 'authors.id' => 'integer', 'id' => 'integer', + 'authors__name' => 'string', 'authors.name' => 'string', - 'name' => 'string' + 'name' => 'string', ], 'decorators' => 0, 'executed' => false, diff --git a/tests/TestCase/ORM/TableTest.php b/tests/TestCase/ORM/TableTest.php index 1223930f4a1..6230798441d 100644 --- a/tests/TestCase/ORM/TableTest.php +++ b/tests/TestCase/ORM/TableTest.php @@ -82,25 +82,35 @@ public function setUp() $this->usersTypeMap = new TypeMap([ 'Users.id' => 'integer', 'id' => 'integer', + 'Users__id' => 'integer', 'Users.username' => 'string', + 'Users__username' => 'string', 'username' => 'string', 'Users.password' => 'string', + 'Users__password' => 'string', 'password' => 'string', 'Users.created' => 'timestamp', + 'Users__created' => 'timestamp', 'created' => 'timestamp', 'Users.updated' => 'timestamp', + 'Users__updated' => 'timestamp', 'updated' => 'timestamp', ]); $this->articlesTypeMap = new TypeMap([ 'Articles.id' => 'integer', + 'Articles__id' => 'integer', 'id' => 'integer', 'Articles.title' => 'string', + 'Articles__title' => 'string', 'title' => 'string', 'Articles.author_id' => 'integer', + 'Articles__author_id' => 'integer', 'author_id' => 'integer', 'Articles.body' => 'text', + 'Articles__body' => 'text', 'body' => 'text', 'Articles.published' => 'string', + 'Articles__published' => 'string', 'published' => 'string', ]); } @@ -1756,7 +1766,7 @@ public function testSaveReplaceSaveStrategy() 'entityClass' => 'Cake\ORM\Entity', ] ); - + $authors->hasMany('Articles', ['saveStrategy' => 'replace']); $entity = $authors->newEntity([ @@ -1775,9 +1785,9 @@ public function testSaveReplaceSaveStrategy() $articleId = $entity->articles[0]->id; unset($entity->articles[0]); $entity->dirty('articles', true); - + $authors->save($entity, ['associated' => ['Articles']]); - + $this->assertEquals($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count()); $this->assertTrue($authors->Articles->exists(['id' => $articleId])); } @@ -1798,7 +1808,7 @@ public function testSaveReplaceSaveStrategyNotAdding() 'entityClass' => 'Cake\ORM\Entity', ] ); - + $authors->hasMany('Articles', ['saveStrategy' => 'replace']); $entity = $authors->newEntity([ @@ -1815,9 +1825,9 @@ public function testSaveReplaceSaveStrategyNotAdding() $this->assertCount($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])); $entity->set('articles', []); - + $entity = $authors->save($entity, ['associated' => ['Articles']]); - + $this->assertCount(0, $authors->Articles->find('all')->where(['author_id' => $entity['id']])); } @@ -1852,13 +1862,13 @@ public function testSaveAppendSaveStrategy() $sizeArticles = count($entity->articles); $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count()); - + $articleId = $entity->articles[0]->id; unset($entity->articles[0]); $entity->dirty('articles', true); - + $authors->save($entity, ['associated' => ['Articles']]); - + $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count()); $this->assertTrue($authors->Articles->exists(['id' => $articleId])); } @@ -1916,9 +1926,9 @@ public function testSaveReplaceSaveStrategyDependent() $articleId = $entity->articles[0]->id; unset($entity->articles[0]); $entity->dirty('articles', true); - + $authors->save($entity, ['associated' => ['Articles']]); - + $this->assertEquals($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count()); $this->assertFalse($authors->Articles->exists(['id' => $articleId])); } @@ -1962,7 +1972,7 @@ public function testSaveReplaceSaveStrategyNotNullable() $this->assertEquals($sizeComments, $articles->Comments->find('all')->where(['article_id' => $article->id])->count()); $this->assertTrue($articles->Comments->exists(['id' => $commentId])); - + unset($article->comments[0]); $article->dirty('comments', true); $article = $articles->save($article, ['associated' => ['Comments']]); @@ -2011,7 +2021,7 @@ public function testSaveReplaceSaveStrategyAdding() $this->assertEquals($sizeComments, $articles->Comments->find('all')->where(['article_id' => $article->id])->count()); $this->assertTrue($articles->Comments->exists(['id' => $commentId])); - + unset($article->comments[0]); $article->comments[] = $articles->Comments->newEntity([ 'user_id' => 1, @@ -3351,18 +3361,7 @@ public function testMagicFindAllOr() $this->assertInstanceOf('Cake\ORM\Query', $result); $this->assertNull($result->clause('limit')); $expected = new QueryExpression(); - $expected->typeMap()->defaults([ - 'Users.id' => 'integer', - 'id' => 'integer', - 'Users.username' => 'string', - 'username' => 'string', - 'Users.password' => 'string', - 'password' => 'string', - 'Users.created' => 'timestamp', - 'created' => 'timestamp', - 'Users.updated' => 'timestamp', - 'updated' => 'timestamp', - ]); + $expected->typeMap()->defaults($this->usersTypeMap->toArray()); $expected->add( ['or' => ['Users.author_id' => 1, 'Users.published' => 'Y']] ); From 3bffd01788ae876cb42b498ba22b63769e430be7 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 22 Nov 2015 16:04:17 +0100 Subject: [PATCH 035/128] Fixing doc blocks --- src/Database/Expression/QueryExpression.php | 4 ++-- src/Database/Query.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Expression/QueryExpression.php b/src/Database/Expression/QueryExpression.php index 73421319434..c8bfd8f25b8 100644 --- a/src/Database/Expression/QueryExpression.php +++ b/src/Database/Expression/QueryExpression.php @@ -75,7 +75,7 @@ public function __construct($conditions = [], $types = [], $conjunction = 'AND') * Changes the conjunction for the conditions at this level of the expression tree. * If called with no arguments it will return the currently configured value. * - * @param string $conjunction value to be used for joining conditions. If null it + * @param string|null $conjunction value to be used for joining conditions. If null it * will not set any value, but return the currently stored one * @return string|$this */ @@ -92,7 +92,7 @@ public function tieWith($conjunction = null) /** * Backwards compatible wrapper for tieWith() * - * @param string $conjunction value to be used for joining conditions. If null it + * @param string|null $conjunction value to be used for joining conditions. If null it * will not set any value, but return the currently stored one * @return string|$this * @deprecated 3.2.0 Use tieWith() instead diff --git a/src/Database/Query.php b/src/Database/Query.php index e45562471df..0665dd657ec 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -1698,7 +1698,7 @@ public function bufferResults($enable = null) * * When called with no arguments, the current TypeMap object is returned. * - * @param \Cake\Database\TypeMap $typeMap The map object to use + * @param \Cake\Database\TypeMap|null $typeMap The map object to use * @return $this|\Cake\Database\TypeMap */ public function selectTypeMap(TypeMap $typeMap = null) From bff580d311ef73f749f270c68e98083d02e492ee Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 22 Nov 2015 19:31:59 +0100 Subject: [PATCH 036/128] Re-applied an optimization that existed in the previous implementation. String results will not be "casted" using the identity function found in the StringType class, as a way of sparing some function calls. --- src/Database/FieldTypeConverter.php | 8 +++++ .../Type/OptionalConvertInterface.php | 31 +++++++++++++++++++ src/Database/Type/StringType.php | 12 ++++++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/Database/Type/OptionalConvertInterface.php diff --git a/src/Database/FieldTypeConverter.php b/src/Database/FieldTypeConverter.php index afdb17333ae..07913281ed1 100644 --- a/src/Database/FieldTypeConverter.php +++ b/src/Database/FieldTypeConverter.php @@ -14,6 +14,8 @@ */ namespace Cake\Database; +use Cake\Database\Type\OptionalConvertInterface; + /** * A callable class to be used for processing each of the rows in a statement * result, so that the values are converted to the right PHP types. @@ -49,6 +51,12 @@ public function __construct(TypeMap $typeMap, Driver $driver) $types = array_map(['Cake\Database\Type', 'build'], array_combine($types, $types)); $result = []; + foreach ($types as $k => $type) { + if ($type instanceof OptionalConvertInterface && !$type->requiresToPHPCast()) { + unset($types[$k]); + } + } + foreach ($map as $field => $type) { if (isset($types[$type])) { $result[$field] = $types[$type]; diff --git a/src/Database/Type/OptionalConvertInterface.php b/src/Database/Type/OptionalConvertInterface.php new file mode 100644 index 00000000000..1e4bc513aba --- /dev/null +++ b/src/Database/Type/OptionalConvertInterface.php @@ -0,0 +1,31 @@ + Date: Mon, 23 Nov 2015 02:30:40 +0530 Subject: [PATCH 037/128] Fix CS error --- src/Database/Type/OptionalConvertInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Type/OptionalConvertInterface.php b/src/Database/Type/OptionalConvertInterface.php index 1e4bc513aba..f9d6757813a 100644 --- a/src/Database/Type/OptionalConvertInterface.php +++ b/src/Database/Type/OptionalConvertInterface.php @@ -25,7 +25,7 @@ interface OptionalConvertInterface * Returns whehter the cast to PHP is required to be invoked, since * it is not a indentity function. * - * @return boolean + * @return bool */ public function requiresToPHPCast(); } From 0bfab982d93e3cc9eea7016d5819bf1fc7f0c103 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 22 Nov 2015 16:29:29 -0500 Subject: [PATCH 038/128] Add a build() method to CorsBuilder. Having a build() method lets us do 2 things: * Call the cors methods in any order. * Still enforce the constraint that headers should only be set for when an allowed origin is set. It also makes the CorsBuilder easier to integrate in the future when we support PSR7 request/response object. --- src/Network/CorsBuilder.php | 40 ++++---- tests/TestCase/Network/CorsBuilderTest.php | 105 ++++++++++++--------- 2 files changed, 76 insertions(+), 69 deletions(-) diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php index 934a48310da..11b30be8e1a 100644 --- a/src/Network/CorsBuilder.php +++ b/src/Network/CorsBuilder.php @@ -9,6 +9,7 @@ class CorsBuilder protected $_response; protected $_origin; protected $_isSsl; + protected $_headers = []; public function __construct(Response $response, $origin, $isSsl = false) { @@ -17,18 +18,26 @@ public function __construct(Response $response, $origin, $isSsl = false) $this->_response = $response; } - public function allowOrigin($domain) + public function build() { if (empty($this->_origin)) { - return $this; + return $this->_response; + } + if (isset($this->_headers['Access-Control-Allow-Origin'])) { + $this->_response->header($this->_headers); } + return $this->_response; + } + + public function allowOrigin($domain) + { $allowed = $this->_normalizeDomains((array)$domain); foreach ($allowed as $domain) { if (!preg_match($domain['preg'], $this->_origin)) { continue; } $value = $domain['original'] === '*' ? '*' : $this->_origin; - $this->_response->header('Access-Control-Allow-Origin', $value); + $this->_headers['Access-Control-Allow-Origin'] = $value; break; } return $this; @@ -61,46 +70,31 @@ protected function _normalizeDomains($domains) public function allowMethods(array $methods) { - if (empty($this->_origin)) { - return $this; - } - $this->_response->header('Access-Control-Allow-Methods', implode(', ', $methods)); + $this->_headers['Access-Control-Allow-Methods'] = implode(', ', $methods); return $this; } public function allowCredentials() { - if (empty($this->_origin)) { - return $this; - } - $this->_response->header('Access-Control-Allow-Credentials', 'true'); + $this->_headers['Access-Control-Allow-Credentials'] = 'true'; return $this; } public function allowHeaders(array $headers) { - if (empty($this->_origin)) { - return $this; - } - $this->_response->header('Access-Control-Allow-Headers', implode(', ', $headers)); + $this->_headers['Access-Control-Allow-Headers'] = implode(', ', $headers); return $this; } public function exposeHeaders(array $headers) { - if (empty($this->_origin)) { - return $this; - } - $this->_response->header('Access-Control-Expose-Headers', implode(', ', $headers)); + $this->_headers['Access-Control-Expose-Headers'] = implode(', ', $headers); return $this; } public function maxAge($age) { - if (empty($this->_origin)) { - return $this; - } - $this->_response->header('Access-Control-Max-Age', $age); + $this->_headers['Access-Control-Max-Age'] = $age; return $this; } } diff --git a/tests/TestCase/Network/CorsBuilderTest.php b/tests/TestCase/Network/CorsBuilderTest.php index d31d8f8d40b..d480fb029be 100644 --- a/tests/TestCase/Network/CorsBuilderTest.php +++ b/tests/TestCase/Network/CorsBuilderTest.php @@ -17,7 +17,7 @@ public function testAllowOriginNoOrigin() $response = new Response(); $builder = new CorsBuilder($response, ''); $this->assertSame($builder, $builder->allowOrigin(['*.example.com', '*.foo.com'])); - $this->assertNoHeader($response, 'Access-Control-Origin'); + $this->assertNoHeader($builder->build(), 'Access-Control-Origin'); } /** @@ -30,17 +30,18 @@ public function testAllowOrigin() $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin('*')); - $this->assertHeader('*', $response, 'Access-Control-Allow-Origin'); + $this->assertHeader('*', $builder->build(), 'Access-Control-Allow-Origin'); $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin(['*.example.com', '*.foo.com'])); - $this->assertHeader('http://www.example.com', $response, 'Access-Control-Allow-Origin'); + $builder->build(); + $this->assertHeader('http://www.example.com', $builder->build(), 'Access-Control-Allow-Origin'); $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin('*.example.com')); - $this->assertHeader('http://www.example.com', $response, 'Access-Control-Allow-Origin'); + $this->assertHeader('http://www.example.com', $builder->build(), 'Access-Control-Allow-Origin'); } /** @@ -58,92 +59,104 @@ public function testAllowOriginSsl() $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com', true); $this->assertSame($builder, $builder->allowOrigin('https://example.com')); - $this->assertNoHeader($response, 'Access-Control-Allow-Origin'); + $this->assertNoHeader($builder->build(), 'Access-Control-Allow-Origin'); $response = new Response(); $builder = new CorsBuilder($response, 'http://www.example.com'); $this->assertSame($builder, $builder->allowOrigin('https://example.com')); - $this->assertNoHeader($response, 'Access-Control-Allow-Origin'); - } - - public function testAllowMethodsNoOrigin() - { - $response = new Response(); - $builder = new CorsBuilder($response, ''); - $this->assertSame($builder, $builder->allowMethods(['GET', 'POST'])); - $this->assertNoHeader($response, 'Access-Control-Allow-Methods'); + $this->assertNoHeader($builder->build(), 'Access-Control-Allow-Origin'); } public function testAllowMethods() { $response = new Response(); $builder = new CorsBuilder($response, 'http://example.com'); + $builder->allowOrigin('*'); $this->assertSame($builder, $builder->allowMethods(['GET', 'POST'])); - $this->assertHeader('GET, POST', $response, 'Access-Control-Allow-Methods'); - } - - public function testAllowCredentialsNoOrigin() - { - $response = new Response(); - $builder = new CorsBuilder($response, ''); - $this->assertSame($builder, $builder->allowCredentials()); - $this->assertNoHeader($response, 'Access-Control-Allow-Credentials'); + $this->assertHeader('GET, POST', $builder->build(), 'Access-Control-Allow-Methods'); } public function testAllowCredentials() { $response = new Response(); $builder = new CorsBuilder($response, 'http://example.com'); + $builder->allowOrigin('*'); $this->assertSame($builder, $builder->allowCredentials()); - $this->assertHeader('true', $response, 'Access-Control-Allow-Credentials'); - } - - public function testAllowHeadersNoOrigin() - { - $response = new Response(); - $builder = new CorsBuilder($response, ''); - $this->assertSame($builder, $builder->allowHeaders(['X-THING'])); - $this->assertNoHeader($response, 'Access-Control-Allow-Headers'); + $this->assertHeader('true', $builder->build(), 'Access-Control-Allow-Credentials'); } public function testAllowHeaders() { $response = new Response(); $builder = new CorsBuilder($response, 'http://example.com'); + $builder->allowOrigin('*'); $this->assertSame($builder, $builder->allowHeaders(['Content-Type', 'Accept'])); - $this->assertHeader('Content-Type, Accept', $response, 'Access-Control-Allow-Headers'); + $this->assertHeader('Content-Type, Accept', $builder->build(), 'Access-Control-Allow-Headers'); } - public function testExposeHeadersNoOrigin() + public function testExposeHeaders() { $response = new Response(); - $builder = new CorsBuilder($response, ''); - $this->assertSame($builder, $builder->exposeHeaders(['X-THING'])); - $this->assertNoHeader($response, 'Access-Control-Expose-Headers'); + $builder = new CorsBuilder($response, 'http://example.com'); + $builder->allowOrigin('*'); + $this->assertSame($builder, $builder->exposeHeaders(['Content-Type', 'Accept'])); + $this->assertHeader('Content-Type, Accept', $builder->build(), 'Access-Control-Expose-Headers'); } - public function testExposeHeaders() + public function testMaxAge() { $response = new Response(); $builder = new CorsBuilder($response, 'http://example.com'); - $this->assertSame($builder, $builder->exposeHeaders(['Content-Type', 'Accept'])); - $this->assertHeader('Content-Type, Accept', $response, 'Access-Control-Expose-Headers'); + $builder->allowOrigin('*'); + $this->assertSame($builder, $builder->maxAge(300)); + $this->assertHeader('300', $builder->build(), 'Access-Control-Max-Age'); } - public function testMaxAgeNoOrigin() + /** + * When no origin is allowed, none of the other headers should be applied. + * + * @return void + */ + public function testNoAllowedOriginNoHeadersSet() { $response = new Response(); - $builder = new CorsBuilder($response, ''); - $this->assertSame($builder, $builder->maxAge(300)); + $builder = new CorsBuilder($response, 'http://example.com'); + $response = $builder->allowCredentials() + ->allowMethods(['GET', 'POST']) + ->allowHeaders(['Content-Type']) + ->exposeHeaders(['X-CSRF-Token']) + ->maxAge(300) + ->build(); + $this->assertNoHeader($response, 'Access-Control-Allow-Origin'); + $this->assertNoHeader($response, 'Access-Control-Allow-Headers'); + $this->assertNoHeader($response, 'Access-Control-Expose-Headers'); + $this->assertNoHeader($response, 'Access-Control-Allow-Methods'); + $this->assertNoHeader($response, 'Access-Control-Allow-Authentication'); $this->assertNoHeader($response, 'Access-Control-Max-Age'); } - public function testMaxAge() + /** + * When an invalid origin is used, none of the other headers should be applied. + * + * @return void + */ + public function testInvalidAllowedOriginNoHeadersSet() { $response = new Response(); $builder = new CorsBuilder($response, 'http://example.com'); - $this->assertSame($builder, $builder->maxAge(300)); - $this->assertHeader('300', $response, 'Access-Control-Max-Age'); + $response = $builder->allowOrigin(['http://google.com']) + ->allowCredentials() + ->allowMethods(['GET', 'POST']) + ->allowHeaders(['Content-Type']) + ->exposeHeaders(['X-CSRF-Token']) + ->maxAge(300) + ->build(); + $this->assertNoHeader($response, 'Access-Control-Allow-Origin'); + $this->assertNoHeader($response, 'Access-Control-Allow-Headers'); + $this->assertNoHeader($response, 'Access-Control-Expose-Headers'); + $this->assertNoHeader($response, 'Access-Control-Allow-Methods'); + $this->assertNoHeader($response, 'Access-Control-Allow-Authentication'); + $this->assertNoHeader($response, 'Access-Control-Max-Age'); } /** From a15df3ae42e4f9bba104454c007bf5e0f3558a7a Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 22 Nov 2015 16:41:57 -0500 Subject: [PATCH 039/128] Update Response test. header() is only called once now, and using a mock doesn't make a lot of sense anymore. --- src/Network/Response.php | 15 +++++++-------- tests/TestCase/Network/ResponseTest.php | 25 ++++++++++++------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/Network/Response.php b/src/Network/Response.php index f94e2b32ff7..55d808d6ddd 100644 --- a/src/Network/Response.php +++ b/src/Network/Response.php @@ -1365,15 +1365,14 @@ public function cors(Request $request, $allowedDomains = [], $allowedMethods = [ if (!$origin) { return $builder; } - if ($allowedDomains) { - $builder->allowOrigin($allowedDomains); - } - if ($allowedMethods) { - $builder->allowMethods((array)$allowedMethods); - } - if ($allowedHeaders) { - $builder->allowHeaders((array)$allowedHeaders); + if (empty($allowedDomains) && empty($allowedMethods) && empty($allowedHeaders)) { + return $builder; } + + $builder->allowOrigin($allowedDomains) + ->allowMethods((array)$allowedMethods) + ->allowHeaders((array)$allowedHeaders) + ->build(); return $builder; } diff --git a/tests/TestCase/Network/ResponseTest.php b/tests/TestCase/Network/ResponseTest.php index e34ff8ef7ff..637cf4709a2 100644 --- a/tests/TestCase/Network/ResponseTest.php +++ b/tests/TestCase/Network/ResponseTest.php @@ -1082,25 +1082,24 @@ public function testCookieSettings() public function testCors($request, $origin, $domains, $methods, $headers, $expectedOrigin, $expectedMethods = false, $expectedHeaders = false) { $request->env('HTTP_ORIGIN', $origin); + $response = new Response(); - $response = $this->getMock('Cake\Network\Response', ['header']); - - $method = $response->expects(!$expectedOrigin ? $this->never() : $this->at(0))->method('header'); - $expectedOrigin && $method->with('Access-Control-Allow-Origin', $expectedOrigin ? $expectedOrigin : $this->anything()); + $result = $response->cors($request, $domains, $methods, $headers); + $this->assertInstanceOf('Cake\Network\CorsBuilder', $result); - $i = 1; + $headers = $response->header(); + if ($expectedOrigin) { + $this->assertArrayHasKey('Access-Control-Allow-Origin', $headers); + $this->assertEquals($expectedOrigin, $headers['Access-Control-Allow-Origin']); + } if ($expectedMethods) { - $response->expects($this->at($i++)) - ->method('header') - ->with('Access-Control-Allow-Methods', $expectedMethods ? $expectedMethods : $this->anything()); + $this->assertArrayHasKey('Access-Control-Allow-Methods', $headers); + $this->assertEquals($expectedMethods, $headers['Access-Control-Allow-Methods']); } if ($expectedHeaders) { - $response->expects($this->at($i++)) - ->method('header') - ->with('Access-Control-Allow-Headers', $expectedHeaders ? $expectedHeaders : $this->anything()); + $this->assertArrayHasKey('Access-Control-Allow-Headers', $headers); + $this->assertEquals($expectedHeaders, $headers['Access-Control-Allow-Headers']); } - - $response->cors($request, $domains, $methods, $headers); unset($_SERVER['HTTP_ORIGIN']); } From 1c6ac85d070357da524c09de6628f6d47078a44c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 22 Nov 2015 21:01:43 -0500 Subject: [PATCH 040/128] Extract RelativeTimeFormatter from Time & Date. Pulling these methods out into a separate object will let code be shared with the immutable variants that are being added. It also allows for code to be shared between date and time objects. --- src/I18n/Date.php | 161 +----------- src/I18n/RelativeTimeFormatter.php | 406 +++++++++++++++++++++++++++++ src/I18n/Time.php | 206 +-------------- tests/TestCase/I18n/TimeTest.php | 6 +- 4 files changed, 411 insertions(+), 368 deletions(-) create mode 100644 src/I18n/RelativeTimeFormatter.php diff --git a/src/I18n/Date.php b/src/I18n/Date.php index 8cb2693967b..38d6187d74a 100644 --- a/src/I18n/Date.php +++ b/src/I18n/Date.php @@ -130,165 +130,6 @@ class Date extends MutableDate implements JsonSerializable */ public function timeAgoInWords(array $options = []) { - $date = $this; - - $options += [ - 'from' => static::now(), - 'timezone' => null, - 'format' => static::$wordFormat, - 'accuracy' => static::$wordAccuracy, - 'end' => static::$wordEnd, - 'relativeString' => __d('cake', '%s ago'), - 'absoluteString' => __d('cake', 'on %s'), - ]; - if (is_string($options['accuracy'])) { - foreach (static::$wordAccuracy as $key => $level) { - $options[$key] = $options['accuracy']; - } - } else { - $options['accuracy'] += static::$wordAccuracy; - } - if ($options['timezone']) { - $date = $date->timezone($options['timezone']); - } - - $now = $options['from']->format('U'); - $inSeconds = $date->format('U'); - $backwards = ($inSeconds > $now); - - $futureTime = $now; - $pastTime = $inSeconds; - if ($backwards) { - $futureTime = $inSeconds; - $pastTime = $now; - } - $diff = $futureTime - $pastTime; - - if (!$diff) { - return __d('cake', 'today'); - } - - if ($diff > abs($now - (new static($options['end']))->format('U'))) { - return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); - } - - // If more than a week, then take into account the length of months - if ($diff >= 604800) { - list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); - - list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime)); - $weeks = $days = $hours = $minutes = $seconds = 0; - - $years = $future['Y'] - $past['Y']; - $months = $future['m'] + ((12 * $years) - $past['m']); - - if ($months >= 12) { - $years = floor($months / 12); - $months = $months - ($years * 12); - } - if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { - $years--; - } - - if ($future['d'] >= $past['d']) { - $days = $future['d'] - $past['d']; - } else { - $daysInPastMonth = date('t', $pastTime); - $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); - - if (!$backwards) { - $days = ($daysInPastMonth - $past['d']) + $future['d']; - } else { - $days = ($daysInFutureMonth - $past['d']) + $future['d']; - } - - if ($future['m'] != $past['m']) { - $months--; - } - } - - if (!$months && $years >= 1 && $diff < ($years * 31536000)) { - $months = 11; - $years--; - } - - if ($months >= 12) { - $years = $years + 1; - $months = $months - 12; - } - - if ($days >= 7) { - $weeks = floor($days / 7); - $days = $days - ($weeks * 7); - } - } else { - $years = $months = $weeks = 0; - $days = floor($diff / 86400); - - $diff = $diff - ($days * 86400); - - $hours = floor($diff / 3600); - $diff = $diff - ($hours * 3600); - - $minutes = floor($diff / 60); - $diff = $diff - ($minutes * 60); - $seconds = $diff; - } - - $fWord = $options['accuracy']['day']; - if ($years > 0) { - $fWord = $options['accuracy']['year']; - } elseif (abs($months) > 0) { - $fWord = $options['accuracy']['month']; - } elseif (abs($weeks) > 0) { - $fWord = $options['accuracy']['week']; - } elseif (abs($days) > 0) { - $fWord = $options['accuracy']['day']; - } - - $fNum = str_replace(['year', 'month', 'week', 'day'], [1, 2, 3, 4], $fWord); - - $relativeDate = ''; - if ($fNum >= 1 && $years > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} year', '{0} years', $years, $years); - } - if ($fNum >= 2 && $months > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} month', '{0} months', $months, $months); - } - if ($fNum >= 3 && $weeks > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); - } - if ($fNum >= 4 && $days > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} day', '{0} days', $days, $days); - } - - // When time has passed - if (!$backwards && $relativeDate) { - return sprintf($options['relativeString'], $relativeDate); - } - if (!$backwards) { - $aboutAgo = [ - 'day' => __d('cake', 'about a day ago'), - 'week' => __d('cake', 'about a week ago'), - 'month' => __d('cake', 'about a month ago'), - 'year' => __d('cake', 'about a year ago') - ]; - - return $aboutAgo[$fWord]; - } - - // When time is to come - if (!$relativeDate) { - $aboutIn = [ - 'day' => __d('cake', 'in about a day'), - 'week' => __d('cake', 'in about a week'), - 'month' => __d('cake', 'in about a month'), - 'year' => __d('cake', 'in about a year') - ]; - - return $aboutIn[$fWord]; - } - - return $relativeDate; + return (new RelativeTimeFormatter($this))->dateAgoInWords($options); } } diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php new file mode 100644 index 00000000000..3d3143967d7 --- /dev/null +++ b/src/I18n/RelativeTimeFormatter.php @@ -0,0 +1,406 @@ +_time = $time; + } + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current time and this object. + * + * ### Options: + * + * - `from` => another Time object representing the "now" time + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "hour") + * - hour => The format if hours > 0 (default "minute") + * - minute => The format if minutes > 0 (default "minute") + * - second => The format if seconds > 0 (default "second") + * - `end` => The end of relative time telling + * - `relativeString` => The printf compatible string when outputting relative time + * - `absoluteString` => The printf compatible string when outputting absolute time + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 15 seconds ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + $time = $this->_time; + + $timezone = null; + // TODO use options like below. + $format = FrozenTime::$wordFormat; + $end = FrozenTime::$wordEnd; + $relativeString = __d('cake', '%s ago'); + $absoluteString = __d('cake', 'on %s'); + $accuracy = FrozenTime::$wordAccuracy; + $from = FrozenTime::now(); + $opts = ['timezone', 'format', 'end', 'relativeString', 'absoluteString', 'from']; + + foreach ($opts as $option) { + if (isset($options[$option])) { + ${$option} = $options[$option]; + unset($options[$option]); + } + } + + if (isset($options['accuracy'])) { + if (is_array($options['accuracy'])) { + $accuracy = $options['accuracy'] + $accuracy; + } else { + foreach ($accuracy as $key => $level) { + $accuracy[$key] = $options['accuracy']; + } + } + } + + if ($timezone) { + $time = $time->timezone($timezone); + } + + $now = $from->format('U'); + $inSeconds = $time->format('U'); + $backwards = ($inSeconds > $now); + + $futureTime = $now; + $pastTime = $inSeconds; + if ($backwards) { + $futureTime = $inSeconds; + $pastTime = $now; + } + $diff = $futureTime - $pastTime; + + if (!$diff) { + return __d('cake', 'just now', 'just now'); + } + + if ($diff > abs($now - (new FrozenTime($end))->format('U'))) { + return sprintf($absoluteString, $time->i18nFormat($format)); + } + + // If more than a week, then take into account the length of months + if ($diff >= 604800) { + list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); + + list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime)); + $weeks = $days = $hours = $minutes = $seconds = 0; + + $years = $future['Y'] - $past['Y']; + $months = $future['m'] + ((12 * $years) - $past['m']); + + if ($months >= 12) { + $years = floor($months / 12); + $months = $months - ($years * 12); + } + if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { + $years--; + } + + if ($future['d'] >= $past['d']) { + $days = $future['d'] - $past['d']; + } else { + $daysInPastMonth = date('t', $pastTime); + $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); + + if (!$backwards) { + $days = ($daysInPastMonth - $past['d']) + $future['d']; + } else { + $days = ($daysInFutureMonth - $past['d']) + $future['d']; + } + + if ($future['m'] != $past['m']) { + $months--; + } + } + + if (!$months && $years >= 1 && $diff < ($years * 31536000)) { + $months = 11; + $years--; + } + + if ($months >= 12) { + $years = $years + 1; + $months = $months - 12; + } + + if ($days >= 7) { + $weeks = floor($days / 7); + $days = $days - ($weeks * 7); + } + } else { + $years = $months = $weeks = 0; + $days = floor($diff / 86400); + + $diff = $diff - ($days * 86400); + + $hours = floor($diff / 3600); + $diff = $diff - ($hours * 3600); + + $minutes = floor($diff / 60); + $diff = $diff - ($minutes * 60); + $seconds = $diff; + } + + $fWord = $accuracy['second']; + if ($years > 0) { + $fWord = $accuracy['year']; + } elseif (abs($months) > 0) { + $fWord = $accuracy['month']; + } elseif (abs($weeks) > 0) { + $fWord = $accuracy['week']; + } elseif (abs($days) > 0) { + $fWord = $accuracy['day']; + } elseif (abs($hours) > 0) { + $fWord = $accuracy['hour']; + } elseif (abs($minutes) > 0) { + $fWord = $accuracy['minute']; + } + + $fNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $fWord); + + $relativeDate = ''; + if ($fNum >= 1 && $years > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} year', '{0} years', $years, $years); + } + if ($fNum >= 2 && $months > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} month', '{0} months', $months, $months); + } + if ($fNum >= 3 && $weeks > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); + } + if ($fNum >= 4 && $days > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} day', '{0} days', $days, $days); + } + if ($fNum >= 5 && $hours > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} hour', '{0} hours', $hours, $hours); + } + if ($fNum >= 6 && $minutes > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} minute', '{0} minutes', $minutes, $minutes); + } + if ($fNum >= 7 && $seconds > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} second', '{0} seconds', $seconds, $seconds); + } + + // When time has passed + if (!$backwards && $relativeDate) { + return sprintf($relativeString, $relativeDate); + } + if (!$backwards) { + $aboutAgo = [ + 'second' => __d('cake', 'about a second ago'), + 'minute' => __d('cake', 'about a minute ago'), + 'hour' => __d('cake', 'about an hour ago'), + 'day' => __d('cake', 'about a day ago'), + 'week' => __d('cake', 'about a week ago'), + 'year' => __d('cake', 'about a year ago') + ]; + + return $aboutAgo[$fWord]; + } + + // When time is to come + if (!$relativeDate) { + $aboutIn = [ + 'second' => __d('cake', 'in about a second'), + 'minute' => __d('cake', 'in about a minute'), + 'hour' => __d('cake', 'in about an hour'), + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'year' => __d('cake', 'in about a year') + ]; + + return $aboutIn[$fWord]; + } + + return $relativeDate; + } + + public function dateAgoInWords(array $options = []) + { + $date = $this->_time; + $options += [ + 'from' => FrozenDate::now(), + 'timezone' => null, + 'format' => FrozenDate::$wordFormat, + 'accuracy' => FrozenDate::$wordAccuracy, + 'end' => FrozenDate::$wordEnd, + 'relativeString' => __d('cake', '%s ago'), + 'absoluteString' => __d('cake', 'on %s'), + ]; + if (is_string($options['accuracy'])) { + foreach (FrozenDate::$wordAccuracy as $key => $level) { + $options[$key] = $options['accuracy']; + } + } else { + $options['accuracy'] += FrozenDate::$wordAccuracy; + } + if ($options['timezone']) { + $date = $date->timezone($options['timezone']); + } + + $now = $options['from']->format('U'); + $inSeconds = $date->format('U'); + $backwards = ($inSeconds > $now); + + $futureTime = $now; + $pastTime = $inSeconds; + if ($backwards) { + $futureTime = $inSeconds; + $pastTime = $now; + } + $diff = $futureTime - $pastTime; + + if (!$diff) { + return __d('cake', 'today'); + } + + if ($diff > abs($now - (new FrozenDate($options['end']))->format('U'))) { + return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); + } + + // If more than a week, then take into account the length of months + if ($diff >= 604800) { + list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); + + list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime)); + $weeks = $days = $hours = $minutes = $seconds = 0; + + $years = $future['Y'] - $past['Y']; + $months = $future['m'] + ((12 * $years) - $past['m']); + + if ($months >= 12) { + $years = floor($months / 12); + $months = $months - ($years * 12); + } + if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { + $years--; + } + + if ($future['d'] >= $past['d']) { + $days = $future['d'] - $past['d']; + } else { + $daysInPastMonth = date('t', $pastTime); + $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); + + if (!$backwards) { + $days = ($daysInPastMonth - $past['d']) + $future['d']; + } else { + $days = ($daysInFutureMonth - $past['d']) + $future['d']; + } + + if ($future['m'] != $past['m']) { + $months--; + } + } + + if (!$months && $years >= 1 && $diff < ($years * 31536000)) { + $months = 11; + $years--; + } + + if ($months >= 12) { + $years = $years + 1; + $months = $months - 12; + } + + if ($days >= 7) { + $weeks = floor($days / 7); + $days = $days - ($weeks * 7); + } + } else { + $years = $months = $weeks = 0; + $days = floor($diff / 86400); + + $diff = $diff - ($days * 86400); + + $hours = floor($diff / 3600); + $diff = $diff - ($hours * 3600); + + $minutes = floor($diff / 60); + $diff = $diff - ($minutes * 60); + $seconds = $diff; + } + + $fWord = $options['accuracy']['day']; + if ($years > 0) { + $fWord = $options['accuracy']['year']; + } elseif (abs($months) > 0) { + $fWord = $options['accuracy']['month']; + } elseif (abs($weeks) > 0) { + $fWord = $options['accuracy']['week']; + } elseif (abs($days) > 0) { + $fWord = $options['accuracy']['day']; + } + + $fNum = str_replace(['year', 'month', 'week', 'day'], [1, 2, 3, 4], $fWord); + + $relativeDate = ''; + if ($fNum >= 1 && $years > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} year', '{0} years', $years, $years); + } + if ($fNum >= 2 && $months > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} month', '{0} months', $months, $months); + } + if ($fNum >= 3 && $weeks > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); + } + if ($fNum >= 4 && $days > 0) { + $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} day', '{0} days', $days, $days); + } + + // When time has passed + if (!$backwards && $relativeDate) { + return sprintf($options['relativeString'], $relativeDate); + } + if (!$backwards) { + $aboutAgo = [ + 'day' => __d('cake', 'about a day ago'), + 'week' => __d('cake', 'about a week ago'), + 'month' => __d('cake', 'about a month ago'), + 'year' => __d('cake', 'about a year ago') + ]; + + return $aboutAgo[$fWord]; + } + + // When time is to come + if (!$relativeDate) { + $aboutIn = [ + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'month' => __d('cake', 'in about a month'), + 'year' => __d('cake', 'in about a year') + ]; + + return $aboutIn[$fWord]; + } + return $relativeDate; + } +} diff --git a/src/I18n/Time.php b/src/I18n/Time.php index a1d7798bf69..de7eb9f2c28 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -153,211 +153,7 @@ public function __construct($time = null, $tz = null) */ public function timeAgoInWords(array $options = []) { - $time = $this; - - $timezone = null; - $format = static::$wordFormat; - $end = static::$wordEnd; - $relativeString = __d('cake', '%s ago'); - $absoluteString = __d('cake', 'on %s'); - $accuracy = static::$wordAccuracy; - $from = static::now(); - $opts = ['timezone', 'format', 'end', 'relativeString', 'absoluteString', 'from']; - - foreach ($opts as $option) { - if (isset($options[$option])) { - ${$option} = $options[$option]; - unset($options[$option]); - } - } - - if (isset($options['accuracy'])) { - if (is_array($options['accuracy'])) { - $accuracy = $options['accuracy'] + $accuracy; - } else { - foreach ($accuracy as $key => $level) { - $accuracy[$key] = $options['accuracy']; - } - } - } - - if ($timezone) { - $time = $time->timezone($timezone); - } - - $now = $from->format('U'); - $inSeconds = $time->format('U'); - $backwards = ($inSeconds > $now); - - $futureTime = $now; - $pastTime = $inSeconds; - if ($backwards) { - $futureTime = $inSeconds; - $pastTime = $now; - } - $diff = $futureTime - $pastTime; - - if (!$diff) { - return __d('cake', 'just now', 'just now'); - } - - if ($diff > abs($now - (new static($end))->format('U'))) { - return sprintf($absoluteString, $time->i18nFormat($format)); - } - - // If more than a week, then take into account the length of months - if ($diff >= 604800) { - list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); - - list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime)); - $weeks = $days = $hours = $minutes = $seconds = 0; - - $years = $future['Y'] - $past['Y']; - $months = $future['m'] + ((12 * $years) - $past['m']); - - if ($months >= 12) { - $years = floor($months / 12); - $months = $months - ($years * 12); - } - if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { - $years--; - } - - if ($future['d'] >= $past['d']) { - $days = $future['d'] - $past['d']; - } else { - $daysInPastMonth = date('t', $pastTime); - $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); - - if (!$backwards) { - $days = ($daysInPastMonth - $past['d']) + $future['d']; - } else { - $days = ($daysInFutureMonth - $past['d']) + $future['d']; - } - - if ($future['m'] != $past['m']) { - $months--; - } - } - - if (!$months && $years >= 1 && $diff < ($years * 31536000)) { - $months = 11; - $years--; - } - - if ($months >= 12) { - $years = $years + 1; - $months = $months - 12; - } - - if ($days >= 7) { - $weeks = floor($days / 7); - $days = $days - ($weeks * 7); - } - } else { - $years = $months = $weeks = 0; - $days = floor($diff / 86400); - - $diff = $diff - ($days * 86400); - - $hours = floor($diff / 3600); - $diff = $diff - ($hours * 3600); - - $minutes = floor($diff / 60); - $diff = $diff - ($minutes * 60); - $seconds = $diff; - } - - $fWord = $accuracy['second']; - if ($years > 0) { - $fWord = $accuracy['year']; - } elseif (abs($months) > 0) { - $fWord = $accuracy['month']; - } elseif (abs($weeks) > 0) { - $fWord = $accuracy['week']; - } elseif (abs($days) > 0) { - $fWord = $accuracy['day']; - } elseif (abs($hours) > 0) { - $fWord = $accuracy['hour']; - } elseif (abs($minutes) > 0) { - $fWord = $accuracy['minute']; - } - - $fNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $fWord); - - $relativeDate = ''; - if ($fNum >= 1 && $years > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} year', '{0} years', $years, $years); - } - if ($fNum >= 2 && $months > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} month', '{0} months', $months, $months); - } - if ($fNum >= 3 && $weeks > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); - } - if ($fNum >= 4 && $days > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} day', '{0} days', $days, $days); - } - if ($fNum >= 5 && $hours > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} hour', '{0} hours', $hours, $hours); - } - if ($fNum >= 6 && $minutes > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} minute', '{0} minutes', $minutes, $minutes); - } - if ($fNum >= 7 && $seconds > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} second', '{0} seconds', $seconds, $seconds); - } - - // When time has passed - if (!$backwards && $relativeDate) { - return sprintf($relativeString, $relativeDate); - } - if (!$backwards) { - $aboutAgo = [ - 'second' => __d('cake', 'about a second ago'), - 'minute' => __d('cake', 'about a minute ago'), - 'hour' => __d('cake', 'about an hour ago'), - 'day' => __d('cake', 'about a day ago'), - 'week' => __d('cake', 'about a week ago'), - 'year' => __d('cake', 'about a year ago') - ]; - - return $aboutAgo[$fWord]; - } - - // When time is to come - if (!$relativeDate) { - $aboutIn = [ - 'second' => __d('cake', 'in about a second'), - 'minute' => __d('cake', 'in about a minute'), - 'hour' => __d('cake', 'in about an hour'), - 'day' => __d('cake', 'in about a day'), - 'week' => __d('cake', 'in about a week'), - 'year' => __d('cake', 'in about a year') - ]; - - return $aboutIn[$fWord]; - } - - return $relativeDate; - } - - /** - * Returns the difference between this date and the provided one in a human - * readable format. - * - * See `Time::timeAgoInWords()` for a full list of options that can be passed - * to this method. - * - * @param \Cake\Chronos\ChronosInterface|null $other the date to diff with - * @param array $options options accepted by timeAgoInWords - * @return string - * @see Time::timeAgoInWords() - */ - public function diffForHumans(ChronosInterface $other = null, array $options = []) - { - $options = ['from' => $other] + $options; - return $this->timeAgoInWords($options); + return (new RelativeTimeFormatter($this))->timeAgoInWords($options); } /** diff --git a/tests/TestCase/I18n/TimeTest.php b/tests/TestCase/I18n/TimeTest.php index 8959c83af5a..04d8a77fd42 100644 --- a/tests/TestCase/I18n/TimeTest.php +++ b/tests/TestCase/I18n/TimeTest.php @@ -569,13 +569,13 @@ public function testDiffForHumans() { $time = new Time('2014-04-20 10:10:10'); $other = new Time('2014-04-27 10:10:10'); - $this->assertEquals('1 week ago', $time->diffForHumans($other)); + $this->assertEquals('1 week before', $time->diffForHumans($other)); $other = new Time('2014-04-21 09:10:10'); - $this->assertEquals('23 hours ago', $time->diffForHumans($other)); + $this->assertEquals('23 hours before', $time->diffForHumans($other)); $other = new Time('2014-04-13 09:10:10'); - $this->assertEquals('1 week', $time->diffForHumans($other)); + $this->assertEquals('1 week after', $time->diffForHumans($other)); } /** From 6aa0bf7c4b6d606a31c47951a575c9c9a8a1ba68 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 22 Nov 2015 21:23:54 -0500 Subject: [PATCH 041/128] Implement FrozenTime. This class has a bunch of copy and paste from Time, but because of static public properties, I don't really see a better way to do it. Without a __staticGet, there isn't a good way to consolidate this duplication. --- src/I18n/DateFormatTrait.php | 3 +- src/I18n/FrozenTime.php | 274 +++++++++++++++++++++++++++++++ tests/TestCase/I18n/TimeTest.php | 230 ++++++++++++++++---------- 3 files changed, 422 insertions(+), 85 deletions(-) create mode 100644 src/I18n/FrozenTime.php diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index 471a8bf751b..cfa2d55be6f 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -127,8 +127,9 @@ public function i18nFormat($format = null, $timezone = null, $locale = null) $time = $this; if ($timezone) { + // Handle the immutable and mutable object cases. $time = clone $this; - $time->timezone($timezone); + $time = $time->timezone($timezone); } $format = $format !== null ? $format : static::$_toStringFormat; diff --git a/src/I18n/FrozenTime.php b/src/I18n/FrozenTime.php new file mode 100644 index 00000000000..b1b0a545e76 --- /dev/null +++ b/src/I18n/FrozenTime.php @@ -0,0 +1,274 @@ + "day", + 'month' => "day", + 'week' => "day", + 'day' => "hour", + 'hour' => "minute", + 'minute' => "minute", + 'second' => "second", + ]; + + /** + * The end of relative time telling + * + * @var string + * @see \Cake\I18n\FrozenTime::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * {@inheritDoc} + */ + public function __construct($time = null, $tz = null) + { + if ($time instanceof DateTime) { + $tz = $time->getTimeZone(); + $time = $time->format('Y-m-d H:i:s'); + } + + if (is_numeric($time)) { + $time = '@' . $time; + } + + parent::__construct($time, $tz); + } + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current time and this object. + * + * ### Options: + * + * - `from` => another Time object representing the "now" time + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "hour") + * - hour => The format if hours > 0 (default "minute") + * - minute => The format if minutes > 0 (default "minute") + * - second => The format if seconds > 0 (default "second") + * - `end` => The end of relative time telling + * - `relativeString` => The printf compatible string when outputting relative time + * - `absoluteString` => The printf compatible string when outputting absolute time + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 15 seconds ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + return (new RelativeTimeFormatter($this))->timeAgoInWords($options); + } + + /** + * Get list of timezone identifiers + * + * @param int|string $filter A regex to filter identifier + * Or one of DateTimeZone class constants + * @param string $country A two-letter ISO 3166-1 compatible country code. + * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY + * @param bool|array $options If true (default value) groups the identifiers list by primary region. + * Otherwise, an array containing `group`, `abbr`, `before`, and `after` + * keys. Setting `group` and `abbr` to true will group results and append + * timezone abbreviation in the display value. Set `before` and `after` + * to customize the abbreviation wrapper. + * @return array List of timezone identifiers + * @since 2.2 + */ + public static function listTimezones($filter = null, $country = null, $options = []) + { + if (is_bool($options)) { + $options = [ + 'group' => $options, + ]; + } + $defaults = [ + 'group' => true, + 'abbr' => false, + 'before' => ' - ', + 'after' => null, + ]; + $options += $defaults; + $group = $options['group']; + + $regex = null; + if (is_string($filter)) { + $regex = $filter; + $filter = null; + } + if ($filter === null) { + $filter = DateTimeZone::ALL; + } + $identifiers = DateTimeZone::listIdentifiers($filter, $country); + + if ($regex) { + foreach ($identifiers as $key => $tz) { + if (!preg_match($regex, $tz)) { + unset($identifiers[$key]); + } + } + } + + if ($group) { + $groupedIdentifiers = []; + $now = time(); + $before = $options['before']; + $after = $options['after']; + foreach ($identifiers as $key => $tz) { + $abbr = null; + if ($options['abbr']) { + $dateTimeZone = new DateTimeZone($tz); + $trans = $dateTimeZone->getTransitions($now, $now); + $abbr = isset($trans[0]['abbr']) ? + $before . $trans[0]['abbr'] . $after : + null; + } + $item = explode('/', $tz, 2); + if (isset($item[1])) { + $groupedIdentifiers[$item[0]][$tz] = $item[1] . $abbr; + } else { + $groupedIdentifiers[$item[0]] = [$tz => $item[0] . $abbr]; + } + } + return $groupedIdentifiers; + } + return array_combine($identifiers, $identifiers); + } + + /** + * Returns true this instance will happen within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function wasWithinLast($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + $timeInterval = $tmp . ' days'; + } + return parent::wasWithinLast($timeInterval); + } + + /** + * Returns true this instance happened within the specified interval + * + * This overridden method provides backwards compatible behavior for integers, + * or strings with trailing spaces. This behavior is *deprecated* and will be + * removed in future versions of CakePHP. + * + * @param string|int $timeInterval the numeric value with space then time type. + * Example of valid types: 6 hours, 2 days, 1 minute. + * @return bool + */ + public function isWithinNext($timeInterval) + { + $tmp = trim($timeInterval); + if (is_numeric($tmp)) { + $timeInterval = $tmp . ' days'; + } + return parent::isWithinNext($timeInterval); + } +} diff --git a/tests/TestCase/I18n/TimeTest.php b/tests/TestCase/I18n/TimeTest.php index 04d8a77fd42..2e4317e3c34 100644 --- a/tests/TestCase/I18n/TimeTest.php +++ b/tests/TestCase/I18n/TimeTest.php @@ -14,6 +14,7 @@ */ namespace Cake\Test\TestCase\I18n; +use Cake\I18n\FrozenTime; use Cake\I18n\Time; use Cake\TestSuite\TestCase; @@ -32,8 +33,10 @@ public function setUp() { parent::setUp(); $this->now = Time::getTestNow(); + $this->frozenNow = FrozenTime::getTestNow(); $this->locale = Time::$defaultLocale; Time::$defaultLocale = 'en_US'; + FrozenTime::$defaultLocale = 'en_US'; } /** @@ -47,6 +50,10 @@ public function tearDown() Time::setTestNow($this->now); Time::$defaultLocale = $this->locale; Time::resetToStringFormat(); + + FrozenTime::setTestNow($this->frozenNow); + FrozenTime::$defaultLocale = $this->locale; + FrozenTime::resetToStringFormat(); date_default_timezone_set('UTC'); } @@ -60,6 +67,16 @@ protected function _restoreSystemTimezone() date_default_timezone_set($this->_systemTimezoneIdentifier); } + /** + * Provider for ensuring that Time and FrozenTime work the same way. + * + * @return void + */ + public static function classNameProvider() + { + return ['mutable' => ['Cake\I18n\Time'], 'immutable' => ['Cake\I18n\FrozenTime']]; + } + /** * provider for timeAgoInWords() tests * @@ -97,6 +114,19 @@ public function testTimeAgoInWords($input, $expected) $this->assertEquals($expected, $result); } + /** + * testTimeAgoInWords method + * + * @dataProvider timeAgoProvider + * @return void + */ + public function testTimeAgoInWordsFrozenTime($input, $expected) + { + $time = new FrozenTime($input); + $result = $time->timeAgoInWords(); + $this->assertEquals($expected, $result); + } + /** * provider for timeAgo with an end date. * @@ -145,11 +175,12 @@ public function timeAgoEndProvider() /** * test the timezone option for timeAgoInWords * + * @dataProvider classNameProvider * @return void */ - public function testTimeAgoInWordsTimezone() + public function testTimeAgoInWordsTimezone($class) { - $time = new Time('1990-07-31 20:33:00 UTC'); + $time = new FrozenTime('1990-07-31 20:33:00 UTC'); $result = $time->timeAgoInWords( [ 'timezone' => 'America/Vancouver', @@ -176,11 +207,12 @@ public function testTimeAgoInWordsEnd($input, $expected, $end) /** * test the custom string options for timeAgoInWords * + * @dataProvider classNameProvider * @return void */ - public function testTimeAgoInWordsCustomStrings() + public function testTimeAgoInWordsCustomStrings($class) { - $time = new Time('-8 years -4 months -2 weeks -3 days'); + $time = new $class('-8 years -4 months -2 weeks -3 days'); $result = $time->timeAgoInWords([ 'relativeString' => 'at least %s ago', 'accuracy' => ['year' => 'year'], @@ -189,7 +221,7 @@ public function testTimeAgoInWordsCustomStrings() $expected = 'at least 8 years ago'; $this->assertEquals($expected, $result); - $time = new Time('+4 months +2 weeks +3 days'); + $time = new $class('+4 months +2 weeks +3 days'); $result = $time->timeAgoInWords([ 'absoluteString' => 'exactly on %s', 'accuracy' => ['year' => 'year'], @@ -202,11 +234,12 @@ public function testTimeAgoInWordsCustomStrings() /** * Test the accuracy option for timeAgoInWords() * + * @dataProvider classNameProvider * @return void */ - public function testTimeAgoInWordsAccuracy() + public function testTimeAgoInWordsAccuracy($class) { - $time = new Time('+8 years +4 months +2 weeks +3 days'); + $time = new $class('+8 years +4 months +2 weeks +3 days'); $result = $time->timeAgoInWords([ 'accuracy' => ['year' => 'year'], 'end' => '+10 years' @@ -214,7 +247,7 @@ public function testTimeAgoInWordsAccuracy() $expected = '8 years'; $this->assertEquals($expected, $result); - $time = new Time('+8 years +4 months +2 weeks +3 days'); + $time = new $class('+8 years +4 months +2 weeks +3 days'); $result = $time->timeAgoInWords([ 'accuracy' => ['year' => 'month'], 'end' => '+10 years' @@ -222,7 +255,7 @@ public function testTimeAgoInWordsAccuracy() $expected = '8 years, 4 months'; $this->assertEquals($expected, $result); - $time = new Time('+8 years +4 months +2 weeks +3 days'); + $time = new $class('+8 years +4 months +2 weeks +3 days'); $result = $time->timeAgoInWords([ 'accuracy' => ['year' => 'week'], 'end' => '+10 years' @@ -230,7 +263,7 @@ public function testTimeAgoInWordsAccuracy() $expected = '8 years, 4 months, 2 weeks'; $this->assertEquals($expected, $result); - $time = new Time('+8 years +4 months +2 weeks +3 days'); + $time = new $class('+8 years +4 months +2 weeks +3 days'); $result = $time->timeAgoInWords([ 'accuracy' => ['year' => 'day'], 'end' => '+10 years' @@ -238,7 +271,7 @@ public function testTimeAgoInWordsAccuracy() $expected = '8 years, 4 months, 2 weeks, 3 days'; $this->assertEquals($expected, $result); - $time = new Time('+1 years +5 weeks'); + $time = new $class('+1 years +5 weeks'); $result = $time->timeAgoInWords([ 'accuracy' => ['year' => 'year'], 'end' => '+10 years' @@ -246,14 +279,14 @@ public function testTimeAgoInWordsAccuracy() $expected = '1 year'; $this->assertEquals($expected, $result); - $time = new Time('+58 minutes'); + $time = new $class('+58 minutes'); $result = $time->timeAgoInWords([ 'accuracy' => 'hour' ]); $expected = 'in about an hour'; $this->assertEquals($expected, $result); - $time = new Time('+23 hours'); + $time = new $class('+23 hours'); $result = $time->timeAgoInWords([ 'accuracy' => 'day' ]); @@ -264,19 +297,20 @@ public function testTimeAgoInWordsAccuracy() /** * Test the format option of timeAgoInWords() * + * @dataProvider classNameProvider * @return void */ - public function testTimeAgoInWordsWithFormat() + public function testTimeAgoInWordsWithFormat($class) { - $time = new Time('2007-9-25'); + $time = new $class('2007-9-25'); $result = $time->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertEquals('on 2007-09-25', $result); - $time = new Time('+2 weeks +2 days'); + $time = new $class('+2 weeks +2 days'); $result = $time->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertRegExp('/^2 weeks, [1|2] day(s)?$/', $result); - $time = new Time('+2 months +2 days'); + $time = new $class('+2 months +2 days'); $result = $time->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']); $this->assertEquals('on ' . date('Y-m-d', strtotime('+2 months +2 days')), $result); } @@ -284,57 +318,58 @@ public function testTimeAgoInWordsWithFormat() /** * test timeAgoInWords() with negative values. * + * @dataProvider classNameProvider * @return void */ - public function testTimeAgoInWordsNegativeValues() + public function testTimeAgoInWordsNegativeValues($class) { - $time = new Time('-2 months -2 days'); + $time = new $class('-2 months -2 days'); $result = $time->timeAgoInWords(['end' => '3 month']); $this->assertEquals('2 months, 2 days ago', $result); - $time = new Time('-2 months -2 days'); + $time = new $class('-2 months -2 days'); $result = $time->timeAgoInWords(['end' => '3 month']); $this->assertEquals('2 months, 2 days ago', $result); - $time = new Time('-2 months -2 days'); + $time = new $class('-2 months -2 days'); $result = $time->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']); $this->assertEquals('on ' . date('Y-m-d', strtotime('-2 months -2 days')), $result); - $time = new Time('-2 years -5 months -2 days'); + $time = new $class('-2 years -5 months -2 days'); $result = $time->timeAgoInWords(['end' => '3 years']); $this->assertEquals('2 years, 5 months, 2 days ago', $result); - $time = new Time('-2 weeks -2 days'); + $time = new $class('-2 weeks -2 days'); $result = $time->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertEquals('2 weeks, 2 days ago', $result); - $time = new Time('-3 years -12 months'); + $time = new $class('-3 years -12 months'); $result = $time->timeAgoInWords(); $expected = 'on ' . $time->format('n/j/y'); $this->assertEquals($expected, $result); - $time = new Time('-1 month -1 week -6 days'); + $time = new $class('-1 month -1 week -6 days'); $result = $time->timeAgoInWords( ['end' => '1 year', 'accuracy' => ['month' => 'month']] ); $this->assertEquals('1 month ago', $result); - $time = new Time('-1 years -2 weeks -3 days'); + $time = new $class('-1 years -2 weeks -3 days'); $result = $time->timeAgoInWords( ['accuracy' => ['year' => 'year']] ); $expected = 'on ' . $time->format('n/j/y'); $this->assertEquals($expected, $result); - $time = new Time('-13 months -5 days'); + $time = new $class('-13 months -5 days'); $result = $time->timeAgoInWords(['end' => '2 years']); $this->assertEquals('1 year, 1 month, 5 days ago', $result); - $time = new Time('-58 minutes'); + $time = new $class('-58 minutes'); $result = $time->timeAgoInWords(['accuracy' => 'hour']); $this->assertEquals('about an hour ago', $result); - $time = new Time('-23 hours'); + $time = new $class('-23 hours'); $result = $time->timeAgoInWords(['accuracy' => 'day']); $this->assertEquals('about a day ago', $result); } @@ -342,11 +377,12 @@ public function testTimeAgoInWordsNegativeValues() /** * testNice method * + * @dataProvider classNameProvider * @return void */ - public function testNice() + public function testNice($class) { - $time = new Time('2014-04-20 20:00', 'UTC'); + $time = new $class('2014-04-20 20:00', 'UTC'); $this->assertTimeFormat('Apr 20, 2014, 8:00 PM', $time->nice()); $result = $time->nice('America/New_York'); @@ -360,11 +396,12 @@ public function testNice() /** * test formatting dates taking in account preferred i18n locale file * + * @dataProvider classNameProvider * @return void */ - public function testI18nFormat() + public function testI18nFormat($class) { - $time = new Time('Thu Jan 14 13:59:28 2010'); + $time = new $class('Thu Jan 14 13:59:28 2010'); $result = $time->i18nFormat(); $expected = '1/14/10, 1:59 PM'; $this->assertTimeFormat($expected, $result); @@ -382,7 +419,7 @@ public function testI18nFormat() $expected = '00:59:28'; $this->assertTimeFormat($expected, $result); - Time::$defaultLocale = 'fr-FR'; + $class::$defaultLocale = 'fr-FR'; $result = $time->i18nFormat(\IntlDateFormatter::FULL); $expected = 'jeudi 14 janvier 2010 13:59:28 UTC'; $this->assertTimeFormat($expected, $result); @@ -435,22 +472,23 @@ public function testI18nFormat() /** * test formatting dates with offset style timezone * + * @dataProvider classNameProvider * @see https://github.com/facebook/hhvm/issues/3637 * @return void */ - public function testI18nFormatWithOffsetTimezone() + public function testI18nFormatWithOffsetTimezone($class) { - $time = new Time('2014-01-01T00:00:00+00'); + $time = new $class('2014-01-01T00:00:00+00'); $result = $time->i18nFormat(\IntlDateFormatter::FULL); $expected = 'Wednesday January 1 2014 12:00:00 AM GMT'; $this->assertTimeFormat($expected, $result); - $time = new Time('2014-01-01T00:00:00+09'); + $time = new $class('2014-01-01T00:00:00+09'); $result = $time->i18nFormat(\IntlDateFormatter::FULL); $expected = 'Wednesday January 1 2014 12:00:00 AM GMT+09:00'; $this->assertTimeFormat($expected, $result); - $time = new Time('2014-01-01T00:00:00-01:30'); + $time = new $class('2014-01-01T00:00:00-01:30'); $result = $time->i18nFormat(\IntlDateFormatter::FULL); $expected = 'Wednesday January 1 2014 12:00:00 AM GMT-01:30'; $this->assertTimeFormat($expected, $result); @@ -459,11 +497,12 @@ public function testI18nFormatWithOffsetTimezone() /** * testListTimezones * + * @dataProvider classNameProvider * @return void */ - public function testListTimezones() + public function testListTimezones($class) { - $return = Time::listTimezones(); + $return = $class::listTimezones(); $this->assertTrue(isset($return['Asia']['Asia/Bangkok'])); $this->assertEquals('Bangkok', $return['Asia']['Asia/Bangkok']); $this->assertTrue(isset($return['America']['America/Argentina/Buenos_Aires'])); @@ -472,16 +511,16 @@ public function testListTimezones() $this->assertFalse(isset($return['Cuba'])); $this->assertFalse(isset($return['US'])); - $return = Time::listTimezones('#^Asia/#'); + $return = $class::listTimezones('#^Asia/#'); $this->assertTrue(isset($return['Asia']['Asia/Bangkok'])); $this->assertFalse(isset($return['Pacific'])); - $return = Time::listTimezones(null, null, ['abbr' => true]); + $return = $class::listTimezones(null, null, ['abbr' => true]); $this->assertTrue(isset($return['Asia']['Asia/Jakarta'])); $this->assertEquals('Jakarta - WIB', $return['Asia']['Asia/Jakarta']); $this->assertEquals('Regina - CST', $return['America']['America/Regina']); - $return = Time::listTimezones(null, null, [ + $return = $class::listTimezones(null, null, [ 'abbr' => true, 'before' => ' (', 'after' => ')', @@ -489,15 +528,15 @@ public function testListTimezones() $this->assertEquals('Jayapura (WIT)', $return['Asia']['Asia/Jayapura']); $this->assertEquals('Regina (CST)', $return['America']['America/Regina']); - $return = Time::listTimezones('#^(America|Pacific)/#', null, false); + $return = $class::listTimezones('#^(America|Pacific)/#', null, false); $this->assertTrue(isset($return['America/Argentina/Buenos_Aires'])); $this->assertTrue(isset($return['Pacific/Tahiti'])); - $return = Time::listTimezones(\DateTimeZone::ASIA); + $return = $class::listTimezones(\DateTimeZone::ASIA); $this->assertTrue(isset($return['Asia']['Asia/Bangkok'])); $this->assertFalse(isset($return['Pacific'])); - $return = Time::listTimezones(\DateTimeZone::PER_COUNTRY, 'US', false); + $return = $class::listTimezones(\DateTimeZone::PER_COUNTRY, 'US', false); $this->assertTrue(isset($return['Pacific/Honolulu'])); $this->assertFalse(isset($return['Asia/Bangkok'])); } @@ -505,13 +544,14 @@ public function testListTimezones() /** * Tests that __toString uses the i18n formatter * + * @dataProvider classNameProvider * @return void */ - public function testToString() + public function testToString($class) { - $time = new Time('2014-04-20 22:10'); - Time::$defaultLocale = 'fr-FR'; - Time::setToStringFormat(\IntlDateFormatter::FULL); + $time = new $class('2014-04-20 22:10'); + $class::$defaultLocale = 'fr-FR'; + $class::setToStringFormat(\IntlDateFormatter::FULL); $this->assertTimeFormat('dimanche 20 avril 2014 22:10:00 UTC', (string)$time); } @@ -542,20 +582,34 @@ public function testToStringInvalid($value) $this->assertNotEmpty((string)$time); } + /** + * Test that invalid datetime values do not trigger errors. + * + * @dataProvider invalidDataProvider + * @return void + */ + public function testToStringInvalidFrozen($value) + { + $time = new FrozenTime($value); + $this->assertInternalType('string', (string)$time); + $this->assertNotEmpty((string)$time); + } + /** * These invalid values are not invalid on windows :( * + * @dataProvider classNameProvider * @return void */ - public function testToStringInvalidZeros() + public function testToStringInvalidZeros($class) { $this->skipIf(DS === '\\', 'All zeros are valid on windows.'); $this->skipIf(PHP_INT_SIZE === 4, 'IntlDateFormatter throws exceptions on 32-bit systems'); - $time = new Time('0000-00-00'); + $time = new $class('0000-00-00'); $this->assertInternalType('string', (string)$time); $this->assertNotEmpty((string)$time); - $time = new Time('0000-00-00 00:00:00'); + $time = new $class('0000-00-00 00:00:00'); $this->assertInternalType('string', (string)$time); $this->assertNotEmpty((string)$time); } @@ -563,46 +617,50 @@ public function testToStringInvalidZeros() /** * Tests diffForHumans * + * @dataProvider classNameProvider * @return void */ - public function testDiffForHumans() + public function testDiffForHumans($class) { - $time = new Time('2014-04-20 10:10:10'); - $other = new Time('2014-04-27 10:10:10'); + $time = new $class('2014-04-20 10:10:10'); + $other = new $class('2014-04-27 10:10:10'); $this->assertEquals('1 week before', $time->diffForHumans($other)); - $other = new Time('2014-04-21 09:10:10'); + $other = new $class('2014-04-21 09:10:10'); $this->assertEquals('23 hours before', $time->diffForHumans($other)); - $other = new Time('2014-04-13 09:10:10'); + $other = new $class('2014-04-13 09:10:10'); $this->assertEquals('1 week after', $time->diffForHumans($other)); } /** * Tests encoding a Time object as json * + * @dataProvider classNameProvider * @return void */ - public function testJsonEnconde() + public function testJsonEnconde($class) { - $time = new Time('2014-04-20 10:10:10'); + $time = new $class('2014-04-20 10:10:10'); $this->assertEquals('"2014-04-20T10:10:10+0000"', json_encode($time)); - Time::setJsonEncodeFormat('yyyy-MM-dd HH:mm:ss'); + + $class::setJsonEncodeFormat('yyyy-MM-dd HH:mm:ss'); $this->assertEquals('"2014-04-20 10:10:10"', json_encode($time)); } /** * Tests debugInfo * + * @dataProvider classNameProvider * @return void */ - public function testDebugInfo() + public function testDebugInfo($class) { - $time = new Time('2014-04-20 10:10:10'); + $time = new $class('2014-04-20 10:10:10'); $expected = [ 'time' => '2014-04-20T10:10:10+00:00', 'timezone' => 'UTC', - 'fixedNowTime' => Time::getTestNow()->toIso8601String() + 'fixedNowTime' => $class::getTestNow()->toIso8601String() ]; $this->assertEquals($expected, $time->__debugInfo()); } @@ -610,47 +668,49 @@ public function testDebugInfo() /** * Tests parsing a string into a Time object based on the locale format. * + * @dataProvider classNameProvider * @return void */ - public function testParseDateTime() + public function testParseDateTime($class) { - $time = Time::parseDateTime('10/13/2013 12:54am'); + $time = $class::parseDateTime('10/13/2013 12:54am'); $this->assertNotNull($time); $this->assertEquals('2013-10-13 00:54', $time->format('Y-m-d H:i')); - Time::$defaultLocale = 'fr-FR'; - $time = Time::parseDateTime('13 10, 2013 12:54'); + $class::$defaultLocale = 'fr-FR'; + $time = $class::parseDateTime('13 10, 2013 12:54'); $this->assertNotNull($time); $this->assertEquals('2013-10-13 12:54', $time->format('Y-m-d H:i')); - $time = Time::parseDateTime('13 foo 10 2013 12:54'); + $time = $class::parseDateTime('13 foo 10 2013 12:54'); $this->assertNull($time); } /** * Tests parsing a string into a Time object based on the locale format. * + * @dataProvider classNameProvider * @return void */ - public function testParseDate() + public function testParseDate($class) { - $time = Time::parseDate('10/13/2013 12:54am'); + $time = $class::parseDate('10/13/2013 12:54am'); $this->assertNotNull($time); $this->assertEquals('2013-10-13 00:00', $time->format('Y-m-d H:i')); - $time = Time::parseDate('10/13/2013'); + $time = $class::parseDate('10/13/2013'); $this->assertNotNull($time); $this->assertEquals('2013-10-13 00:00', $time->format('Y-m-d H:i')); - Time::$defaultLocale = 'fr-FR'; - $time = Time::parseDate('13 10, 2013 12:54'); + $class::$defaultLocale = 'fr-FR'; + $time = $class::parseDate('13 10, 2013 12:54'); $this->assertNotNull($time); $this->assertEquals('2013-10-13 00:00', $time->format('Y-m-d H:i')); - $time = Time::parseDate('13 foo 10 2013 12:54'); + $time = $class::parseDate('13 foo 10 2013 12:54'); $this->assertNull($time); - $time = Time::parseDate('13 10, 2013', 'dd M, y'); + $time = $class::parseDate('13 10, 2013', 'dd M, y'); $this->assertNotNull($time); $this->assertEquals('2013-10-13', $time->format('Y-m-d')); } @@ -658,33 +718,35 @@ public function testParseDate() /** * Tests parsing times using the parseTime function * + * @dataProvider classNameProvider * @return void */ - public function testParseTime() + public function testParseTime($class) { - $time = Time::parseTime('12:54am'); + $time = $class::parseTime('12:54am'); $this->assertNotNull($time); $this->assertEquals('00:54:00', $time->format('H:i:s')); - Time::$defaultLocale = 'fr-FR'; - $time = Time::parseTime('23:54'); + $class::$defaultLocale = 'fr-FR'; + $time = $class::parseTime('23:54'); $this->assertNotNull($time); $this->assertEquals('23:54:00', $time->format('H:i:s')); - $time = Time::parseTime('31c2:54'); + $time = $class::parseTime('31c2:54'); $this->assertNull($time); } /** * Tests that parsing a date respects de default timezone in PHP. * + * @dataProvider classNameProvider * @return void */ - public function testParseDateDifferentTimezone() + public function testParseDateDifferentTimezone($class) { date_default_timezone_set('Europe/Paris'); - Time::$defaultLocale = 'fr-FR'; - $result = Time::parseDate('12/03/2015'); + $class::$defaultLocale = 'fr-FR'; + $result = $class::parseDate('12/03/2015'); $this->assertEquals('2015-03-12', $result->format('Y-m-d')); $this->assertEquals(new \DateTimeZone('Europe/Paris'), $result->tz); } From 173815a6b76fc7e377b0de3a747d5c322f63775f Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 23 Nov 2015 09:55:16 +0100 Subject: [PATCH 042/128] Implemented a Type::buildAll() method to cleanup This helped cleanup another part of the code --- src/Database/FieldTypeConverter.php | 3 +-- src/Database/Type.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Database/FieldTypeConverter.php b/src/Database/FieldTypeConverter.php index 07913281ed1..6dff4899eba 100644 --- a/src/Database/FieldTypeConverter.php +++ b/src/Database/FieldTypeConverter.php @@ -47,8 +47,7 @@ public function __construct(TypeMap $typeMap, Driver $driver) { $this->_driver = $driver; $map = $typeMap->toArray(); - $types = array_keys(Type::map()); - $types = array_map(['Cake\Database\Type', 'build'], array_combine($types, $types)); + $types = Type::buildAll(); $result = []; foreach ($types as $k => $type) { diff --git a/src/Database/Type.php b/src/Database/Type.php index 30d9f2fc597..f24b8704614 100644 --- a/src/Database/Type.php +++ b/src/Database/Type.php @@ -105,6 +105,20 @@ public static function build($name) return static::$_builtTypes[$name] = new static::$_types[$name]($name); } + /** + * Returns an arrays with all the mapped type objects, indexed by name + * + * @return array + */ + public static function buildAll() + { + $result = []; + foreach (self::$_types as $name => $type) { + $result[$name] = isset(static::$_builtTypes[$name]) ? static::$_builtTypes[$name] : static::build($name); + } + return $result; + } + /** * Returns a Type object capable of converting a type identified by $name * From 8bcfeb3a60e4c8979392bee09005f80d8b3d3903 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 23 Nov 2015 10:42:09 +0100 Subject: [PATCH 043/128] Also cloning the select type map when cloning the query --- src/Database/Query.php | 5 ++++- tests/TestCase/Database/QueryTest.php | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 0665dd657ec..f48e40e8793 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -1790,9 +1790,12 @@ protected function _dirty() public function __clone() { $this->_iterator = null; - if ($this->_valueBinder) { + if ($this->_valueBinder !== null) { $this->_valueBinder = clone $this->_valueBinder; } + if ($this->_selectTypeMap !== null) { + $this->_selectTypeMap = clone $this->_selectTypeMap; + } foreach ($this->_parts as $name => $part) { if (empty($part)) { continue; diff --git a/tests/TestCase/Database/QueryTest.php b/tests/TestCase/Database/QueryTest.php index bf108d0c738..bdd16e0639f 100644 --- a/tests/TestCase/Database/QueryTest.php +++ b/tests/TestCase/Database/QueryTest.php @@ -3487,6 +3487,11 @@ public function testDeepClone() $query->order(['Articles.title' => 'ASC']); $this->assertNotEquals($query->clause('order'), $dupe->clause('order')); + + $this->assertNotSame( + $query->selectTypeMap(), + $dupe->selectTypeMap() + ); } /** From 2d363d85237cff381b6b4b48acb8f5f9b4378672 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 23 Nov 2015 10:49:37 +0100 Subject: [PATCH 044/128] Changing casing of function in interface --- src/Database/FieldTypeConverter.php | 2 +- src/Database/Type/OptionalConvertInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/FieldTypeConverter.php b/src/Database/FieldTypeConverter.php index 6dff4899eba..71744319ca2 100644 --- a/src/Database/FieldTypeConverter.php +++ b/src/Database/FieldTypeConverter.php @@ -51,7 +51,7 @@ public function __construct(TypeMap $typeMap, Driver $driver) $result = []; foreach ($types as $k => $type) { - if ($type instanceof OptionalConvertInterface && !$type->requiresToPHPCast()) { + if ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast()) { unset($types[$k]); } } diff --git a/src/Database/Type/OptionalConvertInterface.php b/src/Database/Type/OptionalConvertInterface.php index 1e4bc513aba..ba311e5d7f7 100644 --- a/src/Database/Type/OptionalConvertInterface.php +++ b/src/Database/Type/OptionalConvertInterface.php @@ -27,5 +27,5 @@ interface OptionalConvertInterface * * @return boolean */ - public function requiresToPHPCast(); + public function requiresToPhpCast(); } From 5b0db649401b0b39346efeb5580506c451c8cd21 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 23 Nov 2015 21:46:51 +0100 Subject: [PATCH 045/128] Changed the casing to another function --- src/Database/Type/StringType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Type/StringType.php b/src/Database/Type/StringType.php index f2f03dc33f1..e3898fdff9a 100644 --- a/src/Database/Type/StringType.php +++ b/src/Database/Type/StringType.php @@ -100,7 +100,7 @@ public function marshal($value) * * @return boolean False as databse results are returned already as strings */ - public function requiresToPHPCast() + public function requiresToPhpCast() { return false; } From 3fde5d8b4b7f6416197afbe2bb5dcaee78a4bdd3 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 23 Nov 2015 22:28:46 -0500 Subject: [PATCH 046/128] Add doc blocks for CorsBuilder. --- src/Network/CorsBuilder.php | 102 +++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php index 11b30be8e1a..7ca55563957 100644 --- a/src/Network/CorsBuilder.php +++ b/src/Network/CorsBuilder.php @@ -1,16 +1,70 @@ _origin = $origin; @@ -18,6 +72,14 @@ public function __construct(Response $response, $origin, $isSsl = false) $this->_response = $response; } + /** + * Apply the queued headers to the response. + * + * If the builer has no Origin, or if there are no allowed domains, + * or if the allowed domains do not match the Origin header no headers will be applied. + * + * @return \Cake\Network\Response + */ public function build() { if (empty($this->_origin)) { @@ -29,6 +91,15 @@ public function build() return $this->_response; } + /** + * Set the list of allowed domains. + * + * Accepts a string or an array of domains that have CORS enabled. + * You can use `*.example.com` wildcards to accept subdomains, or `*` to allow all domains + * + * @param string|array $domain The allowed domains + * @return $this + */ public function allowOrigin($domain) { $allowed = $this->_normalizeDomains((array)$domain); @@ -68,30 +139,59 @@ protected function _normalizeDomains($domains) return $result; } + /** + * Set the list of allowed HTTP Methods. + * + * @param array $domain The allowed HTTP methods + * @return $this + */ public function allowMethods(array $methods) { $this->_headers['Access-Control-Allow-Methods'] = implode(', ', $methods); return $this; } + /** + * Enable cookies to be sent in CORS requests. + * + * @return $this + */ public function allowCredentials() { $this->_headers['Access-Control-Allow-Credentials'] = 'true'; return $this; } + /** + * Whitelist headers that can be sent in CORS requests. + * + * @param array $headers The list of headers to accept in CORS requests. + * @return $this + */ public function allowHeaders(array $headers) { $this->_headers['Access-Control-Allow-Headers'] = implode(', ', $headers); return $this; } + /** + * Define the headers a client library/browser can expose to scripting + * + * @param array $headers The list of headers to expose CORS responses + * @return $this + */ public function exposeHeaders(array $headers) { $this->_headers['Access-Control-Expose-Headers'] = implode(', ', $headers); return $this; } + /** + * Define the max-age preflight OPTIONS requests are valid for. + * + * @param int $age The max-age for OPTIONS requests in seconds + * @return $this + */ public function maxAge($age) { $this->_headers['Access-Control-Max-Age'] = $age; From 08a054a5e075a0e148bb06ecd1c60de814a2f205 Mon Sep 17 00:00:00 2001 From: mark_story Date: Tue, 24 Nov 2015 14:39:53 -0500 Subject: [PATCH 047/128] Fix PHPCS errors. --- src/Network/CorsBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php index 7ca55563957..f3bb6d370fa 100644 --- a/src/Network/CorsBuilder.php +++ b/src/Network/CorsBuilder.php @@ -25,7 +25,7 @@ * a modified Response. * * It is most convenient to get this object via `Request::cors()`. - * + * * @see Cake\Network\Response::cors() */ class CorsBuilder @@ -142,7 +142,7 @@ protected function _normalizeDomains($domains) /** * Set the list of allowed HTTP Methods. * - * @param array $domain The allowed HTTP methods + * @param array $methods The allowed HTTP methods * @return $this */ public function allowMethods(array $methods) From d9d9ed4448d67ee3acd7f5bfc8a1cd7244b6fa09 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 23 Nov 2015 22:19:28 -0500 Subject: [PATCH 048/128] Add FrozenDate and related tests. Using a classNameProvider ensures that the Date and FrozenDate behaviors don't diverge over time. --- src/Database/Type/DateTimeType.php | 11 +++ src/Database/Type/DateType.php | 11 +++ src/I18n/FrozenDate.php | 137 ++++++++++++++++++++++++++++ tests/TestCase/I18n/DateTest.php | 139 +++++++++++++++++++---------- 4 files changed, 253 insertions(+), 45 deletions(-) create mode 100644 src/I18n/FrozenDate.php diff --git a/src/Database/Type/DateTimeType.php b/src/Database/Type/DateTimeType.php index 8f64e45534f..14782d74885 100644 --- a/src/Database/Type/DateTimeType.php +++ b/src/Database/Type/DateTimeType.php @@ -220,6 +220,17 @@ public function setLocaleFormat($format) return $this; } + /** + * Change the preferred class name to the FrozenTime implementation. + * + * @return $this + */ + public function useImmutable() + { + static::$dateTimeClass = 'Cake\I18n\FrozenTime'; + return $this; + } + /** * Converts a string into a DateTime object after parseing it using the locale * aware parser with the specified format. diff --git a/src/Database/Type/DateType.php b/src/Database/Type/DateType.php index bae140a78f2..a08a3d538d6 100644 --- a/src/Database/Type/DateType.php +++ b/src/Database/Type/DateType.php @@ -33,6 +33,17 @@ class DateType extends DateTimeType */ protected $_format = 'Y-m-d'; + /** + * Change the preferred class name to the FrozenDate implementation. + * + * @return $this + */ + public function useImmutable() + { + static::$dateTimeClass = 'Cake\I18n\FrozenDate'; + return $this; + } + /** * Convert request data into a datetime object. * diff --git a/src/I18n/FrozenDate.php b/src/I18n/FrozenDate.php new file mode 100644 index 00000000000..509dcefaed6 --- /dev/null +++ b/src/I18n/FrozenDate.php @@ -0,0 +1,137 @@ + "day", + 'month' => "day", + 'week' => "day", + 'day' => "day", + 'hour' => "day", + 'minute' => "day", + 'second' => "day", + ]; + + /** + * The end of relative time telling + * + * @var string + * @see \Cake\I18n\Date::timeAgoInWords() + */ + public static $wordEnd = '+1 month'; + + /** + * Returns either a relative or a formatted absolute date depending + * on the difference between the current date and this object. + * + * ### Options: + * + * - `from` => another Date object representing the "now" date + * - `format` => a fall back format if the relative time is longer than the duration specified by end + * - `accuracy` => Specifies how accurate the date should be described (array) + * - year => The format if years > 0 (default "day") + * - month => The format if months > 0 (default "day") + * - week => The format if weeks > 0 (default "day") + * - day => The format if weeks > 0 (default "day") + * - `end` => The end of relative date telling + * - `relativeString` => The printf compatible string when outputting relative date + * - `absoluteString` => The printf compatible string when outputting absolute date + * - `timezone` => The user timezone the timestamp should be formatted in. + * + * Relative dates look something like this: + * + * - 3 weeks, 4 days ago + * - 1 day ago + * + * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using + * `i18nFormat`, see the method for the valid formatting strings. + * + * The returned string includes 'ago' or 'on' and assumes you'll properly add a word + * like 'Posted ' before the function output. + * + * NOTE: If the difference is one week or more, the lowest level of accuracy is day. + * + * @param array $options Array of options. + * @return string Relative time string. + */ + public function timeAgoInWords(array $options = []) + { + return (new RelativeTimeFormatter($this))->dateAgoInWords($options); + } +} diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index 1ecf24454ab..20cae6b8b4f 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -14,6 +14,7 @@ */ namespace Cake\Test\TestCase\I18n; +use Cake\I18n\FrozenDate; use Cake\I18n\Date; use Cake\TestSuite\TestCase; use DateTimeZone; @@ -50,16 +51,28 @@ public function tearDown() { parent::tearDown(); Date::$defaultLocale = $this->locale; + FrozenDate::$defaultLocale = $this->locale; + } + + /** + * Provider for ensuring that Date and FrozenDate work the same way. + * + * @return void + */ + public static function classNameProvider() + { + return ['mutable' => ['Cake\I18n\Date'], 'immutable' => ['Cake\I18n\FrozenDate']]; } /** * test formatting dates taking in account preferred i18n locale file * + * @dataProvider classNameProvider * @return void */ - public function testI18nFormat() + public function testI18nFormat($class) { - $time = new Date('Thu Jan 14 13:59:28 2010'); + $time = new $class('Thu Jan 14 13:59:28 2010'); $result = $time->i18nFormat(); $expected = '1/14/10'; $this->assertEquals($expected, $result); @@ -73,7 +86,7 @@ public function testI18nFormat() $expected = '00:00:00'; $this->assertEquals($expected, $result); - Date::$defaultLocale = 'fr-FR'; + $class::$defaultLocale = 'fr-FR'; $result = $time->i18nFormat(\IntlDateFormatter::FULL); $expected = 'jeudi 14 janvier 2010 00:00:00 UTC'; $this->assertEquals($expected, $result); @@ -85,22 +98,24 @@ public function testI18nFormat() /** * test __toString * + * @dataProvider classNameProvider * @return void */ - public function testToString() + public function testToString($class) { - $date = new Date('2015-11-06 11:32:45'); + $date = new $class('2015-11-06 11:32:45'); $this->assertEquals('11/6/15', (string)$date); } /** * test nice() * + * @dataProvider classNameProvider * @return void */ - public function testNice() + public function testNice($class) { - $date = new Date('2015-11-06 11:32:45'); + $date = new $class('2015-11-06 11:32:45'); $this->assertEquals('Nov 6, 2015', $date->nice()); $this->assertEquals('Nov 6, 2015', $date->nice(new DateTimeZone('America/New_York'))); @@ -110,41 +125,44 @@ public function testNice() /** * test jsonSerialize() * + * @dataProvider classNameProvider * @return void */ - public function testJsonSerialize() + public function testJsonSerialize($class) { - $date = new Date('2015-11-06 11:32:45'); + $date = new $class('2015-11-06 11:32:45'); $this->assertEquals('"2015-11-06T00:00:00+0000"', json_encode($date)); } /** * test parseDate() * + * @dataProvider classNameProvider * @return void */ - public function testParseDate() + public function testParseDate($class) { - $date = Date::parseDate('11/6/15'); + $date = $class::parseDate('11/6/15'); $this->assertEquals('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s')); - Date::$defaultLocale = 'fr-FR'; - $date = Date::parseDate('13 10, 2015'); + $class::$defaultLocale = 'fr-FR'; + $date = $class::parseDate('13 10, 2015'); $this->assertEquals('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s')); } /** * test parseDateTime() * + * @dataProvider classNameProvider * @return void */ - public function testParseDateTime() + public function testParseDateTime($class) { - $date = Date::parseDate('11/6/15 12:33:12'); + $date = $class::parseDate('11/6/15 12:33:12'); $this->assertEquals('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s')); - Date::$defaultLocale = 'fr-FR'; - $date = Date::parseDate('13 10, 2015 12:54:12'); + $class::$defaultLocale = 'fr-FR'; + $date = $class::parseDate('13 10, 2015 12:54:12'); $this->assertEquals('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s')); } @@ -186,14 +204,28 @@ public function testTimeAgoInWords($input, $expected) $this->assertEquals($expected, $result); } + /** + * testTimeAgoInWords with Frozen Date + * + * @dataProvider timeAgoProvider + * @return void + */ + public function testTimeAgoInWordsFrozenDate($input, $expected) + { + $date = new FrozenDate($input); + $result = $date->timeAgoInWords(); + $this->assertEquals($expected, $result); + } + /** * test the timezone option for timeAgoInWords * + * @dataProvider classNameProvider * @return void */ - public function testTimeAgoInWordsTimezone() + public function testTimeAgoInWordsTimezone($class) { - $date = new Date('1990-07-31 20:33:00 UTC'); + $date = new $class('1990-07-31 20:33:00 UTC'); $result = $date->timeAgoInWords( [ 'timezone' => 'America/Vancouver', @@ -263,14 +295,28 @@ public function testTimeAgoInWordsEnd($input, $expected, $end) $this->assertEquals($expected, $result); } + /** + * test the end option for timeAgoInWords + * + * @dataProvider timeAgoEndProvider + * @return void + */ + public function testTimeAgoInWordsEndFrozenDate($input, $expected, $end) + { + $time = new FrozenDate($input); + $result = $time->timeAgoInWords(['end' => $end]); + $this->assertEquals($expected, $result); + } + /** * test the custom string options for timeAgoInWords * + * @dataProvider classNameProvider * @return void */ - public function testTimeAgoInWordsCustomStrings() + public function testTimeAgoInWordsCustomStrings($class) { - $date = new Date('-8 years -4 months -2 weeks -3 days'); + $date = new $class('-8 years -4 months -2 weeks -3 days'); $result = $date->timeAgoInWords([ 'relativeString' => 'at least %s ago', 'accuracy' => ['year' => 'year'], @@ -279,7 +325,7 @@ public function testTimeAgoInWordsCustomStrings() $expected = 'at least 8 years ago'; $this->assertEquals($expected, $result); - $date = new Date('+4 months +2 weeks +3 days'); + $date = new $class('+4 months +2 weeks +3 days'); $result = $date->timeAgoInWords([ 'absoluteString' => 'exactly on %s', 'accuracy' => ['year' => 'year'], @@ -292,11 +338,12 @@ public function testTimeAgoInWordsCustomStrings() /** * Test the accuracy option for timeAgoInWords() * + * @dataProvider classNameProvider * @return void */ - public function testDateAgoInWordsAccuracy() + public function testDateAgoInWordsAccuracy($class) { - $date = new Date('+8 years +4 months +2 weeks +3 days'); + $date = new $class('+8 years +4 months +2 weeks +3 days'); $result = $date->timeAgoInWords([ 'accuracy' => ['year' => 'year'], 'end' => '+10 years' @@ -304,7 +351,7 @@ public function testDateAgoInWordsAccuracy() $expected = '8 years'; $this->assertEquals($expected, $result); - $date = new Date('+8 years +4 months +2 weeks +3 days'); + $date = new $class('+8 years +4 months +2 weeks +3 days'); $result = $date->timeAgoInWords([ 'accuracy' => ['year' => 'month'], 'end' => '+10 years' @@ -312,7 +359,7 @@ public function testDateAgoInWordsAccuracy() $expected = '8 years, 4 months'; $this->assertEquals($expected, $result); - $date = new Date('+8 years +4 months +2 weeks +3 days'); + $date = new $class('+8 years +4 months +2 weeks +3 days'); $result = $date->timeAgoInWords([ 'accuracy' => ['year' => 'week'], 'end' => '+10 years' @@ -320,7 +367,7 @@ public function testDateAgoInWordsAccuracy() $expected = '8 years, 4 months, 2 weeks'; $this->assertEquals($expected, $result); - $date = new Date('+8 years +4 months +2 weeks +3 days'); + $date = new $class('+8 years +4 months +2 weeks +3 days'); $result = $date->timeAgoInWords([ 'accuracy' => ['year' => 'day'], 'end' => '+10 years' @@ -328,7 +375,7 @@ public function testDateAgoInWordsAccuracy() $expected = '8 years, 4 months, 2 weeks, 3 days'; $this->assertEquals($expected, $result); - $date = new Date('+1 years +5 weeks'); + $date = new $class('+1 years +5 weeks'); $result = $date->timeAgoInWords([ 'accuracy' => ['year' => 'year'], 'end' => '+10 years' @@ -336,7 +383,7 @@ public function testDateAgoInWordsAccuracy() $expected = '1 year'; $this->assertEquals($expected, $result); - $date = new Date('+23 hours'); + $date = new $class('+23 hours'); $result = $date->timeAgoInWords([ 'accuracy' => 'day' ]); @@ -347,23 +394,24 @@ public function testDateAgoInWordsAccuracy() /** * Test the format option of timeAgoInWords() * + * @dataProvider classNameProvider * @return void */ - public function testDateAgoInWordsWithFormat() + public function testDateAgoInWordsWithFormat($class) { - $date = new Date('2007-9-25'); + $date = new $class('2007-9-25'); $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertEquals('on 2007-09-25', $result); - $date = new Date('2007-9-25'); + $date = new $class('2007-9-25'); $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertEquals('on 2007-09-25', $result); - $date = new Date('+2 weeks +2 days'); + $date = new $class('+2 weeks +2 days'); $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertRegExp('/^2 weeks, [1|2] day(s)?$/', $result); - $date = new Date('+2 months +2 days'); + $date = new $class('+2 months +2 days'); $result = $date->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']); $this->assertEquals('on ' . date('Y-m-d', strtotime('+2 months +2 days')), $result); } @@ -371,53 +419,54 @@ public function testDateAgoInWordsWithFormat() /** * test timeAgoInWords() with negative values. * + * @dataProvider classNameProvider * @return void */ - public function testDateAgoInWordsNegativeValues() + public function testDateAgoInWordsNegativeValues($class) { - $date = new Date('-2 months -2 days'); + $date = new $class('-2 months -2 days'); $result = $date->timeAgoInWords(['end' => '3 month']); $this->assertEquals('2 months, 2 days ago', $result); - $date = new Date('-2 months -2 days'); + $date = new $class('-2 months -2 days'); $result = $date->timeAgoInWords(['end' => '3 month']); $this->assertEquals('2 months, 2 days ago', $result); - $date = new Date('-2 months -2 days'); + $date = new $class('-2 months -2 days'); $result = $date->timeAgoInWords(['end' => '1 month', 'format' => 'yyyy-MM-dd']); $this->assertEquals('on ' . date('Y-m-d', strtotime('-2 months -2 days')), $result); - $date = new Date('-2 years -5 months -2 days'); + $date = new $class('-2 years -5 months -2 days'); $result = $date->timeAgoInWords(['end' => '3 years']); $this->assertEquals('2 years, 5 months, 2 days ago', $result); - $date = new Date('-2 weeks -2 days'); + $date = new $class('-2 weeks -2 days'); $result = $date->timeAgoInWords(['format' => 'yyyy-MM-dd']); $this->assertEquals('2 weeks, 2 days ago', $result); - $date = new Date('-3 years -12 months'); + $date = new $class('-3 years -12 months'); $result = $date->timeAgoInWords(); $expected = 'on ' . $date->format('n/j/y'); $this->assertEquals($expected, $result); - $date = new Date('-1 month -1 week -6 days'); + $date = new $class('-1 month -1 week -6 days'); $result = $date->timeAgoInWords( ['end' => '1 year', 'accuracy' => ['month' => 'month']] ); $this->assertEquals('1 month ago', $result); - $date = new Date('-1 years -2 weeks -3 days'); + $date = new $class('-1 years -2 weeks -3 days'); $result = $date->timeAgoInWords( ['accuracy' => ['year' => 'year']] ); $expected = 'on ' . $date->format('n/j/y'); $this->assertEquals($expected, $result); - $date = new Date('-13 months -5 days'); + $date = new $class('-13 months -5 days'); $result = $date->timeAgoInWords(['end' => '2 years']); $this->assertEquals('1 year, 1 month, 5 days ago', $result); - $date = new Date('-23 hours'); + $date = new $class('-23 hours'); $result = $date->timeAgoInWords(['accuracy' => 'day']); $this->assertEquals('today', $result); } From cb3821aed39f0f15817e549331b3660aa52a96d1 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 26 Nov 2015 23:16:46 -0500 Subject: [PATCH 049/128] Add useImmutable and useMutable to date types. Add methods to enable immutable object creation. This allows us to retain BC and also enable immutable objects for new apps. The static::$dateTimeClass property is now deprecated. The new methods allow better control without the drawbacks that statics incur (shared global state). --- src/Database/Type/DateTimeType.php | 58 +++++++++++++++---- src/Database/Type/DateType.php | 35 ++++++++++- src/Database/Type/TimeType.php | 2 +- .../Database/Type/DateTimeTypeTest.php | 32 +++++----- tests/TestCase/Database/Type/DateTypeTest.php | 19 +++++- tests/TestCase/Database/Type/TimeTypeTest.php | 20 ++++++- 6 files changed, 132 insertions(+), 34 deletions(-) diff --git a/src/Database/Type/DateTimeType.php b/src/Database/Type/DateTimeType.php index 14782d74885..2f870d3287e 100644 --- a/src/Database/Type/DateTimeType.php +++ b/src/Database/Type/DateTimeType.php @@ -31,7 +31,11 @@ class DateTimeType extends Type /** * The class to use for representing date objects * + * This property can only be used before an instance of this type + * class is constructed. After that use `useMutable()` or `useImmutable()` instead. + * * @var string + * @deprecated Use DateTimeType::useMutable() or DateTimeType::useImmutable() instead. */ public static $dateTimeClass = 'Cake\I18n\Time'; @@ -65,18 +69,20 @@ class DateTimeType extends Type */ protected $_datetimeInstance; + /** + * The classname to use when creating objects. + * + * @var string + */ + protected $_className; + /** * {@inheritDoc} */ public function __construct($name = null) { parent::__construct($name); - - if (!class_exists(static::$dateTimeClass)) { - static::$dateTimeClass = 'DateTime'; - } - - $this->_datetimeInstance = new static::$dateTimeClass; + $this->_setClassName(static::$dateTimeClass, 'DateTime'); } /** @@ -92,7 +98,8 @@ public function toDatabase($value, Driver $driver) return $value; } if (is_int($value)) { - $value = new static::$dateTimeClass('@' . $value); + $class = $this->_className; + $value = new $class('@' . $value); } return $value->format($this->_format); } @@ -130,7 +137,7 @@ public function marshal($value) return $value; } - $class = static::$dateTimeClass; + $class = $this->_className; try { $compare = $date = false; if ($value === '' || $value === null || $value === false || $value === true) { @@ -196,12 +203,12 @@ public function useLocaleParser($enable = true) $this->_useLocaleParser = $enable; return $this; } - if (method_exists(static::$dateTimeClass, 'parseDateTime')) { + if (method_exists($this->_className, 'parseDateTime')) { $this->_useLocaleParser = $enable; return $this; } throw new RuntimeException( - sprintf('Cannot use locale parsing with the %s class', static::$dateTimeClass) + sprintf('Cannot use locale parsing with the %s class', $this->_className) ); } @@ -227,7 +234,34 @@ public function setLocaleFormat($format) */ public function useImmutable() { - static::$dateTimeClass = 'Cake\I18n\FrozenTime'; + $this->_setClassName('Cake\I18n\FrozenTime', 'DateTimeImmutable'); + return $this; + } + + /** + * Set the classname to use when building objects. + * + * @param string $class The classname to use. + * @param string $fallback The classname to use when the preferred class does not exist. + * @return void + */ + protected function _setClassName($class, $fallback) + { + if (!class_exists($class)) { + $class = $fallback; + } + $this->_className = $class; + $this->_datetimeInstance = new $this->_className; + } + + /** + * Change the preferred class name to the mutable Time implementation. + * + * @return $this + */ + public function useMutable() + { + $this->_setClassName('Cake\I18n\Time', 'DateTime'); return $this; } @@ -240,7 +274,7 @@ public function useImmutable() */ protected function _parseValue($value) { - $class = static::$dateTimeClass; + $class = $this->_className; return $class::parseDateTime($value, $this->_localeFormat); } } diff --git a/src/Database/Type/DateType.php b/src/Database/Type/DateType.php index a08a3d538d6..948f445f878 100644 --- a/src/Database/Type/DateType.php +++ b/src/Database/Type/DateType.php @@ -22,7 +22,11 @@ class DateType extends DateTimeType /** * The class to use for representing date objects * + * This property can only be used before an instance of this type + * class is constructed. After that use `useMutable()` or `useImmutable()` instead. + * * @var string + * @deprecated Use DateType::useMutable() or DateType::useImmutable() instead. */ public static $dateTimeClass = 'Cake\I18n\Date'; @@ -40,7 +44,18 @@ class DateType extends DateTimeType */ public function useImmutable() { - static::$dateTimeClass = 'Cake\I18n\FrozenDate'; + $this->_setClassName('Cake\I18n\FrozenDate', 'DateTimeImmutable'); + return $this; + } + + /** + * Change the preferred class name to the mutable Date implementation. + * + * @return $this + */ + public function useMutable() + { + $this->_setClassName('Cake\I18n\Date', 'DateTime'); return $this; } @@ -80,7 +95,23 @@ public function toPHP($value, Driver $driver) */ protected function _parseValue($value) { - $class = static::$dateTimeClass; + $class = $this->_className; return $class::parseDate($value, $this->_localeFormat); } + + /** + * Test that toImmutable changes all the methods to create frozen time instances. + * + * @return void + */ + public function testToImmutableAndToMutable() + { + $this->type->useImmutable(); + $this->assertInstanceOf('DateTimeImmutable', $this->type->marshal('2015-11-01')); + $this->assertInstanceOf('DateTimeImmutable', $this->type->toPhp('2015-11-01', $this->driver)); + + $this->type->useMutable(); + $this->assertInstanceOf('DateTime', $this->type->marshal('2015-11-01')); + $this->assertInstanceOf('DateTime', $this->type->toPhp('2015-11-01', $this->driver)); + } } diff --git a/src/Database/Type/TimeType.php b/src/Database/Type/TimeType.php index 58bba4a7532..f2ef4fa8d09 100644 --- a/src/Database/Type/TimeType.php +++ b/src/Database/Type/TimeType.php @@ -34,7 +34,7 @@ class TimeType extends DateTimeType */ protected function _parseValue($value) { - $class = static::$dateTimeClass; + $class = $this->_className; return $class::parseTime($value, $this->_localeFormat); } } diff --git a/tests/TestCase/Database/Type/DateTimeTypeTest.php b/tests/TestCase/Database/Type/DateTimeTypeTest.php index ab676bae45a..8473ef56d72 100644 --- a/tests/TestCase/Database/Type/DateTimeTypeTest.php +++ b/tests/TestCase/Database/Type/DateTimeTypeTest.php @@ -14,7 +14,6 @@ */ namespace Cake\Test\TestCase\Database\Type; -use Cake\Database\Type; use Cake\Database\Type\DateTimeType; use Cake\I18n\Time; use Cake\TestSuite\TestCase; @@ -40,21 +39,8 @@ class DateTimeTypeTest extends TestCase public function setUp() { parent::setUp(); - $this->type = Type::build('datetime'); + $this->type = new DateTimeType(); $this->driver = $this->getMock('Cake\Database\Driver'); - $this->_originalMap = Type::map(); - } - - /** - * Restores Type class state - * - * @return void - */ - public function tearDown() - { - parent::tearDown(); - - Type::map($this->_originalMap); } /** @@ -247,4 +233,20 @@ public function testMarshalWithLocaleParsingWithFormat() $result = $this->type->marshal('13 Oct, 2013 01:54pm'); $this->assertEquals($expected, $result); } + + /** + * Test that toImmutable changes all the methods to create frozen time instances. + * + * @return void + */ + public function testToImmutableAndToMutable() + { + $this->type->useImmutable(); + $this->assertInstanceOf('DateTimeImmutable', $this->type->marshal('2015-11-01 11:23:00')); + $this->assertInstanceOf('DateTimeImmutable', $this->type->toPhp('2015-11-01 11:23:00', $this->driver)); + + $this->type->useMutable(); + $this->assertInstanceOf('DateTime', $this->type->marshal('2015-11-01 11:23:00')); + $this->assertInstanceOf('DateTime', $this->type->toPhp('2015-11-01 11:23:00', $this->driver)); + } } diff --git a/tests/TestCase/Database/Type/DateTypeTest.php b/tests/TestCase/Database/Type/DateTypeTest.php index 294e21f099e..1c6f6dba04d 100644 --- a/tests/TestCase/Database/Type/DateTypeTest.php +++ b/tests/TestCase/Database/Type/DateTypeTest.php @@ -15,7 +15,6 @@ namespace Cake\Test\TestCase\Database\Type; use Cake\Chronos\Date; -use Cake\Database\Type; use Cake\Database\Type\DateType; use Cake\I18n\Time; use Cake\TestSuite\TestCase; @@ -34,7 +33,7 @@ class DateTypeTest extends TestCase public function setUp() { parent::setUp(); - $this->type = Type::build('date'); + $this->type = new DateType(); $this->driver = $this->getMock('Cake\Database\Driver'); } @@ -194,4 +193,20 @@ public function testMarshalWithLocaleParsingWithFormat() $result = $this->type->marshal('13 Oct, 2013'); $this->assertEquals($expected->format('Y-m-d'), $result->format('Y-m-d')); } + + /** + * Test that toImmutable changes all the methods to create frozen time instances. + * + * @return void + */ + public function testToImmutableAndToMutable() + { + $this->type->useImmutable(); + $this->assertInstanceOf('DateTimeImmutable', $this->type->marshal('2015-11-01')); + $this->assertInstanceOf('DateTimeImmutable', $this->type->toPhp('2015-11-01', $this->driver)); + + $this->type->useMutable(); + $this->assertInstanceOf('DateTime', $this->type->marshal('2015-11-01')); + $this->assertInstanceOf('DateTime', $this->type->toPhp('2015-11-01', $this->driver)); + } } diff --git a/tests/TestCase/Database/Type/TimeTypeTest.php b/tests/TestCase/Database/Type/TimeTypeTest.php index eb28f65919c..036bfcec778 100644 --- a/tests/TestCase/Database/Type/TimeTypeTest.php +++ b/tests/TestCase/Database/Type/TimeTypeTest.php @@ -14,7 +14,6 @@ */ namespace Cake\Test\TestCase\Database\Type; -use Cake\Database\Type; use Cake\Database\Type\TimeType; use Cake\I18n\Time; use Cake\TestSuite\TestCase; @@ -33,7 +32,7 @@ class TimeTypeTest extends TestCase public function setUp() { parent::setUp(); - $this->type = Type::build('time'); + $this->type = new TimeType(); $this->driver = $this->getMock('Cake\Database\Driver'); } @@ -166,6 +165,7 @@ public function testMarshal($value, $expected) $result = $this->type->marshal($value); if (is_object($expected)) { $this->assertEquals($expected, $result); + $this->assertInstanceOf('DateTime', $result); } else { $this->assertSame($expected, $result); } @@ -185,4 +185,20 @@ public function testMarshalWithLocaleParsing() $this->assertNull($this->type->marshal('derp:23')); } + + /** + * Test that toImmutable changes all the methods to create frozen time instances. + * + * @return void + */ + public function testToImmutableAndToMutable() + { + $this->type->useImmutable(); + $this->assertInstanceOf('DateTimeImmutable', $this->type->marshal('11:23:12')); + $this->assertInstanceOf('DateTimeImmutable', $this->type->toPhp('11:23:12', $this->driver)); + + $this->type->useMutable(); + $this->assertInstanceOf('DateTime', $this->type->marshal('11:23:12')); + $this->assertInstanceOf('DateTime', $this->type->toPhp('11:23:12', $this->driver)); + } } From fea9d428a3fd89cd2cd823ed00c8e51cb499bc3b Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 26 Nov 2015 23:22:02 -0500 Subject: [PATCH 050/128] Add docs for RelativeTimeFormatter. --- src/I18n/RelativeTimeFormatter.php | 56 +++++++++++++----------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 3d3143967d7..1eeafbef6e8 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -4,51 +4,36 @@ use Cake\I18n\FrozenDate; use Cake\I18n\FrozenTime; +/** + * Helper class for formatting relative dates & times. + * + * @internal + */ class RelativeTimeFormatter { + /** + * The datetime instance being formatted. + * + * @var \DateTime + */ protected $_time; + /** + * Constructor + * + * @var \DateTime $time The DateTime instance to format. + */ public function __construct($time) { $this->_time = $time; } /** - * Returns either a relative or a formatted absolute date depending - * on the difference between the current time and this object. - * - * ### Options: - * - * - `from` => another Time object representing the "now" time - * - `format` => a fall back format if the relative time is longer than the duration specified by end - * - `accuracy` => Specifies how accurate the date should be described (array) - * - year => The format if years > 0 (default "day") - * - month => The format if months > 0 (default "day") - * - week => The format if weeks > 0 (default "day") - * - day => The format if weeks > 0 (default "hour") - * - hour => The format if hours > 0 (default "minute") - * - minute => The format if minutes > 0 (default "minute") - * - second => The format if seconds > 0 (default "second") - * - `end` => The end of relative time telling - * - `relativeString` => The printf compatible string when outputting relative time - * - `absoluteString` => The printf compatible string when outputting absolute time - * - `timezone` => The user timezone the timestamp should be formatted in. - * - * Relative dates look something like this: - * - * - 3 weeks, 4 days ago - * - 15 seconds ago - * - * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using - * `i18nFormat`, see the method for the valid formatting strings - * - * The returned string includes 'ago' or 'on' and assumes you'll properly add a word - * like 'Posted ' before the function output. - * - * NOTE: If the difference is one week or more, the lowest level of accuracy is day + * Format a into a relative timestring. * * @param array $options Array of options. * @return string Relative time string. + * @see Cake\I18n\Time::timeAgoInWords() */ public function timeAgoInWords(array $options = []) { @@ -242,6 +227,13 @@ public function timeAgoInWords(array $options = []) return $relativeDate; } + /** + * Format a into a relative date string. + * + * @param array $options Array of options. + * @return string Relative date string. + * @see Cake\I18n\Date::timeAgoInWords() + */ public function dateAgoInWords(array $options = []) { $date = $this->_time; From c11ad68e2f4a8b9c0eaa17785a3a0dff3ee644b0 Mon Sep 17 00:00:00 2001 From: mark_story Date: Fri, 27 Nov 2015 17:43:12 -0500 Subject: [PATCH 051/128] Fix PHPCS errors. --- src/I18n/RelativeTimeFormatter.php | 2 +- tests/TestCase/I18n/DateTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 1eeafbef6e8..28a003103cf 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -21,7 +21,7 @@ class RelativeTimeFormatter /** * Constructor * - * @var \DateTime $time The DateTime instance to format. + * @param \DateTime $time The DateTime instance to format. */ public function __construct($time) { diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index 20cae6b8b4f..ae43c9ffd84 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -14,8 +14,8 @@ */ namespace Cake\Test\TestCase\I18n; -use Cake\I18n\FrozenDate; use Cake\I18n\Date; +use Cake\I18n\FrozenDate; use Cake\TestSuite\TestCase; use DateTimeZone; From 669894a165cc4374be89797b8aec2e63a5c8c329 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 27 Nov 2015 23:09:38 -0500 Subject: [PATCH 052/128] Extract common option handling code into a protected method. --- src/I18n/RelativeTimeFormatter.php | 101 +++++++++++++---------------- 1 file changed, 44 insertions(+), 57 deletions(-) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 28a003103cf..41bae2b9677 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -38,39 +38,12 @@ public function __construct($time) public function timeAgoInWords(array $options = []) { $time = $this->_time; - - $timezone = null; - // TODO use options like below. - $format = FrozenTime::$wordFormat; - $end = FrozenTime::$wordEnd; - $relativeString = __d('cake', '%s ago'); - $absoluteString = __d('cake', 'on %s'); - $accuracy = FrozenTime::$wordAccuracy; - $from = FrozenTime::now(); - $opts = ['timezone', 'format', 'end', 'relativeString', 'absoluteString', 'from']; - - foreach ($opts as $option) { - if (isset($options[$option])) { - ${$option} = $options[$option]; - unset($options[$option]); - } - } - - if (isset($options['accuracy'])) { - if (is_array($options['accuracy'])) { - $accuracy = $options['accuracy'] + $accuracy; - } else { - foreach ($accuracy as $key => $level) { - $accuracy[$key] = $options['accuracy']; - } - } - } - - if ($timezone) { - $time = $time->timezone($timezone); + $options = $this->_options($options, FrozenTime::class); + if ($options['timezone']) { + $time = $time->timezone($options['timezone']); } - $now = $from->format('U'); + $now = $options['from']->format('U'); $inSeconds = $time->format('U'); $backwards = ($inSeconds > $now); @@ -86,8 +59,8 @@ public function timeAgoInWords(array $options = []) return __d('cake', 'just now', 'just now'); } - if ($diff > abs($now - (new FrozenTime($end))->format('U'))) { - return sprintf($absoluteString, $time->i18nFormat($format)); + if ($diff > abs($now - (new FrozenTime($options['end']))->format('U'))) { + return sprintf($options['absoluteString'], $time->i18nFormat($options['format'])); } // If more than a week, then take into account the length of months @@ -153,19 +126,19 @@ public function timeAgoInWords(array $options = []) $seconds = $diff; } - $fWord = $accuracy['second']; + $fWord = $options['accuracy']['second']; if ($years > 0) { - $fWord = $accuracy['year']; + $fWord = $options['accuracy']['year']; } elseif (abs($months) > 0) { - $fWord = $accuracy['month']; + $fWord = $options['accuracy']['month']; } elseif (abs($weeks) > 0) { - $fWord = $accuracy['week']; + $fWord = $options['accuracy']['week']; } elseif (abs($days) > 0) { - $fWord = $accuracy['day']; + $fWord = $options['accuracy']['day']; } elseif (abs($hours) > 0) { - $fWord = $accuracy['hour']; + $fWord = $options['accuracy']['hour']; } elseif (abs($minutes) > 0) { - $fWord = $accuracy['minute']; + $fWord = $options['accuracy']['minute']; } $fNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $fWord); @@ -195,7 +168,7 @@ public function timeAgoInWords(array $options = []) // When time has passed if (!$backwards && $relativeDate) { - return sprintf($relativeString, $relativeDate); + return sprintf($options['relativeString'], $relativeDate); } if (!$backwards) { $aboutAgo = [ @@ -237,22 +210,7 @@ public function timeAgoInWords(array $options = []) public function dateAgoInWords(array $options = []) { $date = $this->_time; - $options += [ - 'from' => FrozenDate::now(), - 'timezone' => null, - 'format' => FrozenDate::$wordFormat, - 'accuracy' => FrozenDate::$wordAccuracy, - 'end' => FrozenDate::$wordEnd, - 'relativeString' => __d('cake', '%s ago'), - 'absoluteString' => __d('cake', 'on %s'), - ]; - if (is_string($options['accuracy'])) { - foreach (FrozenDate::$wordAccuracy as $key => $level) { - $options[$key] = $options['accuracy']; - } - } else { - $options['accuracy'] += FrozenDate::$wordAccuracy; - } + $options = $this->_options($options, FrozenDate::class); if ($options['timezone']) { $date = $date->timezone($options['timezone']); } @@ -395,4 +353,33 @@ public function dateAgoInWords(array $options = []) } return $relativeDate; } + + /** + * Build the options for relative date formatting. + * + * @param array $options The options provided by the user. + * @param string $class The class name to use for defaults. + */ + protected function _options($options, $class) + { + $options += [ + 'from' => $class::now(), + 'timezone' => null, + 'format' => $class::$wordFormat, + 'accuracy' => $class::$wordAccuracy, + 'end' => $class::$wordEnd, + 'relativeString' => __d('cake', '%s ago'), + 'absoluteString' => __d('cake', 'on %s'), + ]; + if (is_string($options['accuracy'])) { + $accuracy = $options['accuracy']; + $options['accuracy'] = []; + foreach ($class::$wordAccuracy as $key => $level) { + $options['accuracy'][$key] = $accuracy; + } + } else { + $options['accuracy'] += $class::$wordAccuracy; + } + return $options; + } } From 5f52362db5034c3fa01421de97e54ba08854db23 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 27 Nov 2015 23:23:33 -0500 Subject: [PATCH 053/128] Split out the difference calculation method into a helper method. Further reduce the duplication in RelativeDateFormatter. --- src/I18n/RelativeTimeFormatter.php | 165 ++++++++++------------------- 1 file changed, 55 insertions(+), 110 deletions(-) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 41bae2b9677..79f8798a502 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -63,85 +63,8 @@ public function timeAgoInWords(array $options = []) return sprintf($options['absoluteString'], $time->i18nFormat($options['format'])); } - // If more than a week, then take into account the length of months - if ($diff >= 604800) { - list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); - - list($past['H'], $past['i'], $past['s'], $past['d'], $past['m'], $past['Y']) = explode('/', date('H/i/s/d/m/Y', $pastTime)); - $weeks = $days = $hours = $minutes = $seconds = 0; - - $years = $future['Y'] - $past['Y']; - $months = $future['m'] + ((12 * $years) - $past['m']); - - if ($months >= 12) { - $years = floor($months / 12); - $months = $months - ($years * 12); - } - if ($future['m'] < $past['m'] && $future['Y'] - $past['Y'] === 1) { - $years--; - } - - if ($future['d'] >= $past['d']) { - $days = $future['d'] - $past['d']; - } else { - $daysInPastMonth = date('t', $pastTime); - $daysInFutureMonth = date('t', mktime(0, 0, 0, $future['m'] - 1, 1, $future['Y'])); - - if (!$backwards) { - $days = ($daysInPastMonth - $past['d']) + $future['d']; - } else { - $days = ($daysInFutureMonth - $past['d']) + $future['d']; - } - - if ($future['m'] != $past['m']) { - $months--; - } - } - - if (!$months && $years >= 1 && $diff < ($years * 31536000)) { - $months = 11; - $years--; - } - - if ($months >= 12) { - $years = $years + 1; - $months = $months - 12; - } - - if ($days >= 7) { - $weeks = floor($days / 7); - $days = $days - ($weeks * 7); - } - } else { - $years = $months = $weeks = 0; - $days = floor($diff / 86400); - - $diff = $diff - ($days * 86400); - - $hours = floor($diff / 3600); - $diff = $diff - ($hours * 3600); - - $minutes = floor($diff / 60); - $diff = $diff - ($minutes * 60); - $seconds = $diff; - } - - $fWord = $options['accuracy']['second']; - if ($years > 0) { - $fWord = $options['accuracy']['year']; - } elseif (abs($months) > 0) { - $fWord = $options['accuracy']['month']; - } elseif (abs($weeks) > 0) { - $fWord = $options['accuracy']['week']; - } elseif (abs($days) > 0) { - $fWord = $options['accuracy']['day']; - } elseif (abs($hours) > 0) { - $fWord = $options['accuracy']['hour']; - } elseif (abs($minutes) > 0) { - $fWord = $options['accuracy']['minute']; - } - - $fNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $fWord); + $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); + list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); $relativeDate = ''; if ($fNum >= 1 && $years > 0) { @@ -201,40 +124,18 @@ public function timeAgoInWords(array $options = []) } /** - * Format a into a relative date string. + * Calculate the data needed to format a relative difference string. * - * @param array $options Array of options. - * @return string Relative date string. - * @see Cake\I18n\Date::timeAgoInWords() + * @param \DateTime $futureTime The time from the future. + * @param \DateTime $pastTime The time from the past. + * @param bool $backwards Whether or not the difference was backwards. + * @param array $options An array of options. + * @return array An array of values. */ - public function dateAgoInWords(array $options = []) + protected function _diffData($futureTime, $pastTime, $backwards, $options) { - $date = $this->_time; - $options = $this->_options($options, FrozenDate::class); - if ($options['timezone']) { - $date = $date->timezone($options['timezone']); - } - - $now = $options['from']->format('U'); - $inSeconds = $date->format('U'); - $backwards = ($inSeconds > $now); - - $futureTime = $now; - $pastTime = $inSeconds; - if ($backwards) { - $futureTime = $inSeconds; - $pastTime = $now; - } $diff = $futureTime - $pastTime; - if (!$diff) { - return __d('cake', 'today'); - } - - if ($diff > abs($now - (new FrozenDate($options['end']))->format('U'))) { - return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); - } - // If more than a week, then take into account the length of months if ($diff >= 604800) { list($future['H'], $future['i'], $future['s'], $future['d'], $future['m'], $future['Y']) = explode('/', date('H/i/s/d/m/Y', $futureTime)); @@ -298,7 +199,7 @@ public function dateAgoInWords(array $options = []) $seconds = $diff; } - $fWord = $options['accuracy']['day']; + $fWord = $options['accuracy']['second']; if ($years > 0) { $fWord = $options['accuracy']['year']; } elseif (abs($months) > 0) { @@ -307,9 +208,53 @@ public function dateAgoInWords(array $options = []) $fWord = $options['accuracy']['week']; } elseif (abs($days) > 0) { $fWord = $options['accuracy']['day']; + } elseif (abs($hours) > 0) { + $fWord = $options['accuracy']['hour']; + } elseif (abs($minutes) > 0) { + $fWord = $options['accuracy']['minute']; + } + + $fNum = str_replace(['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], [1, 2, 3, 4, 5, 6, 7], $fWord); + return [$fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds]; + } + + /** + * Format a into a relative date string. + * + * @param array $options Array of options. + * @return string Relative date string. + * @see Cake\I18n\Date::timeAgoInWords() + */ + public function dateAgoInWords(array $options = []) + { + $date = $this->_time; + $options = $this->_options($options, FrozenDate::class); + if ($options['timezone']) { + $date = $date->timezone($options['timezone']); + } + + $now = $options['from']->format('U'); + $inSeconds = $date->format('U'); + $backwards = ($inSeconds > $now); + + $futureTime = $now; + $pastTime = $inSeconds; + if ($backwards) { + $futureTime = $inSeconds; + $pastTime = $now; + } + $diff = $futureTime - $pastTime; + + if (!$diff) { + return __d('cake', 'today'); + } + + if ($diff > abs($now - (new FrozenDate($options['end']))->format('U'))) { + return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); } - $fNum = str_replace(['year', 'month', 'week', 'day'], [1, 2, 3, 4], $fWord); + $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); + list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); $relativeDate = ''; if ($fNum >= 1 && $years > 0) { From c7e89d43f00c2114b6ee7f4997f2571fde4a2097 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 28 Nov 2015 16:08:46 -0500 Subject: [PATCH 054/128] Fix docblock issues. --- src/I18n/RelativeTimeFormatter.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 79f8798a502..87e3f4f10fd 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -1,4 +1,17 @@ Date: Sat, 28 Nov 2015 17:02:17 -0500 Subject: [PATCH 055/128] Reduce complexity a bit by removing branches/ternaries. --- src/I18n/RelativeTimeFormatter.php | 83 +++++++++++++----------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 87e3f4f10fd..d9e50fff8cd 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -79,33 +79,31 @@ public function timeAgoInWords(array $options = []) $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); - $relativeDate = ''; + $relativeDate = []; if ($fNum >= 1 && $years > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} year', '{0} years', $years, $years); + $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years); } if ($fNum >= 2 && $months > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} month', '{0} months', $months, $months); + $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months); } if ($fNum >= 3 && $weeks > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); + $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); } if ($fNum >= 4 && $days > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} day', '{0} days', $days, $days); + $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days); } if ($fNum >= 5 && $hours > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} hour', '{0} hours', $hours, $hours); + $relativeDate[] = __dn('cake', '{0} hour', '{0} hours', $hours, $hours); } if ($fNum >= 6 && $minutes > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} minute', '{0} minutes', $minutes, $minutes); + $relativeDate[] = __dn('cake', '{0} minute', '{0} minutes', $minutes, $minutes); } if ($fNum >= 7 && $seconds > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} second', '{0} seconds', $seconds, $seconds); + $relativeDate[] = __dn('cake', '{0} second', '{0} seconds', $seconds, $seconds); } + $relativeDate = implode(', ', $relativeDate); // When time has passed - if (!$backwards && $relativeDate) { - return sprintf($options['relativeString'], $relativeDate); - } if (!$backwards) { $aboutAgo = [ 'second' => __d('cake', 'about a second ago'), @@ -115,25 +113,22 @@ public function timeAgoInWords(array $options = []) 'week' => __d('cake', 'about a week ago'), 'year' => __d('cake', 'about a year ago') ]; - - return $aboutAgo[$fWord]; + return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord]; } // When time is to come - if (!$relativeDate) { - $aboutIn = [ - 'second' => __d('cake', 'in about a second'), - 'minute' => __d('cake', 'in about a minute'), - 'hour' => __d('cake', 'in about an hour'), - 'day' => __d('cake', 'in about a day'), - 'week' => __d('cake', 'in about a week'), - 'year' => __d('cake', 'in about a year') - ]; - - return $aboutIn[$fWord]; + if ($relativeDate) { + return $relativeDate; } - - return $relativeDate; + $aboutIn = [ + 'second' => __d('cake', 'in about a second'), + 'minute' => __d('cake', 'in about a minute'), + 'hour' => __d('cake', 'in about an hour'), + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'year' => __d('cake', 'in about a year') + ]; + return $aboutIn[$fWord]; } /** @@ -269,24 +264,22 @@ public function dateAgoInWords(array $options = []) $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); - $relativeDate = ''; + $relativeDate = []; if ($fNum >= 1 && $years > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} year', '{0} years', $years, $years); + $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years); } if ($fNum >= 2 && $months > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} month', '{0} months', $months, $months); + $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months); } if ($fNum >= 3 && $weeks > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); + $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); } if ($fNum >= 4 && $days > 0) { - $relativeDate .= ($relativeDate ? ', ' : '') . __dn('cake', '{0} day', '{0} days', $days, $days); + $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days); } + $relativeDate = implode(', ', $relativeDate); // When time has passed - if (!$backwards && $relativeDate) { - return sprintf($options['relativeString'], $relativeDate); - } if (!$backwards) { $aboutAgo = [ 'day' => __d('cake', 'about a day ago'), @@ -294,22 +287,20 @@ public function dateAgoInWords(array $options = []) 'month' => __d('cake', 'about a month ago'), 'year' => __d('cake', 'about a year ago') ]; - - return $aboutAgo[$fWord]; + return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord]; } // When time is to come - if (!$relativeDate) { - $aboutIn = [ - 'day' => __d('cake', 'in about a day'), - 'week' => __d('cake', 'in about a week'), - 'month' => __d('cake', 'in about a month'), - 'year' => __d('cake', 'in about a year') - ]; - - return $aboutIn[$fWord]; + if ($relativeDate) { + return $relativeDate; } - return $relativeDate; + $aboutIn = [ + 'day' => __d('cake', 'in about a day'), + 'week' => __d('cake', 'in about a week'), + 'month' => __d('cake', 'in about a month'), + 'year' => __d('cake', 'in about a year') + ]; + return $aboutIn[$fWord]; } /** From 8a6089af017c610e908885ed3f872945e76ae995 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 28 Nov 2015 17:06:34 -0500 Subject: [PATCH 056/128] Remove unnecessary imports. --- src/I18n/RelativeTimeFormatter.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index d9e50fff8cd..0947ddd601f 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -14,9 +14,6 @@ */ namespace Cake\I18n; -use Cake\I18n\FrozenDate; -use Cake\I18n\FrozenTime; - /** * Helper class for formatting relative dates & times. * From 8e5b9e347675d9f7d5d0d9d495e6e631202d7575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Lorenzo=20Rodr=C3=ADguez?= Date: Sun, 29 Nov 2015 15:13:28 +0100 Subject: [PATCH 057/128] Remove extra whitespace --- src/I18n/RelativeTimeFormatter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 0947ddd601f..30bfe4d011b 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -78,7 +78,7 @@ public function timeAgoInWords(array $options = []) $relativeDate = []; if ($fNum >= 1 && $years > 0) { - $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years); + $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years); } if ($fNum >= 2 && $months > 0) { $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months); @@ -272,7 +272,7 @@ public function dateAgoInWords(array $options = []) $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks); } if ($fNum >= 4 && $days > 0) { - $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days); + $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days); } $relativeDate = implode(', ', $relativeDate); From 04857106b6492ad1e74b8082d901073f41e6edcd Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 1 Dec 2015 22:33:30 +0100 Subject: [PATCH 058/128] Beginings of automatically converting sql functions to their right types --- .../Expression/FunctionExpression.php | 10 +++- src/Database/FunctionsBuilder.php | 48 ++++++++++--------- .../Database/FunctionsBuilderTest.php | 24 ++++++++-- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/Database/Expression/FunctionExpression.php b/src/Database/Expression/FunctionExpression.php index 93aa0b4d8e2..1ef1ce4aa8f 100644 --- a/src/Database/Expression/FunctionExpression.php +++ b/src/Database/Expression/FunctionExpression.php @@ -15,6 +15,8 @@ namespace Cake\Database\Expression; use Cake\Database\ExpressionInterface; +use Cake\Database\TypedResultInterface; +use Cake\Database\TypedResultTrait; use Cake\Database\ValueBinder; /** @@ -25,9 +27,11 @@ * * @internal */ -class FunctionExpression extends QueryExpression +class FunctionExpression extends QueryExpression implements TypedResultInterface { + use TypedResultTrait; + /** * The name of the function to be constructed when generating the SQL string * @@ -58,10 +62,12 @@ class FunctionExpression extends QueryExpression * If associative the key would be used as argument when value is 'literal' * @param array $types associative array of types to be associated with the * passed arguments + * @param string $returnType The return type of this expression */ - public function __construct($name, $params = [], $types = []) + public function __construct($name, $params = [], $types = [], $returnType = 'string') { $this->_name = $name; + $this->_returnType = $returnType; parent::__construct($params, $types, ','); } diff --git a/src/Database/FunctionsBuilder.php b/src/Database/FunctionsBuilder.php index 18cd01a09a3..2b3544e3f49 100644 --- a/src/Database/FunctionsBuilder.php +++ b/src/Database/FunctionsBuilder.php @@ -31,11 +31,12 @@ class FunctionsBuilder * @param string $name the name of the SQL function to constructed * @param array $params list of params to be passed to the function * @param array $types list of types for each function param + * @param string $return The return type of the function expression * @return FunctionExpression */ - protected function _build($name, $params = [], $types = []) + protected function _build($name, $params = [], $types = [], $return = 'string') { - return new FunctionExpression($name, $params, $types); + return new FunctionExpression($name, $params, $types, $return); } /** @@ -45,16 +46,17 @@ protected function _build($name, $params = [], $types = []) * @param string $name name of the function to build * @param mixed $expression the function argument * @param array $types list of types to bind to the arguments + * @param string $return The return type for the function * @return FunctionExpression */ - protected function _literalArgumentFunction($name, $expression, $types = []) + protected function _literalArgumentFunction($name, $expression, $types = [], $return = 'string') { if (!is_string($expression)) { $expression = [$expression]; } else { $expression = [$expression => 'literal']; } - return $this->_build($name, $expression, $types); + return $this->_build($name, $expression, $types, $return); } /** @@ -66,7 +68,7 @@ protected function _literalArgumentFunction($name, $expression, $types = []) */ public function sum($expression, $types = []) { - return $this->_literalArgumentFunction('SUM', $expression, $types); + return $this->_literalArgumentFunction('SUM', $expression, $types, 'float'); } /** @@ -78,7 +80,7 @@ public function sum($expression, $types = []) */ public function avg($expression, $types = []) { - return $this->_literalArgumentFunction('AVG', $expression, $types); + return $this->_literalArgumentFunction('AVG', $expression, $types, 'float'); } /** @@ -90,7 +92,7 @@ public function avg($expression, $types = []) */ public function max($expression, $types = []) { - return $this->_literalArgumentFunction('MAX', $expression, $types); + return $this->_literalArgumentFunction('MAX', $expression, $types, current($types) ?: 'string'); } /** @@ -102,7 +104,7 @@ public function max($expression, $types = []) */ public function min($expression, $types = []) { - return $this->_literalArgumentFunction('MIN', $expression, $types); + return $this->_literalArgumentFunction('MIN', $expression, $types, current($types) ?: 'string'); } /** @@ -114,7 +116,7 @@ public function min($expression, $types = []) */ public function count($expression, $types = []) { - return $this->_literalArgumentFunction('COUNT', $expression, $types); + return $this->_literalArgumentFunction('COUNT', $expression, $types, 'integer'); } /** @@ -126,7 +128,7 @@ public function count($expression, $types = []) */ public function concat($args, $types = []) { - return $this->_build('CONCAT', $args, $types); + return $this->_build('CONCAT', $args, $types, 'string'); } /** @@ -138,7 +140,7 @@ public function concat($args, $types = []) */ public function coalesce($args, $types = []) { - return $this->_build('COALESCE', $args, $types); + return $this->_build('COALESCE', $args, $types, current($types) ?: 'string'); } /** @@ -151,7 +153,7 @@ public function coalesce($args, $types = []) */ public function dateDiff($args, $types = []) { - return $this->_build('DATEDIFF', $args, $types); + return $this->_build('DATEDIFF', $args, $types, 'integer'); } /** @@ -177,7 +179,7 @@ public function datePart($part, $expression, $types = []) */ public function extract($part, $expression, $types = []) { - $expression = $this->_literalArgumentFunction('EXTRACT', $expression, $types); + $expression = $this->_literalArgumentFunction('EXTRACT', $expression, $types, 'integer'); $expression->tieWith(' FROM')->add([$part => 'literal'], [], true); return $expression; } @@ -197,7 +199,7 @@ public function dateAdd($expression, $value, $unit, $types = []) $value = 0; } $interval = $value . ' ' . $unit; - $expression = $this->_literalArgumentFunction('DATE_ADD', $expression, $types); + $expression = $this->_literalArgumentFunction('DATE_ADD', $expression, $types, 'datetime'); $expression->tieWith(', INTERVAL')->add([$interval => 'literal']); return $expression; } @@ -212,7 +214,7 @@ public function dateAdd($expression, $value, $unit, $types = []) */ public function dayOfWeek($expression, $types = []) { - return $this->_literalArgumentFunction('DAYOFWEEK', $expression, $types); + return $this->_literalArgumentFunction('DAYOFWEEK', $expression, $types, 'integer'); } /** @@ -239,13 +241,13 @@ public function weekday($expression, $types = []) public function now($type = 'datetime') { if ($type === 'datetime') { - return $this->_build('NOW'); + return $this->_build('NOW')->returnType('datetime'); } if ($type === 'date') { - return $this->_build('CURRENT_DATE'); + return $this->_build('CURRENT_DATE')->returnType('date'); } if ($type === 'time') { - return $this->_build('CURRENT_TIME'); + return $this->_build('CURRENT_TIME')->returnType('time'); } } @@ -253,9 +255,9 @@ public function now($type = 'datetime') * Magic method dispatcher to create custom SQL function calls * * @param string $name the SQL function name to construct - * @param array $args list with up to 2 arguments, first one being an array with - * parameters for the SQL function and second one a list of types to bind to those - * params + * @param array $args list with up to 3 arguments, first one being an array with + * parameters for the SQL function, the second one a list of types to bind to those + * params, and the third one the return type of the function * @return \Cake\Database\Expression\FunctionExpression */ public function __call($name, $args) @@ -265,8 +267,10 @@ public function __call($name, $args) return $this->_build($name); case 1: return $this->_build($name, $args[0]); - default: + case 2: return $this->_build($name, $args[0], $args[1]); + default: + return $this->_build($name, $args[0], $args[1], $args[2]); } } } diff --git a/tests/TestCase/Database/FunctionsBuilderTest.php b/tests/TestCase/Database/FunctionsBuilderTest.php index fce2731e5f1..5ed6a61e02c 100644 --- a/tests/TestCase/Database/FunctionsBuilderTest.php +++ b/tests/TestCase/Database/FunctionsBuilderTest.php @@ -46,6 +46,9 @@ public function testArbitrary() $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals('MyFunc', $function->name()); $this->assertEquals('MyFunc(b)', $function->sql(new ValueBinder)); + + $function = $this->functions->MyFunc(['b'], ['string'], 'integer'); + $this->assertEquals('integer', $function->returnType()); } /** @@ -58,6 +61,7 @@ public function testSum() $function = $this->functions->sum('total'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals('SUM(total)', $function->sql(new ValueBinder)); + $this->assertEquals('float', $function->returnType()); } /** @@ -70,6 +74,7 @@ public function testAvg() $function = $this->functions->avg('salary'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals('AVG(salary)', $function->sql(new ValueBinder)); + $this->assertEquals('float', $function->returnType()); } /** @@ -79,9 +84,10 @@ public function testAvg() */ public function testMAX() { - $function = $this->functions->max('created'); + $function = $this->functions->max('created', ['datetime']); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals('MAX(created)', $function->sql(new ValueBinder)); + $this->assertEquals('datetime', $function->returnType()); } /** @@ -91,9 +97,10 @@ public function testMAX() */ public function testMin() { - $function = $this->functions->min('created'); + $function = $this->functions->min('created', ['date']); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals('MIN(created)', $function->sql(new ValueBinder)); + $this->assertEquals('date', $function->returnType()); } /** @@ -106,6 +113,7 @@ public function testCount() $function = $this->functions->count('*'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals('COUNT(*)', $function->sql(new ValueBinder)); + $this->assertEquals('integer', $function->returnType()); } /** @@ -118,6 +126,7 @@ public function testConcat() $function = $this->functions->concat(['title' => 'literal', ' is a string']); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("CONCAT(title, :c0)", $function->sql(new ValueBinder)); + $this->assertEquals('string', $function->returnType()); } /** @@ -127,9 +136,10 @@ public function testConcat() */ public function testCoalesce() { - $function = $this->functions->coalesce(['NULL' => 'literal', '1', '2']); + $function = $this->functions->coalesce(['NULL' => 'literal', '1', 'a'], ['a' => 'date']); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("COALESCE(NULL, :c0, :c1)", $function->sql(new ValueBinder)); + $this->assertEquals('date', $function->returnType()); } /** @@ -142,14 +152,17 @@ public function testNow() $function = $this->functions->now(); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("NOW()", $function->sql(new ValueBinder)); + $this->assertEquals('datetime', $function->returnType()); $function = $this->functions->now('date'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("CURRENT_DATE()", $function->sql(new ValueBinder)); + $this->assertEquals('date', $function->returnType()); $function = $this->functions->now('time'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("CURRENT_TIME()", $function->sql(new ValueBinder)); + $this->assertEquals('time', $function->returnType()); } /** @@ -162,10 +175,12 @@ public function testExtract() $function = $this->functions->extract('day', 'created'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("EXTRACT(day FROM created)", $function->sql(new ValueBinder)); + $this->assertEquals('integer', $function->returnType()); $function = $this->functions->datePart('year', 'modified'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("EXTRACT(year FROM modified)", $function->sql(new ValueBinder)); + $this->assertEquals('integer', $function->returnType()); } /** @@ -178,6 +193,7 @@ public function testDateAdd() $function = $this->functions->dateAdd('created', -3, 'day'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("DATE_ADD(created, INTERVAL -3 day)", $function->sql(new ValueBinder)); + $this->assertEquals('datetime', $function->returnType()); } /** @@ -190,9 +206,11 @@ public function testDayOfWeek() $function = $this->functions->dayOfWeek('created'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("DAYOFWEEK(created)", $function->sql(new ValueBinder)); + $this->assertEquals('integer', $function->returnType()); $function = $this->functions->weekday('created'); $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals("DAYOFWEEK(created)", $function->sql(new ValueBinder)); + $this->assertEquals('integer', $function->returnType()); } } From 375191ce875af649a228826dc7d697fc3d64a920 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 1 Dec 2015 22:47:05 +0100 Subject: [PATCH 059/128] Automatically converting SQL functions to the corresponding type --- src/ORM/Query.php | 9 +++++++-- tests/TestCase/ORM/QueryRegressionTest.php | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ORM/Query.php b/src/ORM/Query.php index 76f3080c449..5ef298496b8 100644 --- a/src/ORM/Query.php +++ b/src/ORM/Query.php @@ -18,6 +18,7 @@ use Cake\Database\ExpressionInterface; use Cake\Database\Query as DatabaseQuery; use Cake\Database\TypeMap; +use Cake\Database\TypedResultInterface; use Cake\Database\ValueBinder; use Cake\Datasource\QueryInterface; use Cake\Datasource\QueryTrait; @@ -933,12 +934,16 @@ protected function _addDefaultSelectTypes() { $typeMap = $this->typeMap()->defaults(); $selectTypeMap = $this->selectTypeMap(); - $select = array_keys($this->clause('select')); + $select = $this->clause('select'); $types = []; - foreach ($select as $alias) { + foreach ($select as $alias => $value) { if (isset($typeMap[$alias])) { $types[$alias] = $typeMap[$alias]; + continue; + } + if ($value instanceof TypedResultInterface) { + $types[$alias] = $value->returnType(); } } $this->selectTypeMap()->addDefaults($types); diff --git a/tests/TestCase/ORM/QueryRegressionTest.php b/tests/TestCase/ORM/QueryRegressionTest.php index 19c14d0ca0e..0dddd500f7b 100644 --- a/tests/TestCase/ORM/QueryRegressionTest.php +++ b/tests/TestCase/ORM/QueryRegressionTest.php @@ -963,9 +963,9 @@ public function testTypemapInFunctions() ]); $result = $query->all()->first(); $this->assertSame( - '-1', + -1, $result['coalesced'], - 'Output values for functions are not cast yet.' + 'Output values for functions should be casted' ); } From eaea672d9b6286ca57b0f454f21f055dfd439322 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 1 Dec 2015 22:55:22 +0100 Subject: [PATCH 060/128] Adding more tests --- tests/TestCase/ORM/QueryRegressionTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/TestCase/ORM/QueryRegressionTest.php b/tests/TestCase/ORM/QueryRegressionTest.php index 0dddd500f7b..7c5e347862a 100644 --- a/tests/TestCase/ORM/QueryRegressionTest.php +++ b/tests/TestCase/ORM/QueryRegressionTest.php @@ -969,6 +969,24 @@ public function testTypemapInFunctions() ); } + /** + * Test that the typemaps used in function expressions + * create the correct results. + * + * @return void + */ + public function testTypemapInFunctions2() + { + $table = TableRegistry::get('Comments'); + $query = $table->find(); + $query->select([ + 'id', + 'max' => $query->func()->max('created', ['datetime']) + ]); + $result = $query->all()->first(); + $this->assertEquals(new Time('2007-03-18 10:55:23'), $result['max']); + } + /** * Test that contain queries map types correctly. * From 3592bea36597b9d7cccb738a7e89d038123e2352 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Tue, 1 Dec 2015 23:13:44 +0100 Subject: [PATCH 061/128] Added missing files :D --- src/Database/TypedResultInterface.php | 31 ++++++++++++++++++ src/Database/TypedResultTrait.php | 46 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/Database/TypedResultInterface.php create mode 100644 src/Database/TypedResultTrait.php diff --git a/src/Database/TypedResultInterface.php b/src/Database/TypedResultInterface.php new file mode 100644 index 00000000000..0638b9c9414 --- /dev/null +++ b/src/Database/TypedResultInterface.php @@ -0,0 +1,31 @@ +_returnType = $type; + return $this; + } + + return $this->_returnType; + } +} From fb0848e77308da2836151ff26612b39b0b9e45b6 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 1 Dec 2015 22:34:16 -0500 Subject: [PATCH 062/128] Make RelativeTimeFormatter static. This will result in fewer object constructions and hopefully improved performance. --- src/I18n/Date.php | 2 +- src/I18n/FrozenDate.php | 2 +- src/I18n/FrozenTime.php | 2 +- src/I18n/RelativeTimeFormatter.php | 39 +++++++++--------------------- src/I18n/Time.php | 2 +- 5 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/I18n/Date.php b/src/I18n/Date.php index 38d6187d74a..14bd89059f0 100644 --- a/src/I18n/Date.php +++ b/src/I18n/Date.php @@ -130,6 +130,6 @@ class Date extends MutableDate implements JsonSerializable */ public function timeAgoInWords(array $options = []) { - return (new RelativeTimeFormatter($this))->dateAgoInWords($options); + return RelativeTimeFormatter::dateAgoInWords($this, $options); } } diff --git a/src/I18n/FrozenDate.php b/src/I18n/FrozenDate.php index 509dcefaed6..16e2690a837 100644 --- a/src/I18n/FrozenDate.php +++ b/src/I18n/FrozenDate.php @@ -132,6 +132,6 @@ class FrozenDate extends ChronosDate implements JsonSerializable */ public function timeAgoInWords(array $options = []) { - return (new RelativeTimeFormatter($this))->dateAgoInWords($options); + return RelativeTimeFormatter::dateAgoInWords($this, $options); } } diff --git a/src/I18n/FrozenTime.php b/src/I18n/FrozenTime.php index b1b0a545e76..253e903204c 100644 --- a/src/I18n/FrozenTime.php +++ b/src/I18n/FrozenTime.php @@ -154,7 +154,7 @@ public function __construct($time = null, $tz = null) */ public function timeAgoInWords(array $options = []) { - return (new RelativeTimeFormatter($this))->timeAgoInWords($options); + return RelativeTimeFormatter::timeAgoInWords($this, $options); } /** diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 30bfe4d011b..78674a104cb 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -14,6 +14,8 @@ */ namespace Cake\I18n; +use DatetimeInterface; + /** * Helper class for formatting relative dates & times. * @@ -21,34 +23,17 @@ */ class RelativeTimeFormatter { - /** - * The datetime instance being formatted. - * - * @var \DateTime - */ - protected $_time; - - /** - * Constructor - * - * @param \DateTime $time The DateTime instance to format. - */ - public function __construct($time) - { - $this->_time = $time; - } - /** * Format a into a relative timestring. * + * @param \DateTimeInterface $time The time instance to format. * @param array $options Array of options. * @return string Relative time string. * @see Cake\I18n\Time::timeAgoInWords() */ - public function timeAgoInWords(array $options = []) + public static function timeAgoInWords(DatetimeInterface $time, array $options = []) { - $time = $this->_time; - $options = $this->_options($options, FrozenTime::class); + $options = static::_options($options, FrozenTime::class); if ($options['timezone']) { $time = $time->timezone($options['timezone']); } @@ -73,7 +58,7 @@ public function timeAgoInWords(array $options = []) return sprintf($options['absoluteString'], $time->i18nFormat($options['format'])); } - $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); + $diffData = static::_diffData($futureTime, $pastTime, $backwards, $options); list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); $relativeDate = []; @@ -137,7 +122,7 @@ public function timeAgoInWords(array $options = []) * @param array $options An array of options. * @return array An array of values. */ - protected function _diffData($futureTime, $pastTime, $backwards, $options) + protected static function _diffData($futureTime, $pastTime, $backwards, $options) { $diff = $futureTime - $pastTime; @@ -226,14 +211,14 @@ protected function _diffData($futureTime, $pastTime, $backwards, $options) /** * Format a into a relative date string. * + * @param \DatetimeInterface $date The date to format. * @param array $options Array of options. * @return string Relative date string. * @see Cake\I18n\Date::timeAgoInWords() */ - public function dateAgoInWords(array $options = []) + public static function dateAgoInWords(DatetimeInterface $date, array $options = []) { - $date = $this->_time; - $options = $this->_options($options, FrozenDate::class); + $options = static::_options($options, FrozenDate::class); if ($options['timezone']) { $date = $date->timezone($options['timezone']); } @@ -258,7 +243,7 @@ public function dateAgoInWords(array $options = []) return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); } - $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); + $diffData = static::_diffData($futureTime, $pastTime, $backwards, $options); list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); $relativeDate = []; @@ -307,7 +292,7 @@ public function dateAgoInWords(array $options = []) * @param string $class The class name to use for defaults. * @return array Options with defaults applied. */ - protected function _options($options, $class) + protected static function _options($options, $class) { $options += [ 'from' => $class::now(), diff --git a/src/I18n/Time.php b/src/I18n/Time.php index de7eb9f2c28..099bbfb39d5 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -153,7 +153,7 @@ public function __construct($time = null, $tz = null) */ public function timeAgoInWords(array $options = []) { - return (new RelativeTimeFormatter($this))->timeAgoInWords($options); + return RelativeTimeFormatter::timeAgoInWords($this, $options); } /** From 9de8b5c539a7a6d6f679af8819ff55376f768495 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Wed, 2 Dec 2015 09:35:36 +0100 Subject: [PATCH 063/128] Fixed tests in posstgresql --- tests/TestCase/ORM/QueryRegressionTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TestCase/ORM/QueryRegressionTest.php b/tests/TestCase/ORM/QueryRegressionTest.php index 7c5e347862a..51d15d87463 100644 --- a/tests/TestCase/ORM/QueryRegressionTest.php +++ b/tests/TestCase/ORM/QueryRegressionTest.php @@ -980,7 +980,6 @@ public function testTypemapInFunctions2() $table = TableRegistry::get('Comments'); $query = $table->find(); $query->select([ - 'id', 'max' => $query->func()->max('created', ['datetime']) ]); $result = $query->all()->first(); From 52b4649d716350a07548c30f32acf5e6c978ea83 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Wed, 2 Dec 2015 23:46:34 +0100 Subject: [PATCH 064/128] Fixed CS errors --- tests/TestCase/Database/Driver/MysqlTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/TestCase/Database/Driver/MysqlTest.php b/tests/TestCase/Database/Driver/MysqlTest.php index ddf8d6187cd..4db89d73e43 100644 --- a/tests/TestCase/Database/Driver/MysqlTest.php +++ b/tests/TestCase/Database/Driver/MysqlTest.php @@ -134,7 +134,8 @@ public function testConnectionConfigCustom() * * @return void */ - public function testIsConnected() { + public function testIsConnected() + { $connection = ConnectionManager::get('test'); $connection->disconnect(); $this->assertFalse($connection->isConnected(), 'Not connected now.'); From 3b8bd64a4edd6f36e226161c18decf97e0f49f5a Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Thu, 3 Dec 2015 19:26:20 +0100 Subject: [PATCH 065/128] Pleases CS checker --- src/ORM/Query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ORM/Query.php b/src/ORM/Query.php index 5ef298496b8..84176a8ba1a 100644 --- a/src/ORM/Query.php +++ b/src/ORM/Query.php @@ -17,8 +17,8 @@ use ArrayObject; use Cake\Database\ExpressionInterface; use Cake\Database\Query as DatabaseQuery; -use Cake\Database\TypeMap; use Cake\Database\TypedResultInterface; +use Cake\Database\TypeMap; use Cake\Database\ValueBinder; use Cake\Datasource\QueryInterface; use Cake\Datasource\QueryTrait; From aaf981784913bcbde17ecfc88ad166f86932dad1 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 4 Dec 2015 09:12:44 -0500 Subject: [PATCH 066/128] Use the diffFormatter hook provided by chronos Hook into chronos' diffFormatter hook. Doing this gives us a pluggable way for to modify timeAgoInWords() and diffForHumans(). Furthermore, it makes diffForHumans() compatible with Cake's i18n libraries. --- src/I18n/Date.php | 2 +- src/I18n/DateFormatTrait.php | 19 ++++++++ src/I18n/FrozenDate.php | 2 +- src/I18n/FrozenTime.php | 2 +- src/I18n/RelativeTimeFormatter.php | 75 ++++++++++++++++++++++++++---- src/I18n/Time.php | 2 +- tests/TestCase/I18n/TimeTest.php | 55 ++++++++++++++++++++++ 7 files changed, 145 insertions(+), 12 deletions(-) diff --git a/src/I18n/Date.php b/src/I18n/Date.php index 14bd89059f0..0a6aa65582e 100644 --- a/src/I18n/Date.php +++ b/src/I18n/Date.php @@ -130,6 +130,6 @@ class Date extends MutableDate implements JsonSerializable */ public function timeAgoInWords(array $options = []) { - return RelativeTimeFormatter::dateAgoInWords($this, $options); + return $this->diffFormatter()->dateAgoInWords($this, $options); } } diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index cfa2d55be6f..4e13d8ca988 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -14,6 +14,7 @@ */ namespace Cake\I18n; +use Cake\I18n\RelativeTimeFormatter; use DateTime; use IntlDateFormatter; @@ -349,6 +350,24 @@ public function jsonSerialize() return $this->i18nFormat(static::$_jsonEncodeFormat); } + /** + * Get the difference formatter instance or overwrite the current one. + * + * @param \Cake\I18n\RelativeTimeFormatter|null $formatter The formatter instance when setting. + * @return \Cake\I18n\RelativeTimeFormatter The formatter instance. + */ + public function diffFormatter($formatter = null) + { + if ($formatter === null) { + // Use the static property defined in chronos. + if (static::$diffFormatter === null) { + static::$diffFormatter = new RelativeTimeFormatter(); + } + return static::$diffFormatter; + } + return static::$diffFormatter = $translator; + } + /** * Returns the data that should be displayed when debugging this object * diff --git a/src/I18n/FrozenDate.php b/src/I18n/FrozenDate.php index 16e2690a837..e19d05b82a3 100644 --- a/src/I18n/FrozenDate.php +++ b/src/I18n/FrozenDate.php @@ -132,6 +132,6 @@ class FrozenDate extends ChronosDate implements JsonSerializable */ public function timeAgoInWords(array $options = []) { - return RelativeTimeFormatter::dateAgoInWords($this, $options); + return $this->diffFormatter()->dateAgoInWords($this, $options); } } diff --git a/src/I18n/FrozenTime.php b/src/I18n/FrozenTime.php index 253e903204c..9e8cb7ebe8f 100644 --- a/src/I18n/FrozenTime.php +++ b/src/I18n/FrozenTime.php @@ -154,7 +154,7 @@ public function __construct($time = null, $tz = null) */ public function timeAgoInWords(array $options = []) { - return RelativeTimeFormatter::timeAgoInWords($this, $options); + return $this->diffFormatter()->timeAgoInWords($this, $options); } /** diff --git a/src/I18n/RelativeTimeFormatter.php b/src/I18n/RelativeTimeFormatter.php index 78674a104cb..4692770686f 100644 --- a/src/I18n/RelativeTimeFormatter.php +++ b/src/I18n/RelativeTimeFormatter.php @@ -14,6 +14,7 @@ */ namespace Cake\I18n; +use Cake\Chronos\ChronosInterface; use DatetimeInterface; /** @@ -23,6 +24,64 @@ */ class RelativeTimeFormatter { + /** + * Get the difference in a human readable format. + * + * @param \Cake\Chronos\ChronosInterface $date The datetime to start with. + * @param \Cake\Chronos\ChronosInterface|null $other The datetime to compare against. + * @param bool $absolute removes time difference modifiers ago, after, etc + * @return string The difference between the two days in a human readable format + * @see Cake\Chronos\ChronosInterface::diffForHumans + */ + public function diffForHumans(ChronosInterface $date, ChronosInterface $other = null, $absolute = false) + { + $isNow = $other === null; + if ($isNow) { + $other = $date->now($date->tz); + } + $diffInterval = $date->diff($other); + + switch (true) { + case ($diffInterval->y > 0): + $count = $diffInterval->y; + $message = __dn('cake', '{0} year', '{0} years', $count, $count); + break; + case ($diffInterval->m > 0): + $count = $diffInterval->m; + $message = __dn('cake', '{0} month', '{0} months', $count, $count); + break; + case ($diffInterval->d > 0): + $count = $diffInterval->d; + if ($count >= ChronosInterface::DAYS_PER_WEEK) { + $count = (int)($count / ChronosInterface::DAYS_PER_WEEK); + $message = __dn('cake', '{0} week', '{0} weeks', $count, $count); + } else { + $message = __dn('cake', '{0} day', '{0} days', $count, $count); + } + break; + case ($diffInterval->h > 0): + $count = $diffInterval->h; + $message = __dn('cake', '{0} hour', '{0} hours', $count, $count); + break; + case ($diffInterval->i > 0): + $count = $diffInterval->i; + $message = __dn('cake', '{0} minute', '{0} minutes', $count, $count); + break; + default: + $count = $diffInterval->s; + $message = __dn('cake', '{0} second', '{0} seconds', $count, $count); + break; + } + if ($absolute) { + return $message; + } + $isFuture = $diffInterval->invert === 1; + if ($isNow) { + return $isFuture ? __d('cake', '{0} from now', $message) : __d('cake', '{0} ago', $message); + } + return $isFuture ? __d('cake', '{0} after', $message) : __d('cake', '{0} before', $message); + } + /** * Format a into a relative timestring. * @@ -31,9 +90,9 @@ class RelativeTimeFormatter * @return string Relative time string. * @see Cake\I18n\Time::timeAgoInWords() */ - public static function timeAgoInWords(DatetimeInterface $time, array $options = []) + public function timeAgoInWords(DatetimeInterface $time, array $options = []) { - $options = static::_options($options, FrozenTime::class); + $options = $this->_options($options, FrozenTime::class); if ($options['timezone']) { $time = $time->timezone($options['timezone']); } @@ -58,7 +117,7 @@ public static function timeAgoInWords(DatetimeInterface $time, array $options = return sprintf($options['absoluteString'], $time->i18nFormat($options['format'])); } - $diffData = static::_diffData($futureTime, $pastTime, $backwards, $options); + $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); $relativeDate = []; @@ -122,7 +181,7 @@ public static function timeAgoInWords(DatetimeInterface $time, array $options = * @param array $options An array of options. * @return array An array of values. */ - protected static function _diffData($futureTime, $pastTime, $backwards, $options) + protected function _diffData($futureTime, $pastTime, $backwards, $options) { $diff = $futureTime - $pastTime; @@ -216,9 +275,9 @@ protected static function _diffData($futureTime, $pastTime, $backwards, $options * @return string Relative date string. * @see Cake\I18n\Date::timeAgoInWords() */ - public static function dateAgoInWords(DatetimeInterface $date, array $options = []) + public function dateAgoInWords(DatetimeInterface $date, array $options = []) { - $options = static::_options($options, FrozenDate::class); + $options = $this->_options($options, FrozenDate::class); if ($options['timezone']) { $date = $date->timezone($options['timezone']); } @@ -243,7 +302,7 @@ public static function dateAgoInWords(DatetimeInterface $date, array $options = return sprintf($options['absoluteString'], $date->i18nFormat($options['format'])); } - $diffData = static::_diffData($futureTime, $pastTime, $backwards, $options); + $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options); list($fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds) = array_values($diffData); $relativeDate = []; @@ -292,7 +351,7 @@ public static function dateAgoInWords(DatetimeInterface $date, array $options = * @param string $class The class name to use for defaults. * @return array Options with defaults applied. */ - protected static function _options($options, $class) + protected function _options($options, $class) { $options += [ 'from' => $class::now(), diff --git a/src/I18n/Time.php b/src/I18n/Time.php index 099bbfb39d5..7a399754a7c 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -153,7 +153,7 @@ public function __construct($time = null, $tz = null) */ public function timeAgoInWords(array $options = []) { - return RelativeTimeFormatter::timeAgoInWords($this, $options); + return $this->diffFormatter()->timeAgoInWords($this, $options); } /** diff --git a/tests/TestCase/I18n/TimeTest.php b/tests/TestCase/I18n/TimeTest.php index 2e4317e3c34..1383eca90ef 100644 --- a/tests/TestCase/I18n/TimeTest.php +++ b/tests/TestCase/I18n/TimeTest.php @@ -623,6 +623,7 @@ public function testToStringInvalidZeros($class) public function testDiffForHumans($class) { $time = new $class('2014-04-20 10:10:10'); + $other = new $class('2014-04-27 10:10:10'); $this->assertEquals('1 week before', $time->diffForHumans($other)); @@ -631,6 +632,60 @@ public function testDiffForHumans($class) $other = new $class('2014-04-13 09:10:10'); $this->assertEquals('1 week after', $time->diffForHumans($other)); + + $other = new $class('2014-04-06 09:10:10'); + $this->assertEquals('2 weeks after', $time->diffForHumans($other)); + + $other = new $class('2014-04-21 10:10:10'); + $this->assertEquals('1 day before', $time->diffForHumans($other)); + + $other = new $class('2014-04-22 10:10:10'); + $this->assertEquals('2 days before', $time->diffForHumans($other)); + + $other = new $class('2014-04-20 10:11:10'); + $this->assertEquals('1 minute before', $time->diffForHumans($other)); + + $other = new $class('2014-04-20 10:12:10'); + $this->assertEquals('2 minutes before', $time->diffForHumans($other)); + + $other = new $class('2014-04-20 10:10:09'); + $this->assertEquals('1 second after', $time->diffForHumans($other)); + + $other = new $class('2014-04-20 10:10:08'); + $this->assertEquals('2 seconds after', $time->diffForHumans($other)); + } + + /** + * Tests diffForHumans absolute + * + * @dataProvider classNameProvider + * @return void + */ + public function testDiffForHumansAbsolute($class) + { + $time = new $class('2014-04-20 10:10:10'); + $this->assertEquals('1 year', $time->diffForHumans(null, ['absolute' => true])); + + $other = new $class('2014-04-27 10:10:10'); + $this->assertEquals('1 week', $time->diffForHumans($other, ['absolute' => true])); + + $time = new $class('2016-04-20 10:10:10'); + $this->assertEquals('4 months', $time->diffForHumans(null, ['absolute' => true])); + } + + /** + * Tests diffForHumans with now + * + * @dataProvider classNameProvider + * @return void + */ + public function testDiffForHumansNow($class) + { + $time = new $class('2014-04-20 10:10:10'); + $this->assertEquals('1 year ago', $time->diffForHumans()); + + $time = new $class('2016-04-20 10:10:10'); + $this->assertEquals('4 months from now', $time->diffForHumans()); } /** From 8c9a043bae8c806db94d2716b19baaec3de408c3 Mon Sep 17 00:00:00 2001 From: antograssiot Date: Sat, 5 Dec 2015 07:57:51 +0100 Subject: [PATCH 067/128] Remove extra import --- src/I18n/DateFormatTrait.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index 4e13d8ca988..3ba42476501 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -14,7 +14,6 @@ */ namespace Cake\I18n; -use Cake\I18n\RelativeTimeFormatter; use DateTime; use IntlDateFormatter; From 8cc2424e4acef25874712f28b049e0b110523592 Mon Sep 17 00:00:00 2001 From: antograssiot Date: Sun, 6 Dec 2015 08:39:56 +0100 Subject: [PATCH 068/128] update tests to pass with ICU56 --- tests/TestCase/I18n/DateTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/TestCase/I18n/DateTest.php b/tests/TestCase/I18n/DateTest.php index ae43c9ffd84..283c60b8f91 100644 --- a/tests/TestCase/I18n/DateTest.php +++ b/tests/TestCase/I18n/DateTest.php @@ -88,6 +88,7 @@ public function testI18nFormat($class) $class::$defaultLocale = 'fr-FR'; $result = $time->i18nFormat(\IntlDateFormatter::FULL); + $result = str_replace(' à', '', $result); $expected = 'jeudi 14 janvier 2010 00:00:00 UTC'; $this->assertEquals($expected, $result); From 37d33e8ad7e0c6b2e90cf5bcb54efd7d99413f8a Mon Sep 17 00:00:00 2001 From: antograssiot Date: Sun, 6 Dec 2015 09:25:35 +0100 Subject: [PATCH 069/128] use spaces for indentation --- tests/test_app/Plugin/TestPluginTwo/src/Shell/ExampleShell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app/Plugin/TestPluginTwo/src/Shell/ExampleShell.php b/tests/test_app/Plugin/TestPluginTwo/src/Shell/ExampleShell.php index 058495dca86..b76d0cd998f 100644 --- a/tests/test_app/Plugin/TestPluginTwo/src/Shell/ExampleShell.php +++ b/tests/test_app/Plugin/TestPluginTwo/src/Shell/ExampleShell.php @@ -41,6 +41,6 @@ public function main() */ public function say_hello() { - $this->out('Hello from the TestPluginTwo.ExampleShell'); + $this->out('Hello from the TestPluginTwo.ExampleShell'); } } From ed0901f18b107e97ca3cbf7b4ec1d8dfde55c539 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 6 Dec 2015 16:04:47 +0100 Subject: [PATCH 070/128] Better return type inference for sum() --- src/Database/FunctionsBuilder.php | 6 +++++- tests/TestCase/Database/FunctionsBuilderTest.php | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Database/FunctionsBuilder.php b/src/Database/FunctionsBuilder.php index 2b3544e3f49..64fb97ff32c 100644 --- a/src/Database/FunctionsBuilder.php +++ b/src/Database/FunctionsBuilder.php @@ -68,7 +68,11 @@ protected function _literalArgumentFunction($name, $expression, $types = [], $re */ public function sum($expression, $types = []) { - return $this->_literalArgumentFunction('SUM', $expression, $types, 'float'); + $returnType = 'float'; + if (current($types) === 'integer') { + $returnType = 'integer'; + } + return $this->_literalArgumentFunction('SUM', $expression, $types, $returnType); } /** diff --git a/tests/TestCase/Database/FunctionsBuilderTest.php b/tests/TestCase/Database/FunctionsBuilderTest.php index 5ed6a61e02c..578efc81b9c 100644 --- a/tests/TestCase/Database/FunctionsBuilderTest.php +++ b/tests/TestCase/Database/FunctionsBuilderTest.php @@ -62,6 +62,11 @@ public function testSum() $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); $this->assertEquals('SUM(total)', $function->sql(new ValueBinder)); $this->assertEquals('float', $function->returnType()); + + $function = $this->functions->sum('total', ['integer']); + $this->assertInstanceOf('Cake\Database\Expression\FunctionExpression', $function); + $this->assertEquals('SUM(total)', $function->sql(new ValueBinder)); + $this->assertEquals('integer', $function->returnType()); } /** From aaf61e7c1396ca0b1501c166dc1c3f8eb31db993 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 6 Dec 2015 17:07:35 +0100 Subject: [PATCH 071/128] Merging queryBuilders in contain. Fixes #6696 This makes it a lot easir to break queries into multiple custom finders where the same assocaition is used multiple times in contain. It also brings the orm closer to the behavior in 2.x where you could appen to a contain clause anywhere. --- src/ORM/EagerLoader.php | 9 +++++++++ tests/TestCase/ORM/EagerLoaderTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/ORM/EagerLoader.php b/src/ORM/EagerLoader.php index c82b1ad35df..13eb91e299f 100644 --- a/src/ORM/EagerLoader.php +++ b/src/ORM/EagerLoader.php @@ -309,6 +309,15 @@ protected function _reformatContain($associations, $original) } $pointer += [$table => []]; + + if (isset($options['queryBuilder']) && isset($pointer[$table]['queryBuilder'])) { + $first = $pointer[$table]['queryBuilder']; + $second = $options['queryBuilder']; + $options['queryBuilder'] = function ($query) use ($first, $second) { + return $second($first($query)); + }; + } + $pointer[$table] = $options + $pointer[$table]; } diff --git a/tests/TestCase/ORM/EagerLoaderTest.php b/tests/TestCase/ORM/EagerLoaderTest.php index ced2cc2353f..d5d7a84062d 100644 --- a/tests/TestCase/ORM/EagerLoaderTest.php +++ b/tests/TestCase/ORM/EagerLoaderTest.php @@ -308,6 +308,31 @@ public function testContainClosure() $this->assertEquals($expected, $loader->contain()); } + /** + * Tests that query builders are stacked + * + * @return void + */ + public function testContainMergeBuilders() + { + $loader = new EagerLoader; + $loader->contain([ + 'clients' => function ($query) { + return $query->select(['a']); + } + ]); + $loader->contain([ + 'clients' => function ($query) { + return $query->select(['b']); + } + ]); + $builder = $loader->contain()['clients']['queryBuilder']; + $table = TableRegistry::get('foo'); + $query = new Query($this->connection, $table); + $query = $builder($query); + $this->assertEquals(['a', 'b'], $query->clause('select')); + } + /** * Test that fields for contained models are aliased and added to the select clause * From 407f886d3fe3166cc4347595a46254c15712c3aa Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 6 Dec 2015 16:12:44 -0500 Subject: [PATCH 072/128] Make diffFormatter() static as it is in chronos. --- src/I18n/DateFormatTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index 3ba42476501..7f47e270340 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -355,7 +355,7 @@ public function jsonSerialize() * @param \Cake\I18n\RelativeTimeFormatter|null $formatter The formatter instance when setting. * @return \Cake\I18n\RelativeTimeFormatter The formatter instance. */ - public function diffFormatter($formatter = null) + public static function diffFormatter($formatter = null) { if ($formatter === null) { // Use the static property defined in chronos. From fb4ffa69e0d20a2400ddf342acd667bebf82cb3c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 14 Dec 2015 21:38:11 -0500 Subject: [PATCH 073/128] Fix incorrectly handled domains. This was supposed to be part of the master->3.2 merge but I am bad at git. --- src/Network/CorsBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Network/CorsBuilder.php b/src/Network/CorsBuilder.php index f3bb6d370fa..3a1ad5dda5f 100644 --- a/src/Network/CorsBuilder.php +++ b/src/Network/CorsBuilder.php @@ -133,7 +133,7 @@ protected function _normalizeDomains($domains) if (strpos($domain, '://') === false) { $preg = ($this->_isSsl ? 'https://' : 'http://') . $domain; } - $preg = '@' . str_replace('*', '.*', $domain) . '@'; + $preg = '@^' . str_replace('\*', '.*', preg_quote($preg, '@')) . '$@'; $result[] = compact('original', 'preg'); } return $result; From 0a49df71702c0db1de1ac74b71dc9e556535ee2b Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 21 Dec 2015 16:33:55 +0100 Subject: [PATCH 074/128] Deprecate action for Form::create(). --- src/View/Helper/FormHelper.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/View/Helper/FormHelper.php b/src/View/Helper/FormHelper.php index 45770502608..989f178065f 100644 --- a/src/View/Helper/FormHelper.php +++ b/src/View/Helper/FormHelper.php @@ -317,7 +317,7 @@ protected function _isRequiredField($validationRules) * - `type` Form method defaults to autodetecting based on the form context. If * the form context's isCreate() method returns false, a PUT request will be done. * - `action` The controller action the form submits to, (optional). Use this option if you - * don't need to change the controller from the current request's controller. + * don't need to change the controller from the current request's controller. Deprecated since 3.2, use `url`. * - `url` The URL the form submits to. Can be a string or a URL array. If you use 'url' * you should leave 'action' undefined. * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')` @@ -357,6 +357,10 @@ public function create($model = null, array $options = []) 'idPrefix' => null, ]; + if (isset($options['action'])) { + trigger_error('Using key `action` is deprecated, use `url` directly instead.', E_USER_DEPRECATED); + } + if ($options['idPrefix'] !== null) { $this->_idPrefix = $options['idPrefix']; } From a0c5405c60b97a89d1b2a2ed4401d8d3bd9a7188 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 21 Dec 2015 16:34:07 +0100 Subject: [PATCH 075/128] Adjust tests. --- tests/TestCase/View/Helper/FormHelperTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TestCase/View/Helper/FormHelperTest.php b/tests/TestCase/View/Helper/FormHelperTest.php index ff7bc5c4207..bba59dd80b7 100644 --- a/tests/TestCase/View/Helper/FormHelperTest.php +++ b/tests/TestCase/View/Helper/FormHelperTest.php @@ -658,7 +658,7 @@ public function testCreateAutoUrl() $this->article['defaults'] = ['id' => 1]; $this->Form->request->here = '/articles/edit/1'; $this->Form->request['action'] = 'delete'; - $result = $this->Form->create($this->article, ['action' => 'edit']); + $result = $this->Form->create($this->article, ['url' => ['action' => 'edit']]); $expected = [ 'form' => [ 'method' => 'post', @@ -695,7 +695,7 @@ public function testCreateAutoUrl() $this->assertHtml($expected, $result); $this->Form->request['controller'] = 'Pages'; - $result = $this->Form->create($this->article, ['action' => 'signup']); + $result = $this->Form->create($this->article, ['url' => ['action' => 'signup']]); $expected = [ 'form' => [ 'method' => 'post', 'action' => '/Pages/signup/1', @@ -743,7 +743,7 @@ public function testCreateCustomRoute() $this->Form->request['controller'] = 'users'; - $result = $this->Form->create(false, ['action' => 'login']); + $result = $this->Form->create(false, ['url' => ['action' => 'login']]); $expected = [ 'form' => [ 'method' => 'post', 'action' => '/login', From 2f064b11316897ea7d854bada63d5ed44bc3ebd9 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 21 Dec 2015 16:39:37 +0100 Subject: [PATCH 076/128] Fix tests. --- tests/TestCase/View/Helper/FormHelperTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/TestCase/View/Helper/FormHelperTest.php b/tests/TestCase/View/Helper/FormHelperTest.php index bba59dd80b7..cb4a9d2667e 100644 --- a/tests/TestCase/View/Helper/FormHelperTest.php +++ b/tests/TestCase/View/Helper/FormHelperTest.php @@ -727,7 +727,7 @@ public function testCreateNoUrl() ]; $this->assertHtml($expected, $result); - $result = $this->Form->create(false, ['action' => false]); + $result = $this->Form->create(false, ['url' => ['action' => false]]); $this->assertHtml($expected, $result); } @@ -783,7 +783,7 @@ public function testCreateWithAcceptCharset() $result = $this->Form->create( $this->article, [ - 'type' => 'post', 'action' => 'index', 'encoding' => 'iso-8859-1' + 'type' => 'post', 'url' => ['action' => 'index'], 'encoding' => 'iso-8859-1' ] ); $expected = [ From d9670cae26efbb3214fb8f28aa266a62e036e699 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 21 Dec 2015 17:14:15 +0100 Subject: [PATCH 077/128] Remove deprecated test. --- tests/TestCase/View/Helper/FormHelperTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/TestCase/View/Helper/FormHelperTest.php b/tests/TestCase/View/Helper/FormHelperTest.php index cb4a9d2667e..698cfcc274c 100644 --- a/tests/TestCase/View/Helper/FormHelperTest.php +++ b/tests/TestCase/View/Helper/FormHelperTest.php @@ -715,7 +715,7 @@ public function testCreateAutoUrl() */ public function testCreateNoUrl() { - $result = $this->Form->create(false, ['url' => false]); + $result = $this->Form->create(false, ['url' => ['action' => false]]); $expected = [ 'form' => [ 'method' => 'post', @@ -726,9 +726,6 @@ public function testCreateNoUrl() '/div' ]; $this->assertHtml($expected, $result); - - $result = $this->Form->create(false, ['url' => ['action' => false]]); - $this->assertHtml($expected, $result); } /** From 3f1cff1b3e6b08cf9fb0637cb84bc2a78d075abe Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 21 Dec 2015 17:55:42 +0100 Subject: [PATCH 078/128] Use url=>false for not displaying action. --- tests/TestCase/View/Helper/FormHelperTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/View/Helper/FormHelperTest.php b/tests/TestCase/View/Helper/FormHelperTest.php index 698cfcc274c..66ef72b3b3a 100644 --- a/tests/TestCase/View/Helper/FormHelperTest.php +++ b/tests/TestCase/View/Helper/FormHelperTest.php @@ -715,7 +715,7 @@ public function testCreateAutoUrl() */ public function testCreateNoUrl() { - $result = $this->Form->create(false, ['url' => ['action' => false]]); + $result = $this->Form->create(false, ['url' => false]); $expected = [ 'form' => [ 'method' => 'post', From 051a23928fbbd273e29edbd13c2944c767cd8ea8 Mon Sep 17 00:00:00 2001 From: antograssiot Date: Tue, 22 Dec 2015 22:09:30 +0100 Subject: [PATCH 079/128] use a defined time in test DiffForHumans --- tests/TestCase/I18n/TimeTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/TestCase/I18n/TimeTest.php b/tests/TestCase/I18n/TimeTest.php index c3c4974e0a6..0be01c7869a 100644 --- a/tests/TestCase/I18n/TimeTest.php +++ b/tests/TestCase/I18n/TimeTest.php @@ -647,6 +647,7 @@ public function testDiffForHumans($class) */ public function testDiffForHumansAbsolute($class) { + $class::setTestNow(new $class('2015-12-12 10:10:10')); $time = new $class('2014-04-20 10:10:10'); $this->assertEquals('1 year', $time->diffForHumans(null, ['absolute' => true])); @@ -665,6 +666,7 @@ public function testDiffForHumansAbsolute($class) */ public function testDiffForHumansNow($class) { + $class::setTestNow(new $class('2015-12-12 10:10:10')); $time = new $class('2014-04-20 10:10:10'); $this->assertEquals('1 year ago', $time->diffForHumans()); From 7c3a38505bcfa977a36aa04e6a0503d61ea7c456 Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Mon, 21 Dec 2015 16:04:30 +0700 Subject: [PATCH 080/128] Add convenience methods Shell::info|warn|success() --- src/Console/Shell.php | 41 +++++++++++++++++++++++++++ tests/TestCase/Console/ShellTest.php | 42 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/Console/Shell.php b/src/Console/Shell.php index 9e833aead27..bc75073255d 100644 --- a/src/Console/Shell.php +++ b/src/Console/Shell.php @@ -639,6 +639,47 @@ public function err($message = null, $newlines = 1) $this->_io->err($message, $newlines); } + /** + * Convenience method for out() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool Returns the number of bytes returned from writing to stdout. + * @see http://book.cakephp.org/3.0/en/console-and-shells.html#Shell::out + */ + public function info($message = null, $newlines = 1, $level = Shell::NORMAL) + { + return $this->out('' . $message . '', $newlines, $level); + } + + /** + * Convenience method for err() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @return void + * @see http://book.cakephp.org/3.0/en/console-and-shells.html#Shell::err + */ + public function warn($message = null, $newlines = 1) + { + return $this->err('' . $message . '', $newlines); + } + + /** + * Convenience method for out() that wraps message between tag + * + * @param string|array|null $message A string or an array of strings to output + * @param int $newlines Number of newlines to append + * @param int $level The message's output level, see above. + * @return int|bool Returns the number of bytes returned from writing to stdout. + * @see http://book.cakephp.org/3.0/en/console-and-shells.html#Shell::out + */ + public function success($message = null, $newlines = 1, $level = Shell::NORMAL) + { + return $this->out('' . $message . '', $newlines, $level); + } + /** * Returns a single or multiple linefeeds sequences. * diff --git a/tests/TestCase/Console/ShellTest.php b/tests/TestCase/Console/ShellTest.php index 5b03cdc5bb1..ef03302b638 100644 --- a/tests/TestCase/Console/ShellTest.php +++ b/tests/TestCase/Console/ShellTest.php @@ -315,6 +315,48 @@ public function testErr() $this->Shell->err('Just a test'); } + /** + * testInfo method + * + * @return void + */ + public function testInfo() + { + $this->io->expects($this->once()) + ->method('out') + ->with('Just a test', 1); + + $this->Shell->info('Just a test'); + } + + /** + * testWarn method + * + * @return void + */ + public function testWarn() + { + $this->io->expects($this->once()) + ->method('err') + ->with('Just a test', 1); + + $this->Shell->warn('Just a test'); + } + + /** + * testSuccess method + * + * @return void + */ + public function testSuccess() + { + $this->io->expects($this->once()) + ->method('out') + ->with('Just a test', 1); + + $this->Shell->success('Just a test'); + } + /** * testNl * From fdea6da3914d7c88d2c210ed46db94c57abfd22b Mon Sep 17 00:00:00 2001 From: antograssiot Date: Wed, 23 Dec 2015 21:57:59 +0100 Subject: [PATCH 081/128] fix phpcs --- src/Console/Shell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/Shell.php b/src/Console/Shell.php index bc75073255d..6810df0cbc2 100644 --- a/src/Console/Shell.php +++ b/src/Console/Shell.php @@ -658,7 +658,7 @@ public function info($message = null, $newlines = 1, $level = Shell::NORMAL) * * @param string|array|null $message A string or an array of strings to output * @param int $newlines Number of newlines to append - * @return void + * @return int|bool Returns the number of bytes returned from writing to stderr. * @see http://book.cakephp.org/3.0/en/console-and-shells.html#Shell::err */ public function warn($message = null, $newlines = 1) From 4957a5c7581d5bc5800e38fe5d43ff25dbe0dbd4 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Fri, 25 Dec 2015 15:58:31 +0100 Subject: [PATCH 082/128] Using APCu instead of APC The APC extension is gone in PHP 5.5. Since we require this version now, we need to adapt the engine so it is usable again --- src/Cache/Engine/ApcEngine.php | 48 ++++++++----------- tests/TestCase/Cache/Engine/ApcEngineTest.php | 16 +++---- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/Cache/Engine/ApcEngine.php b/src/Cache/Engine/ApcEngine.php index d4d0d44309f..172c504a4b7 100644 --- a/src/Cache/Engine/ApcEngine.php +++ b/src/Cache/Engine/ApcEngine.php @@ -14,7 +14,7 @@ */ namespace Cake\Cache\Engine; -use APCIterator; +use APCUIterator; use Cake\Cache\CacheEngine; /** @@ -42,7 +42,7 @@ class ApcEngine extends CacheEngine */ public function init(array $config = []) { - if (!extension_loaded('apc')) { + if (!extension_loaded('apcu')) { return false; } @@ -66,8 +66,8 @@ public function write($key, $value) if ($duration) { $expires = time() + $duration; } - apc_store($key . '_expires', $expires, $duration); - return apc_store($key, $value, $duration); + apcu_store($key . '_expires', $expires, $duration); + return apcu_store($key, $value, $duration); } /** @@ -82,11 +82,11 @@ public function read($key) $key = $this->_key($key); $time = time(); - $cachetime = (int)apc_fetch($key . '_expires'); + $cachetime = (int)apcu_fetch($key . '_expires'); if ($cachetime !== 0 && ($cachetime < $time || ($time + $this->_config['duration']) < $cachetime)) { return false; } - return apc_fetch($key); + return apcu_fetch($key); } /** @@ -100,7 +100,7 @@ public function increment($key, $offset = 1) { $key = $this->_key($key); - return apc_inc($key, $offset); + return apcu_inc($key, $offset); } /** @@ -114,7 +114,7 @@ public function decrement($key, $offset = 1) { $key = $this->_key($key); - return apc_dec($key, $offset); + return apcu_dec($key, $offset); } /** @@ -127,7 +127,7 @@ public function delete($key) { $key = $this->_key($key); - return apc_delete($key); + return apcu_delete($key); } /** @@ -142,21 +142,11 @@ public function clear($check) if ($check) { return true; } - if (class_exists('APCIterator', false)) { - $iterator = new APCIterator( - 'user', - '/^' . preg_quote($this->_config['prefix'], '/') . '/', - APC_ITER_NONE - ); - apc_delete($iterator); - return true; - } - $cache = apc_cache_info('user'); - foreach ($cache['cache_list'] as $key) { - if (strpos($key['info'], $this->_config['prefix']) === 0) { - apc_delete($key['info']); - } - } + $iterator = new APCUIterator( + '/^' . preg_quote($this->_config['prefix'], '/') . '/', + APC_ITER_NONE + ); + apcu_delete($iterator); return true; } @@ -178,8 +168,8 @@ public function add($key, $value) if ($duration) { $expires = time() + $duration; } - apc_add($key . '_expires', $expires, $duration); - return apc_add($key, $value, $duration); + apcu_add($key . '_expires', $expires, $duration); + return apcu_add($key, $value, $duration); } /** @@ -197,11 +187,11 @@ public function groups() } } - $groups = apc_fetch($this->_compiledGroupNames); + $groups = apcu_fetch($this->_compiledGroupNames); if (count($groups) !== count($this->_config['groups'])) { foreach ($this->_compiledGroupNames as $group) { if (!isset($groups[$group])) { - apc_store($group, 1); + apcu_store($group, 1); $groups[$group] = 1; } } @@ -225,7 +215,7 @@ public function groups() */ public function clearGroup($group) { - apc_inc($this->_config['prefix'] . $group, 1, $success); + apcu_inc($this->_config['prefix'] . $group, 1, $success); return $success; } } diff --git a/tests/TestCase/Cache/Engine/ApcEngineTest.php b/tests/TestCase/Cache/Engine/ApcEngineTest.php index a753fa5ec13..6f716b158a4 100644 --- a/tests/TestCase/Cache/Engine/ApcEngineTest.php +++ b/tests/TestCase/Cache/Engine/ApcEngineTest.php @@ -34,7 +34,7 @@ class ApcEngineTest extends TestCase public function setUp() { parent::setUp(); - $this->skipIf(!function_exists('apc_store'), 'Apc is not installed or configured properly.'); + $this->skipIf(!function_exists('apcu_store'), 'Apc is not installed or configured properly.'); if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) { $this->skipIf(!ini_get('apc.enable_cli'), 'APC is not enabled for the CLI.'); @@ -155,8 +155,6 @@ public function testDeleteCache() */ public function testDecrement() { - $this->skipIf(!function_exists('apc_dec'), 'No apc_dec() function, cannot test decrement().'); - $result = Cache::write('test_decrement', 5, 'apc'); $this->assertTrue($result); @@ -180,8 +178,6 @@ public function testDecrement() */ public function testIncrement() { - $this->skipIf(!function_exists('apc_inc'), 'No apc_inc() function, cannot test increment().'); - $result = Cache::write('test_increment', 5, 'apc'); $this->assertTrue($result); @@ -205,14 +201,14 @@ public function testIncrement() */ public function testClear() { - apc_store('not_cake', 'survive'); + apcu_store('not_cake', 'survive'); Cache::write('some_value', 'value', 'apc'); $result = Cache::clear(false, 'apc'); $this->assertTrue($result); $this->assertFalse(Cache::read('some_value', 'apc')); - $this->assertEquals('survive', apc_fetch('not_cake')); - apc_delete('not_cake'); + $this->assertEquals('survive', apcu_fetch('not_cake')); + apcu_delete('not_cake'); } /** @@ -233,12 +229,12 @@ public function testGroupsReadWrite() $this->assertTrue(Cache::write('test_groups', 'value', 'apc_groups')); $this->assertEquals('value', Cache::read('test_groups', 'apc_groups')); - apc_inc('test_group_a'); + apcu_inc('test_group_a'); $this->assertFalse(Cache::read('test_groups', 'apc_groups')); $this->assertTrue(Cache::write('test_groups', 'value2', 'apc_groups')); $this->assertEquals('value2', Cache::read('test_groups', 'apc_groups')); - apc_inc('test_group_b'); + apcu_inc('test_group_b'); $this->assertFalse(Cache::read('test_groups', 'apc_groups')); $this->assertTrue(Cache::write('test_groups', 'value3', 'apc_groups')); $this->assertEquals('value3', Cache::read('test_groups', 'apc_groups')); From a67a631a1973f1d40566f29ef79de00a7c4d51fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Lorenzo=20Rodr=C3=ADguez?= Date: Fri, 25 Dec 2015 19:57:05 +0100 Subject: [PATCH 083/128] Changing error message --- tests/TestCase/Cache/Engine/ApcEngineTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Cache/Engine/ApcEngineTest.php b/tests/TestCase/Cache/Engine/ApcEngineTest.php index 6f716b158a4..f948ed66c76 100644 --- a/tests/TestCase/Cache/Engine/ApcEngineTest.php +++ b/tests/TestCase/Cache/Engine/ApcEngineTest.php @@ -34,7 +34,7 @@ class ApcEngineTest extends TestCase public function setUp() { parent::setUp(); - $this->skipIf(!function_exists('apcu_store'), 'Apc is not installed or configured properly.'); + $this->skipIf(!function_exists('apcu_store'), 'APCu is not installed or configured properly.'); if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) { $this->skipIf(!ini_get('apc.enable_cli'), 'APC is not enabled for the CLI.'); From a77d0c42133273a83c032c8397ed3787c3d72e35 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 29 Nov 2015 03:40:19 -0600 Subject: [PATCH 084/128] Increase memory during shutdown for Fatal Error handling --- src/Error/BaseErrorHandler.php | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/Error/BaseErrorHandler.php b/src/Error/BaseErrorHandler.php index 6a9ca73da88..cc4dac79168 100644 --- a/src/Error/BaseErrorHandler.php +++ b/src/Error/BaseErrorHandler.php @@ -72,6 +72,13 @@ public function register() if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) { return; } + $megabytes = Configure::read('Error.extraFatalErrorMemory'); + if ($megabytes === null) { + $megabytes = 4; + } + if ($megabytes !== false && $megabytes > 0) { + static::increaseMemoryLimit($megabytes * 1024); + } $error = error_get_last(); if (!is_array($error)) { return; @@ -212,6 +219,36 @@ public function handleFatalError($code, $description, $file, $line) return true; } + /** + * Increases the PHP "memory_limit" ini setting by the specified amount + * in kilobytes + * + * @param string $additionalKb Number in kilobytes + * @return void + */ + public static function increaseMemoryLimit($additionalKb) + { + $limit = ini_get("memory_limit"); + if (!is_string($limit) || !strlen($limit)) { + return; + } + $limit = trim($limit); + $units = strtoupper(substr($limit, -1)); + $current = substr($limit, 0, strlen($limit) - 1); + if ($units === "M") { + $current = $current * 1024; + $units = "K"; + } + if ($units === "G") { + $current = $current * 1024 * 1024; + $units = "K"; + } + + if ($units === "K") { + ini_set("memory_limit", ceil($current + $additionalKb) . "K"); + } + } + /** * Log an error. * From 0c541b7141f3cc249393363b769599ccc546a1e8 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 27 Dec 2015 21:19:28 -0500 Subject: [PATCH 085/128] Add coverage for memory limit increases. Include tests for common memory limits. Refs #7550 --- src/Error/BaseErrorHandler.php | 26 ++++++++-------- tests/TestCase/Error/ErrorHandlerTest.php | 37 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/Error/BaseErrorHandler.php b/src/Error/BaseErrorHandler.php index cc4dac79168..68b2d26a3c7 100644 --- a/src/Error/BaseErrorHandler.php +++ b/src/Error/BaseErrorHandler.php @@ -76,8 +76,8 @@ public function register() if ($megabytes === null) { $megabytes = 4; } - if ($megabytes !== false && $megabytes > 0) { - static::increaseMemoryLimit($megabytes * 1024); + if ($megabytes > 0) { + $this->increaseMemoryLimit($megabytes * 1024); } $error = error_get_last(); if (!is_array($error)) { @@ -93,7 +93,7 @@ public function register() } $this->handleFatalError( $error['type'], - $error['message'], + $error['message'], $error['file'], $error['line'] ); @@ -222,30 +222,30 @@ public function handleFatalError($code, $description, $file, $line) /** * Increases the PHP "memory_limit" ini setting by the specified amount * in kilobytes - * + * * @param string $additionalKb Number in kilobytes * @return void */ - public static function increaseMemoryLimit($additionalKb) + public function increaseMemoryLimit($additionalKb) { - $limit = ini_get("memory_limit"); - if (!is_string($limit) || !strlen($limit)) { + $limit = ini_get('memory_limit'); + if (!strlen($limit) || $limit === '-1') { return; } $limit = trim($limit); $units = strtoupper(substr($limit, -1)); $current = substr($limit, 0, strlen($limit) - 1); - if ($units === "M") { + if ($units === 'M') { $current = $current * 1024; - $units = "K"; + $units = 'K'; } - if ($units === "G") { + if ($units === 'G') { $current = $current * 1024 * 1024; - $units = "K"; + $units = 'K'; } - if ($units === "K") { - ini_set("memory_limit", ceil($current + $additionalKb) . "K"); + if ($units === 'K') { + ini_set('memory_limit', ceil($current + $additionalKb) . 'K'); } } diff --git a/tests/TestCase/Error/ErrorHandlerTest.php b/tests/TestCase/Error/ErrorHandlerTest.php index 244ad2c70e9..bb4191f8572 100644 --- a/tests/TestCase/Error/ErrorHandlerTest.php +++ b/tests/TestCase/Error/ErrorHandlerTest.php @@ -433,4 +433,41 @@ public function testHandlePHP7Error() $errorHandler->handleException($error); $this->assertContains('Unexpected variable foo', $errorHandler->response->body(), 'message missing.'); } + + /** + * Data provider for memory limit changing. + * + * @return array + */ + public function memoryLimitProvider() + { + return [ + // start, adjust, expected + ['256M', 4, '262148K'], + ['262144K', 4, '262148K'], + ['1G', 128, '1048704K'], + ]; + } + + /** + * Test increasing the memory limit. + * + * @dataProvider memoryLimitProvider + * @return void + */ + public function testIncreaseMemoryLimit($start, $adjust, $expected) + { + $initial = ini_get('memory_limit'); + $this->skipIf(strlen($initial) === 0, 'Cannot read memory limit, and cannot test increasing it.'); + + // phpunit.xml often has -1 as memory limit + ini_set('memory_limit', $start); + + $errorHandler = new TestErrorHandler(); + $this->assertNull($errorHandler->increaseMemoryLimit($adjust)); + $new = ini_get('memory_limit'); + $this->assertEquals($expected, $new, 'memory limit did not get increased.'); + + ini_set('memory_limit', $initial); + } } From ab0da785290cd05acc2830873c86cca61619486d Mon Sep 17 00:00:00 2001 From: antograssiot Date: Mon, 28 Dec 2015 05:51:26 +0100 Subject: [PATCH 086/128] fix CS after 282f6c80 --- src/Error/BaseErrorHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Error/BaseErrorHandler.php b/src/Error/BaseErrorHandler.php index 68b2d26a3c7..72a5c09d63d 100644 --- a/src/Error/BaseErrorHandler.php +++ b/src/Error/BaseErrorHandler.php @@ -93,7 +93,7 @@ public function register() } $this->handleFatalError( $error['type'], - $error['message'], + $error['message'], $error['file'], $error['line'] ); From c6f40880dfe6cd9636f8d452fd91cc54ad910344 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 29 Dec 2015 00:35:42 -0500 Subject: [PATCH 087/128] Start sketching in the APIs presented in #7908 but with exceptions I feel that exceptions are better than exit() long term and will lead to more maintainable code. --- src/Console/Exception/StopException.php | 26 +++++++++++++++++++ src/Console/Shell.php | 33 ++++++++++++++++++++----- 2 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/Console/Exception/StopException.php diff --git a/src/Console/Exception/StopException.php b/src/Console/Exception/StopException.php new file mode 100644 index 00000000000..cda9c18c453 --- /dev/null +++ b/src/Console/Exception/StopException.php @@ -0,0 +1,26 @@ +_io->hr($newlines, $width); } + /** + * Displays a formatted error message + * and exits the application with status code 1 + * + * @param string $message The error message + * @param int $exitCode The exit code for the shell task. + * @throws \Cake\Console\Exception\StopException + * @return void + * @link http://book.cakephp.org/3.0/en/console-and-shells.html#styling-output + */ + public function abort($message, $exitCode) + { + $this->_io->err('' . $message . ''); + throw new StopException($message, $exitCode); + } + /** * Displays a formatted error message * and exits the application with status code 1 * * @param string $title Title of the error * @param string|null $message An optional error message + * @param int $exitCode The exit code for the shell task. + * @throws \Cake\Console\Exception\StopException * @return int Error code * @link http://book.cakephp.org/3.0/en/console-and-shells.html#styling-output + * @deprecated Since 3.2.0. Use Shell::abort() instead. */ - public function error($title, $message = null) + public function error($title, $message = null, $exitCode = self::CODE_ERROR) { $this->_io->err(sprintf('Error: %s', $title)); if (!empty($message)) { $this->_io->err($message); } - $this->_stop(self::CODE_ERROR); - return self::CODE_ERROR; + $this->_stop($exitCode); + return $exitCode; } /** @@ -827,15 +847,16 @@ public function helper($name, array $settings = []) } /** - * Stop execution of the current script. Wraps exit() making - * testing easier. + * Stop execution of the current script. + * Raises a StopException to try and halt the execution. * * @param int|string $status see http://php.net/exit for values + * @throws \Cake\Console\Exception\StopException * @return void */ protected function _stop($status = 0) { - exit($status); + throw new StopException('Halting error reached', $status); } /** From a723f6b1bae5db403525f731ac2d29353540399c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 29 Dec 2015 14:17:02 -0500 Subject: [PATCH 088/128] Handle StopException in ShellDispatcher. The ShellDispatcher should drop exception messages, and use the exception code as the exit code. Also add a default error code of 1 which will be non-zero making shell environments behave correctly. --- src/Console/Shell.php | 2 +- src/Console/ShellDispatcher.php | 7 ++++- .../TestCase/Console/ShellDispatcherTest.php | 26 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Console/Shell.php b/src/Console/Shell.php index b82a02da087..14d437953e1 100644 --- a/src/Console/Shell.php +++ b/src/Console/Shell.php @@ -723,7 +723,7 @@ public function hr($newlines = 0, $width = 63) * @return void * @link http://book.cakephp.org/3.0/en/console-and-shells.html#styling-output */ - public function abort($message, $exitCode) + public function abort($message, $exitCode = self::CODE_ERROR) { $this->_io->err('' . $message . ''); throw new StopException($message, $exitCode); diff --git a/src/Console/ShellDispatcher.php b/src/Console/ShellDispatcher.php index 03ce43ddeda..e7f2b78b3b3 100644 --- a/src/Console/ShellDispatcher.php +++ b/src/Console/ShellDispatcher.php @@ -15,6 +15,7 @@ namespace Cake\Console; use Cake\Console\Exception\MissingShellException; +use Cake\Console\Exception\StopException; use Cake\Core\App; use Cake\Core\Configure; use Cake\Core\Exception\Exception; @@ -176,7 +177,11 @@ protected function _bootstrap() */ public function dispatch($extra = []) { - $result = $this->_dispatch($extra); + try { + $result = $this->_dispatch($extra); + } catch (StopException $e) { + return $e->getCode(); + } if ($result === null || $result === true) { return 0; } diff --git a/tests/TestCase/Console/ShellDispatcherTest.php b/tests/TestCase/Console/ShellDispatcherTest.php index a3ab420b1a9..1667de3c31d 100644 --- a/tests/TestCase/Console/ShellDispatcherTest.php +++ b/tests/TestCase/Console/ShellDispatcherTest.php @@ -148,6 +148,32 @@ public function testFindShellAliasedAppShadow() $this->assertEquals('Sample', $result->name); } + /** + * Verify dispatch handling stop errors + * + * @return void + */ + public function testDispatchShellWithAbort() + { + $io = $this->getMock('Cake\Console\ConsoleIo'); + $shell = $this->getMock('Cake\Console\Shell', ['main'], [$io]); + $shell->expects($this->once()) + ->method('main') + ->will($this->returnCallback(function () use ($shell) { + $shell->abort('Bad things', 99); + })); + + $dispatcher = $this->getMock('Cake\Console\ShellDispatcher', ['findShell']); + $dispatcher->expects($this->any()) + ->method('findShell') + ->with('aborter') + ->will($this->returnValue($shell)); + + $dispatcher->args = ['aborter']; + $result = $dispatcher->dispatch(); + $this->assertSame(99, $result, 'Should return the exception error code.'); + } + /** * Verify correct dispatch of Shell subclasses with a main method * From e0f42abbb40e46db5b35abf6851728b8c54b99b5 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 31 Dec 2015 12:53:23 -0500 Subject: [PATCH 089/128] Tighten up CSRF Component restrictions. Any request with a 'post' data should be subject to CSRF token validation. This lets us catch scenarios with OPTIONS has a request body. Also force commonly methods that commonly mutate state to require a CSRF token. Based on feedback from @bittarman --- src/Controller/Component/CsrfComponent.php | 2 +- .../Controller/Component/CsrfComponentTest.php | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Controller/Component/CsrfComponent.php b/src/Controller/Component/CsrfComponent.php index 1824c281deb..0235cf732e9 100644 --- a/src/Controller/Component/CsrfComponent.php +++ b/src/Controller/Component/CsrfComponent.php @@ -94,7 +94,7 @@ public function startup(Event $event) if ($request->is('get') && $cookieData === null) { $this->_setCookie($request, $response); } - if (!$request->is(['head', 'get', 'options'])) { + if ($request->is(['put', 'post', 'delete', 'patch']) || !empty($request->data)) { $this->_validateToken($request); unset($request->data[$this->_config['field']]); } diff --git a/tests/TestCase/Controller/Component/CsrfComponentTest.php b/tests/TestCase/Controller/Component/CsrfComponentTest.php index 8b0a25bf8b2..c0dcd1ce485 100644 --- a/tests/TestCase/Controller/Component/CsrfComponentTest.php +++ b/tests/TestCase/Controller/Component/CsrfComponentTest.php @@ -83,12 +83,14 @@ public function testSettingCookie() /** * Data provider for HTTP method tests. * + * HEAD and GET do not populate $_POST or request->data. + * * @return void */ public static function safeHttpMethodProvider() { return [ - ['GET'], ['OPTIONS'], ['HEAD'] + ['GET', 'HEAD'] ]; } @@ -116,14 +118,14 @@ public function testSafeMethodNoCsrfRequired($method) } /** - * Data provider for HTTP method tests. + * Data provider for HTTP methods that can contain request bodies. * * @return void */ public static function httpMethodProvider() { return [ - ['PATCH'], ['PUT'], ['POST'], ['DELETE'], ['PURGE'], ['INVALIDMETHOD'] + ['OPTIONS'], ['PATCH'], ['PUT'], ['POST'], ['DELETE'], ['PURGE'], ['INVALIDMETHOD'] ]; } @@ -142,6 +144,7 @@ public function testValidTokenInHeader($method) 'REQUEST_METHOD' => $method, 'HTTP_X_CSRF_TOKEN' => 'testing123', ], + 'post' => ['a' => 'b'], 'cookies' => ['csrfToken' => 'testing123'] ]); $controller->response = new Response(); @@ -167,6 +170,7 @@ public function testInvalidTokenInHeader($method) 'REQUEST_METHOD' => $method, 'HTTP_X_CSRF_TOKEN' => 'nope', ], + 'post' => ['a' => 'b'], 'cookies' => ['csrfToken' => 'testing123'] ]); $controller->response = new Response(); From 11f44cfea71f0284458fef402294aa03318c8d2d Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 31 Dec 2015 13:13:33 -0500 Subject: [PATCH 090/128] Default CSRF token to Httponly. While this _could_ be backwards incompatible. I think it is a safer default to start new applications with. --- src/Controller/Component/CsrfComponent.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/Component/CsrfComponent.php b/src/Controller/Component/CsrfComponent.php index 0235cf732e9..e23154aa6ee 100644 --- a/src/Controller/Component/CsrfComponent.php +++ b/src/Controller/Component/CsrfComponent.php @@ -46,7 +46,7 @@ class CsrfComponent extends Component * - cookieName = The name of the cookie to send. * - expiry = How long the CSRF token should last. Defaults to browser session. * - secure = Whether or not the cookie will be set with the Secure flag. Defaults to false. - * - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to false. + * - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to true. * - field = The form field to check. Changing this will also require configuring * FormHelper. * @@ -56,7 +56,7 @@ class CsrfComponent extends Component 'cookieName' => 'csrfToken', 'expiry' => 0, 'secure' => false, - 'httpOnly' => false, + 'httpOnly' => true, 'field' => '_csrfToken', ]; From 2044d6db296c27bff734c28442b40cf469adeb39 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 31 Dec 2015 14:08:18 -0500 Subject: [PATCH 091/128] Require native prepares for most drivers. I could not enable native prepares for MySQL as it caused a number of tests to fail. From looking at other projects this is due to MySQL's native prepare methods having a few gotchas. --- src/Database/Driver/Mysql.php | 2 +- src/Database/Driver/Postgres.php | 1 + src/Database/Driver/Sqlite.php | 1 + src/Database/Driver/Sqlserver.php | 1 + tests/TestCase/Database/Driver/PostgresTest.php | 2 ++ tests/TestCase/Database/Driver/SqliteTest.php | 2 ++ tests/TestCase/Database/Driver/SqlserverTest.php | 1 + 7 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Database/Driver/Mysql.php b/src/Database/Driver/Mysql.php index a504584dbe9..9f5a1295068 100644 --- a/src/Database/Driver/Mysql.php +++ b/src/Database/Driver/Mysql.php @@ -70,7 +70,7 @@ public function connect() $config['flags'] += [ PDO::ATTR_PERSISTENT => $config['persistent'], PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ]; if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) { diff --git a/src/Database/Driver/Postgres.php b/src/Database/Driver/Postgres.php index fc5d8170f9e..9af9a8a36d0 100644 --- a/src/Database/Driver/Postgres.php +++ b/src/Database/Driver/Postgres.php @@ -56,6 +56,7 @@ public function connect() $config = $this->_config; $config['flags'] += [ PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]; if (empty($config['unix_socket'])) { diff --git a/src/Database/Driver/Sqlite.php b/src/Database/Driver/Sqlite.php index 7be956bcd8b..25b376c4a05 100644 --- a/src/Database/Driver/Sqlite.php +++ b/src/Database/Driver/Sqlite.php @@ -55,6 +55,7 @@ public function connect() $config = $this->_config; $config['flags'] += [ PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]; diff --git a/src/Database/Driver/Sqlserver.php b/src/Database/Driver/Sqlserver.php index 7172893dfd0..7eeec5928a7 100644 --- a/src/Database/Driver/Sqlserver.php +++ b/src/Database/Driver/Sqlserver.php @@ -60,6 +60,7 @@ public function connect() $config = $this->_config; $config['flags'] += [ PDO::ATTR_PERSISTENT => $config['persistent'], + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]; diff --git a/tests/TestCase/Database/Driver/PostgresTest.php b/tests/TestCase/Database/Driver/PostgresTest.php index bdefe5c408a..c39b2154b01 100644 --- a/tests/TestCase/Database/Driver/PostgresTest.php +++ b/tests/TestCase/Database/Driver/PostgresTest.php @@ -53,6 +53,7 @@ public function testConnectionConfigDefault() $expected['flags'] += [ PDO::ATTR_PERSISTENT => true, + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]; @@ -107,6 +108,7 @@ public function testConnectionConfigCustom() $expected = $config; $expected['flags'] += [ PDO::ATTR_PERSISTENT => false, + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]; diff --git a/tests/TestCase/Database/Driver/SqliteTest.php b/tests/TestCase/Database/Driver/SqliteTest.php index abbab30355d..5f8294e1723 100644 --- a/tests/TestCase/Database/Driver/SqliteTest.php +++ b/tests/TestCase/Database/Driver/SqliteTest.php @@ -47,6 +47,7 @@ public function testConnectionConfigDefault() $expected['flags'] += [ PDO::ATTR_PERSISTENT => false, + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]; $driver->expects($this->once())->method('_connect') @@ -80,6 +81,7 @@ public function testConnectionConfigCustom() $expected += ['username' => null, 'password' => null]; $expected['flags'] += [ PDO::ATTR_PERSISTENT => true, + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]; diff --git a/tests/TestCase/Database/Driver/SqlserverTest.php b/tests/TestCase/Database/Driver/SqlserverTest.php index a18772389e0..ac4ac88d6e9 100644 --- a/tests/TestCase/Database/Driver/SqlserverTest.php +++ b/tests/TestCase/Database/Driver/SqlserverTest.php @@ -67,6 +67,7 @@ public function testConnectionConfigCustom() $expected = $config; $expected['flags'] += [ PDO::ATTR_PERSISTENT => false, + PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::SQLSRV_ATTR_ENCODING => 'a-language' ]; From 34a32fed44ca4a2e00e518018eadebf787d86151 Mon Sep 17 00:00:00 2001 From: antograssiot Date: Fri, 1 Jan 2016 17:34:22 +0100 Subject: [PATCH 092/128] bump minimum version to 5.5.10 ref https://bugs.php.net/bug.php?id=45543 --- appveyor.yml | 5 ++++- composer.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4b41b21978a..ca8a8efc4df 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,10 +27,12 @@ init: install: - cd c:\ - - appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-5.5.8-nts-Win32-VC11-x86.zip -FileName php.zip + - appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-5.5.10-nts-Win32-VC11-x86.zip -FileName php.zip - 7z x php.zip -oc:\php - appveyor DownloadFile https://dl.dropboxusercontent.com/s/euip490d9183jkr/SQLSRV32.cab -FileName sqlsrv.cab - 7z x sqlsrv.cab -oc:\php\ext php*_55_nts.dll + - cd c:\php\ext + - appveyor DownloadFile http://xdebug.org/files/php_xdebug-2.3.3-5.5-vc11-nts.dll - cd c:\php - copy php.ini-production php.ini - echo date.timezone="UTC" >> php.ini @@ -41,6 +43,7 @@ install: - echo extension=php_intl.dll >> php.ini - echo extension=php_mbstring.dll >> php.ini - echo extension=php_fileinfo.dll >> php.ini + - echo extension=php_xdebug-2.3.3-5.5-vc11-nts.dll >> php.ini - cd C:\projects\cakephp - appveyor DownloadFile https://getcomposer.org/composer.phar - php composer.phar install --prefer-dist --no-interaction --ansi --no-progress diff --git a/composer.json b/composer.json index 5aa3ac584b8..56090fe9de9 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "source": "https://github.com/cakephp/cakephp" }, "require": { - "php": ">=5.5.0", + "php": ">=5.5.10", "ext-intl": "*", "ext-mbstring": "*", "cakephp/chronos": "*", From 931ed118ded254cd33bbe66fcc9dc30dc8b9953e Mon Sep 17 00:00:00 2001 From: antograssiot Date: Fri, 1 Jan 2016 18:51:45 +0100 Subject: [PATCH 093/128] remove x-debug extension --- appveyor.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ca8a8efc4df..252ce361d7e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,8 +31,6 @@ install: - 7z x php.zip -oc:\php - appveyor DownloadFile https://dl.dropboxusercontent.com/s/euip490d9183jkr/SQLSRV32.cab -FileName sqlsrv.cab - 7z x sqlsrv.cab -oc:\php\ext php*_55_nts.dll - - cd c:\php\ext - - appveyor DownloadFile http://xdebug.org/files/php_xdebug-2.3.3-5.5-vc11-nts.dll - cd c:\php - copy php.ini-production php.ini - echo date.timezone="UTC" >> php.ini @@ -43,7 +41,6 @@ install: - echo extension=php_intl.dll >> php.ini - echo extension=php_mbstring.dll >> php.ini - echo extension=php_fileinfo.dll >> php.ini - - echo extension=php_xdebug-2.3.3-5.5-vc11-nts.dll >> php.ini - cd C:\projects\cakephp - appveyor DownloadFile https://getcomposer.org/composer.phar - php composer.phar install --prefer-dist --no-interaction --ansi --no-progress From 32ee1d72ed8d0170d3491ef9d0d9424817672d6a Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Fri, 1 Jan 2016 21:35:33 +0100 Subject: [PATCH 094/128] Added helper methods in validator for the functions in Validation class This makes the validation API more discoverable and faster to create validators. Fixes #7684 --- src/Validation/Validation.php | 3 - src/Validation/Validator.php | 319 +++++++++++++++ tests/TestCase/Validation/ValidatorTest.php | 416 ++++++++++++++++++++ 3 files changed, 735 insertions(+), 3 deletions(-) diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index cb5caaf7bdc..b4b8ae97a2d 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -130,9 +130,6 @@ public static function lengthBetween($check, $min, $max) * Returns true if field is left blank -OR- only whitespace characters are present in its value * Whitespace characters include Space, Tab, Carriage Return, Newline * - * $check can be passed as an array: - * ['check' => 'valueToCheck']; - * * @param string|array $check Value to check * @return bool Success * @deprecated 3.0.2 diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 65a811cc413..fb3c6573c22 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -17,6 +17,7 @@ use ArrayAccess; use ArrayIterator; use Countable; +use InvalidArgumentException; use IteratorAggregate; /** @@ -535,6 +536,324 @@ public function notEmpty($field, $message = null, $when = false) return $this; } + public function notBlank($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'notBlank', $extra + [ + 'rule' => 'notBlank', + ]); + } + + public function alphaNumeric($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'alphaNumeric', $extra + [ + 'rule' => 'alphaNumeric', + ]); + } + + public function lengthBetween($field, array $range, $message = null, $when = null) + { + if (count($range) !== 2) { + throw new InvalidArgumentException('The $range argument requires 2 numbers'); + } + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'lengthBetween', $extra + [ + 'rule' => ['lengthBetween', array_shift($range), array_shift($range)], + ]); + } + + public function creditCard($field, $type = 'all', $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'creditCard', $extra + [ + 'rule' => ['cc', $type, true], + ]); + } + + public function greaterThan($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'greaterThan', $extra + [ + 'rule' => ['comparison', '>', $value] + ]); + } + + public function greaterThanOrEqual($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'greaterThanOrEqual', $extra + [ + 'rule' => ['comparison', '>=', $value] + ]); + } + + public function lessThan($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'lessThan', $extra + [ + 'rule' => ['comparison', '<', $value] + ]); + } + + public function lessThanOrEqual($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'lessThanOrEqual', $extra + [ + 'rule' => ['comparison', '<=', $value] + ]); + } + + public function equals($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'equals', $extra + [ + 'rule' => ['comparison', '=', $value] + ]); + } + + public function notEquals($field, $value, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'notEquals', $extra + [ + 'rule' => ['comparison', '!=', $value] + ]); + } + + public function sameAs($field, $secondField, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'sameAs', $extra + [ + 'rule' => ['compareWith', $secondField] + ]); + } + + public function containsNonAlphaNumeric($field, $limit = 1, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'containsNonAlphaNumeric', $extra + [ + 'rule' => ['containsNonAlphaNumeric', $limit] + ]); + } + + public function date($field, $formats = ['ymd'], $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'date', $extra + [ + 'rule' => ['date', $formats] + ]); + } + + public function dateTime($field, $formats = ['ymd'], $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'dateTime', $extra + [ + 'rule' => ['datetime', $formats] + ]); + } + + public function time($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'time', $extra + [ + 'rule' => 'time' + ]); + } + + public function boolean($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'boolean', $extra + [ + 'rule' => 'boolean' + ]); + } + + public function decimal($field, $places = null, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'decimal', $extra + [ + 'rule' => ['decimal', $places] + ]); + } + + public function email($field, $checkMX = false, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'email', $extra + [ + 'rule' => ['email', $checkMX] + ]); + } + + public function ip($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'ip', $extra + [ + 'rule' => 'ip' + ]); + } + + public function ipv4($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'ipv4', $extra + [ + 'rule' => ['ip', 'ipv4'] + ]); + } + + public function ipv6($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'ipv6', $extra + [ + 'rule' => ['ip', 'ipv6'] + ]); + } + + public function minLength($field, $min, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'minLength', $extra + [ + 'rule' => ['minLength', $min] + ]); + } + + public function maxLength($field, $max, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'maxLength', $extra + [ + 'rule' => ['maxLength', $max] + ]); + } + + public function numeric($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'numeric', $extra + [ + 'rule' => 'numeric' + ]); + } + + public function naturalNumber($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'naturalNumber', $extra + [ + 'rule' => ['naturalNumber', false] + ]); + } + + public function nonNegativeInteger($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'nonNegativeInteger', $extra + [ + 'rule' => ['naturalNumber', true] + ]); + } + + public function range($field, array $range, $message = null, $when = null) + { + if (count($range) !== 2) { + throw new InvalidArgumentException('The $range argument requires 2 numbers'); + } + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'range', $extra + [ + 'rule' => ['range', array_shift($range), array_shift($range)] + ]); + } + + public function url($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'url', $extra + [ + 'rule' => ['url', false] + ]); + } + + public function urlWithProtocol($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'urlWithProtocol', $extra + [ + 'rule' => ['url', true] + ]); + } + + public function inList($field, array $list, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'inList', $extra + [ + 'rule' => ['inList', $list] + ]); + } + + public function uuid($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'uuid', $extra + [ + 'rule' => 'uuid' + ]); + } + + public function uploadedFile($field, array $options, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'uploadedFile', $extra + [ + 'rule' => ['uploadedFile', $options] + ]); + } + + public function latLong($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'latLong', $extra + [ + 'rule' => 'geoCoordinate' + ]); + } + + public function latitude($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'latitude', $extra + [ + 'rule' => 'latitude' + ]); + } + + public function longitude($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'longitude', $extra + [ + 'rule' => 'longitude' + ]); + } + + public function ascii($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'ascii', $extra + [ + 'rule' => 'ascii' + ]); + } + + public function utf8($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'utf8', $extra + [ + 'rule' => ['utf8', ['extended' => false]] + ]); + } + + public function utf8Strict($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'utf8Strict', $extra + [ + 'rule' => ['utf8', ['extended' => true]] + ]); + } + + public function integer($field, $message = null, $when = null) + { + $extra = array_filter(['on' => $when, 'message' => $message]); + return $this->add($field, 'integer', $extra + [ + 'rule' =>'isInteger' + ]); + } + /** * Returns whether or not a field can be left empty for a new or already existing * record. diff --git a/tests/TestCase/Validation/ValidatorTest.php b/tests/TestCase/Validation/ValidatorTest.php index 34b65009a04..e68b4b89bf0 100644 --- a/tests/TestCase/Validation/ValidatorTest.php +++ b/tests/TestCase/Validation/ValidatorTest.php @@ -1047,4 +1047,420 @@ public function testNestedManyValidatorCreate() $this->assertNotEmpty($validator->errors(['user' => [['username' => 'example']]], true)); $this->assertEmpty($validator->errors(['user' => [['username' => 'a']]], false)); } + + /** + * Tests the notBlank proxy method + * + * @return void + */ + public function testNotBlank() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'notBlank'); + $this->assertNotEmpty($validator->errors(['username' => ' '])); + } + + /** + * Tests the alphanumeric proxy method + * + * @return void + */ + public function testAlphanumeric() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'alphaNumeric'); + $this->assertNotEmpty($validator->errors(['username' => '$'])); + } + + /** + * Tests the lengthBetween proxy method + * + * @return void + */ + public function testLengthBetween() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'lengthBetween', [5, 7], [5, 7]); + $this->assertNotEmpty($validator->errors(['username' => 'foo'])); + } + + /** + * Tests the creditCard proxy method + * + * @return void + */ + public function testCreditCard() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'creditCard', 'all', ['all', true], 'cc'); + $this->assertNotEmpty($validator->errors(['username' => 'foo'])); + } + + /** + * Tests the greaterThan proxy method + * + * @return void + */ + public function testGreaterThan() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'greaterThan', 5, ['>', 5], 'comparison'); + $this->assertNotEmpty($validator->errors(['username' => 2])); + } + + /** + * Tests the greaterThanOrEqual proxy method + * + * @return void + */ + public function testGreaterThanOrEqual() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'greaterThanOrEqual', 5, ['>=', 5], 'comparison'); + $this->assertNotEmpty($validator->errors(['username' => 2])); + } + + /** + * Tests the lessThan proxy method + * + * @return void + */ + public function testLessThan() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'lessThan', 5, ['<', 5], 'comparison'); + $this->assertNotEmpty($validator->errors(['username' => 5])); + } + + /** + * Tests the lessThanOrEqual proxy method + * + * @return void + */ + public function testLessThanOrEqual() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'lessThanOrEqual', 5, ['<=', 5], 'comparison'); + $this->assertNotEmpty($validator->errors(['username' => 6])); + } + + /** + * Tests the equals proxy method + * + * @return void + */ + public function testEquals() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'equals', 5, ['=', 5], 'comparison'); + $this->assertNotEmpty($validator->errors(['username' => 6])); + } + + /** + * Tests the notEquals proxy method + * + * @return void + */ + public function testNotEquals() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'notEquals', 5, ['!=', 5], 'comparison'); + $this->assertNotEmpty($validator->errors(['username' => 5])); + } + + /** + * Tests the sameAs proxy method + * + * @return void + */ + public function testSameAs() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'sameAs', 'other', ['other'], 'compareWith'); + $this->assertNotEmpty($validator->errors(['username' => 'foo'])); + } + + /** + * Tests the containsNonAlphaNumeric proxy method + * + * @return void + */ + public function testContainsNonAlphaNumeric() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'containsNonAlphaNumeric', 2, [2]); + $this->assertNotEmpty($validator->errors(['username' => '$'])); + } + + /** + * Tests the date proxy method + * + * @return void + */ + public function testDate() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'date', ['ymd'], [['ymd']]); + $this->assertNotEmpty($validator->errors(['username' => 'not a date'])); + } + + + /** + * Tests the dateTime proxy method + * + * @return void + */ + public function testDateTime() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'dateTime', ['ymd'], [['ymd']], 'datetime'); + $this->assertNotEmpty($validator->errors(['username' => 'not a date'])); + } + + /** + * Tests the time proxy method + * + * @return void + */ + public function testTime() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'time'); + $this->assertNotEmpty($validator->errors(['username' => 'not a time'])); + } + + /** + * Tests the boolean proxy method + * + * @return void + */ + public function testBoolean() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'boolean'); + $this->assertNotEmpty($validator->errors(['username' => 'not a boolean'])); + } + + /** + * Tests the decimal proxy method + * + * @return void + */ + public function testDecimal() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'decimal', 2, [2]); + $this->assertNotEmpty($validator->errors(['username' => 10.1])); + } + + /** + * Tests the ip proxy methods + * + * @return void + */ + public function testIps() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'ip'); + $this->assertNotEmpty($validator->errors(['username' => 'not ip'])); + + + $this->assertProxyMethod($validator, 'ipv4', null, ['ipv4'], 'ip'); + $this->assertNotEmpty($validator->errors(['username' => 'not ip'])); + + $this->assertProxyMethod($validator, 'ipv6', null, ['ipv6'], 'ip'); + $this->assertNotEmpty($validator->errors(['username' => 'not ip'])); + } + + /** + * Tests the maxLength proxy method + * + * @return void + */ + public function testMaxLength() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'maxLength', 2, [2]); + $this->assertNotEmpty($validator->errors(['username' => 'aaa'])); + } + + /** + * Tests the naturalNumber proxy method + * + * @return void + */ + public function testNaturalNumber() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'naturalNumber', null, [false]); + $this->assertNotEmpty($validator->errors(['username' => 0])); + } + + /** + * Tests the nonNegativeInteger proxy method + * + * @return void + */ + public function testNonNegativeInteger() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'nonNegativeInteger', null, [true], 'naturalNumber'); + $this->assertNotEmpty($validator->errors(['username' => -1])); + } + + /** + * Tests the range proxy method + * + * @return void + */ + public function testRange() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'range', [1, 4], [1, 4]); + $this->assertNotEmpty($validator->errors(['username' => 5])); + } + + /** + * Tests the url proxy method + * + * @return void + */ + public function testUrl() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'url', null, [false]); + $this->assertNotEmpty($validator->errors(['username' => 'not url'])); + } + + /** + * Tests the urlWithProtocol proxy method + * + * @return void + */ + public function testUrlWithProtocol() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'urlWithProtocol', null, [true], 'url'); + $this->assertNotEmpty($validator->errors(['username' => 'google.com'])); + } + + /** + * Tests the inList proxy method + * + * @return void + */ + public function testInList() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'inList', ['a', 'b'], [['a', 'b']]); + $this->assertNotEmpty($validator->errors(['username' => 'c'])); + } + + /** + * Tests the uuid proxy method + * + * @return void + */ + public function testUuid() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'uuid'); + $this->assertNotEmpty($validator->errors(['username' => 'not uuid'])); + } + + /** + * Tests the uploadedFile proxy method + * + * @return void + */ + public function testUploadedFile() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'uploadedFile', ['foo' => 'bar'], [['foo' => 'bar']]); + $this->assertNotEmpty($validator->errors(['username' => []])); + } + + /** + * Tests the latlog proxy methods + * + * @return void + */ + public function testLatLong() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'latLong', null, [], 'geoCoordinate'); + $this->assertNotEmpty($validator->errors(['username' => 2000])); + + $this->assertProxyMethod($validator, 'latitude'); + $this->assertNotEmpty($validator->errors(['username' => 2000])); + + $this->assertProxyMethod($validator, 'longitude'); + $this->assertNotEmpty($validator->errors(['username' => 2000])); + } + + /** + * Tests the ascii proxy method + * + * @return void + */ + public function testAscii() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'ascii'); + $this->assertNotEmpty($validator->errors(['username' => 'ü'])); + } + + /** + * Tests the utf8 proxy methods + * + * @return void + */ + public function testUtf8() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'utf8', null, [['extended' => false]]); + $this->assertEmpty($validator->errors(['username' => 'ü'])); + + $this->assertProxyMethod($validator, 'utf8Strict', null, [['extended' => true]], 'utf8'); + $this->assertEmpty($validator->errors(['username' => 'ü'])); + } + + /** + * Tests the integer proxy method + * + * @return void + */ + public function testInteger() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'integer', null, [], 'isInteger'); + $this->assertNotEmpty($validator->errors(['username' => 'not integer'])); + } + + protected function assertProxyMethod($validator, $method, $extra = null, $pass = [], $name = null) + { + $name = $name ?: $method; + if ($extra !== null) { + $this->assertSame($validator, $validator->{$method}('username', $extra)); + } else { + $this->assertSame($validator, $validator->{$method}('username')); + } + + $rule = $validator->field('username')->rule($method); + $this->assertNull($rule->get('message')); + $this->assertNull($rule->get('on')); + $this->assertEquals($name, $rule->get('rule')); + $this->assertEquals($pass, $rule->get('pass')); + $this->assertEquals('default', $rule->get('provider')); + + if ($extra !== null) { + $validator->{$method}('username', $extra, 'the message', 'create'); + } else { + $validator->{$method}('username', 'the message', 'create'); + } + + $rule = $validator->field('username')->rule($method); + $this->assertEquals('the message', $rule->get('message')); + $this->assertEquals('create', $rule->get('on')); + } } From c46244d5cea0d069fce4a31746f19887861e110a Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 1 Jan 2016 17:45:57 -0500 Subject: [PATCH 095/128] Revert defaulting the httpOnly flag to true. The reason this was switched was due to concern around XSS attacks allowing access to the CSRF token through `document.cookie`. However, if there is a successful XSS attack, the attacker doesn't need the CSRF cookie, as they can use Javascript to submit forms. In this situation the browser will forward the cookies automatically. Another situation where httponly could be useful is 3rd party scripts. But those scripts could also fetch the token from a hidden input and submit forms without access to the cookie value. --- src/Controller/Component/CsrfComponent.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/Component/CsrfComponent.php b/src/Controller/Component/CsrfComponent.php index e23154aa6ee..0235cf732e9 100644 --- a/src/Controller/Component/CsrfComponent.php +++ b/src/Controller/Component/CsrfComponent.php @@ -46,7 +46,7 @@ class CsrfComponent extends Component * - cookieName = The name of the cookie to send. * - expiry = How long the CSRF token should last. Defaults to browser session. * - secure = Whether or not the cookie will be set with the Secure flag. Defaults to false. - * - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to true. + * - httpOnly = Whether or not the cookie will be set with the HttpOnly flag. Defaults to false. * - field = The form field to check. Changing this will also require configuring * FormHelper. * @@ -56,7 +56,7 @@ class CsrfComponent extends Component 'cookieName' => 'csrfToken', 'expiry' => 0, 'secure' => false, - 'httpOnly' => true, + 'httpOnly' => false, 'field' => '_csrfToken', ]; From 8e9048bda228549b383cc7583781b08e5325dc11 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sat, 2 Jan 2016 13:20:45 +0100 Subject: [PATCH 096/128] Added `@method` annotations for the collection methods in Query --- src/ORM/Query.php | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/ORM/Query.php b/src/ORM/Query.php index b482801f64b..2d8ce08c371 100644 --- a/src/ORM/Query.php +++ b/src/ORM/Query.php @@ -15,15 +15,17 @@ namespace Cake\ORM; use ArrayObject; +use Cake\Collection\CollectionInterface; use Cake\Database\ExpressionInterface; use Cake\Database\Query as DatabaseQuery; -use Cake\Database\TypedResultInterface; use Cake\Database\TypeMap; +use Cake\Database\TypedResultInterface; use Cake\Database\ValueBinder; use Cake\Datasource\QueryInterface; use Cake\Datasource\QueryTrait; use JsonSerializable; use RuntimeException; +use Traversable; /** * Extends the base Query class to provide new methods related to association @@ -31,6 +33,40 @@ * into a specific iterator that will be responsible for hydrating results if * required. * + * @see CollectionInterface For a full description of the collection methods supported by this class + * @method CollectionInterface each(callable $c) Passes each of the query results to the callable + * @method CollectionInterface filter(callable $c = null) Keeps the results using passing the callable test + * @method CollectionInterface reject(callable $c) Removes the results passing the callable test + * @method bool every(callable $c) Returns true if all the results pass the callable test + * @method bool some(callable $c) Returns true if at least one of the results pass the callable test + * @method CollectionInterface map(callable $c) Modifies each of the results using the callable + * @method mixed reduce(callable $c, $zero = null) Folds all the results into a single value using the callable. + * @method CollectionInterface extract($field) Extracts a single column from each row + * @method mixed max($field, $type = SORT_NUMERIC) Returns the maximum value for a single column in all the results. + * @method mixed min($field, $type = SORT_NUMERIC) Returns the minimum value for a single column in all the results. + * @method CollectionInterface groupBy(string|callable $field) In-memory group all results by the value of a column. + * @method CollectionInterface indexBy(string|callable $field) Returns the results indexed by the value of a column. + * @method int countBy(string|callable $field) Returns the number of unique values for a column + * @method float sumOf(string|callable $field) Returns the sum of all values for a single column + * @method CollectionInterface shuffle() In-memory randomize he order the results are returned + * @method CollectionInterface sample($size = 10) In-memory shuffle the results and return a subset of them. + * @method CollectionInterface take($size = 1, $from = 0) In-memory limit and offset for the query results. + * @method CollectionInterface skip(int $howMany) Skips some rows from the start of the query result. + * @method mixed last() Return the last row of the query result + * @method CollectionInterface append(array|Traversable $items) Appends more rows to the result of the query. + * @method CollectionInterface combine($k, $v, $g = null) Returns the values of the column $v index by column $k, + * and grouped by $g. + * @method CollectionInterface nest($k, $p) Creates a tree structure by nesting the values of column $p into that + * with the same value for $k. + * @method array toArray() Returns a key-value array with the results of this query. + * @method array toList() Returns a numerically indexed array with the results of this query. + * @method CollectionInterface stopWhen(callable $c) Returns each row until the callable returns true. + * @method CollectionInterface zip(array|Traversable $c) Returns the first result of both the query and $c in an array, + * then the second results and so on. + * @method CollectionInterface zipWith(...$collections, callable $c) Returns each of the results out of calling $c + * with the first rows of the query and each of the items, then the second rows and so on. + * @method CollectionInterface chunk($size) Groups the results in arrays of $size rows each. + * @method bool isEmpty($size) Returns true if this query found no results. */ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface { From 52c7d5829d7d308afca46081d6430974310e9158 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sat, 2 Jan 2016 14:42:24 +0100 Subject: [PATCH 097/128] Fomratting, fixing typos and adding another annotation --- src/ORM/Query.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ORM/Query.php b/src/ORM/Query.php index 2d8ce08c371..6b56b85080d 100644 --- a/src/ORM/Query.php +++ b/src/ORM/Query.php @@ -18,8 +18,8 @@ use Cake\Collection\CollectionInterface; use Cake\Database\ExpressionInterface; use Cake\Database\Query as DatabaseQuery; -use Cake\Database\TypeMap; use Cake\Database\TypedResultInterface; +use Cake\Database\TypeMap; use Cake\Database\ValueBinder; use Cake\Datasource\QueryInterface; use Cake\Datasource\QueryTrait; @@ -48,25 +48,26 @@ * @method CollectionInterface indexBy(string|callable $field) Returns the results indexed by the value of a column. * @method int countBy(string|callable $field) Returns the number of unique values for a column * @method float sumOf(string|callable $field) Returns the sum of all values for a single column - * @method CollectionInterface shuffle() In-memory randomize he order the results are returned + * @method CollectionInterface shuffle() In-memory randomize the order the results are returned * @method CollectionInterface sample($size = 10) In-memory shuffle the results and return a subset of them. * @method CollectionInterface take($size = 1, $from = 0) In-memory limit and offset for the query results. * @method CollectionInterface skip(int $howMany) Skips some rows from the start of the query result. * @method mixed last() Return the last row of the query result * @method CollectionInterface append(array|Traversable $items) Appends more rows to the result of the query. * @method CollectionInterface combine($k, $v, $g = null) Returns the values of the column $v index by column $k, - * and grouped by $g. + * and grouped by $g. * @method CollectionInterface nest($k, $p) Creates a tree structure by nesting the values of column $p into that - * with the same value for $k. + * with the same value for $k. * @method array toArray() Returns a key-value array with the results of this query. * @method array toList() Returns a numerically indexed array with the results of this query. * @method CollectionInterface stopWhen(callable $c) Returns each row until the callable returns true. * @method CollectionInterface zip(array|Traversable $c) Returns the first result of both the query and $c in an array, - * then the second results and so on. + * then the second results and so on. * @method CollectionInterface zipWith(...$collections, callable $c) Returns each of the results out of calling $c - * with the first rows of the query and each of the items, then the second rows and so on. + * with the first rows of the query and each of the items, then the second rows and so on. * @method CollectionInterface chunk($size) Groups the results in arrays of $size rows each. * @method bool isEmpty($size) Returns true if this query found no results. + * @method $this find(string $type, array $options) Compose this query with another finder from the same table. */ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface { From cbb1f4c8a8eebb6eb45477712ff5f01351218246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Lorenzo=20Rodr=C3=ADguez?= Date: Sat, 2 Jan 2016 20:27:30 +0100 Subject: [PATCH 098/128] Removing unneeded annotation --- src/ORM/Query.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ORM/Query.php b/src/ORM/Query.php index 6b56b85080d..de22ba4b80a 100644 --- a/src/ORM/Query.php +++ b/src/ORM/Query.php @@ -67,7 +67,6 @@ * with the first rows of the query and each of the items, then the second rows and so on. * @method CollectionInterface chunk($size) Groups the results in arrays of $size rows each. * @method bool isEmpty($size) Returns true if this query found no results. - * @method $this find(string $type, array $options) Compose this query with another finder from the same table. */ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface { From ca89846ff7060252f5f5bb4af8226a7610235aea Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 2 Jan 2016 17:06:51 -0500 Subject: [PATCH 099/128] Add doc blocks for Validator methods. Rename utf8Strict to utf8Extended as that better represents what the method does. --- src/Validation/Validator.php | 392 +++++++++++++++++++- tests/TestCase/Validation/ValidatorTest.php | 2 +- 2 files changed, 390 insertions(+), 4 deletions(-) diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index fb3c6573c22..1401e47922f 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -512,7 +512,7 @@ public function allowEmpty($field, $when = true) * method called will take precedence. * * @param string $field the name of the field - * @param string $message The validation message to show if the field is not + * @param string $message The message to show if the field is not * @param bool|string|callable $when Indicates when the field is not allowed * to be empty. Valid values are true (always), 'create', 'update'. If a * callable is passed then the field will allowed be empty only when @@ -536,6 +536,15 @@ public function notEmpty($field, $message = null, $when = false) return $this; } + /** + * Add a notBlank rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::notBlank() + */ public function notBlank($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -544,6 +553,15 @@ public function notBlank($field, $message = null, $when = null) ]); } + /** + * Add an alphanumeric rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::alphaNumeric() + */ public function alphaNumeric($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -552,6 +570,16 @@ public function alphaNumeric($field, $message = null, $when = null) ]); } + /** + * Add an rule that ensures a string length is within a range. + * + * @param string $field The field you want to apply the rule to. + * @param array $range The inclusive minimum and maximum length you want permitted. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::alphaNumeric() + */ public function lengthBetween($field, array $range, $message = null, $when = null) { if (count($range) !== 2) { @@ -563,6 +591,17 @@ public function lengthBetween($field, array $range, $message = null, $when = nul ]); } + /** + * Add a credit card rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $type The type of cards you want to allow. Defaults to 'all'. + * You can also supply an array of accepted card types. e.g `['mastercard', 'visa', 'amex']` + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::cc() + */ public function creditCard($field, $type = 'all', $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -571,6 +610,16 @@ public function creditCard($field, $type = 'all', $message = null, $when = null) ]); } + /** + * Add a greater than comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be greater than. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::comparison() + */ public function greaterThan($field, $value, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -579,6 +628,16 @@ public function greaterThan($field, $value, $message = null, $when = null) ]); } + /** + * Add a greater than or equal to comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be greater than or equal to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::comparison() + */ public function greaterThanOrEqual($field, $value, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -587,6 +646,16 @@ public function greaterThanOrEqual($field, $value, $message = null, $when = null ]); } + /** + * Add a less than comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be less than. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::comparison() + */ public function lessThan($field, $value, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -595,6 +664,16 @@ public function lessThan($field, $value, $message = null, $when = null) ]); } + /** + * Add a less than or equal comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be less than or equal to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::comparison() + */ public function lessThanOrEqual($field, $value, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -603,6 +682,16 @@ public function lessThanOrEqual($field, $value, $message = null, $when = null) ]); } + /** + * Add a equal to comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be equal to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::comparison() + */ public function equals($field, $value, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -611,6 +700,16 @@ public function equals($field, $value, $message = null, $when = null) ]); } + /** + * Add a not equal to comparison rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int|float $value The value user data must be not be equal to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::comparison() + */ public function notEquals($field, $value, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -619,6 +718,18 @@ public function notEquals($field, $value, $message = null, $when = null) ]); } + /** + * Add a rule to compare two fields to each other. + * + * If both fields have the exact same value the rule will pass. + * + * @param mixed $field The field you want to apply the rule to. + * @param mixed $secondField The field you want to compare against. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::compareWith() + */ public function sameAs($field, $secondField, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -627,6 +738,16 @@ public function sameAs($field, $secondField, $message = null, $when = null) ]); } + /** + * Add a rule to check if a field contains non alpha numeric characters. + * + * @param string $field The field you want to apply the rule to. + * @param int $limit The minimum number of non-alphanumeric fields required. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::containsNonAlphaNumeric() + */ public function containsNonAlphaNumeric($field, $limit = 1, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -635,6 +756,16 @@ public function containsNonAlphaNumeric($field, $limit = 1, $message = null, $wh ]); } + /** + * Add a date format validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param array $format A list of accepted date formats. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::date() + */ public function date($field, $formats = ['ymd'], $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -643,6 +774,16 @@ public function date($field, $formats = ['ymd'], $message = null, $when = null) ]); } + /** + * Add a date time format validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param array $format A list of accepted date formats. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::datetime() + */ public function dateTime($field, $formats = ['ymd'], $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -651,6 +792,16 @@ public function dateTime($field, $formats = ['ymd'], $message = null, $when = nu ]); } + /** + * Add a time format validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param array $format A list of accepted date formats. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::time() + */ public function time($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -659,6 +810,15 @@ public function time($field, $message = null, $when = null) ]); } + /** + * Add a boolean validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::boolean() + */ public function boolean($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -667,6 +827,16 @@ public function boolean($field, $message = null, $when = null) ]); } + /** + * Add a decimal validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int $places The number of decimal places to require. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::decimal() + */ public function decimal($field, $places = null, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -675,6 +845,16 @@ public function decimal($field, $places = null, $message = null, $when = null) ]); } + /** + * Add an email validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param bool $checkMX Whether or not to check the MX records. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::email() + */ public function email($field, $checkMX = false, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -683,6 +863,17 @@ public function email($field, $checkMX = false, $message = null, $when = null) ]); } + /** + * Add an IP validation rule to a field. + * + * This rule will accept both IPv4 and IPv6 addresses. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::ip() + */ public function ip($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -691,6 +882,15 @@ public function ip($field, $message = null, $when = null) ]); } + /** + * Add an IPv4 validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::ip() + */ public function ipv4($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -699,6 +899,15 @@ public function ipv4($field, $message = null, $when = null) ]); } + /** + * Add an IPv6 validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::ip() + */ public function ipv6($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -707,6 +916,16 @@ public function ipv6($field, $message = null, $when = null) ]); } + /** + * Add a string length validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int $min The minimum length required. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::minLength() + */ public function minLength($field, $min, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -715,6 +934,16 @@ public function minLength($field, $min, $message = null, $when = null) ]); } + /** + * Add a string length validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param int $max The maximum length allowed. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::maxLength() + */ public function maxLength($field, $max, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -723,6 +952,15 @@ public function maxLength($field, $max, $message = null, $when = null) ]); } + /** + * Add a numeric value validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::numeric() + */ public function numeric($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -731,6 +969,15 @@ public function numeric($field, $message = null, $when = null) ]); } + /** + * Add a natural number validation rule to a field. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::naturalNumber() + */ public function naturalNumber($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -739,6 +986,15 @@ public function naturalNumber($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure a field is a non negative integer. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::naturalNumber() + */ public function nonNegativeInteger($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -747,6 +1003,16 @@ public function nonNegativeInteger($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure a field is within a numeric range + * + * @param string $field The field you want to apply the rule to. + * @param array $range The inclusive upper and lower bounds of the valid range. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::range() + */ public function range($field, array $range, $message = null, $when = null) { if (count($range) !== 2) { @@ -758,6 +1024,17 @@ public function range($field, array $range, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure a field is a URL. + * + * This validator does not require a protocol. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::url() + */ public function url($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -766,6 +1043,17 @@ public function url($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure a field is a URL. + * + * This validator requires the URL to have a protocol. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::url() + */ public function urlWithProtocol($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -774,6 +1062,16 @@ public function urlWithProtocol($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure the field value is within a whitelist. + * + * @param string $field The field you want to apply the rule to. + * @param array $list The list of valid options. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::inList() + */ public function inList($field, array $list, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -782,6 +1080,15 @@ public function inList($field, array $list, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure the field is a UUID + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::uuid() + */ public function uuid($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -790,6 +1097,18 @@ public function uuid($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure the field is an uploaded file + * + * For options see Cake\Validation\Validation::uploadedFile() + * + * @param string $field The field you want to apply the rule to. + * @param array $options An array of options. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::uploadedFile() + */ public function uploadedFile($field, array $options, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -798,6 +1117,17 @@ public function uploadedFile($field, array $options, $message = null, $when = nu ]); } + /** + * Add a validation rule to ensure the field is a lat/long tuple. + * + * e.g. `, ` + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::uuid() + */ public function latLong($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -806,6 +1136,15 @@ public function latLong($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure the field is a latitude. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::latitude() + */ public function latitude($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -814,6 +1153,15 @@ public function latitude($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure the field is a longitude. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::longitude() + */ public function longitude($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -822,6 +1170,15 @@ public function longitude($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure a field contains only ascii bytes + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::ascii() + */ public function ascii($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -830,6 +1187,15 @@ public function ascii($field, $message = null, $when = null) ]); } + /** + * Add a validation rule to ensure a field contains only BMP utf8 bytes + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::utf8() + */ public function utf8($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); @@ -838,14 +1204,34 @@ public function utf8($field, $message = null, $when = null) ]); } - public function utf8Strict($field, $message = null, $when = null) + /** + * Add a validation rule to ensure a field contains only utf8 bytes. + * + * This rule will accept 3 and 4 byte UTF8 sequences, which are necessary for emoji. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::utf8() + */ + public function utf8Extended($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); - return $this->add($field, 'utf8Strict', $extra + [ + return $this->add($field, 'utf8Extended', $extra + [ 'rule' => ['utf8', ['extended' => true]] ]); } + /** + * Add a validation rule to ensure a field is an integer value. + * + * @param string $field The field you want to apply the rule to. + * @param string $message The error message when the rule fails. + * @param string|callable $when Either 'create' or 'update' or a callable that returns + * true when the valdiation rule should be applied. + * @see Cake\Validation\Validation::isInteger() + */ public function integer($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); diff --git a/tests/TestCase/Validation/ValidatorTest.php b/tests/TestCase/Validation/ValidatorTest.php index e68b4b89bf0..bdab464119b 100644 --- a/tests/TestCase/Validation/ValidatorTest.php +++ b/tests/TestCase/Validation/ValidatorTest.php @@ -1421,7 +1421,7 @@ public function testUtf8() $this->assertProxyMethod($validator, 'utf8', null, [['extended' => false]]); $this->assertEmpty($validator->errors(['username' => 'ü'])); - $this->assertProxyMethod($validator, 'utf8Strict', null, [['extended' => true]], 'utf8'); + $this->assertProxyMethod($validator, 'utf8Extended', null, [['extended' => true]], 'utf8'); $this->assertEmpty($validator->errors(['username' => 'ü'])); } From bd798d9fd71fc84303fdd1cc493044cfdfaa7498 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 2 Jan 2016 17:20:48 -0500 Subject: [PATCH 100/128] Add pass/fail test for utf8extended. --- tests/TestCase/Validation/ValidatorTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/TestCase/Validation/ValidatorTest.php b/tests/TestCase/Validation/ValidatorTest.php index bdab464119b..fa2f5bd472b 100644 --- a/tests/TestCase/Validation/ValidatorTest.php +++ b/tests/TestCase/Validation/ValidatorTest.php @@ -1417,12 +1417,29 @@ public function testAscii() */ public function testUtf8() { + // Grinning face + $extended = 'some' . "\xf0\x9f\x98\x80" . 'value'; $validator = new Validator(); + $this->assertProxyMethod($validator, 'utf8', null, [['extended' => false]]); $this->assertEmpty($validator->errors(['username' => 'ü'])); + $this->assertNotEmpty($validator->errors(['username' => $extended])); + } + + /** + * Test utf8extended proxy method. + * + * @return void + */ + public function testUtf8Extended() + { + // Grinning face + $extended = 'some' . "\xf0\x9f\x98\x80" . 'value'; + $validator = new Validator(); $this->assertProxyMethod($validator, 'utf8Extended', null, [['extended' => true]], 'utf8'); $this->assertEmpty($validator->errors(['username' => 'ü'])); + $this->assertEmpty($validator->errors(['username' => $extended])); } /** From d58bb116856319e123d82256b576095e42231a55 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 2 Jan 2016 17:29:46 -0500 Subject: [PATCH 101/128] Increase test coverage. Cover off previously uncovered code paths. --- tests/TestCase/Validation/ValidatorTest.php | 75 +++++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/tests/TestCase/Validation/ValidatorTest.php b/tests/TestCase/Validation/ValidatorTest.php index fa2f5bd472b..3ecdd11ff80 100644 --- a/tests/TestCase/Validation/ValidatorTest.php +++ b/tests/TestCase/Validation/ValidatorTest.php @@ -1084,6 +1084,18 @@ public function testLengthBetween() $this->assertNotEmpty($validator->errors(['username' => 'foo'])); } + /** + * Tests the lengthBetween proxy method + * + * @expectedException InvalidArgumentException + * @return void + */ + public function testLengthBetweenFailure() + { + $validator = new Validator(); + $validator->lengthBetween('username', [7]); + } + /** * Tests the creditCard proxy method * @@ -1272,6 +1284,18 @@ public function testIps() $this->assertNotEmpty($validator->errors(['username' => 'not ip'])); } + /** + * Tests the minLength proxy method + * + * @return void + */ + public function testMinLength() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'minLength', 2, [2]); + $this->assertNotEmpty($validator->errors(['username' => 'a'])); + } + /** * Tests the maxLength proxy method * @@ -1284,6 +1308,19 @@ public function testMaxLength() $this->assertNotEmpty($validator->errors(['username' => 'aaa'])); } + /** + * Tests the numeric proxy method + * + * @return void + */ + public function testNumeric() + { + $validator = new Validator(); + $this->assertProxyMethod($validator, 'numeric'); + $this->assertEmpty($validator->errors(['username' => '22'])); + $this->assertNotEmpty($validator->errors(['username' => 'a'])); + } + /** * Tests the naturalNumber proxy method * @@ -1320,6 +1357,17 @@ public function testRange() $this->assertNotEmpty($validator->errors(['username' => 5])); } + /** + * Tests the range failure case + * + * @expectedException InvalidArgumentException + * @return void + */ + public function testRangeFailure() + { + $validator = new Validator(); + $validator->range('username', [1]); + } /** * Tests the url proxy method * @@ -1442,6 +1490,19 @@ public function testUtf8Extended() $this->assertEmpty($validator->errors(['username' => $extended])); } + /** + * Tests the email proxy method + * + * @return void + */ + public function testEmail() + { + $validator = new Validator(); + $validator->email('username'); + $this->assertEmpty($validator->errors(['username' => 'test@example.com'])); + $this->assertNotEmpty($validator->errors(['username' => 'not an email'])); + } + /** * Tests the integer proxy method * @@ -1464,11 +1525,11 @@ protected function assertProxyMethod($validator, $method, $extra = null, $pass = } $rule = $validator->field('username')->rule($method); - $this->assertNull($rule->get('message')); - $this->assertNull($rule->get('on')); - $this->assertEquals($name, $rule->get('rule')); - $this->assertEquals($pass, $rule->get('pass')); - $this->assertEquals('default', $rule->get('provider')); + $this->assertNull($rule->get('message'), 'Message is present when it should not be'); + $this->assertNull($rule->get('on'), 'On clause is present when it should not be'); + $this->assertEquals($name, $rule->get('rule'), 'Rule name does not match'); + $this->assertEquals($pass, $rule->get('pass'), 'Passed options are different'); + $this->assertEquals('default', $rule->get('provider'), 'Provider does not match'); if ($extra !== null) { $validator->{$method}('username', $extra, 'the message', 'create'); @@ -1477,7 +1538,7 @@ protected function assertProxyMethod($validator, $method, $extra = null, $pass = } $rule = $validator->field('username')->rule($method); - $this->assertEquals('the message', $rule->get('message')); - $this->assertEquals('create', $rule->get('on')); + $this->assertEquals('the message', $rule->get('message'), 'Error messages are not the same'); + $this->assertEquals('create', $rule->get('on'), 'On clause is wrong'); } } From 41e04c0c0e36e3dffb54d50a2ec0bd5e74924d83 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 3 Jan 2016 00:08:00 +0100 Subject: [PATCH 102/128] Adding missing return annotations --- src/Validation/Validator.php | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 1401e47922f..47b9716aefd 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -544,6 +544,7 @@ public function notEmpty($field, $message = null, $when = false) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::notBlank() + * @return $this */ public function notBlank($field, $message = null, $when = null) { @@ -561,6 +562,7 @@ public function notBlank($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::alphaNumeric() + * @return $this */ public function alphaNumeric($field, $message = null, $when = null) { @@ -579,6 +581,7 @@ public function alphaNumeric($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::alphaNumeric() + * @return $this */ public function lengthBetween($field, array $range, $message = null, $when = null) { @@ -601,6 +604,7 @@ public function lengthBetween($field, array $range, $message = null, $when = nul * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::cc() + * @return $this */ public function creditCard($field, $type = 'all', $message = null, $when = null) { @@ -619,6 +623,7 @@ public function creditCard($field, $type = 'all', $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::comparison() + * @return $this */ public function greaterThan($field, $value, $message = null, $when = null) { @@ -637,6 +642,7 @@ public function greaterThan($field, $value, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::comparison() + * @return $this */ public function greaterThanOrEqual($field, $value, $message = null, $when = null) { @@ -655,6 +661,7 @@ public function greaterThanOrEqual($field, $value, $message = null, $when = null * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::comparison() + * @return $this */ public function lessThan($field, $value, $message = null, $when = null) { @@ -673,6 +680,7 @@ public function lessThan($field, $value, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::comparison() + * @return $this */ public function lessThanOrEqual($field, $value, $message = null, $when = null) { @@ -691,6 +699,7 @@ public function lessThanOrEqual($field, $value, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::comparison() + * @return $this */ public function equals($field, $value, $message = null, $when = null) { @@ -709,6 +718,7 @@ public function equals($field, $value, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::comparison() + * @return $this */ public function notEquals($field, $value, $message = null, $when = null) { @@ -729,6 +739,7 @@ public function notEquals($field, $value, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::compareWith() + * @return $this */ public function sameAs($field, $secondField, $message = null, $when = null) { @@ -747,6 +758,7 @@ public function sameAs($field, $secondField, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::containsNonAlphaNumeric() + * @return $this */ public function containsNonAlphaNumeric($field, $limit = 1, $message = null, $when = null) { @@ -765,6 +777,7 @@ public function containsNonAlphaNumeric($field, $limit = 1, $message = null, $wh * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::date() + * @return $this */ public function date($field, $formats = ['ymd'], $message = null, $when = null) { @@ -783,6 +796,7 @@ public function date($field, $formats = ['ymd'], $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::datetime() + * @return $this */ public function dateTime($field, $formats = ['ymd'], $message = null, $when = null) { @@ -801,6 +815,7 @@ public function dateTime($field, $formats = ['ymd'], $message = null, $when = nu * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::time() + * @return $this */ public function time($field, $message = null, $when = null) { @@ -818,6 +833,7 @@ public function time($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::boolean() + * @return $this */ public function boolean($field, $message = null, $when = null) { @@ -836,6 +852,7 @@ public function boolean($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::decimal() + * @return $this */ public function decimal($field, $places = null, $message = null, $when = null) { @@ -854,6 +871,7 @@ public function decimal($field, $places = null, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::email() + * @return $this */ public function email($field, $checkMX = false, $message = null, $when = null) { @@ -873,6 +891,7 @@ public function email($field, $checkMX = false, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::ip() + * @return $this */ public function ip($field, $message = null, $when = null) { @@ -890,6 +909,7 @@ public function ip($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::ip() + * @return $this */ public function ipv4($field, $message = null, $when = null) { @@ -907,6 +927,7 @@ public function ipv4($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::ip() + * @return $this */ public function ipv6($field, $message = null, $when = null) { @@ -925,6 +946,7 @@ public function ipv6($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::minLength() + * @return $this */ public function minLength($field, $min, $message = null, $when = null) { @@ -943,6 +965,7 @@ public function minLength($field, $min, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::maxLength() + * @return $this */ public function maxLength($field, $max, $message = null, $when = null) { @@ -960,6 +983,7 @@ public function maxLength($field, $max, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::numeric() + * @return $this */ public function numeric($field, $message = null, $when = null) { @@ -977,6 +1001,7 @@ public function numeric($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::naturalNumber() + * @return $this */ public function naturalNumber($field, $message = null, $when = null) { @@ -994,6 +1019,7 @@ public function naturalNumber($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::naturalNumber() + * @return $this */ public function nonNegativeInteger($field, $message = null, $when = null) { @@ -1012,6 +1038,7 @@ public function nonNegativeInteger($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::range() + * @return $this */ public function range($field, array $range, $message = null, $when = null) { @@ -1034,6 +1061,7 @@ public function range($field, array $range, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::url() + * @return $this */ public function url($field, $message = null, $when = null) { @@ -1053,6 +1081,7 @@ public function url($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::url() + * @return $this */ public function urlWithProtocol($field, $message = null, $when = null) { @@ -1071,6 +1100,7 @@ public function urlWithProtocol($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::inList() + * @return $this */ public function inList($field, array $list, $message = null, $when = null) { @@ -1088,6 +1118,7 @@ public function inList($field, array $list, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::uuid() + * @return $this */ public function uuid($field, $message = null, $when = null) { @@ -1108,6 +1139,7 @@ public function uuid($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::uploadedFile() + * @return $this */ public function uploadedFile($field, array $options, $message = null, $when = null) { @@ -1127,6 +1159,7 @@ public function uploadedFile($field, array $options, $message = null, $when = nu * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::uuid() + * @return $this */ public function latLong($field, $message = null, $when = null) { @@ -1144,6 +1177,7 @@ public function latLong($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::latitude() + * @return $this */ public function latitude($field, $message = null, $when = null) { @@ -1161,6 +1195,7 @@ public function latitude($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::longitude() + * @return $this */ public function longitude($field, $message = null, $when = null) { @@ -1178,6 +1213,7 @@ public function longitude($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::ascii() + * @return $this */ public function ascii($field, $message = null, $when = null) { @@ -1195,6 +1231,7 @@ public function ascii($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::utf8() + * @return $this */ public function utf8($field, $message = null, $when = null) { @@ -1214,6 +1251,7 @@ public function utf8($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::utf8() + * @return $this */ public function utf8Extended($field, $message = null, $when = null) { @@ -1231,6 +1269,7 @@ public function utf8Extended($field, $message = null, $when = null) * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. * @see Cake\Validation\Validation::isInteger() + * @return $this */ public function integer($field, $message = null, $when = null) { From 034db629275a3629a907a26949a25c17b1b37663 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 3 Jan 2016 00:33:36 +0100 Subject: [PATCH 103/128] Fixing some CS errors --- src/Validation/Validator.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 47b9716aefd..d421600209e 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -772,7 +772,7 @@ public function containsNonAlphaNumeric($field, $limit = 1, $message = null, $wh * Add a date format validation rule to a field. * * @param string $field The field you want to apply the rule to. - * @param array $format A list of accepted date formats. + * @param array $formats A list of accepted date formats. * @param string $message The error message when the rule fails. * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. @@ -791,7 +791,7 @@ public function date($field, $formats = ['ymd'], $message = null, $when = null) * Add a date time format validation rule to a field. * * @param string $field The field you want to apply the rule to. - * @param array $format A list of accepted date formats. + * @param array $formats A list of accepted date formats. * @param string $message The error message when the rule fails. * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. @@ -810,7 +810,6 @@ public function dateTime($field, $formats = ['ymd'], $message = null, $when = nu * Add a time format validation rule to a field. * * @param string $field The field you want to apply the rule to. - * @param array $format A list of accepted date formats. * @param string $message The error message when the rule fails. * @param string|callable $when Either 'create' or 'update' or a callable that returns * true when the valdiation rule should be applied. @@ -1275,7 +1274,7 @@ public function integer($field, $message = null, $when = null) { $extra = array_filter(['on' => $when, 'message' => $message]); return $this->add($field, 'integer', $extra + [ - 'rule' =>'isInteger' + 'rule' => 'isInteger' ]); } From e8a577cc85653fb134cf507e946bcd986e74d95a Mon Sep 17 00:00:00 2001 From: antograssiot Date: Sun, 3 Jan 2016 13:46:53 +0100 Subject: [PATCH 104/128] specify the correct version for 3.2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 109fc5bd723..8a9aa23db99 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ recommend using the [app skeleton](https://github.com/cakephp/app) as a starting point. For existing applications you can run the following: ``` bash -$ composer require cakephp/cakephp:"~3.1" +$ composer require cakephp/cakephp:"~3.2" ``` ## Running Tests From 95cd46fc53dbc220cd956ac2a2da54b3e2760037 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 3 Jan 2016 18:03:03 +0100 Subject: [PATCH 105/128] Trying to fix test in travis --- tests/TestCase/Database/QueryTest.php | 31 ++++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/TestCase/Database/QueryTest.php b/tests/TestCase/Database/QueryTest.php index 42902c2395c..1f0171d70a5 100644 --- a/tests/TestCase/Database/QueryTest.php +++ b/tests/TestCase/Database/QueryTest.php @@ -133,28 +133,29 @@ public function testSelectFieldsFromTable() public function testSelectAliasedFieldsFromTable() { $query = new Query($this->connection); - $result = $query->select(['text' => 'body', 'author_id'])->from('articles')->execute(); - $this->assertEquals(['text' => 'First Article Body', 'author_id' => 1], $result->fetch('assoc')); - $this->assertEquals(['text' => 'Second Article Body', 'author_id' => 3], $result->fetch('assoc')); + $result = $query->select(['text' => 'comment', 'article_id'])->from('comments')->execute(); + $this->assertEquals(['text' => 'First Comment for First Article', 'article_id' => 1], $result->fetch('assoc')); + $this->assertEquals(['text' => 'Second Comment for First Article', 'article_id' => 1], $result->fetch('assoc')); $query = new Query($this->connection); - $result = $query->select(['text' => 'body', 'author' => 'author_id'])->from('articles')->execute(); - $this->assertEquals(['text' => 'First Article Body', 'author' => 1], $result->fetch('assoc')); - $this->assertEquals(['text' => 'Second Article Body', 'author' => 3], $result->fetch('assoc')); + $result = $query->select(['text' => 'comment', 'article' => 'article_id'])->from('comments')->execute(); + $this->assertEquals(['text' => 'First Comment for First Article', 'article' => 1], $result->fetch('assoc')); + $this->assertEquals(['text' => 'Second Comment for First Article', 'article' => 1], $result->fetch('assoc')); $query = new Query($this->connection); - $query->select(['text' => 'body'])->select(['author_id', 'foo' => 'body']); - $result = $query->from('articles')->execute(); - $this->assertEquals(['foo' => 'First Article Body', 'text' => 'First Article Body', 'author_id' => 1], $result->fetch('assoc')); - $this->assertEquals(['foo' => 'Second Article Body', 'text' => 'Second Article Body', 'author_id' => 3], $result->fetch('assoc')); + $query->select(['text' => 'comment'])->select(['article_id', 'foo' => 'comment']); + $result = $query->from('comments')->execute(); + $this->assertEquals( + ['foo' => 'First Comment for First Article', 'text' => 'First Comment for First Article', 'article_id' => 1], + $result->fetch('assoc') + ); $query = new Query($this->connection); $exp = $query->newExpr('1 + 1'); - $comp = $query->newExpr(['author_id +' => 2]); - $result = $query->select(['text' => 'body', 'two' => $exp, 'three' => $comp]) - ->from('articles')->execute(); - $this->assertEquals(['text' => 'First Article Body', 'two' => 2, 'three' => 3], $result->fetch('assoc')); - $this->assertEquals(['text' => 'Second Article Body', 'two' => 2, 'three' => 5], $result->fetch('assoc')); + $comp = $query->newExpr(['article_id +' => 2]); + $result = $query->select(['text' => 'comment', 'two' => $exp, 'three' => $comp]) + ->from('comments')->execute(); + $this->assertEquals(['text' => 'First Comment for First Article', 'two' => 2, 'three' => 3], $result->fetch('assoc')); } /** From 80e5e0b691fbadce88e694f041fd3a049f985d29 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 3 Jan 2016 18:58:15 +0100 Subject: [PATCH 106/128] Fixed CS error --- tests/TestCase/Database/QueryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Database/QueryTest.php b/tests/TestCase/Database/QueryTest.php index 1f0171d70a5..6a1c0fb811a 100644 --- a/tests/TestCase/Database/QueryTest.php +++ b/tests/TestCase/Database/QueryTest.php @@ -138,7 +138,7 @@ public function testSelectAliasedFieldsFromTable() $this->assertEquals(['text' => 'Second Comment for First Article', 'article_id' => 1], $result->fetch('assoc')); $query = new Query($this->connection); - $result = $query->select(['text' => 'comment', 'article' => 'article_id'])->from('comments')->execute(); + $result = $query->select(['text' => 'comment', 'article' => 'article_id'])->from('comments')->execute(); $this->assertEquals(['text' => 'First Comment for First Article', 'article' => 1], $result->fetch('assoc')); $this->assertEquals(['text' => 'Second Comment for First Article', 'article' => 1], $result->fetch('assoc')); From a423b61db862f40c3f32ba5fe28c0b02456ae000 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sun, 3 Jan 2016 19:13:01 +0100 Subject: [PATCH 107/128] Update version number to 3.2.0-RC1 --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index fe1576234e6..0c3e03c54c9 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -16,4 +16,4 @@ // @license http://www.opensource.org/licenses/mit-license.php MIT License // +--------------------------------------------------------------------------------------------+ // //////////////////////////////////////////////////////////////////////////////////////////////////// -3.2.0-dev +3.2.0-RC1 From b1b8dac9c8d3758bc8071dc99f717c3faac0d057 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 26 Dec 2015 16:50:53 +0100 Subject: [PATCH 108/128] Allow invalid field data to be kept along with the error messages in the Entity. --- src/Datasource/EntityTrait.php | 49 ++++++++++++++++++- src/Datasource/RulesChecker.php | 8 +++ src/ORM/Marshaller.php | 6 +++ tests/TestCase/ORM/MarshallerTest.php | 22 +++++++++ .../ORM/RulesCheckerIntegrationTest.php | 1 + 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/Datasource/EntityTrait.php b/src/Datasource/EntityTrait.php index de42200521d..5b6928e1366 100644 --- a/src/Datasource/EntityTrait.php +++ b/src/Datasource/EntityTrait.php @@ -95,6 +95,13 @@ trait EntityTrait */ protected $_errors = []; + /** + * List of invalid fields and their data for errors upon validation/patching + * + * @var array + */ + protected $_invalid = []; + /** * Map of properties in this entity that can be safely assigned, each * property name points to a boolean indicating its status. An empty array @@ -600,7 +607,7 @@ public function dirty($property = null, $isDirty = null) } $this->_dirty[$property] = true; - unset($this->_errors[$property]); + unset($this->_errors[$property], $this->_invalid[$property]); return true; } @@ -615,6 +622,7 @@ public function clean() { $this->_dirty = []; $this->_errors = []; + $this->_invalid = []; } /** @@ -779,6 +787,44 @@ protected function _readError($object, $path = null) return []; } + /** + * Sets a field as invalid and not patchable into the entity. + * + * This is useful for batch operations when one needs to get the original value for an error message after patching. + * This value could not be patched into the entity and is simply copied into the _invalid property for debugging purposes + * or to be able to log it away. + * + * @param string|array|null $field + * @param string|null $value + * @param bool $overwrite + * @return $this|mixed + */ + public function invalid($field = null, $value = null, $overwrite = false) + { + if ($field === null) { + return $this->_invalid; + } + + if (is_string($field) && $value === null) { + $value = isset($this->_invalid[$field]) ? $this->_invalid[$field] : null; + return $value; + } + + if (!is_array($field)) { + $field = [$field => $value]; + } + + foreach ($field as $f => $value) { + if ($overwrite) { + $this->_invalid[$f] = $value; + continue; + } + $this->_invalid += [$f => $value]; + } + + return $this; + } + /** * Stores whether or not a property value can be changed or set in this entity. * The special property `*` can also be marked as accessible or protected, meaning @@ -881,6 +927,7 @@ public function __debugInfo() '[original]' => $this->_original, '[virtual]' => $this->_virtual, '[errors]' => $this->_errors, + '[invalid]' => $this->_invalid, '[repository]' => $this->_registryAlias ]; } diff --git a/src/Datasource/RulesChecker.php b/src/Datasource/RulesChecker.php index 290470c2879..47c15c2c062 100644 --- a/src/Datasource/RulesChecker.php +++ b/src/Datasource/RulesChecker.php @@ -327,6 +327,14 @@ protected function _addError($rule, $name, $options) $message = [$message]; } $entity->errors($options['errorField'], $message); + + if (method_exists($entity, 'invalid')) { + $invalid = null; + if (isset($entity->{$options['errorField']})) { + $invalid = $entity->{$options['errorField']}; + } + $entity->invalid($options['errorField'], $invalid); + } return $pass === true; }; } diff --git a/src/ORM/Marshaller.php b/src/ORM/Marshaller.php index db7a079c327..e9dbd959d17 100644 --- a/src/ORM/Marshaller.php +++ b/src/ORM/Marshaller.php @@ -128,6 +128,9 @@ public function one(array $data, array $options = []) $properties = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { + if (method_exists($entity, 'invalid')) { + $entity->invalid($key, $value); + } continue; } $columnType = $schema->columnType($key); @@ -463,6 +466,9 @@ public function merge(EntityInterface $entity, array $data, array $options = []) $properties = $marshalledAssocs = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { + if (method_exists($entity, 'invalid')) { + $entity->invalid($key, $value); + } continue; } diff --git a/tests/TestCase/ORM/MarshallerTest.php b/tests/TestCase/ORM/MarshallerTest.php index 66c6ab60b1f..26fe25dff79 100644 --- a/tests/TestCase/ORM/MarshallerTest.php +++ b/tests/TestCase/ORM/MarshallerTest.php @@ -2523,6 +2523,24 @@ public function testPassingCustomValidator() $this->assertNotEmpty($entity->errors('thing')); } + /** + * Tests that invalid propery is being filled when data cannot be patched into an entity + * + * @return void + */ + public function testValidationWithInvalidFilled() { + $data = [ + 'title' => 'foo', + 'number' => 'bar', + ]; + $validator = (new Validator)->add('number', 'numeric', ['rule' => 'numeric']); + $marshall = new Marshaller($this->articles); + $entity = $marshall->one($data, ['validate' => $validator]); + $this->assertNotEmpty($entity->errors('number')); + $this->assertNull($entity->number); + $this->assertSame(['number' => 'bar'], $entity->invalid()); + } + /** * Test merge with validation error * @@ -2541,6 +2559,8 @@ public function testMergeWithValidation() 'body' => 'My Content', 'author_id' => 1 ]); + $this->assertEmpty($entity->invalid()); + $entity->accessible('*', true); $entity->isNew(false); $entity->clean(); @@ -2561,7 +2581,9 @@ public function testMergeWithValidation() $this->articles->validator()->requirePresence('thing', 'create'); $result = $marshall->merge($entity, $data, []); + $this->assertEmpty($result->errors('thing')); + $this->assertSame(['author_id' => 'foo'], $result->invalid()); } /** diff --git a/tests/TestCase/ORM/RulesCheckerIntegrationTest.php b/tests/TestCase/ORM/RulesCheckerIntegrationTest.php index 4d52325ed6b..218c43ff17c 100644 --- a/tests/TestCase/ORM/RulesCheckerIntegrationTest.php +++ b/tests/TestCase/ORM/RulesCheckerIntegrationTest.php @@ -116,6 +116,7 @@ function (Entity $entity) { $this->assertNull($entity->article->get('author_id')); $this->assertFalse($entity->article->dirty('author_id')); $this->assertNotEmpty($entity->article->errors('title')); + $this->assertSame('A Title', $entity->article->invalid('title')); } /** From d212869e3a2ac92db360e90a9ea632235ee723a7 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 3 Jan 2016 20:45:18 +0100 Subject: [PATCH 109/128] Rebase into 3.2 and set interface. --- src/Datasource/InvalidPropertyInterface.php | 36 +++++++++++++++++++++ src/Datasource/RulesChecker.php | 11 +++---- src/ORM/Entity.php | 3 +- src/ORM/Marshaller.php | 4 +-- 4 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 src/Datasource/InvalidPropertyInterface.php diff --git a/src/Datasource/InvalidPropertyInterface.php b/src/Datasource/InvalidPropertyInterface.php new file mode 100644 index 00000000000..e87839957fc --- /dev/null +++ b/src/Datasource/InvalidPropertyInterface.php @@ -0,0 +1,36 @@ +errors($options['errorField'], $message); - if (method_exists($entity, 'invalid')) { - $invalid = null; - if (isset($entity->{$options['errorField']})) { - $invalid = $entity->{$options['errorField']}; - } - $entity->invalid($options['errorField'], $invalid); + $invalid = null; + if (isset($entity->{$options['errorField']})) { + $invalid = $entity->{$options['errorField']}; } + $entity->invalid($options['errorField'], $invalid); + return $pass === true; }; } diff --git a/src/ORM/Entity.php b/src/ORM/Entity.php index 93e4b2c36d9..58be2b33e46 100644 --- a/src/ORM/Entity.php +++ b/src/ORM/Entity.php @@ -15,13 +15,14 @@ namespace Cake\ORM; use Cake\Datasource\EntityInterface; +use Cake\Datasource\InvalidPropertyInterface; use Cake\Datasource\EntityTrait; /** * An entity represents a single result row from a repository. It exposes the * methods for retrieving and storing properties associated in this row. */ -class Entity implements EntityInterface +class Entity implements EntityInterface, InvalidPropertyInterface { use EntityTrait; diff --git a/src/ORM/Marshaller.php b/src/ORM/Marshaller.php index e9dbd959d17..56ccb1ec4c1 100644 --- a/src/ORM/Marshaller.php +++ b/src/ORM/Marshaller.php @@ -128,9 +128,7 @@ public function one(array $data, array $options = []) $properties = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { - if (method_exists($entity, 'invalid')) { - $entity->invalid($key, $value); - } + $entity->invalid($key, $value); continue; } $columnType = $schema->columnType($key); From 357a729d592853e6e6b6841a75453baa88f77689 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 3 Jan 2016 20:50:00 +0100 Subject: [PATCH 110/128] Nitpicks --- src/Datasource/InvalidPropertyInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Datasource/InvalidPropertyInterface.php b/src/Datasource/InvalidPropertyInterface.php index e87839957fc..45bd4190e52 100644 --- a/src/Datasource/InvalidPropertyInterface.php +++ b/src/Datasource/InvalidPropertyInterface.php @@ -9,7 +9,7 @@ * * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) * @link http://cakephp.org CakePHP(tm) Project - * @since 3.0.0 + * @since 3.2.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Cake\Datasource; From 5426aebf8554883a9cae08a0b39755afdb617e1c Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 3 Jan 2016 21:02:39 +0100 Subject: [PATCH 111/128] Conditionally set invalid property. --- src/Datasource/RulesChecker.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Datasource/RulesChecker.php b/src/Datasource/RulesChecker.php index e3cfb49163b..ed9023921de 100644 --- a/src/Datasource/RulesChecker.php +++ b/src/Datasource/RulesChecker.php @@ -328,11 +328,10 @@ protected function _addError($rule, $name, $options) } $entity->errors($options['errorField'], $message); - $invalid = null; if (isset($entity->{$options['errorField']})) { - $invalid = $entity->{$options['errorField']}; + $invalidValue = $entity->{$options['errorField']}; + $entity->invalid($options['errorField'], $invalidValue); } - $entity->invalid($options['errorField'], $invalid); return $pass === true; }; From db8802722dd6b97259d7126397abe4513a35de03 Mon Sep 17 00:00:00 2001 From: ADmad Date: Mon, 4 Jan 2016 01:37:14 +0530 Subject: [PATCH 112/128] Lower PHP version requirement. As of date Ubuntu 14.04 LTS reports it's PHP version as "PHP 5.5.9-1ubuntu4.14" (although it's actually latest patched up 5.5) which prevents installation of CakePHP 3.2. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 56090fe9de9..92695035a07 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "source": "https://github.com/cakephp/cakephp" }, "require": { - "php": ">=5.5.10", + "php": ">=5.5.9", "ext-intl": "*", "ext-mbstring": "*", "cakephp/chronos": "*", From 9415d4416a2cfa555069b4f6b4941ec15af31ec0 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 3 Jan 2016 22:39:12 +0100 Subject: [PATCH 113/128] Fix up tests. --- src/Datasource/EntityTrait.php | 6 +++--- src/Datasource/InvalidPropertyInterface.php | 6 +++--- src/ORM/Entity.php | 3 +-- tests/TestCase/ORM/EntityTest.php | 2 ++ tests/TestCase/ORM/MarshallerTest.php | 5 +++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Datasource/EntityTrait.php b/src/Datasource/EntityTrait.php index 5b6928e1366..600191d600e 100644 --- a/src/Datasource/EntityTrait.php +++ b/src/Datasource/EntityTrait.php @@ -794,9 +794,9 @@ protected function _readError($object, $path = null) * This value could not be patched into the entity and is simply copied into the _invalid property for debugging purposes * or to be able to log it away. * - * @param string|array|null $field - * @param string|null $value - * @param bool $overwrite + * @param string|array|null $field The field to get invalid value for, or the value to set. + * @param mixed|null $value The invalid value to be set for $field. + * @param bool $overwrite Whether or not to overwrite pre-existing values for $field. * @return $this|mixed */ public function invalid($field = null, $value = null, $overwrite = false) diff --git a/src/Datasource/InvalidPropertyInterface.php b/src/Datasource/InvalidPropertyInterface.php index 45bd4190e52..60c3d20779e 100644 --- a/src/Datasource/InvalidPropertyInterface.php +++ b/src/Datasource/InvalidPropertyInterface.php @@ -27,9 +27,9 @@ interface InvalidPropertyInterface * This value could not be patched into the entity and is simply copied into the _invalid property for debugging purposes * or to be able to log it away. * - * @param string|array|null $field - * @param string|null $value - * @param bool $overwrite + * @param string|array|null $field The field to get invalid value for, or the value to set. + * @param mixed|null $value The invalid value to be set for $field. + * @param bool $overwrite Whether or not to overwrite pre-existing values for $field. * @return $this|mixed */ public function invalid($field = null, $value = null, $overwrite = false); diff --git a/src/ORM/Entity.php b/src/ORM/Entity.php index 58be2b33e46..c8248c876ab 100644 --- a/src/ORM/Entity.php +++ b/src/ORM/Entity.php @@ -15,8 +15,8 @@ namespace Cake\ORM; use Cake\Datasource\EntityInterface; -use Cake\Datasource\InvalidPropertyInterface; use Cake\Datasource\EntityTrait; +use Cake\Datasource\InvalidPropertyInterface; /** * An entity represents a single result row from a repository. It exposes the @@ -24,7 +24,6 @@ */ class Entity implements EntityInterface, InvalidPropertyInterface { - use EntityTrait; /** diff --git a/tests/TestCase/ORM/EntityTest.php b/tests/TestCase/ORM/EntityTest.php index e18894014c5..f5f1048b1b1 100644 --- a/tests/TestCase/ORM/EntityTest.php +++ b/tests/TestCase/ORM/EntityTest.php @@ -1205,6 +1205,7 @@ public function testDebugInfo() $entity->virtualProperties(['baz']); $entity->dirty('foo', true); $entity->errors('foo', ['An error']); + $entity->invalid('foo', 'a value'); $entity->source('foos'); $result = $entity->__debugInfo(); $expected = [ @@ -1216,6 +1217,7 @@ public function testDebugInfo() '[original]' => [], '[virtual]' => ['baz'], '[errors]' => ['foo' => ['An error']], + '[invalid]' => ['foo' => 'a value'], '[repository]' => 'foos' ]; $this->assertSame($expected, $result); diff --git a/tests/TestCase/ORM/MarshallerTest.php b/tests/TestCase/ORM/MarshallerTest.php index 26fe25dff79..16060cfd44b 100644 --- a/tests/TestCase/ORM/MarshallerTest.php +++ b/tests/TestCase/ORM/MarshallerTest.php @@ -2524,11 +2524,12 @@ public function testPassingCustomValidator() } /** - * Tests that invalid propery is being filled when data cannot be patched into an entity + * Tests that invalid property is being filled when data cannot be patched into an entity. * * @return void */ - public function testValidationWithInvalidFilled() { + public function testValidationWithInvalidFilled() + { $data = [ 'title' => 'foo', 'number' => 'bar', From 51ec8846e92ae8adbfd2751d612736464b067cb5 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 4 Jan 2016 10:32:04 +0100 Subject: [PATCH 114/128] Add interface checks --- src/Datasource/RulesChecker.php | 3 ++- src/ORM/Marshaller.php | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Datasource/RulesChecker.php b/src/Datasource/RulesChecker.php index ed9023921de..06d590b9297 100644 --- a/src/Datasource/RulesChecker.php +++ b/src/Datasource/RulesChecker.php @@ -14,6 +14,7 @@ */ namespace Cake\Datasource; +use Cake\Datasource\InvalidPropertyInterface; use InvalidArgumentException; /** @@ -328,7 +329,7 @@ protected function _addError($rule, $name, $options) } $entity->errors($options['errorField'], $message); - if (isset($entity->{$options['errorField']})) { + if ($entity instanceof InvalidPropertyInterface && isset($entity->{$options['errorField']})) { $invalidValue = $entity->{$options['errorField']}; $entity->invalid($options['errorField'], $invalidValue); } diff --git a/src/ORM/Marshaller.php b/src/ORM/Marshaller.php index 56ccb1ec4c1..cbbd8207f5a 100644 --- a/src/ORM/Marshaller.php +++ b/src/ORM/Marshaller.php @@ -19,6 +19,7 @@ use Cake\Database\Expression\TupleComparison; use Cake\Database\Type; use Cake\Datasource\EntityInterface; +use Cake\Datasource\InvalidPropertyInterface; use RuntimeException; /** @@ -128,7 +129,9 @@ public function one(array $data, array $options = []) $properties = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { - $entity->invalid($key, $value); + if ($entity instanceof InvalidPropertyInterface) { + $entity->invalid($key, $value); + } continue; } $columnType = $schema->columnType($key); From e6ab885ba675a858a4b18e0c1acf215bf2824670 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 5 Jan 2016 11:28:34 +0100 Subject: [PATCH 115/128] Add missing interface check --- src/ORM/Marshaller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ORM/Marshaller.php b/src/ORM/Marshaller.php index cbbd8207f5a..eed5e210561 100644 --- a/src/ORM/Marshaller.php +++ b/src/ORM/Marshaller.php @@ -467,7 +467,7 @@ public function merge(EntityInterface $entity, array $data, array $options = []) $properties = $marshalledAssocs = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { - if (method_exists($entity, 'invalid')) { + if ($entity instanceof InvalidPropertyInterface) { $entity->invalid($key, $value); } continue; From a1d4c653beb2813c556580161f908a3e9685da34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Lorenzo=20Rodr=C3=ADguez?= Date: Fri, 8 Jan 2016 19:05:26 +0100 Subject: [PATCH 116/128] Using chronos in the composer.json fo I18n --- src/I18n/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/I18n/composer.json b/src/I18n/composer.json index 139be26530e..0ab597da241 100644 --- a/src/I18n/composer.json +++ b/src/I18n/composer.json @@ -17,7 +17,7 @@ "require": { "cakephp/core": "~3.0", "ext-intl": "*", - "nesbot/Carbon": "1.13.*", + "cakephp/chronos": "*", "aura/intl": "1.1.*" }, "suggest": { From 6c5c6cb04ba59d2193b59e37298793e4b2013d83 Mon Sep 17 00:00:00 2001 From: ypnos-web Date: Thu, 14 Jan 2016 17:01:42 +0100 Subject: [PATCH 117/128] Cache accessor methods per-property Instead of inflecting a method name in each get() or set() call, cache the method names (or an empty string that indicates a method is not defined) per-*property*. Successive calls to get() and set() on the same property lead to only one isset() before either the inflicted method string or an empty string (no method defined) is returned. --- src/Datasource/EntityTrait.php | 40 ++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Datasource/EntityTrait.php b/src/Datasource/EntityTrait.php index b47d0142572..01fb4bce859 100644 --- a/src/Datasource/EntityTrait.php +++ b/src/Datasource/EntityTrait.php @@ -73,7 +73,7 @@ trait EntityTrait protected $_dirty = []; /** - * Holds a cached list of methods that exist in the instanced class + * Holds a cached list of getters/setters per class * * @var array */ @@ -251,8 +251,8 @@ public function set($property, $value = null, array $options = []) continue; } - $setter = '_set' . Inflector::camelize($p); - if ($this->_methodExists($setter)) { + $setter = $this->_accessor($p, 'set'); + if ($setter) { $value = $this->{$setter}($value); } $this->_properties[$p] = $value; @@ -275,13 +275,13 @@ public function &get($property) } $value = null; - $method = '_get' . Inflector::camelize($property); + $method = $this->_accessor($property); if (isset($this->_properties[$property])) { $value =& $this->_properties[$property]; } - if ($this->_methodExists($method)) { + if ($method) { $result = $this->{$method}($value); return $result; } @@ -499,17 +499,33 @@ public function offsetUnset($offset) } /** - * Determines whether a method exists in this class + * Fetch accessor method name + * Accessor methods (available or not) are cached in $_accessors * - * @param string $method the method to check for existence - * @return bool true if method exists + * @param string $property the field name to derive getter name from + * @param string $type the accessor type ('get' or 'set') + * @return string method name or empty string (no method available) */ - protected function _methodExists($method) + protected function _accessor($property, $type = 'get') { - if (empty(static::$_accessors[$this->_className])) { - static::$_accessors[$this->_className] = array_flip(get_class_methods($this)); + if (!isset(static::$_accessors[$this->_className][$type][$property])) { + /* first time for this class: build all fields */ + if (empty(static::$_accessors[$this->_className])) { + foreach (get_class_methods($this) as $m) { + $t = substr($m, 1, 3); + if ($m[0] === '_' && in_array($t, ['get', 'set'])) { + $p = Inflector::underscore(substr($m, 4)); + static::$_accessors[$this->_className][$t][$p] = $m; + } + } + } + /* if still not set, remember that the method indeed is not present */ + if (!isset(static::$_accessors[$this->_className][$type][$property])) { + static::$_accessors[$this->_className][$type][$property] = ''; + } + // now static::$_accessors[$this->_className] is always set } - return isset(static::$_accessors[$this->_className][$method]); + return static::$_accessors[$this->_className][$type][$property]; } /** From 63e9f0324bff9663f7e8018169f1695cfa05ee3c Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Sat, 16 Jan 2016 12:36:44 +0100 Subject: [PATCH 118/128] Using the typeMap in the specific comparison functions --- src/Database/Expression/QueryExpression.php | 48 +++++++++++++++++++ .../Expression/QueryExpressionTest.php | 33 +++++++++++++ tests/TestCase/Database/QueryTest.php | 20 ++++++++ 3 files changed, 101 insertions(+) diff --git a/src/Database/Expression/QueryExpression.php b/src/Database/Expression/QueryExpression.php index 0d22903597e..ef8137c821c 100644 --- a/src/Database/Expression/QueryExpression.php +++ b/src/Database/Expression/QueryExpression.php @@ -150,6 +150,9 @@ public function add($conditions, $types = []) */ public function eq($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, '=')); } @@ -165,6 +168,9 @@ public function eq($field, $value, $type = null) */ public function notEq($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, '!=')); } @@ -178,6 +184,9 @@ public function notEq($field, $value, $type = null) */ public function gt($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, '>')); } @@ -191,6 +200,9 @@ public function gt($field, $value, $type = null) */ public function lt($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, '<')); } @@ -204,6 +216,9 @@ public function lt($field, $value, $type = null) */ public function gte($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, '>=')); } @@ -217,6 +232,9 @@ public function gte($field, $value, $type = null) */ public function lte($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, '<=')); } @@ -260,6 +278,9 @@ public function isNotNull($field) */ public function like($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, 'LIKE')); } @@ -273,6 +294,9 @@ public function like($field, $value, $type = null) */ public function notLike($field, $value, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new Comparison($field, $value, $type, 'NOT LIKE')); } @@ -287,6 +311,9 @@ public function notLike($field, $value, $type = null) */ public function in($field, $values, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } $type = $type ?: 'string'; $type .= '[]'; $values = $values instanceof ExpressionInterface ? $values : (array)$values; @@ -320,6 +347,9 @@ public function addCase($conditions, $values = [], $types = []) */ public function notIn($field, $values, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } $type = $type ?: 'string'; $type .= '[]'; $values = $values instanceof ExpressionInterface ? $values : (array)$values; @@ -338,6 +368,9 @@ public function notIn($field, $values, $type = null) */ public function between($field, $from, $to, $type = null) { + if ($type === null) { + $type = $this->_calculateType($field); + } return $this->add(new BetweenExpression($field, $from, $to, $type)); } @@ -663,6 +696,21 @@ protected function _parseCondition($field, $value) return new Comparison($expression, $value, $type, $operator); } + /** + * Returns the type name for the passed field if it was stored in the typeMap + * + * @param mixed $field + * @return string|null + */ + protected function _calculateType($field) + { + $field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field; + if (is_string($field)) { + return $this->typeMap()->type($field); + } + return null; + } + /** * Clone this object and its subtree of expressions. * diff --git a/tests/TestCase/Database/Expression/QueryExpressionTest.php b/tests/TestCase/Database/Expression/QueryExpressionTest.php index b3578dd2980..41edbbb6de8 100644 --- a/tests/TestCase/Database/Expression/QueryExpressionTest.php +++ b/tests/TestCase/Database/Expression/QueryExpressionTest.php @@ -133,4 +133,37 @@ public function testHasNestedExpression() $expr->add(new QueryExpression('1 = 1')); $this->assertTrue($expr->hasNestedExpression()); } + + /** + * Returns the list of specific comparison methods + * + * @return void + */ + public function methodsProvider() + { + return [ + ['eq'], ['notEq'], ['gt'], ['lt'], ['gte'], ['lte'], ['like'], + ['notLike'], ['in'], ['notIn'] + ]; + } + + /** + * Tests that the query expression uses the type map when the + * specific comparison functions are used. + * + * @dataProvider methodsProvider + * @return void + */ + public function testTypeMapUsage($method) + { + $expr = new QueryExpression([], ['created' => 'date']); + $expr->{$method}('created', 'foo'); + + $binder = new ValueBinder(); + $expr->sql($binder); + $bindings = $binder->bindings(); + $type = current($bindings)['type']; + + $this->assertEquals('date', $type); + } } diff --git a/tests/TestCase/Database/QueryTest.php b/tests/TestCase/Database/QueryTest.php index 18c77cf4bb2..006797431ea 100644 --- a/tests/TestCase/Database/QueryTest.php +++ b/tests/TestCase/Database/QueryTest.php @@ -3549,6 +3549,26 @@ public function testRemoveJoin() $this->assertArrayNotHasKey('authors', $query->join()); } + /** + * Tests that types in the type map are used in the + * specific comparison functions when using a callable + * + * @return void + */ + public function testBetweenExpressionAndTypeMap() + { + $query = new Query($this->connection); + $query->select('id') + ->from('comments') + ->defaultTypes(['created' => 'datetime']) + ->where(function ($expr) { + $from = new \DateTime('2007-03-18 10:45:00'); + $to = new \DateTime('2007-03-18 10:48:00'); + return $expr->between('created', $from, $to); + }); + $this->assertCount(2, $query->execute()->fetchAll()); + } + /** * Assertion for comparing a table's contents with what is in it. * From e4afd0c7b754fd5f400aaf5d9d22c076907d4a38 Mon Sep 17 00:00:00 2001 From: ypnos-web Date: Sat, 16 Jan 2016 13:46:37 +0100 Subject: [PATCH 119/128] remove default $type argument --- src/Datasource/EntityTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Datasource/EntityTrait.php b/src/Datasource/EntityTrait.php index 01fb4bce859..ce2f7f97f24 100644 --- a/src/Datasource/EntityTrait.php +++ b/src/Datasource/EntityTrait.php @@ -275,7 +275,7 @@ public function &get($property) } $value = null; - $method = $this->_accessor($property); + $method = $this->_accessor($property, 'get'); if (isset($this->_properties[$property])) { $value =& $this->_properties[$property]; @@ -506,7 +506,7 @@ public function offsetUnset($offset) * @param string $type the accessor type ('get' or 'set') * @return string method name or empty string (no method available) */ - protected function _accessor($property, $type = 'get') + protected function _accessor($property, $type) { if (!isset(static::$_accessors[$this->_className][$type][$property])) { /* first time for this class: build all fields */ From ab3f5d5a2fdbb3cf2f02e0baa12483b53ee02ea0 Mon Sep 17 00:00:00 2001 From: ypnos-web Date: Sat, 16 Jan 2016 16:51:09 +0100 Subject: [PATCH 120/128] reduce nesting --- src/Datasource/EntityTrait.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Datasource/EntityTrait.php b/src/Datasource/EntityTrait.php index ce2f7f97f24..b4ec33c3357 100644 --- a/src/Datasource/EntityTrait.php +++ b/src/Datasource/EntityTrait.php @@ -513,10 +513,11 @@ protected function _accessor($property, $type) if (empty(static::$_accessors[$this->_className])) { foreach (get_class_methods($this) as $m) { $t = substr($m, 1, 3); - if ($m[0] === '_' && in_array($t, ['get', 'set'])) { - $p = Inflector::underscore(substr($m, 4)); - static::$_accessors[$this->_className][$t][$p] = $m; + if ($m[0] !== '_' || !in_array($t, ['get', 'set'])) { + continue; } + $p = Inflector::underscore(substr($m, 4)); + static::$_accessors[$this->_className][$t][$p] = $m; } } /* if still not set, remember that the method indeed is not present */ From 48c171e6ca6fa7dd6860490df1950c257cd2d614 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 16 Jan 2016 13:57:44 -0500 Subject: [PATCH 121/128] Fix PHPCS error related to doc blocks. --- src/Database/Expression/QueryExpression.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Expression/QueryExpression.php b/src/Database/Expression/QueryExpression.php index ef8137c821c..5c7801de381 100644 --- a/src/Database/Expression/QueryExpression.php +++ b/src/Database/Expression/QueryExpression.php @@ -699,8 +699,8 @@ protected function _parseCondition($field, $value) /** * Returns the type name for the passed field if it was stored in the typeMap * - * @param mixed $field - * @return string|null + * @param string|Cake\Database\Expression\QueryExpression $field The field name to get a type for. + * @return string|null The computed type or null, if the type is unknown. */ protected function _calculateType($field) { From aabedb0ed81396a14730a9b2a082537625148056 Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Mon, 18 Jan 2016 16:31:58 -0430 Subject: [PATCH 122/128] Reducing code nesting and removing unneeded property --- src/Datasource/EntityTrait.php | 51 ++++++++++--------- src/ORM/Entity.php | 1 - .../TestApp/Model/Entity/NonExtending.php | 1 - 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Datasource/EntityTrait.php b/src/Datasource/EntityTrait.php index b4ec33c3357..f3cbbb80f8b 100644 --- a/src/Datasource/EntityTrait.php +++ b/src/Datasource/EntityTrait.php @@ -57,13 +57,6 @@ trait EntityTrait */ protected $_virtual = []; - /** - * Holds the name of the class for the instance object - * - * @var string - */ - protected $_className; - /** * Holds a list of the properties that were modified or added after this object * was originally created. @@ -506,27 +499,35 @@ public function offsetUnset($offset) * @param string $type the accessor type ('get' or 'set') * @return string method name or empty string (no method available) */ - protected function _accessor($property, $type) + protected static function _accessor($property, $type) { - if (!isset(static::$_accessors[$this->_className][$type][$property])) { - /* first time for this class: build all fields */ - if (empty(static::$_accessors[$this->_className])) { - foreach (get_class_methods($this) as $m) { - $t = substr($m, 1, 3); - if ($m[0] !== '_' || !in_array($t, ['get', 'set'])) { - continue; - } - $p = Inflector::underscore(substr($m, 4)); - static::$_accessors[$this->_className][$t][$p] = $m; - } - } - /* if still not set, remember that the method indeed is not present */ - if (!isset(static::$_accessors[$this->_className][$type][$property])) { - static::$_accessors[$this->_className][$type][$property] = ''; + $class = static::class; + if (isset(static::$_accessors[$class][$type][$property])) { + return static::$_accessors[$class][$type][$property]; + } + + if (!empty(static::$_accessors[$class])) { + return static::$_accessors[$class][$type][$property] = ''; + } + + if ($class === 'Cake\ORM\Entity') { + return ''; + } + + foreach (get_class_methods($class) as $method) { + $prefix = substr($method, 1, 3); + if ($method[0] !== '_' || ($prefix !== 'get' && $prefix !== 'set')) { + continue; } - // now static::$_accessors[$this->_className] is always set + $field = Inflector::underscore(substr($method, 4)); + static::$_accessors[$class][$prefix][$field] = $method; + } + + if (!isset(static::$_accessors[$class][$type][$property])) { + static::$_accessors[$class][$type][$property] = ''; } - return static::$_accessors[$this->_className][$type][$property]; + + return static::$_accessors[$class][$type][$property]; } /** diff --git a/src/ORM/Entity.php b/src/ORM/Entity.php index 93e4b2c36d9..f57073e2665 100644 --- a/src/ORM/Entity.php +++ b/src/ORM/Entity.php @@ -54,7 +54,6 @@ public function __construct(array $properties = [], array $options = []) 'guard' => false, 'source' => null ]; - $this->_className = get_class($this); if (!empty($options['source'])) { $this->source($options['source']); diff --git a/tests/test_app/TestApp/Model/Entity/NonExtending.php b/tests/test_app/TestApp/Model/Entity/NonExtending.php index 84fd8d12699..bc00db8a2e8 100644 --- a/tests/test_app/TestApp/Model/Entity/NonExtending.php +++ b/tests/test_app/TestApp/Model/Entity/NonExtending.php @@ -23,7 +23,6 @@ public function __construct(array $properties = [], array $options = []) 'guard' => false, 'source' => null ]; - $this->_className = get_class($this); if (!empty($properties)) { $this->set($properties, [ From 6f6be2060e7bbad44884e01f7aeff1f55d61c8d2 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Tue, 19 Jan 2016 21:52:09 -0500 Subject: [PATCH 123/128] Proof of concept approach for Redirect exception. Tests, docs and other requirements are coming soon. --- src/Routing/Exception/RedirectException.php | 9 +++++++++ src/Routing/Filter/RoutingFilter.php | 16 ++++++++++++---- src/Routing/Route/RedirectRoute.php | 8 ++------ 3 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 src/Routing/Exception/RedirectException.php diff --git a/src/Routing/Exception/RedirectException.php b/src/Routing/Exception/RedirectException.php new file mode 100644 index 00000000000..95b221b2bd1 --- /dev/null +++ b/src/Routing/Exception/RedirectException.php @@ -0,0 +1,9 @@ +data['request']; Router::setRequestInfo($request); - if (empty($request->params['controller'])) { - $params = Router::parse($request->url); - $request->addParams($params); + try { + if (empty($request->params['controller'])) { + $params = Router::parse($request->url); + $request->addParams($params); + } + } catch (RedirectException $e) { + $response = $event->data['response']; + $response->statusCode($e->getCode()); + $response->header('Location', $e->getMessage()); + return $response; } } } diff --git a/src/Routing/Route/RedirectRoute.php b/src/Routing/Route/RedirectRoute.php index 39db72f3f1f..981856066fd 100644 --- a/src/Routing/Route/RedirectRoute.php +++ b/src/Routing/Route/RedirectRoute.php @@ -15,6 +15,7 @@ namespace Cake\Routing\Route; use Cake\Network\Response; +use Cake\Routing\Exception\RedirectException; use Cake\Routing\Router; /** @@ -91,12 +92,7 @@ public function parse($url) if (isset($this->options['status']) && ($this->options['status'] >= 300 && $this->options['status'] < 400)) { $status = $this->options['status']; } - $this->response->header([ - 'Location' => Router::url($redirect, true) - ]); - $this->response->statusCode($status); - $this->response->send(); - $this->response->stop(); + throw new RedirectException(Router::url($redirect, true), $status); } /** From 93668043e7d044ec55587f1618792d3d49fdb434 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 20 Jan 2016 08:56:37 -0500 Subject: [PATCH 124/128] Add documentation and updated tests for RedirectRoute Now that matching raises exceptions, the tests needed to be restructured. --- src/Routing/Exception/RedirectException.php | 24 ++- src/Routing/Route/RedirectRoute.php | 10 +- .../Routing/Route/RedirectRouteTest.php | 164 +++++++++++++----- 3 files changed, 145 insertions(+), 53 deletions(-) diff --git a/src/Routing/Exception/RedirectException.php b/src/Routing/Exception/RedirectException.php index 95b221b2bd1..b316e514fd2 100644 --- a/src/Routing/Exception/RedirectException.php +++ b/src/Routing/Exception/RedirectException.php @@ -1,9 +1,31 @@ response) { - $this->response = new Response(); - } $redirect = $this->redirect; if (count($this->redirect) === 1 && !isset($this->redirect['controller'])) { $redirect = $this->redirect[0]; diff --git a/tests/TestCase/Routing/Route/RedirectRouteTest.php b/tests/TestCase/Routing/Route/RedirectRouteTest.php index cdb59ad60ed..fe68c8bd4b2 100644 --- a/tests/TestCase/Routing/Route/RedirectRouteTest.php +++ b/tests/TestCase/Routing/Route/RedirectRouteTest.php @@ -36,82 +36,150 @@ class RedirectRouteTest extends TestCase public function setUp() { parent::setUp(); - Configure::write('Routing', ['admin' => null, 'prefixes' => []]); Router::reload(); + + Router::connect('/:controller', ['action' => 'index']); + Router::connect('/:controller/:action/*'); } /** * test the parsing of routes. * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/posts + * @expectedExceptionCode 301 * @return void */ - public function testParsing() + public function testParseSimple() { - Router::connect('/:controller', ['action' => 'index']); - Router::connect('/:controller/:action/*'); - $route = new RedirectRoute('/home', ['controller' => 'posts']); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/home'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/posts', true), $header['Location']); + $route->parse('/home'); + } + /** + * test the parsing of routes. + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/posts + * @expectedExceptionCode 301 + * @return void + */ + public function testParseArray() + { $route = new RedirectRoute('/home', ['controller' => 'posts', 'action' => 'index']); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/home'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/posts', true), $header['Location']); - $this->assertEquals(301, $route->response->statusCode()); + $route->parse('/home'); + } + /** + * test redirecting to an external url + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://google.com + * @expectedExceptionCode 301 + * @return void + */ + public function testParseAbsolute() + { $route = new RedirectRoute('/google', 'http://google.com'); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/google'); - $header = $route->response->header(); - $this->assertEquals('http://google.com', $header['Location']); + $route->parse('/google'); + } + /** + * test redirecting with a status code + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/posts/view + * @expectedExceptionCode 302 + * @return void + */ + public function testParseStatusCode() + { $route = new RedirectRoute('/posts/*', ['controller' => 'posts', 'action' => 'view'], ['status' => 302]); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/posts/2'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/posts/view', true), $header['Location']); - $this->assertEquals(302, $route->response->statusCode()); + $route->parse('/posts/2'); + } + /** + * test redirecting with the persist option + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/posts/view/2 + * @expectedExceptionCode 301 + * @return void + */ + public function testParsePersist() + { $route = new RedirectRoute('/posts/*', ['controller' => 'posts', 'action' => 'view'], ['persist' => true]); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/posts/2'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/posts/view/2', true), $header['Location']); + $route->parse('/posts/2'); + } + /** + * test redirecting with persist and string target URLs + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/test + * @expectedExceptionCode 301 + * @return void + */ + public function testParsePersistStringUrl() + { $route = new RedirectRoute('/posts/*', '/test', ['persist' => true]); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/posts/2'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/test', true), $header['Location']); + $route->parse('/posts/2'); + } + /** + * test redirecting with persist and passed args + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/tags/add/passme + * @expectedExceptionCode 301 + * @return void + */ + public function testParsePersistPassedArgs() + { $route = new RedirectRoute('/my_controllers/:action/*', ['controller' => 'tags', 'action' => 'add'], ['persist' => true]); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/my_controllers/do_something/passme'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/tags/add/passme', true), $header['Location']); + $route->parse('/my_controllers/do_something/passme'); + } + /** + * test redirecting without persist and passed args + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/tags/add + * @expectedExceptionCode 301 + * @return void + */ + public function testParseNoPersistPassedArgs() + { $route = new RedirectRoute('/my_controllers/:action/*', ['controller' => 'tags', 'action' => 'add']); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/my_controllers/do_something/passme'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/tags/add', true), $header['Location']); + $route->parse('/my_controllers/do_something/passme'); + } + /** + * test redirecting with patterns + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/tags/add?lang=nl + * @expectedExceptionCode 301 + * @return void + */ + public function testParsePersistPatterns() + { $route = new RedirectRoute('/:lang/my_controllers', ['controller' => 'tags', 'action' => 'add'], ['lang' => '(nl|en)', 'persist' => ['lang']]); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/nl/my_controllers/'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/tags/add?lang=nl', true), $header['Location']); + $route->parse('/nl/my_controllers/'); + } - Router::reload(); // reset default routes + /** + * test redirecting with patterns and a routed target + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/nl/preferred_controllers + * @expectedExceptionCode 301 + * @return void + */ + public function testParsePersistMatchesAnotherRoute() + { Router::connect('/:lang/preferred_controllers', ['controller' => 'tags', 'action' => 'add'], ['lang' => '(nl|en)', 'persist' => ['lang']]); $route = new RedirectRoute('/:lang/my_controllers', ['controller' => 'tags', 'action' => 'add'], ['lang' => '(nl|en)', 'persist' => ['lang']]); - $route->response = $this->getMock('Cake\Network\Response', ['_sendHeader', 'stop']); - $result = $route->parse('/nl/my_controllers/'); - $header = $route->response->header(); - $this->assertEquals(Router::url('/nl/preferred_controllers', true), $header['Location']); + $route->parse('/nl/my_controllers/'); } } From 1b10d28dfe16252fe0c29a914d5cf12d5ac5927c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 20 Jan 2016 09:05:21 -0500 Subject: [PATCH 125/128] Add test to cover uncovered cases in RedirectRoute. --- .../Routing/Route/RedirectRouteTest.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/TestCase/Routing/Route/RedirectRouteTest.php b/tests/TestCase/Routing/Route/RedirectRouteTest.php index fe68c8bd4b2..539c27042a8 100644 --- a/tests/TestCase/Routing/Route/RedirectRouteTest.php +++ b/tests/TestCase/Routing/Route/RedirectRouteTest.php @@ -42,6 +42,29 @@ public function setUp() Router::connect('/:controller/:action/*'); } + /** + * test match + * + * @return void + */ + public function testMatch() + { + $route = new RedirectRoute('/home', ['controller' => 'posts']); + $this->assertFalse($route->match(['controller' => 'posts', 'action' => 'index'])); + } + + /** + * test parse failure + * + * @return void + */ + public function testParseMiss() + { + $route = new RedirectRoute('/home', ['controller' => 'posts']); + $this->assertFalse($route->parse('/nope')); + $this->assertFalse($route->parse('/homes')); + } + /** * test the parsing of routes. * @@ -56,6 +79,20 @@ public function testParseSimple() $route->parse('/home'); } + /** + * test the parsing of routes. + * + * @expectedException Cake\Routing\Exception\RedirectException + * @expectedExceptionMessage http://localhost/posts + * @expectedExceptionCode 301 + * @return void + */ + public function testParseRedirectOption() + { + $route = new RedirectRoute('/home', ['redirect' => ['controller' => 'posts']]); + $route->parse('/home'); + } + /** * test the parsing of routes. * From 78fe78ef84d03290a6eda6587ca6eea737e17fad Mon Sep 17 00:00:00 2001 From: ADmad Date: Thu, 21 Jan 2016 20:19:27 +0530 Subject: [PATCH 126/128] Fix docblocks --- src/I18n/Time.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/I18n/Time.php b/src/I18n/Time.php index 0fa1652cacd..297cc5c2a38 100644 --- a/src/I18n/Time.php +++ b/src/I18n/Time.php @@ -115,8 +115,6 @@ public function __construct($time = null, $tz = null) } /** -<<<<<<< HEAD -======= * Returns a nicely formatted date string for this object. * * The format to be used is stored in the static property `Time::niceFormat`. @@ -199,7 +197,6 @@ public function toUnixString() } /** ->>>>>>> master * Returns either a relative or a formatted absolute date depending * on the difference between the current time and this object. * From cbe2e9e8a71f0153b0b85afc7f7ec4808a9445fe Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 21 Jan 2016 21:53:15 -0500 Subject: [PATCH 127/128] Add test for redirect exception creating a response. --- .../Routing/Filter/RoutingFilterTest.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/TestCase/Routing/Filter/RoutingFilterTest.php b/tests/TestCase/Routing/Filter/RoutingFilterTest.php index 9fc51b2dc4a..9e698463ad1 100644 --- a/tests/TestCase/Routing/Filter/RoutingFilterTest.php +++ b/tests/TestCase/Routing/Filter/RoutingFilterTest.php @@ -16,6 +16,7 @@ use Cake\Event\Event; use Cake\Network\Request; +use Cake\Network\Response; use Cake\Routing\Filter\RoutingFilter; use Cake\Routing\Router; use Cake\TestSuite\TestCase; @@ -68,6 +69,27 @@ public function testBeforeDispatchSetsParameters() $this->assertFalse(!empty($request['form'])); } + /** + * test setting parameters in beforeDispatch method + * + * @return void + * @triggers __CLASS__ $this, compact(request) + */ + public function testBeforeDispatchRedirectRoute() + { + Router::redirect('/home', ['controller' => 'articles']); + Router::connect('/:controller/:action/*'); + $filter = new RoutingFilter(); + + $request = new Request("/home"); + $response = new Response(); + $event = new Event(__CLASS__, $this, compact('request', 'response')); + $response = $filter->beforeDispatch($event); + $this->assertInstanceOf('Cake\Network\Response', $response); + $this->assertSame('http://localhost/articles/index', $response->header()['Location']); + $this->assertSame(301, $response->statusCode()); + } + /** * test setting parameters in beforeDispatch method * From 7d79b1bc4c688718b185f69391155eb0e2235c2e Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 29 Jan 2016 22:07:17 -0500 Subject: [PATCH 128/128] Commit changes I forgot about when merging master -> 3.2 --- src/I18n/DateFormatTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/I18n/DateFormatTrait.php b/src/I18n/DateFormatTrait.php index 7f47e270340..d95e004545d 100644 --- a/src/I18n/DateFormatTrait.php +++ b/src/I18n/DateFormatTrait.php @@ -272,7 +272,7 @@ public static function parseDateTime($time, $format = null) $pattern ); $time = $formatter->parse($time); - if ($time) { + if ($time !== false) { $result = new static('@' . $time); return $result->setTimezone(date_default_timezone_get()); }