Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel custom model casts

Is there a way in Laravel to add your own custom model casts in addition to the built in ones?

Currently I can use getters and mutators but these end up getting repeated for lots of fields.

like image 661
edlouth Avatar asked Sep 26 '16 20:09

edlouth


People also ask

What is casts in Laravel model?

Casting a value means changing it to (or ensuring it is already) a particular type. Some types you might be familiar with are Integer or Boolean . Simply put, type casting is a method of changing an entity from one data type to another.

What is accessors and mutators in Laravel?

Accessors and mutators allow you to format Eloquent attributes when retrieving them from a model or setting their value. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model.

What is Laravel model attributes?

Laravel model attributes are basically representing the database fields for the given model.

What is Laravel eloquent?

Eloquent is an object relational mapper (ORM) that is included by default within the Laravel framework. An ORM is software that facilitates handling database records by representing data as objects, working as a layer of abstraction on top of the database engine used to store an application's data.


3 Answers

In Laravel 7 and above, this is built in. Custom casts must implement the Illuminate\Contracts\Database\Eloquent\CastsAttributes interface, with a set and get method:

App/Casts/YourCast.php

namespace App\Casts;


use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class YourCast implements CastsAttributes
{

    public function get($model, string $key, $value, array $attributes)
    {
        return json_decode($value,true);
    }

    public function set($model, string $key, $value, array $attributes)
    {
       return json_encode($value);
    }

}

App/Models/YourModel

protected $casts = [
    'attribute' => \App\Casts\YourCast::class,
]
like image 179
Rory Avatar answered Nov 16 '22 09:11

Rory


So I ended up going down the Traits route to override various Model methods, this turns out not to be for the fainthearted as attribute casting is quite deeply embedded into the way models work.

To make this work for the most general case, i.e. being able to easily add custom casts, would require a fairly major rewrite of Model.

Here is the Trait I wrote to add a time cast:

<?php

namespace App\Models;

use Carbon\Carbon;

trait CustomCasts
{
    /**
     * Cast an attribute to a native PHP type.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function castAttribute($key, $value)
    {
        if (is_null($value)) {
            return $value;
        }

        switch ($this->getCastType($key)) {
            case 'int':
            case 'integer':
                return (int) $value;
            case 'real':
            case 'float':
            case 'double':
                return (float) $value;
            case 'string':
                return (string) $value;
            case 'bool':
            case 'boolean':
                return (bool) $value;
            case 'object':
                return $this->fromJson($value, true);
            case 'array':
            case 'json':
                return $this->fromJson($value);
            case 'collection':
                return new BaseCollection($this->fromJson($value));
            case 'date':
            case 'datetime':
                return $this->asDateTime($value);
            case 'timestamp':
                return $this->asTimeStamp($value);
            case 'time':
                return $this->asTime($value);
            default:
                return $value;
        }
    }

    protected function asTime($value)
    {
        // If this value is already a Carbon instance, we shall just return it as is.
        // This prevents us having to re-instantiate a Carbon instance when we know
        // it already is one, which wouldn't be fulfilled by the DateTime check.
        if ($value instanceof Carbon) {
            return $value;
        }

         // If the value is already a DateTime instance, we will just skip the rest of
         // these checks since they will be a waste of time, and hinder performance
         // when checking the field. We will just return the DateTime right away.
        if ($value instanceof DateTimeInterface) {
            return new Carbon(
                $value->format('Y-m-d H:i:s.u'), $value->getTimeZone()
            );
        }

        // If this value is an integer, we will assume it is a UNIX timestamp's value
        // and format a Carbon object from this timestamp. This allows flexibility
        // when defining your date fields as they might be UNIX timestamps here.
        if (is_numeric($value)) {
            return Carbon::createFromTimestamp($value);
        }

        // If the value is in simply year, month, day format, we will instantiate the
        // Carbon instances from that format. Again, this provides for simple date
        // fields on the database, while still supporting Carbonized conversion.
        if (preg_match('/^(\d{1,2}):(\d{2}):(\d{2})$/', $value)) {
            return Carbon::createFromFormat('h:i:s', $value);
        }

        var_dump($value);

        // Finally, we will just assume this date is in the format used by default on
        // the database connection and use that format to create the Carbon object
        // that is returned back out to the developers after we convert it here.
        return Carbon::createFromFormat($this->getTimeFormat(), $value);
    }

    /**
     * Get the format for database stored dates.
     *
     * @return string
     */
    protected function getTimeFormat()
    {
        //return $this->timeFormat ?: $this->getConnection()->getQueryGrammar()->getTimeFormat();
        return $this->timeFormat ?: 'h:i:s';
    }

    /**
     * Set a given attribute on the model.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return $this
     */
    public function setAttribute($key, $value)
    {
        // First we will check for the presence of a mutator for the set operation
        // which simply lets the developers tweak the attribute as it is set on
        // the model, such as "json_encoding" an listing of data for storage.
        if ($this->hasSetMutator($key)) {
            $method = 'set'.Str::studly($key).'Attribute';

            return $this->{$method}($value);
        }

        // If an attribute is listed as a "date", we'll convert it from a DateTime
        // instance into a form proper for storage on the database tables using
        // the connection grammar's date format. We will auto set the values.
        elseif ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) {
            $value = $this->fromDateTime($value);
        }

        elseif ($value && ($this->isTimeCastable($key))) {
            $value = $this->fromTime($value);
        }

        if ($this->isJsonCastable($key) && ! is_null($value)) {
            $value = $this->asJson($value);
        }

        $this->attributes[$key] = $value;

        return $this;
    }

    /**
     * Convert a Carbon Time to a storable string.
     *
     * @param  \Carbon\Carbon|int  $value
     * @return string
     */
    public function fromTime($value)
    {
        $format = $this->getTimeFormat();

        $value = $this->asTime($value);

        return $value->format($format);
    }

    /**
     * Determine whether a value is Date / DateTime castable for inbound manipulation.
     *
     * @param  string  $key
     * @return bool
     */
    protected function isTimeCastable($key)
    {
        return $this->hasCast($key, ['time']);
    }
}
like image 38
edlouth Avatar answered Nov 16 '22 11:11

edlouth


If someone interested, I've recently created Laravel package dealing with this problematics.

You can find the package and instructions here: https://github.com/vkovic/laravel-custom-casts.

like image 26
vkovic Avatar answered Nov 16 '22 11:11

vkovic