OOP Principles for Writing Better Code in Laravel Explained
By: Peter Matisko
Tags: Software Development, PHP, Laravel, Oop, Programming

Start Your Design With Layers; Your Life Will Become Much Easier!

One of the most important programming principles is an architectural pattern (1, 2) — simply, layers. Let’s think of what each layer we typically need does. Is there any good approach that fits most standard situations of a typical web application?

Possible layers of a typical web application.

1. Routing. A user or an API client calls a certain URL and expects something to happen. Our application has to recognize the URL. That is done by a routing class. In Laravel, there is a great routing system with a lot of helpful features [1]. VueJs also includes a great routing mechanism, [1].

2. The application checks important properties of the call. Does it have proper headers? Is the session active? Is the user logged in? Is the call authorized? Does the user have proper permissions to use this call?

All kinds of call validations are managed by middleware. In Laravel, you can create as many middlewares as needed. Many are prepared for you right away.

3. Laravel creates a Request object encapsulating all the input data. Laravel sends the Request object into a Controller.

Now, we are in a Controller, which is a kind of entry point of our application. At the end of the controller’s function, the app is supposed to return some data. Between the entry point and the return statement, we have a business logic doing some magic that our client wants.

Note: Controller is a place where most of the tutorials for Laravel end. For trivial examples, it doesn’t make sense to build more layers. However, because of that, new programmers have a very hard time learning something more advanced. I went through this struggle myself as well.

4. A controller is supposed to validate the incoming data, call a service doing some business logic, and then form a response in the desired format (e.g. a json or a blade view).

5. Data validation is an extremely important task. Many programmers don’t pay enough attention to it creating insecure applications. SQL injection is just one of the well-known security threats.

Remember that you should never trust the client’s data. Always validate them, even when they come from your own front-end client. Remember that the API may be used by other applications.

6. After the data is validated, we are ready to call a service. The business logic should be separated from the controller and if possible, also separated from the framework itself. It is easier to unit test your app if classes do not depend on the framework. An example of the recommendation is to avoid using resolve (‘ClassName’) ; Prefer inversion of control or passing dependencies to the lower layers manually.

What is business logic? Most of the time, your application will need CRUD operations. The service class will take the incoming data and save it to the database. Or, it will read the data, put it into the desired format, and return back. If anything more complex is needed, the service class will call other services.

7. If the service needs access to a database, it calls a Repository class. Repository handles all calls for a database. The layers above do not know how the data is stored. You simply call

$userRepository->getById(1);

And that’s it. It must not matter for the service, whether your DB is MySQL, NoSQL or data are saved somewhere in the cloud via API.

8. Laravel native models are very powerful and can save some time for creating simple applications. However, they violate several OOP rules. For this reason, I decided to create my own system, and completely get rid of the Active Record (anti) pattern. I will explain this idea later in the article.

A repository calls a DataMapper that knows how to get data from the database. The DataMapper class is database-specific. It has to be created separately for each type of DB (mySql, noSql, files). However, its interface should be exactly the same. My DataMappers always have these common public functions:

public function getById($id, $attributes = '*');

public function getBySlug($slug, $attributes = '*');

public function patchById (IModel $model, $selectedAttributes);

public function deleteById ($id);

public function search (array $criteria, $attributes = '*');

public function store (IModel &$model);

If a DataMapper requires some special functions, they are implemented within the child class. E.g. UserDataMapper needs functions such as getByUserName(), isRoot(), etc.

9. DataMapper prepares data for the Laravel Query Builder. This is the lowest layer of the application. The query builder returns data or throws an exception if something fails.

Important rule: No class or function should skip layers! It creates tight coupling, and ultimately leads to problems with testing and distributing work among your teammates. This rule is really important, however, even Laravel supports breaking it in a horrible way.

Example: Laravel Validator is a powerful tool. No question about it. However, it allows you to create a validation rule like this:

'state' => 'exists:states'

It looks simple and elegant. However, from the OOP rules perspective, it is a horrible concept. It connects your top layer with the bottom layer being the SQL database. On top of that, it connects your validation with the actual name of the database column. This is an extremely tight coupling that will ultimately kick you in the back. Once you go this path, you almost certainly end up painfully rewriting a huge portion of your code in the future.

On top of that, you are allowed to do even a worse thing with this concept. You can validate user’s data like this (pseudo-code):

IF Validator( 'email' => 'exists:emails') == False THEN createNewUser()

Why is this code dangerous? What if the email appears in the DB before your code reaches the save() function? It will most likely not happen 99% of the time. However, when your app gets busier serving more clients, you have to expect this kind of situation happens. Your app would crash without a warning. The validation passes, the email is not taken, but the data will not be saved, the email is already there. An exception you’ve never seen may crash the app.