Introspection Beyond the Reflection API: get_class, instanceof and is_callable in Detail
AI generated
<?php
8.4
PHP · Introspection · Metaprogramming
Introspection Beyond the Reflection API
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.

16 min read get_class · instanceof · is_callable PHP 8.x · 8.4

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.

11. FAQ: Introspection Beyond the Reflection API

1get_class() vs. ReflectionClass::getName()?
Functionally the same, get_class() is faster since no reflection object needs constructing.
2instanceof or is_a()?
instanceof for a known literal class name, is_a() for a class name as a string at runtime.
3is_a() vs. is_subclass_of()?
is_a() returns true on an exact match too, is_subclass_of() only for real subclasses.
4Private properties visible?
Only inside a method of the same class. From outside, only public properties.
5Safely checking callables?
is_callable() generally, method_exists() plus is_callable([$obj, 'method']) for specific instances.
6Inherited interfaces visible?
Yes, class_implements() returns all interfaces across the entire inheritance chain.
7Traits inside traits?
No, nested traits must be queried separately on the respective trait.
8Faster than Reflection?
Yes, noticeably, since no reflection object is constructed, relevant in hot code paths.
9When not enough anymore?
With parameter types, attributes or private state outside your own context, then use the full Reflection API.
10Does is_callable() replace try/catch?
No, it only checks callability. Exceptions during execution still need try/catch.