From 3a0b17a058f35adbd64977c01b51aae99f0426ff Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 10:52:45 +0000 Subject: [PATCH 01/14] Ensure Carbon 3 is installed --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fd4b62cef1..a329a6dc09 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "league/glide": "^2.3", "maennchen/zipstream-php": "^3.1", "michelf/php-smartypants": "^1.8.1", - "nesbot/carbon": "^2.62.1", + "nesbot/carbon": "^3.0", "pixelfear/composer-dist-plugin": "^0.1.4", "rebing/graphql-laravel": "^9.7", "rhukster/dom-sanitizer": "^1.0.6", From 56d7befe971f4c980416ad5e5038db360815074a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 12:22:51 +0000 Subject: [PATCH 02/14] Refactor how we get the current string format --- src/Http/Middleware/Localize.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Http/Middleware/Localize.php b/src/Http/Middleware/Localize.php index 17a3ecdca0..4805b7f2f7 100644 --- a/src/Http/Middleware/Localize.php +++ b/src/Http/Middleware/Localize.php @@ -5,8 +5,10 @@ use Carbon\Carbon; use Closure; use Illuminate\Support\Facades\Date; +use ReflectionClass; use Statamic\Facades\Site; use Statamic\Statamic; +use Statamic\Support\Arr; class Localize { @@ -30,9 +32,8 @@ public function handle($request, Closure $next) // Get original Carbon format so it can be restored later. // There's no getter for it, so we'll use reflection. - $format = (new \ReflectionClass(Carbon::class))->getProperty('toStringFormat'); - $format->setAccessible(true); - $originalToStringFormat = $format->getValue(); + $reflection = (new ReflectionClass($date = Date::now()))->getMethod('getFactory'); + $originalToStringFormat = Arr::get($reflection->invoke($date)->getSettings(), 'toStringFormat'); Date::setToStringFormat(Statamic::dateFormat()); $response = $next($request); From 54128bd3d6e0bfa757ab055ea6b627b34546b6de Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 12:25:54 +0000 Subject: [PATCH 03/14] Modifiers should return integers, not floats. --- src/Modifiers/CoreModifiers.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index dca01d2c57..ddda779b21 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -546,7 +546,7 @@ public function dashify($value) */ public function daysAgo($value, $params) { - return $this->carbon($value)->diffInDays(Arr::get($params, 0)); + return (int) $this->carbon($value)->diffInDays(Arr::get($params, 0)); } /** @@ -1055,7 +1055,7 @@ public function hexToRgb($value) */ public function hoursAgo($value, $params) { - return $this->carbon($value)->diffInHours(Arr::get($params, 0)); + return (int) $this->carbon($value)->diffInHours(Arr::get($params, 0)); } /** @@ -1652,7 +1652,7 @@ public function modifyDate($value, $params) */ public function monthsAgo($value, $params) { - return $this->carbon($value)->diffInMonths(Arr::get($params, 0)); + return (int) $this->carbon($value)->diffInMonths(Arr::get($params, 0)); } /** @@ -2234,7 +2234,7 @@ public function segment($value, $params, $context) */ public function secondsAgo($value, $params) { - return $this->carbon($value)->diffInSeconds(Arr::get($params, 0)); + return (int) $this->carbon($value)->diffInSeconds(Arr::get($params, 0)); } /** @@ -2933,7 +2933,7 @@ public function values($value) */ public function weeksAgo($value, $params) { - return $this->carbon($value)->diffInWeeks(Arr::get($params, 0)); + return (int) $this->carbon($value)->diffInWeeks(Arr::get($params, 0)); } /** From bd0eb89e83ab9ba0039247d672ada567c4a7b88c Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 12:35:52 +0000 Subject: [PATCH 04/14] failedRequestRetrySeconds should return a positive integer --- src/Licensing/LicenseManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Licensing/LicenseManager.php b/src/Licensing/LicenseManager.php index 94ca85e7c0..5c8f392204 100644 --- a/src/Licensing/LicenseManager.php +++ b/src/Licensing/LicenseManager.php @@ -35,7 +35,7 @@ public function requestRateLimited() public function failedRequestRetrySeconds() { return $this->requestRateLimited() - ? Carbon::createFromTimestamp($this->response('expiry'))->diffInSeconds() + ? (int) Carbon::createFromTimestamp($this->response('expiry'))->diffInSeconds(absolute: true) : null; } From 33f30f1e9f457609420217250536b382652b6d58 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 13:00:22 +0000 Subject: [PATCH 05/14] Support Carbon 2.x and 3.x at the same time. --- composer.json | 2 +- src/Http/Middleware/Localize.php | 29 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a329a6dc09..5fdafcae10 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "league/glide": "^2.3", "maennchen/zipstream-php": "^3.1", "michelf/php-smartypants": "^1.8.1", - "nesbot/carbon": "^3.0", + "nesbot/carbon": "^2.62.1 || ^3.0", "pixelfear/composer-dist-plugin": "^0.1.4", "rebing/graphql-laravel": "^9.7", "rhukster/dom-sanitizer": "^1.0.6", diff --git a/src/Http/Middleware/Localize.php b/src/Http/Middleware/Localize.php index 4805b7f2f7..745df44a61 100644 --- a/src/Http/Middleware/Localize.php +++ b/src/Http/Middleware/Localize.php @@ -31,9 +31,7 @@ public function handle($request, Closure $next) app()->setLocale($site->lang()); // Get original Carbon format so it can be restored later. - // There's no getter for it, so we'll use reflection. - $reflection = (new ReflectionClass($date = Date::now()))->getMethod('getFactory'); - $originalToStringFormat = Arr::get($reflection->invoke($date)->getSettings(), 'toStringFormat'); + $originalToStringFormat = $this->getToStringFormat(); Date::setToStringFormat(Statamic::dateFormat()); $response = $next($request); @@ -46,4 +44,29 @@ public function handle($request, Closure $next) return $response; } + + /** + * This method is used to get the current toStringFormat for Carbon, in order for us + * to restore it later. There's no getter for it, so we need to use reflection. + * + * @throws \ReflectionException + */ + private function getToStringFormat(): ?string + { + $reflection = new ReflectionClass($date = Date::now()); + + // Carbon 2.x + if ($reflection->hasProperty('toStringFormat')) { + $format = $reflection->getProperty('toStringFormat'); + $format->setAccessible(true); + + return $format->getValue(); + } + + // Carbon 3.x + $factory = $reflection->getMethod('getFactory'); + $factory->setAccessible(true); + + return Arr::get($factory->invoke($date)->getSettings(), 'toStringFormat'); + } } From 1754610f1cf40a5d125615d48d7febd25e152745 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 14:49:06 +0000 Subject: [PATCH 06/14] Refactor the `getToStringFormat` macro in a test --- .../GraphQL/Fieldtypes/DateFieldtypeTest.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php index 4cf18632ce..ee2edbc39e 100644 --- a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php +++ b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php @@ -5,6 +5,8 @@ use Illuminate\Support\Carbon; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; +use ReflectionClass; +use Statamic\Support\Arr; #[Group('graphql')] class DateFieldtypeTest extends FieldtypeTestCase @@ -14,7 +16,17 @@ public function setUp(): void parent::setUp(); Carbon::macro('getToStringFormat', function () { - return static::$toStringFormat; + // Carbon 2.x + if (isset(static::$toStringFormat)) { + return static::$toStringFormat; + } + + // Carbon 3.x + $reflection = new ReflectionClass(self::this()); + $factory = $reflection->getMethod('getFactory'); + $factory->setAccessible(true); + + return Arr::get($factory->invoke(self::this())->getSettings(), 'toStringFormat'); }); } From 10088d2625eaba37f070dc6565dc19b8c05b3ea2 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 14:53:02 +0000 Subject: [PATCH 07/14] `Carbon::addSeconds()` doesn't accept string values --- src/Licensing/Outpost.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Licensing/Outpost.php b/src/Licensing/Outpost.php index 32ffba1daa..4385a2c078 100644 --- a/src/Licensing/Outpost.php +++ b/src/Licensing/Outpost.php @@ -199,7 +199,7 @@ private function cacheAndReturnValidationResponse($e) private function cacheAndReturnRateLimitResponse($e) { - $seconds = $e->getResponse()->getHeader('Retry-After')[0]; + $seconds = (int) $e->getResponse()->getHeader('Retry-After')[0]; return $this->cacheResponse(now()->addSeconds($seconds), ['error' => 429]); } From 36621f6151830a53bf08f52a88da1d9d5a247fe1 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 13 Jan 2025 15:02:36 +0000 Subject: [PATCH 08/14] wip --- tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php index ee2edbc39e..776cfee9b2 100644 --- a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php +++ b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php @@ -17,7 +17,7 @@ public function setUp(): void Carbon::macro('getToStringFormat', function () { // Carbon 2.x - if (isset(static::$toStringFormat)) { + if (property_exists(static::this(), 'toStringFormat')) { return static::$toStringFormat; } From afd141c13648ff43f4588de0d410adcd372d7672 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 6 Feb 2025 15:35:36 +0000 Subject: [PATCH 09/14] Drop support for Carbon 2 --- composer.json | 2 +- src/Http/Middleware/Localize.php | 30 +++---------------- .../GraphQL/Fieldtypes/DateFieldtypeTest.php | 6 ---- 3 files changed, 5 insertions(+), 33 deletions(-) diff --git a/composer.json b/composer.json index 5fdafcae10..a329a6dc09 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "league/glide": "^2.3", "maennchen/zipstream-php": "^3.1", "michelf/php-smartypants": "^1.8.1", - "nesbot/carbon": "^2.62.1 || ^3.0", + "nesbot/carbon": "^3.0", "pixelfear/composer-dist-plugin": "^0.1.4", "rebing/graphql-laravel": "^9.7", "rhukster/dom-sanitizer": "^1.0.6", diff --git a/src/Http/Middleware/Localize.php b/src/Http/Middleware/Localize.php index 745df44a61..47cb6b7df5 100644 --- a/src/Http/Middleware/Localize.php +++ b/src/Http/Middleware/Localize.php @@ -31,7 +31,10 @@ public function handle($request, Closure $next) app()->setLocale($site->lang()); // Get original Carbon format so it can be restored later. - $originalToStringFormat = $this->getToStringFormat(); + $reflection = new ReflectionClass($date = Date::now()); + $factory = $reflection->getMethod('getFactory'); + $factory->setAccessible(true); + $originalToStringFormat = Arr::get($factory->invoke($date)->getSettings(), 'toStringFormat'); Date::setToStringFormat(Statamic::dateFormat()); $response = $next($request); @@ -44,29 +47,4 @@ public function handle($request, Closure $next) return $response; } - - /** - * This method is used to get the current toStringFormat for Carbon, in order for us - * to restore it later. There's no getter for it, so we need to use reflection. - * - * @throws \ReflectionException - */ - private function getToStringFormat(): ?string - { - $reflection = new ReflectionClass($date = Date::now()); - - // Carbon 2.x - if ($reflection->hasProperty('toStringFormat')) { - $format = $reflection->getProperty('toStringFormat'); - $format->setAccessible(true); - - return $format->getValue(); - } - - // Carbon 3.x - $factory = $reflection->getMethod('getFactory'); - $factory->setAccessible(true); - - return Arr::get($factory->invoke($date)->getSettings(), 'toStringFormat'); - } } diff --git a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php index 776cfee9b2..690f744b19 100644 --- a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php +++ b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php @@ -16,12 +16,6 @@ public function setUp(): void parent::setUp(); Carbon::macro('getToStringFormat', function () { - // Carbon 2.x - if (property_exists(static::this(), 'toStringFormat')) { - return static::$toStringFormat; - } - - // Carbon 3.x $reflection = new ReflectionClass(self::this()); $factory = $reflection->getMethod('getFactory'); $factory->setAccessible(true); From 11dad3133f9641dac189e5f59718bda229d24130 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Fri, 7 Feb 2025 17:28:45 +0000 Subject: [PATCH 10/14] Revert "Drop support for Carbon 2" This reverts commit afd141c13648ff43f4588de0d410adcd372d7672. --- composer.json | 2 +- src/Http/Middleware/Localize.php | 30 ++++++++++++++++--- .../GraphQL/Fieldtypes/DateFieldtypeTest.php | 6 ++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index a329a6dc09..5fdafcae10 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "league/glide": "^2.3", "maennchen/zipstream-php": "^3.1", "michelf/php-smartypants": "^1.8.1", - "nesbot/carbon": "^3.0", + "nesbot/carbon": "^2.62.1 || ^3.0", "pixelfear/composer-dist-plugin": "^0.1.4", "rebing/graphql-laravel": "^9.7", "rhukster/dom-sanitizer": "^1.0.6", diff --git a/src/Http/Middleware/Localize.php b/src/Http/Middleware/Localize.php index 47cb6b7df5..745df44a61 100644 --- a/src/Http/Middleware/Localize.php +++ b/src/Http/Middleware/Localize.php @@ -31,10 +31,7 @@ public function handle($request, Closure $next) app()->setLocale($site->lang()); // Get original Carbon format so it can be restored later. - $reflection = new ReflectionClass($date = Date::now()); - $factory = $reflection->getMethod('getFactory'); - $factory->setAccessible(true); - $originalToStringFormat = Arr::get($factory->invoke($date)->getSettings(), 'toStringFormat'); + $originalToStringFormat = $this->getToStringFormat(); Date::setToStringFormat(Statamic::dateFormat()); $response = $next($request); @@ -47,4 +44,29 @@ public function handle($request, Closure $next) return $response; } + + /** + * This method is used to get the current toStringFormat for Carbon, in order for us + * to restore it later. There's no getter for it, so we need to use reflection. + * + * @throws \ReflectionException + */ + private function getToStringFormat(): ?string + { + $reflection = new ReflectionClass($date = Date::now()); + + // Carbon 2.x + if ($reflection->hasProperty('toStringFormat')) { + $format = $reflection->getProperty('toStringFormat'); + $format->setAccessible(true); + + return $format->getValue(); + } + + // Carbon 3.x + $factory = $reflection->getMethod('getFactory'); + $factory->setAccessible(true); + + return Arr::get($factory->invoke($date)->getSettings(), 'toStringFormat'); + } } diff --git a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php index 690f744b19..776cfee9b2 100644 --- a/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php +++ b/tests/Feature/GraphQL/Fieldtypes/DateFieldtypeTest.php @@ -16,6 +16,12 @@ public function setUp(): void parent::setUp(); Carbon::macro('getToStringFormat', function () { + // Carbon 2.x + if (property_exists(static::this(), 'toStringFormat')) { + return static::$toStringFormat; + } + + // Carbon 3.x $reflection = new ReflectionClass(self::this()); $factory = $reflection->getMethod('getFactory'); $factory->setAccessible(true); From 76057a305cc4abbe25befd38df9d9a81ae138460 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 20 Feb 2025 09:58:36 -0500 Subject: [PATCH 11/14] minutes ago and years ago should remain ints --- src/Modifiers/CoreModifiers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index ddda779b21..5b6ebef100 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -1617,7 +1617,7 @@ public function md5($value) */ public function minutesAgo($value, $params) { - return $this->carbon($value)->diffInMinutes(Arr::get($params, 0)); + return (int) $this->carbon($value)->diffInMinutes(Arr::get($params, 0)); } /** @@ -3045,7 +3045,7 @@ public function wordCount($value) */ public function yearsAgo($value, $params) { - return $this->carbon($value)->diffInYears(Arr::get($params, 0)); + return (int) $this->carbon($value)->diffInYears(Arr::get($params, 0)); } /** From 7c2bc84da1fd917b8f6394ad34c0f3329bf4e0ae Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 20 Feb 2025 17:00:55 -0500 Subject: [PATCH 12/14] Add tests for the "ago" modifiers --- tests/Modifiers/DaysAgoTest.php | 44 +++++++++++++++++++++++++++++ tests/Modifiers/HoursAgoTest.php | 44 +++++++++++++++++++++++++++++ tests/Modifiers/MinutesAgoTest.php | 44 +++++++++++++++++++++++++++++ tests/Modifiers/MonthsAgoTest.php | 44 +++++++++++++++++++++++++++++ tests/Modifiers/SecondsAgoTest.php | 42 ++++++++++++++++++++++++++++ tests/Modifiers/WeeksAgoTest.php | 45 ++++++++++++++++++++++++++++++ tests/Modifiers/YearsAgoTest.php | 42 ++++++++++++++++++++++++++++ 7 files changed, 305 insertions(+) create mode 100644 tests/Modifiers/DaysAgoTest.php create mode 100644 tests/Modifiers/HoursAgoTest.php create mode 100644 tests/Modifiers/MinutesAgoTest.php create mode 100644 tests/Modifiers/MonthsAgoTest.php create mode 100644 tests/Modifiers/SecondsAgoTest.php create mode 100644 tests/Modifiers/WeeksAgoTest.php create mode 100644 tests/Modifiers/YearsAgoTest.php diff --git a/tests/Modifiers/DaysAgoTest.php b/tests/Modifiers/DaysAgoTest.php new file mode 100644 index 0000000000..06541a51c3 --- /dev/null +++ b/tests/Modifiers/DaysAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same time' => ['2025-02-20 00:00', 0], + 'less than a day ago' => ['2025-02-19 11:00', 0], + '1 day ago' => ['2025-02-19 00:00', 1], + '2 days ago' => ['2025-02-18 00:00', 2], + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one day from now' => ['2025-02-21 00:00', 1], + 'less than a day from now' => ['2025-02-20 13:00', 0], + 'more than a day from now' => ['2025-02-21 13:00', 1], + ]; + } + + public function modify($value) + { + return Modify::value($value)->daysAgo()->fetch(); + } +} diff --git a/tests/Modifiers/HoursAgoTest.php b/tests/Modifiers/HoursAgoTest.php new file mode 100644 index 0000000000..761bc16fb0 --- /dev/null +++ b/tests/Modifiers/HoursAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same time' => ['2025-02-20 13:10:00', 0], // 0.0 + 'less than a hour ago' => ['2025-02-20 13:00:00', 0], // 0.17 + '1 hour ago' => ['2025-02-20 12:10:00', 1], // 1.0 + '2 hours ago' => ['2025-02-20 11:10:00', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one hour from now' => ['2025-02-20 14:10:00', 1], // -1.0 + 'less than a hour from now' => ['2025-02-20 13:30:00', 0], // -0.33 + 'more than a hour from now' => ['2025-02-20 15:10:00', 2], // -2.0 + ]; + } + + public function modify($value) + { + return Modify::value($value)->hoursAgo()->fetch(); + } +} diff --git a/tests/Modifiers/MinutesAgoTest.php b/tests/Modifiers/MinutesAgoTest.php new file mode 100644 index 0000000000..b564eebc39 --- /dev/null +++ b/tests/Modifiers/MinutesAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same time' => ['2025-02-20 13:10:00', 0], // 0.0 + 'less than a minute ago' => ['2025-02-20 13:09:30', 0], // 0.5 + '1 minute ago' => ['2025-02-20 13:09:00', 1], // 1.0 + '2 minutes ago' => ['2025-02-20 13:08:00', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one minute from now' => ['2025-02-20 13:11:00', 1], // -1.0 + 'less than a minute from now' => ['2025-02-20 13:10:30', 0], // -0.5 + 'more than a minute from now' => ['2025-02-20 13:11:30', 1], // -1.5 + ]; + } + + public function modify($value) + { + return Modify::value($value)->minutesAgo()->fetch(); + } +} diff --git a/tests/Modifiers/MonthsAgoTest.php b/tests/Modifiers/MonthsAgoTest.php new file mode 100644 index 0000000000..338e5db024 --- /dev/null +++ b/tests/Modifiers/MonthsAgoTest.php @@ -0,0 +1,44 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same month' => ['2025-02-20', 0], // 0.0 + 'less than a month ago' => ['2025-02-10', 0], // 0.36 + '1 month ago' => ['2025-01-20', 1], // 1.0 + '2 months ago' => ['2024-12-20', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one month from now' => ['2025-03-20', 1], // -1.0 + 'less than a month from now' => ['2025-02-25', 0], // -0.18 + 'more than a month from now' => ['2025-04-20', 2], // -2.0 + ]; + } + + public function modify($value) + { + return Modify::value($value)->monthsAgo()->fetch(); + } +} diff --git a/tests/Modifiers/SecondsAgoTest.php b/tests/Modifiers/SecondsAgoTest.php new file mode 100644 index 0000000000..4a9ba572c4 --- /dev/null +++ b/tests/Modifiers/SecondsAgoTest.php @@ -0,0 +1,42 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same second' => ['2025-02-20 13:10:30', 0], // 0.0 + '1 second ago' => ['2025-02-20 13:10:29', 1], // 1.0 + '2 seconds ago' => ['2025-02-20 13:10:28', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one second from now' => ['2025-02-20 13:10:31', 1], // -1.0 + 'two seconds from now' => ['2025-02-20 13:10:32', 2], // -2.0 + ]; + } + + public function modify($value) + { + return Modify::value($value)->secondsAgo()->fetch(); + } +} diff --git a/tests/Modifiers/WeeksAgoTest.php b/tests/Modifiers/WeeksAgoTest.php new file mode 100644 index 0000000000..cb9c993ace --- /dev/null +++ b/tests/Modifiers/WeeksAgoTest.php @@ -0,0 +1,45 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + 'same day' => ['2025-02-20', 0], // 0.0 + 'same week' => ['2025-02-19', 0], // 0.14 + 'less than a week ago' => ['2025-02-17', 0], // 0.43 + '1 week ago' => ['2025-02-13', 1], // 1.0 + '2 weeks ago' => ['2025-02-06', 2], // 2.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + 'one week from now' => ['2025-02-27', 1], // -1.0 + 'less than a week from now' => ['2025-02-22', 0], // -0.29 + 'more than a week from now' => ['2025-03-08', 2], // -2.29 + ]; + } + + public function modify($value) + { + return Modify::value($value)->weeksAgo()->fetch(); + } +} diff --git a/tests/Modifiers/YearsAgoTest.php b/tests/Modifiers/YearsAgoTest.php new file mode 100644 index 0000000000..24c5894637 --- /dev/null +++ b/tests/Modifiers/YearsAgoTest.php @@ -0,0 +1,42 @@ +assertSame($expected, $this->modify(Carbon::parse($input))); + } + + public static function dateProvider() + { + return [ + // Carbon 3 would return floats but to preserve backwards compatibility + // with Carbon 2 we will cast to integers. + '2 years' => ['2023-02-20', 2], // 2.0 + 'not quite 3 years' => ['2022-08-20', 2], // 2.5 + '3 years' => ['2022-02-20', 3], // 3.0 + + // Future dates would return negative numbers in Carbon 3 but to preserve + // backwards compatibility with Carbon 2, we keep them positive. + '1 year from now' => ['2026-02-20', 1], // -1.0 + 'less than a year from now' => ['2025-12-20', 0], // -0.83 + ]; + } + + public function modify($value) + { + return Modify::value($value)->yearsAgo()->fetch(); + } +} From 4c82f0282d75f75c04aeb3fefb0c7f4fdf638142 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 20 Feb 2025 17:02:15 -0500 Subject: [PATCH 13/14] "ago" modifiers should be absolute for bc --- src/Modifiers/CoreModifiers.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 5b6ebef100..03328d3d64 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -546,7 +546,7 @@ public function dashify($value) */ public function daysAgo($value, $params) { - return (int) $this->carbon($value)->diffInDays(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInDays(Arr::get($params, 0))); } /** @@ -1055,7 +1055,7 @@ public function hexToRgb($value) */ public function hoursAgo($value, $params) { - return (int) $this->carbon($value)->diffInHours(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInHours(Arr::get($params, 0))); } /** @@ -1617,7 +1617,7 @@ public function md5($value) */ public function minutesAgo($value, $params) { - return (int) $this->carbon($value)->diffInMinutes(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInMinutes(Arr::get($params, 0))); } /** @@ -1652,7 +1652,7 @@ public function modifyDate($value, $params) */ public function monthsAgo($value, $params) { - return (int) $this->carbon($value)->diffInMonths(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInMonths(Arr::get($params, 0))); } /** @@ -2234,7 +2234,7 @@ public function segment($value, $params, $context) */ public function secondsAgo($value, $params) { - return (int) $this->carbon($value)->diffInSeconds(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInSeconds(Arr::get($params, 0))); } /** @@ -2933,7 +2933,7 @@ public function values($value) */ public function weeksAgo($value, $params) { - return (int) $this->carbon($value)->diffInWeeks(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInWeeks(Arr::get($params, 0))); } /** @@ -3045,7 +3045,7 @@ public function wordCount($value) */ public function yearsAgo($value, $params) { - return (int) $this->carbon($value)->diffInYears(Arr::get($params, 0)); + return (int) abs($this->carbon($value)->diffInYears(Arr::get($params, 0))); } /** From 4d613c68649124277651ac30be9aca93fa0964d4 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Fri, 21 Feb 2025 11:14:32 -0500 Subject: [PATCH 14/14] Pass app timezone to createFromTimestamp as per carbon 3 migration guide --- src/Assets/Asset.php | 2 +- src/Auth/File/User.php | 4 ++-- src/Auth/Passwords/TokenRepository.php | 2 +- src/Data/ExistsAsFile.php | 2 +- src/Data/TracksLastModified.php | 2 +- src/Entries/Entry.php | 2 +- src/Forms/Submission.php | 2 +- src/Http/Controllers/CP/SessionTimeoutController.php | 2 +- src/Licensing/LicenseManager.php | 2 +- src/Modifiers/CoreModifiers.php | 2 +- src/Revisions/RevisionRepository.php | 2 +- src/Stache/Stache.php | 2 +- src/Support/FileCollection.php | 2 +- src/Taxonomies/LocalizedTerm.php | 2 +- src/Tokens/FileTokenRepository.php | 2 +- tests/Assets/AssetTest.php | 2 +- tests/Feature/Assets/StoreAssetTest.php | 2 +- tests/Feature/Entries/EntryRevisionsTest.php | 4 ++-- tests/Feature/Fieldtypes/FilesTest.php | 2 +- 19 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index c700ed7d7d..53d921ee96 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -589,7 +589,7 @@ public function mimeType() */ public function lastModified() { - return Carbon::createFromTimestamp($this->meta('last_modified')); + return Carbon::createFromTimestamp($this->meta('last_modified'), config('app.timezone')); } /** diff --git a/src/Auth/File/User.php b/src/Auth/File/User.php index 4da8ee696e..65c305b177 100644 --- a/src/Auth/File/User.php +++ b/src/Auth/File/User.php @@ -116,7 +116,7 @@ public function lastModified() ? File::disk('users')->lastModified($path) : time(); - return Carbon::createFromTimestamp($timestamp); + return Carbon::createFromTimestamp($timestamp, config('app.timezone')); } /** @@ -298,7 +298,7 @@ public function lastLogin() { $last_login = $this->getMeta('last_login'); - return $last_login ? Carbon::createFromTimestamp($last_login) : $last_login; + return $last_login ? Carbon::createFromTimestamp($last_login, config('app.timezone')) : $last_login; } public function setLastLogin($carbon) diff --git a/src/Auth/Passwords/TokenRepository.php b/src/Auth/Passwords/TokenRepository.php index 9b226c8b5f..0a717929fb 100644 --- a/src/Auth/Passwords/TokenRepository.php +++ b/src/Auth/Passwords/TokenRepository.php @@ -70,7 +70,7 @@ public function exists(CanResetPasswordContract $user, $token) $record = $this->getResets()->get($user->email()); return $record && - ! $this->tokenExpired(Carbon::createFromTimestamp($record['created_at'])) + ! $this->tokenExpired(Carbon::createFromTimestamp($record['created_at'], config('app.timezone'))) && $this->hasher->check($token, $record['token']); } diff --git a/src/Data/ExistsAsFile.php b/src/Data/ExistsAsFile.php index 7a549641c0..bc1efb2e1d 100644 --- a/src/Data/ExistsAsFile.php +++ b/src/Data/ExistsAsFile.php @@ -74,7 +74,7 @@ public function fileLastModified() return Carbon::now(); } - return Carbon::createFromTimestamp(File::lastModified($this->path())); + return Carbon::createFromTimestamp(File::lastModified($this->path()), config('app.timezone')); } public function fileExtension() diff --git a/src/Data/TracksLastModified.php b/src/Data/TracksLastModified.php index 76b4585c19..30b2f2d1b9 100644 --- a/src/Data/TracksLastModified.php +++ b/src/Data/TracksLastModified.php @@ -10,7 +10,7 @@ trait TracksLastModified public function lastModified() { return $this->has('updated_at') - ? Carbon::createFromTimestamp($this->get('updated_at')) + ? Carbon::createFromTimestamp($this->get('updated_at'), config('app.timezone')) : $this->fileLastModified(); } diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index 06a18e7c0e..decfbd6094 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -693,7 +693,7 @@ public function makeFromRevision($revision) ->slug($attrs['slug']); if ($this->collection()->dated() && ($date = Arr::get($attrs, 'date'))) { - $entry->date(Carbon::createFromTimestamp($date)); + $entry->date(Carbon::createFromTimestamp($date, config('app.timezone'))); } return $entry; diff --git a/src/Forms/Submission.php b/src/Forms/Submission.php index c279bef08a..1442ee84fa 100644 --- a/src/Forms/Submission.php +++ b/src/Forms/Submission.php @@ -101,7 +101,7 @@ public function columns() */ public function date() { - return Carbon::createFromTimestamp($this->id()); + return Carbon::createFromTimestamp($this->id(), config('app.timezone')); } /** diff --git a/src/Http/Controllers/CP/SessionTimeoutController.php b/src/Http/Controllers/CP/SessionTimeoutController.php index d333de9526..a2b6483016 100644 --- a/src/Http/Controllers/CP/SessionTimeoutController.php +++ b/src/Http/Controllers/CP/SessionTimeoutController.php @@ -13,7 +13,7 @@ public function __invoke() // remember me would have already been served a 403 error and wouldn't have got this far. $lastActivity = session('last_activity', now()->timestamp); - return Carbon::createFromTimestamp($lastActivity) + return Carbon::createFromTimestamp($lastActivity, config('app.timezone')) ->addMinutes(config('session.lifetime')) ->diffInSeconds(); } diff --git a/src/Licensing/LicenseManager.php b/src/Licensing/LicenseManager.php index 5c8f392204..aa879bec11 100644 --- a/src/Licensing/LicenseManager.php +++ b/src/Licensing/LicenseManager.php @@ -35,7 +35,7 @@ public function requestRateLimited() public function failedRequestRetrySeconds() { return $this->requestRateLimited() - ? (int) Carbon::createFromTimestamp($this->response('expiry'))->diffInSeconds(absolute: true) + ? (int) Carbon::createFromTimestamp($this->response('expiry'), config('app.timezone'))->diffInSeconds(absolute: true) : null; } diff --git a/src/Modifiers/CoreModifiers.php b/src/Modifiers/CoreModifiers.php index 03328d3d64..2e8c30ec4c 100644 --- a/src/Modifiers/CoreModifiers.php +++ b/src/Modifiers/CoreModifiers.php @@ -3210,7 +3210,7 @@ private function getMathModifierNumber($params, $context) private function carbon($value) { if (! $value instanceof Carbon) { - $value = (is_numeric($value)) ? Date::createFromTimestamp($value) : Date::parse($value); + $value = (is_numeric($value)) ? Date::createFromTimestamp($value, config('app.timezone')) : Date::parse($value); } return $value; diff --git a/src/Revisions/RevisionRepository.php b/src/Revisions/RevisionRepository.php index 66e433ae2f..7e4331b38e 100644 --- a/src/Revisions/RevisionRepository.php +++ b/src/Revisions/RevisionRepository.php @@ -69,7 +69,7 @@ protected function makeRevisionFromFile($key, $path) ->key($key) ->action($yaml['action'] ?? false) ->id($date = $yaml['date']) - ->date(Carbon::createFromTimestamp($date)) + ->date(Carbon::createFromTimestamp($date, config('app.timezone'))) ->user($yaml['user'] ?? false) ->message($yaml['message'] ?? false) ->attributes($yaml['attributes']); diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index 2c08f63036..1eb76aec62 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -176,7 +176,7 @@ public function buildDate() return null; } - return Carbon::createFromTimestamp($cache['date']); + return Carbon::createFromTimestamp($cache['date'], config('app.timezone')); } public function disableUpdatingIndexes() diff --git a/src/Support/FileCollection.php b/src/Support/FileCollection.php index 3ed53e8de9..4aebd51509 100644 --- a/src/Support/FileCollection.php +++ b/src/Support/FileCollection.php @@ -212,7 +212,7 @@ public function toArray() 'size_mb' => $kb, 'size_gb' => $kb, 'is_file' => File::isImage($path), - 'last_modified' => Carbon::createFromTimestamp(File::lastModified($path)), + 'last_modified' => Carbon::createFromTimestamp(File::lastModified($path), config('app.timezone')), ]; } diff --git a/src/Taxonomies/LocalizedTerm.php b/src/Taxonomies/LocalizedTerm.php index f40b394ae0..41d4985897 100644 --- a/src/Taxonomies/LocalizedTerm.php +++ b/src/Taxonomies/LocalizedTerm.php @@ -485,7 +485,7 @@ protected function defaultAugmentedRelations() public function lastModified() { return $this->has('updated_at') - ? Carbon::createFromTimestamp($this->get('updated_at')) + ? Carbon::createFromTimestamp($this->get('updated_at'), config('app.timezone')) : $this->term->fileLastModified(); } diff --git a/src/Tokens/FileTokenRepository.php b/src/Tokens/FileTokenRepository.php index 95d0d36357..94a9caee9d 100644 --- a/src/Tokens/FileTokenRepository.php +++ b/src/Tokens/FileTokenRepository.php @@ -55,7 +55,7 @@ private function makeFromPath(string $path): FileToken return $this ->make($token, $yaml['handler'], $yaml['data'] ?? []) - ->expireAt(Carbon::createFromTimestamp($yaml['expires_at'])); + ->expireAt(Carbon::createFromTimestamp($yaml['expires_at'], config('app.timezone'))); } public static function bindings(): array diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index 06fc356c62..3cbc1ef3d0 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -2017,7 +2017,7 @@ public function it_can_process_a_custom_image_format() public function it_appends_timestamp_to_uploaded_files_filename_if_it_already_exists() { Event::fake(); - Carbon::setTestNow(Carbon::createFromTimestamp(1549914700)); + Carbon::setTestNow(Carbon::createFromTimestamp(1549914700, config('app.timezone'))); $asset = $this->container->makeAsset('path/to/asset.jpg'); Facades\AssetContainer::shouldReceive('findByHandle')->with('test_container')->andReturn($this->container); Storage::disk('test')->put('path/to/asset.jpg', ''); diff --git a/tests/Feature/Assets/StoreAssetTest.php b/tests/Feature/Assets/StoreAssetTest.php index c417a6d077..bba5ce6c3e 100644 --- a/tests/Feature/Assets/StoreAssetTest.php +++ b/tests/Feature/Assets/StoreAssetTest.php @@ -171,7 +171,7 @@ public function it_can_upload_and_overwrite() #[Test] public function it_can_upload_and_append_timestamp() { - Carbon::setTestNow(Carbon::createFromTimestamp(1697379288)); + Carbon::setTestNow(Carbon::createFromTimestamp(1697379288, config('app.timezone'))); Storage::disk('test')->put('path/to/test.jpg', 'contents'); Storage::disk('test')->assertExists('path/to/test.jpg'); $this->assertCount(1, Storage::disk('test')->files('path/to')); diff --git a/tests/Feature/Entries/EntryRevisionsTest.php b/tests/Feature/Entries/EntryRevisionsTest.php index e50009df67..d9d032ead2 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -283,7 +283,7 @@ public function it_restores_a_published_entrys_working_copy_to_another_revision( $revision = tap((new Revision) ->key('collections/blog/en/123') - ->date(Carbon::createFromTimestamp('1553546421')) + ->date(Carbon::createFromTimestamp('1553546421', config('app.timezone'))) ->attributes([ 'published' => false, 'slug' => 'existing-slug', @@ -345,7 +345,7 @@ public function it_restores_an_unpublished_entrys_contents_to_another_revision() $revision = tap((new Revision) ->key('collections/blog/en/123') - ->date(Carbon::createFromTimestamp('1553546421')) + ->date(Carbon::createFromTimestamp('1553546421', config('app.timezone'))) ->attributes([ 'published' => true, 'slug' => 'existing-slug', diff --git a/tests/Feature/Fieldtypes/FilesTest.php b/tests/Feature/Fieldtypes/FilesTest.php index 70b13539ab..38e9ce2fdb 100644 --- a/tests/Feature/Fieldtypes/FilesTest.php +++ b/tests/Feature/Fieldtypes/FilesTest.php @@ -44,7 +44,7 @@ public function it_uploads_a_file($container, $isImage, $expectedPath, $expected ? UploadedFile::fake()->image('test.jpg', 50, 75) : UploadedFile::fake()->create('test.txt'); - Date::setTestNow(Date::createFromTimestamp(1671484636)); + Date::setTestNow(Date::createFromTimestamp(1671484636, config('app.timezone'))); $disk = Storage::fake('local');