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