Good time, ladies and gentlemen.
Not so long ago, I encountered the phenomenon of duplicate and repetitive code with a code review of a single project on Laravel.
The point is this: the system has some internal API structure for AJAX requests, in effect returning a collection of something from the database (orders, users, quotas, etc ...). The whole essence of this structure is to return JSON with results, no more. With code review, I counted 5 or 6 classes using the same code, the difference was only in the ResourceCollection, JsonResource dependency injection, and the model itself. This approach seemed to me fundamentally wrong, and I decided to make my own, as I believe, correct changes to this code, using the powerful DI that the Laravel Framework provides us with.
So, how did I come to what I will discuss next.
')
I have about a year and a half of development experience with Magento 2, and when I first encountered this CMS, I was shocked about her 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 refer to the "real" class. We refer to a virtual type that was “assembled” on the basis of a certain “real” class (for example, the Collection for the admin grid, collected via DI). That is, we can actually build any class for use with our dependencies, simply by writing something like this into DI:
<virtualType name="Vendor\Module\Model\ResourceModel\MyData\Grid\Collection" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult"> <arguments> <argument name="mainTable" xsi:type="string">vendor_table</argument> <argument name="resourceModel" xsi:type="string">Vendor\Module\Model\ResourceModel\MyData </argument> </arguments> </virtualType>
Now, by querying the Vendor \ Module \ Model \ ResourceModel \ MyData \ Grid \ Collection class, we will get an instance of Magento \ Framework \ View \ Element \ UiComponent \ DataProvider \ SearchResult, but with the dependencies of the mainTable - and Vendor \ Module \ Model \ ResourceModel \ MyData ".
At first, such an approach seemed to me not entirely clear, not quite “pertinent” and not quite normal, but after a year of working out
for this ball , I, on the contrary, became committed to this approach, and, moreover, I found use for it in my projects .
We return to Laravel.
DI Laravel is built on a “service container” —the entity that manages the bindings and dependencies in the system. Thus, we can, for example, specify the DummyDataProviderInterface interface to be quite an implementation of this DummyDataProvider interface.
app()->bind(DummyDataProviderInterface::class, DummyDataProvider::class);
Then, when we request the DummyDataProviderInterface in the service container (for example, through the class constructor), we get an instance of the DummyDataProvider class.
Many
(for some reason) end up with this knowledge of the Laravel service container and go to do their own, much more interesting things,
but in vain .
Laravel can “bandage” not only real entities, such as this interface, but also create so-called “virtual types” (as well as aliases). And even in this case, Laravel does not have to pass a class that implements your type. The second argument bind () method can take an anonymous function, with the $ app parameter passed to it - an instance of the application class. In general, now we are moving more 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 identical classes, skip this material.
So, to begin with, we will define what will act as a “real” class. Using the example of a project that got me on the code review, let's take the same situation with resource requests (in fact, CRUD, but a bit truncated).
Let's look at the implementation of a common Crud controller:
<?php namespace Wolf\Http\Controllers\Backend\Crud; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Wolf\Http\Controllers\Controller; class BaseController extends Controller { protected $model; protected $resourceCollection; protected $jsonResource; public function __construct( $model, $resourceCollection = null, $jsonResource = null ) { $this->model = $model; $this->resourceCollection = $resourceCollection; $this->jsonResource = $jsonResource; } public function index(Request $request) { return $this->resourceCollection::make($this->model->get()); } 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 stage, in fact, planning.
We have two methods that should return something to us: index, which returns a collection of entities from the database, and show, which returns a json-resource of a specific entity.
If we used real classes, we would create a class each time that contains 1-2 setters that would set classes for models, resources, and collections. Imagine dozens of files, of which the truly complex implementation is only 1-2. To avoid such "clones" we can, 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 a direct reference to the classes that will be used as collections, models, resources, etc ...
For example, such:
{ "Wolf\\Http\\Controllers\\Backend\\Crud\\OrdersResourceController": { "model": "Wolf\\Model\\Backend\\Order", "resourceCollection": "Wolf\\Http\\Resources\\OrdersCollection", "jsonResource": "Wolf\\Http\\Resources\\OrderResource" } }
Next, using the Laravel binding, we will specify for our virtual type Wolf \ Http \ Controllers \ Backend \ Crud \ OrdersResourceController as the implementing class our base coot controller Wolf \ Http \ Controllers \ Backend \ Crud \ BaseController (note that 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, put the following code in the boot () method:
$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) { $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 bindings. Notice that from the service container we get only an instance of the model, and the ResourceCollection and JsonResource remain just the names of the classes. Why is that? The model does not have to take attributes to fill in, it can easily do without them. But collections must take in some kind of a resource from which they will get data and entities. Therefore, in BaseController we use static methods collection () and make (), respectively (in principle, we can add dynamic getters that will put something into the resource and return us an instance, but I will leave it to you), which will return us instances of these the same collections, but with the data passed to them.
In fact, you can, in principle, the entire binding of Laravel bring to this state.
Total, by requesting Wolf \ Http \ Controllers \ Backend \ Crud \ OrdersResourceController we get an instance of the Wolf \ Http \ Controllers \ Backend \ Crud \ BaseController controller, but with the built-in dependencies of our model, resource and collection. It remains only to create a ResourceCollection and JsonResource and you can manage the returned data.