1b4b0b31bSMichael Große<?php 2*d4f83172SAndreas Gohr 3b4b0b31bSMichael Große/** 4b4b0b31bSMichael Große * Trait for issuing warnings on deprecated access. 5b4b0b31bSMichael Große * 6b4b0b31bSMichael Große * Adapted from https://github.com/wikimedia/mediawiki/blob/4aedefdbfd193f323097354bf581de1c93f02715/includes/debug/DeprecationHelper.php 7b4b0b31bSMichael Große * 8b4b0b31bSMichael Große */ 9b4b0b31bSMichael Große 10b4b0b31bSMichael Großenamespace dokuwiki\Debug; 11b4b0b31bSMichael Große 12b4b0b31bSMichael Große/** 13b4b0b31bSMichael Große * Use this trait in classes which have properties for which public access 14b4b0b31bSMichael Große * is deprecated. Set the list of properties in $deprecatedPublicProperties 15b4b0b31bSMichael Große * and make the properties non-public. The trait will preserve public access 16b4b0b31bSMichael Große * but issue deprecation warnings when it is needed. 17b4b0b31bSMichael Große * 18b4b0b31bSMichael Große * Example usage: 19b4b0b31bSMichael Große * class Foo { 20b4b0b31bSMichael Große * use DeprecationHelper; 21b4b0b31bSMichael Große * protected $bar; 22b4b0b31bSMichael Große * public function __construct() { 23b4b0b31bSMichael Große * $this->deprecatePublicProperty( 'bar', '1.21', __CLASS__ ); 24b4b0b31bSMichael Große * } 25b4b0b31bSMichael Große * } 26b4b0b31bSMichael Große * 27b4b0b31bSMichael Große * $foo = new Foo; 28b4b0b31bSMichael Große * $foo->bar; // works but logs a warning 29b4b0b31bSMichael Große * 30b4b0b31bSMichael Große * Cannot be used with classes that have their own __get/__set methods. 31b4b0b31bSMichael Große * 32b4b0b31bSMichael Große */ 33b4b0b31bSMichael Großetrait PropertyDeprecationHelper 34b4b0b31bSMichael Große{ 35b4b0b31bSMichael Große /** 36b4b0b31bSMichael Große * List of deprecated properties, in <property name> => <class> format 37b4b0b31bSMichael Große * where <class> is the the name of the class defining the property 38b4b0b31bSMichael Große * 39b4b0b31bSMichael Große * E.g. [ '_event' => '\dokuwiki\Cache\Cache' ] 40b4b0b31bSMichael Große * @var string[] 41b4b0b31bSMichael Große */ 42b4b0b31bSMichael Große protected $deprecatedPublicProperties = []; 43b4b0b31bSMichael Große 44b4b0b31bSMichael Große /** 45b4b0b31bSMichael Große * Mark a property as deprecated. Only use this for properties that used to be public and only 46b4b0b31bSMichael Große * call it in the constructor. 47b4b0b31bSMichael Große * 48b4b0b31bSMichael Große * @param string $property The name of the property. 49b4b0b31bSMichael Große * @param null $class name of the class defining the property 500c5eb5e2SMichael Große * @see DebugHelper::dbgDeprecatedProperty 51b4b0b31bSMichael Große */ 52b4b0b31bSMichael Große protected function deprecatePublicProperty( 53b4b0b31bSMichael Große $property, 54b4b0b31bSMichael Große $class = null 55b4b0b31bSMichael Große ) { 56b4b0b31bSMichael Große $this->deprecatedPublicProperties[$property] = $class ?: get_class(); 57b4b0b31bSMichael Große } 58b4b0b31bSMichael Große 59b4b0b31bSMichael Große public function __get($name) 60b4b0b31bSMichael Große { 61b4b0b31bSMichael Große if (isset($this->deprecatedPublicProperties[$name])) { 62b4b0b31bSMichael Große $class = $this->deprecatedPublicProperties[$name]; 630c5eb5e2SMichael Große DebugHelper::dbgDeprecatedProperty($class, $name); 64b4b0b31bSMichael Große return $this->$name; 65b4b0b31bSMichael Große } 66b4b0b31bSMichael Große 67b4b0b31bSMichael Große $qualifiedName = get_class() . '::$' . $name; 68b4b0b31bSMichael Große if ($this->deprecationHelperGetPropertyOwner($name)) { 69b4b0b31bSMichael Große // Someone tried to access a normal non-public property. Try to behave like PHP would. 70b4b0b31bSMichael Große trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR); 71b4b0b31bSMichael Große } else { 72b4b0b31bSMichael Große // Non-existing property. Try to behave like PHP would. 73b4b0b31bSMichael Große trigger_error("Undefined property: $qualifiedName", E_USER_NOTICE); 74b4b0b31bSMichael Große } 75b4b0b31bSMichael Große return null; 76b4b0b31bSMichael Große } 77b4b0b31bSMichael Große 78b4b0b31bSMichael Große public function __set($name, $value) 79b4b0b31bSMichael Große { 80b4b0b31bSMichael Große if (isset($this->deprecatedPublicProperties[$name])) { 81b4b0b31bSMichael Große $class = $this->deprecatedPublicProperties[$name]; 820c5eb5e2SMichael Große DebugHelper::dbgDeprecatedProperty($class, $name); 83b4b0b31bSMichael Große $this->$name = $value; 84b4b0b31bSMichael Große return; 85b4b0b31bSMichael Große } 86b4b0b31bSMichael Große 87b4b0b31bSMichael Große $qualifiedName = get_class() . '::$' . $name; 88b4b0b31bSMichael Große if ($this->deprecationHelperGetPropertyOwner($name)) { 89b4b0b31bSMichael Große // Someone tried to access a normal non-public property. Try to behave like PHP would. 90b4b0b31bSMichael Große trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR); 91b4b0b31bSMichael Große } else { 92b4b0b31bSMichael Große // Non-existing property. Try to behave like PHP would. 93b4b0b31bSMichael Große $this->$name = $value; 94b4b0b31bSMichael Große } 95b4b0b31bSMichael Große } 96b4b0b31bSMichael Große 97b4b0b31bSMichael Große /** 98b4b0b31bSMichael Große * Like property_exists but also check for non-visible private properties and returns which 99b4b0b31bSMichael Große * class in the inheritance chain declared the property. 100b4b0b31bSMichael Große * @param string $property 101b4b0b31bSMichael Große * @return string|bool Best guess for the class in which the property is defined. 102b4b0b31bSMichael Große */ 103b4b0b31bSMichael Große private function deprecationHelperGetPropertyOwner($property) 104b4b0b31bSMichael Große { 105b4b0b31bSMichael Große // Easy branch: check for protected property / private property of the current class. 106b4b0b31bSMichael Große if (property_exists($this, $property)) { 107b4b0b31bSMichael Große // The class name is not necessarily correct here but getting the correct class 108b4b0b31bSMichael Große // name would be expensive, this will work most of the time and getting it 109b4b0b31bSMichael Große // wrong is not a big deal. 1101490c177SAndreas Gohr return self::class; 111b4b0b31bSMichael Große } 112b4b0b31bSMichael Große // property_exists() returns false when the property does exist but is private (and not 113b4b0b31bSMichael Große // defined by the current class, for some value of "current" that differs slightly 114b4b0b31bSMichael Große // between engines). 115b4b0b31bSMichael Große // Since PHP triggers an error on public access of non-public properties but happily 116b4b0b31bSMichael Große // allows public access to undefined properties, we need to detect this case as well. 117b4b0b31bSMichael Große // Reflection is slow so use array cast hack to check for that: 118b4b0b31bSMichael Große $obfuscatedProps = array_keys((array)$this); 119b4b0b31bSMichael Große $obfuscatedPropTail = "\0$property"; 120b4b0b31bSMichael Große foreach ($obfuscatedProps as $obfuscatedProp) { 121b4b0b31bSMichael Große // private props are in the form \0<classname>\0<propname> 122b4b0b31bSMichael Große if (strpos($obfuscatedProp, $obfuscatedPropTail, 1) !== false) { 123b4b0b31bSMichael Große $classname = substr($obfuscatedProp, 1, -strlen($obfuscatedPropTail)); 124b4b0b31bSMichael Große if ($classname === '*') { 125b4b0b31bSMichael Große // sanity; this shouldn't be possible as protected properties were handled earlier 1261490c177SAndreas Gohr $classname = self::class; 127b4b0b31bSMichael Große } 128b4b0b31bSMichael Große return $classname; 129b4b0b31bSMichael Große } 130b4b0b31bSMichael Große } 131b4b0b31bSMichael Große return false; 132b4b0b31bSMichael Große } 133b4b0b31bSMichael Große} 134