Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

carstenjaksch's avatar

Global mutator or sanitizer for all models?

Hey,

is there a way to mutate or sanitize properties of all models without writing a mutator for every property in every model?

I tried model events and writing my own BaseModel class to update the properties on all models that extend my new model class. But Filament does mass updates and model events do not trigger in that case.

Background: I want to apply Str::squish() on all string properties of all models.

0 likes
6 replies
LaryAI's avatar
Level 58

To achieve global mutation or sanitization of properties for all models in Laravel, you can use a combination of a custom base model and overriding the setAttribute method. This approach ensures that any time a property is set on any model, your custom logic will be applied.

Here's a step-by-step solution:

  1. Create a BaseModel Class: Create a new base model class that extends Illuminate\Database\Eloquent\Model. This will be the base class for all your models.

    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    use Illuminate\Support\Str;
    
    class BaseModel extends Model
    {
        /**
         * Set a given attribute on the model.
         *
         * @param  string  $key
         * @param  mixed  $value
         * @return $this
         */
        public function setAttribute($key, $value)
        {
            // Apply Str::squish() to all string attributes
            if (is_string($value)) {
                $value = Str::squish($value);
            }
    
            return parent::setAttribute($key, $value);
        }
    }
    
  2. Extend Your Models from BaseModel: Ensure that all your models extend this new BaseModel instead of the default Model.

    namespace App\Models;
    
    class User extends BaseModel
    {
        // Your model code here
    }
    
    namespace App\Models;
    
    class Post extends BaseModel
    {
        // Your model code here
    }
    
  3. Handle Mass Assignment: Since you mentioned that Filament does mass updates and model events do not trigger in that case, you need to ensure that the setAttribute method is used during mass assignment. Laravel's mass assignment uses the fill method, which in turn uses setAttribute.

    If you encounter any issues with mass assignment, you can override the fill method in your BaseModel to ensure your custom logic is applied:

    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    use Illuminate\Support\Str;
    
    class BaseModel extends Model
    {
        /**
         * Set a given attribute on the model.
         *
         * @param  string  $key
         * @param  mixed  $value
         * @return $this
         */
        public function setAttribute($key, $value)
        {
            // Apply Str::squish() to all string attributes
            if (is_string($value)) {
                $value = Str::squish($value);
            }
    
            return parent::setAttribute($key, $value);
        }
    
        /**
         * Fill the model with an array of attributes.
         *
         * @param  array  $attributes
         * @return $this
         */
        public function fill(array $attributes)
        {
            foreach ($attributes as $key => $value) {
                $this->setAttribute($key, $value);
            }
    
            return $this;
        }
    }
    

By following these steps, you ensure that all string properties of all models are sanitized using Str::squish() whenever they are set, whether through individual assignments or mass assignments.

carstenjaksch's avatar

@LaryAI Tried overriding both setAttribute and fill methods. Does not work unfortunately.

martinbean's avatar

@carstenjaksch Why do you need this? Laravel has TrimStrings middleware that will be doing this automatically already.

carstenjaksch's avatar

@martinbean Thanks for mentioning this middleware. It does not squish the strings, only trims them, but is a good starting point. I tried to implement my own middleware based on the TrimStrings one, but Livewire (Filament uses that) has an issue with the squished value:

Livewire encountered corrupt data when trying to hydrate a component. Ensure that the [name, id, data] of the Livewire component wasn't tampered with between requests.

I dumped all values before and after – they are exactly the same. Don't know what's the issue here.

carstenjaksch's avatar

@martinbean It is basically a copy of the TrimStrings middleware:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Foundation\Http\Middleware\TransformsRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class SquishStrings extends TransformsRequest
{
    /**
     * The attributes that should not be trimmed.
     *
     * @var array<int, string>
     */
    protected $except = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * The globally ignored attributes that should not be squished.
     *
     * @var array
     */
    protected static $neverSquish = [];

    /**
     * All of the registered skip callbacks.
     *
     * @var array
     */
    protected static $skipCallbacks = [];

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        foreach (static::$skipCallbacks as $callback) {
            if ($callback($request)) {
                return $next($request);
            }
        }

        return parent::handle($request, $next);
    }

    /**
     * Transform the given value.
     *
     * @param  string  $key
     * @param  mixed  $value
     * @return mixed
     */
    protected function transform($key, $value)
    {
        $except = array_merge($this->except, static::$neverSquish);

        if (
            $this->shouldSkip($key, $except) || ! is_string($value)) {
            return $value;
        }

        // dd($key, $value);

        return Str::trim($value);
    }

    /**
     * Determine if the given key should be skipped.
     *
     * @param  string  $key
     * @param  array  $except
     * @return bool
     */
    protected function shouldSkip($key, $except)
    {
        return in_array($key, $except, true);
    }

    /**
     * Indicate that the given attributes should never be trimmed.
     *
     * @param  array|string  $attributes
     * @return void
     */
    public static function except($attributes)
    {
        static::$neverSquish = array_values(array_unique(
            array_merge(static::$neverSquish, Arr::wrap($attributes))
        ));
    }

    /**
     * Register a callback that instructs the middleware to be skipped.
     *
     * @return void
     */
    public static function skipWhen(Closure $callback)
    {
        static::$skipCallbacks[] = $callback;
    }

    /**
     * Flush the middleware's global state.
     *
     * @return void
     */
    public static function flushState()
    {
        static::$neverSquish = [];

        static::$skipCallbacks = [];
    }
}

I renamed $neverTrim to $neverSquish and replaced Str::trim($value) with Str::squish($value). in transform().

Then, I replaced TrimString with my middleware:

// bootstrap/app.php
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->use([
            // \Illuminate\Http\Middleware\TrustHosts::class,
            \Illuminate\Http\Middleware\TrustProxies::class,
            \Illuminate\Http\Middleware\HandleCors::class,
            \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
            \Illuminate\Http\Middleware\ValidatePostSize::class,
            // \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
            \App\Http\Middleware\SquishStrings::class,
            \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        ]);
    })

Please or to participate in this conversation.