Phan v6 introduces significant improvements to type inference and type narrowing, making analysis more accurate and useful for complex code patterns.
- Intersection Types
- Type Narrowing
- Array Type Inference
- Generator Type Support
- Literal Type Handling
- Union Type Clamping
Phan v6 improves handling of intersection types (types that must satisfy multiple interfaces/classes simultaneously).
interface Countable {
public function count(): int;
}
interface Iterator {
public function next(): void;
}
/**
* @param Countable&Iterator $collection
*/
function processCollection($collection) {
$count = $collection->count(); // Safe: Countable methods available
$collection->next(); // Safe: Iterator methods available
// Error: stdClass methods not available
// $collection->property;
}New in V6: Improved handling when some classes are unknown.
interface Known {}
// When intersection includes unknown class
/** @param Known&UnknownClass $value */
function process($value) {
// Phan still validates against Known
// But can't validate against UnknownClass
}Phan v6 analyzes the known types and skips unknown ones, enabling type checking even with incomplete stubs.
catch (FileException&UnknownException $e) {
// Phan knows about FileException
$e->getFilePath(); // Type-checked
// UnknownException methods not validated
}interface Reader {
public function read(): string;
}
interface Writer {
public function write(string $data): void;
}
class ReadOnlyStream implements Reader {
public function read(): string { return "data"; }
}
/** @param Reader&Writer $stream */
function processStream($stream) {
echo $stream->read();
}
// Error: ReadOnlyStream doesn't implement Writer
processStream(new ReadOnlyStream());V6 improves type narrowing in various contexts.
/**
* @param array<string|int> $items
*/
function processItems($items) {
// Without narrowing - generic error
foreach ($items as $item) {
strlen($item); // Error: int might not support strlen
}
// With type checks - narrowed
foreach ($items as $item) {
if (is_string($item)) {
strlen($item); // OK: Phan knows $item is string
}
}
}New in V6: Phan now narrows types when excluding literals from known arrays.
/** @param 'read'|'write'|'execute'|null $permission */
function checkPermission($permission) {
if (!in_array($permission, ['read', 'write', 'execute'], true)) {
// Phan narrows to null (only possibility left)
echo $permission; // string|int can't be accepted
}
}Limits: Works for arrays up to 50 literal elements.
New in V6: Improved type narrowing with intermediary variables.
function process($value) {
$isValid = is_string($value);
if ($isValid) {
// Phan now understands $value is string here
echo strlen($value); // OK
}
}New in V6: Type narrowing works correctly with static properties.
class Config {
public static string|null $host = null;
}
function getHost(): string {
if (Config::$host === null) {
Config::$host = 'localhost';
}
// Phan knows $host is non-null string here
return Config::$host;
}interface Entity {
public function getId(): int;
}
/**
* @param mixed $value
*/
function getEntityId($value) {
// Phan handles instanceof with is_object()
if (is_object($value) && $value instanceof Entity) {
return $value->getId(); // Type-safe
}
// Or directly
if ($value instanceof Entity) {
return $value->getId();
}
}New in V6: array_filter() recognizes null-stripping callbacks.
/**
* @param array<string|null> $items
* @return array<string> // Null stripped!
*/
function getValidItems($items) {
// Phan infers: returns array<string>
// because the callback removes null values
return array_filter($items);
}
/**
* @param array<mixed> $items
* @return array<string>
*/
function getStrings($items) {
// Explicit callback - type checked
return array_filter($items, 'is_string');
}New in V6: Improved array_chunk() inference based on parameters.
/**
* @param array<int,string> $items
*/
function processChunks($items) {
// With preserve_keys=false (default)
$chunks = array_chunk($items, 2);
// Type: array<int, array<int, string>>
// Keys reset to 0, 1, 2, ...
// With preserve_keys=true
$chunks = array_chunk($items, 2, true);
// Type: array<int, array<int, string>>
// Original keys preserved
}New in V6: constant() return type inference.
define('API_KEY', 'secret123');
define('MAX_RETRIES', 3);
function getConstant(string $name) {
$value = constant($name); // mixed (can't determine)
}
function getStaticConstant() {
// For non-dynamic constants, Phan can infer
$key = constant('API_KEY'); // string
$max = constant('MAX_RETRIES'); // int
}New in V6: Improved inference for explicit iterator generics.
/**
* @template T
* @implements Iterator<int, T>
*/
class ItemIterator implements Iterator {
// Implementation
}
/**
* @param ItemIterator<User> $iterator
*/
function processUsers($iterator) {
foreach ($iterator as $key => $user) {
// Phan knows: $key is int, $user is User
echo $user->getName();
}
}/**
* @return Generator<int, User>
*/
function getUserGenerator() {
yield 1 => new User('Alice');
yield 2 => new User('Bob');
}
function useGenerator() {
foreach (getUserGenerator() as $id => $user) {
// Phan knows: $id is int, $user is User
echo $user->getName();
}
}/**
* @return Generator<int, string, string, array>
*/
function doubleYield() {
$received = yield 1 => 'first';
$final = yield 2 => 'second';
return ['received' => $received, 'final' => $final];
}Type parameters:
- Key type (yield key)
- Value type (yield value)
- Send type (what can be sent back)
- Return type (final return value)
/**
* @param 'read'|'write'|'execute' $mode
*/
function setFileMode($mode) {
if ($mode === 'read') {
// Phan narrows to literal 'read'
$permissions = 0444;
} elseif ($mode === 'write') {
// Literal 'write'
$permissions = 0644;
} else {
// Literal 'execute'
$permissions = 0755;
}
}New in V6: Better handling of enum cases as constants.
enum Status: string {
case ACTIVE = 'active';
case INACTIVE = 'inactive';
}
/**
* @param Status::ACTIVE|Status::INACTIVE $status
*/
function setStatus($status) {
// Phan knows $status is one of the literal values
echo $status->value; // 'active' or 'inactive'
}New in V6: Literal types are erased in reference assignments.
$var1 = 'literal_value';
$var2 =& $var1;
// Before: $var2 has type string with literal 'literal_value'
// After: $var2 has type string (literal erased)
// Because reference could be modified elsewhereThis prevents false positives when variables are aliased:
$id = 42; // literal int
$ref =& $id;
$ref = 99;
// After reference assignment, $id is int (not literal 42)
if ($id === 42) { // Warning: always false - good!
}function complexMerge() {
$result = [];
// Each iteration potentially adds new types
foreach ($arrays as $arr) {
$result = array_merge($result, $arr);
}
// Union type could grow exponentially
// V6 clamps it: int|string|float|bool|null
// Instead of: thousands of specific types
}// .phan/config.php
return [
'max_union_type_set_size' => 1024, // Default
];Impact:
512: Very conservative, may lose precision1024: Good balance (default)2048: Better precision, more memory4096+: Precise but potentially slow
When union exceeds limit:
// Before clamping
int|float|string|bool|object|array|resource|null
// After clamping (simplified)
int|float|string|mixedCommon types stay separate:
intandfloatkept separatestringkept separate- Less common types merged to
mixed
New in V6: Improved static return type inference.
class Base {
public function create(): static {
return new static();
}
}
class Derived extends Base {
public function special(): string {
return "special";
}
}
/**
* @template T of Base
* @param class-string<T> $class
* @return T
*/
function instantiate($class) {
$instance = new $class();
// For Derived: Phan knows instance is Derived
// Can call special()
return $instance;
}New in V6: Better inference with mixed containing objects.
/**
* @param object|string $value
*/
function process($value) {
if (is_object($value)) {
// Phan knows $value is object
$class = get_class($value);
}
}New in V6: Stricter checking for unions with mixed.
/**
* @param int|mixed $value // Effectively just mixed
*/
function process($value) {
// Phan treats as mixed (mixed dominates)
}New in V6: Improved type narrowing for nested array shapes.
/**
* @param array{
* user: array{id: int, name: string},
* status: string
* } $data
*/
function refineData($data) {
if (isset($data['user']['id'])) {
// Phan refines: $data['user']['id'] is int
$id = $data['user']['id'];
}
}/**
* @param array{id?: int, name: string} $record
*/
function processRecord($record) {
if (array_key_exists('id', $record)) {
// Phan narrows: $record['id'] is int
echo $record['id'] + 1;
}
}/**
* @strict
* @param array $items // Error: specify element type
*/
function process($items) {
}/**
* Suppress specific type warnings
* @suppress PhanTypeMismatchArgument
*/
function legacyFunction($value) {
// Type checks not enforced
}/**
* @template T
* @param mixed $value
* @param class-string<T> $type
* @return T
* @throws TypeError
*/
function assertType($value, $type) {
if (!$value instanceof $type) {
throw new TypeError("Expected $type");
}
return $value;
}
// Usage
$user = assertType($data, User::class);
// Phan knows: $user is Userinterface Service {
public function create(): Entity;
}
class SpecialService implements Service {
public function create(): SpecialEntity {
// OK: SpecialEntity extends Entity
return new SpecialEntity();
}
}/**
* @param array{type: 'user', user: User}|array{type: 'admin', admin: Admin} $data
*/
function processData($data) {
match ($data['type']) {
'user' => handleUser($data['user']),
'admin' => handleAdmin($data['admin']),
};
}- [[Annotating-Your-Source-Code-V6]] - Annotation reference
- [[Generic-Types-V6]] - Generics and templates
- [[About-Union-Types]] - Union type details
- [[Memory-and-Performance-Optimizations]] - Type system performance