What you'll learn
Use PHP's modern type system to make invalid states difficult to express. Enums model closed choices, union and intersection types clarify contracts, and attributes attach structured metadata for frameworks and tools.
By the end of this lesson, you'll be able to:
- Apply strict and expressive type declarations
- Model finite states with backed enums
- Read a custom attribute through reflection
Core mental model
Professional PHP is less about memorizing APIs and more about choosing a clear boundary for each responsibility. Use this table as a decision guide while reading the examples.
| Concept | What it protects | Decision rule |
|---|---|---|
| Strict types | Predictable scalar calls | Declare it in every application PHP file. |
| Enum | Closed valid choices | Use instead of magic strings with a known set. |
| Attribute | Machine-readable metadata | Use when another component must inspect declarations. |
Professional workflow
Build the feature in small, verifiable steps. Each step leaves the system in a state you can test.
- Describe the type and metadata boundary boundary: its inputs, outputs, invariants, and expected failures.
- Implement the smallest happy path behind an explicit contract.
- Add validation and translate low-level failures into language the caller understands.
- Exercise the boundary with realistic data, then inspect output, logs, and resource cleanup.
- Refactor only after behavior is protected by a repeatable check.
Make the boundary visible
Guided code lab
Model lifecycle states
A backed enum gives storage values while keeping valid choices exhaustive.
<?php
declare(strict_types=1);
enum CourseStatus: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
public function isVisible(): bool
{
return $this === self::Published;
}
}
$status = CourseStatus::from('published');Attach route metadata
The attribute carries data; reflection-based infrastructure decides how to use it.
<?php
#[Attribute(Attribute::TARGET_METHOD)]
final readonly class Route
{
public function __construct(public string $method, public string $path) {}
}
final class CourseController
{
#[Route('GET', '/courses')]
public function index(): array { return []; }
}
$method = new ReflectionMethod(CourseController::class, 'index');
$route = $method->getAttributes(Route::class)[0]->newInstance();Production practice
Contract
Prefer the narrowest honest return and parameter types; avoid mixed at internal boundaries.
Verification
Test every enum case and any reflection convention that turns attributes into runtime behavior.
Operations
Static analysis should run at a strict level so declared types stay truthful across changes.
Common failure mode
Independent workshop
Create a typed command bus with enum priorities and a Handler attribute discovered through reflection.
Your finished workshop must include:
- Backed Priority enum with behavior
- Repeatable Handler attribute
- A discovery script rejecting duplicate handlers
Definition of done
Recap & quick check
Key takeaways
- Strict declarations reduce surprising coercion.
- Enums replace scattered magic strings.
- Attributes are metadata, not hidden business logic.
- Static analysis extends what runtime types can prove.
Quick check
1. What does an enum represent best?
2. Who interprets an attribute?
3. Does string guarantee a valid email?
Keep the workshop: later phases deliberately build on these boundaries, so today's small example can become part of your portfolio architecture.