briannesbitt / Carbon

A simple PHP API extension for DateTime.
https://carbon.nesbot.com/
MIT License
16.58k stars 1.28k forks source link

Globally set CarbonInterval::toStringFormat() #2656

Closed Max13 closed 2 years ago

Max13 commented 2 years ago

Hello,

Is there a way to globally change the CarbonInterval::__toString() format? I'm using CarbonInterval in a generic class which type juggle to string when an attribute is Stringable.

Since PHP 8, CarbonInterval is Stringable, so will be converted to string in my class, which currently defaults to $this->forHuman(). I would like to set globally something like:

CarbonInterval::setToStringFormat(function ($interval) {
    return $interval->spec();
});

// OR

CarbonInterval::settings([
    'toStringFormat' => function ($interval) {
        return $interval->spec();
    }
]);

Is there a way to do that please?

Carbon version: 2.62.0

PHP version: 8.1.0

Thanks!

@kylekatarnls edit: replacing CarbonInterface occurrences with CarbonInterval as it sounds it was the intended meaning.

kylekatarnls commented 2 years ago

Hello 👋

We might implement a global setToStringFormat for consistency with Carbon date time class, but please keep in mind that it comes with some unexpected risks that are hard to anticipate.

For instance you might have sub-sub-dependency you're not even aware of that is casting CarbonInterval and relies on the forHumans() output to make some parsing, generating content for mail or whatever. If you set a global format for all CarbonInterval, it will apply to all casts to string in your own code base but also all the casts in your vendor code and you probably don't control 100% of that code, plus it might change when you update dependencies. And last the resulting bugs would not be easy to catch as they would probably not throw exceptions but more likely just cause unwanted behavior changes.

So even if we implement it, I recommend to stick with non-static settings() calls as much as you can so it's scoped to your source code and don't apply to vendor code.

Maybe you can:

class IntervalService
{
    public function wrapInterval(mixed $value): CarbonInterval
    {
        if (!($value instanceof CarbonInterval)) {
            return $value;
        }

        return $value->settings([
            'toStringFormat' => static fn($interval) => $interval->spec(),
        ]);
    }

    public function __call(string $name, array $args)
    {
        return $this->wrapInterval(CarbonInterval::$name(...$args));
    }
}

$interval = new IntervalService();

echo $interval->fromString('2 days 5 hours'); // P1DT5H
echo $interval->wrapInterval(Carbon::parse('2022-08-27')->diffAsCarbonInterval('2022-08-29')); // P2D

This way instead of directly creating CarbonInterval objects from static constructors, you create them via your application factory (or wrap a result) which is then responsible for giving the settings they need for your app.

This is also more scalable as if you need at some point to extract part of your code to an other application, you won't have the global settings to be ported, or if you merge code from other place that would rely on a different __toString it won't collide.

Max13 commented 2 years ago

@kylekatarnls I agree with you, thank you for pointing it out and thank you for the edit. Currently, what I could do (taking your comment into account) is:

// Laravel Cast class
public function set($model, string $key, $value, array $attributes)
{
    CarbonInterval::setToStringFormat(function (CarbonInterval $interval) {
        return $interval->spec();
    });

    $casted = $value->toJson();

    CarbonInterval::setToStringFormat(null);

    return is_null($value) ? null : $value->toJson();
}

Also, I can setToStringFormat() on creation, because once the above $value exists, I can't know what it contains but the wrapper is a great idea, thank you

kylekatarnls commented 2 years ago

->settings() and ::setToStringFormat() will work in 2.63.0 on CarbonInterval and CarbonPeriod as they do now for Carbon and CarbonImmutable.

You can test it using composer require "nesbot/carbon:dev-master as 2.63.0"