PHPDoc Types

PHP is a dynamically typed language, but by using static analysis tools like psalm or phpstan along with PHPDoc, we can express advanced type concepts and benefit from type checking during static analysis. This reference explains the types available in PHPDoc and other related concepts.

Table of Contents

  1. Atomic Types
  2. Compound Types
  3. Advanced Type System
  4. Type Operators (Utility Types)
  5. Functional Programming Concepts
  6. Assert Annotations
  7. Security Annotations
  8. Example: Using Types in Design Patterns

Atomic Types

These are the basic types that cannot be further divided.

Scalar Types

/** @param int $i */
/** @param float $f */
/** @param string $str */
/** @param lowercase-string $lowercaseStr */
/** @param non-empty-string $nonEmptyStr */
/** @param non-empty-lowercase-string $nonEmptyLowercaseStr */
/** @param class-string $class */
/** @param class-string<AbstractFoo> $fooClass */
/** @param callable-string $callable */
/** @param numeric-string $num */ 
/** @param bool $isSet */
/** @param array-key $key */
/** @param numeric $num */
/** @param scalar $a */
/** @param positive-int $positiveInt */
/** @param negative-int $negativeInt */
/** @param int-range<0, 100> $percentage */
/** @param int-mask<1, 2, 4> $flags */
/** @param int-mask-of<MyClass::CLASS_CONSTANT_*> $classFlags */
/** @param trait-string $trait */
/** @param enum-string $enum */
/** @param literal-string $literalStr */
/** @param literal-int $literalInt */

These types can be combined using Compound Types and Advanced Type System.

Object Types

/** @param object $obj */
/** @param stdClass $std */
/** @param Foo\Bar $fooBar */
/** @param object{foo: string, bar?: int} $objWithProperties */
/** @return ArrayObject<int, string> */
/** @param Collection<User> $users */
/** @return Generator<int, string, mixed, void> */

Object types can be combined with Generic Types.

Array Types

Generic Arrays

/** @return array<TKey, TValue> */
/** @return array<int, Foo> */
/** @return array<string, int|string> */
/** @return non-empty-array<string, int> */

Generic arrays use the concept of Generic Types.

Object-like Arrays

/** @return array{0: string, 1: string, foo: stdClass, 28: false} */
/** @return array{foo: string, bar: int} */
/** @return array{optional?: string, bar: int} */

Lists

/** @param list<string> $stringList */
/** @param non-empty-list<int> $nonEmptyIntList */

PHPDoc Arrays (Legacy Notation)

/** @param string[] $strings */
/** @param int[][] $nestedInts */

Callable Types

/** @return callable(Type1, OptionalType2=, SpreadType3...): ReturnType */
/** @return Closure(bool):int */
/** @param callable(int): string $callback */

Callable types are especially important in Higher-Order Functions.

Value Types

/** @return null */
/** @return true */
/** @return false */
/** @return 42 */
/** @return 3.14 */
/** @return "specific string" */
/** @param Foo\Bar::MY_SCALAR_CONST $const */
/** @param A::class|B::class $classNames */

Special Types

/** @return void */
/** @return never */
/** @return empty */
/** @return mixed */
/** @return resource */
/** @return closed-resource */
/** @return iterable<TKey, TValue> */

Compound Types

These are types created by combining multiple Atomic Types.

Union Types

/** @param int|string $id */
/** @return string|null */
/** @var array<string|int> $mixedArray */
/** @return 'success'|'error'|'pending' */

Intersection Types

/** @param Countable&Traversable $collection */
/** @param Renderable&Serializable $object */

Intersection types can be useful in implementing Design Patterns.

Advanced Type System

These are advanced features that allow for more complex and flexible type expressions.

Generic Types

/**
 * @template T
 * @param array<T> $items
 * @param callable(T): bool $predicate
 * @return array<T>
 */
function filter(array $items, callable $predicate): array {
    return array_filter($items, $predicate);
}

Generic types are often used in combination with Higher-Order Functions.

Template Types

/**
 * @template T of object
 * @param class-string<T> $className
 * @return T
 */
function create(string $className)
{
    return new $className();
}

Template types can be used in combination with Type Constraints.

Conditional Types

/**
 * @template T
 * @param T $value
 * @return (T is string ? int : string)
 */
function processValue($value) {
    return is_string($value) ? strlen($value) : strval($value);
}

Conditional types may be used in combination with Union Types.

Type Aliases

/**
 * @psalm-type UserId = positive-int
 * @psalm-type UserData = array{id: UserId, name: string, email: string}
 */

/**
 * @param UserData $userData
 * @return UserId
 */
function createUser(array $userData): int {
    // User creation logic
    return $userData['id'];
}

Type aliases are helpful for simplifying complex type definitions.

Type Constraints

Type constraints allow you to specify more concrete type requirements for type parameters.

/**
 * @template T of \DateTimeInterface
 * @param T $date
 * @return T
 */
function cloneDate($date) {
    return clone $date;
}

// Usage example
$dateTime = new DateTime();
$clonedDateTime = cloneDate($dateTime);

In this example, T is constrained to classes that implement \DateTimeInterface.

Covariance and Contravariance

When dealing with generic types, the concepts of covariance and contravariance become important.

/**
 * @template-covariant T
 */
interface Producer {
    /** @return T */
    public function produce();
}

/**
 * @template-contravariant T
 */
interface Consumer {
    /** @param T $item */
    public function consume($item);
}

// Usage example
/** @var Producer<Dog> $dogProducer */
/** @var Consumer<Animal> $animalConsumer */

Covariance allows you to use a more specific type (subtype), while contravariance means you can use a more basic type (supertype).

Type Operators

Type operators allow you to generate new types from existing ones. Psalm refers to these as utility types.

Key-of and Value-of Types

  • key-of retrieves the type of all keys in a specified array or object, while value-of retrieves the type of its values.
/**
 * @param key-of<UserData> $key
 * @return value-of<UserData>
 */
function getUserData(string $key) {
    $userData = ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'];
    return $userData[$key] ?? null;
}

/**
 * @return ArrayIterator<key-of<UserData>, value-of<UserData>>
 */
function getUserDataIterator() {
    $userData = ['id' => 1, 'name' => 'John', 'email' => 'john@example.com'];
    return new ArrayIterator($userData);
}

Properties-of Type

properties-of represents the type of all properties of a class. This is useful when dealing with class properties dynamically.

class User {
    public int $id;
    public string $name;
    public ?string $email;
}

/**
 * @param User $user
 * @param key-of<properties-of<User>> $property
 * @return value-of<properties-of<User>>
 */
function getUserProperty(User $user, string $property) {
    return $user->$property;
}

// Usage example
$user = new User();
$propertyValue = getUserProperty($user, 'name'); // $propertyValue is of type string

properties-of has the following variants:

  • public-properties-of<T>: Targets only public properties.
  • protected-properties-of<T>: Targets only protected properties.
  • private-properties-of<T>: Targets only private properties.

Using these variants allows you to deal with properties of specific access modifiers.

Class Name Mapping Type

class-string-map represents an array with class names as keys and their instances as values. This is useful for implementing dependency injection containers or factory patterns.

/**
 * @template T of object
 * @param class-string-map<T, T> $map
 * @param class-string<T> $className
 * @return T
 */
function getInstance(array $map, string $className) {
    return $map[$className] ?? new $className();
}

// Usage example
$container = [
    UserRepository::class => new UserRepository(),
    ProductRepository::class => new ProductRepository(),
];

$userRepo = getInstance($container, UserRepository::class);

Index Access Type

The index access type (T[K]) represents the element of type T at index K. This is useful for accurately representing types when accessing array or object properties.

/**
 * @template T of array
 * @template K of key-of<T>
 * @param T $data
 * @param K $key
 * @return T[K]
 */
function getArrayValue(array $data, $key) {
    return $data[$key];
}

// Usage example
$config = ['debug' => true, 'version' => '1.0.0'];
$debugMode = getArrayValue($config, 'debug'); // $debugMode is of type bool

These utility types are specific to psalm. They can be considered part of the Advanced Type System.

Functional Programming Concepts

PHPDoc supports important concepts influenced by functional programming. Using these concepts can improve the predictability and reliability of your code.

Pure Functions

Pure functions are functions without side effects that always return the same output for the same input.

/**
 * @pure
 */
function add(int $a, int $b): int 
{
    return $a + $b;
}

This annotation indicates that the function has no side effects and always produces the same output for the same input.

Immutable Objects

Immutable objects are objects whose state cannot be altered once they are created.

/**
 * @immutable
 *
 * - All properties are considered readonly.
 * - All methods are implicitly treated as `@psalm-mutation-free`.
 */
class Point {
    public function __construct(
        private float $x, 
        private float $y
    ) {}

    public function withX(float $x): static 
    {
        return new self($x, $this->y);
    }

    public function withY(float $y): static
    {
        return new self($this->x, $y);
    }
}

@psalm-mutation-free

This annotation indicates that a method does not change the internal state of the class or any external state. Methods of @immutable classes implicitly have this property, but it can also be used for specific methods of non-immutable classes.

class Calculator {
    private float $lastResult = 0;

    /**
     * @psalm-mutation-free
     */
    public function add(float $a, float $b): float {
        return $a + $b;
    }

    public function addAndStore(float $a, float $b): float {
        $this->lastResult = $a + $b; // This is not allowed with @psalm-mutation-free
        return $this->lastResult;
    }
}

@psalm-external-mutation-free

This annotation indicates that a method does not change any external state. Changes to the internal state of the class are allowed.

class Logger {
    private array $logs = [];

    /**
     * @psalm-external-mutation-free
     */
    public function log(string $message): void {
        $this->logs[] = $message; // Internal state change is allowed
    }

    public function writeToFile(string $filename): void {
        file_put_contents($filename, implode("\n", $this->logs)); // This changes external state, so it can't be @psalm-external-mutation-free
    }
}

Guidelines for Using Immutability Annotations

  1. Use @immutable when the entire class is immutable.
  2. Use @psalm-mutation-free for specific methods that don’t change any state.
  3. Use @psalm-external-mutation-free for methods that don’t change external state but may change internal state.

Properly expressing immutability can lead to many benefits, including improved safety in concurrent processing, reduced side effects, and easier-to-understand code.

Side Effect Annotations

When a function has side effects, it can be explicitly annotated to caution its usage.

/**
 * @side-effect This function writes to the database
 */
function logMessage(string $message): void {
    // Logic to write message to database
}

Higher-Order Functions

Higher-order functions are functions that take functions as arguments or return functions. PHPDoc can be used to accurately express the types of higher-order functions.

/**
 * @param callable(int): bool $predicate
 * @param list<int>           $numbers
 * @return list<int>
 */
function filter(callable $predicate, array $numbers): array {
    return array_filter($numbers, $predicate);
}

Higher-order functions are closely related to Callable Types.

Assert Annotations

Assert annotations are used to inform static analysis tools that certain conditions are met.

/**
 * @psalm-assert string $value
 * @psalm-assert-if-true string $value
 * @psalm-assert-if-false null $value
 */
function isString($value): bool {
    return is_string($value);
}

/**
 * @psalm-assert !null $value
 */
function assertNotNull($value): void {
    if ($value === null) {
        throw new \InvalidArgumentException('Value must not be null');
    }
}

/**
 * @psalm-assert-if-true positive-int $number
 */
function isPositiveInteger($number): bool {
    return is_int($number) && $number > 0;
}

These assert annotations are used as follows:

  • @psalm-assert: Indicates that the assertion is true if the function terminates normally (without throwing an exception).
  • @psalm-assert-if-true: Indicates that the assertion is true if the function returns true.
  • @psalm-assert-if-false: Indicates that the assertion is true if the function returns false.

Assert annotations may be used in combination with Type Constraints.

Security Annotations

Security annotations are used to highlight security-critical parts of the code and track potential vulnerabilities. There are mainly three annotations:

  1. @psalm-taint-source: Indicates an untrusted input source.
  2. @psalm-taint-sink: Indicates where security-critical operations are performed.
  3. @psalm-taint-escape: Indicates where data has been safely escaped or sanitized.

Here’s an example of using these annotations:

/**
 * @psalm-taint-source input
 */
function getUserInput(): string {
    return $_GET['user_input'] ?? '';
}

/**
 * @psalm-taint-sink sql
 */
function executeQuery(string $query): void {
    // Execute SQL query
}

/**
 * @psalm-taint-escape sql
 */
function escapeForSql(string $input): string {
    return addslashes($input);
}

// Usage example
$userInput = getUserInput();
$safeSqlInput = escapeForSql($userInput);
executeQuery("SELECT * FROM users WHERE name = '$safeSqlInput'");

By using these annotations, static analysis tools can track the flow of untrusted input and detect potential security issues (such as SQL injection).

Example: Using Types in Design Patterns

You can use the type system to implement common design patterns in a more type-safe manner.

Builder Pattern

/**
 * @template T
 */
interface BuilderInterface {
    /**
     * @return T
     */
    public function build();
}

/**
 * @template T
 * @template-implements BuilderInterface<T>
 */
abstract class AbstractBuilder implements BuilderInterface {
    /** @var array<string, mixed> */
    protected $data = [];

    /** @param mixed $value */
    public function set(string $name, $value): static {
        $this->data[$name] = $value;
        return $this;
    }
}

/**
 * @extends AbstractBuilder<User>
 */
class UserBuilder extends AbstractBuilder {
    public function build(): User {
        return new User($this->data);
    }
}

// Usage example
$user = (new UserBuilder())
    ->set('name', 'John Doe')
    ->set('email', 'john@example.com')
    ->build();

Repository Pattern

/**
 * @template T
 */
interface RepositoryInterface {
    /**
     * @param int $id
     * @return T|null
     */
    public function find(int $id);

    /**
     * @param T $entity
     */
    public function save($entity): void;
}

/**
 * @implements RepositoryInterface<User>
 */
class UserRepository implements RepositoryInterface {
    public function find(int $id): ?User {
        // Logic to retrieve user from database
    }

    public function save(User $user): void {
        // Logic to save user to database
    }
}

Summary

By deeply understanding and appropriately using the PHPDoc type system, you can benefit from self-documenting code, early bug detection through static analysis, powerful code completion and assistance from IDEs, clarification of code intentions and structure, and mitigation of security risks. This allows you to write more robust and maintainable PHP code.

References

To make the most of PHPDoc types, static analysis tools like Psalm or PHPStan are necessary. For more details, refer to the following resources: