Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

a.verrecchia's avatar

Managing UI buttons in client app with Laravel API (Services/Policies)

In my Laravel API project, I have a Service for handling quote rejection and a Policy for controlling access based on application type. Here's how I'm managing access to the "Reject" button in the client.

Service for rejecting a quote

public function reject(Quote $quote): bool
{
    if ($quote->rejected_at) {
        throw new QuoteAlreadyRejectedException();
    }

    return $quote->update(['rejected_at' => Carbon::now()]);
}

Policy for access control

public function reject(User $user, Quote $quote, AppTypeEnum $app): Response
{
    if (!$user->can('reject quotes')) {
        return Response::deny();
    }

    if ($app === AppTypeEnum::CONSUMER) {
        return Response::allow();
    }

    return Response::deny();
}

Controller to handle reject request

public function reject(Quote $quote): JsonResponse
{
    try {
        $this->authorize('reject', [$quote, AppTypeEnum::CONSUMER]);
        $success = $this->quoteService->reject($quote);
    } catch (QuoteAlreadyRejectedException $e) {
        return $this->errorResponse($e->getMessage(), $e->getCode());
    }

    return $this->successResponse($success);
}

I have integrated a quote rejection functionality in my Laravel API. The Service (QuoteService) is responsible for updating the quote's rejected_at timestamp and throws a QuoteAlreadyRejectedException if the quote has already been rejected. On the other hand, the Policy (QuotePolicy) controls access to the "Reject" action based on user permissions ('reject quotes' capability) and the application type (AppTypeEnum::CONSUMER).

I know it should be placed in the service, but I also wanted to add it in the policy. In my resource, I have an actions array with 'reject' that returns true or false. This is needed to display buttons in the interface. How can I manage this? Because if I move the check to the policy, it doesn't seem correct, as the button would always be visible when it shouldn't.

0 likes
4 replies
xuuto's avatar

Step 1: Update Policy to Check Permissions and App Type

Your QuotePolicy should check both the user's permission and the application type. However, to keep the button state logic separate from the policy, we'll include the business logic in the service

namespace App\Policies;

use App\Models\User;
use App\Models\Quote;
use App\Enums\AppTypeEnum;
use Illuminate\Auth\Access\Response;

class QuotePolicy
{
    public function reject(User $user, Quote $quote, AppTypeEnum $app): Response
    {
        if (!$user->can('reject quotes')) {
            return Response::deny('You do not have permission to reject quotes.');
        }

        if ($app === AppTypeEnum::CONSUMER) {
            return Response::allow();
        }

        return Response::deny('This action is not allowed for the current application type.');
    }
}

Step 2: Controller to Handle Reject Request

Your QuoteController should handle the rejection request, using the policy to check access and the service to perform the rejection.

namespace App\Http\Controllers;

use App\Models\Quote;
use App\Services\QuoteService;
use App\Enums\AppTypeEnum;
use App\Exceptions\QuoteAlreadyRejectedException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class QuoteController extends Controller
{
    protected $quoteService;

    public function __construct(QuoteService $quoteService)
    {
        $this->quoteService = $quoteService;
    }

    public function reject(Quote $quote): JsonResponse
    {
        try {
            $this->authorize('reject', [$quote, AppTypeEnum::CONSUMER]);
            $success = $this->quoteService->reject($quote);
        } catch (QuoteAlreadyRejectedException $e) {
            return $this->errorResponse($e->getMessage(), $e->getCode());
        }

        return $this->successResponse($success);
    }

    public function permissions(Quote $quote): JsonResponse
    {
        $canReject = auth()->user()->can('reject', [$quote, AppTypeEnum::CONSUMER]);

        return response()->json(['canReject' => $canReject]);
    }
}

Step 3: Define the Service Logic

Your QuoteService should handle the logic for rejecting a quote.

namespace App\Services;

use App\Models\Quote;
use Carbon\Carbon;
use App\Exceptions\QuoteAlreadyRejectedException;

class QuoteService
{
    public function reject(Quote $quote): bool
    {
        if ($quote->rejected_at) {
            throw new QuoteAlreadyRejectedException('Quote has already been rejected.');
        }

        return $quote->update(['rejected_at' => Carbon::now()]);
    }
}

Step 4: Define Routes

Define routes for handling the reject request and checking permissions.

use App\Http\Controllers\QuoteController;

Route::middleware('auth:api')->group(function () {
    Route::post('quotes/{quote}/reject', [QuoteController::class, 'reject']);
    Route::get('quotes/{quote}/permissions', [QuoteController::class, 'permissions']);
});

Step 5: Client-side Logic

Fetch the user's permissions to manage the UI button state.

1- fetch permissions

async function checkRejectPermission(quoteId) {
    const response = await fetch(`/api/quotes/${quoteId}/permissions`, {
        headers: {
            'Authorization': `Bearer ${localStorage.getItem('token')}`
        }
    });

    const data = await response.json();
    return data.canReject;
}

manage the button state

document.addEventListener('DOMContentLoaded', async () => {
    const quoteId = 123; // Example quote ID
    const canReject = await checkRejectPermission(quoteId);

    const rejectButton = document.getElementById('reject-button');
    if (canReject) {
        rejectButton.style.display = 'block';
    } else {
        rejectButton.style.display = 'none';
    }
});
1 like
a.verrecchia's avatar

@xuuto It's perfect, one thing though: if it has already been rejected, I am not aware of it in permissions, because the policy would return true as it indeed has the permission to reject it, but doesn't know it has been rejected, so the button would be shown. Should I implement the button display logic inside the permissions method? Because there might be other more complex relationships that could result in other buttons being shown more or less.

martinbean's avatar

@a.verrecchia There’s two things going on: authorisation whether the current user can reject a quote, but also whether the quote is in a state that it can be rejected.

You’re already doing a good job throwing specific exceptions from your service class (i.e. your QuoteAlreadyRejectedException class) but you shouldn’t be dropping try/catch statements all over your application code such as controllers. Instead, use Laravel’s exception handler to convert your domain-specific exceptions into appropriate HTTP responses:

$exceptions->map(
    QuoteAlreadyRejectedException::class,
    to: function (QuoteAlreadyRejectedException $e) {
        // Convert QuoteAlreadyRejectedException to 400 Bad Request response
        return new BadRequestHttpException($e->getMessage(), previous: $e);
    },
);

This will clean up your controllers to make them only concerned with the task they’re actually trying to perform, and not unrelated logic such as error handling:

public function reject(Quote $quote)
{
    $this->authorize('reject', [$quote, AppTypeEnum::CUSTOMER]);

    $this->quoteService->reject($quote);

    // Return successful response
}
1 like

Please or to participate in this conversation.