extending laravel model

Published on Jun 22, 2024

Gone are the days of hand-writing SQL queries in web apps. Laravel’s Eloquent ORM frees us from query syntax and security details. Though beginners often use where and join, advanced users can leverage features like scopes, accessors, and mutators for more expressive queries.

Another technique to simplify repetitive where statements and local scopes is to extend Eloquent models. By inheriting the parent model's functionality and adding custom methods, scopes, or event listeners, you create a new model with full functionality. This is known as "Model Inheritance" or "Single Table Inheritance".

Here some code

Many web apps feature an "administrator" role, granting elevated permissions and access to restricted areas. To differentiate between a regular user and an admin user, developers often use statements like the following:

     
    User::where('is_admin', true)->get(); 
    

If a particular where statement appears throughout your app, consider using a local scope instead. For example, by adding an isAdmin scope to the User model, you can create a more expressive and reusable Eloquent statement:

    
    $admins = User::isAdmin()->get();

    // Implementation:
    class User extends Model
    {
        public function scopeIsAdmin($query)
        {
            $query->where('is_admin', true);
        }
    }
    

Model inheritance can take the abstraction further by extending the User model with a global scope, resulting in a new entity (Admin) in the app. This entity can include custom methods, scopes, and functionality beyond what was possible before.

    
    class Admin extends User
    {
        protected $table = 'users';

        public static function boot()
        {
            parent::boot();

            static::addGlobalScope(function ($query) {
                $query->where('is_admin', true);
            });
        }
    }
    

Gotchas

Just as Eloquent determines table names from model names, a model's class name is used to determine pivot tables and foreign keys. This can cause issues when accessing relationships from the Admin model.

Eloquent can’t process this relationship because it assumes each Post has an admin_id field instead of a user_id field. We can fix this by explicitly passing the user_id foreign key on the User model:

    
    // Working implementation:
    class Admin extends User {
        // ...stuff
    }

    class User extends Model {
        public function posts() {
            return $this->hasMany(Post::class, 'user_id');
        }

        public function tags() {
            return $this->belongsToMany(Tag::class, 'user_tag', 'user_id');
        }

        // does not work, when called from Admin model
        // because Eloquent assumes admin_id
        public function posts() {
            return $this->hasMany(Post::class);
        }

        // does not work
        public function tags() {
            return $this->belongsToMany(Tag::class);
        }
    }
    

Or you can just use this trait:

Below is a trait that can be used to extend Eloquent models. Thanks to Caleb Porzio for the idea.

    
    trait HasParentModel
    {
        public function getParentClass()
        {
            static $parentClassName;

            if (isset($parentClassName)) {
                return $parentClassName;
            }

            return $parentClassName = (new ReflectionClass($this))->getParentClass()->getName();
        }

        public function getTable()
        {
            $parent = $this->getParentClass();

            if (! isset($this->table)) {
                return str_replace('\', '', Str::snake(Str::plural(class_basename($parent))));
            }

            return $this->table;
        }

        public function getForeignKey()
        {
            $parent = $this->getParentClass();
            $snakedModelName = Str::snake(class_basename($parent));

            return sprintf("%s_%s", $snakedModelName, $this->getKeyName());
        }

        public function joiningTable($related)
        {
            $parent = $this->getParentClass();

            $models = [
                Str::snake(class_basename($related)),
                Str::snake(class_basename($parent)),
            ];

            sort($models);

            return strtolower(implode('_', $models));
        }
    }
    
← Go back