MediaWiki 1.44.0
DeprecationHelper.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Debug;
22
23use Error;
24use ReflectionFunction;
25use ReflectionProperty;
26
66trait DeprecationHelper {
67
77 protected static $deprecatedPublicProperties = [];
78
84 private $dynamicPropertiesAccessDeprecated = false;
85
100 protected function deprecatePublicProperty(
101 $property,
102 $version,
103 $class = null,
104 $component = null
105 ) {
106 if ( isset( self::$deprecatedPublicProperties[$property] ) ) {
107 return;
108 }
109 self::$deprecatedPublicProperties[$property] = [
110 $version,
111 $class ?: __CLASS__,
112 $component,
113 null, null
114 ];
115 }
116
135 string $property,
136 string $version,
137 $getter,
138 $setter = null,
139 $class = null,
140 $component = null
141 ) {
142 if ( isset( self::$deprecatedPublicProperties[$property] ) ) {
143 return;
144 }
145 self::$deprecatedPublicProperties[$property] = [
146 $version,
147 $class ?: __CLASS__,
148 null,
149 $getter,
150 $setter,
151 $component
152 ];
153 }
154
164 string $version,
165 ?string $class = null,
166 ?string $component = null
167 ) {
168 $this->dynamicPropertiesAccessDeprecated = [ $version, $class ?: __CLASS__, $component ];
169 }
170
171 public function __isset( $name ) {
172 // Overriding magic __isset is required not only for isset() and empty(),
173 // but to correctly support null coalescing for dynamic properties,
174 // e.g. $foo->bar ?? 'default'
175 if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
176 [ $version, $class, $component, $getter ] = self::$deprecatedPublicProperties[$name];
177 $qualifiedName = $class . '::$' . $name;
178 wfDeprecated( $qualifiedName, $version, $component, 2 );
179 if ( $getter ) {
180 return $this->deprecationHelperCallGetter( $getter );
181 }
182 return true;
183 }
184
185 $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
186 if ( $ownerClass ) {
187 // Someone tried to access a normal non-public property. Try to behave like PHP would.
188 return false;
189 } else {
190 if ( $this->dynamicPropertiesAccessDeprecated ) {
191 [ $version, $class, $component ] = $this->dynamicPropertiesAccessDeprecated;
192 $qualifiedName = $class . '::$' . $name;
193 wfDeprecated( $qualifiedName, $version, $component, 2 );
194 }
195 return false;
196 }
197 }
198
199 public function __get( $name ) {
200 if ( get_object_vars( $this ) === [] ) {
201 // Object is being destructed, all bets are off (T363492);
202 // in particular, we can't check $this->dynamicPropertiesAccessDeprecated anymore.
203 // Just get the property and hope for the best...
204 return $this->$name;
205 }
206
207 if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
208 [ $version, $class, $component, $getter ] = self::$deprecatedPublicProperties[$name];
209 $qualifiedName = $class . '::$' . $name;
210 wfDeprecated( $qualifiedName, $version, $component, 2 );
211 if ( $getter ) {
212 return $this->deprecationHelperCallGetter( $getter );
213 }
214 return $this->$name;
215 }
216
217 $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
218 $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
219 if ( $ownerClass ) {
220 // Someone tried to access a normal non-public property. Try to behave like PHP would.
221 throw new Error( "Cannot access non-public property $qualifiedName" );
222 } elseif ( property_exists( $this, $name ) ) {
223 // Normally __get method will not be even called if the property exists,
224 // but in tests if we mock an object that uses DeprecationHelper,
225 // __get and __set magic methods will be mocked as well, and called
226 // regardless of the property existence. Support that use-case.
227 return $this->$name;
228 } else {
229 // Non-existing property. Try to behave like PHP would.
230 trigger_error( "Undefined property: $qualifiedName", E_USER_NOTICE );
231 }
232 return null;
233 }
234
235 public function __set( $name, $value ) {
236 if ( get_object_vars( $this ) === [] ) {
237 // Object is being destructed, all bets are off (T363492);
238 // in particular, we can't check $this->dynamicPropertiesAccessDeprecated anymore.
239 // Just set the property and hope for the best...
240 $this->$name = $value;
241 return;
242 }
243
244 if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
245 [ $version, $class, $component, , $setter ] = self::$deprecatedPublicProperties[$name];
246 $qualifiedName = $class . '::$' . $name;
247 wfDeprecated( $qualifiedName, $version, $component, 2 );
248 if ( $setter ) {
249 $this->deprecationHelperCallSetter( $setter, $value );
250 } elseif ( property_exists( $this, $name ) ) {
251 $this->$name = $value;
252 } else {
253 throw new Error( "Cannot access non-public property $qualifiedName" );
254 }
255 return;
256 }
257
258 $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
259 $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
260 if ( $ownerClass ) {
261 // Someone tried to access a normal non-public property. Try to behave like PHP would.
262 throw new Error( "Cannot access non-public property $qualifiedName" );
263 } else {
264 if ( $this->dynamicPropertiesAccessDeprecated ) {
265 [ $version, $class, $component ] = $this->dynamicPropertiesAccessDeprecated;
266 $qualifiedName = $class . '::$' . $name;
267 wfDeprecated( $qualifiedName, $version, $component, 2 );
268 }
269 // Non-existing property. Try to behave like PHP would.
270 $this->$name = $value;
271 }
272 }
273
281 private function deprecationHelperGetPropertyOwner( $property ) {
282 // Returning false is a non-error path and should avoid slow checks like reflection.
283 // Use array cast hack instead.
284 $obfuscatedProps = array_keys( (array)$this );
285 $obfuscatedPropTail = "\0$property";
286 foreach ( $obfuscatedProps as $obfuscatedProp ) {
287 // private props are in the form \0<classname>\0<propname>
288 if ( strpos( $obfuscatedProp, $obfuscatedPropTail, 1 ) !== false ) {
289 $classname = substr( $obfuscatedProp, 1, -strlen( $obfuscatedPropTail ) );
290 if ( $classname === '*' ) {
291 // protected property; we didn't get the name, but we are on an error path
292 // now so it's fine to use reflection
293 return ( new ReflectionProperty( $this, $property ) )->getDeclaringClass()->getName();
294 }
295 return $classname;
296 }
297 }
298 return false;
299 }
300
305 private function deprecationHelperCallGetter( $getter ) {
306 if ( is_string( $getter ) ) {
307 $getter = [ $this, $getter ];
308 } elseif ( ( new ReflectionFunction( $getter ) )->getClosureThis() !== null ) {
309 $getter = $getter->bindTo( $this );
310 }
311 return $getter();
312 }
313
318 private function deprecationHelperCallSetter( $setter, $value ) {
319 if ( is_string( $setter ) ) {
320 $setter = [ $this, $setter ];
321 } elseif ( ( new ReflectionFunction( $setter ) )->getClosureThis() !== null ) {
322 $setter = $setter->bindTo( $this );
323 }
324 $setter( $value );
325 }
326}
328class_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.