Architecture of Complex Web Applications Sample
Architecture of Complex Web Applications Sample
applications
With examples in Laravel(PHP)
Adel F
This book is for sale at
http://leanpub.com/architecture-of-complex-web-applications
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean
Publishing process. Lean Publishing is the act of publishing an in-progress ebook
using lightweight tools and many iterations to get reader feedback, pivot until you
have the right book and build traction once you do.
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2. Bad Habits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
SRP violation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
CRUD-style thinking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
The worship of PHP dark magic . . . . . . . . . . . . . . . . . . . . . . . . . . 10
“Rapid” Application Development . . . . . . . . . . . . . . . . . . . . . . . . 12
Saving lines of code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Other sources of pain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3. Dependency injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Single responsibility principle . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Image uploader example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Extending interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Traits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Static methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
1. Introduction
“Software Engineering Is Art Of Compromise” wiki.c2.com
I have seen many projects evolve from a simple “MVC” structure. Many developers
explain the MVC pattern like this: “View is a blade file, Model is an Eloquent entity,
and one Controller to rule them all!” Well, not one, but every additional logic
usually implemented in a controller. Controllers become very large containers of
very different logic (image uploading, 3rd party APIs calling, working with Eloquent
models, etc.). Some logic are moved to base controllers just to reduce the copy-
pasting. The same problems exist both in average projects and in big ones with 10+
millions unique visitors per day.
The MVC pattern was introduced in the 1970-s for the Graphical User Interface.
Simple CRUD web applications are just an interface between user and database,
so the reinvented “MVC for web” pattern became very popular. Although, web
applications can easily become more than just an interface for a database. What does
the MVC pattern tell us about working with binary files(images, music, videos…),
3rd party APIs, cache? What are we to do if entities have non-CRUD behavior? The
answer is simple: Model is not just an Active Record (Eloquent) entity; it contains all
logic that work with an application’s data. More than 90 percent of a usual modern
complex web application code is Model in terms of MVC. Symfony framework
creator Fabien Potencier once said: “I don’t like MVC because that’s not how the
web works. Symfony2 is an HTTP framework; it is a Request/Response framework.”
The same I can say about Laravel.
Frameworks like Laravel contain a lot of Rapid Application Development features,
which help to build applications very fast by allowing developers to cut corners. They
are very useful in the “interface for database” phase, but later might become a source
of pain. I did a lot of refactorings just to remove them from the projects. All these
auto-magical things and “convenient” validations, like “quickly check that this email
Introduction 2
is unique in this table” are great, but the developer should fully understand how they
work and when better to implement it another way.
On the other hand, advice from cool developers like “your code should be 100% cov-
ered by unit tests”, “don’t use static methods”, and “depend only upon abstractions”
can become cargo cults for some projects. I saw an interface IUser with 50+ properties
and class User: IUser with all these properties copy-pasted(it was a C# project). I
saw a huge amount of “abstractions” just to reach a needed percentage of unit test
coverage. These pieces of advice are good, but only for some cases and they must be
interpreted correctly. Each best practice contains an explanation of what problem it
solves and which conditions the destination project should apply. Developers have
to understand them before using them in their projects.
All projects are different. Some of them are suitable for some best practices. Others
will be okay without them. Someone very clever said: “Software development is
always a trade-off between short term and long term productivity”. If I need some
functionality already implemented in the project in another place, I can just copy-
paste it. It will be very productive in the short term, but very quickly it will start to
create problems. Almost every decision about refactoring or using a pattern has the
same dilemma. Sometimes, it is a good decision to not use some pattern that makes
the code “better”, because the positive effect for the project will not be worth the
amount of time needed to implement it. Balancing between patterns, techniques and
technologies and choosing the most suitable combination of them for the current
project is one the most important skills of a software developer/architect.
In this book, I talk about common problems appearing in projects and how developers
usually solve them. The reasons and conditions for using these solutions are a very
important part of the book. I don’t want to create a new cargo cult :)
I have to warn you:
• This book is not for beginners. To understand the problems I will talk about,
the developer should at least have participated in one project.
• This book is not a tutorial. Most of the patterns will be observed superficially,
just to inform the reader about them and how and when they can help. The
links to useful books and articles will be at the end of the book.
• The example code will never be ideal. I can call some code “correct” and still
find a lot of problems in it, as shown in the next chapter.
2. Bad Habits
SRP violation
In this chapter, I’ll try to show how projects, written according to documentation,
usually grow up. How developers of real projects try to fight with complexity. Let’s
start with a simple example.
$user = User::create($request->all());
if(!$user) {
return redirect()->back()->withMessage('...');
}
return redirect()->route('users');
}
It was very easy to write. Then, new requirements appear. Add avatar uploading in
create and update forms. Also, an email should be sent after registration.
Bad Habits 4
$avatarFileName = ...;
\Storage::disk('s3')->put(
$avatarFileName, $request->file('avatar'));
return redirect()->route('users');
}
Some logic should be copied to update method. But, for example, email sending
should happen only after user creation. The code still looks okay, but the amount of
copy-pasted code grows. Customer asks for a new requirement - automatically check
images for adult content. Some developers just add this code(usually, it’s just a call
for some API) to store method and copy-paste it to update. Experienced developers
extract upload logic to new controller method. More experienced Laravel developers
find out that file uploading code becomes too big and instead create a class, for ex-
ample, ImageUploader, where all this upload and adult content-checking logic will
be. Also, a Laravel facade(Service Locator pattern implementation) ImageUploader
will be introduced for easier access to it.
Bad Habits 5
/**
* @returns bool|string
*/
public function upload(UploadedFile $file)
This function returns false if something wrong happens, like adult content or S3 error.
Otherwise, uploaded image url.
Controller methods became simpler. All image uploading logic was moved to another
class. Good. The project needs image uploading in another place and we already have
a class for it! Only one new parameter will be added to upload method: a folder where
images should be stored will be different for avatars and publication images.
New requirement - immediately ban user who uploaded adult content. Well, it
sounds weird, because current image analysis tools aren’t very accurate, but it’s a
requirement(it was a real requirement in one of my projects!).
Bad Habits 6
New requirement - application should not ban user if he uploads something wrong
to private places.
When I say “new requirement”, it doesn’t mean that it appears the next day. In big
projects it can take months and years and can be implemented by another developer
who doesn’t know why the code is written like this. His job - just implement this
task as fast as possible. Even if he doesn’t like some code, it’s hard to estimate how
much time this refactoring will take in a big system. And, much more important, it’s
hard to not break something. It’s a very common problem. I hope this book will help
to organize your code to make it more suitable for safe refactoring. New requirement
- user’s private places needs weaker rules for adult content.
The last requirement for this example - application shouldn’t ban user immediately.
Only after some tries.
Bad Habits 7
Okay, this code doesn’t look good anymore. Image upload function has a lot of
strange parameters about image checking and user banning. If user banning process
should be changed, developers have to go to ImageUploader class and implement
changes there. upload function call looks weird:
\ImageUploader::upload(
$request->file('avatar'), 'avatars', true, false);
Single Responsibility Principle was violated here. ImageUploader class has also
some other problems but we will talk about them later. As I mentioned before, store
and update methods are almost the same. Let’s imagine some very big entity with
huge logic and image uploading, other API’s calling, etc.
Bad Habits 8
Sometimes a developer tries to remove all this copy-paste by extracting the method
like this:
I saw this kind of method with 700+ lines. After many requirement changes, there
were a huge amount of if($update) checks. This is definitely the wrong way to
remove copy-pasting. When I refactored this method by creating different create
and update methods and extracting similar logic to their own methods/classes, the
code become much easier to read.
CRUD-style thinking
The REST is very popular. Laravel developers use resource controllers with ready
store, update, delete, etc. methods even for web routes, not only for API. It looks
very simple. Only 4 verbs: GET(read), POST(create), PUT/PATCH(update) and
DELETE(delete). It is simple when your project is just a CRUD application with
Bad Habits 9
create/update forms and lists with a delete button. But when the application becomes
a bit more complex, the REST way becomes too hard. For example, I googled “REST
API ban user” and the first three results with some API’s documentation were very
different.
PUT /api/users/banstatus
params:
UserID
IsBanned
Description
PUT /api/users/{id}/status
params:
status: guest, regular, banned, quarantine
There also was a big table: which status can be changed to which
and what will happen
As you see, any non-standard verb(ban) and REST becomes not so simple. Especially
for beginners. Usually, all other methods are implemented by the update method.
When I asked in one of the seminars how to implement user banning with REST, the
first answer was:
PUT /api/users/{id}
params:
IsBanned=true
Ok. IsBanned is the property of User, but when user actually was banned, we
should send, for example, an email for this user. This requirement consequences
very complicated conditions with comparing “old” and “new” values on user update
operation. Another example: password change.
Bad Habits 10
PUT /api/users/{id}
params:
oldPassword=***
password=***
oldPassword is not a user property. So, another condition at user update. This
CRUD-style thinking, as I call it, affects even the user interface. I always remember
“typical Apple product, typical Google product” image as the best illustration of the
problem.
$key = CacheKeys::getUserByIdKey($id);
Do you remember the dogma “Don’t use static functions”? Almost always, it’s
true. But this is a good example of exception. We will talk about it in further in
the Dependency Injection chapter. Well, when another project needed the same
functionality, I showed this class to the developer and said you can do the same.
After some time, he said that this class “isn’t very beautiful” and committed this
code:
/**
* @method static string getUserByIdKey(int $id)
* @method static string getUserByEmailKey(string $email)
*/
class CacheKeys
{
const USER_BY_ID = 'user_%d';
const USER_BY_EMAIL = 'user_email_%s';
static::fromCamelCase(
substr($input, 3, strlen($input) - 6))
);
}
$key = CacheKeys::getUserById($id);
Shortly, this code transforms “getUserById” string to “USER_BY_ID” and uses this
constant value. A lot of developers, especially young ones, like to make this kind of
“beautiful” code. Sometimes, it can save a lot of lines of code. Sometimes not. But it’s
always hard to debug and support. The developer should think 10 times before using
any “cool” dynamic feature of language.
class UserController
{
public function update($id)
{
$user = User::find($id);
if($user === null)
{
abort(404);
}
//...
}
}
Laravel starting from some version suggests to use implicit route binding. This code
does the same as previous:
Route::post('api/users/{user}', 'UserController@update');
class UserController
{
public function update(User $user)
{
//...
}
}
It definitely looks better and reduces a lot of “copy-pasted” code. Later, the project
can grow and caching will be implemented. For GET queries, it is better to use cache,
but not for POST (there are a lot of reasons to not use cached entities in update
operations). Another possible issue: different databases for read and write queries.
It happens when one database server can’t serve all of a project’s queries. Usually,
database scaling starts from creating one database to write queries and one or more
read databases. Laravel has convenient configurations for read&write databases. So,
the route binding code can now look like this:
Bad Habits 14
Route::get('api/users/{user}', 'UserController@edit');
Route::post('api/users/{userToWrite}', 'UserController@update');
class UserController
{
public function update($id)
{
$user = User::findOrFail($id);
//...
}
}
There is no need to “optimize” this one line of code. Frameworks suggest a lot of
other ways to lose control of your code. Be very careful with them.
A few words about Laravel’s convenient configuration for read&write databases.
It’s really convenient, but again, we lose control here. It isn’t smart enough. It just
uses read connection to select queries and write connection to insert/update/delete
queries. Sometimes we need to select from a write connection. It can be solved with
::onWriteConnection() helpers. But, for example, lazy loading relation will be fetched
from read connection again! In some very rare cases it made our data inconsistent.
Can you imagine how difficult it was to find this bug? In Laravel 5.5, one option
was added to fix that. It will send each query to write database after the first write
database query. This option partially solves the problem, but looks so weird.
Bad Habits 15
As a conclusion, I can say this: “Less magic in the code - much easier to debug and
support it”. Very rarely, in some cases, like ORM, is it okay to make some magic, but
only there.
$done = $user->getIsBlocked()
? $user->unblock()
: $user->block($request->get('reason'));
return response()->json([
'status' => $done,
'blocked' => $user->getIsBlocked()
]);
}
The developer wanted to save some lines of code and implemented user blocking
and unblocking in the same method. The problems started from naming. The not
Bad Habits 16
very precise ‘blockage’ noun instead of the natural ‘block’ and ‘unblock’ verbs. The
main problem is concurrency: two moderators could open the same user’s page and
try to block him. First one will block, but the other one will unblock! Some kind of
optimistic locking could solve this issue, but the idea of optimistic locking not very
popular in Laravel projects (I’ve found some packages, but they have less than 50
stars in github). The best solution is to create two separate methods for blocking and
unblocking.
$user = User::create($request->all());
if(!$user) {
return redirect()->back()->withMessage('...');
Dependency injection 18
return redirect()->route('users');
}
What kind of changes can this app have? Add/remove fields, add/remove entities…
It’s hard to imagine something more. I don’t think this code violates SRP. Almost.
Theoretically, redirecting to another route change is possible, but it’s not very
important. I don’t see any reason to refactor this code. New requirements can appear
with application growth: user avatar image uploading and email sending:
$avatarFileName = ...;
\Storage::disk('s3')->put(
$avatarFileName, $request->file('avatar'));
if(!$user->save()) {
return redirect()->back()->withMessage('...');
}
return redirect()->route('users');
}
Here are several responsibilities already. Something might be changed in image up-
loading or email sending. Sometimes it’s hard to catch the moment when refactoring
Dependency injection 19
should be started. If these changes appeared only for user entity, there is probably
no sense in changing anything. However, other parts of the application will for sure
use the image uploading feature.
I want to talk about two important characteristics of application code - cohesion and
coupling. They are very basic and SRP is just a consequence of them. Cohesion is the
degree to which all methods of one class or parts of another unit of code (function,
module) are concentrated in its main goal.
Close to SRP. Coupling between two classes (functions, modules) is the degree of
how much they know about each other. High coupling means that some knowledge is
shared between several parts of code and each change can cause a cascade of changes
in other parts of the application.
Current case with store method is a good illustration of losing code quality. It
contains several responsibilities - it loses cohesion. Image uploading responsibility
is implemented in a few different parts of the application - high coupling. It’s time
to extract this responsibility to its own class.
First try:
Dependency injection 20
$user->avatarUrl = $avatarFileName;
}
}
I gave this example because I constantly encounter the fact that developers, trying to
take out the infrastructure functionality, take too much with them. In this case, the
ImageUploader class, in addition to its primary responsibility (file upload), assigns
the value to the User class property. What is bad about this? The ImageUploader
class “knows” about the User class and its avatarUrl property. Any such knowledge
tends to change. You will also have to change the ImageUploader class. This is high
coupling again.
Lets try to write ImageUploader with a single responsibility:
Yes, this doesn’t look like a case where refactoring helped a lot. But let’s imagine that
ImageUploader also generates a thumbnail or something like that. Even if it doesn’t,
we extracted its responsibility to its own class and spent very little time on it. All
future changes with the image uploading process will be much easier.
Dependency injection 21
Dependency Injection
Well, we created ImageUploader class, but how do we use it in UserController::store
method?
Or just make the upload method static and call it like this:
ImageUploader::upload(...);
It was easy, right? But now store method has a hard-coded dependency to the
ImageUploader class. Lets imagine a lot of methods with this hard dependency and
then the company decided to use another image storage. Not for all images, only for
some of them. How would developers usually implement that? They just create An-
otherImageUploader class and change ImageUploader to AnotherImageUploader
in all needed methods. But what happened? According SRP, each of these methods
should have only one reason to change. Why does changing the image storage cause
several changes in the ImageUploader class dependents?
Dependency injection 22
As you see, the application looks like metal grid. It’s very hard to take, for example,
the ImageUploader class and move it to another project. Or just unit test it. Image-
Uploader can’t work without Storage and ThumbCreator classes, and they can’t
work without their dependencies, etc. Instead of direct dependencies to classes, the
Dependency Injection technique suggests just to ask dependencies to be provided
to the class.
Laravel and many other frameworks contain “DI container” - a special service, which
takes all responsibilities of creating class instances and injecting them to other classes.
So, method store can be rewritten like this:
//...
}
Here, the Laravel feature was used to request dependencies directly in the parameters
of the controller method. Dependencies have become softer. Classes do not create
dependency instances and do not require static methods. However, both the store
method and the ImageUploader class refer to specific classes. The Dependency In-
version principle says “High-level modules should not depend on low-level modules.
Both should depend on abstractions”. Abstractions should not depend on details.
Details should depend on abstractions. The requirement of abstraction in OOP
languages is interpreted unequivocally: dependence should be on interfaces, and
not on classes. However, I have repeatedly stated that projects are different. Let’s
consider two options.
You’ve probably heard about Test-driven Development (TDD) techniques. Roughly
speaking, TDD postulates writing tests at the same time as the code. A review of TDD
Dependency injection 24
techniques is beyond the scope of this book, so we will look at just one of its faces.
Imagine that you need to implement the ImageUploader class, but the Storage and
ThumbCreator classes are not yet available. We will discuss unit testing in detail in
the corresponding chapter, so we will not dwell on the test code now. You can simply
create the Storage and ThumbCreator interfaces, which are not yet implemented.
Then you can simply write the ImageUploader class and tests for it, creating mocks
from these interfaces (we will talk about mocks later).
interface Storage
{
//...Some methods
}
interface ThumbCreator
{
//...Some methods
}
$this->storage->...
}
}
The ImageUploader class still cannot be used in the application, but it has already
been written and tested. Later, when the implementations of these interfaces are
ready, you can configure the container in Laravel, for example:
$this->app->bind(Storage::class, S3Storage::class);
$this->app->bind(ThumbCreator::class, ImagickThumbCreator::class);
After that, the ImageUploader class can be used in the application. When the
container creates an instance of the ImageUploader class, it will create instances
of the required classes and substitute them instead of interfaces into the constructor.
TDD has proven itself in many projects where it is part of the standard. I also like
this approach. Developing with TDD, you get little comparable pleasure. However,
I have rarely seen its use. It imposes quite serious requirements on the developer for
architectural thinking. Developers need to know what to put in separate interfaces
and classes and decompose the application in advance.
Usually everything in projects is much simpler. First, the ImageUploader class is
written, in which the logic of creating thumbnails and the logic of saving everything
to the repository are concentrated. Then, perhaps, is the extraction of logic into
Dependency injection 26
the classes Storage and ThumbCreator, leaving only a certain orchestration over
these two classes in ImageUploader. Interfaces are not used. Occasionally a very
interesting event takes place in such projects - one of the developers reads about the
Dependency Inversion principle and decides that there are serious problems with
the architecture on the project. Classes do not depend on abstractions! Interfaces
should be introduced immediately! But the names ImageUploader, Storage, and
ThumbCreator are already taken. As a rule, in this situation, developers choose one
of two terrible ways to extract the interface.
The first is the creation of *Contracts namespace and the creation of all interfaces
there. As an example, Laravel source:
namespace Illuminate\Contracts\Cache;
interface Repository
{
//...
}
namespace Illuminate\Contracts\Config;
interface Repository
{
//...
}
namespace Illuminate\Cache;
namespace Illuminate\Config;
use ArrayAccess;
use Illuminate\Contracts\Config\Repository as ConfigContract;
There is a double sin here: the use of the same name for the interface and class, as well
as the use of the same name for different program objects. The namespace feature
provides an opportunity for such detour maneuvers. As you can see, even in the
source code of classes, you have to use CacheContract and ConfigContract aliases.
For the rest of the project, we have 4 program objects with the name Repository. And
the classes that use the configuration and cache via DI look something like this (if
you do not use aliases):
use Illuminate\Contracts\Cache\Repository;
class SomeClassWhoWantsConfigAndCache
{
/** @var Repository */
private $cache;
Only variable names help to guess what dependencies are used here. However, the
names for Laravel-facades for these interfaces are quite natural: Config and Cache.
With such names for interfaces, the classes that use them would look much better.
The second option is to use the Interface suffix, as such: creating an interface with the
name StorageInterface. Thus, having class Storage implements StorageInterface,
we postulate that there is an interface and its default implementation. All other
classes that implement it, if they exist at all, appear secondary compared to Storage.
The existence of the StorageInterface interface looks very artificial: it was created
either to make the code conform to some principles, or only for unit testing. Such
a phenomenon is found in many languages. In C#, the IList interface and the List
class, for example. In Java, prefixes or suffixes to interfaces are not accepted, but this
often happens there:
This is also the situation with the default implementation of the interface. There are
two possible situations:
If there is no direct need to use interfaces on the project, then it is quite normal to
use classes and Dependency Injection. Let’s look again at ImageUploader:
Dependency injection 29
It uses some software objects Storage and ThumbCreator. The only thing he uses
is public methods. It absolutely doesn’t care whether it’s interfaces or real classes.
Dependency Injection, removing the need to instantiate objects from classes, gives
us super-abstraction: there is no need for classes to even know what type of program
object it is dependent on. At any time, when conditions change, classes can be
converted to interfaces with the allocation of functionality to a new class (S3Storage).
Together with the configuration of the DI-container, these will be the only changes
that will have to be made on the project. Of course, if it’s a public package, the
code must be written as flexibly as possible and all dependencies must be easily
replaceable, therefore interfaces are required. However, on a regular project, using
dependencies on real classes is an absolutely normal trade-off.
Dependency injection 30
Inheritance
Inheritance is called one of the main concepts of OOP and developers adore it.
However, quite often inheritance is used in the wrong key, when a new class needs
some kind of functionality and this class is inherited from the class that has this
functionality. A simple example. Laravel has an interface Queue and many classes
implementing it. Let’s say our project uses RedisQueue.
interface Queue
{
public function push($job, $data = '', $queue = null);
}
Once it became necessary to log all the tasks in the queue, the result was the
OurRedisQueue class, which was inherited from RedisQueue.
The task is completed: all push methods calls are logged. After some time, the
framework is updated and a new method pushOn appears in the Queue interface.
Dependency injection 31
It is actually a push alias, but with a different order of parameters. The expected
implementation appears in the RedisQueue class.
interface Queue
{
public function push($job, $data = '', $queue = null);
public function pushOn($queue, $job, $data = '');
}
Because OurRedisQueue inherits the RedisQueue, we did not need to take any
action during the upgrade. Everything works as before and the team gladly began
using the new pushOn method.
In the new update, the Laravel team could, for some reason, do some refactoring.
Dependency injection 32
Refactoring is absolutely natural and doesn’t change the class contract. It still
implements the Queue interface. However, after some time after this update, the
team notices that logging does not always work. It is easy to guess that now it will
only log push calls, not pushOn. When we inherit a non-abstract class, this class has
two responsibilities at a high level. A responsibility to their own clients, as well as to
the inheritors, who also use its functionality. The authors of the class may not even
suspect the second responsibility, and this can lead to complex, elusive bugs on the
project. Even such a simple example quite easily led to a bug that would not be so
easy to catch. To avoid such difficulties in my projects, all non-abstract classes are
marked as final, thus prohibiting inheritance from myself. The template for creating
a new class in my IDE contains the ‘final class’ instead of just the ‘class’. Final classes
have responsibility only to their clients.
By the way, Kotlin language designers seem to think the same way and decided to
make classes there final by default. If you want your class to be open for inheritance,
the ‘open’ or ‘abstract’ keyword should be used:
Dependency injection 33
I like this :)
However, the danger of inheriting an implementation is still possible. An abstract
class with protected methods and its descendants can fall into exactly the same sit-
uation that I described above. The protected keyword creates an implicit connection
between parent class and child class. Changes in the parent can lead to bugs in the
children. The DI mechanism gives us a simple and natural opportunity to ask for
the implementation we need. The logging issue is easily solved using the Decorator
pattern:
$this->app->when(LoggingQueue::class)
->needs(Queue::class)
->give(RedisQueue::class);
Warning: this code will not work in a real Laravel environment, because the
framework has a more complex procedure for initiating these classes. This container
configuration will inject an instance of LoggingQueue to anyone who wants to get a
Queue. LoggingQueue will get a RedisQueue instance as a constructor parameter.
The Laravel update with a new pushOn method results in an error - LoggingQueue
does not implement the required method. Thus, we immediately implement logging
of this method, also.
Plus, you probably noticed that we now completely control the constructor. In the
variant with inheritance, we would have to call parent::__construct and pass on
everything that it asks for. This would be an additional, completely unnecessary
link between the two classes. As you can see, the decorator class does not have any
implicit links between classes and allows you to avoid a whole class of troubles in
the future.
/**
* @param UploadedFile $file
* @param string $folder
* @param bool $dontBan
* @param bool $weakerRules
* @param int $banThreshold
* @return bool|string
*/
public function upload(
UploadedFile $file,
string $folder,
bool $dontBan = false,
bool $weakerRules = false,
int $banThreshold = 5)
{
$fileContent = $file->getContents();
if(check failed)
if(!$dontBan) {
if(\RateLimiter::..., $banThreshold)) {
$this->banUser(\Auth::user());
}
}
return false;
}
$this->fileSystemManager
->disk('...')
->put($fileName, $fileContent);
return $fileName;
}
Basic refactoring
Simple image uploading responsibility becomes too big and contains some other
responsibilities. It definitely needs some refactoring.
If ImageUploader will be called from console command, the Auth::user() command
will return null and ImageUploader has to add a ‘!== null’ check to its code. Better to
provide the User object by another parameter(User $uploadedBy), which is always
not null. The user banning functionality can be used somewhere else. Now it’s only
Dependency injection 37
2 lines of code, but in the future it may contain some email sending or other actions.
Better to create a class for that.
Next, the “ban user after some wrong upload tries” responsibility. $banThreshold
parameter was added to the function parameters by mistake. It’s constant.
->tooManyAttempts(
'user_wrong_image_uploads_' . $user->id,
self::BAN_THRESHOLD);
if($rateLimiterResult) {
$this->banUserCommand->banUser($user);
return false;
}
}
}
Our system’s wrong image uploading reaction might be changed in the future. These
changes will only affect this class. Next, the responsibility to remove is “image
content checking”:
/**
* @param string $imageContent
* @param bool $weakerRules
* @return bool true if content is correct
*/
public function check(
string $imageContent,
bool $weakerRules): bool
{
// Some checking using $this->googleVision,
Dependency injection 39
/**
* @param UploadedFile $file
* @param User $uploadedBy
* @param string $folder
* @param bool $dontBan
* @param bool $weakerRules
* @return bool|string
*/
public function upload(
UploadedFile $file,
Dependency injection 40
User $uploadedBy,
string $folder,
bool $dontBan = false,
bool $weakerRules = false)
{
$fileContent = $file->getContents();
if(!$this->imageGuard->check($fileContent, $weakerRules)) {
if(!$dontBan) {
$this->listener->handle($uploadedBy);
}
return false;
}
$this->fileSystemManager
->disk('...')
->put($fileName, $fileContent);
return $fileName;
}
}
ImageUploader lost some responsibilities and is happy about it. It doesn’t care about
how to check images and what will happen with a user who uploaded something
wrong now. It only makes some orchestration job. But I still don’t like a parameters
of upload method. Responsibilities were removed from ImageUploader, but their
parameters are still there and upload method calls still look ugly:
Boolean parameters always look ugly and increase the cognitive load for reading the
code. The new boolean parameter might be added if the requirement to not check
images will appear… I’ll try to remove them two different ways:
Dependency injection 41
• OOP way
• Configuration way
OOP way
I’m going to use polymorphism, so I have to introduce interfaces.
interface ImageChecker
{
public function check(string $imageContent): bool;
}
interface WrongImageUploadsListener
{
public function handle(User $user);
}
ImageChecker $imageChecker,
FileSystemManager $fileSystemManager,
WrongImageUploadsListener $listener)
{
$this->imageChecker = $imageChecker;
$this->fileSystemManager = $fileSystemManager;
$this->listener = $listener;
}
/**
* @param UploadedFile $file
* @param User $uploadedBy
* @param string $folder
* @return bool|string
*/
public function upload(
UploadedFile $file,
User $uploadedBy,
string $folder)
{
$fileContent = $file->getContents();
if (!$this->imageChecker->check($fileContent)) {
$this->listener->handle($uploadedBy);
return false;
}
$this->fileSystemManager
->disk('...')
->put($fileName, $fileContent);
return $fileName;
}
Dependency injection 45
The logic of boolean parameters was moved to interfaces and their implementors.
Working with FileSystemManager also can be simplified by creating a facade for
it (I’m talking about Facade pattern, not Laravel facades). The only problem now
is instantiating the configured ImageUploader instance for each client. It can be
solved by a combination of Builder and Factory patterns. This will give full control
of configuring the needed ImageUploader object to client code.
Also, it might be solved by configuring DI-container rules, which ImageUploader
object will be provided for each client. All configuration will be placed in one
container config file. I think for this task the OOP way looks too over-engineered. It
might be solved simply by one configuration file.
Configuration way
I’ll use a Laravel configuration file to store all needed configuration. config/im-
age.php:
return [
'disk' => 's3',
'avatars' => [
'check' => true,
'ban' => true,
'folder' => 'avatars',
],
'gallery' => [
'check' => true,
'weak' => true,
'ban' => false,
'folder' => 'gallery',
],
];
/**
* @param UploadedFile $file
* @param User $uploadedBy
* @param string $type
* @return bool|string
*/
public function upload(
UploadedFile $file,
User $uploadedBy,
string $type)
Dependency injection 47
{
$fileContent = $file->getContents();
if(!$this->imageGuard->check($fileContent, $weak)){
return false;
}
}
$defaultDisk = $this->config->get('image.disk');
$this->fileSystemManager
->disk(Arr::get($options, 'disk', $defaultDisk))
->put($fileName, $fileContent);
return $fileName;
}
}
Well, the code looks not as clean as the “OOP” variant, but its configuration and
implementation are very simple. For the image uploading task I prefer this way, but
for other tasks with more complicated configurations or orchestrations, the “OOP”
way might be more optimal.
Dependency injection 48
Extending interfaces
Sometimes, we need to extend an interface with some method. In the Domain layer
chapter, I’ll need a multiple events dispatch feature in each method of service classes.
Laravel’s event dispatcher only has the single dispatch method:
interface Dispatcher
{
//...
/**
* Dispatch an event and call the listeners.
*
* @param string|object $event
* @param mixed $payload
* @param bool $halt
* @return array|null
*/
public function dispatch($event,
$payload = [], $halt = false);
}
But I don’t want to copy-paste it in each method of each service class. C# and Kotlin
language’s “extension method” feature solves this problem:
Dependency injection 49
namespace ExtensionMethods
{
public static class MyExtensions
{
public static void MultiDispatch(
this Dispatcher dispatcher, Event[] events)
{
foreach (var event in events) {
dispatcher.Dispatch(event);
}
}
}
}
using ExtensionMethods;
//...
dispatcher.MultiDispatch(events);
PHP doesn’t have this feature. For your own interfaces, the new method can be
added to the interface and implemented in each implementor. In case of an abstract
class(instead of interface), the method can be added right there without touching
inheritors. That’s why I usually prefer abstract classes. For vendor’s interfaces, this
is not possible, so the usual solution is:
Dependency injection 50
use Illuminate\Contracts\Events\Dispatcher;
$this->dispatchEvents($events);
}
}
Dependency injection 51
interface MultiDispatcher
{
public function multiDispatch(array $events);
}
use Illuminate\Contracts\Events\Dispatcher;
{
public function boot()
{
$this->app->bind(
MultiDispatcher::class,
LaravelMultiDispatcher::class);
}
}
BaseService class can be deleted, and service classes will just use this new interface:
$this->dispatcher->multiDispatch($events);
}
}
As a bonus, now I can switch from the Laravel events engine to another, just by
another implementation of the MultiDispatcher interface.
When clients want to use the full interface, just with a new method, a new interface
can extend the base one:
Dependency injection 53
For big interfaces, it might be annoying to delegate each method there. Some IDEs
for other languages(like C#) have commands to do it automatically. I hope PHP IDEs
will implement that, too.
Traits
PHP traits are the magical way to “inject” dependencies for free. They are very
powerful: they can access private fields of the main class and add new public and
even private methods there. I don’t like them, because they are part of PHP dark
magic, powerful and dangerous. I use them in unit test classes, because there is no
good reason to implement the Dependency Injection pattern there, but avoid doing
it in main application code. Traits are not OOP, so every case with them can be
implemented using OOP.
trait MultiDispatch
{
public function multiDispatch(array $events)
{
foreach($events as $event)
{
$this->dispatcher->dispatch($event);
}
}
}
$this->multiDispatch($events);
}
}
The MultiDispatch trait assumes that the host class has a dispatcher field of the
Dispatcher class. It is better to not make these kinds of implicit dependencies. A
solution with the MultiDispatcher interface is more convenient and explicit.
// Foo.cs file
partial class Foo
{
public void bar(){}
}
// Foo2.cs file
partial class Foo
{
public void bar2(){}
}
When the same happens in PHP, traits can be used as a partial class. Example from
Laravel:
Big Request class has been separated for several traits. When some class “wants” to
be separated, it’s a very big hint: this class has too many responsibilities. Request
class can be composed by Session, RequestInput and other classes. Instead of
combining a class with traits, it is better to separate the responsibilities, create a class
for each of them, and use composition to use them together. Actually, the constructor
of Request class tells a lot:
Dependency injection 57
class Request
{
public function __construct(
array $query = array(),
array $request = array(),
array $attributes = array(),
array $cookies = array(),
array $files = array(),
array $server = array(),
$content = null)
{
//...
}
//...
}
Traits as a behavior
Eloquent traits, such as SoftDeletes, are examples of behavior traits. They change
the behavior of classes. Eloquent classes contain at least two responsibilities: storing
entity state and fetching/saving/deleting entities from a database, so behavior traits
can also change the way entities are fetched/saved/deleted and add new fields and
relations there. What about the configuration of a trait? There are a lot of possibilities.
SoftDeletes trait:
Dependency injection 58
trait SoftDeletes
{
/**
* Get the name of the "deleted at" column.
*
* @return string
*/
public function getDeletedAtColumn()
{
return defined('static::DELETED_AT')
? static::DELETED_AT
: 'deleted_at';
}
}
trait DetectsChanges
{
//...
public function shouldLogUnguarded(): bool
{
if (! isset(static::$logUnguarded)) {
return false;
}
if (! static::$logUnguarded) {
return false;
}
if (in_array('*', $this->getGuarded())) {
return false;
}
return true;
Dependency injection 59
}
}
class SomeModel
{
protected function behaviors(): array
{
return [
new SoftDeletes('another_deleted_at'),
DetectsChanges::create('column1', 'column2')
->onlyDirty()
->logUnguarded()
];
}
}
Explicit behaviors with a convenient configuration without polluting the host class.
Excellent! Fields and relations in Eloquent are virtual, so its implementations are also
possible.
Traits can also add public methods to the host class interface… I don’t think it’s a
good idea, but it’s also possible with something like macros, which are widely used
in Laravel. Active record implementations are impossible without magic, so traits and
behaviors will also contain it, but behaviors look more explicit, more object oriented,
and configuring them is much easier.
Of course, Eloquent behaviors exist only in my imagination. I tried to imagine a better
alternative and maybe I don’t understand some possible problems, but I definitely like
them more than traits.
Useless traits
Some traits are just useless. I found this one in Laravel sources:
Dependency injection 60
trait DispatchesJobs
{
protected function dispatch($job)
{
return app(Dispatcher::class)->dispatch($job);
}
I don’t know why one method is protected and another one is public… I think it’s
just a mistake. It just adds the methods from Dispatcher to the host class.
class WantsToDispatchJobs
{
use DispatchesJobs;
$this->dispatch(...);
}
}
class WantsToDispatchJobs
{
public function someMethod()
{
//...
\Bus::dispatch(...);
//or just
dispatch(...);
}
}
This “simplicity” is the main reason why people don’t use Dependency Injection in
PHP.
class WantsToDispatchJobs
{
/** @var Dispatcher */
private $dispatcher;
$this->dispatcher->dispatch(...);
}
}
This class is much simpler than previous examples, because the dependency on
Dispatcher is explicit, not implicit. It can be used in any application, which can create
Dependency injection 62
the Dispatcher instance. It doesn’t need a Laravel facade, trait or ‘dispatch’ function.
The only problem is bulky syntax with the constructor and private field. Even with
convenient auto-completion from the IDE, it looks a bit noisy. Kotlin language syntax
is much more elegant:
PHP syntax is a big barrier to using DI. I hope something will reduce it in the future
(language syntax or IDE improvements).
After years of using and not using traits, I can say that developers create traits for
two reasons:
Static methods
I wrote that using a static method of another class creates a hard coded dependency,
but sometimes it’s okay. Example from previous chapter:
Dependency injection 63
$key = CacheKeys::getUserByIdKey($id);
Cache keys are needed in at least two places: cache decorators for data fetching
classes and event listeners to catch entity changed events, and delete old ones from
the cache.
I could use this CacheKeys class by DI, but it doesn’t make sense. All these decorator
and listener classes form some structure which can be called a “cache module” for
this app. CacheKeys class will be a private part of this module. All other application
code shouldn’t know about it.
Dependency injection 64
Using static methods for these kinds of internal dependencies that don’t work with
the outside world(files, database, APIs) is normal practice.
Conclusion
One of the biggest advantages of using the Dependency Injection technique is the
explicit contract of each class. The public methods of this class fully describe what it
can do. The constructor parameters fully describe what this class needs to do its job.
In big, long-term projects it’s a big advantage: classes can be easily unit tested and
used in different conditions(with dependencies provided). All these magic methods,
like __call, Laravel facades and traits break this harmony.
However, I can’t imagine HTTP controllers outside Laravel applications and almost
nobody unit tests them. That’s why I use typical helper functions (redirect(), view())
and Laravel facades (Response, URL) there.
This is a free sample of the book. Full version is here - https://adelf.tech/2019/architecture-
of-complex-web-applications¹
¹https://adelf.tech/2019/architecture-of-complex-web-applications