https://laravel.com/docs/8.x/routing#route-model-binding
I want to find another way to do the route model binding but more dynamic. I find Laravel's route model binding (implicit and explicit) a bit limited for the way I want to use it.
On my code I have a lot of Controllers' that have methods that begin with the following:
if (!$model = Model::with('Relation')->find($id)) {
return back()->withErrorMsg('Something went wrong')
}
OR
if (!$model = Model::with('Another Relation')->withTrashed()->find($id)) {
return back()->withErrorMsg('Something went wrong')
}
OR
if (!$model = Model::find($id)) {
return back()->withErrorMsg('Something went wrong')
}
Instead of repeating these blocks of code on a each method I want this to be done before reaching the controller. If the record is not found then have my own redirect logic if the record is found pass the model to the controller (similar way to what Laravel does) but with more control.
Laravel's route model binding will just find the record or throw an exception and if you need any relations you have to do extra call to the DB which is not ideal. Even if you override the UrlRoutable resolveRouteBinding method you wont be able to cover all the other scenarios I have above unless you create multiple model classes that extend the base model with different resolveRouteBinding logic. Also there is t no place to define your own redirect logic if the model is not found.
I tried to do this in a similar way that Laravel's does the Form Requests but the problem is that at the end the class that is passed to the controller is an object of BatchWithTrashedBinding and in order to get the model is to do the $binding->model
class ModelBinding implements FindModelWhenResolved
{
use FindModelWhenResolvedTrait;
/**
* The container instance.
*
* @var Container
*/
protected $container;
/**
* The request instance.
*
* @var Request
*/
protected $request;
/**
* The route key.
*
* @var string $key
*/
protected $key;
/**
* The model to bind.
*
* @var string
*/
protected $model;
/**
* @var Closure
*/
protected $query;
/**
* Begin querying a model with eager loading.
*
* @var array|string $relations
*/
protected $relations = [];
/**
* Throw model not found exception.
*/
protected function modelNotFound()
{
throw (new ModelNotFoundException())
->redirectTo($this->redirect());
}
/**
* Get the URL to redirect to on a validation error.
*
* @return RedirectResponse|JsonResponse
*/
protected function redirect()
{
return back();
}
/**
* Set the Request instance.
*
* @param Request $request
* @return $this
*/
public function setRequest(Request $request): ModelBinding
{
$this->request = $request;
return $this;
}
/**
* Set the container implementation.
*
* @param Container $container
* @return $this
*/
public function setContainer(Container $container): ModelBinding
{
$this->container = $container;
return $this;
}
/**
* Get the route value.
*
* @return Route|object|string
*/
protected function getRouteValue()
{
return $this->request->route($this->key);
}
}
trait FindModelWhenResolvedTrait
{
/**
* Find the model of the resolved instance.
*/
public function findResolved() {
$query = $this->prepareNewQuery();
$this->prepareAdditionalQueries($query, $this->setQueries());
if (!$model = $this->find($query)) {
$this->modelNotFound();
} else {
$this->model = $model;
}
}
/**
* Prepare the instance.
*
* @return mixed
*/
private function prepareNewQuery()
{
$instance = $this->container->make($this->model);
return $instance->newQuery();
}
/**
* Prepare the instance queries.
*
* @param Builder $query
* @param Closure $closure
*/
private function prepareAdditionalQueries(Builder $query, Closure $closure)
{
$closure($query->newQuery());
}
/**
* Find the model.
*
* @param Builder $query
* @return mixed
*/
private function find(Builder $query)
{
$value = $this->getRouteValue();
return $query->where($query->getModel()->getRouteKeyName(), $value)->first();
}
/**
* Set queries to the model.
*
* @return Closure
*/
protected function setQueries(): Closure
{
return function () {};
}
}
class ModelWithTrashedBinding extends ModelBinding
{
/**
* Define the route key.
*
* @var string $key
*/
protected $key = 'model_id';
/**
* The model instance.
*
* @var Model
*/
public $model = Model::class;
/**
* Where to redirect.
*
* @return RedirectResponse|JsonResponse
*/
public function redirect()
{
$value = $this->getRouteValue();
if ($this->request->wantsJson()) {
return response()->json([
'return' => false,
'message' => 'Something went wrong'
]);
}
return redirect()->route('a route name')
->withErrorMsg('Something went wrong');
}
/**
* Set additional queries.
*
* @return Closure
*/
protected function setQueries(): Closure
{
return function(Builder $query) {
$query->with('a relation')->withTrashed();
};
}
}
AppServiceProvider
/**
* Boot the application events.
*
* @return void
*/
public function boot()
{
$this->app->afterResolving(FindModelWhenResolved::class, function ($resolved, $app) {
$resolved->findResolved();
});
$this->app->resolving(ModelBinding::class, function ($model, $app) {
$model->setContainer($app)->setRequest($app->make(Request::class));
});
}
Then I use it like this on a controller.
class ExampleController extends Controller
{
/**
* Update a model.
*
* @param Request $request
* @param BatchWithTrashedBinding $binding
* @param $id
* @return RedirectResponse
*/
public function update(Request $request, BatchWithTrashedBinding $binding, $id): RedirectResponse
{
return $this->doUpdate($request, $binding->model, $id);
}
}