Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.17% |
82 / 93 |
|
66.67% |
6 / 9 |
CRAP | |
0.00% |
0 / 1 |
DeprecationHelper | |
89.13% |
82 / 92 |
|
66.67% |
6 / 9 |
39.85 | |
0.00% |
0 / 1 |
deprecatePublicProperty | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
deprecatePublicPropertyFallback | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
deprecateDynamicPropertiesAccess | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
__isset | |
53.33% |
8 / 15 |
|
0.00% |
0 / 1 |
7.54 | |||
__get | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
7.01 | |||
__set | |
90.91% |
20 / 22 |
|
0.00% |
0 / 1 |
8.05 | |||
deprecationHelperGetPropertyOwner | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
deprecationHelperCallGetter | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
deprecationHelperCallSetter | |
100.00% |
5 / 5 |
|
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 | namespace MediaWiki\Debug; |
22 | |
23 | use ReflectionFunction; |
24 | use 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 | */ |
65 | trait 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 */ |
319 | class_alias( DeprecationHelper::class, 'DeprecationHelper' ); |