Lab 14: Type System & Attributes
Objective
Master PHP 8's type system: union types, intersection types, never, mixed, readonly classes, and PHP Attributes (annotations). Use strict_types, type coercion rules, and runtime type checking.
Background
PHP's type system has evolved from weakly-typed (PHP 4) to progressively stricter (PHP 7 scalar types, PHP 8.0 union types, PHP 8.1 never/readonly/intersection, PHP 8.2 readonly classes, PHP 8.3 typed class constants). Understanding PHP's type system helps you write self-documenting, IDE-friendly, refactorable code — and prevents entire classes of bugs.
Time
30 minutes
Prerequisites
Lab 07 (OOP), Lab 08 (Inheritance)
Tools
PHP 8.3 CLI
Docker image:
zchencow/innozverse-php:latest
Lab Instructions
Step 1: Scalar Types & strict_types
💡
declare(strict_types=1)affects only the file it's in. A strict file calling a non-strict function still enforces type checking on the call. A non-strict file calling a strict function does not. Strict mode prevents silent coercions —add('3', '4')throwsTypeErrorinstead of silently converting.
📸 Verified Output:
Step 2: Union Types & Nullable
💡 Union types replace doc-comment workarounds. Before PHP 8, you'd write
@param int|string $idin a docblock. Nowint|stringis enforced at runtime.int|falseis the canonical "search result or not found" type (likearray_searchreturns). Use?Typefor nullable (shorthand forType|null).
📸 Verified Output:
Step 3: Intersection Types
💡 Intersection types (
A&B) require the value to implement ALL listed interfaces — they're stricter than union types. Use them when a function truly needs multiple capabilities. If you find yourself writingA&B&C, consider creating a combined interfaceinterface AWithBC extends A, B, C {}for readability.
📸 Verified Output:
Step 4: Readonly Classes (PHP 8.2)
💡
readonly class(PHP 8.2) makes ALL declared properties readonly without decorating each one. It's perfect for value objects, DTOs (Data Transfer Objects), and domain events — objects that represent facts and should never change after construction. Cloning withclone $obj with {prop: val}is the PHP 8.4+ way to make modified copies.
📸 Verified Output:
Step 5: Typed Class Constants (PHP 8.3)
💡 Typed class constants (PHP 8.3) enforce that
const string APP_NAMEcan only hold a string — assigning an integer would be a compile error. Previously, constants were untyped and could hold any value. This feature closes the last major gap in PHP's type system for class members.
📸 Verified Output:
Step 6: PHP Attributes
💡 PHP Attributes (PHP 8.0) replace docblock annotations (
@ORM\Column). They're real PHP code — syntax-checked, IDE-indexed, and type-safe. Frameworks read them via Reflection at startup (then cache the result). Doctrine, Symfony routing, and PHP-DI all support attribute-based configuration.
📸 Verified Output:
Step 7: Type Checking & instanceof
💡
get_debug_type()(PHP 8.0) is the right tool for debugging type values — unlikegettype(), it returns"null"(not"NULL"),"Circle"(not"object"), and"int"(not"integer"). Use it in error messages, logging, and assertions for human-readable type names.
📸 Verified Output:
Step 8: Complete — DTO + Validation System
💡 Attribute-driven validation is exactly how Symfony's Validator component works —
#[Assert\NotBlank],#[Assert\Email],#[Assert\Range(min: 0)]. Reflection reads these at runtime (cached for performance) and runs the corresponding validators. This approach is self-documenting: constraints live with the property they constrain.
📸 Verified Output:
Summary
PHP 8's type system is comprehensive and expressive. You've covered strict_types, union types, intersection types, DNF types, mixed, readonly classes, typed class constants, PHP Attributes with Reflection, and a complete attribute-driven DTO validator. These features make PHP competitive with TypeScript and Kotlin for type safety.
Further Reading
Last updated
