Failed Conditions
Push — refactorCachePSR2 ( fec08c...b4b0b3 )
by Michael
02:57
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 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)) {
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...
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)) {
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...
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