<?php
/**
* TODO: Fix shitty doc.
*
* Provides type assertions for public object properties. All public properties are affected by this library. The
* following assertions are provided:
*
* - Read-only property: the property can be accessed by everything, but only the object itself can modify it. This is
* the default behavior af all public properties, unless configured otherwise.
* - Guarded property: the property can be set by everything, but the value must pass a custom check for type/format,
* provided by the object.
*
* In both cases the checks only apply for external callers (outside the object). Public properties can't be unset
* (they can still be set to null, however). The trait also protects against dynamic properties being set on the object.
*/
trait PropAssert {
// TODO: Move out all method bodies to another class, so code is loaded only if uses (i.e. with assertions enabled)?
protected $__propAssert = [];
/**
* Initializes PropAssert. This method should be called once for each object, preferably at construction time.
*
* @return bool Always returns true, so it can be comfortably invoked in an assert statement.
*/
protected function initPropAssert(): bool {
$refl = new \ReflectionClass(get_class($this));
$propList = $refl->getProperties(\ReflectionProperty::IS_PUBLIC);
foreach ($propList as $prop) {
$name = $prop->name;
$this->__propAssert[$name] = [
'guard' => null,
'value' => $this->{$name},
];
unset($this->{$name});
}
return true;
}
/**
* Configured how a given public property is handled. By default, public properties are read-only. With this
* method,
* they can be set as writable, with a "guard" function, that checks the type and format of the value being set.
*
* @param string $name A public property name (the property should already be declared as public).
* @param \Closure|null $guard A closure that guards the property while being set, or null to make the property
* read-only (all public properties are read-only by default). If provided, the closure
* should be in format ($value) => bool|string. The guard accepts a value about to be
* set for the given property, and returns true if the value is valid, and false or a
* string error message (for ex. "a string of length 6-32 chars"), if the value is
* invalid.
* @return bool Always returns true, so it can be comfortably invoked in an assert statement.
* @throws \Error On bad input.
*/
protected function setPropAssert($name, ?\Closure $guard): bool {
$this->__propAssertRequireDefined($name);
$this->__propAssert[$name]['guard'] = $guard;
return true;
}
// Internal method, don't invoke.
public function __get($name) {
$this->__propAssertRequireDefined($name);
$isInternal = $this->__propAssertIsInternalCaller();
$prop = $this->__propAssert[$name];
return $this->__propAssert[$name]['value'];
}
// Internal method, don't invoke.
public function __set($name, $value) {
$this->__propAssertRequireDefined($name);
$isInternal = $this->__propAssertIsInternalCaller();
$guard = $this->__propAssert[$name]['guard'];
if (!$isInternal && !$guard) {
throw new \TypeError('Cannot set read-only property ' . get_class($this) . '::$' . $name);
}
$result = $guard($value);
if ($result === true) {
$this->__propAssert[$name]['value'] = $value;
} else {
$details = is_string($result) ? ', expecting ' . $result : '';
throw new \TypeError('Invalid value for property ' . get_class($this) . '::$' . $name . $details);
}
}
// Internal method, don't invoke.
public function __isset($name) {
$this->__propAssertRequireDefined($name);
return $this->__propAssert[$name]['value'] === null;
}
// Internal method, don't invoke.
public function __unset($name) {
$this->__propAssertRequireDefined($name);
throw new \TypeError('Cannot unset property ' . get_class($this) . '::$' . $name);
}
// Internal method, don't invoke.
private function __propAssertRequireDefined($name) {
if (!isset($this->__propAssert[$name])) {
throw new \TypeError('Undefined property ' . get_class($this) . '::$' . $name);
}
}
// Internal method, don't invoke.
private function __propAssertIsInternalCaller() {
// TODO: Relax this check, so static methods of the class and cross-object calls can skip checks?
$object = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['object'] ?? null;
return $this === $object;
}
}
// --------------------------------------------------------------------------------------------------------------------
class Foo {
use PropAssert;
public $a = 10, $b = 20;
function __construct() {
$this->initPropAssert();
$this->setPropAssert('b', function ($v) {
if (!is_int($v)) return 'an integer, you moran';
return true;
});
}
}
$foo = new Foo();
echo $foo->a . PHP_EOL;
echo $foo->b . PHP_EOL;
$foo->b = 123;
echo $foo->b . PHP_EOL;
try {
$foo->b = 'moran';
} catch (\Throwable $e) {
echo $e->getMessage() . PHP_EOL;
}
try {
$foo->a = 100;
} catch (\Throwable $e) {
echo $e->getMessage() . PHP_EOL;
}
10
20
123
Invalid value for property Foo::$b, expecting an integer, you moran
Cannot set read-only property Foo::$a
Output for 8.3.5
Warning: PHP Startup: Unable to load dynamic library 'sodium.so' (tried: /usr/lib/php/8.3.5/modules/sodium.so (libsodium.so.23: cannot open shared object file: No such file or directory), /usr/lib/php/8.3.5/modules/sodium.so.so (/usr/lib/php/8.3.5/modules/sodium.so.so: cannot open shared object file: No such file or directory)) in Unknown on line 0
10
20
123
Invalid value for property Foo::$b, expecting an integer, you moran
Cannot set read-only property Foo::$a
Output for 7.0.0 - 7.0.20
Parse error: syntax error, unexpected '?', expecting variable (T_VARIABLE) in /in/9YFRE on line 57
Process exited with code 255.