stand with ukraine
mandarinian.io
search

PHP best practices

General PHP Best Practices

Float problem

In PHP, as in many other programming languages, is a problem with float numbers. They lose precision. The same problem can appear even in database engines.

Example using PHP:

            php -r "echo (0.7 + 0.1) == 0.8 ? 'Yes' : 'No';"
            (out)No

How to solve:

Replace float types with string and do all math operations with dedicated libraries, that can resolve all math stuff via strings. The best one: brick/math, and extension to work with Money brick/money.

Hashing sensitive data

Use password_hash function with “Argon2” algorithm to hash secret data.

            $password = 'Qwerty123!';
            $hash = password_hash($password, PASSWORD_ARGON2ID);
            // $hash = '$argon2id$v=19$m=65536,t=4,p=1$VnBTWkxQd3E1WDdPUTgvTg$xpe/S57H7zCWpyi6vM1C3dq9076I0O8JtNRguzcw7S4'

            $isValid = password_verify('Qwerty123!', $hash);
            // $isValid = true
Filter data

You can sanitize and validate foreign input by filter_var() and filter_input() functions.

To remove HTML tags, use strip_tags() or escape characters with special meaning with htmlentities() or htmlspecialchars() .

To escape executable command's arguments, use escapeshellarg() .

Foreign data source:
  • $_GET
  • $_POST
  • $_COOKIE
  • $_SERVER
  • $_ENV
  • $_FILES
  • $_REQUEST
  • fopen('php://input', 'r')
  • 3rd party service response data
  • ...
Validate data

In order to validate data you can use beberlei/assert library. Read more and view example on: Enforce strict invariant.

Usage of empty

Do not use empty function as it leads to misunderstanding due to uncertain types.

            // Replace
            if (empty($string)) {
            }

            // With
            if (strlen($string) === 0) {
            }
            // Replace
            if (empty($array)) {
            }

            // With
            if (count($array) === 0) {
            }
            // Replace
            if (empty($array[$key])) {
            }

            // With
            if (array_key_exists($array, $key)) {
            }

            // or if you know that existance also means a non-empty value
            if (isset($array[$key])) {
            }
            // Replace
            if (empty($var)) {
            }

            // With
            if (isset($var)) {
            }
            // Replace
            if (empty($var)) {
            }

            // With
            if ($var === null) {
            }
            // Replace
            if (empty($var)) {
            }

            // With
            if ($var === 0) {
            }
            // Replace
            if (empty($var)) {
            }

            // With
            if ($var === false) {
            }
            if (!$var) {
            }

Classes

Always use final by default

Use final classes, unless they implicitly required to be extendable.

Reasons:
  • Prevent inheritance chain of doom
  • Encourage composition
  • Prevent SRP violation
  • Easier to remove final than add it
  • extends breaks encapsulation
  • Free to change the code
How to mock:
Always use Suffix and Postfix
In order to speed up file finding, always use suffix and postfix in class names.
  • ElementInterface
  • ElementTrait
  • ElementException
  • AbstractElement
Use extension and composition in DDD manner
If you want to extend a class, answer a question:
  • Is ChildClass a ParentClass particularization? "Is Dog and Animal? Yes!" So useextend.
  • Is ParentClass has ChildClass as a part? "Is Animal has Legs? Yes!" So use composition.
            class Animal
            {
              /**
               * @var Leg[]
               */
              private array $legs;

              public function __construct(array $legs)
              {
                $this->legs = $legs;
              }
            }

            final class Dog extends Animal {}
Avoid Trait as much as possible

Traits add coupling between classes.

Avoid __clone() cloning and __sleep() serializing as much as possible
Reasons:
  • You should remember about deep cloning:

                    class LoginRequest
                    {
                      private DateTime $time;
    
                      public function __clone()
                      {
                        $this->time = clone $this->time;
                      }
                    }
  • You should remember about state encapsulation and immutability
  • Keep in mind lifecycle of every dependency
  • Prevents ability to object change (or should keep version)
How to solve:
  • Disable cloning:

                    class Human
                    {
                      public function __clone()
                      {
                        throw new \DomainException("Why would you even clone me?! It's currently illegal!");
                      }
                    }
  • Disable serialization:

                    class MyAwesomeThing
                    {
                      public function __sleep()
                      {
                        throw new BadMethodCallException("NO! MY THING! CANNOT HAZ!");
                      }
                    }
  • Encapsulate cloning:

                    class Human
                    {
                      private Brain $brain;
    
                      public function clone(Brain $brain): self
                      {
                        $instance = clone $this;
                        $instance->brain = $brain;
    
                        return $instance;
                      }
    
                      private function __clone() {}
                    }
Disallow collection access from outside of Entity

You should control collection of data inside entity.

            // Good code
            class User
            {
              private array $bans;

              public function ban(): void
              {
                $this->bans[] = new Ban($this);
              }
            }

            public function banUser(Uuid $userId): void
            {
              $user = $this->repository->find($userId);

              $user->ban();
            }

            // Bad code
            class User
            {
              private Collection $bans;

              public function getBans(): Collection
              {
                return $this->bans;
              }
            }

            public function banUser(Uuid $userId): void
            {
              $user = $this->repository->find($userId);

              $user->getBans()->add(new Ban($user));
            }
Keep classes (entities/domain models) always valid
In order to keep classes valid, you should comply with the following rules:
  • avoid setters for every property
  • use method as one transaction only
  • avoid mixed types, use Value Objects or DTO instead
            class Account
            {
              private AccountStatus $status;

              public function __construct(AccountStatus $status)
              {
                $this->status = $status;
              }

              public function disable(DisableRequestDTO $disableRequest): void
              {
                $this->status->disable($disableRequest);
              }
            }
Decouple Domain layer from Application layer

Domain logic must be implemented in generic way without knowledge of infrastructure, framework, libraries, etc... Domain models lifecycle must be handle only by corresponding domain services.

            // Good code: decouple domain logic from http
            class UserController
            {
              public function registerAction(Request $request): void
              {
                $dto = $this->dtoFactory->createUserRegister($request);

                $this->userManager->register($dto);
              }
            }

            // Bad code: form reads from/writes to user entity
            class UserController
            {
              public function registerAction(): void
              {
                $this->userForm->bind(new User());
              }
            }

            // Bad code: coupling between form and user
            class UserController
            {
              public function registerAction(): void
              {
                $this->em->persist(User::fromFormData($this->form));
              }
            }

Properties

Always mark properties private by default

Use private properties, unless they implicitly required to be used by child classes

Make state immutable and encapsulated

Remember that object passed by reference, so you should store state immutable.

Reasons:
  • Immutable data is simple
  • Immutable data is cacheable
  • Immutable data is predictable
  • Immutable data enables historical analysis
            // Good class
            class BankAccount
            {
              public function setLastRefresh(DateTime $lastRefresh): void
              {
                $this->lastRefresh = clone $lastRefresh;
              }
            }
            // or
            class BankAccount
            {
              public function setLastRefresh(DateTimeImmutable $lastRefresh): void
              {
                $this->lastRefresh = $lastRefresh;
              }
            }

            // Bad class
            class BankAccount
            {
              public function setLastRefresh(DateTime $lastRefresh): void
              {
                $this->lastRefresh = $lastRefresh;
              }
            }

            // Example of bad usage
            $currentTime = new DateTime();
            $bankAccount = new BankAccount();
            $bankAccount->setLastRefresh($currentTime);
            // do stuff...
            $currentTime->setTimestamp($aTimestamp);

Methods

Do not use String method names

It is not possible or hard to find usage of such methods.

            public function calculateDiscount(Product $product, string $type): mixed
            {
              $methodName = 'calculate' . $type; // bad code

              return $this->$methodName($product);
            }

            class ProductController
            {
              public function orderProductAction(Request $request): void
              {
                $form = new OrderProductForm();
                $form->afterSubmit = [$this, 'processForm']; // good code!

                $form->handle($request);
              }

              public function orderProductNewAction(Request $request): void
              {
                $form = new OrderProductForm();
                $form->afterSubmit = $this->processForm(...); // PHP 8.1 first class callable

                $form->handle($request);
              }

              public function processForm(Request $request): void
              {
                // ...
              }
            }

            class Form
            {
              public callable $afterSubmit;

              public function handle(Request $request): void
              {
                // ...

                call_user_func($this->afterSubmit, $request);
              }
            }
Do not use fluent API

It allows you to do same stuff in two different ways. Also you need to be sure about return type.

            // Bad code
            class MyClass
            {
              public function setId($id): self
              {
                ...

                return $this;
              }

              public function setName($name): self
              {
                ...

                return $this;
              }
            }

Fluent API like this is proven to break PHPStan and thus Rector . Such code is almost impossible to upgrade instantly. The longer the fluent methods, the bigger the damage.

Do not use setters, use constructor injection

Do not use setters, use only constructor injection, when possible. That allow to keep classes always valid.

            // Good class
            class MyClass
            {
              private string $property1;
              private string $property2;

              public function __construct(string $property1, string $property2)
              {
                $this->property1 = $property1;
                $this->property2 = $property2;
              }

              public function getProperty1(): string
              {
                return $this->property1;
              }

              public function getProperty2(): string
              {
                return $this->property2;
              }
            }

            // Bad class
            class MyClass
            {
              private string $property1;
              private string $property2;

              public function setProperty1(string $property1): void
              {
                $this->property1 = $property1;
              }

              public function setProperty2(string $property2): void
              {
                $this->property2 = $property2;
              }

              public function getProperty1(): string
              {
                return $this->property1;
              }

              public function getProperty2(): string
              {
                return $this->property2;
              }
            }
Do not use optional dependencies

Replace empty dependencies with stubs.

            // Good class
            class DbConnection
            {
              public function __construct(..., Logger $logger)
              {
                // ...
              }
            }

            $dbConnection = new DbConnection(..., new FakeLogger());

            // Bad class
            class DbConnection
            {
              public function __construct(...)
              {
                // ...
              }

              public function setLogger(Logger $logger = null): void
              {
                $this->logger = $logger;
              }
            }
Avoid unnecessary public methods

Make method public if it is really needed somewhere outside class and if its support really worth it.

Avoid logic switch parameters

Just split method into multiple dedicated methods, or if not make sense - create a new implementation of abstraction.

            // Good class
            class Spammer
            {
              public function sendIllegalSpam(string $email, string $template): void
              {
                // without opt-out link
              }

              public function sendApparentlyLegalSpam(string $email, string $template): void
              {
                // with opt-out link
              }
            }

            // Bad class
            class Spammer
            {
              public function spam(string $email, string $template, bool $optOutLink = false): void
              {
                // yes, this is a really bad spammer
              }
            }
Keep methods as a one transaction

It allow to keep state valid.

            // Good class
            class Account
            {
              public function payBill(Bill $bill, Money $money): void
              {
                $this->addMoney($money);
                $this->markBillPaidWithMoney($bill, $money);
              }

              private function addMoney(Money $money): void
              {
                $this->money[] = $money;
              }

              private function markBillPaidWithMoney(Bill $bill, Money $money): void
              {
                $this->bills[] = $bill->paid($money);
              }
            }

            $account = new Account();
            $account->payBill($bill, $money);

            // Bad class
            class Account
            {
              public function addMoney(Money $money): void
              {
                $this->money[] = $money;
              }

              public function markBillPaidWithMoney(Bill $bill, Money $money): void
              {
                $this->bills[] = $bill->paid($money);
              }
            }

            $account = new Account();
            $account->addMoney($money);
            $account->markBillPaidWithMoney($bill, $money);
Do not assume method idempotence

Do not assume that subsequent method call will be the same.

            // Good code
            $request = $controller->request();
            $userId = $request->get('userId');
            $userRoles = $request->get('userRoles');

            // Bad code
            $userId = $controller->request()->get('userId');
            $userRoles = $controller->request()->get('userRoles');
Enforce strict invariants

Emulate generic type behavior. Use as much strict types as possible.

            class Train
            {
              public function __construct(array $wagons)
              {
                $this->wagons = (function (Wagon ...$wagons): array { return $wagons; })(...$wagons);
              }
            }

Usage of Assert library.

            class Train
            {
              public function __construct(array $wagons)
              {
                Assertion::allIsInstanceOf($wagons, Wagon::class);
              }
            }
Do not use mixed parameter types and return types

Use Value Objects to ensure consistency of objects and prevent multiple data validation.

            // Good class
            class PrisonerTransferRequest
            {
              public function approve(SecurityLevel $securityLevel): SecurityApprovement
              {
                // ...
              }
            }

            // Bad class
            class PrisonerTransferRequest
            {
              /**
              * @param mixed $accessLevel
              *	- false if none
              *	- true if guards are required
              *	- null if to be decided
              *	- 10  if special cargo is needed
              *	- 20  if high security is needed
              *
              * @return mixed
              *	- true if approved
              *	- false if denied
              *	- null if in progress of discussion
              *	- 101 if additional guards applied
              *	- 103 if special cargo applied
              */
              public function approve(mixed $securityLevel): mixed
              {
                // ...
              }
            }

Exceptions

Use quotes (") around parameters
            // good exception
            throw new FilterClassNotFoundException(sprintf(
                'Filter class "%s" was not found',
                $filterClass
            ));
            // Result: Filter class "VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd" was not found

            // bad exception
            throw new FilterClassNotFoundException(sprintf(
                'Filter class %s was not found',
                $filterClass
            ));
            // Result: Filter class VeryLongNamespace\InNestedNamespace\WithMissingClassInTheEnd was not found
Specify exact definition on what is wrong
            throw new InvalidParameterException(
              sprintf('"%s" parameter is invalid.', $parameterName)
            );

            // good parameter name
            $parameterName = 'parameters.page_name.main';

            // bad parameter name
            $parameterName = 'main';
Specify what value is wrong and which values are allowed
            if (is_array($value)) {
                $value = 'array';
            } elseif (is_bool($value)) {
                $value = ($value === true) ? 'true' : 'false';
            }

            throw new InvalidParameterException(sprintf(
                'Parameter value "%s" in "%s" is invalid. It must be a string.',
                $value,
                $parameterName
            ));
Specify relative file paths

You should show file path according to project root, not absolute path, as this data could be very long and really don’t have any value to resolver of a problem. It is also better not to show system directory structure.

            // "$filePath" can be absolute or relative; we don't care, it only must exists
            $fileInfo = new SplFileInfo($filePath);

            // remove absolute path start to cwd (current working directory)
            $relativePath = substr($fileInfo->getRealPath(), strlen(getcwd()) + 1);

            throw new FileProcessingException(sprintf(
                'File "%s" not found',
                $relativePath
            ));

            // good path
            // File "packages/src/TweetPublisher.php" not found

            // bad path
            // File "/var/www/my_project/packages/src/TweetPublisher.php" not found
Standard exceptions
  • LogicException
    Marks that it is error in program logic and leads to fixes in code. May occur when some base conditions used code not met in further code.
    • BadFunctionCallException
      Should be thrown if called function is not exist or some arguments are missing.
      • BadMethodCallException
        Same as above, but used for methods.
    • DomainException
      Should be thrown if some defined domain conditions are not valid. Sanity checks should be used to avoid such exceptions.
    • InvalidArgumentException
      Should be thrown if method/function argument is not valid.
    • LengthException
      Should be thrown if used string length is not valid.
    • OutOfBoundsException
      Should be thrown if required array key is not exist. Works only with string keys, not array indexes.
  • RuntimeException
    Thrown for errors which can be found only on runtime. May occur when some services are not available and should catch those kinds of exception and respond with appropriate message.
    • OutOfRangeException
      Same as OutOfBoundsException buy relates to array indexes.
    • OverflowException
      Should be thrown when some actions are going out of allowed ranges. For example if some container is full, or some retries exceeds.
    • UnderflowException
      Opposite to OverflowException, but notifies that some element is already "empty". For example try to pop some stuff from empty container.
    • RangeException
      Should be thrown if some value is out of specific valid range of values.
    • UnexpectedValueException
      Should be thrown if program receives unexpected for further logic value.

Read more

Annotations

Do not use annotations. Use Attributes instead.

Tags:#PHP