Laravel 5 Eloquent, How to set cast attribute dynamically
Asked Answered
C

4

8

In laravel 5.1 there is new feature called Attribute Casting, well documented at here : http://laravel.com/docs/5.1/eloquent-mutators#attribute-casting

My question is, it is possible to make attribute casting dynamically ?

for example, I have a table with columns :

id | name          | value       | type    |
1  | Test_Array    | [somearray] | array   |
2  | Test_Boolean  | someboolean | boolean |

it is possible to set value attribute cast, depends on type field, that work both in write(create/update) and fetch ?

Concussion answered 6/8, 2015 at 4:47 Comment(0)
M
7

You'll need to overwrite Eloquent model's getCastType() method in your model class:

protected function getCastType($key) {
  if ($key == 'value' && !empty($this->type)) {
    return $this->type;
  } else {
    return parent::getCastType($key);
  }
}

You'll also need to add value to $this->casts so that Eloquent recognizes that field as castable. You can put the default cast there that will be used if you didn't set type.

Update:

The above works perfectly when reading data from the database. When writing data, you have to make sure that type is set before value. There are 2 options:

  1. Always pass an array of attributes where type key comes before value key - at the moment model's fill() method respects the order of keys when processing data, but it's not future-proof.

  2. Explicitely set type attribute before setting other attributes. It can be easily done with the following code:

    $model == (new Model(['type' => $data['type']))->fill($data)->save();
    
Manes answered 6/8, 2015 at 5:57 Comment(10)
Ah, I like that. Much better than my options. Time for sleep.Itch
yeah looks great. let me try this first.Concussion
Cool :) I havven't tested this exact code, so let me know if you have any issues and I'll get that working for youManes
I am working now, hehehe. what exactly mean about $this->type ? when I try to fetch data using find() and do dd($this->type) inside the function. there is nothing. :/Concussion
I can see type column in your table - this I assume is used to determine the type of cast, right?Manes
oh yes my bad, your code is work on fetch data, but doesn't work when write (create or update). because $this->type is from fetched data.Concussion
Looking at Eloquent model's code there is no other way than making sure that type is set before the value - all methods involved in casting have access only to the current attribute being cast, not to the whole change set. But create/update respects the order in the $attributes array when setting values so you could use that.Manes
Let us continue this discussion in chat.Concussion
very nice chat @Manes you save my day :DConcussion
Plus one for being 100% copy-paste-compatible with my model :DGarbe
N
3

Some of the answers here are really overthinking things, or they're subtly wrong.

The principle is simply to set the $casts property before you need it, eg. before writing or reading properties to the database.

In my case I needed to use a configured column name and cast it. Because PHP doesn't allow function calls in constant expressions, it can't be set in the class declaration, so I just declared my column/property's cast in my model's constructor.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model; 

class MyModel extends Model
{
    protected $casts = [
        // we can't call a function in a class constant expression or
        // we'll get 'Constant expression contains invalid operations'
        // config('my-table.array-column.name') => 'array', // this won't work
    ];

    public function __construct()
    {
        $this->casts = array_merge(
            $this->casts, // we can still use $casts above if desired
            [
                // my column name is configured so it isn't known at
                // compile-time so I have to set its cast run-time;
                // the model's constructor is as good a place as any
                config('my-table.array-column.name') => 'array',
            ]
        );

        parent::__construct(...func_get_args());
    }
}
Noland answered 19/2, 2022 at 21:27 Comment(0)
I
2

The $casts attribute it used whenever you access a field, not when it is fetched from the database. Therefore, you can update the $casts attribute after the model has been populated, and it should work fine whenever you access the value field. You just need to figure out how to update the $casts attribute when the type field is changed.

One potential option would be to override the fill() method so that it calls the parent fill() method first, and then updates the $casts attribute with the data in the type field.

Another potential option would be to abuse the mutator functionality, and create a mutator on the type field, so that whenever it changes, it would update the $casts attribute.

Itch answered 6/8, 2015 at 5:56 Comment(0)
A
0

In my situation (storing some sort of general settings under Laravel 8.x), I ended up using an accessor on the value attribute. Something like next:

/**
 * Get the value attributte.
 *
 * @param  string  $value
 * @return mixed
 */
 public function getValueAttribute($value)
 {
    // Workaround to auto cast the value attribute when accesing it, using
    // the hint provided in the type attribute.

    if (in_array($this->type, ['bool', 'boolean'])) {
        return filter_var($value, FILTER_VALIDATE_BOOL);
    } else if (in_array($this->type, ['int', 'integer'])) {
        $v = filter_var($value, FILTER_VALIDATE_INT);
        return $v === false ? 0 : $v;
    } else if ($this->type == 'float') {
        $v = filter_var($value, FILTER_VALIDATE_FLOAT);
        return $v === false ? 0 : $v;
    } else {
        return $value;
    }
}

Just sharing another alternative...

Aliciaalick answered 26/1 at 15:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.