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