Skip to content

PHP conventions

TL;DR

There are the main approaches to using modern PHP: like Ruby or like Java. We prefer Java-way: less magic, more types.

We strive to minimize magic and help IDE and static code analyzers to help us.

We use PSR-12 extended by other rules (mostly by powerful Slevomat Coding Standard). Additionally, we like Spatie’s code guidelines and borrow some rules from them.

We generally observe the standards from the PHP FIG. We use automated tools to check our code on CI:


Final by default

When? — whenever possible. Try to keep the "O" of SOLID in your mind: code should be open for extension, but closed for modification. That’s why you should use private as the default visibility modifier, and final keyword as the default for classes.

This way you’re encouraged to think before opening up your classes to the outside world. You should take a moment to think about possible other ways to solve a problem instead of opening up classes. You could, for example, rely more on composition, dependency injection, and interfaces; instead of extending classes.

Especially in the context of open source packages, you’re encouraged to think twice about making a method public or protected, or opening a class for extension. Every entry point in your code that is open for the public to use is an entry point you’ll have to maintain with backwards compatibility in mind.

Strict types

We do use declare(strict_types=1); by default. Strict typing applies to function calls made from within the file with strict typing enabled. It helps us to catch some tricky bugs. On another hand, it requires a developer to think more possible variable types, but at the end of the day they have more stable code.

Typed properties

You should specify property types whenever possible. Don’t use a docblock when you can use typed properties.

Void return types

If a method return nothing, it SHOULD be indicated with void. This makes it more clear to the users of your code what your intention was when writing it.

Docblocks

Don’t use docblocks for methods and functions that can be fully type hinted (unless you need a description).

Only add a description when it provides more context than the method signature itself (Visual noise is real). Use full sentences for descriptions.

Good example: reveal what your arrays and collections contain:

php
// GOOD
final class Foo
{
    /** @var list<string> */
    private array $urls;

    /** @var \Illuminate\Support\Collection<int, \App\Models\User> */
    private Collection $users;
}

// BAD
final class Foo
{
    private array $urls;
    private Collection $users;
}

When possible, docblocks SHOULD be written on one line.

Always use fully qualified class names in docblocks.

php
// Good
/** @return \App\Models\Post */

// Bad
/** @return Post */

Make inheritance explicit using the @inheritDoc tag

Because inheritance is implicit, it may happen that it is not necessary to include a PHPDoc for an element. To avoid any confusion, please always use the @inheritDoc tag for classes and methods.

Don’t use @inheritDoc for class properties, instead copy the docblock from the parent class or interface.

php
// GOOD
final class Tag extends BaseTag
{
    /** @var list<string> The attributes that are mass assignable. */
    protected $fillable = [
        'name',
    ];
}

// BAD
final class Tag extends BaseTag
{
    /** @inheritDoc */
    protected $fillable = [
        'name',
    ];
}

Describe traversable types

Use Psalm syntax to describe traversable types:

Lists / Ordered maps

php
/** @return Type[] */
/** @return array<int, Type> */
/** @return array<TKey, TValue> */
/** @return Collection<TKey, TValue> */

Key-value store

You may know it as a structure / dictionary / hash / map / hashmap, what is the same.

php
/** @return array{foo: string, bar: int} */
/** @return array{optional?: string, bar: int} */

Generic types and templates

PHP doesn't support generic types out of the box, but there's a workaround that helps us have a cleaner type interface, and therefore less bugs, which is psalm template annotations. Here's an example:

php
    /**
     * @template T of \Illuminate\Notifications\Notification
     * @param class-string<T> $notificationFQCN
     * @return T
     */
    protected function initialize(string $notificationFQCN): Notification
    {
        $reflectionClass = new \ReflectionClass($notificationFQCN);
        $constructor = $reflectionClass->getConstructor();

        // Checks to make sure we can instantiate the object

        return $reflectionClass->newInstance();
    }

Strings

Prefer string interpolation above sprintf and the concatenation . operator whenever possible. Always wrap the variables in curly-braces {} when using interpolation.

php
// GOOD
$greeting = "Hi, I am {$name}.";
php
// BAD (hard to distinguish the variable)
$greeting = "Hi, I am $name.";
// BAD (less readable)
$greeting = 'Hi, I am '.$name.'.';

For more complex cases when there are a lot of variable to concat or when it’s not possible to use string interpolation, please use sprintf function:

php
$greeting = sprintf('Hello, my name is %s', ucfirst(auth()->user()->name));

Comments

Comments SHOULD be avoided as much as possible by writing expressive code. If you do need to use a comment to explain the what, then refactor the code. If you need to explain the reason (why), then format the comments as follows:

// There should be a space before a single line comment.

/*
 * If you need to explain a lot you can use a comment block. Notice the
 * single * on the first line. Comment blocks don’t need to be three
 * lines long or three characters shorter than the previous line.
 */

Class name resolution

Do not use hardcoded fully qualified class names in code. Instead, use resolution via ClassName::class keyword.

php
// GOOD
use App\Modules\Payment\Models\Order;
echo Order::class;

// BAD
echo 'App\Modules\Payment\Models\Order';

Use the class name instead of the self keyword

Use the class name instead of the self keyword for return type hints and when creating an instance of the class.

php
// GOOD
public function createFromName(string $name): Tag
{
    $tag = new Tag();
    $tag->name = $name;

    return $tag;
}

// BAD
public function createFromName(string $name): self
{
    $tag = new self();
    $tag->name = $name;

    return $tag;
}

return self/new self() vs return ClassName/new ClassName()—we can use both, but it would be good to be consistent

Exceptions

Don’t use "Exception" suffix

That forces developers to write better exception class-names. Details: The "Exception" suffix

Be explicit about error

php
// GOOD
abort(404, "The course with the ID {$courseId} could not be found.");

// BAD
abort(404);

Type-casting

When converting the type of variable, use type-casting instead of dedicated methods. Reason: better performance.

php
// GOOD
$score = (int) '7';
$hasMadeAnyProgress = (bool) $this->score;

// BAD
$score = intval('7');
$hasMadeAnyProgress = boolval($this->score);

Use named constructors

Use named static constructors (aka "static factories") to encapsulate logic and data and create valid entities:

php
// GOOD

// Models\Team.php
public static function createFromSignup(AlmostMember $almostMember): Team
{
    $team = new Team();
    $team->name = $almostMember->company_name;
    $team->country = $almostMember->country;
    $team->save();

    return $team;
}

Reason: have robust API that do not allow developers to create objects with invalid state (e.g. missing parameter/dependency). A great video on this topic: Marco Pivetta «Extremely defensive PHP»

Use domain-specific operations

Similar to the previous rule, instead of creating setters to modify model properties or directly modifying public properties, encapsulate the logic in domain-specific methods. This way you can make sure that your models have a valid/consistent state at all times.

php
// GOOD

// Models\Member.php
public function confirmEmailAwaitingConfirmation(): void
{
    $this->email = $this->email_awaiting_confirmation;
    $this->email_awaiting_confirmation = null;
}

// Controllers\MemberEmailConfirmationController.php
public function store(): void
{
    $member = Auth::guard('member')->user();
    $member->confirmEmailWaitingConfirmation();
    $member->save();
}

// BAD =====================================

// Models\Member.php
public function setEmail(string $email): Member;
public function setEmailWaitingConfirmation(string $email): Member;

// Controllers\MemberEmailConfirmationController.php
public function store(): void
{
    $member = Auth::guard('member')->user();
    $member->email = $member->emailWaitingConfirmation;
    $member->emailWaitingConfirmation = null;
    $member->save();
}

Note that this convention aligns with the idea of putting domain logic into models and not into controllers, hence having rich domain models and thin controllers/services.

Want to learn more?

"Cruddy by Design" by Adam Wathan, 40 mins Read more about class invariants for a better understanding of the dangers of modifying class properties from controllers/services.

assert() vs throw

Assertions should only be used to verify conditions that should be logically impossible to be false. These conditions should only be based on inputs generated by your own code. Any checks based on external inputs should use exceptions.

The best use case of assert() is to treat it as a docblock to specify types (great to nullables and polymorphic types). assert() may be disabled on production, so please do not rely on it in a runtime, prefer Exceptions instead if you need to check an expression in a runtime.

Make sure you add a description to assert so that it's easy to reason about the code when a failure occurs.

From official documentation:

Assertions should be used as a debugging feature only. You may use them for sanity-checks that test for conditions that should always be true and that indicate some programming errors if not or to check for the presence of certain features like extension functions or certain system limits and features.

regex

The biggest problem of regex is readability. Please read a perfect article Writing better Regular Expressions in PHP on this topic. There is nothing to add.

You can also use DEFINE to declare recurring patterns in you regex. Here's an example regex that declares attr, which captures HTML attributes and its use-case in declaring an image tag. It also uses sprintf to create reusable regex definitions.

php
class RegexHelper
{
    /** @return array<string, string> */
    public function images(string $htmlContent): array
    {
        $pattern = '
            (?'image' # Capture named group
                (?P>img) # Recurse img subpattern from definitions
            )
        '

        preg_match_all($this->createRegex($pattern), $htmlContent, $matches);

        return $matches['image'];
    }

    private function createRegex(string $pattern): string
    {
        return sprintf($this->getDefinitions(), preg_quote($pattern, '~'));
    }

    private function getDefinitions(): string
    {
        return "~
            (?(DEFINE) # Allows defining reusable patterns
                (?'attr'(?:\s[^>]++)?) # Capture HTML attributes
                (?'img'<img(?P>params)>) # Capture HTML img tag with its attributes
            )
            %s #Allows adding dynamic regex using sprintf
            ~ix";
    }
}

You can use Regex101 to test your patterns.

Materials

  1. Union Types vs. Intersection Types
  2. PHPDoc: Typing in Psalm
  3. PHPDoc: Scalar types in Psalm
  4. When to declare classes final
  5. Proposed PSR for docblocks

🦄