12

Working in a Team

The main goal of this book is to enable you to write code that can be understood, maintained, and extended by you and others. Most of the time, being a PHP developer means that you do not work alone on a project or a tool. And even if you started writing code alone, chances are high that at some point, another developer will join you – be it on a commercial product, or your open source package where other developers start adding new features or bug fixes.

There will always be multiple ways to carry out a task in software development. This is what makes working in a team more challenging when you want to write clean code together. In this chapter, you will find several tips and best practices on how to set up coding standards and coding guidelines. We will also talk about how code reviews will improve the code and ensure the guidelines are kept.

We will also explore the topic of design patterns in more detail at the end of this chapter. These patterns can help your team solve typical software development problems because they offer well-tested solutions.

This chapter will include the following sections:

  • Coding standards
  • Coding guidelines
  • Code reviews
  • Design patterns

Technical requirements

If you followed along with the previous chapters, you do not require any additional setup.

The code samples for this chapter can be found in our GitHub repository: https://github.com/PacktPublishing/Clean-Code-in-PHP.

Coding standards

In the previous chapters, you learned a lot about writing high-quality code. Yet, it is not enough if you do it by yourself only. When you work in a team, you will most likely have the problem that other developers have a different understanding of quality and are on a different skill level than you are.

This is harmful to your code because it might lead to lazy compromises, where the involved parties agree on a way, just to have their peace. Therefore, if you want to work effectively in a team, you want to standardize your work as much as possible.

It makes sense to start with the low-hanging fruit: code formatting. This goes down to the very basics, such as agreeing on how many spaces should be used to indent lines, or where braces should be placed. But why is this even important?

We already shortly addressed this topic in Chapter 5, Optimizing Your Time and Separating Responsibilities. However, we want to expand on it at this point. The main advantage of having a common coding standard (also called coding style) is to reduce cognitive friction when reading code.

Cognitive friction

Cognitive friction basically describes the required mental effort for our brain to process information. Imagine, for example, you read a book, where every other paragraph was written in a different font, size, or line spacing. You would still be able to read it, but it would become annoying or tiring soon. The same applies to reading code.

Introducing a coding standard to a project is relatively easy, thanks to the tools we already presented to you earlier in this book. Agreeing with others on a common standard, on the other hand, requires more work. That is why, in this section, we want to show you how to easily align on a common coding standard.

Going with existing standards

Setting up standards together with others can be a long and painful process. However, nowadays, you do not argue about the size of a sheet of paper anymore. In European countries, the DIN A4 standard is widely accepted, while in other countries, such as the US, you would use the US Letter Size without asking why. Most people accept these measures, and following these standards makes life a bit easier – one thing less to care about.

The same applies to coding standards, which define how you format your code. Of course, you could argue for hours with your teammates about whether tabs or spaces should be used for the indentation. Both sides will come up with valid arguments, and you will never find the right answer, as there simply is no right and wrong here. And once you got the question about indentation sorted, the next topic to discuss could be the placement of brackets. Should they appear in the same line, or in the next?

We do not necessarily need to agree with every detail of a standard, but undoubtedly, it saves time and nerves to use existing norms. In the PHP ecosystem, there are Coding Standards that already exist that you could utilize. A huge additional benefit of doing so is that the code sniffers have built-in rule sets for these standards. In the next section, we will talk about probably the best-known Coding Standard for PHP.

PHP-FIG and PSR

PHP itself has no official Coding Standard. Historically, each major PHP framework that existed, or still exists today, introduced some sort of standards because the developers quickly realized that using them has its benefits.

However, since every project used its own standards, the PHP world ended up with a mixture of different formatting standards. Back in 2009, when the PHP-FIG (PHP Framework Interoperability Group (PHP-FIG) was formed, which consisted of members from all the important PHP projects and frameworks of that time, they wanted to solve exactly these kinds of problems.

At that time, Composer was becoming more and more important, and packages were introduced that could easily be used across different frameworks. To keep the code somewhat consistent, a mutual way to write code was agreed upon: the PHP Standard Recommendations (PSRs) were born.

To make the autoloader of Composer work, it was necessary to agree on how to name classes and directories. This was done with the very first standard recommendation, PSR-0 (yes, nerds start counting at 0), which was eventually replaced by PSR-4.

The first Coding Standard recommendation was introduced with PSR-1 and PSR-2. PSR-2 was later replaced by PSR-12, which contained rules for language features of newer PHP versions.

Although PSR-12 addresses the code style, it does not cover naming conventions or how to structure the code. This is often still predefined by the framework you use. The Symfony framework, for example, has its own set of Coding Standards that are based on the aforementioned PSR-4 and PSR-12, but add further guidelines, for example, conventions on naming or documentation. Even if you do not use a framework at all and just pick single components to build an application, you could consider using these guidelines, which you will find on the Symfony website: https://symfony.com/doc/current/contributing/code/standards.html.

PER coding style

PSR-12 was released in 2019 and thus does not cover the latest PHP features anymore. Therefore, at the time of writing this book, PHP-FIG released the PER Coding Style 1.0.0 (PER is short for PHP Extended Recommendation). It is based on PSR-12 and contains some additions to it. In the future, the PHP-FIG no longer plans to release any new Coding Standards related to PSRs, but new versions of this PER, if it is required. It is very likely that the code quality tools we featured in this book will pick up the new PER soon. You will find more information about it here: https://www.php-fig.org/per/coding-style.

Over time, the PHP-FIG has introduced over a dozen recommendations, and more are in the making. They cover topics such as how to integrate logging, caching, and HTTP clients, to just name a few. You will find a complete list on the official website: https://www.php-fig.org.

Problems with PHP-FIG and PSR

The PHP-FIG should not be considered the official PHP authority, and neither should any PSR be taken as indisputable. In fact, many important frameworks such as Symfony or Laravel are not part of the PHP-FIG anymore, since the recommended standards have interfered too much with their internals. Looking at all the PSRs that are available today, you could even regard them as their own meta-framework. This is not to diminish the relevance of many recommendations though – we just want you to not blindly accept them as granted.

Enforcing coding standards in your IDE

There are several ways to enforce coding standards. In the previous chapter, Chapter 11, Continuous Integration, we explained how to make sure that no wrongly formatted code can spoil the code base. This worked fine, yet it requires an additional step, even if we let our tools fix the code formatting automatically because we need to commit those changed files again. So, would it not actually be useful if our code editor, or IDE, would help us with formatting the code while we write it?

Modern code editors usually have built-in functionality that assists you with adhering to your preferred coding standards, if you configure them. If not built-in, this functionality can at least be provided with plugins.

There are two basic ways your editor could support you:

  • Highlighting the coding standard violations: The IDE marks those parts of the source code that need to be corrected. It will not change the code actively though.
  • Reformatting the code: Either the IDE or an additional plugin takes care of formatting the code, for example, by running a code style fixer such as PHP_CS_Fixer. This can be done upon manual request, or every time a file is saved.

Reformatting the code on file save is a very convenient way to ensure that your code meets the coding standards. How to set this up depends on which IDE you are using, so we will not elaborate on this further in this book.

We would still recommend using Git hooks and continuous integration as the second layer of checks to make sure no badly formatted code gets pushed to the project repository. You can never be sure whether a team member accidentally or willingly disabled the automated reformatting or did not care about the highlighted parts of the code.

Coding Standards are all about how to format code consistently. But that is not all you should agree on when working in a team – in the next section, we will show you what other aspects are worth agreeing upon.

Coding guidelines

In the previous section, we talked about why you should introduce Coding Standards. Once this is accomplished, you should consider setting up coding guidelines. Both topics sound very familiar, and indeed, they are. Yet while Coding Standards usually focus on how to format code, coding guidelines define how to write code. This, of course, includes defining which Coding Standard to use, but covers a lot more, as you will learn in this section.

What does how to write code exactly mean? Usually, there is more than one way to achieve things when writing software. Take the widely known model-view-controller (MVC) pattern, for example. It is used to divide the application logic into three types of interconnected elements – the models, the views, and the controllers. It does not explicitly define where to place the business logic, though. Should it be located inside the controllers, or rather inside the models?

There is no clear right or wrong answer to this question. Our recommendation, however, would be the fat models, skinny controllers approach: business logic should not be written within the controllers, as they are the binding element between the views and your problem-specific code. Also, the controllers usually contain a lot of framework-specific code, and it is good practice to keep that out of your business logic as much as possible.

Regardless of our recommendation, it should be defined in the coding guidelines of your project how you think your team should handle this question. Otherwise, you will most likely end up having both approaches in your code base.

Usually, coding guidelines cover questions such as how to name methods, functions, and properties. As you might know from the famous quote “There are only two hard things in computer science: cache invalidation and naming things,” finding the right names is indeed not a trivial problem. So, having conventions on this topic at least reduces the time of developers trying to come up with a proper name. Furthermore, the same as Coding Standards, they help reduce cognitive friction.

Coding guidelines help the lesser-experienced developers in your team, or those who just started, to have a solution at hand that they otherwise needed to search for in the code or on the internet. It also helps write maintainable code by avoiding bad practices, as we already discussed in Chapter 3, Code, Don’t Do Stunts. To help you get started with setting up the first set of rules, we will give you some examples in the next section.

Examples of coding guidelines

Starting with a blank sheet of (virtual) paper is hard, so in this section, we collected a list of real-world examples of what could be part of your coding guidelines. Please note that this collection of rules, although they are based on best practices, is not meant to be perfect or the only truth. We rather want to give you a good starting point for discussions and examples on what topics should be clarified using coding guidelines.

Naming conventions

By using naming conventions, we make sure that certain elements of our code are being named in a uniform and comprehensible way. This reduces cognitive friction and makes the onboarding of new team members easier.

Services, repositories, and models

Written in UpperCamelCase. Use the type as the suffix.

Here are some examples:

  • UserService
  • ProductRepository
  • OrderModel

Events

Written in UpperCamelCase. Use the correct tense to indicate whether the event is fired before or after the actual event.

Here are some examples:

  • DeletingUser is the event before the deletion
  • DeleteUser is the actual event
  • UserDeleted is the event after the deletion

Properties, variables, and methods

Written in lowerCamelCase.

Here are some examples:

  • $someProperty
  • $longerVariableName
  • $myMethod

Tests

Written in lowerCamelCase. Use the word test as a prefix.

Here are some examples:

  • testClassCanDoSomething()

Traits

Written in UpperCamelCase. Use the adjective to describe what the trait is used for.

Here are some examples:

  • Loggable
  • Injectable

Interfaces

Written in UpperCamelCase. Use the word Interface as the suffix.

Herer are some examples:

  • WriterInterface
  • LoggerInterface

General PHP conventions

Even if you already use Coding Standards such as PSR-12, there are certain aspects that they do not cover. We will pick up some of them in this section.

Comments and DocBlocks

Avoid comments if possible, as they tend to get outdated and thus confuse more than they help. Only keep comments that cannot be replaced by self-explanatory names or by simplifying code, so it is easier to understand and does not require the comment anymore.

Only add DocBlocks if they add information, such as annotations for the code quality tools. Particularly since PHP 8, most DocBlocks can be replaced by type hints, which all modern IDEs will understand. If you use type hints, most DocBlocks can be removed:

// Redundant DocBlock
/**
 * @param int $property
 * @return void 
 */
public function setProperty(int $property): void { 
    // ... 
}

Often, DocBlocks are automatically generated by the IDE. If they are not updated, they are at best useless, or can even be plainly wrong:

// Useless DocBlock
/**
 * @param $property
 */
public function setProperty(int $property): void { 
    // ... 
}
// Wrong DocBlock
/**
 * @param string $property
 */
public function setProperty(int $property): void { 
    // ... 
}

DocBlocks should still be used for information that cannot be provided by PHP language features until now, such as specifying the content of an array, or marking a function as deprecated:

// Useful DocBlock
/**
 * @return string[]
 */
public function getList(): array { 
    return [
       ‘foo’,
       ‘bar’,
    ]; 
}
/**
 * @deprecated use function fooBar() instead
 */
public function foo(): bool { 
    // ... 
}

About DocBlocks

DocBlocks were introduced to, among other things, partially compensate for the shortcomings of weak typing in earlier versions of PHP. The de facto standard was introduced by the phpDocumentor project (https://www.phpdoc.org/) and as such is supported by many tools such as IDEs and static code analyzers. Using strict typing, it is often not necessary to use DocBlocks anymore though, unless you want to use phpDocumentor in your project.

Ternary operators

Every part should be written in a single line to increase readability. Exceptions can be made for very short statements:

// Example for short statement
$isFoo ? ‘foo’ : ‘bar’;
// Usual notation
$isLongerVariable
    ? ‘longerFoo’
    : ‘longerBar’;

Do not use nested ternary operators, as they are hard to read and debug:

// Example for nested operators
$number > 0 ? ‘Positive’ : ($number < 0 ? ‘Negative’ :
‘Zero’);

Constructor

Use constructor property promotion for shorter classes, if working with PHP 8+. Keep the trailing comma after the last property, as this will make it easier to add or comment out lines:

// Before PHP 8+
class ExampleDTO
{
    public string $name;
    public function __construct(
        string $name
    ) {
        $this->name = $name;
    }
}
// Since PHP 8+
class ExampleDTO
{
    public function __construct(
        public string $name, 
    ) {}
}

Arrays

Always use the short array notation and keep the comma after the last entry (see the previous section, Constructor, for an explanation):

// Old notation
$myArray = array(
    ‘first entry’,
    ‘second entry’
);
// Short array notation
$myArray = [
    ‘first entry’,
    ‘second entry’,
];

Control structures

Always use brackets, even for one-liners. This reduces cognitive friction and makes it easier to add more lines of code later:

// Bad
if ($statement === true)
    do_something();
// Good
if ($statement === true) {
    do_something();
}

Avoid else statements and return early, as this is easier to read and reduces the complexity of your code:

// Bad
if ($statement) {
    // Statement was successful
    return;
} else {
    // Statement was not successful
    return;
}
// Good
if (!$statement) {
    // Statement was not successful
    return;
}
// Statement was successful
return;

Exception handling

Empty catch blocks should be avoided, as they silently swallow error messages and thus can make it difficult to find bugs. Instead, log the error message or at least write a comment that explains why the exception can be ignored:

// Bad
try {
    $this->someUnstableCode();
} catch (Exception $exception) {}
// Good
try {
    someUnstableCode();
} catch (Exception $exception) {
    $this->logError($exception->getMessage());
}

Architectural patterns

Coding guidelines are not limited to how to format code or name elements. They can also help you to control how the code is written in an architectural sense.

Fat models, skinny controllers

If the MVC pattern is used, the business logic should be located inside models or similar classes, such as services or repositories. Controllers should contain as little code as possible as is required to receive or transfer data between the views and the models.

Framework-agnostic code

In the context of the fat models, skinny controllers approach, you will probably come across the term framework-agnostic business logic. It means that the code that contains your business rules should use as few features of the underlying framework as possible. This makes framework updates or even migrations to other frameworks much easier.

Single responsibility principle

Classes and methods should only have one responsibility. See Chapter 2, Who Gets to Decide What “Good Practices” Are?, for more information about this principle.

Framework guidelines

In this book, we want to focus on writing clean code in PHP. Often, though, you will be working with frameworks, and although it is essential to include them in the guidelines as well, we do not want to go into much more detail here.

However, next, you will find a list of questions that should give you a good idea about which framework-related topics to include in your guidelines:

  • How to access the database
  • How to configure routes
  • How to register new services
  • How is authentication handled within your project?
  • How should errors or other debug information be logged?
  • How to create and organize view files
  • How to handle translations

Since the answers to these questions highly depend on the used framework, we cannot give you recommendations here. You will need to set the guidelines up together with your team. In the next section, we will give you some ideas on how to do that.

Setting up guidelines

The process of setting up coding guidelines takes time and often requires several workshops in which the rules are discussed. This requires moderation, for example, by a technical lead; otherwise, you might get stuck in endless discussions.

Do not worry if you cannot immediately reach an agreement on all topics though. Remind yourself that the people in your team have different backgrounds, experiences, and skill levels – and no one will directly ditch their personal ways of coding just because there are suddenly rules that they do not understand or accept.

Make sure to set up a process that checks from time to time whether the guidelines need to be updated. Maybe some rules get outdated over time, or new language features must be included. An action point in a regularly occurring team meeting would be a good opportunity for this.

The guidelines should be easily accessible in written form, such as in a wiki or the company’s internal knowledge base, which should be able to track the version history. Every team member should be able to write comments on it so that questions or issues can be handled as soon as they appear. Lastly, all team members should be automatically informed about new changes.

Once your team agrees on a set of rules, make sure to utilize the code quality tools you learned about in earlier chapters to automatically check whether the rules are respected. You can, for example, use PHPStan to detect empty catch blocks, or PHPMD to enforce if without using else.

How can we ensure that our coding guidelines are applied? Obviously, we should use our code quality tools wherever possible. But what if these tools do not include the rules we would like to enforce? With a bit of internet research, you might be able to find a third-party implementation for them. Or, if you cannot find anything, you could even write custom rules yourself, since all static code analyzers are extendable.

For rules that are too complicated to check automatically, we have to manually check whether they are used correctly. This can happen in code reviews, and we think they are so important that they deserve their own section in this chapter.

Setting up coding guidelines alone will just be a waste of time if you do not make sure that they are kept. We can automate checking all the coding style-related rules and also a fair number of coding guidelines. But at the moment, for those rules that are addressing the framework guidelines or architectural aspects, automation is no longer possible, and we humans must jump in, taking over the checks. At this point, code reviews come into play. Let us have a closer look in the next section.

Code reviews

The process of manually checking the code of other developers is called a code review. This includes all changes, that is, not only new functionality but also bug fixes or even simple configuration changes.

A review is always done by at least one fellow developer, and it usually happens in the context of a pull request, shortly before the code of a feature or bug fix branch gets merged into the main branch; only if the reviewer approves the changes will they become part of the actual application.

In this section, we will discuss what you should look for in code reviews, why they are so important, and how they should be done to make them a successful tool in your toolkit.

Why you should do code reviews

It might sound a bit obvious because that is what this whole book is about. Yet, it cannot be stressed enough – code reviews will improve the quality of your code. Let us examine more closely why:

  • Easy to introduce: Introducing code reviews usually comes with no additional costs (except for the required time). All major Git repository services such as Bitbucket, GitLab, or GitHub have a built-in review functionality that you can use immediately.
  • Quick impact: Code reviews are not only easy to introduce but they will show their usefulness very soon after they have been introduced.
  • Knowledge sharing: Because code reviews often lead to discussions between the developers, they are a great tool to spread knowledge about best practices in the team. Of course, junior developers especially will massively benefit from the mentoring, but also the most seasoned developers will learn something new from time to time.
  • Constant improvement: The regular discussions will result in improved coding guidelines, as they are constantly challenged and updated, if necessary.
  • Avoid problems early: Code reviews take place very early in the process (see Chapter 11, Continuous Integration), so chances are good that bugs, security issues, or architectural problems are found before they even reach the test environment.

If you are not yet convinced of the benefits of code reviews, check out the next section, in which we will talk more about what code reviews should cover – and what not.

What code reviews should cover

What aspects should we check when doing code reviews?

  • Code design: Is the code well designed and consistent with the rest of the application? Does it follow general best practices, such as reusability, design patterns (see the next section), or SOLID design principles (see Chapter 2, Who Gets to Decide What "Good Practices" Are?)?
  • Functionality: Does the code do what it should or does it have any side effects?
  • Readability: Is the code easy to understand or too complex? Are the comments necessary? Could the readability be improved by renaming a function or a variable, or by extracting code into a function with a meaningful name?
  • Security: Does the code introduce potential attack vectors? Is all output escaped to prevent XSS attacks? Are database inputs sanitized to avoid SQL injections?
  • Test coverage: Is the new code covered with automated tests? Do they test the right things? Are more test cases needed?
  • Coding standards and guidelines: Does the code follow the Coding Standards and coding guidelines the team agreed upon?

Your team should also consider whether testing the code in their local development environment should be part of the review process or not. There is no clear recommendation on this though.

Best practices for code reviews

Although code reviews have many benefits and can be implemented fairly easily, there are a few pitfalls that you should be aware of, and established best practices that will make the reviews even more successful.

Who should review the code?

First and foremost, who should ideally be doing the code reviews? Of course, this also depends on your setup. If you work in a team together with another PHP developer, then this should surely be the first person to ask. This way, you build up shared domain knowledge; although your colleague has not worked on your ticket directly, they at least get an idea of what you have worked on.

Yet, reaching out to members of other teams (if there are any) from time to time avoids being stuck in a bubble and fosters knowledge sharing. If you are unsure about certain topics, ask the domain experts for their assistance. Often, this includes performance, architecture, or security-related changes.

Automatize

One thing that code reviews should not cover is whether the Coding Standards are kept. In Chapter 7,Code Quality Tools we introduced the necessary tools to do this automatically, and in Chapter 11, Continuous Integration, we integrated them into a CI pipeline.

Make sure that only those pull requests get reviewed where all the checks (such as code sniffers, code analyzers, and automated tests) have passed. Otherwise, you will spend a lot of time on topics that should not even be discussed.

Avoid long code reviews

How many lines should the code change that needs to be reviewed have? Studies suggest that 200 to 400 lines should be the maximum, as the concentration of the reviewer decreases over time. So, try to keep the individual changes relatively small. It is also much more likely for the reviewer to find time to review smaller changes, as for a long tapestry of diffs.

Code reviews, even smaller ones, will require time in which the reviewer will not be able to write code. But how much time should be spent? Again, this depends on your setup. A good ballpark number is a maximum of 60 minutes to avoid the fatigue of the reviewer. Allow enough space for the reviewer to review the code line by line. Reviews should be accepted as part of your daily work or they will quickly become a burden, so nobody should rush through them.

Stay human

How to formulate feedback is crucial to make reviews successful. Watch your tone and try to avoid accusations such as “This is wrong!” or an absolute no-go, “This is stupid.” Developers, especially the lesser experienced ones, should not be anxious to let their code be reviewed.

Remember that a human being will read your comments. It often works well if you write them from the “I” perspective, for example, “I do not understand this line, can you please explain?” or “I think we could also do it like this..."

Do not forget to use the reviews to give praise for parts that are well done. A quick “Great idea!” or “I really like your approach” or “Thanks for the code cleanup” shows your appreciation of the other developer’s work and increases their motivation.

Usually, code reviews are done by writing comments on the Git platform you use. But of course, you can also do them face to face. Some developers appreciate direct feedback more than just comments because written text lacks a lot of meta information, such as the tone of voice or the facial expression.

Don’t overdo it, but don’t be careless either

Remember the Pareto principle and do not overdo things. Maybe there are still small parts in the code that you would change but that are not explicitly wrong, as they adhere to all the team standards. Programming is still a matter of personal style, and having endless discussions in a code review will lead to frustration without further benefit.

Do not accept changes that degrade the overall system health though. If you are convinced that a change is harmful or violates the coding guidelines, you must not approve the changes. If in doubt, get another developer involved.

Embrace changes

Lastly, if you feel an issue that you discussed in a review should be part of the guidelines, note it down and address it in the next team meeting, without mentioning the other developer directly. Maybe you were right and the guidelines will be amended to avoid the issue in the future.

But you could also be wrong, and the rest of the team does not see it as a problem. If you cannot come up with convincing arguments and examples, you have to accept those decisions as well.

Ensuring code reviews are done

During stressful daily work, with high-priority bug fixes and tight deadlines, it is easy to forget about doing code reviews. Fortunately, all the Git service providers offer functionality to assist you here:

  • Make reviews mandatory: Most Git repository services can be configured to allow changes only to be merged into the main branch if they got at least one approval. You should definitely enable this feature.
  • Rotate reviews: If your team is larger, try to request the review not always from the same person. Some tools even allow selecting a reviewer randomly for you.
  • Use a checklist: Checklists have proven to be useful, so you should use them too. Set up a checklist for all the aspects you need to look for in code reviews. In the next section, we will show you how to make sure it gets used.

Definition of done

If you work using agile methodologies, you probably heard the term definition of done already. Here, the team agrees on a list of actions that should be done before a task is completed.

A typical definition of done contains checks such as whether the test has been written or the documentation updated. You can utilize this for the code reviews as well.

Again, our Git tools help us by providing templates for pull requests (also called merge requests). These are texts that will be used to automatically prefill the description of the pull request.

How that works depends on the software you use, so we cannot give you exact instructions here. The following text, however, shows you an example of what it could look like:

# Definition of Done
## Reviewer 
[ ] Code changes reviewed
    1. Coding Guidelines kept
    2. Functionality considered
    3. Code is well-designed
    4. Readability and Complexity considered
    5. No Security issues found
    6. Coding standard and guidelines kept
[ ] Change tested manually
## Developer 
[ ] Acceptance Criteria met
[ ] Automated Tests written or updated
[ ] Documentation written or updated

What gets included in the checklist is up to you and your team. If used as a template, these items will always appear in the pull request description by default. It is meant to be used by both the reviewer and the developer to not forget what needs to be done before approving the pull request and merging it into the main branch.

Some tools, such as GitHub, use a Markdown-style markup language for these templates. They will display the checkboxes (the two square brackets before every item) as clickable checkboxes in the browser and keep track of whether they were clicked or not. Voilà! Without much work, you have set up an easy-to-use and helpful checklist!

Code reviews conclusion

We hope this section gave you good insights into how beneficial code reviews can be for your team and yourself. Since they can be introduced effortlessly, it is worth trying them out. The best practices in this section will help you avoid some of the problems that code reviews could have.

But, as always, they also have some downsides: reviews take a lot of time and they can lead to conflicts between team members. We are convinced that the time spent pays off well though because the positive aspects outweigh the negative ones by far. The conflicts would most likely happen anyway, and the reviews are just the gauge to vent off steam. This cannot be fully avoided if you work in a team but should be addressed early with your manager. It is their job to deal with these kinds of problems.

In the last part of this chapter, we will look at design patterns in more detail. They can act as guidelines on how to solve general problems in software development.

Design patterns

Design patterns are commonly used solutions to problems that occur regularly in software development. As a developer, you will sooner or later come across this term, if you have not done so already – and not without a reason, as these patterns are based on best practices and have proven their usefulness.

In this section, we will tell you more about the different types of design patterns and why they are so important that they became part of this book. Furthermore, we will introduce you to some common design patterns that are widely used in PHP.

Understanding design patterns

Let us have a closer look at design patterns now. They can be considered templates to solve particular problems and are named according to the solution they provide. For example, in this chapter, you will learn about the Observer pattern, which can help you to implement a way to observe changes in objects. This is very useful when you write code, but also when you design software with other developers. It is much easier to use a short name to name a concept rather than having to explain it every time.

Do not mistake design patterns with algorithms though. Algorithms define clear steps that need to be followed to solve a problem, while design patterns describe how to implement the solution on a higher level. They are not bound to any programming language.

You also cannot add design patterns to your code like you would add a Composer package, for example. You have to implement the pattern on your own, and you have certain degrees of freedom in how you do that.

However, design patterns are not the single solution to every problem, nor do they claim to offer the most efficient solutions. Always take these patterns with a grain of salt – often, developers want to implement a certain pattern just because they know it. Or, as the saying goes: "If all you have is a hammer, everything looks like a nail."

Usually, design patterns are divided into three categories:

  • Creational patterns deal with how to efficiently create objects and at the same time offer solutions to reduce code duplication
  • Structural patterns help you organize relationships between entities (i.e., classes and objects) in flexible and efficient structures
  • Behavioral patterns arrange communication between entities while maintaining a high degree of flexibility

In the following pages, we will have a look at some example implementations to explain the idea behind design patterns.

Common design patterns in PHP

We now want to introduce some of the most widely used design patterns in the PHP world. We chose one pattern each from the three categories Creational, Structural, and Behavioral, which we discussed in the previous section.

Factory Method

Imagine the following problem: you need to write an application that should be able to write data into files using different formats. In our example, we want to support CSV and JSON, but potentially, other formats in the future as well. Before the data is written, we would like to apply some filtering, which should always happen, regardless of which output format is chosen.

An applicable pattern to solve this problem would be the Factory Method. It is a Creational pattern, as it deals with the creation of objects.

The main idea of this pattern is that subclasses can implement different ways to achieve the goal. It is important to note that we do not use the new operator in the parent class to instantiate any subclasses, as you can see in the following class:

abstract class AbstractWriter
{
    public function write(array $data): void
    {
        $encoder = $this->createEncoder();
        // Apply some filtering which should always happen, 
        // regardless of the output format.
        array_walk(
            $data,
            function (&$value) {
                $value = str_replace(‘data’, ‘’, $value);
            }
        );
        // For demonstration purposes, we echo the result
        // here, instead of writing it into a file
        echo $encoder->encode($data);
    }
    abstract protected function createEncoder(): Encoder;
}

Note the createEncoder method – this is the factory method that gave the pattern the name, since it acts, in a sense, as a factory for new instances. It is defined as an abstract function, so it needs to be implemented by one or more subclasses.

To be flexible enough for future formats, we intend to use separate Encoder classes for each format. But first, we define an interface for these classes so that they are easily exchangeable:

interface Encoder
{
    public function encode(array $data): string;
}

Then, we create an Encoder class for each format that implements the Encoder interface; first, we create JsonEncoder:

class JsonEncoder implements Encoder
{
    public function encode(array $data): string
    {
        // the actual encoding happens here
        // ...
        return $encodedString;
    }
}

Then we create CsvEncoder:

class CsvEncoder implements Encoder
{
    public function encode(array $data): string
    {
        // the actual encoding happens here
        // ...
        return $encodedString;
    }
}

Now, we need to create one subclass of the AbstractWriter class for each format we want to support. In our case, that is CsvWriter first:

class CsvWriter extends AbstractWriter
{
    public function createEncoder(): Encoder
    {
        $encoder = new CsvEncoder();
        // here, more configuration work would take place
        // e.g. setting the delimiter
        return $encoder;
    }
}

And second, it is JsonWriter:

class JsonWriter extends AbstractWriter
{
    public function createEncoder(): Encoder
    {
        return new JsonEncoder();
    }
}

Please note that both subclasses only overwrite the Factory Method createEncoder. Also, the new operators occur only in the subclasses. The write method remains unchanged, as it gets inherited from AbstractWriter.

Finally, let us put this all together in an example script:

function factoryMethodExample(AbstractWriter $writer)
{
    $exampleData = [
        ‘set1’ => [‘data1’, ‘data2’],
        ‘set2’ => [‘data3’, ‘data4’],
    ];
    $writer->write($exampleData);
}
echo "Output using the CsvWriter: ";
factoryMethodExample(new CsvWriter());
echo "Output using the JsonWriter: ";
factoryMethodExample(new JsonWriter());

The factoryMethodExample function first receives CsvWriter and, for the second run, JsonWriter as parameters. The output will look like this:

Output using the CsvWriter:
3,4
1,2
 
Output using the JsonWriter:
[["3","4"],["1","2"]]

The Factory Method pattern enables us to move the instantiation of the Encoder class away from the AbstractWriter parent class into the subclasses. By doing this, we avoid tight coupling between Writer and Encoder, gaining much more flexibility. As a downside, the code becomes more complex, as we have to introduce interfaces and subclasses to implement this pattern.

Dependency injection

The next pattern we want to introduce is a Structural pattern called Dependency Injection (DI). It helps us to implement a loosely coupled architecture by inserting dependencies into classes already at construction time, instead of instantiating them inside the class.

The following code shows you how a dependency, in this example, a classic Logger, gets instantiated within the constructor:

class InstantiationExample
{
    private Logger $logger;
    public function __construct()
    {
        $this->logger = new FileLogger();
    }
}

The code itself works perfectly fine, yet the problems start when you want to replace FileLogger with a different class. Although we already use the Logger interface for the $logger property, which theoretically makes it easy to exchange it with another implementation, we have hardcoded FileLogger in the constructor. Now, imagine you used that logger in almost every class; replacing it with a different Logger implementation causes some effort, as you would have to touch every single file that uses it.

Not being able to replace FileLogger also makes writing unit tests for the class more difficult. You cannot replace it with a mock, but you also do not want to write information to your actual logs during test runs. If you want to test that the logging works correctly, you have to build quite some workarounds into your code that is also used in production.

DI forces us to think about which, and how many, dependencies should be used in a class. It is considered a code smell (i.e., an indicator for badly structured code) when the constructor takes considerably more than three or four dependencies as a parameter because it indicates that the class violates the single responsibility principle (the “S” in SOLID). This is also known as scope creep: the scope of a class slowly but steadily gets bigger over time.

Let us now see how DI would solve the previously mentioned problems:

class ConstructorInjection
{
    private Logger $logger;
    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }
}

Constructor property promotion

Please note that we did not use constructor property promotion purposely here for better visualization.

The difference to the previous code does not seem to be that big. All we did was pass over the Logger instance as a parameter to the constructor instead of instantiating it there directly. The benefit is huge though: we can now change the instance that gets injected (if it implements the Logger interface) without touching the actual class.

Imagine you no longer want the class to log into the filesystem, but rather into a log management system such as Graylog, which manages all the logs from your different applications in one place. All you need to do is to create GraylogLogger, which implements the Logger interface as well but writes the logs to this system instead of into files. Then, you simply inject GraylogLogger instead of FileLogger into all classes that should use it – congratulations, you just changed the way your applications log information without touching the actual classes.

Likewise, we can easily exchange a dependency with a mock object in our unit tests. This is a massive improvement regarding testability.

The instantiation of Logger, however, no matter which implementation you chose, still has to happen somewhere else. We just moved it out of the InjectionExample class. The dependency gets injected when the class gets instantiated:

$constructorInjection = new ConstructorInjection(
     new FileLogger()
);

Usually, you would find this kind of instantiation within a Factory class. This is a class that implements, for example, the Simple Factory pattern and whose only job is to create instances of a certain class, with all the necessary dependencies.

Simple Factory pattern

We do not discuss this pattern in more detail in this book because it is, well, really simple. You can find more information about it here: https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html.

The injection does not necessarily need to happen through the constructor. Another possible approach is the so-called setter injection:

class SetterInjection
{
    private Logger $logger;
    public function __construct()
    {
        // ....
    }
    public function setLogger(Logger $logger): void
    {
        $this->logger = $logger;
    }
}

The injection of the dependency would then happen using the setLogger method. The same as for the constructor injection, this would most likely happen within the Factory class.

The following is an example of what such a factory could look like:

class SetterInjectionFactory
{
    public function createInstance(): SetterInjection
    {
        $setterInjection = new SetterInjection();
        $setterInjection->setLogger(new FileLogger());
        return $setterInjection;
    }
}

Dependency injection container

You probably already wondered how to manage all the factories that are necessary, especially in a larger project. For this, the DI container has been invented. It is not part of the DI pattern, yet closely related, so we want to introduce it here.

The DI container acts as central storage for all objects that are brought into their target classes using DI patterns. It also contains all the necessary information to instantiate the objects.

It also can store created instances so it does not have to instantiate them twice. For example, you would not create a FileLogger instance for each class that uses it, as this would end up in plenty of identical instances. You rather want to create it once and then pass it over by reference to its destination classes.

DI container

Showing all the functionality of a modern DI container would exceed this book. If you are interested in learning more about this concept, we recommend you to check out the phpleague/container package: https://container.thephpleague.com. It is small yet feature-rich and has great documentation that can introduce you to more exciting concepts such as service providers or inflectors.

The concept of the DI container has been adopted in all major PHP frameworks nowadays, so you have most likely used such a container already. You probably did not notice it though, because it is usually hidden deep in the back of your application and is sometimes also referred to as a service container.

PSR-11 – Container interface

The DI container is so important to the PHP ecosystem that it got its own PSR: https://www.php-fig.org/psr/psr-11.

Observer

The last pattern we want to introduce in this book is the Observer pattern. As a Behavioral pattern, its main purpose is to allow efficient communication between objects. A common task to implement is to trigger a certain action on one object when the state of another object changes. A state change could be something as simple as a value change of a class property.

Let us start with another example: you have to send out an email to the sales team every time a customer cancels their subscription so that they get informed and can do countermeasures to keep the customer.

How do you best do that? You could, for example, set up a recurring job that checks within a certain time interval (e.g., every 5 minutes) whether there had been any cancellations since the check. This would work, but depending on the size of your customer base, the job would probably not return any results most of the time. If the interval between two checks, on the other hand, is too long, you might lose valuable time until the next check.

Now, sales might not be the most time-critical thing in the world (salespeople usually disagree here), but you surely get the idea. Wouldn’t it be great if we could just send out the email as soon as the customer cancels the subscription? So, instead of regularly checking for changes, we only get informed at the moment when the change happens?

The code could look like this simplified example:

class CustomerAccount
{
    public function __construct(
        private MailService $mailService
    ) {}
    public function cancelSubscription(): void
    {
        // Required code for the actual cancellation
        // ...
        $this->mailService->sendEmail(
            ‘[email protected]’,
            ‘Account xy has cancelled the subscription’
        );
    }
}

Simplified example

The example has been simplified. You should, for example, not hardcode the email address.

That approach would surely work, but it has a drawback: the call of MailService is directly coded into the class and hence is tightly coupled to it. And now, the CustomerAccount class has to care about another dependency, which increases the maintenance effort, as the tests have to be extended, for example. If we later do not want to send this email anymore or even send an additional email to another department, CustomerAccount has to be changed again.

Using a loosely coupled approach, the CustomerAccount object would only store a list of other objects that it should notify in case of a change. The list is not hardcoded, and the objects that need to get notified have to be attached to that list during the bootstrap phase.

The object that we want to be observed (in the preceding example, CustomerAccount), is called the subject. The subject is responsible for informing the observers. No code change would be necessary on the subject to add or remove observers, so this approach is very flexible.

The following code shows an example of how the CustomerAccount class could implement the Observer pattern:

use SplSubject;
use SplObjectStorage;
use SplObserver;
class CustomerAccount implements SplSubject
{
    private SplObjectStorage $observers;
    public function __construct()
    {
        $this->observers = new SplObjectStorage();
    }
    public function attach(SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }
    public function detach(SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }
    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }
    public function cancelSubscription(): void
    {
        // Required code for the actual cancellation
        // ...
        $this->notify();
    }
}

A lot has happened here, so let us go through it bit by bit. The first thing notable is that the class makes use of the SplSubject and SplObserver interfaces, as well as the SplObjectStorage class. Since the CustomerAccount class implements the SplSubject interface, it has to provide the attach, detach, and notify methods.

We also use the constructor to initialize the $observers property as SplObjectStorage, which will store all observers of the CustomerAccount class. Luckily, the SPL provides the implementation of this storage already, so we do not need to do it.

Standard PHP Library

We talked about the Standard PHP Library (SPL) in Chapter 3, Code Quality Metrics already. The fact that the SPL includes these entities shows the importance of the Observer pattern as well as the usefulness of this library.

The attach and detach methods are required by the SplSubject interface. They are used for adding or removing observers. Their implementation is easy – we just need to forward the SplObserver object to SplObjectStorage in both cases, which takes over the necessary work for us.

The notify method has to call the update method on all SplObserver objects that are stored in SplObjectStorage. This is as simple as using a foreach loop to iterate over all SplObserver entries and call their update method, passing over a reference to the subject using $this.

The following code shows what such an observer could look like:

class CustomerAccountObserver implements SplObserver
{
    public function __construct(
        private MailService $mailService
    ) {}
    public function update(CustomerAccount|SplSubject
      $splSubject): void
    {
        $this->mailService->sendEmail(
            ‘[email protected]’,
            ‘Account ‘ . $splSubject->id . ‘ has cancelled
              the subscription’
        );
    }
}

The observer, not surprisingly, implements the SplObserver interface. The only required method is update, which gets called from the subject in the notify method. Since the interface requires the $splSubject parameter to implement the SplSubject interface, we have to use that parameter type hint. It would lead to a PHP error otherwise.

Since we know that, in this case, the object is actually a CustomerAccount object, we can add this type hint as well. This will enable our IDE to help us with the proper code completion; it is not required to add it though.

As you can see, all the logic regarding the email sending has now moved into CustomerAccountObserver. In other words, we successfully eliminated the tight coupling between CustomerAccount and MailService.

The last thing we need to do is to attach CustomerAccountObserver:

$mailService = new MailService();
$observer = new CustomerAccountObserver($mailService);
$customerAccount = new CustomerAccount();
$customerAccount->attach($observer);

Again, this code example is simplified. In a real-world application, all three objects would be instantiated in dedicated factories and brought together by a DI container.

The Observer pattern helps you to decouple objects with a relatively low amount of work. It has a few drawbacks though. The order in which the observers are updated cannot be controlled; thus, you cannot use it to implement functionality where the order is crucial. Second, by decoupling the classes, it is no longer obvious just by looking at the code which observers are attached to it.

To sum up the topic of design patterns, we will have a look at those patterns that are still quite common today but have proven to have too significant drawbacks to be recommended. Curtain up for the Anti-patterns!

Anti-patterns

Not every design pattern stood the test of time. Everything evolves, and so do software development and PHP. Some patterns that have been successful in the past have been replaced by newer and/or better versions.

What was once the standard approach to solving a problem a couple of years ago might not be the right solution anymore. The PHP community keeps learning and improving, but this knowledge is not yet evenly distributed. Thus, to make it more obvious which patterns should be avoided, they are often referred to as Anti-patterns – this clearly sounds like something you would not like to have in your code, right?

What does such an Anti-Pattern look like? Let us have a look at the first example.

Singleton

Before DI became increasingly popular in the PHP world, we already had to deal with the problem of how to effectively create instances and how to make them available in the scopes of other classes. The Singleton pattern offered a quick and easy solution that usually looked something like this:

$instance = Singleton::getInstance();

The static getInstance method is surprisingly simple:

class Singleton
{
    private static ?Singleton $instance = null;
    public static function getInstance(): Singleton
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

If the method gets executed, it is checked whether an instance of the class has already been created. If yes, it will be returned; if not, it will be created beforehand. This approach is also called lazy initialization. Being lazy is a good thing here because it only gets initialized when it is required, so it saves resources.

The method furthermore stores the new instance in the static $instance property. This is remarkable as the approach is only possible because static properties can have a value without requiring a class instance. In other words, we can store the instance of a class in its own class definition. Also, in PHP, all objects are passed as a reference, that is, as a pointer to the object in memory. Both peculiarities help us to make sure that the same instance is always returned.

The Singleton actually is quite elegant; as it also uses a static method, it needs no instance of the Singleton class as well. This way, it can literally be executed everywhere in your code, without any further preparation.

The ease of use is one of the main reasons why Singleton eventually became an Anti-Pattern, since it leads to scope creep. We explained this problem in the section about DI.

Another problem is testability: it is very hard to replace the instance with a mock object, so writing unit tests for code that uses the Singleton pattern became much more complex.

Nowadays, you should use DI together with a DI container. It is not as easy to use as the Singleton, but that in turn helps us to think twice before we use another dependency in a class.

However, it does not mean that the Singleton pattern must not be used at all. There might be valid reasons to implement it, or at least to keep it in a legacy project. Just be aware of the risks.

Service locator

The second pattern that could be considered problematic is Service Locator:

class ServiceLocatorExample
{
    public function __construct(
        private ServiceLocator $serviceLocator
    ) {}
    public function fooBar(): void
    {
        $someService = $this->serviceLocator
          ->get(SomeService::class);
        $someService->doSomething();
    }
}

In this example class, we inject ServiceLocator during the construction time of the object. It is then used throughout the class to fetch the required dependencies. In these regards, DI and the Service Locator are both implementations of the dependency inversion principle (the “D” in SOLID): they move the control about their dependencies out of the class scope, helping us to achieve a loosely coupled architecture.

But, if we only need to inject one dependency instead of many, is that not a great idea? Well, the drawback of the Service Locator pattern is that it hides the dependencies of the class behind the ServiceLocator instance. While with DI you can clearly see which dependencies are used by looking at the constructor, you cannot do that when injecting only ServiceLocator.

Unlike DI, it does not force us to question which dependencies should be used in a class as, for larger classes, you can quickly lose the overview of which dependencies are used in a class. This is basically one of the main drawbacks we identified for the Singleton pattern.

Yet again, we do not want to be dogmatic when it comes to the use of the Service Locator pattern. There might be situations where it is appropriate to use it – just handle it with care.

Summary

In this chapter, we discussed the importance of standards and guidelines. Coding standards help you to align with fellow developers on how the code should be formatted, and you learned about existing standards worth adopting.

Coding guidelines help your team to align on how to write software. Although these guidelines are highly individual for each team, we provided you with a good set of examples and best practices to build your team’s guidelines. With code reviews, you also know how to keep the quality up.

Finally, we introduced you to the world of design patterns. We are confident that knowing at least a good part of these patterns will help you to design and write high-quality code together with your team members. There is much more to explore on this topic, and you will find links to some great sources at the end of this chapter.

This almost ends our exciting journey through the many aspects of clean code in PHP. We are sure you now want to use all your new knowledge in your daily work as soon as possible. Yet, before you do, bear with us for the last chapter, when we talk about the importance of documentation.

Further reading

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset