xref: /dokuwiki/inc/Debug/PropertyDeprecationHelper.php (revision dc7da772c0bd95eaee1f2fc25d29cb5108ff0809)
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