Last active
March 21, 2026 18:48
-
-
Save vertexvaar/c19fb842092a6b3e38f5db8cbe367f32 to your computer and use it in GitHub Desktop.
Delegated Referenced Hooked Properties
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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