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

ultrawelfare's avatar

Services and Actions

I've been researching on how to do service or action refactoring in a laravel project.

For services I did it like that:

class CustomerService
{
    public function create(User $user, array $input): Customer
    {
        if (user->cannot("create", Customer::class)) {
            throw new AuthorizationException();
        }
        $validator = Validator::make($input, [
            "name" => ["required"],
            "email" => ["required", "unique:customers,email"],
        ]);
        $data = $validator->validate();
        $customer = Customer::query()->create($data);
        return $customer;
    }
}

so now from a controller you can call it like ($this->customerService->create(auth()->user(), $request->all());` I discussed it with some people and the recommendation they had was to use FormRequest. My problems with form request is that, I don't wanna tie the service layer to the Form Request.

Another solution would be to validate and authorize outside the service layer, onto the controller and pass in the validated data. However I think this misses the purpose, since now you don't have a centralized place for validation-authorization-execution. Anyone can call `create` on the service, with the wrong data and end up exploding.

I took a look at the laravel actions package which implements the actions pattern and as far as I understand, they put the permissions-validation in the same class that does the execution. Which means it is centralized.

I wanted to hear your thoughts about how you design in laravel, whether that's services or actions or something else.

0 likes
7 replies
martinbean's avatar

@ultrawelfare People these days tend to use “DTOs” for passing parameters around layers of their application. I put “DTO” in quotation marks because it’s not the true purpose of what a DTO was actually conceived for, but the gist is you treat it as a parameter bag object that can only be instantiated with valid data. Therefore, if you have an instance of it, you know its contents have already been pre-validated.

So, your services and actions would take an instance of some sort of DTO/parameter bag object, and you can use its contents however you need:

class CustomerService
{
    public function create(CreateCustomerData $data)
    {
        // Use contents of $data to create customer object
    }
}

You can then create instances of this CreateCustomerData object in a controller, console command, or queued job and then pass it to your service class method:

public function store(StoreCustomerRequest $request)
{
    // Create customer data from request data...
    $data = CreateCustomerData::fromArray($request->validated());

    $customer = $this->customers->create($data);

    // Return response...
}
public function handle(): int
{
    // Create customer data from console command arguments...
    $data = CreateCustomerData::fromArray($this->arguments());

    $this->customers->create($data);

    $this->info('Customer created.');

    return Command::SUCCESS;
}
1 like
ultrawelfare's avatar

@martinbean hm. The only point I have to make here is that, if you want to test the service create function, you would not be able to test for validation failures since that belongs to another layer.

Also what about authorization in the example you mentioned, where does it go?

martinbean's avatar

The only point I have to make here is that, if you want to test the service create function, you would not be able to test for validation failures since that belongs to another layer.

@ultrawelfare Correct. Because validation isn’t a concern of the service class any more since the service-class receives pre-validated data only. Therefore the service method doesn’t need to be concerned about validation.

Also what about authorization in the example you mentioned, where does it go?

Like validation, in the application layer. Your service class should just be concerned with doing the job at hand (creating a customer); not validation, authorisation, and anything else you can think of. Otherwise your service layer is just going to be full of multiple concerns.

ultrawelfare's avatar

@martinbean That's very true, SRP! Although I do think there are cases where you need some kind of validation in the service layer.

For example: Fetching a user account and decrementing his balance by X amount.

class UserService
{
    public function decreaseBalance(array $data)
    {
        $user = User::find($data['id']);
		$user->balance -= $data['decrement_by'];
		$user->save();
    }
}

Maybe you want to validate whether his balance is below 0. You can't do that in pre-validation because if you put that validation in a "FormRequest" or inside the controller, you would end up double fetching the User.

martinbean's avatar

@ultrawelfare That’s business logic; not validation. And you can pass objects to services, not just a “parameter bag”.

ultrawelfare's avatar

@martinbean

That’s business logic

How would you handle that in terms of throwing an error; Throw an exception and handle it in the controller to do whatever? or handle it with a middleware?

And you can pass objects to services, not just a “parameter bag”.

I guess classes are better suited for that job

ultrawelfare's avatar

@martinbean I tried to apply everything:

class OrderService
{
    public function create(CreateOrderDto $createOrder): Order
    {
        /** @var Order $order */
        $order = Order::query()->create([
            'customer_id' => $createOrder->customerId,
            'supplier_id' => $createOrder->supplierId,
            'title' => $createOrder->title,
            'comments' => $createOrder->comments,
            'status' => $createOrder->status
        ]);
        return $order;
    }
}
class CreateOrderDto
{
    public function __construct(
        public int    $customerId,
        public int    $supplierId,
        public string $title,
        public string $comments,
        public string $status
    )
    {
    }

    public static function fromRequest(StoreOrderRequest $request): self
    {
        $data = $request->validated();
        return new self(
            customerId: $data['customer_id'],
            supplierId: $data['supplier_id'],
            title: $data['title'],
            comments: $data['comments'],
            status: $data['status']
        );
    }
}
class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'customer_id' => 'required|exists:customers',
            'supplier_id' => 'nullable|exists:suppliers',
            'title' => 'required|string',
            'comments' => 'required|string',
            'status' => 'required|string'
        ];
    }
}
class OrderController extends Controller
{
    public function __construct(private readonly OrderService $orderService)
    {
    }

    public function store(StoreOrderRequest $request)
    {
        $createOrder = CreateOrderDto::fromRequest($request);
        $this->orderService->create($createOrder);
        return back();
    }
}

Please or to participate in this conversation.