Contents
- 1 PHP Reflection API explained
- 1.1 What Reflection really does (and why it feels magical)
- 1.2 Core classes: Your Reflection toolkit
- 1.3 Hands-on: Testing privates like a pro
- 1.4 Dynamic method calls and property hacks
- 1.5 Advanced moves: Dependency injection and beyond
- 1.6 Real-world heroes: Docs, proxies, debugging
- 1.7 Gotchas and when to pause
- 1.8 Wrapping thoughts: The quiet power shift
PHP Reflection API explained
Hey, fellow PHP devs. Picture this: it's 2 AM, your screen's the only light in the room, coffee's gone cold, and you're staring at a class with private properties mocking you during a unit test. You've rewritten the damn thing three times just to peek inside. Sound familiar? That's where PHP Reflection API swoops in like that quiet hero you didn't know you needed. It's not flashy, but it lets you inspect and tweak code at runtime—classes, methods, properties, all of it. Suddenly, testing private logic feels less like breaking into a safe and more like flipping a switch.
I've leaned on Reflection more times than I can count, especially in messy legacy projects or when building tools that need to "understand" other classes without me hardcoding every detail. It's part of PHP's core since 5.0, no extensions required, and it's powered frameworks like Laravel and Symfony under the hood. Today, let's unpack it—not as dry docs, but as the tool that saves your sanity. We'll start simple, build to real-world wins, and I'll share code that actually works.
What Reflection really does (and why it feels magical)
At heart, Reflection is PHP's way of letting your code introspect itself. Pass a class name or object, and it hands back a mirror: every method, property, interface, even docblocks. Want to call a private method? Done. Inject dependencies automatically? Easy. It's runtime reverse-engineering, bridging static code with dynamic behavior.
Think of it like this: your app's a black box. Reflection cracks it open without hammers. Common wins?
- Unit testing private/protected stuff without ugly workarounds.
- Dependency injection containers that auto-wire classes.
- Debugging beasts like unfamiliar codebases or ORMs.
- Dynamic tools—auto-docs, proxies, factories.
I've used it to mock a service in a test suite once, staring at a 10k-line monolith. Ten minutes later, boom—tests green. No refactoring needed.
Core classes: Your Reflection toolkit
PHP's Reflection lives in a family of classes. Here's the starters pack:
- ReflectionClass: Mirror of a class or interface. Get parents, methods, properties.
- ReflectionObject: Like ReflectionClass, but for live objects. Key for tweaking instances.
- ReflectionMethod: Dive into one method—params, visibility, invoke it.
- ReflectionProperty: Properties on steroids. Read/write privates after
setAccessible(true). - ReflectionFunction: For standalone functions.
Instantiate like: $reflector = new ReflectionClass('MyClass');. Boom, you're in.
Let's see it breathe with a basic example. Say you've got this:
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
private function greet() {
return "Hey, {$this->name}!";
}
}
Peek inside without touching the class:
$user = new User('Alex', 'alex@php.com');
$reflection = new ReflectionObject($user);
$property = $reflection->getProperty('name');
$property->setAccessible(true);
echo $property->getValue($user); // "Alex"
$method = $reflection->getMethod('greet');
$method->setAccessible(true);
echo $method->invoke($user); // "Hey, Alex!"
Feels like cheating, right? But it's pure power. That setAccessible(true) flips private/protected to public at runtime. Clean it up with setAccessible(false) after if you're paranoid.
Hands-on: Testing privates like a pro
Unit testing's where Reflection shines brightest. Ever needed to test a protected validator without exposing it? Here's the ritual from those late nights:
- Instantiate your class.
- Grab ReflectionObject.
- Fetch the property/method.
setAccessible(true).- Read or invoke.
- Assert away.
Full test snippet:
class ValidatorTest extends PHPUnit\Framework\TestCase {
public function testPrivateValidation() {
$validator = new UserValidator();
$reflection = new ReflectionObject($validator);
$method = $reflection->getMethod('isEmailValid');
$method->setAccessible(true);
$result = $method->invoke($validator, 'bad@example');
$this->assertFalse($result);
}
}
No more "make it public for tests" debates. This saved a team I worked with weeks on a legacy app—tests passed, code untouched.
Questions for you: How often do you hack tests with setters? Ever left privates untested because it felt impossible? Reflection flips that script.
Dynamic method calls and property hacks
Need to fire a method by name? ReflectionMethod's invoke() handles args dynamically. Perfect for event systems or factories.
class EventHandler {
public function onUserLogin($user) { /* ... */ }
public function onUserLogout($user) { /* ... */ }
}
$handler = new EventHandler();
$reflection = new ReflectionClass($handler);
$method = $reflection->getMethod('onUserLogin');
$method->invoke($handler, $user);
Or batch-invoke listeners:
foreach ($listeners as $listener) {
$reflector = new ReflectionClass($listener);
$methods = $reflector->getMethods();
foreach ($methods as $method) {
if ('on' . $event === $method->getName()) {
$method->invoke($listener, $args);
}
}
}
Properties? Same deal. Set a private ID post-DB insert:
$userReflection = new ReflectionObject($user);
$idProp = $userReflection->getProperty('id');
$idProp->setAccessible(true);
$idProp->setValue($user, $db->insert_id);
I've done this in ORMs—feels elegant, not hacky.
Advanced moves: Dependency injection and beyond
Now we level up. Reflection's the backbone of DI containers. Scan a constructor, resolve params, instantiate. No YAML hell.
Check this mini-container:
class SimpleDI {
public function make($className) {
$reflector = new ReflectionClass($className);
if (!$reflector->isInstantiable()) {
throw new Exception("Can't instantiate $className");
}
$constructor = $reflector->getConstructor();
if (!$constructor) {
return $reflector->newInstance();
}
$params = $constructor->getParameters();
$dependencies = [];
foreach ($params as $param) {
$paramClass = $param->getClass();
if ($paramClass) {
$dependencies[] = $this->make($paramClass->getName());
} elseif ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
throw new Exception("Can't resolve {$param->getName()}");
}
}
return $reflector->newInstanceArgs($dependencies);
}
}
Feed it UserService expecting a Repository:
class UserService {
private $repo;
public function __construct(Repository $repo) {
$this->repo = $repo;
}
}
$service = $di->make(UserService::class); // Auto-wires Repository!
Symfony's container does this at scale. Laravel too. I've built similar for micro-apps—cut config by 80%.
Real-world heroes: Docs, proxies, debugging
- Auto-docs: Loop classes, grab
getDocComment(), params, types. Powers API gens like CakePHP's.
foreach ($reflector->getMethods() as $method) {
echo $method->getDocComment() . "\n";
foreach ($method->getParameters() as $param) {
echo "- {$param->getName()}: {$param->getType()}\n";
}
}
- Dynamic proxies: Wrap calls for logging/timing.
$proxyMethod = $reflection->getMethod('process');
$start = microtime(true);
$proxyMethod->invoke($obj, $args);
echo "Took: " . (microtime(true) - $start) . "s\n";
- Debug unfamiliar code:
getProperties(),getInterfaces(), parent classes. Land in a new repo? List everything fast.
One project: debugging a third-party ORM. Reflection revealed hidden deps in 5 mins. What a rush.
Gotchas and when to pause
It's powerful, but not a toy. Performance hit on big scans—cache reflectors. setAccessible bypasses encapsulation; use sparingly outside tests. PHP 8+ typed properties complicate reads sometimes.
Pro tip: Closures from methods (PHP 7.4+): $closure = $method->getClosure($object); $closure($args);. Fire-and-forget methods.
Have you pushed Reflection too far? I once proxied everything—slow as molasses. Lesson: measure.
Wrapping thoughts: The quiet power shift
Reflection isn't about showing off. It's the tool that lets code speak for itself, turning "impossible" into "done." Next time you're wrestling a test or wiring deps, reach for it. That late-night glow on your monitor? It'll feel a little warmer, the wins a bit sweeter. Keep building, friends—your code's got stories to tell.