Failed Conditions
Push — interwiki-remove-golucky ( 52fcdb...768be5 )
by Henry
12:48 queued 09:48
created

inc/Debug/PropertyDeprecationHelper.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
10
namespace 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
 */
33
trait 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 DebugHelper::dbgDeprecatedProperty
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
            DebugHelper::dbgDeprecatedProperty($class, $name);
65
            return $this->$name;
66
        }
67
68
        $qualifiedName = get_class() . '::$' . $name;
69
        if ($this->deprecationHelperGetPropertyOwner($name)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deprecationHelperGetPropertyOwner($name) of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
70
            // Someone tried to access a normal non-public property. Try to behave like PHP would.
71
            trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR);
72
        } else {
73
            // Non-existing property. Try to behave like PHP would.
74
            trigger_error("Undefined property: $qualifiedName", E_USER_NOTICE);
75
        }
76
        return null;
77
    }
78
79
    public function __set($name, $value)
80
    {
81
        if (isset($this->deprecatedPublicProperties[$name])) {
82
            $class = $this->deprecatedPublicProperties[$name];
83
            DebugHelper::dbgDeprecatedProperty($class, $name);
84
            $this->$name = $value;
85
            return;
86
        }
87
88
        $qualifiedName = get_class() . '::$' . $name;
89
        if ($this->deprecationHelperGetPropertyOwner($name)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deprecationHelperGetPropertyOwner($name) of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
90
            // Someone tried to access a normal non-public property. Try to behave like PHP would.
91
            trigger_error("Cannot access non-public property $qualifiedName", E_USER_ERROR);
92
        } else {
93
            // Non-existing property. Try to behave like PHP would.
94
            $this->$name = $value;
95
        }
96
    }
97
98
    /**
99
     * Like property_exists but also check for non-visible private properties and returns which
100
     * class in the inheritance chain declared the property.
101
     * @param string $property
102
     * @return string|bool Best guess for the class in which the property is defined.
103
     */
104
    private function deprecationHelperGetPropertyOwner($property)
105
    {
106
        // Easy branch: check for protected property / private property of the current class.
107
        if (property_exists($this, $property)) {
108
            // The class name is not necessarily correct here but getting the correct class
109
            // name would be expensive, this will work most of the time and getting it
110
            // wrong is not a big deal.
111
            return __CLASS__;
112
        }
113
        // property_exists() returns false when the property does exist but is private (and not
114
        // defined by the current class, for some value of "current" that differs slightly
115
        // between engines).
116
        // Since PHP triggers an error on public access of non-public properties but happily
117
        // allows public access to undefined properties, we need to detect this case as well.
118
        // Reflection is slow so use array cast hack to check for that:
119
        $obfuscatedProps = array_keys((array)$this);
120
        $obfuscatedPropTail = "\0$property";
121
        foreach ($obfuscatedProps as $obfuscatedProp) {
122
            // private props are in the form \0<classname>\0<propname>
123
            if (strpos($obfuscatedProp, $obfuscatedPropTail, 1) !== false) {
124
                $classname = substr($obfuscatedProp, 1, -strlen($obfuscatedPropTail));
125
                if ($classname === '*') {
126
                    // sanity; this shouldn't be possible as protected properties were handled earlier
127
                    $classname = __CLASS__;
128
                }
129
                return $classname;
130
            }
131
        }
132
        return false;
133
    }
134
}
135