Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.95% |
80 / 87 |
|
88.89% |
8 / 9 |
CRAP | |
0.00% |
0 / 1 |
DeprecationHelper | |
91.95% |
80 / 87 |
|
88.89% |
8 / 9 |
36.68 | |
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 | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
__set | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
7 | |||
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 | /** |
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 | */ |
60 | trait 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 | } |