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.5] [WIP] Eloquent object casting #18106

Closed
wants to merge 2 commits into from

Conversation

tillkruss
Copy link
Collaborator

@tillkruss tillkruss commented Feb 24, 2017

This is a direct port Taylor's PR (#13706). Related "Internals" discussion is: laravel/ideas#9.

Object casting solves often requested features like:

Objects need to implement two methods (all 3 parameters are optional):

class Foobar
{
    public static function fromModelAttributes($attributes, $key, $model);

    public function toModelAttributes($attributes, $key, $model);
}

The method signatures are slightly different to #13706, because this PR passes the attribute key to both methods, in order to make objects reusable (so attribute keys don't have to be hard coded).

Objects can be set/updated as usual:

$user = User::create([
    'name' => 'Taylor',
    'token' => new Token('larav3l-secr3t')
]);

$user->update([
    'token' => new Token('adam123')
]);

// set object properties
$user->address->line1 = 'Infinite Loop 1';
$user->address->city = 'Cupertino';
$user->address->zip = '95014';
$user->address->region = 'CA';
$user->save();

// set entire object
$user->address = new Address('19111 Pruneridge Avenue', null, 'Cupertino', 'CA', '95014', null);
$user->save();

// unset an object
$user->address = null;
$user->save();
echo $user->address->line1; // null

This however does not work:

$user->token = 'larav3l-secr3t';
$user->save();

You may of course add methods to objects:

$user->address->validate(); // bool

User.php

<?php

class User extends Model
{
    protected $guarded = [];

    protected $casts = [
        'address' => Address::class,
        'token' => Token::class,
        'meta' => Json::class,
    ];
}

Token.php

The token example touches a single model attributes and acts as a accessor/mutator.

<?php

class Token
{
    public $token;

    public function __construct($token)
    {
        $this->token = $token;
    }

    public static function fromModelAttributes($attributes, $key)
    {
        return new static(
            empty($attributes[$key]) ? null : decrypt($attributes[$key])
        );
    }

    public function toModelAttributes($attributes, $key)
    {
        return [
            $key => encrypt($this->token),
        ];
    }
}

Address.php

The address example touches multiple model attributes and has a validate() method for extra functionality.

<?php

class Address
{
    public $line1, $line2, $city, $region, $zip, $country;

    public function __construct(...$address)
    {
        [$this->line1, $this->line2, $this->city, $this->region, $this->zip, $this->country] = $address;
    }

    public function validate()
    {
        return Validator::make($this->toModelAttributes(), [
            'line1' => 'required',
            'city' => 'required',
            'zip' => 'required',
            'country' => 'required',
        ])->passes();
    }

    public static function fromModelAttributes($attributes, $key)
    {
        $address = array_map(function ($key) use ($attributes) {
            return $attributes[$key] ?? null;
        }, ['line1', 'price', 'city', 'region', 'zip', 'country']);

        return new static(...$address);
    }

    public function toModelAttributes()
    {
        return get_object_vars($this);
    }
}

Json.php

The JSON example acts as a little helper to make it easier work with nested JSON data.

<?php

namespace App;

class Json
{
    public $json;

    public function __construct($json = null)
    {
        $this->json = is_array($json) ? json_decode(json_encode($json)) : $json;
    }

    public static function fromModelAttributes($attributes, $key)
    {
        return new static(
            empty($attributes[$key]) ? null : json_decode($attributes[$key])
        );
    }

    public function toModelAttributes($attributes, $key)
    {
        return [
            $key => json_encode($this->json),
        ];
    }

    public function __get($name)
    {
        return $this->json->{$name};
    }

    public function __set($name, $value)
    {
        if (is_null($this->json)) {
            $this->json = (object) [];
        }

        if (is_array($value)) {
            $value = (object) $value;
        }

        $this->json->{$name} = $value;
    }
}

@tillkruss tillkruss changed the title [5.5] [WIP] Eloquent object casting [5.5] Eloquent object casting Feb 25, 2017
@tillkruss tillkruss changed the title [5.5] Eloquent object casting [5.5] [WIP] Eloquent object casting Feb 26, 2017
@laravel laravel unlocked this conversation Feb 26, 2017
@tillkruss tillkruss closed this Feb 26, 2017
@lagbox
Copy link
Contributor

lagbox commented Feb 26, 2017

You know I had some thoughts on this, but they were valueless and this was locked so I forgot them.

@Gummibeer
Copy link
Contributor

Gummibeer commented Aug 21, 2019

I like this PR and would love to see it in the core. But a small adjustment would be great. Adding a fourth $value parameter to toModelAttributes() which contains the passed value to allow your "not supported" example.
For example also a password cast which allows me to do $user->password = 'changeme'; which would play well with $user->fill($request->all());.

And would it be an idea to return another value than an instance of the casting class? For example a PHP primitive or any other class. So use the casting class only as caster and not as data wrapper.

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.

None yet

3 participants