deprecatePublicProperty( 'bar', '1.21', __CLASS__ ); * } * } * * $foo = new Foo; * $foo->bar; // works but logs a warning * * Cannot be used with classes that have their own __get/__set methods. * */ trait PropertyDeprecationHelper { /** * List of deprecated properties, in => format * where is the the name of the class defining the property * * E.g. [ '_event' => '\dokuwiki\Cache\Cache' ] * @var string[] */ protected $deprecatedPublicProperties = []; /** * Mark a property as deprecated. Only use this for properties that used to be public and only * call it in the constructor. * * @param string $property The name of the property. * @param null $class name of the class defining the property * @see DebugHelper::dbgDeprecatedProperty */ protected function deprecatePublicProperty( $property, $class = null ) { $this->deprecatedPublicProperties[$property] = $class ?: get_class($this); } public function __get($name) { if (isset($this->deprecatedPublicProperties[$name])) { $class = $this->deprecatedPublicProperties[$name]; DebugHelper::dbgDeprecatedProperty($class, $name); return $this->$name; } $qualifiedName = get_class() . '::$' . $name; if ($this->deprecationHelperGetPropertyOwner($name)) { // Someone tried to access a normal non-public property. Try to behave like PHP would. throw new \RuntimeException("Cannot access non-public property $qualifiedName"); } else { // Non-existing property. Try to behave like PHP would. trigger_error("Undefined property: $qualifiedName", E_USER_NOTICE); } return null; } public function __set($name, $value) { if (isset($this->deprecatedPublicProperties[$name])) { $class = $this->deprecatedPublicProperties[$name]; DebugHelper::dbgDeprecatedProperty($class, $name); $this->$name = $value; return; } $qualifiedName = get_class() . '::$' . $name; if ($this->deprecationHelperGetPropertyOwner($name)) { // Someone tried to access a normal non-public property. Try to behave like PHP would. throw new \RuntimeException("Cannot access non-public property $qualifiedName"); } else { // Non-existing property. Try to behave like PHP would. $this->$name = $value; } } /** * Like property_exists but also check for non-visible private properties and returns which * class in the inheritance chain declared the property. * @param string $property * @return string|bool Best guess for the class in which the property is defined. */ private function deprecationHelperGetPropertyOwner($property) { // Easy branch: check for protected property / private property of the current class. if (property_exists($this, $property)) { // The class name is not necessarily correct here but getting the correct class // name would be expensive, this will work most of the time and getting it // wrong is not a big deal. return self::class; } // property_exists() returns false when the property does exist but is private (and not // defined by the current class, for some value of "current" that differs slightly // between engines). // Since PHP triggers an error on public access of non-public properties but happily // allows public access to undefined properties, we need to detect this case as well. // Reflection is slow so use array cast hack to check for that: $obfuscatedProps = array_keys((array)$this); $obfuscatedPropTail = "\0$property"; foreach ($obfuscatedProps as $obfuscatedProp) { // private props are in the form \0\0 if (strpos($obfuscatedProp, $obfuscatedPropTail, 1) !== false) { $classname = substr($obfuscatedProp, 1, -strlen($obfuscatedPropTail)); if ($classname === '*') { // sanity; this shouldn't be possible as protected properties were handled earlier $classname = self::class; } return $classname; } } return false; } }