IxDF's PHP conventions β
Introduction β
IxDF's PHP coding guidelines favor a Java-like approach: less magic, more types. We prioritize explicit, strongly-typed code to enhance clarity, IDE support, and static analysis capabilities.
Key principles:
- Minimize magic, maximize explicitness
- Leverage PHP's type system
- Optimize for IDE and static analyzer support
This guide assumes familiarity with PHP 8.x features and modern development practices. It focuses on our specific conventions and rationale, rather than explaining basic concepts.
Code Style and Tools β
IxDF adheres to PER Coding Style 2.0, extended with rules from Slevomat Coding Standard,
PHP Coding Standards Fixer and select guidelines from Spatie's Laravel PHP style guide.
Tools β
IxDF uses automated tools to check our code on CI:
- PHP-CS-Fixer (fast and stable)
- PHPCS (extendable and customizable)
- Psalm (
errorLevel="1"
) - PHPStan (
level: 8
) - Rector
- Deptrac (multiple configurations for modules and entire application)
- composer-dependency-analyser (checks for unused and shadow dependencies)
- and more...
Types β
Strict types β
Use declare(strict_types=1);
in all files. This catches type-related bugs early and promotes more thoughtful code, resulting in increased stability.
Type declarations β
- Always specify property types (when possible)
- Always specify parameter types (when possible)
- Always use return types (when possible)
- Use
void
for methods that return nothing - Use
never
for methods that always throw an exception
- Use
Type-casting β
Prefer type-casting over dedicated methods for better performance:
php
// GOOD
$score = (int) '7';
$hasMadeAnyProgress = (bool) $this->score;
// BAD
$score = intval('7');
$hasMadeAnyProgress = boolval($this->score);
Docblocks β
- Avoid docblocks for fully type-hinted methods/functions unless a description is necessary ((Visual noise is real))
- Use docblocks to reveal the contents of arrays and collections
- Write docblocks on one line when possible
- Always use fully qualified class names in docblocks
php
// GOOD
final class Foo
{
/** @var list<string> */
private array $urls;
/** @var \Illuminate\Support\Collection<int, \App\Models\User> */
private Collection $users;
}
Inheritance and @inheritDoc β
- Use
@inheritDoc
for classes and methods to make inheritance explicit - For properties, copy the docblock from the parent class/interface instead of using
@inheritDoc
Traversable Types β
Use advanced PHPDoc syntax to describe traversable types:
php
/** @return list<string> */
/** @return array<int, Type> */
/** @return Collection<TKey, TValue> */
/** @return array{foo: string, optional?: int} */
Technical details
We use IxDF coding-standard package to enforce setting the type of the key and value in the iterable types using phpcs with SlevomatCodingStandard.TypeHints.*
rules (config)
Generic Types and Templates β
Use Psalm template annotations for generic types:
php
/**
* @template T of \Illuminate\Notifications\Notification
* @param class-string<T> $notificationFQCN
* @return T
*/
protected function initialize(string $notificationFQCN): Notification
{
// Implementation...
}
Additional Resources β
- Union Types vs. Intersection Types
- PHPDoc: Typing in Psalm
- PHPDoc: Scalar types in Psalm
- When to declare classes final
- Proposed PSR for docblocks
OOP Practices β
Final by default β
Use final
for classes and private
for methods by default. This encourages composition, dependency injection, and interface use over inheritance. Consider the long-term maintainability, especially for public APIs.
Class name resolution β
Use ClassName::class
instead of hardcoded fully qualified class names.
php
// GOOD
use App\Modules\Payment\Models\Order;
echo Order::class;
// BAD
echo 'App\Modules\Payment\Models\Order';
Use self
keyword β
Prefer self
over the class name for return type hints and instantiation within the class.
php
public static function createFromName(string $name): self
{
return new self($name);
}
Named constructors β
Use named static constructors to create objects with valid state:
php
public static function createFromSignup(AlmostMember $almostMember): self
{
return new self(
$almostMember->company_name,
$almostMember->country
);
}
Reason: have a robust API that does 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Β»
Domain-specific operations β
Encapsulate domain logic in specific methods rather than using generic setters:
php
// GOOD
public function confirmEmailAwaitingConfirmation(): void
{
$this->email = $this->email_awaiting_confirmation;
$this->email_awaiting_confirmation = null;
}
// BAD
public function setEmail(string $email): self;
This approach promotes rich domain models and thin controllers/services.
Want to learn more?
Read more about class invariants for a better understanding of the dangers of modifying class properties from controllers/services.
Enums β
- Use singular names
- Use PascalCase for case names
php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
Strings β
TL;DR: interpolation > sprintf
> concatenation
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}.";
// BAD (hard to distinguish the variable)
$greeting = "Hi, I am $name.";
// BAD (less readable)
$greeting = 'Hi, I am '.$name.'.';
$greeting = 'Hi, I am ' . $name . '.';
For more complex cases when there are a lot of variables to concat or when itβs not possible to use string interpolation, please use sprintf
function:
php
$debugInfo = sprintf('Current FQCN is %s. Method name is: %s', self::class, __METHOD__);
Comments and Code Clarity β
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:
php
// 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.
*/
Exceptions β
Exception Naming β
Avoid the "Exception" suffix in exception class names. This encourages more descriptive naming. For details, see The "Exception" suffix.
Internal docs
assert() vs throw β
- Use
assert()
for conditions that should be logically impossible to be false, based on your own code's inputs. - Use exceptions for checks based on external inputs.
- Treat
assert()
as a debugging tool and type specification aid, not for runtime checks. - Consider adding a description to
assert()
for clarity (2nd arg).
Remember: assert()
may be disabled in production. Use exceptions for critical runtime checks.
For more information:
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.
Internal docs π
Current status of assert()
on production: ENABLED (see infrastructure/php/8.3/production/fpm/php.ini), reasons: #19772.
Regular Expressions β
Prioritize regex readability. For guidance, refer to Writing better Regular Expressions in PHP.
Use DEFINE
for recurring patterns and sprintf
for reusable definitions:
php
final 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";
}
}
Use Regex101 for testing patterns.
TIP
There is a less popular, hidden PHP germ sscanf
function that can be used for parsing strings and simplify your code in some cases.
Performance Considerations β
Functions β
- Prefer type-casting over type conversion functions (e.g.,
(int)$value
instead ofintval($value)
) - Use
isset()
orarray_key_exists()
instead ofin_array()
for large arrays when checking for key existence - Leverage opcache for production environments
- Use
stripos()
instead ofstrpos()
withstrtolower()
for case-insensitive string searches - Consider using
array_column()
for extracting specific columns from multidimensional arrays
For in-depth performance analysis, use tools like Blackfire, XHProf, or Xdebug and Clockwork in development.
Configs β
- Use
opcache
for production environments - Use PHP in worker code (FrankenPHP, RoadRunner, Swoole) for high-performance applications
- If you use PHP-FPM: Mateus GuimarΓ£es: Optimizing PHP applications for performance
Testing and Quality Assurance β
There is a great guide Testing tips by Kamil RuczyΕski.
Security β
See Security section from Laravel conventions.
Dependency Management β
- Use Composer for managing PHP dependencies
- Keep
composer.json
andcomposer.lock
in version control - Specify exact versions or version ranges for production dependencies
- Use
composer update
sparingly in production environments - Regularly update dependencies and review changelogs
- Leverage tools to check for unused and shadow dependencies (
composer-dependency-analyser
orcomposer-unused
+composer-require-checker
) - Consider using
composer-normalize
for consistentcomposer.json
formatting - Use private repositories or artifact repositories for internal packages
- Implement a dependency security scanning tool in your CI pipeline (e.g., Snyk, Sonatype, or GitHub's Dependabot; add
composer audit
to you CI pipeline)
π¦