Skip to content

Instantly share code, notes, and snippets.

@kylekatarnls
Last active March 8, 2019 13:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kylekatarnls/ecfa3267a02165e758894d324cce8344 to your computer and use it in GitHub Desktop.
Save kylekatarnls/ecfa3267a02165e758894d324cce8344 to your computer and use it in GitHub Desktop.
Carbon 2 proposal

Carbon 2 (Draft)

Complete set of PHP Date classes extensions:

class extends comment
Carbon DateTime
CarbonImmutable DateTimeImmutable new
CarbonInterface DateTimeInterface new
CarbonTimeZone DateTimeZone new
CarbonInterval DateInterval Read-only days property
CarbonPeriod DatePeriod Blocked until php/php-src#3121 get merged

Setter methods of Carbon become cloners in CarbonImmutable:

$m = new Carbon;
$c1 = $m->setDay(1);
// $m->day === $c1->day as $c1 === $m
$i = new CarbonImmutable;
$c2 = $i->setDay(1);
// $m->day !== $c2->day as $c2 is a new object
$c1->day = 9; // $m->day === 9
$c2->day = 9; // error, immutable

Generic methods

To reduce code amount, we could rather have a magic __call method parsing methods matching actionUnit with action in a given list (isSame, isCurrent, add, sub, get, set, floor, ceil, round, etc.) and unit as valid unit (second, minute, hour, day, etc.) to route to generic method (action) with a unit argument.

This will enforce some methods consistency (easy way to handle singular/plural, same arguments for a given action). And a easier way to add actions. This would encourage to drop boolean arguments (which give multiple responsability to a single method) in favor of adding methods.

A automated script could update the PHPDoc comment to add @method annotations so even if the methods does not really exist, IDE autocompletion will propose magic methods and we would not have to add them all manually.

Having a methods set compatible with https://momentjs.com could help to create a full-stack standard.

Stricter extends

Critical methods should be final (macro methods, magic methods) so it would help us to prevent breaking changes in sub-classes.

All methods in traits

Methods should be grouped in traits and each trait should remain stand-alone (could be taken alone).

Instead of extends the Carbon classes, users would be able to compose their own date classes picking traits.

Documentation

Documentation could be interactive (see https://pug-demo.herokuapp.com and https://phug-lang.com/, it provides code examples the user can edit in live to see the PHP result).

Carbon become big, the single-page and one-level links menu will become a bit too limited, some search/expand/collapse tools would help to browse it.

Each method must be documented with minimal informations:

  • history, example: added in 2.1.0, argument $foo added in 2.2.0, $foo can be an array since 2.2.1
  • trait, example: set() belongs to Setter trait
  • arguments, return and exceptions thrown
  • code examples

Requirement

To provide reliable solid library, Carbon should rely on a full microseconds support, that means at least PHP 7.1.8.

About version 1.x

Carbon is widely used. We should not abandon v1 users.

  • V1 documentation must remain available
  • An issue templating and 1.x/2.x tagging will help to handle issues
  • A migration guide could help user to upgrade
  • Dropped methods could remain in the documentation (tagged as obsolete with some gray-styling and collapsed description)
@spawnia
Copy link

spawnia commented Apr 24, 2018

First of all, great writeup - thanks! Here are my thoughts on the points you brought up:

PHP class compatibility

Extending all of PHPs core methods seems like a solid move. It should make Carbon able to be used interchangeably and seamlessly. I just read through the documentation for the native methods - while i do think we can make some solid additions, i could not find something to persuade me to break compatibility. Unless someone can come up with a case where the PHP implementation just does not cut it, i would say we should go for it.

The naming for the DateTimeZone is subject to discussion:

  • CarbonTimeZone: sub out Date for Carbon as in CarbonInterval/DateInterval and CarbonPeriod/DatePeriod
  • CarbonTimezone: as in Datetime::getTimezone
  • CarbonZone: sub out DateTime for Carbon as with Carbon/DateTime, CarbonInterface/DateTimeInterface and CarbonImmutable/DateTimeImmutable

For the other Classes, the naming is on point.

Generic methods

Good suggestion. For the roadmap, i would not focus on the implementation details that much though - who cares if it is using magic methods under the hood?

We should settle on a list of units to support. The basics are year, month, day, hour, minute, second. What about quarter, week, microsecond? Any others?

There are some differences between what those methods need, this should be a list of methods that should work on any unit:

Method param $value Unit
sub, add (Plural) x x
sub, add (Singular) x
get x
set x x
floor, ceil, round x
endOf, startOf x
isSame, isEqual x x
isCurrent x

We should put a strong focus on making those methods work consistently across units and offer implementations for all the standard units in a first release for 2.x. If we manage to reduce the number of methods and shrink Carbon as a whole, that would be great.

Stricter Extends

If we declare something as final, we should make sure to offer a way of extending functionality, if so needed. For example, we could do something like the pattern of bootable traits that Laravel uses, see here http://www.archybold.com/blog/post/booting-eloquent-model-traits

Traits

I do like traits as a way of splitting code and grouping methods. On the other hand, given that the Carbon class is "pure" in a way, meaning that it does not have side effects or require adapters, integrations..., i do not see why you would need to replace parts of it or mix and match different traits.

Do you have a use case for that? From my perspective, the ~4500 lines of Carbon are a bit much but not to the point where it is becoming an issue.

Documentation

One of the key points of great documentation for me is seeing code + explanation and in the case of carbon, a result of that code.
The docs for react-router are a great example of having those side-by-side.

For the experimental bit - sure, a playground of some sort would be a nice touch.

Requirements

Fully agree on the microseconds support. Having PHP 7.1 as a minimum requirement enables as to use strict types as well so i am all for that.

About version 1.x

We could put the docs for 1.x and 2.x on the same page, preferrably in a similar format and have a dropdown selector for which version to use.

@kylekatarnls
Copy link
Author

About "who cares about magic methods ?" in fact, it will matter for reflexion stuff (method_exists, get_class_methods, etc.), IDE autocompletion.

The real under-the-hood is: we can use a script to generate all methods (so no magic methods in the distributed package), or we can use magic methods and use a script only to generate PHPDoc that will help autocompletion.

@imbrish
Copy link

imbrish commented May 23, 2018

Compatibility and bugs

After some diving in the depths of PHP I bring a good news regarding CarbonPeriod. Work has been done in php/php-src#3121 to allow modifying DatePeriod's properties. If it gets merged it'll be possible to rewrite CarbonPeriod so that it extends the DatePeriod!

I was thinking about fixing the daylight saving time transitions and other bugs directly in the source. Interestingly RFC regarding DST stuff was proposed and accepted (!) already in 2011. However it looks like nothing was done about it ever after. Discussions in internals here and here. On top of that, it looks like tests for the transitions have been added, but as of now they're marked as "expected to fail" - see http://gcov.php.net/viewer.php?version=PHP_7_2&func=expected_tests.

I bet it wouldn't be difficult to push the fixes through. Unfortunately the fixing itself seems to be beyond my possibilities, both in terms of skills and free time. However best-case scenario here could still be 7.3 release. I suppose we may settle on patching what we can in Carbon, at least we can act faster. Is there anything we can do about DST transitions? Would some variant of the hack used in CarbonPeriod work in Carbon?

Generic methods

I agree that standardization of methods should be done. However I'm afraid that proxying everything through __call will quickly get complicated, particularly as new methods are added. We can automate generating class docblocks to provide auto-completion in IDEs, but we still make Carbon less transparent for someone lurking the code. Also code coverage would become less informative.

As an alternative we could generate a trait with all the repetitive methods based on a template. Drawback here is that contributors could feel tempted to modify or add methods to that auto-generated file.

Last option would be to leave the definitions as they currently are, just do some manual standardization. Would that be so bad after all? These methods don't change very often. Once written will probably stay just the same ever after. Cluttering the code shouldn't be a problem as now the methods can be moved into a dedicated trait.

Traits

In my opinion splitting into traits just for better organization of the codebase. I don't think composing your own Carbon is any useful for normal users, so it's not something I'd encourage or mention in the docs.

Stricter extends

I don't like the idea of making methods final/private. Too many times I was coming up with some weird workarounds to make a simple change, because making it directly wasn't possible. Is it because of backwards compatibility? I think we can adhere to the convention of guarantying only the public methods

On the other hand I don't have much experience with maintaining such projects so I may be missing something.

Immutability

Browsing https://github.com/cakephp/chronos made me realize that immutability comes with it's own set of PHP quirks. Splitting into two variants (Carbon and CarbonImmutable) will also increase number of issues. Just something we should be aware of.

Translations

Translated formats like in https://github.com/jenssegers/date would be a great addition to the toString and diff translations. Not sure how useful is the reverse translation for createFromFormat method, but that's probably something that could also be implemented.

I think it'd be nice if by example of Chronos we could get rid of dependencies. I realize that it would require writing and maintaining our own Translator. I didn't investigate how much effort that would require, thus I'll just leave it as that.

Moment.js compatibility

It'd be great if we could achieve compliance with moment not only in terms of API, but also formatting - so that Carbon's diffForHumans and moment's fromNow would produce the same strings for the same ranges and both could be used interchangeably.

@kylekatarnls
Copy link
Author

Translator seems always simple at first sight, sounds like just a big mapping. But check the size of https://github.com/symfony/translation, it's not filled with air, it's really how tricky is the translation rules. Just have a look at pluralization rules to start: https://github.com/symfony/translation/blob/master/PluralizationRules.php

Nearly all the languages are already configured and ready to translate, ready to be customized by end-users and already documented. And Symfony is very well maintained so if a language is missing (it happened only once until now) they are very reactive, last time, they merged in less than 24 hours: symfony/symfony#27286

The symfony/translation dependency also means consistency between the app and Carbon for each one using Symfony/Laravel.

I prefer to focus on date features and rely on a dedicated package for translation.

About mutable/immutable, there are common ways. modify() exists on both for example but returns new object with Immutable. So we can create common interfaces like Modifyable and trait with common methods using modify() no matter it's mutable or not, then on unit tests, we can have a standard way to check it:

protected function assertCarbonModification($expected, $source, $modification)
{
    $actual = $modification($source);
    self::assertEquals($expected, $actual);
    $method = 'assert'.($source instanceof DateTimeImmutable ? 'Not' : '').'Same';
    self::$method($expected, $actual);
}

@kylekatarnls
Copy link
Author

kylekatarnls commented Jun 17, 2018

floor/ceil/round is nearly finished in my local env.

Quick overview about momentjs compatibility (including briannesbitt/Carbon#1360):

Method exists is compatible
add x x
calendar x x
clone x x
creationData
date (4) (4)
dates (3) (3)
day x
dayOfYear x x
days x
daysInMonth (4) (4)
diff x
endOf x x
format x x(2)
from x x
fromNow x x
get x x
hasAlignedHourOffset
hour x x
hours x x
inspect
invalidAt
isAfter x x
isBefore x x
isBetween x x
isDST x x
isDSTShifted (3) (3)
isLeapYear x x
isLocal x x
isSame
isSameOrAfter
isSameOrBefore
isUTC x x
isUtc x x
isUtcOffset
isValid x x
isoWeek x x
isoWeekYear x x
isoWeekday x x
isoWeeks x x
isoWeeksInYear x x
lang (3) (3)
local
locale x x
localeData
max x (1)
millisecond x x
milliseconds x x
min x (1)
minute x x
minutes x x
month x x
months x x
parseZone
parsingFlags
quarter x x
quarters x x
second x x
seconds x x
set x x
startOf x x
subtract x x
to x x
toArray x
toDate x x
toISOString x x
toJSON x x
toNow x x
toObject x x
toString x x
tz x
unix x x
utc x x
utcOffset x x
valueOf x x
week x x
weekYear x x
weekday x x
weeks x x
weeksInYear x x
year x x
years x x
zone (3) (3)
zoneAbbr (3) (3)
zoneName (3) (3)

(1) min and max are deprecated because of an inverted behavior, our behavior match the non-inverted behavior. Moment also have a static method which could be nice to have for min/max.
(2) format in momentjs is isoFormat in Carbon to keep original format method from DateTime class.
(3) deprecated method not relevant.
(4) provide getter instead.

@kylekatarnls
Copy link
Author

Added from, fromNow, to, toNow via briannesbitt/Carbon#1385

@kylekatarnls
Copy link
Author

Lot of new methods added in briannesbitt/Carbon#1385

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment