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

Property Access Laravel Package

symfony/property-access

Symfony PropertyAccess reads and writes object and array values using simple string notation such as "user.address.street". It handles getters, setters, public properties, ArrayAccess, and magic methods, making nested data access generic and consistent across forms, serializers, and valida...

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and pin deliberately. There is no Laravel auto-discovery here — it is a framework-agnostic Symfony component (no service provider, facade, config file, or publishable asset). Because Symfony 8 requires PHP ≥ 8.4, constrain to the branch that matches your runtime instead of a loose OR-range:

# PHP 8.2/8.3 on Laravel 10/11
composer require symfony/property-access:^7.4

# PHP 8.4+ only
composer require symfony/property-access:^8.0

Also check the lockfile delta — the component sits inside the Symfony component graph and can pull siblings you do not otherwise have.

2. Bind one shared accessor. PropertyAccessor caches per-class read/write metadata internally; constructing one per call throws that cache away. Register a singleton in AppServiceProvider and hide it behind your own contract:

// app/Support/Property/PropertyReader.php
interface PropertyReader
{
    public function read(object|array $target, string $path, mixed $default = null): mixed;
    public function canRead(object|array $target, string $path): bool;
}
// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(PropertyAccessor::class, fn () => PropertyAccess::createPropertyAccessor());
    $this->app->bind(PropertyReader::class, SymfonyPropertyReader::class);
}

Keeping PropertyAccessor out of controller/job signatures means you can swap the implementation in a single file later.

3. First use case: a third-party payload you do not control. This is where native helpers stop short. data_get() traverses arrays and ArrayAccess; an SDK response object with getters is neither. That is the whole pitch:

$city = $reader->read($webhook->payload['customer'], 'address.city', 'unknown');

Where to look first: the docs page (symfony.com/doc/current/components/property_access.html), the PropertyAccessor and PropertyPath classes, and the AccessException hierarchy. File issues against symfony/symfony, not the split repo — the package repo is a read-only mirror.

Implementation Patterns

Reading — notation cheat sheet. One consistent API over objects, arrays, ArrayAccess, getters, and magic accessors:

Path Resolves to
user.address.city getUser()->getAddress()->getCity(), or public/array equivalents
items[0].sku first element of a collection, then sku
order.lines[2].qty positional access into a nested list
user.getName() explicit method call (needs MAGIC_CALL for __call)

Adapter that turns misses into null. This is the single most important piece of glue. NoSuchPropertyException / NoSuchIndexException / UnexpectedTypeException are the normal outcome for sparse payloads — do not let them become 500s:

final class SymfonyPropertyReader implements PropertyReader
{
    public function __construct(private PropertyAccessor $accessor) {}

    public function read(object|array $target, string $path, mixed $default = null): mixed
    {
        if (!$this->accessor->isReadable($target, $path)) {
            return $default;
        }

        try {
            return $this->accessor->getValue($target, $path) ?? $default;
        } catch (AccessException) {
            return $default;
        }
    }

    public function canRead(object|array $target, string $path): bool
    {
        return $this->accessor->isReadable($target, $path);
    }
}

Request → DTO hydration. Map validated input onto a typed DTO without a hand-written getter chain per field. Drive it from a declarative map so the convention lives in one file:

final class WebhookCustomerData
{
    private const MAP = [
        'email'     => 'contact.email',
        'full_name' => 'contact.name',
        'city'      => 'address.city',
    ];

    public static function fromPayload(object|array $payload, PropertyReader $reader): self
    {
        $dto = new self();
        foreach (self::MAP as $property => $path) {
            $reader->write($dto, $property, $reader->read($payload, $path));
        }
        return $dto;
    }
}

PATCH semantics — write only what was sent. This is where a setValue wrapper earns its keep: partial updates become a loop over present keys instead of a wall of if (isset(...)) / $model->setX() blocks:

foreach ($payload as $field => $value) {
    if ($writer->canWrite($dto, $field)) {
        $writer->write($dto, $field, $value);
    }
}

Pair it with a strict allow-list of writable fields — never let the key itself come straight from the request.

Config / feature-flag path resolution. For flag payloads that arrive as nested arrays of varying depth, read($config, 'features.billing.invoices.enabled', false) replaces a chain of ?? operators and stays correct when intermediate nodes are missing.

Reuse PropertyPath on hot paths. If the same path string is resolved thousands of times (a column definition list, a report mapper), parse once and keep it:

$path = new PropertyPath('order.lines[0].qty');   // parse once, reuse
$value = $accessor->getValue($order, $path);

Test fixtures and assertions. isReadable() / isWritable() are cheap, readable assertions for "this DTO exposes the contract the API promises" and catch the resolution-order surprises described below at CI time rather than at runtime.

Free integration via Symfony Serializer. ObjectNormalizer is built on this component. If you already use symfony/serializer for import/export, you get the same path semantics for free — use one convention across both rather than inventing a second.

Gotchas and Tips

Missing object properties throw; missing array indexes usually do not. getValue($dto, 'nickname') on a class without that property raises NoSuchPropertyException, while a missing array key returns null by default (invalid-index exceptions are opt-in via the constructor or enableExceptionOnInvalidIndex()). That asymmetry is the number-one source of "works on arrays, explodes on DTOs" bugs. Always guard with isReadable() or wrap in try/catch (AccessException).

isReadable() returns false for null intermediates. isReadable($order, 'customer.address.city') is false when customer is null — not because the path is invalid, but because there is nothing to traverse. Useful as a guard, misleading as a schema check. Validate paths against your declared map, not against a live instance.

Resolution order is still being patched — pin exact versions. Recent cross-branch fixes landed simultaneously on 6.4 / 7.x / 8.x for: public properties shadowed by same-named methods requiring arguments, getter-shaped methods returning void/never, PHP 8.4 asymmetric visibility, and caching of null read info. The answer to "what does getValue($o, 'foo') call?" can change in an x.y.Z bump. Treat resolution order as an assertion in tests, not as documented behaviour, and let Renovate propose bumps as reviewable PRs.

Never accept a user-supplied path. This component will traverse any graph you hand it and will invoke setters, __get, and (if enabled) __call. A path that originates from saved filters, dynamic config, or admin rules is a user-controlled object-graph read/write primitive: password_hash, remember_token, cross-tenant relation traversal, and mass assignment in one. Enforce an allow-list in the adapter before the string reaches PropertyPath.

Do not layer this on Eloquent models. Eloquent is already ArrayAccess with accessors/mutators; adding PropertyAccess creates a second, competing access semantic for $model->foo and makes debugging ambiguous. Use it for plain DTOs, value objects, and third-party SDK models.

Magic methods are opt-in. __get/__set work by default, but __call requires PropertyAccessor::MAGIC_CALL explicitly. If a path silently fails on an SDK model, that flag is the usual suspect. The flip side: enabling MAGIC_CALL widens your traversal surface — keep it off unless a specific integration needs it.

Writing to unwritable properties fails loudly. Read-only properties and setters with incompatible types produce NoSuchPropertyException / TypeError. Check isWritable() first, and expect PHP's own type coercion rules to apply to typed properties rather than any friendly conversion.

Watch intermediate array writes. Writing setValue($array, 'foo.bar', 'x') where foo does not exist is not the same as creating it — behaviour is easy to get wrong. Write leaf-first or pre-seed the nested structure, and cover it with a test rather than assuming.

One accessor per app, not per call. Metadata caches are keyed by class name. Reuse the singleton, but be aware that heavy use of anonymous classes or mocks (e.g. in a large test suite) inflates that cache.

Debugging recipe. When a path returns nothing useful, log the class, the path, and isReadable() together, then dump (new PropertyPath($path))->getElements() to confirm the path actually parsed the way you intended — a dot inside a key will silently split into two segments (verify escaping behaviour against your installed version).

What to keep in mind when it disappoints. Symfony's LTS branches (6.4, 7.4) get fixes; the standard track moves fast. If resolution semantics become a liability, the adapter interface means replacing the engine is a one-file change — which is exactly why the Symfony types should never appear in controllers, jobs, or Blade.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core