MediaWiki master
DeprecationHelper.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Debug;
22
23use ReflectionFunction;
24use ReflectionProperty;
25
65trait DeprecationHelper {
66
76 protected static $deprecatedPublicProperties = [];
77
83 private $dynamicPropertiesAccessDeprecated = false;
84
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
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
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
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}
319class_alias( DeprecationHelper::class, 'DeprecationHelper' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
__set( $name, $value)
deprecatePublicPropertyFallback(string $property, string $version, $getter, $setter=null, $class=null, $component=null)
Mark a removed public property as deprecated and provide fallback getter and setter callables.
deprecateDynamicPropertiesAccess(string $version, string $class=null, string $component=null)
Emit deprecation warnings when dynamic and unknown properties are accessed.
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.