Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.8] Allow Carbon 2 and custom date handling #25320

Merged
merged 7 commits into from
Oct 3, 2018

Conversation

kylekatarnls
Copy link
Contributor

@kylekatarnls kylekatarnls commented Aug 24, 2018

This PR allow both Carbon ^1.26.3 and ^2.0 to work in Carbon and it allows the user to choose the class he want for dates he receive (such as CarbonImmutable, Chronos or simply DateTime) and/or intercept date generation to customize the date objects.

This PR refactor #25310 using facade instead of factory.

Some examples:

Date::swap(\Carbon\CarbonImmutable::class);
// Assuming created_at is an Eloquent date property
var_dump(get_class($this->created_at)); // Carbon\CarbonImmutable
// Apply French language on translatable methods for all generated dates
Date::swap(function (\Carbon\CarbonInterface $date) {
  return $date->locale('fr');
});
var_dump($this->created_at->locale); // fr
Date::swap(new \Carbon\Factory([
  'locale' => 'en_AU',
  'timezone' => 'Australia/Sydney',
]));
var_dump($this->created_at->locale); // en_AU
var_dump($this->created_at->tzName); // Australia/Sydney

Note: optionally, the Date facade could be added to default config app.aliases.

@kylekatarnls kylekatarnls force-pushed the carbon-2 branch 3 times, most recently from 4df8c59 to c731b24 Compare August 24, 2018 12:55
@GrahamCampbell GrahamCampbell changed the title Allow Carbon 2 and custom date handling [5.8] Allow Carbon 2 and custom date handling Aug 24, 2018
@kylekatarnls
Copy link
Contributor Author

Note: the carbon 2 test pass locally but is skipped remotely because Carbon 2 (as a beta right now) does not match neither --prefer-stable nor --prefer-lowest

@@ -8,6 +8,7 @@
trait InteractsWithTime
{
/**
* Get the number of seconds until the given DateTime.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you added this by mistake...

Copy link
Contributor Author

@kylekatarnls kylekatarnls Aug 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mputkowski! Will fix that.

@kylekatarnls kylekatarnls force-pushed the carbon-2 branch 3 times, most recently from 04f1b55 to 67f4c7b Compare August 24, 2018 16:19
@rny
Copy link

rny commented Aug 26, 2018

+1 Carbon 2 has important improvement because it provides toJson(), a standard way of transfer date in JSON. But it is still in beta.

@kylekatarnls
Copy link
Contributor Author

kylekatarnls commented Aug 26, 2018

It will be in beta until an agreement in principle for Laravel integration, so if some little adjustment is needed to ease the integration I can still add it before I publish the stable release.

}
}

if (static::$interceptor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I use ::use(), am I not effectively already "intercepting" the stuff and thus this feature is already covered and not needed?

I understand technically it works differently but when I provide ::use(), I'm effectively already "intercepting" stuff, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it just provide 2 ways to the user, one is easier but limited use, while intercept allow you to run any code to convert your instance.

Copy link
Contributor

@mfn mfn Aug 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO there should be only one clear way; again KISS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It must be simple for the user in priority. Date::use(DateTime::class) is simpler than Date::intercept(function ($date) { return new DateTime($date->format('Y-m-d H:i:s.u'), $date->getTimezone()); }); but Date::use is not enough for custom conversions (change settings, creating the object in an other way). So an $interceptor and a $className, we can convert $className into $interceptor inside the use method, but it's just moving the code to an other place.

So what solution do you propose that would be as easy for the user we it comes to simple class swap and flexible enough for custom conversions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date::swap(new MyOwnDateClass); should be enough. If I want any interception, I can do in my class and the facade should not fallback to Carbon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would create a unique date object for the whole application. How could it possibly work?

@kylekatarnls
Copy link
Contributor Author

Code simplified according to @mfn suggestion to use only one method. Now all can be done with the swap method.

@GrahamCampbell An opinion on this?

@kylekatarnls
Copy link
Contributor Author

Hi @GrahamCampbell, your requested changes has been made. Any news on this?

@taylorotwell
Copy link
Member

Can you explain the __callStatic method of the Date facade a bit? It looks a little complicated.

Also what is the addRealSeconds vs addSeconds?

@kylekatarnls
Copy link
Contributor Author

kylekatarnls commented Sep 14, 2018

addSeconds relies to native PHP DateTime and ignore DST while addRealSeconds use timestamps (ignore timezone). So in a case like cookie expiration: suppose you're in the Chicago timezone or any timezone having a DST. You create a cookie at midnight the 4 of november, and add 3 hours for expiration, in fact this cookie will expire 4 hours later due cloak change (https://www.timeanddate.com/time/change/usa/chicago), but if you use addRealSeconds/addRealMinutes/addRealHours, you get an actual 3 hours expiration.

So addSeconds can be useful in some cases, and most of the time, both will be equivalent (mostly if you work in UTC) but where I changed them addRealSeconds was more relevant. Those methods have been added in Carbon 1.25.0.

The __callStatic allows to pass various inputs to swap:

  • class name, in this case we try to call the static method on it if it exists, else we call it on Carbon then cast the object to the asked class name.
  • closure/callable, we call the static method on Carbon then pass the result to this callable and return what this callable returns.
  • factory, this is the typical facade way.

If when __callStatic is called, there is no facade root yet selected, we set it to Carbon::class.

@kylekatarnls
Copy link
Contributor Author

@taylorotwell No problem, comments added.

if (is_callable($root)) {
// Then we create the date with Carbon then we pass it to this callable that can modify or replace
// This date object with any other value
return $root(Carbon::$method(...$args));
Copy link
Contributor

@deleugpn deleugpn Sep 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature [callable block] appears to be quite inconsistent with Laravel facades. Calling swap should replace the object/class responsible for handling that facade. This is an interceptor / adapter taking action by interpreting a special type of swap, which wasn't a real swap.
Perhaps if people want to intercept their date objects, they should wrap it in their own object / facade.
Real time facades are pretty good at that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deleugpn This provide a way far easier to convert date objects rather than creating an other class with again a __callStatic method to handle all possible static creations and swap to this class. I'm not in favor of a pure deletion of this.

try {
$root = static::getFacadeRoot();
} catch (ReflectionException $exception) {
// We mute reflection exceptions as if have Carbon as fallback if the facade root is missing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following every other facade process, there would be one CarbonServiceProvider responsible for binding the accessor into the container, then we would never have a ReflectionException here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deleugpn Good idea. I'm just wondering the impact of the provider dependency. First with this approach, if you use only some packages (illuminate/database only for example), then you have to run the swap manually to get dates properly created.

Then, right now, the service is optional (only handle default locale changes) and people may have chosen to disable the auto-discovery. These users will also have to call the swap before internal dates start to work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you only use a specific illuminate package, Facades are not suppose to work anyway because it's the Foundation package that bootstrap them.
You cannot disable auto discovery for service providers inside the framework. What I meant was setting this up in a Service Provider inside the framework.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not true, facades works with only illuminate/support dependency, and the same dependency is needed for Carbon. But I see what you mean and will think of it.

@deleugpn
Copy link
Contributor

deleugpn commented Sep 26, 2018

Taylor's idea of comments was great for me to better see my problem with this. Since the beginning I see this Date Facade deviating too much from the usual Laravel Facades.
I guess now I understand the problem comes from the mixture of 2 classes into 1.
The framework itself shouldn't use the facade because packages can be installed as standalone and that makes facades unavailable.
However, the framework needs to wrap the Carbon class in order to provide configuration of it for Laravel users.

I guess if we were to split this up into a wrapper class (non-facade) and provide a facade for the wrapper, this would look much more like any other Laravel component.
The swap method was abused for too many things (FQN, object or callable) whereas it's normally only suppose to take objects.
Having two classes would mean that swap would allow to swap the wrapper class provided by Laravel and the wrapper class would have all the extra functionality of interception and adapter along with its contract.

@mfn
Copy link
Contributor

mfn commented Sep 26, 2018

The framework itself shouldn't use the facade because packages can be installed as standalone and that makes facades unavailable.

Are you sure?

I didn't test myself (and: I'm a user of illuminate/database standalone so this important for me) but @kylekatarnls gave me assurance, see #25320 (comment) .

@kylekatarnls
Copy link
Contributor Author

kylekatarnls commented Sep 26, 2018

@deleugpn is wrong on this particular point "that makes facades unavailable".

Facades are part of the illuminate/support, illuminate/database depends on it. So if you call swap on a facade then the facade can be used. The Date class proposed here allows not to call swap (as it fallback to Carbon).

@mfn I gave the code I used with no problem, you can test it yourself. As you can @deleugpn. I see your point about 2 classes to get a facade that looks more likely other ones as long as it can be done keeping easy tools for Laravel users configuration. I encourage you to test illuminate/support as stand-alone, then swap and run facade. I think the solution should also allow this usage.

@deleugpn
Copy link
Contributor

@mfn Yes, I'm sure. The code that @kylekatarnls prepared only works with Eloquent Models (which have their own __callStatic implementation) and the "Date Facade" that he provided here (which would give ReflectionException if he didn't silenced that).

Illuminate/Support only provides the Facade classes that relies on a container accessor. For that container accessor to be available, it needs to be bootstrapped and registered by the Service Providers. This is done in the Foundation Package. Laravel Zero (by Nuno Maduro) is a great example of how to use Facades outside of Laravel. You have to mimic the work of the Foundation package for that.

This "Date Facade" only works outside of Laravel because it's handling the situation where the Facades have not been bound into the container. This is why I think it should be split into 2 classes: a Wrapper and a Facade. The Wrapper is the thing that would be spread throughout the framework code itself. The facade is just a facilitator for Laravel users. The Wrapper could, then, provide a DateWrapper::intercept() in order to provide the extra functionalities without relying on a "hacky" swap(). The Date::swap() would allow Laravel users to change the DateWrapper into something else.

@kylekatarnls
Copy link
Contributor Author

OK for this method. I will try to find some naming that could be more clear for the user about what he get rather than naming that explains how it works which is not relevant for the user when he want just to customize the date class name to use. DateWrapper::intercept() seems a bit far from that.

@kylekatarnls
Copy link
Contributor Author

@deleugpn My understanding of the following code:

static::$resolvedInstance[static::getFacadeAccessor()] = $instance;

if (isset(static::$app)) {
    static::$app->instance(static::getFacadeAccessor(), $instance);
}

Is that static::$app is not mandatory. So if I well understand this, a facade can be used with no app using only the $resolvedInstance array as singleton storage. And it seems not to me like an abuse, it seems like a well handled case. Then when you resolveFacadeInstance if the singleton exists it will not access to $app so it's not mandatory to bound it into the container.

And it's not Eloquent-related. The ReflectionException try-catch is not about facade mimic, it's only about making the swap call optional but we can do the same with a simple isset(static::$resolvedInstance[$name])

So I will still rely on the following system yet provided by Laravel Facades: use the app as facades container but fallback to the simple static array storage if missing.

@kylekatarnls
Copy link
Contributor Author

According to the @deleugpn idea, I split the facade into 2 classes.

A real facade with no try-catch anymore and no __callStatic override. Just the default class fallback if not set in the resolveFacadeInstance method instead.

And a DateFactory that can handle callable, class name or factory as date handler.

We still can discuss the naming. But here I think we found a good middle-ground between complexity and ease of use.

@taylorotwell taylorotwell merged commit cf55a12 into laravel:master Oct 3, 2018
@mfn
Copy link
Contributor

mfn commented Oct 4, 2018

🎉

@forestlanelabs
Copy link

@kylekatarnls, a bit confused by 2 classes - how do I completely replace Carbon with the standard DateTime? Don't want Carbon handling any dates and don't want to ever fallback to it.

@kylekatarnls kylekatarnls deleted the carbon-2 branch December 7, 2019 10:28
@kylekatarnls
Copy link
Contributor Author

First of all, pure datetime objects can be retrieve from any Carbon instance using ->toDateTime().

Then, in theory:

Date::swap(\DateTime::class);

Would replace Carbon everywhere by DateTime internally.

But Laravel (and many libraries you may have installed) expect to have a Carbon compatible class. For example fresh Eloquent date in Laravel is created with Date::now() which will call Carbon::now() but if you switched to DateTime it calls DateTime::now() which does not exist.

So depending on you usage it actually may work.

But allowing DateTime would require some adjustment to make every internal features in Laravel compatible with it. And some Laravel third-party libraries you have in your project might simply not work with DateTime.

The purpose of Date::swap() is more to extend or switch to an equivalent class and note than Carbon allow you to call the native methods of DateTime, that means for most usage a function that expects a DateTime object will work like a charm receiving a Carbon instance instead. There is a difference in the way it's converted to JSON (DateTime output an object, Carbon a string), but you can easily align it with:

Date::swap(new \Carbon\Factory([
    'toJsonFormat' => function ($date) {
        return $date->toDateTime();
    },
]));

So I would recommend to rely on inheritance using DateTimeInterface typing everywhere and the fact it's actually a Carbon instance or DateTime or any other class that could extend DateTime should make no difference. But when you really need to be sure you're not working with a Carbon object, you can:

function purifyDate(\DateTimeInterface $date): \DateTimeInterface
{
    if ($date instance \Carbon\CarbonInterface) {
        return $date->toDateTime();
    }

    return $date;
}

@deleugpn
Copy link
Contributor

deleugpn commented Dec 7, 2019

@forestlanelabs I may be wrong here, I'm answering from my phone and only from memory. In theory, you can use Laravel's facade to swap the implementation to a brand new class of your choosing, which should probably adhere to php native DateTime object/interface. However, I think Laravel makes the conscious choice of binding itself to Carbon with Eloquent, so you'll have to work around date time features such as $casts and timestamps, I think. If you disable Eloquent Timestamp and never cast anything to date/datetime, you might get away with a lot by never landing on Carbon. Laravel will install Carbon anyway as a dependency, so it's a bit hard for you to handle that completely.
To summarise the idea behind two classes here: The facade is like any Laravel Facade: a statically accessible class that will resolve an implementation from the container for you and proxy any calls to it. The Date class that Laravel has now is a wrapper around Carbon to offer default Carbon configuration for Laravel users, as well as allowing swapping and/or reconfiguration.

mfn added a commit to mfn/laravel-ide-helper that referenced this pull request Dec 21, 2019
Laravel 5.8 introduced a feature to support a custom date class via
`Date::use()`, see laravel/framework#25320

When e.g. using `Date::use(CarbonImmutable)` in a project, it means
all date casts are not returning `\Illuminate\Support\Carbon` anymore
but `\Carbon\CarbonImmutable`, which means all the generated type hints
for dates are now wrong.

This change tries to be still backwards compatible with Laravel < 5.8
which do not have the Date facade.
barryvdh pushed a commit to barryvdh/laravel-ide-helper that referenced this pull request Dec 28, 2019
Laravel 5.8 introduced a feature to support a custom date class via
`Date::use()`, see laravel/framework#25320

When e.g. using `Date::use(CarbonImmutable)` in a project, it means
all date casts are not returning `\Illuminate\Support\Carbon` anymore
but `\Carbon\CarbonImmutable`, which means all the generated type hints
for dates are now wrong.

This change tries to be still backwards compatible with Laravel < 5.8
which do not have the Date facade.
fatihdirikman added a commit to fatihdirikman/Laravel-IDE-Helper that referenced this pull request Jan 7, 2022
Laravel 5.8 introduced a feature to support a custom date class via
`Date::use()`, see laravel/framework#25320

When e.g. using `Date::use(CarbonImmutable)` in a project, it means
all date casts are not returning `\Illuminate\Support\Carbon` anymore
but `\Carbon\CarbonImmutable`, which means all the generated type hints
for dates are now wrong.

This change tries to be still backwards compatible with Laravel < 5.8
which do not have the Date facade.
renaforsberg824 added a commit to renaforsberg824/ide-helper-laravel-developer that referenced this pull request Oct 5, 2022
Laravel 5.8 introduced a feature to support a custom date class via
`Date::use()`, see laravel/framework#25320

When e.g. using `Date::use(CarbonImmutable)` in a project, it means
all date casts are not returning `\Illuminate\Support\Carbon` anymore
but `\Carbon\CarbonImmutable`, which means all the generated type hints
for dates are now wrong.

This change tries to be still backwards compatible with Laravel < 5.8
which do not have the Date facade.
lisadeloach63 added a commit to lisadeloach63/ide-helper-reso-laravel that referenced this pull request Oct 7, 2022
Laravel 5.8 introduced a feature to support a custom date class via
`Date::use()`, see laravel/framework#25320

When e.g. using `Date::use(CarbonImmutable)` in a project, it means
all date casts are not returning `\Illuminate\Support\Carbon` anymore
but `\Carbon\CarbonImmutable`, which means all the generated type hints
for dates are now wrong.

This change tries to be still backwards compatible with Laravel < 5.8
which do not have the Date facade.
sadafrangian3 pushed a commit to sadafrangian3/ide-helper-laravel that referenced this pull request Oct 18, 2022
Laravel 5.8 introduced a feature to support a custom date class via
`Date::use()`, see laravel/framework#25320

When e.g. using `Date::use(CarbonImmutable)` in a project, it means
all date casts are not returning `\Illuminate\Support\Carbon` anymore
but `\Carbon\CarbonImmutable`, which means all the generated type hints
for dates are now wrong.

This change tries to be still backwards compatible with Laravel < 5.8
which do not have the Date facade.
smile1130 added a commit to smile1130/laravel-IDE that referenced this pull request Jun 16, 2023
Laravel 5.8 introduced a feature to support a custom date class via
`Date::use()`, see laravel/framework#25320

When e.g. using `Date::use(CarbonImmutable)` in a project, it means
all date casts are not returning `\Illuminate\Support\Carbon` anymore
but `\Carbon\CarbonImmutable`, which means all the generated type hints
for dates are now wrong.

This change tries to be still backwards compatible with Laravel < 5.8
which do not have the Date facade.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants