get_class, instanceof and is_callable in detail
Not every question about an object's structure needs ReflectionClass. PHP ships a set of lightweight introspection functions, from get_class through instanceof to is_callable, that avoid the overhead of the Reflection API and are entirely sufficient for most everyday type checks.
Table of Contents
- 1. Why simple introspection exists alongside the Reflection API
- 2. get_class, get_parent_class and class_exists in detail
- 3. instanceof, is_a and is_subclass_of: type checking at runtime
- 4. get_object_vars and get_class_methods without Reflection objects
- 5. is_callable and method_exists: safely checking callability
- 6. class_implements and class_uses: listing interfaces and traits
- 7. Practical example: a lightweight type checker
- 8. The limits of simple introspection: when Reflection is required
- 9. Introspection functions compared to the Reflection API
- 10. Summary
- 11. FAQ
1. Why simple introspection exists alongside the Reflection API
Long before PHP had a complete object oriented Reflection API, a set of procedural functions for introspection already existed: get_class(), instanceof, method_exists() and similar tools have existed since PHP 4 in some cases and were never replaced by the Reflection API, because they solve a different problem. Where ReflectionClass offers a complete, internally consistent object model for arbitrarily complex analysis, the built in introspection functions each answer a single, frequently asked question directly, without the detour through an additional object.
The practical advantage of these functions lies in their simplicity: a check like $object instanceof SomeInterface is immediately understandable for readers, creates no temporary reflection objects, and is optimized directly by the PHP engine itself. For many everyday tasks, such as checking whether an object implements a certain interface or whether a method can be called, this kind of introspection is not just sufficient, it is the clearly better choice over the full Reflection API.
This article places the most important of these functions in context, shows what each of them is meant for, and marks the point where switching to the full Reflection API really becomes necessary, for example when parameter types, docblocks or inheritance hierarchies need to be evaluated programmatically.
2. get_class, get_parent_class and class_exists in detail
get_class() returns the fully qualified class name as a string for a given object instance, without needing to construct a reflection object for it. Since PHP 8.0, the function can also be called without an argument inside a method and then returns the name of the current class, complementing self::class as a constant, but returning the actual runtime class name of an instance, which matters under inheritance since self::class always returns the declaring class, whereas get_class($this) returns the actual runtime class.
get_parent_class() returns the direct parent class, or false if none exists, and class_exists() checks whether a class name is known at all before attempting to instantiate or reflect on it. Together these three functions already cover a large share of the cases where code merely needs to know which class and which parent class are present, without having to dig deeper into methods or properties.
<?php
declare(strict_types=1);
class BaseNotification
{
public function describe(): string
{
// get_class($this) returns the actual runtime class, not the declaring one
return sprintf('%s (parent: %s)', get_class($this), get_parent_class($this) ?: 'none');
}
}
final class OrderShippedNotification extends BaseNotification
{
}
$notification = new OrderShippedNotification();
echo $notification->describe() . PHP_EOL; // OrderShippedNotification (parent: BaseNotification)
// Guard before dynamic instantiation from a config value
$className = 'OrderShippedNotification';
if (class_exists($className)) {
$instance = new $className();
}
3. instanceof, is_a and is_subclass_of: type checking at runtime
The instanceof operator is the idiomatic, engine optimized way to check at runtime whether an object implements a certain class, inherits from it, or implements a certain interface. It only works, however, when the class name is available as a literal or an already loaded variable. If the target class name instead has to be evaluated as a string at runtime, for example from a configuration file, is_a() comes into play, serving the same purpose but accepting a string as its second argument.
is_subclass_of() differs from is_a() in one important detail: it returns false when the checked class exactly matches the given class, so it exclusively checks real subclasses, while is_a() also returns true when the class matches exactly. This difference matters for generic frameworks that need to distinguish between exact type matches and actual inheritance.
<?php
declare(strict_types=1);
interface Shippable
{
}
class Order implements Shippable
{
}
class ExpressOrder extends Order
{
}
$order = new ExpressOrder();
var_dump($order instanceof Shippable); // bool(true) — literal class name
var_dump(is_a($order, 'Order')); // bool(true) — string, includes exact match
var_dump(is_subclass_of($order, 'Order')); // bool(true) — true subclass
var_dump(is_subclass_of(new Order(), 'Order')); // bool(false) — exact match, no subclass
// Dynamic class name from configuration, resolved at runtime
$allowedBaseClass = 'Order';
if (is_a($order, $allowedBaseClass)) {
echo 'Order is processable' . PHP_EOL;
}
4. get_object_vars and get_class_methods without Reflection objects
get_object_vars() returns an associative array of all visible properties of an object instance, where visibility depends on the caller's context: called from outside the class, it returns only public properties; called from inside a method of the class itself, it additionally returns private and protected properties. This context dependent behavior differs fundamentally from ReflectionProperty, which works independently of the calling context.
get_class_methods() similarly returns the names of all public methods of a class as a simple array of strings, without any information about parameters or return types. For tasks that only need a list of names, such as generating simple API documentation or checking whether a certain method exists at all, these two functions are quicker to write and easier to read than the equivalent code using ReflectionClass::getProperties() and getMethods().
<?php
declare(strict_types=1);
final class InvoiceLine
{
public string $sku = 'SKU-1';
public int $quantity = 2;
private float $unitPrice = 19.99;
public function total(): float
{
return $this->quantity * $this->unitPrice;
}
private function auditTrail(): void
{
}
}
$line = new InvoiceLine();
// Called from outside the class: only public properties are visible
print_r(get_object_vars($line)); // ['sku' => 'SKU-1', 'quantity' => 2]
// Public methods only, no visibility or type information
print_r(get_class_methods($line)); // ['total']
5. is_callable and method_exists: safely checking callability
is_callable() checks whether a value, be it a string, an array in the form [$object, 'method'], or a closure, can actually be invoked as a function, before the real call happens through call_user_func() or direct syntax. This check is essential for generic dispatchers that accept callables from external sources like configuration files or routing tables and need to validate them before execution, rather than blindly risking a possibly non existent call.
method_exists() and property_exists() complement this check at a more granular level: they answer the simple question of whether a certain method or property is declared on a class or instance at all, regardless of visibility. For libraries that want to invoke optionally present hooks, for example an onBeforeSave() that only some classes implement, this combination is the most pragmatic path without resorting to the full Reflection API.
<?php
declare(strict_types=1);
final class HookDispatcher
{
public function dispatchOptionalHook(object $subject, string $hookName): void
{
// Only invoke the hook if it actually exists and is callable
if (method_exists($subject, $hookName) && is_callable([$subject, $hookName])) {
$subject->{$hookName}();
}
}
}
final class ProductImporter
{
public function onBeforeSave(): void
{
echo 'Running pre-save validation' . PHP_EOL;
}
}
$dispatcher = new HookDispatcher();
$dispatcher->dispatchOptionalHook(new ProductImporter(), 'onBeforeSave');
$dispatcher->dispatchOptionalHook(new ProductImporter(), 'onAfterSave'); // silently skipped
6. class_implements and class_uses: listing interfaces and traits
class_implements() returns an array of all interfaces that a class implements directly or through inheritance, indexed and equal in value to the respective interface name, which allows checking with isset() instead of a loop. class_parents() similarly returns the complete inheritance chain as an array, and class_uses() lists all traits that a class directly uses, without automatically resolving nested traits, meaning traits inside traits.
These functions are especially useful for generic registration mechanisms, for example a plugin system that needs to check whether a given class belongs to a certain category without having to know every possible class individually. An event dispatcher that only delivers to objects implementing a certain marker interface can get by with class_implements() and a simple isset() check, instead of constructing a ReflectionClass object for every check.
<?php
declare(strict_types=1);
interface Auditable
{
}
interface Cacheable
{
}
trait TimestampableTrait
{
public ?string $updatedAt = null;
}
final class ProductEntity implements Auditable, Cacheable
{
use TimestampableTrait;
}
$interfaces = class_implements(ProductEntity::class);
var_dump(isset($interfaces[Auditable::class])); // bool(true)
$traits = class_uses(ProductEntity::class);
var_dump(isset($traits[TimestampableTrait::class])); // bool(true)
7. Practical example: a lightweight type checker
The functions introduced so far can be combined into a compact, readable type checker that gets by without a single reflection object and still performs a series of structured checks on a class. Such a tool is well suited for validation layers that want to verify, before actual business logic runs, whether a given instance implements the expected interfaces and provides certain methods.
The advantage over a reflection based solution lies in clarity: every single check is a simple, clearly named function whose meaning is immediately obvious, without the reader needing to know which methods on ReflectionClass deliver which piece of information.
<?php
declare(strict_types=1);
final class LightweightTypeChecker
{
/** @param class-string $requiredInterface */
public function implementsAll(object $subject, array $requiredInterfaces): bool
{
$implemented = class_implements($subject);
foreach ($requiredInterfaces as $interface) {
if (!isset($implemented[$interface])) {
return false;
}
}
return true;
}
public function hasCallableMethod(object $subject, string $methodName): bool
{
return method_exists($subject, $methodName)
&& is_callable([$subject, $methodName]);
}
public function usesTrait(object $subject, string $traitName): bool
{
return isset(class_uses($subject)[$traitName]);
}
}
$checker = new LightweightTypeChecker();
$product = new ProductEntity();
var_dump($checker->implementsAll($product, [Auditable::class, Cacheable::class])); // true
var_dump($checker->usesTrait($product, TimestampableTrait::class)); // true
8. The limits of simple introspection: when Reflection is required
The functions introduced here reach their limit where detailed information about method signatures is needed: parameter types, default values, nullability or return types are not accessible through any of the simple introspection functions. As soon as an autowiring container needs to resolve a class's constructor parameters, there is no way around ReflectionMethod and ReflectionParameter, because this information is simply not part of the procedural API.
Simple introspection likewise hits its limit when attributes need to be read, private properties need to be set directly, or objects need to be created without running the constructor. In all these cases, the full Reflection API is the right tool, while the functions covered in this article play to their strengths precisely in the frequent, simple cases where the extra overhead of a reflection object would mean unnecessary baggage.
9. Introspection functions compared to the Reflection API
The following table maps the most important tasks to their respective tools and makes the decision easier for a concrete project.
| Task | Simple introspection | Reflection API needed? | Reason |
|---|---|---|---|
| Determining an instance's class name | get_class() |
No | A simple string return value is enough |
| Checking interface implementation | instanceof / class_implements() |
No | Direct boolean check is sufficient |
| Resolving constructor parameters for autowiring | Not possible | Yes | ReflectionParameter::getType() needed |
| Setting a private property from outside | Not possible | Yes | ReflectionProperty::setValue() needed |
| Checking whether a method is callable | is_callable() |
No | Built in function covers this case exactly |
The table shows a consistent pattern: as soon as the question can be answered with a simple boolean or string, one of the built in introspection functions is sufficient. As soon as structured information about types, signatures or attributes is needed, however, the full Reflection API is unavoidable.
Mironsoft
PHP performance reviews and lean architecture decisions
Finding unnecessary Reflection overhead in your code?
We review existing PHP codebases for superfluous ReflectionClass calls and replace them wherever built in introspection functions are entirely sufficient, for measurably faster code.
Performance audit
Identifying Reflection calls in hot code paths
Code simplification
Replacing superfluous Reflection objects with lean functions
Architecture consulting
Deciding together when Reflection is really necessary
10. Summary
Introspection beyond the Reflection API covers a large share of everyday questions about an object's structure with functions like get_class(), instanceof, is_a(), get_object_vars() and is_callable(), without creating the overhead of a reflection object. These functions have long been part of the language, are optimized directly by the engine, and produce clearly readable code for type checks, interface checks and callability tests.
The boundary to the full Reflection API runs where structured information about method signatures, attributes or private state is needed, for example in autowiring containers or serializers. Anyone who knows both toolsets and applies them deliberately writes code that stays lean in the common cases and only reaches for the full Reflection API where it is genuinely unavoidable.
Introspection Beyond the Reflection API — The Essentials at a Glance
Class and type
get_class(), instanceof, is_a() and is_subclass_of() for type checks without a Reflection object.
Structure
get_object_vars() and get_class_methods() return names without type or visibility detail.
Callability
is_callable() and method_exists() check safely before a dynamic call is made.
Boundary
Parameter types, attributes and private state require the full Reflection API.