minaremonshaker's avatar

Simplifying Database Access in Controllers

I've watched some YouTube videos where developers use both the repository pattern and services together. This approach seems complicated to me. Wouldn't it be simpler to just use a service to handle database operations, inject the service into the controller's constructor, and then call the service methods from within the controller? Am I correct in thinking this is a more straightforward approach?

0 likes
14 replies
Tray2's avatar

I'm a big fan of KISS, and that would mean that you use the simplest option. I normally let my controller handle the database communication.

public function store(Request $request)
{
	//Validate
   Record::create($validatedRecord);
}

But if there are more models involved, I might create a service that does the actual communication with that model. The code below uses two services, one to create foreign keys to the id, and one to handle the storing of the related tracks.

public function __invoke(RecordFormRequest $request, TracksService $tracksService, ForeignKeyService $foreignKeyService)
    {
        $valid = $request->validated();
        $record = Record::create(array_merge($valid, [
            'genre_id' => $foreignKeyService->getGenreId($request->genre_name, 'record'),
            'format_id' => $foreignKeyService->getFormatId($request->format_name, 'record'),
            'artist_id' => $foreignKeyService->getArtistId($request->artist),
            'country_id' => $foreignKeyService->getCountryId($request->country_name),
            'record_label_id' => $foreignKeyService->getRecordLabelId($request->record_label_name),
        ]));

        $tracksService->storeTracks([
            'track_count' => count($valid['track_positions']),
            'track_positions' => $valid['track_positions'],
            'track_titles' => $valid['track_titles'],
            'track_durations' => $valid['track_durations'],
            'track_mixes' => $valid['track_mixes'] ?? null,
            'track_artists' => $valid['track_artists'] ?? null,
            'record_id' => $record->id,
            'record_artist' => $request->artist,
        ], $foreignKeyService);

        return redirect(route('records.index'));
    }

If you are a solo developer, stick to KISS, but if you are part of a team, you need to do things in the same way, and if you are employed by a company, the company most likely have a standard way of doing this, then you need to follow that standard.

I would not create a Repository class that is just a wrapper to Eloquent, but some might not agree with me on that. You need to know how to do all of them, but you can of course have a preferred way of doing it.

  • Controller
  • Model
  • Service class
  • Repository

The three last ones are in my opinion only to be used when there are more than one way of creating the record.

1 like
minaremonshaker's avatar

@Tray2 I prefer to use a service to handle everything in the controller, so I can keep it as lean as possible. For validation, as you demonstrated above, I like to use the FormRequest class since that's its intended purpose. However, when working with services, is it standard practice to pass the request object—such as $request->validated()—and handle it within a service method? Also, I’m curious: can a service contain multiple methods, or should it follow the action pattern, where each service has only one method that performs a specific task?

Tray2's avatar

@minaremonshaker It's your service class, you may do as you please with it.

Take this service class for example. this is the one used in the example above.

Here I made the decision to keep all of them in the same service class, they each do the same thing, but with different models, which in my case makes sense to keep in a single class. The main reason for this, is to be able to get the first record matching the given data, and if it doesn't exist, it creates the record and returns the id.

ian_h's avatar

@minaremonshaker Replace the FormRequest with a DTO (using the Spatie LaravelData package) and a lot of goodness is encapsulated in a single place.

You can add the validation rules to the DTO, you then have a nice object to be able to pass around your application, and in your case (as is also often mine too), a nicely-typed, validated object to pass to the service/action to process as you wish.

eg:

// AController.php
public function update(Model $model, MyData $data, Store $store)
{
	try {
		$result = $store(model: $model, data: $data);
	} catch (AnException $exception) {
		// Handle exception...
	}

	return new JsonResponse($result);
}
// MyData.php
public function __construct(
	#[MapName('a_field')
	#[StringType, Min(3), Max(32)]
	public readonly string $aField,
	#[MapName('a_number')
	#[Min(0)]
	public readonly int $aNumber,
}
Snapey's avatar

or should it follow the action pattern, where each service has only one method that performs a specific task?

Then it would be an Action class and not a Service. Service is a collection of functions based around a specific theme.

Snapey's avatar

Dont over engineer it. I have seen many 'clean' controllers that spend many lines preparing for and calling the service layer, and then afterwards checking for errors, and where calling Eloquent models would be far quicker, easier and more readable

1 like
Tray2's avatar

@minaremonshaker KISS stands for Keep It Simple Stupid, and it's not really a pattern, it's more of tool, that you should apply to all situations.

What is the easiest solution to creating a store method in a controller? You need to do three things most of the time.

  1. Validate the user input.
  2. Store the validated data.
  3. Redirect to somewhere.

Now, the controller's job is to talk to the model, and redirect to the view. The validation on the other hand isn't really the controller's job, and we know that unless we are working at Twitter, that we need to be able to update the data as well, and when we do that, we'll need to validate the data once more.

So the Validation we can extract to a FormRequest class, which we then inject.

public function store(BookRequest $request)
{
}

public function update(BookRequest $request)
{
}

When it comes to storing the data, we need to ask ourselves, will I insert Books in any other way then through this controller? The answer is most likely no, I won't.

Then there is no real reason to extract the insert/create from the controller, unless you are not allowed to use the ORM provided by Laravel, or you are not using a framework at all. In that case I would move the database talk into the model, which is it's responsibility.

However, most of the time you don't need to overthink it.

public function(BookRequest $request)
{
	Book::create($request->validated());
} 

That line of code is in my opinion as clean as it gets, I use the eloquent in the controller directly, rather than injecting the model and then call store method on that.

jlrdw's avatar

Use laravel conventions, stick to MVC, and you are good.

At most if I have some complex search scenario I might have this in another class and call it from controller. But that's not too often.

And watch the 30 days to learn laravel course is my suggestion.

Please or to participate in this conversation.