Skip to content

Conversation

@DarkGhostHunter
Copy link
Contributor

@DarkGhostHunter DarkGhostHunter commented Aug 19, 2025

What?

This adds the ability for a Model to cast an attribute using a Stringable object rather than a string. This allows for expressive casting instead of static methods trying to shove in all arguments possible without hints (and no warranties over consistent argument names between versions).

The idea is to let these complex Casters to call an auxiliary class or methods that handles the configuration, a Castable builder of sorts. It can even be the cast class itself.

// Before
public function casts()
{
    return [
        // true? null? why? time to read the source code?
        'cart' => AsCart::fromColumns('items', true, null),
    ];
}

// After
public function casts()
{
    return [
        // Oh... now I understand!
        'cart' => AsCart::from('items')->withDiscounts()->withoutExpiration(),
    ]
}

The way it works is relatively simple: if we're passing an object that implements Stringable, we will use its string representation as a cast, which most of the time will be manually done by the developer.

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Closure;

class AsCart implements CastsAttributes, Stringable
{
    public function __construct(public $column, public $discounts, public $expires)
    {
    
    }

    public function withDiscounts()
    {
        $this->discounts = true;
        
        return $this;
    }
    
    public function withoutExpiration()
    {
        $this->expires = null;
        
        return $this;
    }
    
    public function __toString(): string
    {
        return static::class . ':' . implode(',', get_class_vars($this));
    }
    
    public static function from(string $column = 'items')
    {
        return new static($column);
    }
}


public function casts()
{
    return [
        'cart' => AsCart::from('items')->withDiscounts()->withoutExpiration()
        // It will be set as "\App\Casts\AsCart:items,1,"
    ]
}

The magic behind all of this new match arm in ensureCastsAreStringValues(), that works when the cast() method of the Model is called. It detects if the cast is an object and proceeds as previously explained. In a nutshell:

is_object($cast) => value(function () use ($cast) {
    // If the object is a Stringable, then let the developer configure the cast.
    if ($cast instanceof Stringable) {
        return (string) $cast;
    }

    // It's just an object? Warn the developer.
    throw new InvalidArgumentException(
        "The cast object for the $attribute attribute must implement Stringable or be declared as string."
    );
})

@github-actions
Copy link

Thanks for submitting a PR!

Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

@DarkGhostHunter DarkGhostHunter marked this pull request as ready for review August 19, 2025 15:58
@DarkGhostHunter DarkGhostHunter changed the title [12.x] Adds Cast as class instances [12.x] Ensures casts objects can be transformed into strings Aug 19, 2025
@shaedrich
Copy link
Contributor

Now, you have named arguments, but the position might not immediately clear. I can definitely see your point, but I'm not sure whether that's the ideal implementation (yet).

@DarkGhostHunter
Copy link
Contributor Author

DarkGhostHunter commented Aug 20, 2025

Now, you have named arguments, but the position might not immediately clear. I can definitely see your point, but I'm not sure whether that's the ideal implementation (yet).

Same as with route middleware declarations with stringables at #39439. The main idea is to allow fluent cast declaration when there is more than one parameter.

This would enable a built-in Illuminate\Database\Eloquent\Cast class to handle cast more expressively and flexibly. For example, I can make the case for casting a datetime with a custom timezone from a a numeric Unix Epoch, for example.

use Illuminate\Database\Eloquent\Cast;

public function casts()
{
    return [
        'retired_at' => Cast::asDatetime()->fromTimestamp()->tz('America/NewYork'),
    ];
}

@taylorotwell
Copy link
Member

I don't follow the CastsAttributes example at all tbh.

@taylorotwell taylorotwell marked this pull request as draft August 21, 2025 19:38
@DarkGhostHunter
Copy link
Contributor Author

I think the CastAttributes can be unreliable because the developer could use constructor arguments totally different from the public properties that are being captured by get_class_vars().

So I gave it a thought and I made it much simpler: if the object is Stringable, it will use what __toString() returns.

That could suffice since it gives total control to the developer on the cast configuration and declaration, much like happen with middleware builders.

@DarkGhostHunter DarkGhostHunter marked this pull request as ready for review August 22, 2025 20:05
@taylorotwell taylorotwell merged commit 47e6d0f into laravel:12.x Aug 26, 2025
57 of 60 checks passed
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.

3 participants