0

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).

TheSETJ
  • 518
  • 9
  • 25
  • What is the common action(s) you need to carry out here? Without knowing that it's difficult to suggest what the best approach would be. – redbirdo Dec 03 '20 at 12:37
  • @redbirdo Something like submitting an abuse report. Each model has many abuse reports (`Report` model). – TheSETJ Dec 03 '20 at 12:49
  • That sounds like something that would not be entity functionality. Although Laravel has specific M, V and C classes, you can also have Service or Manager classes between your controllers and model. For example you might have an AbuseReporter service to report abuse for any applicable entity. – redbirdo Dec 03 '20 at 13:14
  • @redbirdo So you mean it's unavoidable to have some repetitive code for controllers? – TheSETJ Dec 03 '20 at 13:24
  • Are you getting any routing related errors? – Donkarnash Dec 03 '20 at 13:51
  • @Donkarnash Nothing. I just get no response until I cancel the request. – TheSETJ Dec 03 '20 at 14:06
  • Try to return something maybe even just a string "Hello World" from the DoActionOnActionable class __invoke() – Donkarnash Dec 03 '20 at 14:11
  • @Donkarnash Even dd() is not doing anything. I've tried to let my request be over canceling it. I've waited until I've got this error: Controller class App\Http\Controllers\DoActionOnUser for one of your routes was not found. Are you sure this controller exists and is imported correctly? It seems to put all the controller definitions in one file caused it to happen. By the way, I'm still annoyed with having 3 other controllers with almost exactly the same code. – TheSETJ Dec 04 '20 at 06:42
  • @Donkarnash I've extracted specific controllers into their own file to face another error: Declaration of App\Http\Controllers\DoActionOnUser::__invoke(App\User $user, Illuminate\Http\Request $request) should be compatible with App\Http\Controllers\DoActionOnActionable::__invoke(App\Contracts\Actionable $actionable, Illuminate\Http\Request $request) – TheSETJ Dec 04 '20 at 06:52

1 Answers1

1

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

Donkarnash
  • 12,433
  • 5
  • 26
  • 37
  • It's absolutely interesting and has many things to learn from it. Don't delete the answer. I may refer to your answer later for other purposes too. Thanks for sharing. – TheSETJ Dec 05 '20 at 05:13