From 50b1aa0f7511f8c0164a57310295dc1b9ade67d7 Mon Sep 17 00:00:00 2001 From: ftwbzhao Date: Tue, 14 Apr 2015 16:23:12 +0800 Subject: [PATCH 01/22] [utils]update plural regular --- lib/Utils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Utils.php b/lib/Utils.php index af5840cd6..105d28c3a 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -239,7 +239,7 @@ public static function is_blank($var) '/(bu)s$/i' => "$1ses", '/(alias)$/i' => "$1es", '/(octop)us$/i' => "$1i", - '/(ax|test)is$/i' => "$1es", + '/(cris|ax|test)is$/i' => "$1es", '/(us)$/i' => "$1es", '/s$/i' => "s", '/$/' => "s" From 7419a6710347be515801835ff4bda4de62ea7430 Mon Sep 17 00:00:00 2001 From: Thijs Wijnmaalen Date: Tue, 11 Aug 2015 15:20:31 +0200 Subject: [PATCH 02/22] Prevent hhvm.hack.disallow_dynamic_var_env_funcs error in HHVM --- lib/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Table.php b/lib/Table.php index 5f08e1895..1c755efb0 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -516,7 +516,7 @@ private function set_associations() foreach (wrap_strings_in_arrays($definitions) as $definition) { $relationship = null; - $definition += compact('namespace'); + $definition += array('namespace' => $namespace); switch ($name) { From 8c907fe116cd34ef9da6403570287ff6d7e91f08 Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Fri, 11 Dec 2015 04:33:54 -0200 Subject: [PATCH 03/22] Update for PHP 7 Array to string Exception --- lib/Validations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Validations.php b/lib/Validations.php index 4a812ab67..a3989ea75 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -572,7 +572,7 @@ public function validates_uniqueness_of($attrs) { $options = array_merge($configuration, $attr); $pk = $this->model->get_primary_key(); - $pk_value = $this->model->$pk[0]; + $pk_value = $this->model->{$pk[0]}; if (is_array($options[0])) { @@ -908,4 +908,4 @@ public function getIterator() { return new ArrayIterator($this->full_messages()); } -} \ No newline at end of file +} From 90742e21fe21e70939d84e91b4ec0e19a6635c72 Mon Sep 17 00:00:00 2001 From: Bill Zhao Date: Wed, 2 Mar 2016 13:15:29 +0800 Subject: [PATCH 04/22] fix code annotation --- lib/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Model.php b/lib/Model.php index 36e0a4918..0fd633e5d 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -918,7 +918,7 @@ protected function cache_key() * Delete all using a string: * * - * YourModel::delete_all(array('conditions' => 'name = "Tito")); + * YourModel::delete_all(array('conditions' => 'name = "Tito"')); * * * An options array takes the following parameters: From 1fdb61106752e0ff0bf6681d812d4c003f1c2446 Mon Sep 17 00:00:00 2001 From: Jihun Lee Date: Mon, 28 Mar 2016 17:23:59 +0900 Subject: [PATCH 05/22] Remove unnecessary conversion (DateTime) --- lib/Model.php | 2 +- test/ActiveRecordWriteTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/Model.php b/lib/Model.php index 0fd633e5d..0f08cc844 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -460,7 +460,7 @@ public function assign_attribute($name, $value) } // convert php's \DateTime to ours - if ($value instanceof \DateTime) + if (!($value instanceof DateTime) && $value instanceof \DateTime) $value = new DateTime($value->format('Y-m-d H:i:s T')); // make sure DateTime values know what model they belong to so diff --git a/test/ActiveRecordWriteTest.php b/test/ActiveRecordWriteTest.php index 8a6cc8cd7..48c156c34 100644 --- a/test/ActiveRecordWriteTest.php +++ b/test/ActiveRecordWriteTest.php @@ -424,4 +424,21 @@ public function test_update_all_with_limit_and_order() $this->assert_equals(1, $num_affected); $this->assert_true(strpos(Author::table()->last_sql, 'ORDER BY name asc LIMIT 1') !== false); } + + public function test_update_native_datetime() + { + $author = Author::create(array('name' => 'Blah Blah')); + $native_datetime = new \DateTime('1983-12-05'); + $author->some_date = $native_datetime; + $this->assert_false($native_datetime === $author->some_date); + } + + public function test_update_our_datetime() + { + $author = Author::create(array('name' => 'Blah Blah')); + $our_datetime = new DateTime('1983-12-05'); + $author->some_date = $our_datetime; + $this->assert_true($our_datetime === $author->some_date); + } + }; From c52232f66c7ffefc841a63899f2b6af55d7829b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Kowalewski?= Date: Wed, 6 Apr 2016 00:47:32 +0200 Subject: [PATCH 06/22] Copy exact timezone from \DateTime object --- lib/Column.php | 2 +- lib/Model.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Column.php b/lib/Column.php index 6cb45c1c2..60ca02546 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -169,7 +169,7 @@ public function cast($value, $connection) return $value; if ($value instanceof \DateTime) - return new DateTime($value->format('Y-m-d H:i:s T')); + return new DateTime($value->format('Y-m-d H:i:s'), $value->getTimezone()); return $connection->string_to_datetime($value); } diff --git a/lib/Model.php b/lib/Model.php index 0f08cc844..8684e635d 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -461,7 +461,7 @@ public function assign_attribute($name, $value) // convert php's \DateTime to ours if (!($value instanceof DateTime) && $value instanceof \DateTime) - $value = new DateTime($value->format('Y-m-d H:i:s T')); + $value = new DateTime($value->format('Y-m-d H:i:s'), $value->getTimezone()); // make sure DateTime values know what model they belong to so // dirty stuff works when calling set methods on the DateTime object From 695509f7369b2dc63c97547bebd0ff4036530fa6 Mon Sep 17 00:00:00 2001 From: shaneiseminger Date: Thu, 7 Apr 2016 08:26:54 -0700 Subject: [PATCH 07/22] Override additional DateTime methods * Override additional DateTime methods * Call DateTime methods directly, return values for method chaining * Change signature, fix test * Fixes to DateTime method defaults --- lib/DateTime.php | 39 ++++++++++++++++++++++++++++++++------- test/DateTimeTest.php | 6 ++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/DateTime.php b/lib/DateTime.php index 9b488236b..fb44b1316 100644 --- a/lib/DateTime.php +++ b/lib/DateTime.php @@ -127,24 +127,49 @@ private function flag_dirty() public function setDate($year, $month, $day) { $this->flag_dirty(); - call_user_func_array(array($this,'parent::setDate'),func_get_args()); + return parent::setDate($year, $month, $day); } - public function setISODate($year, $week , $day=null) + public function setISODate($year, $week , $day = 1) { $this->flag_dirty(); - call_user_func_array(array($this,'parent::setISODate'),func_get_args()); + return parent::setISODate($year, $week, $day); } - public function setTime($hour, $minute, $second=null) + public function setTime($hour, $minute, $second = 0) { $this->flag_dirty(); - call_user_func_array(array($this,'parent::setTime'),func_get_args()); + return parent::setTime($hour, $minute, $second); } public function setTimestamp($unixtimestamp) { $this->flag_dirty(); - call_user_func_array(array($this,'parent::setTimestamp'),func_get_args()); + return parent::setTimestamp($unixtimestamp); } -} \ No newline at end of file + + public function setTimezone($timezone) + { + $this->flag_dirty(); + return parent::setTimezone($timezone); + } + + public function modify($modify) + { + $this->flag_dirty(); + return parent::modify($modify); + } + + public function add($interval) + { + $this->flag_dirty(); + return parent::add($interval); + } + + public function sub($interval) + { + $this->flag_dirty(); + return parent::sub($interval); + } + +} diff --git a/test/DateTimeTest.php b/test/DateTimeTest.php index 285ca0ea8..2f82f35b7 100644 --- a/test/DateTimeTest.php +++ b/test/DateTimeTest.php @@ -34,10 +34,16 @@ private function assert_dirtifies($method /*, method params, ...*/) public function test_should_flag_the_attribute_dirty() { + $interval = new DateInterval('PT1S'); + $timezone = new DateTimeZone('America/New_York'); $this->assert_dirtifies('setDate',2001,1,1); $this->assert_dirtifies('setISODate',2001,1); $this->assert_dirtifies('setTime',1,1); $this->assert_dirtifies('setTimestamp',1); + $this->assert_dirtifies('setTimezone',$timezone); + $this->assert_dirtifies('modify','+1 day'); + $this->assert_dirtifies('add',$interval); + $this->assert_dirtifies('sub',$interval); } public function test_set_iso_date() From 58506be3fff16ecfd18adbf4877b8e76cb809595 Mon Sep 17 00:00:00 2001 From: Koen Punt Date: Mon, 11 Apr 2016 23:03:10 +0200 Subject: [PATCH 08/22] Create .editorconfig (#534) --- .editorconfig | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..8cc966dfd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.php] +indent_style = tab +indent_size = 4 + +[*.{xml,xml.dist}] +indent_size = 4 From d4b980f02bd31d33526d26bc3e228bf9170dff14 Mon Sep 17 00:00:00 2001 From: Mason Malone Date: Sat, 23 Apr 2016 18:54:43 -0400 Subject: [PATCH 09/22] Add support for using a custom Date class (#532) If your project uses a date library that doesn't extend \DateTime (e.g. Zend_Date), then you currently have to convert to and from ActiveRecord\DateTime when interacting with models, which is a pain and error prone. We've had to fix quite a number of bugs due to missing conversions. This commit removes the need for that by adding support for using a custom date class. The custom class is set in ActiveRecord\Config, which uses similar logic to the logger methods for the getter and setter. The ActiveRecord\Config->set_date_class() method checks if the provided class has "format" and "createFromFormat" methods, which are all that are required. I changed everything that created new date objects to use "createFromFormat()" instead of the constructor, because many date libraries don't accept a date string in the constructor. Besides, I think createFromFormat() is better in general because it's more explicit. For the "attribute_of" method, I made that an optional feature by introducing an interface that defines it, which ActiveRecord\Model->assign_attribute() checks for. The reason I made this optional is because if you use an immutable date class (e.g. PHP's DateTimeImmutable), you don't need to worry about the dirty flagging, so there's no need to have that method. --- ActiveRecord.php | 1 + lib/Column.php | 6 +++-- lib/Config.php | 30 +++++++++++++++++++++++ lib/Connection.php | 7 +++--- lib/DateTime.php | 17 ++++++++++++- lib/DateTimeInterface.php | 35 ++++++++++++++++++++++++++ lib/Model.php | 13 ++++++---- lib/Serialization.php | 3 ++- lib/Table.php | 3 ++- test/ActiveRecordTest.php | 13 ++++++++-- test/ConfigTest.php | 46 +++++++++++++++++++++++++++++++++++ test/DateTimeTest.php | 18 ++++++++++++++ test/helpers/DatabaseTest.php | 3 +++ 13 files changed, 180 insertions(+), 15 deletions(-) create mode 100644 lib/DateTimeInterface.php diff --git a/ActiveRecord.php b/ActiveRecord.php index 17b325703..084587547 100644 --- a/ActiveRecord.php +++ b/ActiveRecord.php @@ -10,6 +10,7 @@ require __DIR__.'/lib/Singleton.php'; require __DIR__.'/lib/Config.php'; require __DIR__.'/lib/Utils.php'; +require __DIR__.'/lib/DateTimeInterface.php'; require __DIR__.'/lib/DateTime.php'; require __DIR__.'/lib/Model.php'; require __DIR__.'/lib/Table.php'; diff --git a/lib/Column.php b/lib/Column.php index 60ca02546..decedb327 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -165,11 +165,13 @@ public function cast($value, $connection) if (!$value) return null; - if ($value instanceof DateTime) + $date_class = Config::instance()->get_date_class(); + + if ($value instanceof $date_class) return $value; if ($value instanceof \DateTime) - return new DateTime($value->format('Y-m-d H:i:s'), $value->getTimezone()); + return $date_class::createFromFormat('Y-m-d H:i:s T', $value->format('Y-m-d H:i:s T'), $value->getTimezone()); return $connection->string_to_datetime($value); } diff --git a/lib/Config.php b/lib/Config.php index 5038af257..1be1eedee 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -72,6 +72,14 @@ class Config extends Singleton */ private $logger; + /** + * Contains the class name for the Date class to use. Must have a public format() method and a + * public static createFromFormat($format, $time) method + * + * @var string + */ + private $date_class = 'ActiveRecord\\DateTime'; + /** * The format to serialize DateTime values into. * @@ -262,6 +270,28 @@ public function get_logger() return $this->logger; } + public function set_date_class($date_class) + { + try { + $klass = Reflections::instance()->add($date_class)->get($date_class); + } catch (\ReflectionException $e) { + throw new ConfigException("Cannot find date class"); + } + + if (!$klass->hasMethod('format') || !$klass->getMethod('format')->isPublic()) + throw new ConfigException('Given date class must have a "public format($format = null)" method'); + + if (!$klass->hasMethod('createFromFormat') || !$klass->getMethod('createFromFormat')->isPublic()) + throw new ConfigException('Given date class must have a "public static createFromFormat($format, $time)" method'); + + $this->date_class = $date_class; + } + + public function get_date_class() + { + return $this->date_class; + } + /** * @deprecated */ diff --git a/lib/Connection.php b/lib/Connection.php index c32311bcb..cde20c36c 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -469,7 +469,7 @@ public function datetime_to_string($datetime) * Converts a string representation of a datetime into a DateTime object. * * @param string $string A datetime in the form accepted by date_create() - * @return DateTime + * @return object The date_class set in Config */ public function string_to_datetime($string) { @@ -479,7 +479,8 @@ public function string_to_datetime($string) if ($errors['warning_count'] > 0 || $errors['error_count'] > 0) return null; - return new DateTime($date->format(static::$datetime_format)); + $date_class = Config::instance()->get_date_class(); + return $date_class::createFromFormat(static::$datetime_format, $date->format(static::$datetime_format)); } /** @@ -530,4 +531,4 @@ public function accepts_limit_and_order_for_update_and_delete() return false; } -} \ No newline at end of file +} diff --git a/lib/DateTime.php b/lib/DateTime.php index fb44b1316..f9f40a0a2 100644 --- a/lib/DateTime.php +++ b/lib/DateTime.php @@ -33,7 +33,7 @@ * @package ActiveRecord * @see http://php.net/manual/en/class.datetime.php */ -class DateTime extends \DateTime +class DateTime extends \DateTime implements DateTimeInterface { /** * Default format used for format() and __toString() @@ -113,6 +113,21 @@ public static function get_format($format=null) return $format; } + /** + * This needs to be overriden so it returns an instance of this class instead of PHP's \DateTime. + * See http://php.net/manual/en/datetime.createfromformat.php + */ + public static function createFromFormat($format, $time, $tz = null) + { + $phpDate = $tz ? parent::createFromFormat($format, $time, $tz) : parent::createFromFormat($format, $time); + if (!$phpDate) + return false; + // convert to this class using the timestamp + $ourDate = new static; + $ourDate->setTimestamp($phpDate->getTimestamp()); + return $ourDate; + } + public function __toString() { return $this->format(); diff --git a/lib/DateTimeInterface.php b/lib/DateTimeInterface.php new file mode 100644 index 000000000..3e7ca3174 --- /dev/null +++ b/lib/DateTimeInterface.php @@ -0,0 +1,35 @@ +assign_attribute() will + * know to call attribute_of() on passed values. This is so the DateTime object can flag the model + * as dirty via $model->flag_dirty() when one of its setters is called. + * + * @package ActiveRecord + * @see http://php.net/manual/en/class.datetime.php + */ +interface DateTimeInterface +{ + /** + * Indicates this object is an attribute of the specified model, with the given attribute name. + * + * @param Model $model The model this object is an attribute of + * @param string $attribute_name The attribute name + * @return void + */ + public function attribute_of($model, $attribute_name); + + /** + * Formats the DateTime to the specified format. + */ + public function format($format=null); + + /** + * See http://php.net/manual/en/datetime.createfromformat.php + */ + public static function createFromFormat($format, $time, $tz = null); +} diff --git a/lib/Model.php b/lib/Model.php index 8684e635d..11ddf68ab 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -460,12 +460,15 @@ public function assign_attribute($name, $value) } // convert php's \DateTime to ours - if (!($value instanceof DateTime) && $value instanceof \DateTime) - $value = new DateTime($value->format('Y-m-d H:i:s'), $value->getTimezone()); + if ($value instanceof \DateTime) { + $date_class = Config::instance()->get_date_class(); + if (!($value instanceof $date_class)) + $value = $date_class::createFromFormat('Y-m-d H:i:s T', $value->format('Y-m-d H:i:s T'), $value->getTimezone()); + } - // make sure DateTime values know what model they belong to so - // dirty stuff works when calling set methods on the DateTime object - if ($value instanceof DateTime) + if ($value instanceof DateTimeInterface) + // Tell the Date object that it's associated with this model and attribute. This is so it + // has the ability to flag this model as dirty if a field in the Date object changes. $value->attribute_of($this,$name); $this->attributes[$name] = $value; diff --git a/lib/Serialization.php b/lib/Serialization.php index 552b13ef9..812ce5b55 100644 --- a/lib/Serialization.php +++ b/lib/Serialization.php @@ -218,9 +218,10 @@ final protected function options_to_a($key) */ final public function to_a() { + $date_class = Config::instance()->get_date_class(); foreach ($this->attributes as &$value) { - if ($value instanceof \DateTime) + if ($value instanceof $date_class) $value = $value->format(self::$DATETIME_FORMAT); } return $this->attributes; diff --git a/lib/Table.php b/lib/Table.php index 1c755efb0..f7bd106d3 100644 --- a/lib/Table.php +++ b/lib/Table.php @@ -428,9 +428,10 @@ private function &process_data($hash) if (!$hash) return $hash; + $date_class = Config::instance()->get_date_class(); foreach ($hash as $name => &$value) { - if ($value instanceof \DateTime) + if ($value instanceof $date_class || $value instanceof \DateTime) { if (isset($this->columns[$name]) && $this->columns[$name]->type == Column::DATE) $hash[$name] = $this->conn->date_to_string($value); diff --git a/test/ActiveRecordTest.php b/test/ActiveRecordTest.php index a20aa913f..f0a0fd110 100644 --- a/test/ActiveRecordTest.php +++ b/test/ActiveRecordTest.php @@ -526,11 +526,20 @@ public function test_gh245_dirty_attribute_should_not_raise_php_notice_if_not_di $this->assert_true($event->attribute_is_dirty('title')); } - public function test_assigning_php_datetime_gets_converted_to_ar_datetime() + public function test_assigning_php_datetime_gets_converted_to_date_class_with_defaults() { $author = new Author(); $author->created_at = $now = new \DateTime(); - $this->assert_is_a("ActiveRecord\\DateTime",$author->created_at); + $this->assert_is_a("ActiveRecord\\DateTime", $author->created_at); + $this->assert_datetime_equals($now,$author->created_at); + } + + public function test_assigning_php_datetime_gets_converted_to_date_class_with_custom_date_class() + { + ActiveRecord\Config::instance()->set_date_class('\\DateTime'); // use PHP built-in DateTime + $author = new Author(); + $author->created_at = $now = new \DateTime(); + $this->assert_is_a("DateTime", $author->created_at); $this->assert_datetime_equals($now,$author->created_at); } diff --git a/test/ConfigTest.php b/test/ConfigTest.php index 59e44541b..f547d5197 100644 --- a/test/ConfigTest.php +++ b/test/ConfigTest.php @@ -8,6 +8,17 @@ class TestLogger private function log() {} } +class TestDateTimeWithoutCreateFromFormat +{ + public function format($format=null) {} +} + +class TestDateTime +{ + public function format($format=null) {} + public static function createFromFormat($format, $time) {} +} + class ConfigTest extends SnakeCase_PHPUnit_Framework_TestCase { public function set_up() @@ -71,6 +82,41 @@ public function test_set_connections_with_default() $this->assert_equals('test',$this->config->get_default_connection()); } + public function test_get_date_class_with_default() + { + $this->assert_equals('ActiveRecord\\DateTime', $this->config->get_date_class()); + } + + /** + * @expectedException ActiveRecord\ConfigException + */ + public function test_set_date_class_when_class_doesnt_exist() + { + $this->config->set_date_class('doesntexist'); + } + + /** + * @expectedException ActiveRecord\ConfigException + */ + public function test_set_date_class_when_class_doesnt_have_format_or_createfromformat() + { + $this->config->set_date_class('TestLogger'); + } + + /** + * @expectedException ActiveRecord\ConfigException + */ + public function test_set_date_class_when_class_doesnt_have_createfromformat() + { + $this->config->set_date_class('TestDateTimeWithoutCreateFromFormat'); + } + + public function test_set_date_class_with_valid_class() + { + $this->config->set_date_class('TestDateTime'); + $this->assert_equals('TestDateTime', $this->config->get_date_class()); + } + public function test_initialize_closure() { $test = $this; diff --git a/test/DateTimeTest.php b/test/DateTimeTest.php index 2f82f35b7..9e40a51de 100644 --- a/test/DateTimeTest.php +++ b/test/DateTimeTest.php @@ -129,5 +129,23 @@ public function test_to_string() { $this->assert_equals(date(DateTime::get_format()), "" . $this->date); } + + public function test_create_from_format_error_handling() + { + $d = DateTime::createFromFormat('H:i:s Y-d-m', '!!!'); + $this->assert_false($d); + } + + public function test_create_from_format_without_tz() + { + $d = DateTime::createFromFormat('H:i:s Y-d-m', '03:04:05 2000-02-01'); + $this->assert_equals(new DateTime('2000-01-02 03:04:05'), $d); + } + + public function test_create_from_format_with_tz() + { + $d = DateTime::createFromFormat('Y-m-d H:i:s', '2000-02-01 03:04:05', new \DateTimeZone('Etc/GMT-10')); + $this->assert_equals(new DateTime('2000-01-31 17:04:05'), $d); + } } ?> diff --git a/test/helpers/DatabaseTest.php b/test/helpers/DatabaseTest.php index b99e44949..c9c29045d 100644 --- a/test/helpers/DatabaseTest.php +++ b/test/helpers/DatabaseTest.php @@ -14,6 +14,8 @@ public function set_up($connection_name=null) $config = ActiveRecord\Config::instance(); $this->original_default_connection = $config->get_default_connection(); + $this->original_date_class = $config->get_date_class(); + if ($connection_name) $config->set_default_connection($connection_name); @@ -42,6 +44,7 @@ public function set_up($connection_name=null) public function tear_down() { + ActiveRecord\Config::instance()->set_date_class($this->original_date_class); if ($this->original_default_connection) ActiveRecord\Config::instance()->set_default_connection($this->original_default_connection); } From da5d27a9d8a0747b98bc29186771dd410f6b364c Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Thu, 19 May 2016 11:56:14 -0400 Subject: [PATCH 10/22] Adding tests for the expected behavior --- test/ColumnTest.php | 28 ++++++++++++++++++++++++++++ test/DateTimeTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/test/ColumnTest.php b/test/ColumnTest.php index f5769bc68..5330814ea 100644 --- a/test/ColumnTest.php +++ b/test/ColumnTest.php @@ -126,5 +126,33 @@ public function test_empty_and_null_datetime_strings_should_return_null() $this->assert_equals(null,$column->cast(null,$this->conn)); $this->assert_equals(null,$column->cast('',$this->conn)); } + + public function test_native_date_time_attribute_copies_exact_tz() + { + $dt = new \DateTime(null, new \DateTimeZone('America/New_York')); + + $column = new Column(); + $column->type = Column::DATETIME; + + $dt2 = $column->cast($dt, $this->conn); + + $this->assert_equals($dt->getTimestamp(), $dt2->getTimestamp()); + $this->assert_equals($dt->getTimeZone(), $dt2->getTimeZone()); + $this->assert_equals($dt->getTimeZone()->getName(), $dt2->getTimeZone()->getName()); + } + + public function test_ar_date_time_attribute_copies_exact_tz() + { + $dt = new DateTime(null, new \DateTimeZone('America/New_York')); + + $column = new Column(); + $column->type = Column::DATETIME; + + $dt2 = $column->cast($dt, $this->conn); + + $this->assert_equals($dt->getTimestamp(), $dt2->getTimestamp()); + $this->assert_equals($dt->getTimeZone(), $dt2->getTimeZone()); + $this->assert_equals($dt->getTimeZone()->getName(), $dt2->getTimeZone()->getName()); + } } ?> diff --git a/test/DateTimeTest.php b/test/DateTimeTest.php index 9e40a51de..edab28fc1 100644 --- a/test/DateTimeTest.php +++ b/test/DateTimeTest.php @@ -147,5 +147,33 @@ public function test_create_from_format_with_tz() $d = DateTime::createFromFormat('Y-m-d H:i:s', '2000-02-01 03:04:05', new \DateTimeZone('Etc/GMT-10')); $this->assert_equals(new DateTime('2000-01-31 17:04:05'), $d); } + + public function test_native_date_time_attribute_copies_exact_tz() + { + $dt = new \DateTime(null, new \DateTimeZone('America/New_York')); + $model = new Author(); + + // Test that the data transforms without modification + $model->assign_attribute('updated_at', $dt); + $dt2 = $model->read_attribute('updated_at'); + + $this->assert_equals($dt->getTimestamp(), $dt2->getTimestamp()); + $this->assert_equals($dt->getTimeZone(), $dt2->getTimeZone()); + $this->assert_equals($dt->getTimeZone()->getName(), $dt2->getTimeZone()->getName()); + } + + public function test_ar_date_time_attribute_copies_exact_tz() + { + $dt = new DateTime(null, new \DateTimeZone('America/New_York')); + $model = new Author(); + + // Test that the data transforms without modification + $model->assign_attribute('updated_at', $dt); + $dt2 = $model->read_attribute('updated_at'); + + $this->assert_equals($dt->getTimestamp(), $dt2->getTimestamp()); + $this->assert_equals($dt->getTimeZone(), $dt2->getTimeZone()); + $this->assert_equals($dt->getTimeZone()->getName(), $dt2->getTimeZone()->getName()); + } } ?> From db2e00acc5823bf3c53623be2d1814bc5fd9df35 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Thu, 19 May 2016 12:08:21 -0400 Subject: [PATCH 11/22] Fixing the format used when converting `DateTime` instances --- lib/Column.php | 2 +- lib/Model.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Column.php b/lib/Column.php index decedb327..e320d48ff 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -171,7 +171,7 @@ public function cast($value, $connection) return $value; if ($value instanceof \DateTime) - return $date_class::createFromFormat('Y-m-d H:i:s T', $value->format('Y-m-d H:i:s T'), $value->getTimezone()); + return $date_class::createFromFormat('Y-m-d H:i:s', $value->format('Y-m-d H:i:s'), $value->getTimezone()); return $connection->string_to_datetime($value); } diff --git a/lib/Model.php b/lib/Model.php index 11ddf68ab..5b9beb4b1 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -463,7 +463,7 @@ public function assign_attribute($name, $value) if ($value instanceof \DateTime) { $date_class = Config::instance()->get_date_class(); if (!($value instanceof $date_class)) - $value = $date_class::createFromFormat('Y-m-d H:i:s T', $value->format('Y-m-d H:i:s T'), $value->getTimezone()); + $value = $date_class::createFromFormat('Y-m-d H:i:s', $value->format('Y-m-d H:i:s'), $value->getTimezone()); } if ($value instanceof DateTimeInterface) From f82ed6130f35be55e8217d0eeaf6aed77bbb51e9 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Thu, 19 May 2016 12:13:18 -0400 Subject: [PATCH 12/22] Adding comment to prevent further regression --- lib/Column.php | 3 +++ lib/Model.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/Column.php b/lib/Column.php index e320d48ff..3baa2271b 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -171,6 +171,9 @@ public function cast($value, $connection) return $value; if ($value instanceof \DateTime) + // NOTE!: The DateTime "format" used must not include a time-zone (name, abbreviation, etc) or offset. + // Including one will cause PHP to ignore the passed in time-zone in the 3rd argument. + // See bug: https://bugs.php.net/bug.php?id=61022 return $date_class::createFromFormat('Y-m-d H:i:s', $value->format('Y-m-d H:i:s'), $value->getTimezone()); return $connection->string_to_datetime($value); diff --git a/lib/Model.php b/lib/Model.php index 5b9beb4b1..915e163c2 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -463,6 +463,9 @@ public function assign_attribute($name, $value) if ($value instanceof \DateTime) { $date_class = Config::instance()->get_date_class(); if (!($value instanceof $date_class)) + // NOTE!: The DateTime "format" used must not include a time-zone (name, abbreviation, etc) or offset. + // Including one will cause PHP to ignore the passed in time-zone in the 3rd argument. + // See bug: https://bugs.php.net/bug.php?id=61022 $value = $date_class::createFromFormat('Y-m-d H:i:s', $value->format('Y-m-d H:i:s'), $value->getTimezone()); } From e0cfce7207b709196144ffb7da900df73fec0130 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Thu, 19 May 2016 12:22:45 -0400 Subject: [PATCH 13/22] The `ActiveRecord\DateTime::createFromFormat()` wasn't passing in the timezone. Fixing... --- lib/DateTime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/DateTime.php b/lib/DateTime.php index f9f40a0a2..67a8ad9ef 100644 --- a/lib/DateTime.php +++ b/lib/DateTime.php @@ -123,7 +123,7 @@ public static function createFromFormat($format, $time, $tz = null) if (!$phpDate) return false; // convert to this class using the timestamp - $ourDate = new static; + $ourDate = new static(null, $phpDate->getTimezone()); $ourDate->setTimestamp($phpDate->getTimestamp()); return $ourDate; } From b283d433f6530b422cdcccbe375997cd1ac3c045 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Thu, 19 May 2016 12:34:10 -0400 Subject: [PATCH 14/22] Fixing the issue in "Connection" too while making a common constant for later reference. --- lib/Connection.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/Connection.php b/lib/Connection.php index cde20c36c..69e8a1b20 100644 --- a/lib/Connection.php +++ b/lib/Connection.php @@ -20,6 +20,17 @@ abstract class Connection { + /** + * The DateTime format to use when translating other DateTime-compatible objects. + * + * NOTE!: The DateTime "format" used must not include a time-zone (name, abbreviation, etc) or offset. + * Including one will cause PHP to ignore the passed in time-zone in the 3rd argument. + * See bug: https://bugs.php.net/bug.php?id=61022 + * + * @var string + */ + const DATETIME_TRANSLATE_FORMAT = 'Y-m-d\TH:i:s'; + /** * The PDO connection object. * @var mixed @@ -480,7 +491,12 @@ public function string_to_datetime($string) return null; $date_class = Config::instance()->get_date_class(); - return $date_class::createFromFormat(static::$datetime_format, $date->format(static::$datetime_format)); + + return $date_class::createFromFormat( + static::DATETIME_TRANSLATE_FORMAT, + $date->format(static::DATETIME_TRANSLATE_FORMAT), + $date->getTimezone() + ); } /** From 0f6bcaf9f393630d59e8867e3bd4a1c7b92cfe64 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Thu, 19 May 2016 12:35:40 -0400 Subject: [PATCH 15/22] Now using the shared constant for a less error prone reusability and easier cross-class documentation --- lib/Column.php | 9 +++++---- lib/Model.php | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/Column.php b/lib/Column.php index 3baa2271b..db6c0a3a6 100644 --- a/lib/Column.php +++ b/lib/Column.php @@ -171,10 +171,11 @@ public function cast($value, $connection) return $value; if ($value instanceof \DateTime) - // NOTE!: The DateTime "format" used must not include a time-zone (name, abbreviation, etc) or offset. - // Including one will cause PHP to ignore the passed in time-zone in the 3rd argument. - // See bug: https://bugs.php.net/bug.php?id=61022 - return $date_class::createFromFormat('Y-m-d H:i:s', $value->format('Y-m-d H:i:s'), $value->getTimezone()); + return $date_class::createFromFormat( + Connection::DATETIME_TRANSLATE_FORMAT, + $value->format(Connection::DATETIME_TRANSLATE_FORMAT), + $value->getTimezone() + ); return $connection->string_to_datetime($value); } diff --git a/lib/Model.php b/lib/Model.php index 915e163c2..6164f8d35 100644 --- a/lib/Model.php +++ b/lib/Model.php @@ -463,10 +463,11 @@ public function assign_attribute($name, $value) if ($value instanceof \DateTime) { $date_class = Config::instance()->get_date_class(); if (!($value instanceof $date_class)) - // NOTE!: The DateTime "format" used must not include a time-zone (name, abbreviation, etc) or offset. - // Including one will cause PHP to ignore the passed in time-zone in the 3rd argument. - // See bug: https://bugs.php.net/bug.php?id=61022 - $value = $date_class::createFromFormat('Y-m-d H:i:s', $value->format('Y-m-d H:i:s'), $value->getTimezone()); + $value = $date_class::createFromFormat( + Connection::DATETIME_TRANSLATE_FORMAT, + $value->format(Connection::DATETIME_TRANSLATE_FORMAT), + $value->getTimezone() + ); } if ($value instanceof DateTimeInterface) From c20a4e082a93386a89e2ecd0c5569904690ee43a Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Thu, 19 May 2016 12:51:08 -0400 Subject: [PATCH 16/22] PHPUnit 3.7 can't correctly compare DateTime objects with different TZs --- test/DateTimeTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/DateTimeTest.php b/test/DateTimeTest.php index edab28fc1..2ebb4006a 100644 --- a/test/DateTimeTest.php +++ b/test/DateTimeTest.php @@ -145,7 +145,9 @@ public function test_create_from_format_without_tz() public function test_create_from_format_with_tz() { $d = DateTime::createFromFormat('Y-m-d H:i:s', '2000-02-01 03:04:05', new \DateTimeZone('Etc/GMT-10')); - $this->assert_equals(new DateTime('2000-01-31 17:04:05'), $d); + $d2 = new DateTime('2000-01-31 17:04:05'); + + $this->assert_equals($d2->getTimestamp(), $d->getTimestamp()); } public function test_native_date_time_attribute_copies_exact_tz() From d7e4e863db7f1762a321cc3de68e1abbcbfb02df Mon Sep 17 00:00:00 2001 From: Jacques Fuentes Date: Thu, 19 May 2016 15:51:38 -0700 Subject: [PATCH 17/22] Upgrading phpunit, wow we're way behind --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 48885d40b..63f932948 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "3.7.*", + "phpunit/phpunit": "4.*", "pear/pear_exception": "1.0-beta1", "pear/log": "~1.12" }, From 1a6ff4c6af9448ce10054484643b8c6cf0a3796d Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Mon, 6 Jun 2016 17:56:41 -0400 Subject: [PATCH 18/22] New test for the expected cloning behavior --- test/DateTimeTest.php | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/test/DateTimeTest.php b/test/DateTimeTest.php index 2ebb4006a..8ce46a18e 100644 --- a/test/DateTimeTest.php +++ b/test/DateTimeTest.php @@ -1,6 +1,7 @@ original_format; } - private function assert_dirtifies($method /*, method params, ...*/) + private function get_model() { try { $model = new Author(); } catch (DatabaseException $e) { $this->mark_test_skipped('failed to connect. '.$e->getMessage()); } + + return $model; + } + + private function assert_dirtifies($method /*, method params, ...*/) + { + $model = $this->get_model(); $datetime = new DateTime(); $datetime->attribute_of($model,'some_date'); @@ -153,7 +161,7 @@ public function test_create_from_format_with_tz() public function test_native_date_time_attribute_copies_exact_tz() { $dt = new \DateTime(null, new \DateTimeZone('America/New_York')); - $model = new Author(); + $model = $this->get_model(); // Test that the data transforms without modification $model->assign_attribute('updated_at', $dt); @@ -167,7 +175,7 @@ public function test_native_date_time_attribute_copies_exact_tz() public function test_ar_date_time_attribute_copies_exact_tz() { $dt = new DateTime(null, new \DateTimeZone('America/New_York')); - $model = new Author(); + $model = $this->get_model(); // Test that the data transforms without modification $model->assign_attribute('updated_at', $dt); @@ -177,5 +185,29 @@ public function test_ar_date_time_attribute_copies_exact_tz() $this->assert_equals($dt->getTimeZone(), $dt2->getTimeZone()); $this->assert_equals($dt->getTimeZone()->getName(), $dt2->getTimeZone()->getName()); } + + public function test_clone() + { + $model = $this->get_model(); + $model_attribute = 'some_date'; + + $datetime = new DateTime(); + $datetime->attribute_of($model, $model_attribute); + + $cloned_datetime = clone $datetime; + + $this->assert_false($model->attribute_is_dirty($model_attribute)); + + $cloned_datetime->add(new DateInterval('PT1S')); + + $this->assert_false($model->attribute_is_dirty($model_attribute)); + + $datetime->add(new DateInterval('PT1S')); + + $this->assert_true($model->attribute_is_dirty($model_attribute)); + + $this->assert_equals($datetime, $cloned_datetime); + $this->assert_not_same($datetime, $cloned_datetime); + } } ?> From 9faa47877659d1c804fb7db0115925ae67ad46d5 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Mon, 6 Jun 2016 17:57:57 -0400 Subject: [PATCH 19/22] Clarification of test flow --- test/DateTimeTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/DateTimeTest.php b/test/DateTimeTest.php index 8ce46a18e..eed9f8d05 100644 --- a/test/DateTimeTest.php +++ b/test/DateTimeTest.php @@ -196,16 +196,20 @@ public function test_clone() $cloned_datetime = clone $datetime; + // Assert initial state $this->assert_false($model->attribute_is_dirty($model_attribute)); $cloned_datetime->add(new DateInterval('PT1S')); + // Assert that modifying the cloned object didn't flag the model $this->assert_false($model->attribute_is_dirty($model_attribute)); $datetime->add(new DateInterval('PT1S')); + // Assert that modifying the model-attached object did flag the model $this->assert_true($model->attribute_is_dirty($model_attribute)); + // Assert that the dates are equal but not the same instance $this->assert_equals($datetime, $cloned_datetime); $this->assert_not_same($datetime, $cloned_datetime); } From 2cd71298361ba32d3ec376f6c3d2c76faa808542 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Mon, 6 Jun 2016 17:58:13 -0400 Subject: [PATCH 20/22] Implementing the `__clone()` method with the proper model dereferencing logic --- lib/DateTime.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/DateTime.php b/lib/DateTime.php index 67a8ad9ef..133d83856 100644 --- a/lib/DateTime.php +++ b/lib/DateTime.php @@ -133,6 +133,20 @@ public function __toString() return $this->format(); } + /** + * Handle PHP object `clone`. + * + * This makes sure that the object doesn't still flag an attached model as + * dirty after cloning the DateTime object and making modifications to it. + * + * @return void + */ + public function __clone() + { + $this->model = null; + $this->attribute_name = null; + } + private function flag_dirty() { if ($this->model) From 2da9dcf4b88aecd7cce95842b241f581c0746971 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Mon, 6 Jun 2016 18:26:51 -0400 Subject: [PATCH 21/22] The test isn't in a namespace. We don't need this. --- test/DateTimeTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/test/DateTimeTest.php b/test/DateTimeTest.php index eed9f8d05..cd3ac6d8c 100644 --- a/test/DateTimeTest.php +++ b/test/DateTimeTest.php @@ -1,7 +1,6 @@ Date: Wed, 22 Jun 2016 03:27:59 -0400 Subject: [PATCH 22/22] Duplicate conditional expression in Validations.php (#545) `!is_string($options['with'])` is repeated. --- lib/Validations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Validations.php b/lib/Validations.php index a3989ea75..64a58cfa5 100644 --- a/lib/Validations.php +++ b/lib/Validations.php @@ -422,7 +422,7 @@ public function validates_format_of($attrs) $attribute = $options[0]; $var = $this->model->$attribute; - if (is_null($options['with']) || !is_string($options['with']) || !is_string($options['with'])) + if (is_null($options['with']) || !is_string($options['with'])) throw new ValidationsArgumentError('A regular expression must be supplied as the [with] option of the configuration array.'); else $expression = $options['with'];