Function Contracts
Functions and methods form the public boundaries of your software modules. TypePHP enforces @param and @return annotations directly at function entry and exit points.
Parameter Contracts (@param)
When you declare @param annotations on a function or class method, TypePHP validates all incoming arguments before entering the function body:
Suppressing Function Contracts: Need to skip type-checking on a legacy function or method? Add
@typephp-ignoreto its docblock. See Ignore Annotations for full details.
<?php
declare(strict_types=1);
/**
* @param positive-int $id
* @param non-empty-string $username
* @param 'admin'|'editor'|'viewer' $role
*/
function registerUser(int $id, string $username, string $role): void
{
// Executed only if all arguments pass validation
}
// Valid Call
registerUser(100, 'Alice', 'admin');
// Invalid Call (Passing negative integer)
registerUser(-5, 'Alice', 'admin');
// Throws: TypeError: registerUser(): Argument $id must be of type positive-int, negative int (-5) givenExecution Order Note: Native PHP type hints (e.g.,
int $id,string $username) are evaluated by PHP's C-engine before function execution begins. TypePHP's extended PHPDoc contracts (e.g.,positive-int,non-empty-string) execute at the very start of the function/method body. If a native type hint fails, PHP throws its nativeTypeErrorbefore TypePHP's guard rails run.
Tooling Annotation Priority Hierarchy (@phpstan-* > @psalm-* > @*)
Modern PHP packages and frameworks (such as Doctrine Collections, Symfony, and Laravel) frequently declare both broad IDE-fallback annotations and strict static analysis contracts on the exact same method signature:
/**
* @param mixed $element // Broad fallback for standard IDEs
* @phpstan-param positive-int $element // Refined contract for static analyzers
*
* @return mixed
* @phpstan-return list<positive-int>
*/
public function add(mixed $element): mixed;When multiple tool annotations are declared on the same parameter or return value, TypePHP resolves the active contract using a deterministic 3-Tier Priority Hierarchy:
$$\text{1. } \mathbf{@phpstan\text{-}} \quad \longrightarrow \quad \text{2. } \mathbf{@psalm\text{-}} \quad \longrightarrow \quad \text{3. } \mathbf{@* \text{ (Standard)}}$$
Why Priority Matters in Real-World Codebases
- Refined Contracts Take Precedence: Tool-specific annotations (
@phpstan-param,@psalm-return) contain specific type constraints (such as generic templates, array shapes, or integer bounds) that standard@param mixedomits. TypePHP always enforces the tighter, intended contract. - Third-Party Framework Compatibility: Libraries like Doctrine Collections declare
@phpstan-param T $elementonCollection::addalongside nativemixed $element. TypePHP automatically prioritizes@phpstan-param, making generic collections enforce types at runtime without manual wrapper code.
Tooling Priority Matrix Across Boundary Contracts
| Boundary Type | Priority 1 (Highest) | Priority 2 | Priority 3 (Fallback) |
|---|---|---|---|
| Parameters | @phpstan-param | @psalm-param | @param |
| Return Values | @phpstan-return | @psalm-return | @return |
PHP 8.0+ Named Arguments
TypePHP natively supports PHP 8.0+ Named Arguments. Because parameter contracts are mapped by parameter name rather than argument position index, you can pass named arguments in any order, and TypePHP will accurately validate each parameter:
<?php
declare(strict_types=1);
/**
* @param positive-int $id
* @param non-empty-string $username
* @param int<1, 100> $age
*/
function registerUser(int $id, string $username, int $age): void
{
// ...
}
// Valid Call: Arguments passed in completely reversed/swapped order
registerUser(age: 25, username: 'Alice', id: 42);
// Invalid Call: $id (-5) passed as 3rd named argument
registerUser(age: 25, username: 'Alice', id: -5);
// Throws: TypeError: registerUser(): Argument $id must be of type positive-int, negative int (-5) givenArguments Passed By-Reference (&$param)
TypePHP natively supports PHP's by-reference parameter semantics (function update(int &$value)).
How By-Reference Validation Works
- Entry Guard Rails: TypePHP inspects and validates the variable's value on function entry before the function body executes.
- In-Place Caller Scope Mutation: If the argument passes validation, the function body executes normally, and any modifications to the variable mutate the caller's variable in the caller's scope.
- Safety Guarantee on Failure: If an invalid value is passed into a by-reference parameter, a
TypeErroris thrown before any code in the function body runs, ensuring the caller's variable remains 100% un-mutated and un-corrupted.
<?php
declare(strict_types=1);
/**
* @param positive-int &$score
* @param non-empty-string &$username
*/
function applyBonus(int &$score, string &$username): void
{
$score += 50;
$username = strtoupper($username);
}
// 1. Valid Call: Value is validated on entry and mutated in caller scope
$userScore = 100;
$userName = 'alice';
applyBonus($userScore, $userName);
echo $userScore; // Output: 150
echo $userName; // Output: 'ALICE'
// 2. Invalid Call: Throws TypeError on entry, leaving caller variable untouched!
$invalidScore = -10;
$userTag = 'alice';
try {
applyBonus($invalidScore, $userTag);
} catch (\TypeError $e) {
echo $invalidScore; // Still -10 (Caller variable was never corrupted!)
}DocBlock Syntax Flexibility (&$param vs. $param)
You can write your DocBlock annotations with or without the leading ampersand (&). Both are recognized identically by TypePHP's parser:
// Option A: Explicit ampersand in DocBlock (Recommended)
/**
* @param positive-int &$count
*/
function incrementCount(int &$count): void { $count++; }
// Option B: Ampersand in native PHP signature only (Fully supported)
/**
* @param positive-int $count
*/
function incrementCount(int &$count): void { $count++; }By-Reference Arrays & Shapes
Mutating collections or array shapes in-place preserves caller scope bindings:
/**
* @param list<positive-int> &$scores
*/
function appendReward(array &$scores): void
{
$scores[] = 500; // Mutates array in caller scope
}
$myScores = [10, 20, 30];
appendReward($myScores);
print_r($myScores); // [10, 20, 30, 500]Variadic By-Reference Parameters (&...$params)
When accepting a variable number of by-reference arguments (int &...$numbers), TypePHP validates every individual argument on entry and preserves in-place mutations across all variadic arguments:
/**
* @param positive-int &...$numbers
*/
function doubleAll(int &...$numbers): void
{
foreach ($numbers as &$num) {
$num *= 2;
}
}
$a = 5;
$b = 10;
$c = 15;
doubleAll($a, $b, $c);
echo "$a, $b, $c"; // Output: 10, 20, 30OOP & Interface Inheritance for By-Reference Parameters
When a child class implements an interface or overrides a parent method with by-reference parameters, the type contract and reference semantics are inherited automatically (even when child methods rename parameters):
interface StatusUpdaterInterface
{
/**
* @param non-empty-string &$status
* @param positive-int &$code
*/
public function update(string &$status, int &$code): void;
}
class StatusUpdater implements StatusUpdaterInterface
{
// Inherits contracts and by-reference semantics seamlessly
public function update(string &$status, int &$statusCode): void
{
$status = strtoupper($status);
$statusCode += 100;
}
}
$updater = new StatusUpdater();
$currentStatus = 'pending';
$currentCode = 200;
$updater->update($currentStatus, $currentCode);
echo $currentStatus; // 'PENDING'
echo $currentCode; // 300Class Methods (Instance & Static)
All parameter and return contract rules apply identically to instance methods (public, protected, private) and static methods:
class UserService
{
/**
* Instance Method Contract
*
* @param positive-int $id
* @return array{id: positive-int, name: non-empty-string}
*/
public function findUser(int $id): array
{
return ['id' => $id, 'name' => 'Alice'];
}
/**
* Static Method Contract
*
* @param non-empty-string $role
* @return list<positive-int>
*/
public static function getRoleIds(string $role): array
{
return [10, 20, 30];
}
}
$service = new UserService();
// Invalid Instance Method Call ($id is negative)
$service->findUser(-10);
// Throws: TypeError: UserService::findUser(): Argument $id must be of type positive-int
// Invalid Static Method Call ($role is empty string)
UserService::getRoleIds('');
// Throws: TypeError: UserService::getRoleIds(): Argument $role must be of type non-empty-stringClass Constructors (__construct)
TypePHP fully validates class constructor arguments, supporting both standard constructors and Constructor Property Promotion (PHP 8.0+).
Promoted Properties (PHP 8.0+)
Annotate promoted properties in the constructor's docblock using standard @param or @phpstan-param tags:
class Order
{
/**
* @param positive-int $id
* @param non-empty-string $sku
* @param int<1, 100> $quantity
*/
public function __construct(
public int $id,
public string $sku,
public int $quantity
) {}
}
// Valid Instance
new Order(1, 'SKU-99', 5);
// Invalid Instance ($id is negative)
new Order(-1, 'SKU-99', 5);
// Throws: TypeError: Order::__construct(): Argument $id must be of type positive-intProperty @var Fallback for Un-Annotated Constructors
If a constructor parameter is un-annotated (or lacks a @param tag), TypePHP automatically inspects the corresponding class property's @var / @phpstan-var docblock to infer the parameter contract:
class User
{
/**
* @var string[]
*/
public array $roles;
// Un-annotated constructor parameter inherits contract from $roles property docblock!
public function __construct(array $roles)
{
$this->roles = $roles;
}
}
// Invalid Instance (element 1 is an integer)
new User(['admin', 12345]);
// Throws: TypeError: User::__construct(): Argument $roles[1] must be of type string, int (12345) givenReturn Contracts (@return)
TypePHP validates return statements before values are returned to the caller:
/**
* @return array{id: positive-int, status: 'active'|'pending'}
*/
function getUserStatus(int $id): array
{
if ($id <= 0) {
return ['id' => $id, 'status' => 'active']; // Invalid: $id is negative
}
return ['id' => $id, 'status' => 'active'];
}
getUserStatus(-10);
// Throws: TypeError: getUserStatus(): Return value['id'] must be of type positive-intFluent $this Identity Returns
For fluent builder or service classes annotated with @return $this, TypePHP verifies strict object identity ($result === $this), preventing accidental instantiation of new instances:
class UserBuilder
{
private string $name = '';
/**
* @return $this
*/
public function setName(string $name): self
{
$this->name = $name;
return $this; // Valid: Strict $this identity
}
/**
* @return $this
*/
public function cloneSelf(): self
{
return new self(); // Invalid: New instance returned instead of $this
}
}
$builder = new UserBuilder();
$builder->cloneSelf();
// Throws: TypeError: UserBuilder::cloneSelf(): Return value must be $this instanceLate Static Binding Return Contracts (@return static)
When a parent class method (static factory method or fluent instance method) is annotated with @return static, TypePHP enforces Late Static Binding at runtime.
It dynamically verifies that the returned object is an instance of the actual calling class (UserEntityFactory), strictly rejecting parent instances (BaseEntityFactory), sibling instances (AdminEntityFactory), or generic objects (stdClass):
abstract class BaseEntityFactory
{
/**
* @return static
*/
public static function create(): static
{
return new static();
}
/**
* @return static
*/
public static function createSibling(): object
{
return new AdminEntityFactory(); // Invalid: Returns sibling instead of calling class!
}
}
class UserEntityFactory extends BaseEntityFactory {}
class AdminEntityFactory extends BaseEntityFactory {}
// Valid: Returns UserEntityFactory instance matching the late-static calling class
$user = UserEntityFactory::create();
// Invalid: UserEntityFactory called, but AdminEntityFactory was returned!
UserEntityFactory::createSibling();
// Throws: TypeError: UserEntityFactory::createSibling(): Return value must be of type App\UserEntityFactory, App\AdminEntityFactory returnedLate Static Binding with Generics (static<T>)
Late static binding seamlessly integrates with TypePHP's Reified Generics engine. A static factory can return a specialized generic instance of the late-static-bound calling class:
/**
* @template T
*/
abstract class BaseGenericFactory
{
/**
* @template TValue
* @param TValue $value
* @return static<TValue>
*/
public static function of(mixed $value): static
{
return new static($value);
}
}
class UserGenericFactory extends BaseGenericFactory {}
// 1. Returns UserGenericFactory instance
// 2. Binds generic template T = Dog in WeakMap memory!
$factory = UserGenericFactory::of(new Dog());Variadic Parameter Contracts
When a function or method accepts variadic arguments (...$items), TypePHP validates every element passed in the variadic argument list:
/**
* @param positive-int ...$ids
*/
function deleteUsers(int ...$ids): void
{
// ...
}
// Valid Call
deleteUsers(10, 20, 30);
// Invalid Call (3rd variadic item violates positive-int)
deleteUsers(10, 20, -5);
// Throws: TypeError: deleteUsers(): Argument $ids[2] must be of type positive-intConditional Return Types
TypePHP supports parameter-based conditional return types (@return ($param is true ? TypeA : TypeB)):
/**
* @param bool $asInt
* @param mixed $value
* @return ($asInt is true ? positive-int : non-empty-string)
*/
function formatValue(bool $asInt, mixed $value): mixed
{
return $value;
}
// Valid Calls
formatValue(true, 42); // Evaluates return type as positive-int
formatValue(false, 'hello'); // Evaluates return type as non-empty-string
// Invalid Call
formatValue(true, 'not_an_int');
// Throws: TypeError: formatValue(): Return value must be of type positive-intPHP 8.0+ Attributes Coexistence
TypePHP seamlessly coexists with native PHP 8.0+ Attributes (#[Route], #[Inject], #[Validate]).
You can place your PHPDoc annotations either above or below native PHP attributes on properties, methods, or functions. TypePHP's AST engine and PHP's Reflection API process both metadata channels independently without any syntax conflicts:
// Option A: DocBlock ABOVE Attribute (Supported)
/**
* @param positive-int $id
* @return array{id: positive-int, username: non-empty-string}
*/
#[Route('/user/{id}', method: 'GET')]
public function showUser(int $id): array
{
return ['id' => $id, 'username' => 'Alice'];
}
// Option B: DocBlock BELOW Attribute (Supported)
#[Route('/user/{id}', method: 'GET')]
/**
* @param positive-int $id
* @return array{id: positive-int, username: non-empty-string}
*/
public function showUser(int $id): array
{
return ['id' => $id, 'username' => 'Alice'];
}