diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json index a38ed6a6..b5c3e94e 100644 --- a/docs/.vitepress/toc_en.json +++ b/docs/.vitepress/toc_en.json @@ -5,6 +5,7 @@ "collapsed": false, "items": [ { "text": "Introduction", "link": "/index" }, + { "text": "4.x Migration Guide", "link": "/4-x-migration-guide" }, { "text": "3.x Migration Guide", "link": "/3-x-migration-guide" }, { "text": "API", "link": "https://api.cakephp.org/chronos" } ] diff --git a/docs/en/4-x-migration-guide.md b/docs/en/4-x-migration-guide.md new file mode 100644 index 00000000..b10ae079 --- /dev/null +++ b/docs/en/4-x-migration-guide.md @@ -0,0 +1,28 @@ +# 4.x Migration Guide + +Chronos 4.x contains breaking changes that could impact your application. This +guide provides an overview of the breaking changes made in 4.x + +## diff() and fromNow() return ChronosInterval + +`Chronos::diff()`, `ChronosDate::diff()` and `Chronos::fromNow()` now return a +`ChronosInterval` instead of a `DateInterval`. `Chronos::fromNow()` previously +returned `DateInterval|false`; it now always returns a `ChronosInterval`. + +`ChronosInterval` decorates the native `DateInterval` and exposes the same +properties (`y`, `m`, `d`, `h`, `i`, `s`, `f`, `invert`, `days`), so most code +keeps working unchanged. However, it does **not** extend `DateInterval`: code +that type-hints `DateInterval` or relies on `instanceof DateInterval` against +the result must call `->toNative()` to get the underlying `DateInterval`: + +```php +// Before (3.x) +$interval = $first->diff($second); // DateInterval + +// After (4.x), when a native DateInterval is required +$interval = $first->diff($second)->toNative(); // DateInterval +``` + +In return, the result gains ISO 8601 duration formatting and a number of +convenience methods. See [Working with Intervals](/index#working-with-intervals) +for the full overview. diff --git a/docs/en/index.md b/docs/en/index.md index d01afc51..d299ac4d 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -210,7 +210,7 @@ $time->isWithinNext('3 hours'); In addition to comparing datetimes, calculating differences or deltas between two values is a common task: ```php -// Get a DateInterval representing the difference +// Get a ChronosInterval representing the difference $first->diff($second); // Get difference as a count of specific units. @@ -230,6 +230,55 @@ echo $date->diffForHumans(); echo $date->diffForHumans($other); // 1 hour ago; ``` +## Working with Intervals + +`Chronos::diff()`, `ChronosDate::diff()` and `Chronos::fromNow()` return a +`ChronosInterval`. It decorates the native `DateInterval`, so all the usual +properties (`y`, `m`, `d`, `h`, `i`, `s`, `f`, `invert`, `days`) keep working +while adding convenience methods on top: +```php +$interval = $first->diff($second); + +// ISO 8601 duration string. __toString() returns the same value. +echo $interval->toIso8601String(); // P1Y2M3D +echo $interval; // P1Y2M3D + +// Totals. totalDays() is exact when the interval comes from diff(); +// totalSeconds() approximates using 30-day months and 365-day years. +$interval->totalDays(); +$interval->totalSeconds(); + +// State checks. +$interval->isZero(); +$interval->isNegative(); + +// A strtotime()-compatible relative string. +echo $interval->toDateString(); // 1 year 2 months 3 days + +// Component-wise arithmetic (no overflow normalization). +$interval->add($other); +$interval->sub($other); +``` +You can also build intervals directly: +```php +use Cake\Chronos\ChronosInterval; + +ChronosInterval::create('P1Y2M3D'); +ChronosInterval::createFromValues(years: 1, months: 2, days: 3); +ChronosInterval::createFromDateString('1 year 2 days'); +ChronosInterval::instance($dateInterval); +``` +When an API requires a native `DateInterval`, call `toNative()`: +```php +$native = $first->diff($second)->toNative(); +``` + +> [!NOTE] +> `ChronosInterval` is a decorator and does **not** extend `DateInterval`, so +> code that type-hints `DateInterval` or relies on `instanceof DateInterval` +> against the result of `diff()`/`fromNow()` must call `->toNative()` to get +> the wrapped `DateInterval` back. + ## Formatting Strings Chronos provides a number of methods for displaying our outputting datetime diff --git a/phpstan.neon b/phpstan.neon index 02711ac3..1d3977ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,3 +17,6 @@ parameters: message: "#with generic class DatePeriod but does not specify its types: TDate, TEnd, TRecurrences$#" count: 1 path: src/ChronosDatePeriod.php + - + identifier: method.childReturnType + path: src/Chronos.php diff --git a/src/Chronos.php b/src/Chronos.php index 2cb3e0c7..baa67b14 100644 --- a/src/Chronos.php +++ b/src/Chronos.php @@ -20,6 +20,7 @@ use DateTimeInterface; use DateTimeZone; use InvalidArgumentException; +use ReturnTypeWillChange; use RuntimeException; use Stringable; @@ -1011,11 +1012,12 @@ public function modify(string $modifier): static * * @param \DateTimeInterface $target Target instance * @param bool $absolute Whether the interval is forced to be positive - * @return \DateInterval + * @return \Cake\Chronos\ChronosInterval */ - public function diff(DateTimeInterface $target, bool $absolute = false): DateInterval + #[ReturnTypeWillChange] + public function diff(DateTimeInterface $target, bool $absolute = false): ChronosInterval { - return parent::diff($target, $absolute); + return new ChronosInterval(parent::diff($target, $absolute)); } /** @@ -2778,9 +2780,9 @@ public function secondsUntilEndOfDay(): int * Convenience method for getting the remaining time from a given time. * * @param \DateTimeInterface $other 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. + * @return \Cake\Chronos\ChronosInterval The ChronosInterval object representing the difference between the two dates. */ - public static function fromNow(DateTimeInterface $other): DateInterval|bool + public static function fromNow(DateTimeInterface $other): ChronosInterval { $timeNow = new static(); diff --git a/src/ChronosDate.php b/src/ChronosDate.php index 3d800892..902314f3 100644 --- a/src/ChronosDate.php +++ b/src/ChronosDate.php @@ -407,11 +407,11 @@ public function setISODate(int $year, int $week, int $dayOfWeek = 1): static * * @param \Cake\Chronos\ChronosDate $target Target instance * @param bool $absolute Whether the interval is forced to be positive - * @return \DateInterval + * @return \Cake\Chronos\ChronosInterval */ - public function diff(ChronosDate $target, bool $absolute = false): DateInterval + public function diff(ChronosDate $target, bool $absolute = false): ChronosInterval { - return $this->native->diff($target->native, $absolute); + return new ChronosInterval($this->native->diff($target->native, $absolute)); } /** diff --git a/tests/TestCase/ChronosIntervalTest.php b/tests/TestCase/ChronosIntervalTest.php index ffefedc4..2875a0d4 100644 --- a/tests/TestCase/ChronosIntervalTest.php +++ b/tests/TestCase/ChronosIntervalTest.php @@ -15,11 +15,32 @@ namespace Cake\Chronos\Test\TestCase; use Cake\Chronos\Chronos; +use Cake\Chronos\ChronosDate; use Cake\Chronos\ChronosInterval; use DateInterval; class ChronosIntervalTest extends TestCase { + public function testChronosDiffReturnsChronosInterval(): void + { + $start = new Chronos('2020-01-01'); + $end = new Chronos('2020-01-11'); + $diff = $start->diff($end); + + $this->assertInstanceOf(ChronosInterval::class, $diff); + $this->assertSame(10, $diff->d); + } + + public function testChronosDateDiffReturnsChronosInterval(): void + { + $start = ChronosDate::create(2020, 1, 1); + $end = ChronosDate::create(2020, 1, 11); + $diff = $start->diff($end); + + $this->assertInstanceOf(ChronosInterval::class, $diff); + $this->assertSame(10, $diff->d); + } + public function testCreateFromSpec(): void { $interval = ChronosInterval::create('P1Y2M3D'); @@ -88,10 +109,8 @@ public function testToIso8601StringNegative(): void { $past = new Chronos('2020-01-01'); $future = new Chronos('2021-02-02'); - $diff = $past->diff($future); - $diff->invert = 1; + $interval = $future->diff($past); - $interval = ChronosInterval::instance($diff); $this->assertStringStartsWith('-P', $interval->toIso8601String()); } @@ -123,9 +142,8 @@ public function testTotalDaysFromDiff(): void { $start = new Chronos('2020-01-01'); $end = new Chronos('2020-01-11'); - $diff = $start->diff($end); + $interval = $start->diff($end); - $interval = ChronosInterval::instance($diff); $this->assertSame(10, $interval->totalDays()); } @@ -136,9 +154,8 @@ public function testIsNegative(): void $past = new Chronos('2020-01-01'); $future = new Chronos('2020-01-02'); - $diff = $future->diff($past); + $interval = $future->diff($past); - $interval = ChronosInterval::instance($diff); $this->assertTrue($interval->isNegative()); } @@ -296,9 +313,8 @@ public function testToDateStringNegative(): void { $past = new Chronos('2020-01-01'); $future = new Chronos('2020-01-02'); - $diff = $future->diff($past); + $interval = $future->diff($past); - $interval = ChronosInterval::instance($diff); $this->assertStringStartsWith('-', $interval->toDateString()); }