MediaWiki master
DeprecationHelper.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Debug;
8
9use Error;
10use ReflectionFunction;
11use ReflectionProperty;
12
52trait DeprecationHelper {
53
65 protected static $deprecatedPublicProperties = [];
66
72 private $dynamicPropertiesAccessDeprecated = false;
73
88 protected function deprecatePublicProperty(
89 $property,
90 $version,
91 $class = null,
92 $component = null
93 ) {
94 if ( isset( self::$deprecatedPublicProperties[$property] ) ) {
95 return;
96 }
97 self::$deprecatedPublicProperties[$property] = [
98 $version,
99 $class ?: __CLASS__,
100 $component ?: false,
101 null,
102 null,
103 ];
104 }
105
124 string $property,
125 string $version,
126 $getter,
127 $setter = null,
128 $class = null,
129 $component = null
130 ) {
131 if ( isset( self::$deprecatedPublicProperties[$property] ) ) {
132 return;
133 }
134 self::$deprecatedPublicProperties[$property] = [
135 $version,
136 $class ?: __CLASS__,
137 $component ?: false,
138 $getter,
139 $setter,
140 ];
141 }
142
152 string $version,
153 ?string $class = null,
154 ?string $component = null
155 ) {
156 $this->dynamicPropertiesAccessDeprecated = [ $version, $class ?: __CLASS__, $component ];
157 }
158
159 public function __isset( $name ) {
160 // Overriding magic __isset is required not only for isset() and empty(),
161 // but to correctly support null coalescing for dynamic properties,
162 // e.g. $foo->bar ?? 'default'
163 if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
164 [ $version, $class, $component, $getter ] = self::$deprecatedPublicProperties[$name];
165 $qualifiedName = $class . '::$' . $name;
166 wfDeprecated( $qualifiedName, $version, $component, 2 );
167 if ( $getter ) {
168 return $this->deprecationHelperCallGetter( $getter );
169 }
170 return true;
171 }
172
173 $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
174 if ( $ownerClass ) {
175 // Someone tried to access a normal non-public property. Try to behave like PHP would.
176 return false;
177 } else {
178 if ( $this->dynamicPropertiesAccessDeprecated ) {
179 [ $version, $class, $component ] = $this->dynamicPropertiesAccessDeprecated;
180 $qualifiedName = $class . '::$' . $name;
181 wfDeprecated( $qualifiedName, $version, $component, 2 );
182 }
183 return false;
184 }
185 }
186
187 public function __get( $name ) {
188 if ( get_object_vars( $this ) === [] ) {
189 // Object is being destructed, all bets are off (T363492);
190 // in particular, we can't check $this->dynamicPropertiesAccessDeprecated anymore.
191 // Just get the property and hope for the best...
192 return $this->$name;
193 }
194
195 if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
196 [ $version, $class, $component, $getter ] = self::$deprecatedPublicProperties[$name];
197 $qualifiedName = $class . '::$' . $name;
198 wfDeprecated( $qualifiedName, $version, $component, 2 );
199 if ( $getter ) {
200 return $this->deprecationHelperCallGetter( $getter );
201 }
202 return $this->$name;
203 }
204
205 $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
206 $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
207 if ( $ownerClass ) {
208 // Someone tried to access a normal non-public property. Try to behave like PHP would.
209 throw new Error( "Cannot access non-public property $qualifiedName" );
210 } elseif ( property_exists( $this, $name ) ) {
211 // Normally __get method will not be even called if the property exists,
212 // but in tests if we mock an object that uses DeprecationHelper,
213 // __get and __set magic methods will be mocked as well, and called
214 // regardless of the property existence. Support that use-case.
215 return $this->$name;
216 } else {
217 // Non-existing property. Try to behave like PHP would.
218 trigger_error( "Undefined property: $qualifiedName", E_USER_NOTICE );
219 }
220 return null;
221 }
222
223 public function __set( $name, $value ) {
224 if ( get_object_vars( $this ) === [] ) {
225 // Object is being destructed, all bets are off (T363492);
226 // in particular, we can't check $this->dynamicPropertiesAccessDeprecated anymore.
227 // Just set the property and hope for the best...
228 $this->$name = $value;
229 return;
230 }
231
232 if ( isset( self::$deprecatedPublicProperties[$name] ) ) {
233 [ $version, $class, $component, , $setter ] = self::$deprecatedPublicProperties[$name];
234 $qualifiedName = $class . '::$' . $name;
235 wfDeprecated( $qualifiedName, $version, $component, 2 );
236 if ( $setter ) {
237 $this->deprecationHelperCallSetter( $setter, $value );
238 } elseif ( property_exists( $this, $name ) ) {
239 $this->$name = $value;
240 } else {
241 throw new Error( "Cannot access non-public property $qualifiedName" );
242 }
243 return;
244 }
245
246 $ownerClass = $this->deprecationHelperGetPropertyOwner( $name );
247 $qualifiedName = ( $ownerClass ?: get_class( $this ) ) . '::$' . $name;
248 if ( $ownerClass ) {
249 // Someone tried to access a normal non-public property. Try to behave like PHP would.
250 throw new Error( "Cannot access non-public property $qualifiedName" );
251 } else {
252 if ( $this->dynamicPropertiesAccessDeprecated ) {
253 [ $version, $class, $component ] = $this->dynamicPropertiesAccessDeprecated;
254 $qualifiedName = $class . '::$' . $name;
255 wfDeprecated( $qualifiedName, $version, $component, 2 );
256 }
257 // Non-existing property. Try to behave like PHP would.
258 $this->$name = $value;
259 }
260 }
261
269 private function deprecationHelperGetPropertyOwner( $property ) {
270 // Returning false is a non-error path and should avoid slow checks like reflection.
271 // Use array cast hack instead.
272 $obfuscatedProps = array_keys( (array)$this );
273 $obfuscatedPropTail = "\0$property";
274 foreach ( $obfuscatedProps as $obfuscatedProp ) {
275 // private props are in the form \0<classname>\0<propname>
276 if ( strpos( $obfuscatedProp, $obfuscatedPropTail, 1 ) !== false ) {
277 $classname = substr( $obfuscatedProp, 1, -strlen( $obfuscatedPropTail ) );
278 if ( $classname === '*' ) {
279 // protected property; we didn't get the name, but we are on an error path
280 // now so it's fine to use reflection
281 return ( new ReflectionProperty( $this, $property ) )->getDeclaringClass()->getName();
282 }
283 return $classname;
284 }
285 }
286 return false;
287 }
288
293 private function deprecationHelperCallGetter( $getter ) {
294 if ( is_string( $getter ) ) {
295 $getter = [ $this, $getter ];
296 } elseif ( ( new ReflectionFunction( $getter ) )->getClosureThis() !== null ) {
297 $getter = $getter->bindTo( $this );
298 }
299 return $getter();
300 }
301
306 private function deprecationHelperCallSetter( $setter, $value ) {
307 if ( is_string( $setter ) ) {
308 $setter = [ $this, $setter ];
309 } elseif ( ( new ReflectionFunction( $setter ) )->getClosureThis() !== null ) {
310 $setter = $setter->bindTo( $this );
311 }
312 $setter( $value );
313 }
314}
316class_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.