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));
}
}