GeorgeKala's avatar

Service Pattern and its structure

hi all, I have a little confusion about the code structure, I use services that are associated with controllers, and I use Spatie laravel permissions for roles and permission, i have some questions about it

this is service code:

public function create(array $data, $user): void
{
    if (! $user->hasPermissionTo('create_player')) {
        throw new Exception('You do not have permission to create a player.', 403);
    }

    $teamId = $data['team_id'] ?? null;
    if ($teamId) {
        $playersCountInTeam = $this->countPlayersInTeam($teamId);
        if ($playersCountInTeam >= 23) {
            throw new Exception('Team is fully occupied. Please consider adding this player to another team or create the player without a team.', 400);
        }
    }

    $this->playerRepository->create($data);
}

this is controller code:

public function store(PlayerRequest $request): JsonResponse
{
    $validatedData = $request->validated();
    $user = auth()->user();
    try {
        $this->playerService->create($validatedData, $user);

        return response()->json(['message' => 'Player created successfully.'], 201);
    } catch (Exception $e) {
        return response()->json(['message' => $e->getMessage()], $e->getCode());
    }
}
  1. where should i write logic of checking permissions, should I move it to the controller, i want to check it before validating the request?
  2. how to return responses and exceptions, what is appropriate way?
1 like
3 replies
martinbean's avatar

@georgekala Your services should just be concerned with whatever business logic it is encapsulating; not anything like authentication or authorisation. That belongs in your application layer (controllers).

If you throw meaningful domain-level exceptions from your service classes, you can then get rid of your try/catch blocks in your controllers and everywhere else, and instead use Laravel’s exception handler for, well, handling exceptions and returning an appropriate response.

So instead of throwing generic Exception instances, create an exception class relevant to the domain and throw those:

class PlayerService
{
    public function createForTeam(Team $team, array $data)
    {
        $team->assertHasCapacity();

        // Logic to create player and assign them to team
    }
}

The assertHasCapacity method would check if the team has capacity to add a player, and throw a meaningful exception if not:

public function assertHasCapacity(): void
{
    if ($this->playersCount() < 23) {
        throw TeamException::capacityReached();
    }
}

You can then “convert” this exception to a more appropriate one in the exception handler:

$exceptions->map(TeamException::class, function (TeamException $e) {
    return new BadRequestHttpException($e->getMessage(), previous: $e);
});

This will convert any TeamExceptions thrown to a 400 Bad Request exception.

So your controller is now just concerned with calling business logic, and not with exception handling:

public function store(StorePlayerRequest $request)
{
    // Get $team from somewhere...

    $this->playerService->createForTeam($team, $request->validated());

    return new JsonResponse([
        'message' => __('Player created successfully.'),
    ], JsonResponse::HTTP_CREATED);
}
2 likes
GeorgeKala's avatar

@martinbean thank you so much, Is it the right way to make custom middleware and check permissions using it?

something like that:

class CheckPermission
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, $permission)
    {
        $user = $request->user();

        if (! $user || ! $user->hasPermissionTo($permission)) {
            return response()->json(['message' => 'You do not have permission to perform this action.'], 403);
        }

        return $next($request);
    }
}

and after use like that:

Route::prefix('players')->group(function () {
        Route::get('/', [PlayerController::class, 'index']);
        Route::get('{player}', [PlayerController::class, 'show']);
        Route::post('/', [PlayerController::class, 'store'])->middleware('permission:create_player');
        Route::put('{player}', [PlayerController::class, 'update'])->middleware('permission:edit_player');
        Route::delete('{player}', [PlayerController::class, 'destroy'])->middleware('permission:delete_player');
    });

Please or to participate in this conversation.