Skip to content

Instantly share code, notes, and snippets.

@vertexvaar
Last active March 21, 2026 18:48
Show Gist options
  • Select an option

  • Save vertexvaar/c19fb842092a6b3e38f5db8cbe367f32 to your computer and use it in GitHub Desktop.

Select an option

Save vertexvaar/c19fb842092a6b3e38f5db8cbe367f32 to your computer and use it in GitHub Desktop.
Delegated Referenced Hooked Properties
<?php
enum Argument
{
case NONE;
}
trait UsesPropertyAccess
{
/** @var mixed|PropertyAccess */
public mixed $propertyAccess {
// Gets the value by delegating the access to PropertyAccess (see PropertyAccess->$value::get)
get => $this->propertyAccess->value;
set {
// The variable is not set when the ($this) object was created
// The first call should always be
if (!isset($this->propertyAccess)) {
if (!$value instanceof PropertyAccess) {
throw new LogicException(
'Initial accessor property value must be an instance of DelegatePropertyAccess',
1774117885,
);
}
$this->propertyAccess = $value;
return;
}
// Just a safeguard. It does not really break things, but
// nesting PropertyAccess is probably a fucking stupid idea.
if ($value instanceof PropertyAccess) {
throw new RuntimeException(
'$propertyAccess is already set',
1774118760,
);
}
// Sets the value by delegating the access to PropertyAccess (see PropertyAccess->$value::set)
$this->propertyAccess->value = $value;
}
}
}
trait ProvidesAccessor
{
/**
* Decides if the property can be passed by reference, and if not, builds a callback which is bound to the object
* so that the property access is delegated.
*
* @param string $property The name of the property
* @return mixed|Closure Direct reference to the property or a callback which delegates the property access
* @throws ReflectionException
*/
public function &accessorFor(string $property): mixed
{
$reflectionProperty = new ReflectionProperty($this, $property);
if ($reflectionProperty->hasHooks()) {
// Delegate property access to closure bound to this object
$accessor = fn(mixed $value = Argument::NONE) => $value === Argument::NONE
? $this->{$property}
: $this->{$property} = $value;
// Reference return requires the closure to live in a variable
return $accessor;
}
// No accessor needed, property can be passed as reference
return $this->{$property};
}
}
class PropertyAccess
{
/**
* If accessor is a closure, it is a delegated property access from ProvidesAccessor, and we need to call it to get
* or set the new value in the object.
*
* Otherwise, it is a direct reference to the object's property and read- and writable.
*
* @var mixed|Closure
*/
public mixed $value {
get => $this->accessor instanceof Closure ? ($this->accessor)() : $this->accessor;
set => $this->accessor instanceof Closure ? ($this->accessor)($value) : $this->accessor = $value;
}
/**
* @param mixed|Closure $accessor Closure if delegated (for properties with hooks) or reference to the property
*/
public function __construct(protected mixed &$accessor) {}
}
class Fruit
{
use ProvidesAccessor;
/**
* @var string Directly accessible, can be passed by reference
*/
public string $name;
/**
* @var string Has hooks, so it can not be passed by reference
*/
public string $color {
get => $this->color;
set(string $color) => $this->color = $color;
}
}
class FruitForm
{
/** @var array<Input> */
public array $children;
public function __construct(public Fruit $fruit)
{
// This is a direct reference to the Fruit property name
$nameAccessor = &$this->fruit->accessorFor('name');
$nameInput = new Input('name', $nameAccessor);
// This is a delegated access to the Fruit property color via accessor
$colorAccessor = &$this->fruit->accessorFor('color');
$colorInput = new Input('color', $colorAccessor);
$this->children = [$nameInput, $colorInput];
}
public function handleRequest(array $data): void
{
foreach ($this->children as $child) {
$child->handleRequest($data);
}
}
}
class Input
{
use UsesPropertyAccess;
public function __construct(public string $name, mixed &$reference)
{
// Initial property value must be PropertyAccess, which holds the reference or accessor to the Fruit property
$this->propertyAccess = new PropertyAccess($reference);
}
public function handleRequest(array $data): void
{
if (!array_key_exists($this->name, $data)) {
return;
}
$this->propertyAccess = $data[$this->name];
}
}
$fruit = new Fruit();
$fruit->name = 'Apple';
$fruit->color = 'Red';
$data = [
'name' => 'Blueberry',
'color' => 'Blue',
];
var_dump($fruit);
$form = new FruitForm($fruit);
$form->handleRequest($data);
var_dump($fruit);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment