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