📜 ⬆️ ⬇️

Expansion of models in Eloquent ORM


We have come a long way since the days when we manually wrote SQL queries in our web applications. Tools such as Laravel Eloquent ORM allow us to work with a database at a higher level, free us from details of a lower level - such as query syntax and security.


When you start working with Eloquent, you will inevitably come to such operators as where and join . For the more advanced ones, there are query stubs (scopes), readers (accessors), mutators (mutators) - offering more expressive alternatives to the old way of constructing queries.


Let's look at other alternatives that can be used as a replacement for the frequently repeated where clause and scopes. This technology is to create a new model of Eloquent that will be inherited from another model. Such a model will inherit all the functionality of the parent model, while retaining the ability to add your own methods, query stubs (scopes), listeners (listeners), etc. Usually this is called Single Table Inheritance, but I prefer to call it Model Inheritance.


Example


Most web applications have the concept of "administrator." The administrator is a regular user with elevated rights and access to the service parts of the application. In order to distinguish regular users from administrators, we write something like this:


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

When the where expression is often repeated in your application, it is useful to replace it with a local query content ( local scope ). By isAdmin query isAdmin into the User model, we can write more expressive and reusable code:


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

Let's go ahead and use model inheritance. Inheriting from the User model and adding a global query preparation, we achieve a more accurate result than we received before, but now with a completely new object. This object ( Admin ) can have its own methods, query stubs, and other functionality.


 $admins = Admin::all(); // : class Admin extends User { protected $table = 'users'; public static function boot() { parent::boot(); static::addGlobalScope(function ($query) { $query->where('is_admin', true); }); } } 

Note: the variable protected $table = 'users' necessary for the proper operation of requests. Eloquent uses the model class name to determine the table name. Therefore, Eloquent assumes that the name of the table is “admins” instead of “users”, which will lead to the error Base table or view not found .

Now that you have the Admin model, it will be easier for you to share the functionality with the User model. For example:


Notifications


Simple operations, such as sending notifications to all administrators, have become easier with the new Admin model.


 Notification::send(Admin::all(), NewSignUp($user)); 

Check


Always when operations with the User model are limited to the administrator, we need to check that the user impersonates the administrator.


 //  if ($admin = User::find($id)->is_admin !== true) { throw new Exception; } $admin->impersonate($user); 

Since the Admin global query stub restricts us to only administrators, the impersonate method can be called immediately for the Admin class.


 Admin::findOrFail($id)->impersonate($user); 

Model Factories


During testing, you may need to create a User model with administrator privileges using the model factory as in the example below.


 $admin = factory(User::class)->create(['is_admin' => true]); // //    $factory->define(User::class, function () { return [ ... 'is_admin' => false, ]; }); 

We can improve this code by adding state to the factory by encapsulating something that the user is an administrator.


 $admin = factory(User::class)->states('admin')->create(); //    $factory->state(User::class, 'admin', function () { return ['is_admin' => true]; }); 

It was definitely better, but we still get an instance of the User model. Having defined a new factory for the Admin model, we will also get a user with administrator rights, but now the factory will return an instance of the Admin model.


 $admin = factory(Admin::class)->create(); //    $factory->define(Admin::class, function () { return ['is_admin' => true] + factory(User::class)->raw(); }); 

Relationships don't work.


In the same way that Eloquent defines table names, model class names are used to define foreign keys and intermediate tables. Consequently, access to the relationships from the Admin model is problematic.


 Admin::first()->posts; //  : Unknown column 'posts.admin_id' //   : class Admin extends User { // } class User extends Model { public function posts() { return $this->hasMany(Post::class); } } 

Eloquent cannot access the relation as it assumes that each instance of the Post model has the admin_id field instead of the user_id field. We can fix this by passing the user_id foreign key in the User model:


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

The same problem exists in the attitude of many to many. Eloquent assumes that the name of the intermediate table corresponds to the name of the current class of the model:


 Admin::first()->tags; //  : Table 'admin_tag' doesn't exist //   : class Admin extends User { // } class User extends Model { public function tags() { return $this->belongsToMany(Tag::class); } ... 

We can also solve this problem by explicitly specifying the name of the pivot table and the name of the remote key:


 //  : class Admin extends User { // } class User extends Model { public function tags() { return $this->belongsToMany(Tag::class, 'user_tag', 'user_id'); } ... 

Although the explicit definition of remote keys and summary tables allows the Admin model to access the User model relationships, this solution is far from ideal. The existence of these seemingly unnecessary definitions does not improve our code.


However, you can create a HasParentModel HasParentModel that will automatically solve this problem. This trait will replace the model class name with the class name of the parent model. GitHub trait code.


You can go further and make Laravel work better with one-table inheritance. We have created a package that will simplify the creation of models in your Laravel application, and are ready to release it from day to day. Follow our tweeter to not miss the announcement!

Let's look at the new model Admin that uses this treit:


 use App\Abilities\HasParentModel; class Admin extends User { use HasParentModel; //    : protected $table = 'users' public static function boot() { parent::boot(); static::addGlobalScope(function ($query) { $query->where('is_admin', true); }); } } 

Now, our User model relationships can return to the state when they relied on default values.


 //  : class User extends Model { public function posts() { return $this->hasMany(Post::class); } public function tags() { return $this->belongsToMany(Tag::class); } } 

Trace HasParentModel clears our model and lets the developer understand that something special is happening inside it.


Model inheritance


We identified the general characteristics of the Eloquent model and made them cleaner using their inheritance. This technology allows us to create better object names and encapsulate them in our application. Remember that inheritance is available for all Eloquent models, not just for Users and Admins . The possibilities are endless!


Be creative, have fun and share your knowledge. Share with me how you use this pattern in your projects! (Tweeter @calebporzio and @tightenco )


Good luck!


')

Source: https://habr.com/ru/post/344728/


All Articles