PropertyDeprecationHelper.php 5.1 KB

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