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