Search code examples
phplaravelroutesrepository-pattern

Laravel: How to retrieve a named route, but assign parameters later


I like to simplify my controllers as much as possible. When dealing with APIs, I sometimes use a service class (repository?) method that expects some input as well as a RedirectResponse like so:

use App\Http\Controllers\Controller;

class MyController extends Controller
{
    public function create(Request $request)
    {
        $validated = $request->validate([...]);
        return MyService::createWithResponse(
            input: $validated,
            response: back()
        );
    }
}

I can focus on just I/O here. MyService::createWithResponse will handle all the repository logic, but it will also handle error/success messaging:

use \Illuminate\Http\RedirectResponse;

class MyService
{
    public static function createWithResponse(array $input, RedirectResponse $response)
    {
        // ...repository logic dealing with the third-party API
        if (/* the API sends me an error */) {
            return $response->with(['error' => $some_error]);
        }
        $resourceId = /**/;
        return $response->with(['success' => 'Your resource has been created!']);
    }
}

Well let's say I have:

Route::get('/resources/{resourceId}', ...)->name('resources.show');

and I'm trying to find a way to call createWithResponse with a response()->redirectToRoute('resources.show'), but I want to pass route parameters (that $resourceId) LATER; I don't yet know the resourceId until I've run the repository logic.

I can't just pass the route name string into createWithResponse, since the expected RedirectResponse might not be for a named route, or I may have already added parameters to the RedirectResponse before I passed it. (And in any event, MyService should be agnostic, and that information should stay in the Controller.)

How can I pass around a Route retrieved by name, then attach parameters later? Or is there some other way you would do this? I have been looking off and on for weeks for a solution.


Solution

  • Edit: technically found a way to do this, for anyone who might Google this:

    $routeByName = app('router')->getRoutes()->getByName('resources.show');
    
    $routeUrl = app('url')->toRoute(
        route: $routeByName, // `Route` instance
        parameters: ['resourceId' => 1], // mixed
        absolute: false // boolean, absolute or relative path
    );
    

    One possible better way, though, is to pass success/failure callbacks to the service method. The callback closures can accept data from the results as arguments.

    Consider:

    use \Illuminate\Http\RedirectResponse;
    
    class MyService
    {
        public static function create(
            array $input,
            callable $onSuccess,
            callable $onFailure
        )
        {
            // ...repository logic dealing with the third-party API
            if (/* the API sends me an error */) {
                return $onFailure($some_error);
            }
            $resourceId = /**/;
            return $onSuccess($resourceId);
        }
    }
    
    use App\Http\Controllers\Controller;
    
    class MyController extends Controller
    {
        public function create(Request $request)
        {
            $validated = $request->validate([...]);
            return MyService::create(
                input: $validated,
                onSuccess: fn(string $resourceId) => response()
                    ->redirectToRoute('resources.show', $resourceId)
                    ->with(['success' => 'Your resource has been created!']),
                onFailure: fn(string $errorMsg) => back()
                    ->with(['error' => $errorMsg])
            );
        }
    }
    

    This keeps the I/O logic fully in the controller. The service method doesn't need to know what type the response is supposed to be, let the controller define that. Now other parts of the app can use that service method and define their own outputs.


    Additionally, you can use some PHPDoc "generics" (@template) syntactic sugar to help out your IDE and make this more obvious:

    
    class MyService
    {
        /**
         * @template TSuccess of mixed
         * @template TFailure of mixed
         * @param array $input
         * @param callable(string):TSuccess $onSuccess
         * @param callable(string):TFailure $onFailure
         * @return TFailure|TSuccess
         */
        public static function create(
            array $input,
            callable $onSuccess,
            callable $onFailure
        )
        {
        ...