Dependency injection on nested resources #806
-
I'm trying to load For example, this nested resource: I tried this controller: // CommentController.php
public function index(CommentIndexQuery $query)
{
return $query->get();
} With this QueryBuilder: class CommentIndexQuery extends QueryBuilder
{
public function __construct(Request $request, Book $book)
{
$query = $book->comments();
parent::__construct($query, $request);
}
} But it didn't work. Current solutionWhat works for me is to manually load the query builder and manually pass the nested model: // CommentController.php
public function index(Request $request, Book $book)
{
$query = new CommentIndexQuery($request, $book);
return $query->get();
} Although this works, it would be great to use dependency injection to load the query builder. Am I doing it wrong, or there's no way to currently do this? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
Here's my solution in case anyone is interested: <?php
// app/Providers/QueryBuilderRouteBindingServiceProvider.php
namespace App\Providers;
use Illuminate\Container\Container;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Routing\ImplicitRouteBinding;
use Illuminate\Routing\Route;
use Illuminate\Support\ServiceProvider;
use ReflectionClass;
use ReflectionMethod;
use Spatie\QueryBuilder\QueryBuilder;
class QueryBuilderRouteBindingServiceProvider extends ServiceProvider
{
public function boot() : void
{
$this->registerRouteBindingResolver();
}
public function registerRouteBindingResolver() : void
{
$this->app->beforeResolving(QueryBuilder::class, function($className) {
$this->app->bind($className, $this->factoryRouteBindingResolver($className));
});
}
protected function factoryRouteBindingResolver(string $className) : callable
{
return function() use ($className) {
$reflection = new ReflectionClass($className);
$arguments = $this->resolveDependenciesWithRouteBinding($reflection->getConstructor(), request());
return $reflection->newInstanceArgs($arguments);
};
}
protected function resolveDependenciesWithRouteBinding(ReflectionMethod $method, Request $request) : array
{
$controllerRoute = $request->route();
$methodRoute = (new Route(
methods: $controllerRoute->methods,
uri: $controllerRoute->uri,
action: [$method->class, $method->name],
))
->withTrashed($controllerRoute->allowsTrashedBindings())
->setContainer($this->getContainer())
->bind($request);
ImplicitRouteBinding::resolveForRoute($this->getContainer(), $methodRoute);
return $methodRoute->resolveMethodDependencies(
$methodRoute->parametersWithoutNulls(),
$method,
);
}
/** @return Application|Container */
protected function getContainer() : mixed
{
return $this->app;
}
} Then, add on /*
* Application Service Providers...
*/
/* ... */
App\Providers\QueryBuilderRouteBindingServiceProvider::class, About this solutionWith Example, loading a model: class CommentIndexQuery extends QueryBuilder
{
public function __construct(Request $request, Book $book)
{
$query = $book->comments();
parent::__construct($query, $request);
}
} This specific type of dependency injection that loads data from the router, including models, is called "Route Model Binding". That's what this solution does. It provides a "Route Model Binding" for extended |
Beta Was this translation helpful? Give feedback.
-
I'm writing here the path I took to implement this code just in case I need to revisit it in the future or if anyone is interested in understanding the code. 1st Attempt: Laravel ContainerMy first thought was to use Laravel Containers. We could do something like this: // AppServiceProvider.php
public function register()
{
$this->app->bind(QueryBuilder::class, function () {
// return an instance of the query builder. Ex. `new BookIndexQuery(...);`
});
} The problem with this solution is that we have to analyze the class constructor parameters and manually inject them. It seemed a lot of work to me. 2nd Attempt: Contextual BindingInstead of binding to the query builder class, what if we monitor just its parameters? More specifically, the models. We could do it with Contextual Binding: // AppServiceProvider.php
public function register()
{
$this->app
->when(QueryBuilder::class)
->needs(Model::class)
->give(function ($app) {
// then, return the model instance with the specified key. Ex. `Model::find(id)`
});
} I like this approach; it looks simple and clean, but unfortunately, it doesn't work. We can't use A solution for this could be to find all model classes by calling That sounded like a good idea, but the problem with this solution is that 3rd Attempt: Monitoring the parent class with
|
Beta Was this translation helpful? Give feedback.
Here's my solution in case anyone is interested: