Laravel - How to implement a shared controller with working model route binding

645 Views Asked by At

Before everything, I'm using Laravel 6 and PHP 7.2.

Alright. I have various models on which I can do the same action. For the of being DRY, I thought of the following idea:

On each model I'll implement an interface, and I'll put the actual implementation for handling the action in a single invokable controller.

The thing is I don't know how to have a working model route binding with such implementation.

To make my question easier for understanding here is some code snippets:

  • Models
class Post extends Model implements Actionable { /* attributes, relationships, etc. */ }
class Comment extends Model implements Actionable { /* attributes, relationships, etc. */ }
class User extends Model implements Actionable { /* attributes, relationships, etc. */ }
  • Controllers
class DoActionOnActionable extends Controller
{
    public function __invoke(Actionable $actionable, Request $request) {
        // implementation
    }
}

I know for Laravel to do the model route binding, it does need to know what model to bind to this I've made the DoActionOnActionable controller abstract and created 3 other controllers in the same file (which kinda annoys me because it's mostly repetitive):

class DoActionOnUser extends DoActionOnActionable
{
    public function __invoke(User $user, Request $request) {
        parent::__invoke($user, $request);
    }
}
class DoActionOnPost extends DoActionOnActionable
{
    public function __invoke(Post $post, Request $request) {
        parent::__invoke($post, $request);
    }
}
class DoActionOnComment extends DoActionOnActionable
{
    public function __invoke(Comment $comment, Request $request) {
        parent::__invoke($comment, $request);
    }
}
  • Routes
Route::post('/users/{user}/actions', 'DoActionOnUser');
Route::post('/posts/{post}/comments/{comment}/actions', 'DoActionOnComment');
Route::post('/posts/{post}/actions', 'DoActionOnPost');

The issue is when I send a request to these routes, it takes as much time to respond that I cancel the request. So, I think something is wrong and it's not working as I expected.

I appreciate anything that helps me understand my implementation issue or a better solution to my problem (being DRY).

1

There are 1 best solutions below

1
Donkarnash On

Have tried a different approach - not exactly with implicit route model binding but attempt at having shared controller

ResourceManager

<?php

namespace App;

use ReflectionClass;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
use Illuminate\Database\Eloquent\Model;

class ResourceManager
{
    public static array $resources = [];

    /**
     * Register resources/models from the class files at given path;
     */
    public static function registerResourcesFrom(string $path): self
    {
        $namespace = rtrim(app()->getNamespace(), '\\');
        $resources = [];
        foreach ((new Finder)->in($path)->files() as $resource) {
            $resource =  $namespace . str_replace(
                ['/', '.php'],
                ['\\', ''],
                Str::after($resource->getPathname(), app_path())
            );

            $reflectionClass = new ReflectionClass($resource);

            if ($reflectionClass->isSubclassOf(Model::class) && !$reflectionClass->isTrait()) {
                $resources[] = $resource;
            }
        }

        static::registerResources($resources);

        return new static;
    }

    /**
     * Register the resources provided as array.
     */
    public static function registerResources(array $resources): self
    {
        static::$resources = array_unique(array_merge(static::$resources, $resources));

        return new static;
    }

    /**
     * Get all registered resources/models
     */
    public static function resources(): array
    {
        return static::$resources;
    }

    /**
     * Get the resource/model class for the given resource name
     */
    public static function resourceClass($resourceName): string
    {
        return collect(static::$resources)->first(
            fn ($resource) => Str::plural(Str::lower(class_basename($resource))) === preg_replace('/[^a-zA-Z0-9]/s', '', $resourceName)
        );
    }
}

In AppServiceProvider


namespace App\Providers;

use App\ResourceManager;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //Register all model classes from app/Models as resources.

        ResourceManager::registerResourcesFrom(app_path('Models'));
    }
}

Controller - show method

<?php

namespace App\Http\Controllers;

use App\ResourceManager;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class ResourceShowController extends Controller
{
    public function __invoke(Request $request)
    {
        $resource        = $request->route('resource');
        $resourceKey     = $request->route('resourceKey');
        $resourceClass   = ResourceManager::resourceClass($resource);
        $routeKeyName    = (new $resourceClass)->getRouteKeyName();

        $record          = $resourceClass::where($routeKeyName, $resourceKey)->first();
        $primaryKey      = (new $resourceClass)->getKeyName();

        return response()->json(['record' => $record, 'foo' => 'bar']);
    }
}

Routes (web.php)

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ResourceShowController;


Route::get('/{resource}/{resourceKey}', ResourceShowController::class);

Still need to extend the concept for nested resource routes - currently it only tackles non-nested routes

Let me know if it's interesting - if not will delete the answer