Laravel patterns - Usage of jobs vs services
Asked Answered
L

1

14

I was wondering how most developers use this two Laravel tools.

In Laravel, you can handle business logic with Services, or with Jobs (let's talk only about not-queueable jobs, only those ones that run in the same process).

For example, an user wants to create an entity, let's say a Book, you can handle the entity creation with a service or dispatching a job.

Using a job it would be something like this:

class PostBook extends Job
{
    ...
    public function handle(Book $bookEntity)
    {
        // Business logic here.
    }
    ...
}

class BooksController extends Controller
{
    public function store(Request $request)
    {
        ...
        dispatch(new PostBook($request->all()));
        ...
    }
}

Using a service, it would be something like this:

class BookService
{
    public function store(Request $request)
    {
        // Business logic here.
    }
}

class BooksController extends Controller
{
    public function store(Request $request)
    {
        ...
        // I could inject the service instead.
        $bookService = $this->app()->make(App\Services\BookService::class);
        $bookService->store($request);
        ...
    }
}

The question is, how do you mostly choose to use one way or the other one? And why?

Surely there are two "schools" in this matter, but I would want to understand pros & cons of each one.

Lsd answered 19/10, 2018 at 15:36 Comment(2)
Conceptually, a job is used more in async operations, like queues or crontab commands. A Service (layer) is a design pattern to encapsulate business logic. In my opinion is usually the way to go in web applications sync operations.Peep
Minor note about your code example: I wouldn't pass the Request object to the service. The request is an object you receive when the user performs an HTTP request. If you directly pass it to the service, it means you cannot use the same service within a console command or a (queued) job, because you miss the request. In my opinion, the controller should transform the request into a model or DTO (data transfer object) and call the service function with these as parameter.Addressograph
G
22

"Business logic" can be handled with anything, so it seems like what's really being asked is which option is better for repeating the same business logic without repeating code.

A Job class typically does one thing, as defined by its handle() method. It's difficult to exclude queued jobs from the comparison because running them synchronously usually defeats the purpose, which is to handle slow, expensive, or unreliable actions (such as calling a web API) after the current request has been completed and a response has been shown to the user.

If all jobs were expected to be synchronous, it wouldn't be much different than defining a function for your business logic. That's actually very close to what dispatching a synchronous job does: Somewhere down the call stack it ends up running call_user_func([$job, 'handle']) to invoke a single method on your job object. More importantly, a synchronous job lacks the mechanism for retrying jobs that might have failed due to external causes such as network failures.

Services, on the other hand, are an easy way to encapsulate the logic around a component, and they may do more than one thing. In this context, a component may be thought of as a piece of your application that could be swapped out for a different implementation without having to modify code that was using it. A perfect example, included in the framework, is the Filesystem service (most commonly accessed with the Storage facade).

Consider if you didn't store books by inserting them into your database, but instead by posting to an external API. You may have a BookRepository service that not only has a store() method, but also has a get(), update(), list(), delete(), or any number of other methods. All of these requests share logic for authenticating to the external web service (like adding headers to requests), and your BookRepository class can encapsulate that re-usable logic. You can use this service class inside of scheduled artisan commands, web controllers, api controllers, jobs, middleware, etc. — without repeating code.

Using this example, you might create a Job for storing a new book so you don't make users wait when there are slow API responses (and it can retry when there are failures). Internally, your job calls your Service's store() method when it runs. The work done by the service is scheduled by the job.

Gardal answered 19/10, 2018 at 17:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.