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') throws TypeError instead 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 $id in a docblock. Now int|string is enforced at runtime. int|false is the canonical "search result or not found" type (like array_search returns). Use ?Type for nullable (shorthand for Type|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 writing A&B&C, consider creating a combined interface interface 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 with clone $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_NAME can 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 — unlike gettype(), 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