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

Direct Property Access Laravel Package

rekalogika/direct-property-access

Direct Property Access lets you read and write private and protected object properties in PHP without getters or setters, using reflection under the hood. Useful for tests, fixtures, and legacy code, with a clean, type-safe API and optional property-path support.

View on GitHub
Deep Wiki
Context7

Getting Started

rekalogika/direct-property-access gives you a PropertyAccessorInterface implementation (Symfony's contract) that reads and writes actual object properties via reflection instead of routing through getters, setters, __get, or __set. For a Laravel developer this is the tool you reach for when your object graph is deliberately "dumb" — DTOs, value objects, import rows, API payloads — and the standard accessor keeps calling accessors you never wanted to exist.

composer require rekalogika/direct-property-access

There is no service provider, no config file, and no facade. It is a plain library, so the first thing to do in a Laravel app is wire it into the container so you're not doing new in controllers:

// app/Providers/AppServiceProvider.php
use Rekalogika\DirectPropertyAccess\DirectPropertyAccessor;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

public function register(): void
{
    $this->app->singleton(PropertyAccessorInterface::class, DirectPropertyAccessor::class);
}

Where to look first: the package's test suite is the real documentation — it shows exactly which property shapes (private, promoted, inherited, readonly, uninitialized) are handled, which is more useful than the README for edge cases.

First use case: hydrating a request DTO from validated input without giving the DTO public setters.

final class CreateOrderData
{
    private string $reference;
    private int $quantity;
    private ?string $note = null;
}

$data = $accessor->newInstance(CreateOrderData::class); // or new CreateOrderData() with a factory
$accessor->setValue($data, 'reference', $request->validated('reference'));

If you only take one thing from this section: the package is a plumbing dependency. It should appear in your service layer and test factories, not scattered through controllers.

Implementation Patterns

Swap it in behind the Symfony contract. Because it implements PropertyAccessorInterface, it is a drop-in wherever Laravel-adjacent code already type-hints that interface — form mappers, CSV importers, data_get-style helpers you've written yourself. Bind it in the container and existing constructors keep working; only the binding line changes. This is the lowest-risk adoption path and the one to start with.

Map external payloads onto typed DTOs. Webhook handlers and queue jobs frequently receive associative arrays with keys that don't match property names. Combine the accessor with an explicit key map so the mapping is visible and testable:

final class WebhookMapper
{
    public function __construct(
        private PropertyAccessorInterface $accessor,
        private array $map = [
            'event_type' => 'type',
            'created_at' => 'occurredAt',
        ],
    ) {}

    public function map(array $payload, string $class): object
    {
        $dto = new $class();

        foreach ($this->map as $source => $target) {
            if (array_key_exists($source, $payload)) {
                $this->accessor->setValue($dto, $target, $payload[$source]);
            }
        }

        return $dto;
    }
}

Test factories for objects with private state. The most immediately valuable use in a Laravel codebase: building fixtures for value objects that intentionally expose no mutators. Instead of reflection boilerplate copy-pasted into a dozen test files, inject the accessor and set only the fields the scenario cares about. Keep this in a base test case or a trait.

Serialize without ceremony. When you want raw typed property values rather than whatever a getter computes, iterate get_object_vars() after binding the accessor to the object scope — or just read each property through the accessor. Useful for snapshot/audit logging where you want the stored state, not the derived presentation state.

Keep it out of Eloquent. Wrap it in a small DtoHydrator / PayloadMapper service and type-hint that in your controllers, jobs, and listeners. That gives you one place to swap the implementation, one place to add validation hooks, and one place to explain to the next developer why reflection is happening in a Laravel app.

Compose with validation at the boundary. The pattern that ages well: validate the array with a FormRequest, then hydrate the DTO — never hydrate first and validate the object. The accessor will happily write a negative quantity; your FormRequest is the only thing standing between you and an invalid object.

Gotchas and Tips

Invariants in setters simply do not run. This is the entire point of the package and also its main hazard. If a constructor or setter enforces quantity > 0, the accessor skips it. Treat any class hydrated this way as untrusted until validated, and prefer to keep validated DTOs immutable after construction.

Uninitialized typed properties throw on read. A private int $id; with no default and no constructor assignment will raise an Error ("must not be accessed before initialization") rather than return null. If your DTOs have optional fields, give them explicit = null defaults, or check initialisation before reading. This is the single most common source of "works in one DTO, breaks in the next."

Readonly properties have scope rules. On PHP 8.4+ writing readonly properties outside the declaring scope is more permissive with reflection, but on 8.1–8.3 you cannot initialise a readonly property from outside the declaring class. Design readonly DTOs with constructor promotion plus a named-constructor/factory, and use the accessor only for the non-readonly cases.

__get/__set are never triggered. If the target has magic accessors — Eloquent models, Collection, most Laravel facades — the accessor bypasses them entirely. Eloquent attributes live in the $attributes array, not as real properties, so hydrating a model this way writes properties that the ORM ignores. Use forceFill() / fill() for models; use this package for plain objects.

Inherited private properties need the declaring class. Setting a private property declared on a parent class from a child-scope attempt will fail. The accessor resolves the declaring class for you in the common cases, but if you're doing manual ReflectionProperty work alongside it, be explicit about which class you reflect on.

Property hooks (PHP 8.4) interact with this by design. If a class uses set/get hooks, decide deliberately whether you want the hook to run. The whole reason to choose this package over Symfony's default accessor is to skip them — but that means a hook doing coercion or validation is also skipped.

Symfony version constraints. 1.4.0 widened the constraint to permit Symfony 8 components. A standard Laravel install pins Symfony 7.x, so Composer resolves to 7 and nothing changes for you. Only worth noting if you have other packages forcing ^8.0 — in that case 1.4.0 is the version that stops the conflict. No API changes, no deprecations, no config changes: 1.3.1 → 1.4.0 is a safe patch-level upgrade.

Debugging reflection failures is opaque. "Could not set property" errors usually mean a typo, an uninitialised parent, or a static property. Dump property_exists() and a ReflectionClass::getProperties() list before assuming an accessor bug — nine times out of ten the property isn't what you think it is.

Performance is fine but not free. Reflection is cached by the runtime per class, so repeated mapping in a loop (a 50k-row import) is acceptable; per-request one-off hydration is unnoticeable. If you're mapping millions of rows in a job, benchmark against hand-written newInstanceWithoutConstructor() plus direct assignment before optimising.

Extension point: if you need per-call behaviour (skip nulls, coerce scalars, log every write), wrap the accessor in your own decorator implementing PropertyAccessorInterface and bind that. Avoid subclassing — the package is small and the interface is the stable seam.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager