Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Object Reflector Laravel Package

sebastian/object-reflector

sebastian/object-reflector lets you reflect an object's properties, including inherited, private, and protected ones. Install it via Composer to inspect and access object state that PHP's default reflection utilities don't easily expose.

View on GitHub
Deep Wiki
Context7

Getting Started

Install as a dev-only dependency — this belongs nowhere near a production code path:

composer require --dev sebastian/object-reflector

There is exactly one class (SebastianBergmann\ObjectReflector\ObjectReflector) and one method (getProperties(object $object): array). The whole package is roughly forty lines, so the README is enough; if you want to know precisely what it does, read the source instead of the docs.

use SebastianBergmann\ObjectReflector\ObjectReflector;

$props = (new ObjectReflector())->getProperties($order);

// ['id' => 42, 'App\Domain\Order::status' => 'confirmed', ...]

Your first use case: an assertion on state you deliberately keep private. A domain service, a value object, or a third-party class where adding a getter just for a test would be worse than the test itself:

public function test_order_is_confirmed_after_checkout(): void
{
    $order = $this->checkoutService->place($this->cart);

    $state = (new ObjectReflector())->getProperties($order);

    $this->assertSame(OrderStatus::Confirmed, $state['status']);
}

Before reaching for it, be honest about what it buys you. Native ReflectionClass::getProperties() already returns private and protected properties from the whole inheritance chain. The package's real value is the convenience: a flat name => value map in one call, with declaring-class qualification for inherited properties. If you only ever need one property, (fn () => $this->foo)->call($obj) is a one-liner with no dependency at all. And if you just want to look at private state, dd() / dump() already show it — Symfony VarDumper renders private and protected properties fine. This package is for programmatic access.

Composer resolves the right major for your runtime (v5 targets PHP 8.3, v6 requires 8.4), so on Laravel 11/12 you'll typically land on v4/v5. Don't hardcode a major in composer.json if a PHP upgrade is on the roadmap.

Implementation Patterns

Centralise it in a test helper. The class is final, so wrap it rather than sprinkling new ObjectReflector() across a hundred tests. You get one place to handle inheritance keys, missing properties, and uninitialised values:

// tests/Support/InspectsPrivateState.php
trait InspectsPrivateState
{
    protected function privateProperty(object $object, string $property): mixed
    {
        $all = (new ObjectReflector())->getProperties($object);

        foreach ($all as $key => $value) {
            if ($key === $property || str_ends_with($key, '::' . $property)) {
                return $value;
            }
        }

        throw new InvalidArgumentException(
            sprintf('%s has no property [%s]', $object::class, $property)
        );
    }
}

Assert queue/job payload integrity. Jobs hold serialised state you rarely expose:

$job = new SyncInventory($sku, $qty);
$props = (new ObjectReflector())->getProperties($job);

$this->assertSame('SKU-1', $props['sku']);

Snapshot for regression tests. When a bug is fixed, lock the internal shape in place:

$this->assertSame(
    ['items' => 3, 'total' => 5999, 'App\Domain\Cart::discount' => null],
    (new ObjectReflector())->getProperties($cart)
);

Careful with this one — it breaks on every refactor. Use it for one or two critical objects, not as a default pattern.

Debug helper for the whole suite. Register once in tests/Pest.php or a base TestCase:

function dump_private(object $object): void
{
    dump((new ObjectReflector())->getProperties($object));
}

Inspect legacy/vendor objects. The strongest legitimate case: a third-party or pre-refactor class where the internal state matters and the alternative is forking the dependency.

Pest expectations. Wrap it as a custom expectation so it reads naturally:

expect($order)->toHavePrivateProperty('status', OrderStatus::Confirmed);

Isolate with an arch test. If you also use Pest's or PHPStan's architecture rules, add a rule that sebastian/object-reflector may only be referenced from tests/ and your helper trait. This is the only reliable guard against it leaking into src/.

Gotchas and Tips

  • Inherited properties get prefixed keys. Anything declared on a parent comes back as App\Models\Model::id, not id. Naive $props['id'] lookups silently fail on any subclassed object. Always match with the str_ends_with($key, '::' . $name) fallback shown above.
  • Static properties are excluded. If you need them, use ReflectionClass directly — the package skips $property->isStatic().
  • Uninitialised typed properties can explode. ReflectionProperty::getValue() throws Error: Typed property ... must not be accessed before initialization. In Laravel this bites on lazily-populated models and DTOs with nullable-promoted properties. Guard inside your helper with $property->isInitialized($object) if you hit it — you cannot fix it from the caller.
  • The output is shallow. Values are returned as-is; nothing recurses. That's good (no infinite loops on circular Eloquent relations) but means a property holding a model prints as App\Models\User object, not its attributes. Recurse deliberately, and depth-limit it.
  • Sensitive data leaks into logs. An Eloquent model's $attributes include hashed passwords, tokens, and API keys. Never pipe raw output into CI logs or an exception message. Sanitise with a key denylist before dumping.
  • Closures and resources break serialisation. json_encode() returns false on closures; print_r() on a resource is useless. Check is_scalar() before stringifying, and consider get_debug_type() for the rest.
  • Read-only. There is no setter. If a test genuinely needs to mutate private state, use ReflectionProperty::setValue() yourself and accept that you're writing a smell, not a test.
  • It is final. No subclassing, no extension points. Your wrapper trait is the extension point.
  • Enforce dev-only mechanically. require-dev alone won't stop someone importing it in a service. Add the arch/PHPStan rule in the same PR that introduces the dependency — retrofitting it later never happens.
  • Watch version drift on PHP upgrades. Each major raises the floor (v5 → 8.3, v6 → 8.4). A Laravel 12 app on PHP 8.2 pins you to v4. If composer update suddenly wants a major bump, that's a PHP requirement signal, not a bug.
  • Ask whether the reflection is the actual problem. Reaching for this in a greenfield class is usually a sign the class has no testable seam — a public query method or a value object would be cheaper than a reflection dependency. It earns its keep on legacy and vendor code, not on code you own.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator