Search code examples
phplaraveleloquentlamp

Conditional Dependency Injection of different possible Eloquent models into controller method


In my Laravel project, which is meant for looking for buildings rents, I'm using an Eloquent model with a polymorphic relationship to store qualifications, both for users and rents. So then, I'm having three relevant models for this issue: 'User', 'Alquiler' (rent), and 'Calificacion' (qualification), having the polymorphic relationships defined in them.

In my 'Alquiler' model, I have defined:

 public function getRouteKeyName()
    {
        return 'slug';
    }

So when I have a route defined like this:

GET|HEAD    alquileres/{alquiler} .... alquileres.show › AlquilerController@show

In my 'AlquilerController' I can do:

public function show(Alquiler $alquiler){
    return view('alquileres.show',compact('alquiler'));
}

To get easily the model instance correspondent to that published rent, having in my Blade view:

<a href="{{route('alquileres.show',$alquiler)}}">...</a>

So, the rendered link looks like: 'http://localhost:8000/alquileres/capital-federal-arcos-3130-7-a'

In my 'User' model, I have not defined the 'getRouteKeyName()' method, so this one it's just the default ID field.

Despite I can easily perform a dependency injection both for 'User' and 'Alquiler' that way, I'm not being able achieve something similar when the model that must be recieved in the controller method, can be either 'User' or 'Alquiler', depending of the first parameter defined in my route, so I've coded this:

web.php:

Route::get('/calificaciones/calificar/{modelo}/{instancia}',  [CalificacionesController::class,'calificar'])->name('calificaciones.calificar');

Route::post('/calificaciones/guardarCalificacion/{modelo}/{instancia}',  [CalificacionesController::class,'guardarCalificacion'])->name('calificaciones.guardarCalificacion');

CalificacionesController.php:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Alquiler;
use App\Models\Calificacion;
class CalificacionesController extends Controller{
    ...

     public function calificar(Request $request, $modelo=null, $identificador=null) {
          $instancia=null;
          if($modelo=='usuario'){
               $instancia=User::find((int) $identificador);
          }
          else if($modelo=='alquiler'){
               $instancia=Alquiler::where('slug', $identificador)->firstOrFail();
          }
          return view('calificaciones.calificar',compact(['modelo','instancia']));
     }


     public function guardarCalificacion(Request $request, $modelo=null, $identificador=null) {
         $calificador=auth()->user();
         $instancia=null;
         if($modelo=='usuario'){
              $instancia=User::find((int) $identificador);
         }
         else if($modelo=='alquiler'){
              $instancia=Alquiler::where('slug', $identificador)->firstOrFail();
         }
         Calificacion::create([
               'calificacionable_id' => $instancia->id,
               'calificacionable_type' => get_class($instancia),
               'puntuacion' => $request->puntuacion,
               'comentario' => $request->comentario,
               'calificador_id' => $calificador->id
          ]);
         
         return redirect()->route('calificaciones.calificacionesRealizadas');
     }
    ...
}

So some possible links I've got are:

http://localhost:8000/calificaciones/calificar/alquiler/buenos-aires--lomas-del-mirador-liniers-554-12

http://localhost:8000/calificaciones/calificar/usuario/2

By doing <a href="{{route('calificaciones.calificar',['alquiler',$alquiler])}}" or well <a href="{{route('calificaciones.calificar',['usuario',$alquiler->publicador])}}"

I have something similar in another routes and controller methods too.

Despite this works, I find it a pretty ugly approach, since I'm not being able to get my needed model instance in my method by performing dependency injection, but I'm getting it manually by performing an Eloquent query instead. Besides that, if I ever define a 'getRouteKeyName()' method in my 'User' model, or if I change it in my 'Alquiler' model, this code will not work anymore, so I would need to edit it in all my methods which uses the same approach.

I've tried to solve this by attempting to do an explicit route model binding on my RouteServiceProvider, as shown in the official Laravel docs and in another places:

https://laravel.com/docs/10.x/routing#route-model-binding https://www.digitalocean.com/community/tutorials/cleaner-laravel-controllers-with-route-model-binding

However, none of those things I've tried worked.

Does anyone could suggest a better approach to achieve the result I want, by being able to perform a DI depending on the model I need in each case? (or at least to improve the code I currently have)

If there's any more info needed, or something not properly understood, please let me know.

Thank's a lot!

Leandro

EDIT:

I've almost found a way to achieve this, by doing contextual binding:

I wrote a contract (interface) like this:

Calificable.php:

<?php
namespace App\Contracts;
interface Calificable {}

In my models definition I've done:

use App\Contracts\Calificable;
class Alquiler extends Model implements Calificable {...

And also:

use App\Contracts\Calificable;
class User extends Model implements Calificable {...

Then, in my RouteServiceProvider.php, I've added:

....
public function boot() {
      ...

        Route::bind('modelo', function (string $value) {
          error_log("Entro al bind de modelo");
          if($value=='usuario'){
               
               $this->app->singleton(Calificable::class, User::class);
          }
          else if($value=='alquiler'){
               $this->app->singleton(Calificable::class, Alquiler::class);
          }
          return $value;
     });
....

And finally, in my controller method i have:

public function calificar(Request $request, string $modelo, Calificable $instancia){
     error_log(get_class($instancia));
     error_log(json_encode($instancia));
     return view('calificaciones.calificar',compact(['modelo','instancia']));
}

When viewing the result of the first 'error_log' of 'calificar', I'm properly getting:

App\Models\User

Or well:

App\Models\Alquiler

In the correspondent cases.

However, in the second 'error_log', I'm seeing an empty model, instead of the one I should get. This not happens when I type-hint my parameter on my controller method with 'Alquiler' or well with 'User', getting there a successful result.

Is there any way to accomplish what I'm trying to do by this way, just by editing a little bit what I'm currently having on RouteServiceProvider.php? I think that I'm almost there by doing that code.


Solution

  • 1- Static approach with better maintainability

    Considering you have only 2 options the simplest and cleanest way to achieve your goal is to split the routes like this:

    web.php

    Route::get('/calificaciones/calificar/usuario/{user}',  [CalificacionesController::class,'userCalificar']);
    
    Route::get('/calificaciones/calificar/alquiler/{alquiler}',  [CalificacionesController::class,'alquilerCalificar']);
    
    

    CalificacionesController.php

    namespace App\Http\Controllers;
    
    use Illuminate\Http\Request;
    use App\Models\User;
    use App\Models\Alquiler;
    use App\Models\Calificacion;
    class CalificacionesController extends Controller{
        ...
    
        public function userCalificar(Request $request, User $instancia) {
            return view('calificaciones.calificar',compact(['instancia']));
        }
    
        public function alquilerCalificar(Request $request, Alquiler $instancia) {
            return view('calificaciones.calificar',compact(['instancia']));
        }
        
        ...
    }
    

    No magic binding makes your code very clean and understandable for future developments.

    2- Dynamic approach

    But in case you or someone else reading this needs a more dynamic approach you can use this:

    RouteServiceProvider.php

            ...
    
            Route::bind('instancia', function ($value, $route) {
                $model = $route->parameter('modelo');
    
                return match ($model) {
                    'usuario' => User::where('id', $value)->firstOrFail(),
                    'alquiler' => Alquiler::where('slug', $value)->firstOrFail(),
                    default => throw new ModelNotFoundException(),
                };
            });
    
            ...
    

    CalificacionesController.php

        ...
    
        public function userCalificar(Request $request, string modelo, Model $instancia) {
            return view('calificaciones.calificar',compact(['instancia']));
        }
        
        ...