Skip to content

Latest commit

 

History

History
389 lines (281 loc) · 13.5 KB

conventions--php.md

File metadata and controls

389 lines (281 loc) · 13.5 KB

IxDF's PHP conventions

[[toc]]

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:

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

Type-casting

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);

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
// 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:

/** @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:

/**
 * @template T of \Illuminate\Notifications\Notification
 * @param class-string<T> $notificationFQCN
 * @return T
 */
protected function initialize(string $notificationFQCN): Notification
{
    // Implementation...
}

Additional Resources

  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

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.

// 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.

public static function createFromName(string $name): self
{
    return new self($name);
}

Named constructors

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»

Domain-specific operations

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?

"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.

Enums

  • Use singular names
  • Use PascalCase for case names
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.

// 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 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:

// 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.

::: tip Internal docs More info about exceptions. :::

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.

::: info 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:

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 of intval($value))
  • Use isset() or array_key_exists() instead of in_array() for large arrays when checking for key existence
  • Leverage opcache for production environments
  • Use stripos() instead of strpos() with strtolower() 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

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 and composer.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 or composer-unused + composer-require-checker)
  • Consider using composer-normalize for consistent composer.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)

🦄