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