summaryrefslogtreecommitdiff
path: root/platform/www/inc/Debug/PropertyDeprecationHelper.php
blob: 6289d5ba8afebc76ac48b84e5c4a03620ebe5ecc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php
/**
 * Trait for issuing warnings on deprecated access.
 *
 * Adapted from https://github.com/wikimedia/mediawiki/blob/4aedefdbfd193f323097354bf581de1c93f02715/includes/debug/DeprecationHelper.php
 *
 */


namespace dokuwiki\Debug;

/**
 * Use this trait in classes which have properties for which public access
 * is deprecated. Set the list of properties in $deprecatedPublicProperties
 * and make the properties non-public. The trait will preserve public access
 * but issue deprecation warnings when it is needed.
 *
 * Example usage:
 *     class Foo {
 *         use DeprecationHelper;
 *         protected $bar;
 *         public function __construct() {
 *             $this->deprecatePublicProperty( 'bar', '1.21', __CLASS__ );
 *         }
 *     }
 *
 *     $foo = new Foo;
 *     $foo->bar; // works but logs a warning
 *
 * Cannot be used with classes that have their own __get/__set methods.
 *
 */
trait PropertyDeprecationHelper
{

    /**
     * List of deprecated properties, in <property name> => <class> format
     * where <class> is the the name of the class defining the property
     *
     * E.g. [ '_event' => '\dokuwiki\Cache\Cache' ]
     * @var string[]
     */
    protected $deprecatedPublicProperties = [];

    /**
     * Mark a property as deprecated. Only use this for properties that used to be public and only
     *   call it in the constructor.
     *
     * @param string $property The name of the property.
     * @param null $class name of the class defining the property
     * @see DebugHelper::dbgDeprecatedProperty
     */
    protected function deprecatePublicProperty(
        $property,
        $class = null
    ) {
        $this->deprecatedPublicProperties[$property] = $class ?: get_class();
    }

    public function __get($name)
    {
        if (isset($this->deprecatedPublicProperties[$name])) {
            $class = $this->deprecatedPublicProperties[$name];
            DebugHelper::dbgDeprecatedProperty($class, $name);
            return $this->$name;
        }

        $qualifiedName = get_class() . '::$' . $name;
        if ($this->deprecationHelperGetPropertyOwner($name)) {
            // Someone tried to access a normal non-public property. Try to behave like PHP would.
            trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR);
        } else {
            // Non-existing property. Try to behave like PHP would.
            trigger_error("Undefined property: $qualifiedName", E_USER_NOTICE);
        }
        return null;
    }

    public function __set($name, $value)
    {
        if (isset($this->deprecatedPublicProperties[$name])) {
            $class = $this->deprecatedPublicProperties[$name];
            DebugHelper::dbgDeprecatedProperty($class, $name);
            $this->$name = $value;
            return;
        }

        $qualifiedName = get_class() . '::$' . $name;
        if ($this->deprecationHelperGetPropertyOwner($name)) {
            // Someone tried to access a normal non-public property. Try to behave like PHP would.
            trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR);
        } else {
            // Non-existing property. Try to behave like PHP would.
            $this->$name = $value;
        }
    }

    /**
     * Like property_exists but also check for non-visible private properties and returns which
     * class in the inheritance chain declared the property.
     * @param string $property
     * @return string|bool Best guess for the class in which the property is defined.
     */
    private function deprecationHelperGetPropertyOwner($property)
    {
        // Easy branch: check for protected property / private property of the current class.
        if (property_exists($this, $property)) {
            // The class name is not necessarily correct here but getting the correct class
            // name would be expensive, this will work most of the time and getting it
            // wrong is not a big deal.
            return __CLASS__;
        }
        // property_exists() returns false when the property does exist but is private (and not
        // defined by the current class, for some value of "current" that differs slightly
        // between engines).
        // Since PHP triggers an error on public access of non-public properties but happily
        // allows public access to undefined properties, we need to detect this case as well.
        // Reflection is slow so use array cast hack to check for that:
        $obfuscatedProps = array_keys((array)$this);
        $obfuscatedPropTail = "\0$property";
        foreach ($obfuscatedProps as $obfuscatedProp) {
            // private props are in the form \0<classname>\0<propname>
            if (strpos($obfuscatedProp, $obfuscatedPropTail, 1) !== false) {
                $classname = substr($obfuscatedProp, 1, -strlen($obfuscatedPropTail));
                if ($classname === '*') {
                    // sanity; this shouldn't be possible as protected properties were handled earlier
                    $classname = __CLASS__;
                }
                return $classname;
            }
        }
        return false;
    }
}