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