[[toc]]
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.
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.
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...
Use declare(strict_types=1);
in all files. This catches type-related bugs early and promotes more thoughtful code, resulting in increased stability.
- 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
Prefer type-casting over dedicated methods for better performance:
// GOOD
$score = (int) '7';
$hasMadeAnyProgress = (bool) $this->score;
// BAD
$score = intval('7');
$hasMadeAnyProgress = boolval($this->score);
- 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
// GOOD
final class Foo
{
/** @var list<string> */
private array $urls;
/** @var \Illuminate\Support\Collection<int, \App\Models\User> */
private Collection $users;
}
- Use
@inheritDoc
for classes and methods to make inheritance explicit - For properties, copy the docblock from the parent class/interface instead of using
@inheritDoc
Use advanced PHPDoc syntax to describe traversable types:
/** @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)
Use Psalm template annotations for generic types:
/**
* @template T of \Illuminate\Notifications\Notification
* @param class-string<T> $notificationFQCN
* @return T
*/
protected function initialize(string $notificationFQCN): Notification
{
// Implementation...
}
- Union Types vs. Intersection Types
- PHPDoc: Typing in Psalm
- PHPDoc: Scalar types in Psalm
- When to declare classes final
- Proposed PSR for docblocks
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.
Use ClassName::class
instead of hardcoded fully qualified class names.
// GOOD
use App\Modules\Payment\Models\Order;
echo Order::class;
// BAD
echo 'App\Modules\Payment\Models\Order';
Prefer self
over the class name for return type hints and instantiation within the class.
public static function createFromName(string $name): self
{
return new self($name);
}
Use named static constructors to create objects with valid state:
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»
Encapsulate domain logic in specific methods rather than using generic setters:
// 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.
- Use singular names
- Use PascalCase for case names
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
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.
// 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:
$debugInfo = sprintf('Current FQCN is %s. Method name is: %s', self::class, __METHOD__);
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.
*/
Avoid the "Exception" suffix in exception class names. This encourages more descriptive naming. For details, see The "Exception" suffix.
::: tip Internal docs More info about exceptions. :::
- 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.
::: info Internal docs 🔒
Current status of assert()
on production: ENABLED (see infrastructure/php/8.3/production/fpm/php.ini), reasons: #19772.
:::
Prioritize regex readability. For guidance, refer to Writing better Regular Expressions in PHP.
Use DEFINE
for recurring patterns and sprintf
for reusable definitions:
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.
- 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.
- 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
There is a great guide Testing tips by Kamil Ruczyński.
See Security section from Laravel conventions.
- 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)
🦄