Courses

How to Structure Laravel 11 Projects

Save Data: Service or Action?

We continue transforming our Controller method and moving logic elsewhere. Now we get to the lines which are probably the main logic of saving the data in the DB.

I see two main approaches where to offload that logic: Service and Action classes.

One of the goals of separating this from Controller is that the new method could be reused from multiple places: Web Controller, API Controller, Unit Tests, Jobs, etc.


Service Classes

First, let's demonstrate how to create a Service class.

The service class can be created manually or using an artisan command make:class.

php artisan make:class Services/UserService

The service class can look like this:

app/Services/UserService.php:

namespace App\Services;
 
use App\Models\User;
 
class UserService
{
public function create(array $userData): User
{
$user = User::create($userData);
$user->roles()->sync($userData['roles']);
 
return $user;
}
}

Here, we make the create() method, which accepts an array of validated data. In this method, we create a user and sync roles and then return the created user.

Now, how do you call this Service in the Controller? There are at least two ways.

The first one is to initialize the service by doing the new UserService() and passing validated data to the create() method:

app/Http/Controllers/UserController.php:

use App\Services\UserService;
 
// ...
 
public function store(StoreUserRequest $request)
{
$user = (new UserService())->create($request->validated());
 
// ...
}

The second is injecting the Service class into a method, type-hinting that, and assigning to a variable. So now our Controller would look like this:

use App\Services\UserService;
 
// ...
 
public function store(StoreUserRequest $request, UserService $userService)
{
$user = $userService->create($request->validated());
 
// ...
}

Laravel has this "magic" of auto-resolving the class in Controller methods if you type-hint it. If you want to find out more about it, I have this article: Laravel Service Container: What Beginners Need to Know


Service into Action: What's the Difference?

Another alternative to a Service class is called an Action class. Again, PHP class can be created manually or using Artisan command make:class.

php artisan make:class Actions/CreateUserAction

Inside, typically there's one method called handle() or execute().

app/Actions/CreateUserAction.php:

namespace App\Actions;
 
use App\Models\User;
 
class CreateUserAction
{
public function execute(array $userData): User
{
$user = User::create($userData);
$user->roles()->sync($userData['roles']);
 
return $user;
}
}

To call this Action class in the Controller, you would just initialize the action and call the execute() method by passing data to it.

app/Http/Controllers/UserController.php:

use App\Actions\CreateUserAction;
 
// ...
 
public function store(StoreUserRequest $request)
{
$user = (new CreateUserAction())->execute($request->validated());
 
// ...
}

Or, similarly to the Service class, you can type-hint the Action class:

use App\Actions\CreateUserAction;
 
// ...
 
public function store(StoreUserRequest $request, CreateUserAction $action)
{
$user = $action->execute($request->validated());
 
// ...
}

As you can see, there's not much syntax difference when using Service and Action classes.

The difference is more about how YOU want to divide your logic:

  • Either into model-related entities like UserService or TaskService, with many methods inside
  • Or, each operation as an Action class like CreateUserAction or UpdateUserAction, with one method inside

Of course, inside that Action class, you may also have some private methods for more logic if you have something more complicated. But at its core, Action is similar to a one-time Job class, just without a queue mechanism. By the way, Jobs will be the exact topic of the next lesson.


Open-Source Examples

Example Project 1. ash-jc-allen/find-a-pr

The service example is from a ash-jc-allen/find-a-pr open-source project. The Service has a method to return a Collection of GitHub repositories.

app/Services/RepoService.php:

final readonly class RepoService
{
public function reposToCrawl(): Collection
{
return collect(config('repos.repos'))
->merge($this->fetchReposFromOrgs())
->flatMap(function (array $repoNames, string $owner): array {
return Arr::map(
$repoNames,
static fn (string $repoName): Repository => new Repository($owner, $repoName)
);
});
}
 
// ...
}

This is a perfect example of a Service because it is reused in multiple places: an Artisan command and a Livewire component.

The first usage example:

app/Console/Commands/PreloadRepoData.php:

final class PreloadRepoData extends Command
{
protected $signature = 'repos:preload';
 
protected $description = 'Preload the repos and cache them to improve load time.';
 
public function handle(): int
{
$this->components->info('Preloading and caching issues...');
 
$batches = app(RepoService::class)
->reposToCrawl()
->chunk(25)
->map(function (Collection $repos): PreloadIssuesForRepos {
return new PreloadIssuesForRepos($repos);
})
->all();
 
// ...
}
}

The second usage example:

app/Livewire/ListIssues.php:

final class ListIssues extends Component
{
// ...
 
public function mount(): void
{
$this->setSortOrderOnPageLoad();
 
$this->labels = config('repos.labels');
$this->repos = app(RepoService::class)->reposToCrawl()->sort();
 
try {
$this->originalIssues = app(IssueService::class)->getAll()->shuffle();
} catch (GitHubRateLimitException $e) {
abort(503, $e->getMessage());
}
 
$this->shouldDisplayFirstTimeNotice = ! Cookie::get('firstTimeNoticeClosed');
}
 
// ...
}

Example Project 2. christophrumpel/larastreamers

Next, let's look at how the Action class can be used.

For this example, we will look at christophrumpel/larastreamers, an open-source project.

In this project, there is an action called ApproveStreamAction. That action is responsible for everything related to approving the stream:

  • getting info from YouTube
  • setting that the stream is approved
  • sending emails
  • etc.

app/Actions/Submission/ApproveStreamAction.php:

class ApproveStreamAction
{
public function handle(Stream $stream): void
{
if ($stream->approved_at) {
return;
}
 
$streamData = YouTube::video($stream->youtube_id);
(new UpdateStreamAction())->handle($stream, $streamData);
 
if (is_null($stream->channel_id)) {
Artisan::call(ImportChannelsForStreamsCommand::class, ['stream' => $stream]);
}
 
$stream->update(['approved_at' => now()]);
 
Mail::to($stream->submitted_by_email)->queue(new StreamApprovedMail($stream));
}
}

Also, notice that there's another Action class UpdateStreamAction used in this Action class.

Then, the Controller only has two lines of code:

  • one for calling the Action
  • and the second for returning a View

app/Http/Controllers/Submission/ApproveStreamController.php:

class ApproveStreamController
{
public function __invoke(Stream $stream, ApproveStreamAction $approveStream): View
{
$approveStream->handle($stream);
 
return view('pages.streamApproved');
}
}

So yeah, if you want to separate the action of saving the data to DB, you may (again!) move it from Controller to a Service or an Action class.

avatar

You forgot change code in block after:

Or, similarly to the Service class, you can type-hint the Action class:

Thank you!

avatar

Thank you for bringing this to us. Updated the example!

avatar

Hi, I don't believe the update was done...initializing and type-hinting examples looks similar to me.

avatar

Whoops! Sorry about that, there was indeed a caching issue on our end!

avatar

Would it be unusal to use both in a project? A handful of relatively simple, related actions in a service, but more complicated logic in an action?

avatar

It really depends. Some would say it's bad, others would say it's okay.

Our point here would be - as long as you are doing it consistently and it is easy to follow/understand - you should be okay :)

avatar

Would you suggest a Service is better when it can be used by various Models and an Action is better when it is specific to a single Model?

avatar

It really depends.

A service can work with one model or with one action. For example, User service can do anything with users, but like UserProfileService - works with user profile only.

The same is with Actions - they can be either really specific or cover more than one thing.

So really hard to say if that fits a specific pattern! But, if you make it into one on your application - keep the structure and enforce it everywhere!

avatar

Could you explain why you chose for a Service class to be object oriented and not static and for a Helper class the other way around?

avatar

A service class is often used with Dependency Injection. This allows us to emit the new Service() creation most of the time and just use the service as normal.

Apart from that, some services are chainable (you can chain multiple methods together).

Now,. with helpers - they are often just one-method things. This means that you don't need the full instance or multiple methods. And this specific thing allows you to have static functions inside.

The difference is subtle, but they differ in their usage/workflows

avatar

That makes a lot of sense. Thank you!