nandan108 / prop-access
Accessor layer for PHP objects using reflection, extensible via custom resolvers
Requires
- php: ^8.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.75
- nunomaduro/collision: ^6.4
- phpunit/phpunit: ^10.0
- vimeo/psalm: ^6.10
README
A minimal and extensible property accessor library for PHP objects.
Provides getter and setter resolution via reflection, supporting both public properties and get*/set* methods.
Designed to be:
- โ Framework-agnostic
- ๐ Easily extensible to support more object types
๐ Installation
composer require nandan108/prop-access
๐ง Features
- ๐ง Default resolvers for public properties and
getProp()/setProp()methods - ๐งฉ Pluggable resolver priority (later-registered resolvers are called first)
- ๐งผ
CaseConverterutility for camelCase, snake_case, kebab-case, etc. - ๐งฐ Convenience methods:
getValueMap(),resolveValues(),canGetGetterMap()...
๐ Usage
Accessor maps are cached by class name, so the returned closures are stateless and require the target object to be passed as an argument:
use Nandan108\PropAccess\PropAccess; $getterMap = PropAccess::getGetterMap($myObj); $value = $getterMap['propertyName']($myObj); $setterMap = PropAccess::getSetterMap($myObj); $setterMap['propertyName']($myObj, $newValue);
To resolve only specific properties:
$getters = PropAccess::getGetterMap($myObj, ['foo_bar']);
๐งฐ Convenience Utilities
Quickly resolve values from a target object:
use Nandan108\PropAccess\PropAccess; $values = PropAccess::getValueMap($myDto); // โ ['prop1' => 'value1', 'prop2' => 42, ...]
You can also resolve values from a previously obtained getter map:
$getters = PropAccess::getGetterMap($entity, ['foo', 'bar']); $values = PropAccess::resolveValues($getters, $entity);
Check if accessors are supported for a given target:
if (PropAccess::canGetGetterMap($target)) { // Safe to call getGetterMap() }
These methods are especially useful when working with dynamic sources, fallbacks, or introspection-based tools.
๐ง Resolution Behavior
You can call getGetterMap() / getSetterMap() in two ways:
-
Without property list: Returns a full canonical map using camelCase keys. If both a public property (e.g.
my_prop) and a corresponding getter (getMyProp()) exist, only the getter will be included to avoid duplication and ensure value transformation logic is preserved.$map = PropAccess::getGetterMap($entity); $map['myProp']($entity); // uses getMyProp(), not $entity->my_prop
-
With a property list: Allows access to both public properties and getter/setter methods via multiple aliases:
foo_barโ accesses the public property (if available)fooBarโ accesses the getter/setter method (if available)
[$directSetter, $indirectSetter] = PropAccess::getSetterMap($myObj, ['foo_bar', 'fooBar']); $directSetter($myObj, 'A'); // -> $myObj->foo_bar = 'A'; $indirectSetter($myObj, 'B'); // -> $myObj->setFooBar('B');
๐ Custom Accessor Resolvers
Resolvers can be registered to override or extend behavior:
PropAccess::bootDefaultResolvers(); // Registers built-in property/method resolvers // Deprecated: use registerResolvers([...]) instead. PropAccess::registerGetterResolver(new MyCustomGetterResolver()); PropAccess::registerSetterResolver(new MyCustomSetterResolver()); // Register many at once: PropAccess::registerResolvers([ new MyCustomGetterResolver(), new MyCustomSetterResolver(), ]); // Remove resolvers later (by class name or instance), as an array: PropAccess::unregisterResolvers([ MyCustomGetterResolver::class, MyCustomSetterResolver::class, ]);
Later-registered resolvers are tried first. If ->supports($object) returns false, fallback continues down the chain. Registering the same resolver class multiple times is ignored. registerGetterResolver() and registerSetterResolver() remain available for backward compatibility, but are deprecated.
๐งฌ CaseConverter Utility
CaseConverter::toCamel('user_name'); // "userName" CaseConverter::toPascal('user_name'); // "UserName" CaseConverter::toSnake('UserName'); // "user_name" CaseConverter::toKebab('UserName'); // "user-name" CaseConverter::toUpperSnake('UserName'); // "USER_NAME"
You can also use the generic method:
CaseConverter::to('camel', 'foo_bar'); // Equivalent to toCamel()
๐ AccessorProxy Helper
Need array-style access to object properties? AccessorProxy wraps an object and exposes property access via ArrayAccess, Traversable, and Countable.
use Nandan108\PropAccess\AccessorProxy; $proxy = AccessorProxy::getFor($user); // read-only by default echo $proxy['firstName']; // -> $user->getFirstName() $proxy['lastName'] = 'Smith'; // throws PropAccessConfigException (read-only) $rwProxy = AccessorProxy::GetFor($user, readOnly: false); $rwProxy['lastName'] = 'Smith'; // works if setLastName() or $lastName is available
Includes convenience methods:
$proxy->toArray(); // ['firstName' => 'John', ...] $proxy->readableKeys(); // ['firstName', 'lastName', ...] $proxy->writeableKeys();
Use AccessorProxy::getFor() to fail gracefully:
$proxy = AccessorProxy::getFor($target); if (!$proxy) { // target does not support accessors }
๐ See docs/AccessorProxy.md for full reference.
โ ๏ธ Exceptions & Message IDs
Exceptions are structured for i18n-ready messages and configuration errors:
AccessorExceptionโ runtime access errors (missing resolver/getter/setter).PropAccessConfigExceptionโ boot/config misuse (e.g. read-only writes).
AccessorException uses message IDs as templates plus parameters for translation:
use Nandan108\PropAccess\Exception\DefaultMessageMap; $id = 'prop_access.getter_not_found'; $english = DefaultMessageMap::MESSAGES[$id];
โ Quality
- โ 100% test coverage
- โ Psalm level 1
- โ Zero dependencies
MIT License ยฉ nandan108