Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.95% covered (success)
91.95%
80 / 87
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeprecationHelper
91.95% covered (success)
91.95%
80 / 87
88.89% covered (warning)
88.89%
8 / 9
36.68
0.00% covered (danger)
0.00%
0 / 1
 deprecatePublicProperty
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 deprecatePublicPropertyFallback
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 deprecateDynamicPropertiesAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 __isset
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
7.54
 __get
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 __set
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 deprecationHelperGetPropertyOwner
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 deprecationHelperCallGetter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 deprecationHelperCallSetter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21/**
22 * Trait for issuing warnings on deprecated access.
23 *
24 * Use this trait in classes which have properties for which public access
25 * is deprecated or implementation has been moved to another class.
26 * Set the list of properties in $deprecatedPublicProperties
27 * and make the properties non-public. The trait will preserve public access
28 * but issue deprecation warnings when it is needed.
29 *
30 * Example usage:
31 *     class Foo {
32 *         use DeprecationHelper;
33 *         protected $bar;
34 *         public function __construct() {
35 *             $this->deprecatePublicProperty( 'bar', '1.21', __CLASS__ );
36 *             $this->deprecatePublicPropertyFallback(
37 *                 'movedValue',
38 *                 '1.35',
39 *                 function () {
40 *                     return MediaWikiServices()::getInstance()
41 *                         ->getNewImplementationService()->getValue();
42 *                 },
43 *                 function ( $value ) {
44 *                     MediaWikiServices()::getInstance()
45 *                         ->getNewImplementationService()->setValue( $value );
46 *                 }
47 *             );
48 *         }
49 *     }
50 *
51 *     $foo = new Foo;
52 *     $foo->bar; // works but logs a warning
53 *     $foo->movedValue = 10; // works but logs a warning
54 *     $movedValue = $foo->movedValue; // also works
55 *
56 * Cannot be used with classes that have their own __get/__set methods.
57 *
58 * @since 1.32
59 */
60trait DeprecationHelper {
61
62    /**
63     * List of deprecated properties, in <property name> => [<version>, <class>,
64     * <component>, <getter>, <setter> ] format where <version> is the MediaWiki version
65     * where the property got deprecated, <class> is the
66     * the name of the class defining the property, <component> is the MediaWiki component
67     * (extension, skin etc.) for use in the deprecation warning) or null if it is MediaWiki.
68     * E.g. [ 'mNewRev' => [ '1.32', 'DifferenceEngine', null ]
69     * @var string[][]
70     */
71    protected static $deprecatedPublicProperties = [];
72
73    /**
74     * Whether to emit a deprecation warning when unknown properties are accessed.
75     *
76     * @var bool|array
77     */
78    private $dynamicPropertiesAccessDeprecated = false;
79
80    /**
81     * Mark a property as deprecated. Only use this for properties that used to be public and only
82     *   call it in the constructor.
83     *
84     * @note  Providing callbacks makes it not serializable
85     *
86     * @param string $property The name of the property.
87     * @param string $version MediaWiki version where the property became deprecated.
88     * @param string|null $class The class which has the deprecated property. This can usually be
89     *   guessed, but PHP can get confused when both the parent class and the subclass use the
90     *   trait, so it should be specified in classes meant for subclassing.
91     * @param string|null $component
92     * @see wfDeprecated()
93     */
94    protected function deprecatePublicProperty(
95        $property,
96        $version,
97        $class = null,
98        $component = null
99    ) {
100        if ( isset( self::$deprecatedPublicProperties[$property] ) ) {
101            return;
102        }
103        self::$deprecatedPublicProperties[$property] = [
104            $version,
105            $class ?: __CLASS__,
106            $component,
107            null, null
108        ];
109    }
110
111    /**
112     * Mark a removed public property as deprecated and provide fallback getter and setter callables.
113     * Only use this for properties that used to be public and only
114     * call it in the constructor.
115     *
116     * @param string $property The name of the property.
117     * @param string $version MediaWiki version where the property became deprecated.
118     * @param callable|string $getter A user provided getter that implements a `get` logic
119     *        for the property. If a string is given, it is called as a method on $this.
120     * @param callable|string|null $setter A user provided setter that implements a `set` logic
121     *        for the property. If a string is given, it is called as a method on $this.
122     * @param string|null $class The class which has the deprecated property.
123     * @param string|null $component
124     *
125     * @since 1.36
126     * @see wfDeprecated()
127     */
128    protected function deprecatePublicPropertyFallback(
129        string $property,
130        string $version,
131        $getter,
132        $setter = null,
133        $class = null,
134        $component = null
135    ) {
136        if ( isset( self::$deprecatedPublicProperties[$property] ) ) {
137            return;
138        }
139        self::$deprecatedPublicProperties[$property] = [
140            $version,
141            $class ?: __CLASS__,
142            null,
143            $getter,
144            $setter,
145            $component
146        ];
147    }
148
149    /**
150     * Emit deprecation warnings when dynamic and unknown properties
151     * are accessed.
152     *
153     * @param string $version MediaWiki version where the property became deprecated.
154     * @param string|null $class The class which has the deprecated property.
155     * @param string|null $component
156     */
157    protected function deprecateDynamicPropertiesAccess(
158        string $version,
159        string $class = null,
160        string $component = null
161    ) {
162        $this->dynamicPropertiesAccessDeprecated = [ $version, $class ?: __CLASS__, $component ];
163    }
164
165    public function __isset( $name ) {
166        // Overriding magic __isset is required not only for isset() and empty(),
167        // but to correctly support null coalescing for dynamic properties,
168        // e.g. $foo->bar ?? 'default'
169        if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
170            [ $version, $class, $component, $getter ] = self::$deprecatedPublicProperties[$name];
171            $qualifiedName = $class . '::$' . $name;
172            wfDeprecated( $qualifiedName, $version, $component, 2 );
173            if ( $getter ) {
174                return $this->deprecationHelperCallGetter( $getter );
175            }
176            return true;
177        }
178
179        $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
180        if ( $ownerClass ) {
181            // Someone tried to access a normal non-public property. Try to behave like PHP would.
182            return false;
183        } else {
184            if ( $this->dynamicPropertiesAccessDeprecated ) {
185                [ $version, $class, $component ] = $this->dynamicPropertiesAccessDeprecated;
186                $qualifiedName = $class . '::$' . $name;
187                wfDeprecated( $qualifiedName, $version, $component, 2 );
188            }
189            return false;
190        }
191    }
192
193    public function __get( $name ) {
194        if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
195            [ $version, $class, $component, $getter ] = self::$deprecatedPublicProperties[$name];
196            $qualifiedName = $class . '::$' . $name;
197            wfDeprecated( $qualifiedName, $version, $component, 2 );
198            if ( $getter ) {
199                return $this->deprecationHelperCallGetter( $getter );
200            }
201            return $this->$name;
202        }
203
204        $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
205        $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
206        if ( $ownerClass ) {
207            // Someone tried to access a normal non-public property. Try to behave like PHP would.
208            trigger_error( "Cannot access non-public property $qualifiedName", E_USER_ERROR );
209        } elseif ( property_exists( $this, $name ) ) {
210            // Normally __get method will not be even called if the property exists,
211            // but in tests if we mock an object that uses DeprecationHelper,
212            // __get and __set magic methods will be mocked as well, and called
213            // regardless of the property existence. Support that use-case.
214            return $this->$name;
215        } else {
216            // Non-existing property. Try to behave like PHP would.
217            trigger_error( "Undefined property: $qualifiedName", E_USER_NOTICE );
218        }
219        return null;
220    }
221
222    public function __set( $name, $value ) {
223        if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
224            [ $version, $class, $component, , $setter ] = self::$deprecatedPublicProperties[$name];
225            $qualifiedName = $class . '::$' . $name;
226            wfDeprecated( $qualifiedName, $version, $component, 2 );
227            if ( $setter ) {
228                $this->deprecationHelperCallSetter( $setter, $value );
229            } elseif ( property_exists( $this, $name ) ) {
230                $this->$name = $value;
231            } else {
232                trigger_error( "Cannot access non-public property $qualifiedName", E_USER_ERROR );
233            }
234            return;
235        }
236
237        $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
238        $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
239        if ( $ownerClass ) {
240            // Someone tried to access a normal non-public property. Try to behave like PHP would.
241            trigger_error( "Cannot access non-public property $qualifiedName", E_USER_ERROR );
242        } else {
243            if ( $this->dynamicPropertiesAccessDeprecated ) {
244                [ $version, $class, $component ] = $this->dynamicPropertiesAccessDeprecated;
245                $qualifiedName = $class . '::$' . $name;
246                wfDeprecated( $qualifiedName, $version, $component, 2 );
247            }
248            // Non-existing property. Try to behave like PHP would.
249            $this->$name = $value;
250        }
251    }
252
253    /**
254     * Like property_exists but also check for non-visible private properties and returns which
255     * class in the inheritance chain declared the property.
256     * @param string $property
257     * @return string|bool Best guess for the class in which the property is defined. False if
258     *   the object does not have such a property.
259     */
260    private function deprecationHelperGetPropertyOwner( $property ) {
261        // Returning false is a non-error path and should avoid slow checks like reflection.
262        // Use array cast hack instead.
263        $obfuscatedProps = array_keys( (array)$this );
264        $obfuscatedPropTail = "\0$property";
265        foreach ( $obfuscatedProps as $obfuscatedProp ) {
266            // private props are in the form \0<classname>\0<propname>
267            if ( strpos( $obfuscatedProp, $obfuscatedPropTail, 1 ) !== false ) {
268                $classname = substr( $obfuscatedProp, 1, -strlen( $obfuscatedPropTail ) );
269                if ( $classname === '*' ) {
270                    // protected property; we didn't get the name, but we are on an error path
271                    // now so it's fine to use reflection
272                    return ( new ReflectionProperty( $this, $property ) )->getDeclaringClass()->getName();
273                }
274                return $classname;
275            }
276        }
277        return false;
278    }
279
280    private function deprecationHelperCallGetter( $getter ) {
281        if ( is_string( $getter ) ) {
282            $getter = [ $this, $getter ];
283        } elseif ( ( new ReflectionFunction( $getter ) )->getClosureThis() !== null ) {
284            $getter = $getter->bindTo( $this );
285        }
286        return $getter();
287    }
288
289    private function deprecationHelperCallSetter( $setter, $value ) {
290        if ( is_string( $setter ) ) {
291            $setter = [ $this, $setter ];
292        } elseif ( ( new ReflectionFunction( $setter ) )->getClosureThis() !== null ) {
293            $setter = $setter->bindTo( $this );
294        }
295        $setter( $value );
296    }
297}