Write less duplicate code using binders in Laravel
- Tutorial
Good time, ladies and gentlemen.
Not so long ago I encountered the phenomenon of duplicate and duplicate code when reviewing a single project in Laravel.
The bottom line is: the system has some internal API structure for AJAX requests, which essentially returns a collection of something from the database (orders, users, quotas, etc ...). The whole point of this structure is to return JSON with the results, no more. In the code review, I counted 5 or 6 classes using the same code, the only difference was in the dependency injection of ResourceCollection, JsonResource, and the model itself. This approach seemed fundamentally wrong to me, and I decided to make my own, as I believe, the correct changes to this code, using the powerful DI that the Laravel Framework provides us with.
So, how did I come to what I’ll talk about later.
I already have about a year and a half of development experience for Magento 2, and when I first encountered this CMS, I was shocked about its DI. For those who do not know: in Magento 2, not a small part of the system is built on the so-called "virtual types". That is, referring to a particular class, we do not always turn to the "real" class. We are referring to a virtual type that has been “assembled” based on a specific “real” class (for example, Collection for the admin grid, assembled through DI). That is, we can actually build any class for use with our dependencies by simply writing something similar in DI:
vendor_table Vendor\Module\Model\ResourceModel\MyData
Now, by requesting the Vendor \ Module \ Model \ ResourceModel \ MyData \ Grid \ Collection class, we get an instance of Magento \ Framework \ View \ Element \ UiComponent \ DataProvider \ SearchResult, but with the mainTable dependencies set to “vendor_table” and resourceModel - “ Vendor \ Module \ Model \ ResourceModel \ MyData. "
At first, such an approach seemed incomprehensible to me, not entirely “appropriate” and not quite normal, however, after a year of development
Back to Laravel.
DI Laravel is built on a “service container” - an entity that manages binders and dependencies in the system. Thus, we can, for example, indicate to the DummyDataProviderInterface interface the implementation of this DummyDataProvider interface itself.
app()->bind(DummyDataProviderInterface::class, DummyDataProvider::class);
Then, when we request DummyDataProviderInterface in the service container (for example, through the class constructor), we get an instance of the DummyDataProvider class.
Many
Laravel can “bind” not only real entities, such as a given interface, but also create so-called “virtual types” (aka aliases). And, even in this case, Laravel does not have to pass a class that implements your type. The bind () method can take an anonymous function as the second argument, with the $ app parameter passed there - an instance of the application class. In general, now we are more going into contextual binding, where what we pass to the class that implements the "virtual type" depends on the current situation.
I warn you that not everyone agrees with this approach to building application architecture, so if you are a fan of hundreds of the same classes, skip this material.
So, for a start we will decide what will act as a "real" class. Using the example of a project that came to me in a code review, let’s take the same situation with resource requests (in fact, CRUD, but a bit cut down).
Let's look at the implementation of a common Crud controller:
model = $model;
$this->resourceCollection = $resourceCollection;
$this->jsonResource = $jsonResource;
}
/**
* Display a listing of the resource.
*
* @param Request $request
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function index(Request $request)
{
return $this->resourceCollection::make($this->model->get());
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function show($id)
{
return $this->jsonResource::make($this->model->find($id));
}
}
I did not bother much with the implementation, because the project is at the planning stage, in fact.
We have two methods that should return something to us: index, which returns a collection of entities from the database, and show, which returns the json resource of a specific entity.
If we used real classes, each time we would create a class containing 1-2 setters that would define classes for models, resources and collections. Imagine dozens of files, of which the truly complex implementation is only 1-2. We can avoid such “clones” using DI Laravel.
So, the architecture of this system will be simple, but reliable as a Swiss watch.
There is a json file that contains an array of "virtual types" with direct reference to the classes that will be used as collections, models, resources, etc ...
For example, this:
{
"Wolf\\Http\\Controllers\\Backend\\Crud\\OrdersResourceController": {
"model": "Wolf\\Model\\Backend\\Order",
"resourceCollection": "Wolf\\Http\\Resources\\OrdersCollection",
"jsonResource": "Wolf\\Http\\Resources\\OrderResource"
}
}
Next, using Laravel binding, we will set Wolf \ Http \ Controllers \ Backend \ Crud \ BaseController as our class for our virtual type Wolf \ Http \ Controllers \ Backend \ Crud \ OrdersResourceController as the implementing class (note that the class should not be abstract, because when requesting Wolf \ Http \ Controllers \ Backend \ Crud \ OrdersResourceController we should get an instance of Wolf \ Http \ Controllers \ Backend \ Crud \ BaseController, not an abstract class).
In CrudServiceProvider, in the boot () method, put the following code:
$path = app_path('etc/crud.json');
if ($this->filesystem->isFile($path)) {
$virtualTypes = json_decode($this->filesystem->get($path), true);
foreach ($virtualTypes as $virtualType => $data) {
$this->app->bind($virtualType, function ($app) use ($data) {
/** @var Application $app */
$bindingData = [
'model' => $app->make($data['model']),
'resourceCollection' => $data['resourceCollection'],
'jsonResource' => $data['jsonResource']
];
return $app->makeWith(self::BASE_CRUD_CONTROLLER, $bindingData);
});
}
}
The constant BASE_CRUD_CONTROLLER contains the name of the class that implements the logic of the CRUD controller.
Far from ideal, but it works :)
Here we go through an array with virtual types and set the binders. Notice that we only get the model instance from the service container, and ResourceCollection and JsonResource are just class names. Why is that? The model does not have to take in the attributes to fill, it can well do without them. But collections must take in some kind of resource from which they will get data and entities. Therefore, in BaseController we use the static methods collection () and make () respectively (in principle, we can add dynamic getters that will put something into the resource and return an instance to us, but I will leave this to you) that will return us instances of these same collections, but with the data transferred to them.
In fact, you can, in principle, bring the entire Laravel binding to such a state.
In total, requesting Wolf \ Http \ Controllers \ Backend \ Crud \ OrdersResourceController we get an instance of the controller Wolf \ Http \ Controllers \ Backend \ Crud \ BaseController, but with built-in dependencies of our model, resource and collection. It remains only to create a ResourceCollection and JsonResource and you can control the returned data.