This guide covers how to annotate your PHP source code for Phan V6, including all union type syntax, PHPDoc annotations, and new V6 features like generic types, variance annotations, and utility types.
- Basic Annotations
- Union Type Syntax
- Multiline Doc Comments (V6)
- Generic Type Annotations (V6)
- Template Constraints (V6)
- Variance Annotations (V6)
- Utility Types (V6)
- PHP 8.4/8.5 Features
- Property and Method Annotations
- Assertion Annotations
- Suppression Annotations
- Advanced Features
The @var annotation specifies types for class properties and constants. It cannot be used on local variables.
class Example {
/** @var string */
private $name;
/** @var array<int,string> */
private $items = [];
/** @var int */
const MAX_SIZE = 100;
/** @var DateTime|null */
public $timestamp = null;
}For local variables, use inline type annotations with string literals ('@phan-var UnionType $varName'). These must appear as standalone statement expressions after the variable is assigned.
function processData() {
$result = fetchComplexData();
'@phan-var array<int,User> $result';
// Phan now knows $result is array<int,User>
foreach ($result as $id => $user) {
echo "$id: {$user->getName()}\n";
}
// Use @phan-var-force to create variable if it doesn't exist
'@phan-var-force int $counter';
$counter = 0;
// Heredoc syntax also works
list($status, $data) = fetchResponse();
<<<'PHAN'
@phan-var bool $status
@phan-var array{id:int,name:string} $data
PHAN;
}Annotate function and method parameters:
/**
* @param string $name User's full name
* @param int $age User's age in years
* @param array<string> $tags List of tags
* @return User
*/
function createUser($name, $age, array $tags) {
return new User($name, $age, $tags);
}Specify what a function returns:
/**
* @return int|null Returns user ID or null if not found
*/
function findUserId($email) {
return $this->db->query("SELECT id FROM users WHERE email = ?", [$email]);
}
/**
* @return never This function never returns (throws or exits)
*/
function handleFatalError($message) {
throw new RuntimeException($message);
}
/**
* @return void
*/
function logMessage($message) {
error_log($message);
}Document magic properties on classes:
/**
* @property string $name
* @property int $age
* @property-read string $id Read-only property
* @property-write string $password Write-only property
*/
class User {
private $data = [];
public function __get($key) {
return $this->data[$key] ?? null;
}
public function __set($key, $value) {
$this->data[$key] = $value;
}
}Phan supports a rich set of union type syntax for precise type annotations. Union types allow you to specify that a value can be one of several types.
/**
* @param int|string $value Can be an integer or string
* @param int|float|null $number Can be a number or null
* @return bool|array
*/
function process($value, $number) {
return is_string($value) ? [$value] : true;
}/**
* @param array<int,string> $map Array with int keys and string values
* @param array<string> $list Array with string values (any keys)
* @param string[] $strings Equivalent to array<int,string>
* @param non-empty-array<User> $users Non-empty array of User objects
* @return array<string,mixed>
*/
function processArrays($map, $list, $strings, $users) {
return ['result' => $map];
}Array shapes allow you to specify exact array structures:
/**
* @param array{name:string,age:int} $person Exact array structure
* @param array{id:int,email:string,active?:bool} $user Optional 'active' key
* @return array{success:bool,message:string}
*/
function validateUser($person, $user) {
return ['success' => true, 'message' => 'Valid'];
}/**
* @param callable(int,string):bool $callback Takes int and string, returns bool
* @param callable():void $noArgs No arguments, no return value
* @param callable(User):array<string> $transform User to string array
*/
function runCallbacks($callback, $noArgs, $transform) {
$callback(42, "test");
$noArgs();
$transform(new User());
}/**
* @template T
* @param class-string<T> $className Name of a class
* @return T Instance of that class
*/
function instantiate($className) {
return new $className();
}
/**
* @param class-string $anyClass Any class name
* @param class-string<Exception> $exceptionClass Only exception class names
*/
function loadClasses($anyClass, $exceptionClass) {
// ...
}/**
* @param 'read'|'write'|'execute' $permission Specific string values only
* @param 1|2|3 $level Specific integer values only
* @return true Always returns true (not just bool)
*/
function checkPermission($permission, $level) {
return true;
}/**
* @param object{name:string,age:int} $person Object with specific properties
* @return object{id:int,created_at:string}
*/
function createRecord($person) {
return (object)['id' => 1, 'created_at' => date('Y-m-d')];
}/**
* @param int|string|array<string>|null $mixed Complex union
* @param array<int,string>|array<string,int> $either One array type or another
* @param callable(string):void|null $optionalCallback Callback or null
*/
function complexTypes($mixed, $either, $optionalCallback) {
// ...
}New in V6: Doc comments for @param, @var, @return, and Phan-specific annotations can now span multiple lines. This is useful for complex nested array structures and generic types.
/**
* @param array{
* user: array{id: int, name: string},
* permissions: array<string>,
* metadata: array{created_at: string, updated_at: string}
* } $data Complex nested structure
*/
function processData($data) {
// $data has complex nested structure with full type checking
}For complex generic types, you can spread them across lines for readability:
/**
* @template TKey
* @template TValue
* @param array<
* TKey,
* array{
* id: int,
* value: TValue,
* metadata: array<string, mixed>
* }
* > $items
* @return array<TKey, TValue>
*/
function transformItems($items) {
// Process and return transformed data
}Extended multiline support for Phan-specific annotations:
/**
* @phan-param array{
* users: array<int, User>,
* roles: array<string, Role>
* } $config
*
* @phan-var list<array{
* id: positive-int,
* name: non-empty-string,
* active: bool
* }> $result
*/
function processUsers($config) {
// Multiline Phan annotations provide clearer documentation
}This feature helps with:
- Readability: Complex types are easier to understand when formatted nicely
- Maintenance: Changes to nested structures are clearer
- Documentation: Self-documenting code through formatted type definitions
Phan V6 introduces comprehensive support for generic types across classes, interfaces, and traits.
Define reusable classes with type parameters:
/**
* @template T
*/
class Container {
/** @var T */
private $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
/** @return T */
public function get() {
return $this->value;
}
}
// Usage
/** @var Container<string> $stringContainer */
$stringContainer = new Container("hello");Define generic interfaces and implement them with specific types:
/**
* @template T
*/
interface Repository {
/** @param int $id */
/** @return T|null */
public function find($id);
/** @param T $entity */
public function save($entity);
}
/**
* @implements Repository<User>
*/
class UserRepository implements Repository {
public function find($id) {
// Returns User|null
return $this->db->findUser($id);
}
public function save($entity) {
// $entity is User
$this->db->saveUser($entity);
}
}Define generic traits and use them with specific types:
/**
* @template T
*/
trait Timestampable {
/** @var T */
private $timestamp;
/** @param T $time */
public function setTimestamp($time) {
$this->timestamp = $time;
}
/** @return T */
public function getTimestamp() {
return $this->timestamp;
}
}
/**
* @use Timestampable<int>
*/
class UnixTimestampedEntity {
use Timestampable;
}
/**
* @use Timestampable<DateTime>
*/
class DateTimeEntity {
use Timestampable;
}/**
* @template TKey
* @template TValue
*/
class Map {
/** @var array<TKey,TValue> */
private $items = [];
/**
* @param TKey $key
* @param TValue $value
*/
public function set($key, $value) {
$this->items[$key] = $value;
}
/**
* @param TKey $key
* @return TValue|null
*/
public function get($key) {
return $this->items[$key] ?? null;
}
}/**
* @template T
* @param array<T> $items
* @param callable(T):bool $predicate
* @return array<T>
*/
function filter(array $items, callable $predicate) {
return array_filter($items, $predicate);
}
/**
* @template TIn
* @template TOut
* @param array<TIn> $items
* @param callable(TIn):TOut $mapper
* @return array<TOut>
*/
function map(array $items, callable $mapper) {
return array_map($mapper, $items);
}Template constraints restrict template parameters to specific types or their subtypes:
/**
* @template T of Animal
*/
class AnimalShelter {
/** @var array<T> */
private $animals = [];
/** @param T $animal */
public function add($animal) {
$this->animals[] = $animal;
$animal->feed(); // Safe: all Animals have feed()
}
}
// Valid usage
/** @var AnimalShelter<Dog> $dogShelter */
$dogShelter = new AnimalShelter(); // Dog extends Animal
// Invalid usage - Phan will report an error
/** @var AnimalShelter<Car> $carShelter */
$carShelter = new AnimalShelter(); // Car does not extend Animal/**
* @template T of JsonSerializable
*/
class JsonCollection {
/** @var array<T> */
private $items;
/** @param array<T> $items */
public function __construct(array $items) {
$this->items = $items;
}
public function toJson() {
return json_encode($this->items); // Safe: all items are JsonSerializable
}
}/**
* @template T of ArrayAccess&Countable
*/
class CollectionWrapper {
/** @var T */
private $collection;
/** @param T $collection */
public function __construct($collection) {
$this->collection = $collection;
}
public function isEmpty() {
return count($this->collection) === 0; // Safe: T is Countable
}
public function first() {
return $this->collection[0]; // Safe: T is ArrayAccess
}
}Variance annotations control how generic types relate to each other in inheritance hierarchies.
Covariant templates allow subtype substitution in output positions (return types):
/**
* @template-covariant T
*/
interface Producer {
/** @return T */
public function produce();
}
/**
* @implements Producer<Animal>
*/
class AnimalProducer implements Producer {
public function produce() {
return new Animal();
}
}
/**
* @implements Producer<Dog>
*/
class DogProducer implements Producer {
public function produce() {
return new Dog(); // Dog is subtype of Animal
}
}
// Valid: DogProducer is subtype of Producer<Animal>
/** @param Producer<Animal> $producer */
function acceptProducer($producer) {
$animal = $producer->produce();
}
acceptProducer(new DogProducer()); // OK with covarianceContravariant templates allow supertype substitution in input positions (parameters):
/**
* @template-contravariant T
*/
interface Consumer {
/** @param T $item */
public function consume($item);
}
/**
* @implements Consumer<Animal>
*/
class AnimalConsumer implements Consumer {
public function consume($item) {
$item->feed();
}
}
/**
* @implements Consumer<Dog>
*/
class DogConsumer implements Consumer {
public function consume($item) {
$item->bark();
}
}
// Valid: AnimalConsumer can consume Dogs (Animals can consume any subtype)
/** @param Consumer<Dog> $consumer */
function processDogs($consumer) {
$consumer->consume(new Dog());
}
processDogs(new AnimalConsumer()); // OK with contravarianceWithout variance annotations, templates are invariant (exact type match required):
/**
* @template T (invariant by default)
*/
class Box {
/** @var T */
private $item;
/** @param T $item */
public function set($item) {
$this->item = $item;
}
/** @return T */
public function get() {
return $this->item;
}
}
/** @param Box<Animal> $box */
function useBox($box) {
// ...
}
useBox(new Box()); // Must be exactly Box<Animal>, not Box<Dog>Phan V6 introduces several utility types for more precise type annotations.
Extract key or value types from array type expressions:
/**
* @return key-of<array{foo:int,bar:string}> Returns 'foo'|'bar'
*/
function getValidKey() {
return 'foo'; // OK
}
/**
* @return value-of<array{foo:int,bar:string}> Returns int|string
*/
function getValidValue() {
return 42; // OK - int is valid
}
/**
* @param key-of<array<string,int>> $key String type
* @param value-of<array<string,int>> $value Int type
*/
function processMapping($key, $value) {
// $key is string, $value is int
}
// Invalid examples that Phan will catch:
/**
* @return key-of<array{foo:int,bar:string}>
*/
function invalidKey() {
return 'baz'; // Error: 'baz' is not a valid key
}
/**
* @return value-of<array{foo:int,bar:string}>
*/
function invalidValue() {
return false; // Error: bool is not a valid value type
}Specify integer ranges:
/**
* @param int-range<1,12> $month Month number (1-12)
* @param int-range<1,31> $day Day of month (1-31)
* @param int-range<1900,2100> $year
* @return string
*/
function formatDate($month, $day, $year) {
return sprintf("%04d-%02d-%02d", $year, $month, $day);
}
formatDate(12, 25, 2024); // OK
formatDate(13, 25, 2024); // Phan error: 13 is outside range 1-12
/**
* @param int-range<0,100> $percentage
*/
function setOpacity($percentage) {
// $percentage is guaranteed to be 0-100
}/**
* @param positive-int $count Must be > 0
* @return array<int>
*/
function generateSequence($count) {
return range(1, $count);
}
generateSequence(5); // OK
generateSequence(0); // Phan error: 0 is not positive
generateSequence(-1); // Phan error: -1 is not positive
/**
* @param negative-int $debt Amount owed (negative value)
*/
function recordDebt($debt) {
// $debt is guaranteed to be < 0
}
recordDebt(-100); // OK
recordDebt(100); // Phan error: 100 is not negativeA string that is not the empty string ''. This includes the string '0' (which is falsey in PHP but non-empty). Phan infers this type when a variable is compared with !== ''.
/**
* @param non-empty-string $name Cannot be empty string
*/
function greetUser($name) {
echo "Hello, $name!";
}
greetUser("Alice"); // OK
greetUser("0"); // OK - '0' is non-empty
greetUser(""); // Phan error: empty string not allowedA string that is truthy in PHP — excludes both '' and '0'. This is stricter than non-empty-string. Phan infers this type when a string passes a truthiness check such as if ($s).
/**
* @param non-falsy-string $code Cannot be empty or '0'
*/
function processCode($code) {
echo "Processing: $code";
}
processCode("ABC"); // OK
processCode(""); // Phan error: '' is not non-falsy-string
processCode("0"); // Phan error: '0' is not non-falsy-stringThe two types relate as follows:
| Type | Excludes '' |
Excludes '0' |
Inferred from |
|---|---|---|---|
non-empty-string |
Yes | No | $s !== '' |
non-falsy-string |
Yes | Yes | if ($s), !empty($s) |
/**
* @var array{
* users: array<positive-int,non-empty-string>,
* status: 'active'|'inactive'|'pending',
* priority: int-range<1,5>
* }
*/
$config = [
'users' => [1 => 'Alice', 2 => 'Bob'],
'status' => 'active',
'priority' => 3
];
/**
* @param key-of<$config> $key
* @return value-of<$config>
*/
function getConfig($key) {
global $config;
return $config[$key];
}Phan V6 adds full support for PHP 8.4 and 8.5 language features with type annotations.
Mark functions, methods, and class constants as deprecated using the attribute:
#[Deprecated(message: "Use newMethod() instead", since: "2.0.0")]
public function oldMethod() {
return $this->newMethod();
}
#[Deprecated]
class LegacyClass {
#[Deprecated(message: "Use VALUE_NEW instead")]
public const VALUE_OLD = 1;
}Phan will emit PhanDeprecatedFunction, PhanDeprecatedMethod, and PhanDeprecatedClassConstant when deprecated items are used.
Declare types for class constants with full inheritance checking:
interface Config {
public const int PORT = 8080;
public const string HOST = 'localhost';
}
class ApiConfig implements Config {
public const int PORT = 3000; // OK: compatible type
public const string HOST = '0.0.0.0'; // OK: compatible type
}
class BadConfig implements Config {
public const string PORT = '3000'; // Error: type mismatch
}Phan validates:
- Type compatibility in inheritance (
PhanConstantTypeMismatchInheritance) - Assignment types (
PhanTypeMismatchDeclaredConstant) - Never type violations (
PhanTypeMismatchDeclaredConstantNever)
Full support for property hooks with type validation:
class User {
/**
* @var non-empty-string
*/
public string $username {
set {
if (strlen($value) === 0) {
throw new ValueError("Username cannot be empty");
}
$this->username = $value;
}
}
/**
* @var positive-int
*/
public int $age {
get {
return $this->age;
}
set {
if ($value < 0) {
throw new ValueError("Age must be positive");
}
$this->age = $value;
}
}
}Phan detects:
- Hook parameter type mismatches (
PhanPropertyHookIncompatibleParamType) - Return type incompatibilities (
PhanPropertyHookIncompatibleReturnType) - Set hooks on readonly properties (
PhanReadonlyPropertyHasSetHook) - Default values on hooked properties (
PhanPropertyHookWithDefaultValue)
Mark return values that shouldn't be ignored:
#[NoDiscard]
function generateId(): string {
return uniqid('id_');
}
#[NoDiscard(message: "The transaction was not committed")]
function startTransaction(): Transaction {
return new Transaction();
}
// Using (void) cast to suppress the warning
(void) generateId(); // OK - explicit discard
$id = generateId(); // OK - value is used
generateId(); // Error: PhanNoDiscardReturnValueIgnoredFull type inference through pipe operator chains:
/**
* @param positive-int $value
* @return positive-int
*/
function double($value) {
return $value * 2;
}
/**
* @param int $value
* @return string
*/
function stringify($value) {
return (string) $value;
}
// Phan understands the full type chain
$result = 5 |> double(...) |> stringify(...);
// $result is stringDocument magic methods that don't exist in the source code:
/**
* @method string getName() Get the user's name
* @method void setName(string $name) Set the user's name
* @method static User create(array $data) Create a new user
* @method int|null findIdByEmail(string $email)
*/
class User {
private $attributes = [];
public function __call($method, $args) {
// Magic method handling
}
public static function __callStatic($method, $args) {
// Static magic method handling
}
}
$user = new User();
$user->setName("Alice"); // Phan knows this method exists
$name = $user->getName(); // Phan knows this returns stringDocument property hooks:
class User {
/**
* @var non-empty-string
*/
public string $username {
set {
if (strlen($value) === 0) {
throw new ValueError("Username cannot be empty");
}
$this->username = $value;
}
}
/**
* @var positive-int
*/
public int $age {
set {
if ($value <= 0) {
throw new ValueError("Age must be positive");
}
$this->age = $value;
}
}
}class User {
/**
* @var non-empty-string
* @readonly
*/
public string $id;
public function __construct(string $id) {
$this->id = $id;
}
}Phan supports assertion annotations to help with type narrowing and validation.
Assert types for parameters:
/**
* @param mixed $value
* @phan-assert string $value
*/
function assertString($value) {
if (!is_string($value)) {
throw new TypeError("Expected string");
}
}
function process($input) {
assertString($input);
// Phan now knows $input is a string
echo strlen($input);
}/**
* @param mixed $value
* @return bool
* @phan-assert-true-condition string $value
*/
function isString($value) {
return is_string($value);
}
function handle($data) {
if (isString($data)) {
// Phan knows $data is string here
echo strlen($data);
}
}/**
* @template T
* @param mixed $value
* @param class-string<T> $className
* @return T
* @phan-assert T $value
*/
function assertInstanceOf($value, $className) {
if (!$value instanceof $className) {
throw new TypeError("Expected instance of $className");
}
return $value;
}
function processUser($input) {
$user = assertInstanceOf($input, User::class);
// Phan knows $user is User
echo $user->getName();
}Suppress specific issue types:
/**
* @suppress PhanTypeInvalidThrowsIsInterface
*/
function riskyOperation() {
throw new Exception();
}
class Example {
/**
* @suppress PhanUnreferencedPublicMethod
*/
public function unusedMethod() {
// Method is intentionally unused
}
}
// Inline suppression
/** @suppress PhanTypeMismatchArgument */
processValue($wrongType);Suppress issues on the following line only:
/** @phan-suppress-next-line PhanTypeMismatchArgument */
processValue($wrongType);
/** @phan-suppress-next-line PhanTypeArraySuspicious, PhanTypeMismatchReturn */
return $complexValue;Suppress issues for an entire file:
<?php
/**
* @phan-file-suppress PhanUnreferencedClass
* @phan-file-suppress PhanUnreferencedPublicMethod
*/
// All classes and methods in this file won't trigger these issues
class HelperClass {
public function helperMethod() {
// ...
}
}When Phan infers an unexpected type for a variable, you can ask it to print the inferred type by adding an inline string expression statement containing @phan-debug-var:
function example(array $items): void {
$first = $items[0] ?? null;
'@phan-debug-var $first';
// Phan emits: PhanDebugAnnotation - $first has union type mixed
}This must be a string expression statement — not a comment — because php-ast does not expose comments to Phan. Phan emits a PhanDebugAnnotation diagnostic with the inferred union type.
You can debug multiple variables at once, including array elements and properties:
'@phan-debug-var $arr[\'key\'], $obj->prop, $result';This is useful when:
- Investigating why Phan reports a type mismatch you don't expect
- Verifying that type narrowing in a conditional branch works correctly
- Understanding how a complex union type is flowing through your code
Note: Remove
@phan-debug-varstatements before committing — they intentionally emit diagnostics every time Phan runs.
Mark deprecated code:
/**
* @deprecated Use newMethod() instead
*/
function oldMethod() {
return $this->newMethod();
}
/**
* @deprecated 2.0.0 Will be removed in 3.0.0
*/
class LegacyClass {
}Mark internal APIs:
/**
* @internal This class is for internal use only
*/
class InternalHelper {
}
class PublicAPI {
/**
* @internal
*/
public function internalMethod() {
// Not part of public API
}
}Indicate method overrides (PHP 8.3+):
class Parent {
public function process() {
}
}
class Child extends Parent {
/**
* @override
*/
public function process() {
// Override parent method
}
}Document thrown exceptions:
/**
* @param string $filename
* @throws FileNotFoundException When file doesn't exist
* @throws PermissionDeniedException When file isn't readable
* @return string
*/
function readFile($filename) {
if (!file_exists($filename)) {
throw new FileNotFoundException($filename);
}
if (!is_readable($filename)) {
throw new PermissionDeniedException($filename);
}
return file_get_contents($filename);
}Indicate trait or class mixing:
/**
* @mixin Builder
*/
class Model {
use HasBuilder;
// Phan understands Model has Builder methods
}Define type aliases:
/**
* @phan-type UserId = positive-int
* @phan-type UserData = array{id:UserId,name:non-empty-string,email:string}
* @phan-type UserCollection = array<UserId,UserData>
*/
class UserRepository {
/**
* @param UserId $id
* @return UserData|null
*/
public function find($id) {
// ...
}
/**
* @return UserCollection
*/
public function findAll() {
// ...
}
}Mark pure functions (no side effects):
/**
* @phan-pure
* @param int $a
* @param int $b
* @return int
*/
function add($a, $b) {
return $a + $b;
}Mark immutable classes:
/**
* @phan-immutable
*/
class Point {
public function __construct(
public readonly int $x,
public readonly int $y
) {}
public function withX(int $x): self {
return new self($x, $this->y);
}
}/**
* @template T
* @phan-type Result<T> = array{success:bool,data:T|null,error:string|null}
*/
class ApiClient {
/**
* @template TResponse
* @param string $url
* @return Result<TResponse>
*/
public function get($url) {
// ...
}
}// Bad: Too generic
/** @param array $users */
// Good: Specific structure
/** @param array<int,User> $users */
// Better: Even more specific
/** @param non-empty-array<positive-int,User> $users */// Bad: No constraints
/**
* @template T
*/
class Repository {
/** @param T $entity */
public function save($entity) {
$entity->getId(); // Unsafe!
}
}
// Good: With constraints
/**
* @template T of Entity
*/
class Repository {
/** @param T $entity */
public function save($entity) {
$entity->getId(); // Safe: all Entities have getId()
}
}/**
* @return array{
* user: User,
* permissions: array<string>,
* metadata: array{created_at:string,updated_at:string}
* }
*/
function getUserData($id) {
// Complex structured return
}// Producers: use covariant
/**
* @template-covariant T
*/
interface Source {
/** @return T */
public function get();
}
// Consumers: use contravariant
/**
* @template-contravariant T
*/
interface Sink {
/** @param T $item */
public function put($item);
}/**
* @param array{
* port: int-range<1,65535>,
* host: non-empty-string,
* timeout: positive-int
* } $config
*/
function connectToServer(array $config) {
// Type-safe configuration
}/**
* @template T
*/
class Builder {
/** @var array<string,mixed> */
private $data = [];
/**
* @param string $key
* @param mixed $value
* @return $this
*/
public function set($key, $value) {
$this->data[$key] = $value;
return $this;
}
/**
* @param class-string<T> $className
* @return T
*/
public function build($className) {
return new $className($this->data);
}
}/**
* @template T of Entity
*/
abstract class AbstractRepository {
/**
* @param positive-int $id
* @return T|null
*/
abstract public function find($id);
/**
* @return array<positive-int,T>
*/
abstract public function findAll();
/**
* @param T $entity
*/
abstract public function save($entity);
}
/**
* @extends AbstractRepository<User>
*/
class UserRepository extends AbstractRepository {
public function find($id) {
// Returns User|null
}
public function findAll() {
// Returns array<positive-int,User>
}
public function save($entity) {
// $entity is User
}
}/**
* @template T
*/
interface Factory {
/**
* @param array<string,mixed> $config
* @return T
*/
public function create(array $config);
}
/**
* @implements Factory<Database>
*/
class DatabaseFactory implements Factory {
public function create(array $config) {
return new Database($config);
}
}- Generic Types V6 - Detailed guide on generics
- Issue Types Caught by Phan
- Phan Configuration