PHP best practices

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.
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
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()
.
$_GET
$_POST
$_COOKIE
$_SERVER
$_ENV
$_FILES
$_REQUEST
fopen('php://input', 'r')
- 3rd party service response data
- ...
In order to validate data you can use beberlei/assert library. Read more and view example on: Enforce strict invariant.
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) {
}
Use final
classes, unless they implicitly required to be extendable.
- 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
- Prefer abstractions over implementation (use
interface
) - Bypass Finals during tests
ElementInterface
ElementTrait
ElementException
AbstractElement
- Is
ChildClass
aParentClass
particularization? "Is Dog and Animal? Yes!" So useextend
. - Is
ParentClass
hasChildClass
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 {}
Traits add coupling between classes.
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)
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() {} }
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));
}
- 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);
}
}
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));
}
}
Use private
properties, unless they implicitly required to be used by child classes
Remember that object passed by reference, so you should store state immutable.
- 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);
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);
}
}
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 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;
}
}
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;
}
}
Make method public
if it is really needed somewhere outside class and if its support really worth it.
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
}
}
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 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');
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);
}
}
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
{
// ...
}
}
// 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
throw new InvalidParameterException(
sprintf('"%s" parameter is invalid.', $parameterName)
);
// good parameter name
$parameterName = 'parameters.page_name.main';
// bad parameter name
$parameterName = 'main';
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
));
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
- LogicExceptionMarks 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.
- BadFunctionCallExceptionShould be thrown if called function is not exist or some arguments are missing.
- BadMethodCallExceptionSame as above, but used for methods.
- DomainExceptionShould be thrown if some defined domain conditions are not valid. Sanity checks should be used to avoid such exceptions.
- InvalidArgumentExceptionShould be thrown if method/function argument is not valid.
- LengthExceptionShould be thrown if used string length is not valid.
- OutOfBoundsExceptionShould be thrown if required array key is not exist. Works only with string keys, not array indexes.
- RuntimeExceptionThrown 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.
- OutOfRangeExceptionSame as OutOfBoundsException buy relates to array indexes.
- OverflowExceptionShould be thrown when some actions are going out of allowed ranges. For example if some container is full, or some retries exceeds.
- UnderflowExceptionOpposite to OverflowException, but notifies that some element is already "empty". For example try to pop some stuff from empty container.
- RangeExceptionShould be thrown if some value is out of specific valid range of values.
- UnexpectedValueExceptionShould be thrown if program receives unexpected for further logic value.
Do not use annotations. Use Attributes instead.