Skip to content

Commit c362c2a

Browse files
Code Modernization: Deprecate dynamic properties in WP_List_Table magic methods.
The unknown use of unknown dynamic property within the `WP_List_Table` property magic methods is now deprecated. A descriptive deprecation notice is provided to alert developers to declare the property on the child class extending `WP_List_Table`. Changes in this commit: * Adds a deprecation notice to the `__get()`, `__set()`, `__isset()`, `__unset()` magic methods, i.e. to alert and inform developers when attempting to get/set/isset/unset a dynamic property. * Fixes `__get()` to explicitly returns `null` when attempting to get a dynamic property. * Removes returning the value when setting a declared property, as (a) unnecessary and (b) `__set()` should return `void` [https://www.php.net/manual/en/language.oop5.overloading.php#object.set per the PHP handbook]. * Adds unit tests for happy and unhappy paths. For backward compatibility, no changes are made to the internal declared properties listed in `$compat_fields` and accessed through the magic methods. For example: A child class uses a property named `$data` that is not declared / defined as a property on the child class. When getting its value, e.g. `$list_table->data`, the `WP_List_Table::__get()` magic method is invoked, the following deprecation notice thrown, and `null` returned: >The property `data` is not defined. Setting a dynamic (undefined) property is deprecated since version 6.4.0! Instead, define the property on the class. === Why not remove the magic methods, remove the `$compat_fields` property, and restore the properties `public`? tl;dr Backward compatibility. Several plugins, one of which has over 5M installs, add a property to the `$compat_fields` array. Removing the property would cause an `Undefined property` `Warning` (PHP 8) | `Notice` (PHP 7) to be thrown. Removing the associated code would change the functionality. === Why not limit the deprecation for PHP versions >= 8.2? tl;dr original design intent and inform The magic methods and `$compat_fields` property were added for one purpose: to continue providing external access to internal properties declared on `WP_List_Table`. They were not intended to be used for dynamic properties. Deprecating that unintended usage both alerts developers a change is needed in their child class and informs them what to change. References: * Dynamic (non-explicitly declared) properties are deprecated as of PHP 8.2 and are expected to become a fatal error in PHP 9.0. * A [https://www.youtube.com/live/vDZWepDQQVE?feature=share&t=10097 live open public working session] where these changes were discussed and agreed to. * [https://wiki.php.net/rfc/deprecate_dynamic_properties PHP RFC: Deprecate dynamic properties.] Related to #14579, #22234, #30891. Follow-up to [15491], [28493], [28521], [28524], [31146]. Props antonvlasenko, jrf, markjaquith, hellofromTonya, SergeyBiryukov, desrosj, peterwilsoncc, audrasjb, costdev, oglekler, jeffpaul. Fixes #58896. See #56034. git-svn-id: https://develop.svn.wordpress.org/trunk@56349 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 88570fd commit c362c2a

2 files changed

Lines changed: 187 additions & 2 deletions

File tree

src/wp-admin/includes/class-wp-list-table.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ public function __construct( $args = array() ) {
176176
* Makes private properties readable for backward compatibility.
177177
*
178178
* @since 4.0.0
179+
* @since 6.4.0 Getting a dynamic property is deprecated.
179180
*
180181
* @param string $name Property to get.
181182
* @return mixed Property.
@@ -184,27 +185,42 @@ public function __get( $name ) {
184185
if ( in_array( $name, $this->compat_fields, true ) ) {
185186
return $this->$name;
186187
}
188+
189+
trigger_error(
190+
"The property `{$name}` is not defined. Getting a dynamic (undefined) property is " .
191+
'deprecated since version 6.4.0! Instead, define the property on the class.',
192+
E_USER_DEPRECATED
193+
);
194+
return null;
187195
}
188196

189197
/**
190198
* Makes private properties settable for backward compatibility.
191199
*
192200
* @since 4.0.0
201+
* @since 6.4.0 Setting a dynamic property is deprecated.
193202
*
194203
* @param string $name Property to check if set.
195204
* @param mixed $value Property value.
196-
* @return mixed Newly-set property.
197205
*/
198206
public function __set( $name, $value ) {
199207
if ( in_array( $name, $this->compat_fields, true ) ) {
200-
return $this->$name = $value;
208+
$this->$name = $value;
209+
return;
201210
}
211+
212+
trigger_error(
213+
"The property `{$name}` is not defined. Setting a dynamic (undefined) property is " .
214+
'deprecated since version 6.4.0! Instead, define the property on the class.',
215+
E_USER_DEPRECATED
216+
);
202217
}
203218

204219
/**
205220
* Makes private properties checkable for backward compatibility.
206221
*
207222
* @since 4.0.0
223+
* @since 6.4.0 Checking a dynamic property is deprecated.
208224
*
209225
* @param string $name Property to check if set.
210226
* @return bool Whether the property is a back-compat property and it is set.
@@ -214,20 +230,33 @@ public function __isset( $name ) {
214230
return isset( $this->$name );
215231
}
216232

233+
trigger_error(
234+
"The property `{$name}` is not defined. Checking `isset()` on a dynamic (undefined) property " .
235+
'is deprecated since version 6.4.0! Instead, define the property on the class.',
236+
E_USER_DEPRECATED
237+
);
217238
return false;
218239
}
219240

220241
/**
221242
* Makes private properties un-settable for backward compatibility.
222243
*
223244
* @since 4.0.0
245+
* @since 6.4.0 Unsetting a dynamic property is deprecated.
224246
*
225247
* @param string $name Property to unset.
226248
*/
227249
public function __unset( $name ) {
228250
if ( in_array( $name, $this->compat_fields, true ) ) {
229251
unset( $this->$name );
252+
return;
230253
}
254+
255+
trigger_error(
256+
"A property `{$name}` is not defined. Unsetting a dynamic (undefined) property is " .
257+
'deprecated since version 6.4.0! Instead, define the property on the class.',
258+
E_USER_DEPRECATED
259+
);
231260
}
232261

233262
/**

tests/phpunit/tests/admin/wpListTable.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,160 @@ public function data_get_views_links_doing_it_wrong() {
361361
),
362362
);
363363
}
364+
365+
/**
366+
* @dataProvider data_compat_fields
367+
* @ticket 58896
368+
*
369+
* @covers WP_List_Table::__get()
370+
*
371+
* @param string $property_name Property name to get.
372+
* @param mixed $expected Expected value.
373+
*/
374+
public function test_should_get_compat_fields_defined_property( $property_name, $expected ) {
375+
$list_table = new WP_List_Table( array( 'plural' => '_wp_tests__get' ) );
376+
377+
if ( 'screen' === $property_name ) {
378+
$this->assertInstanceOf( $expected, $list_table->$property_name );
379+
} else {
380+
$this->assertSame( $expected, $list_table->$property_name );
381+
}
382+
}
383+
384+
/**
385+
* @ticket 58896
386+
*
387+
* @covers WP_List_Table::__get()
388+
*/
389+
public function test_should_throw_deprecation_when_getting_dynamic_property() {
390+
$this->expectDeprecation();
391+
$this->expectDeprecationMessage(
392+
'The property `undefined_property` is not defined. Getting a dynamic (undefined) property is ' .
393+
'deprecated since version 6.4.0! Instead, define the property on the class.'
394+
);
395+
$this->assertNull( $this->list_table->undefined_property, 'Getting a dynamic property should return null from WP_List_Table::__get()' );
396+
}
397+
398+
/**
399+
* @dataProvider data_compat_fields
400+
* @ticket 58896
401+
*
402+
* @covers WP_List_Table::__set()
403+
*
404+
* @param string $property_name Property name to set.
405+
*/
406+
public function test_should_set_compat_fields_defined_property( $property_name ) {
407+
$value = uniqid();
408+
$this->list_table->$property_name = $value;
409+
410+
$this->assertSame( $value, $this->list_table->$property_name );
411+
}
412+
413+
/**
414+
* @ticket 58896
415+
*
416+
* @covers WP_List_Table::__set()
417+
*/
418+
public function test_should_throw_deprecation_when_setting_dynamic_property() {
419+
$this->expectDeprecation();
420+
$this->expectDeprecationMessage(
421+
'The property `undefined_property` is not defined. Setting a dynamic (undefined) property is ' .
422+
'deprecated since version 6.4.0! Instead, define the property on the class.'
423+
);
424+
$this->list_table->undefined_property = 'some value';
425+
}
426+
427+
/**
428+
* @dataProvider data_compat_fields
429+
* @ticket 58896
430+
*
431+
* @covers WP_List_Table::__isset()
432+
*
433+
* @param string $property_name Property name to check.
434+
* @param mixed $expected Expected value.
435+
*/
436+
public function test_should_isset_compat_fields_defined_property( $property_name, $expected ) {
437+
$actual = isset( $this->list_table->$property_name );
438+
if ( is_null( $expected ) ) {
439+
$this->assertFalse( $actual );
440+
} else {
441+
$this->assertTrue( $actual );
442+
}
443+
}
444+
445+
/**
446+
* @ticket 58896
447+
*
448+
* @covers WP_List_Table::__isset()
449+
*/
450+
public function test_should_throw_deprecation_when_isset_of_dynamic_property() {
451+
$this->expectDeprecation();
452+
$this->expectDeprecationMessage(
453+
'The property `undefined_property` is not defined. Checking `isset()` on a dynamic (undefined) property ' .
454+
'is deprecated since version 6.4.0! Instead, define the property on the class.'
455+
);
456+
$this->assertFalse( isset( $this->list_table->undefined_property ), 'Checking a dyanmic property should return false from WP_List_Table::__isset()' );
457+
}
458+
459+
/**
460+
* @dataProvider data_compat_fields
461+
* @ticket 58896
462+
*
463+
* @covers WP_List_Table::__unset()
464+
*
465+
* @param string $property_name Property name to unset.
466+
*/
467+
public function test_should_unset_compat_fields_defined_property( $property_name ) {
468+
unset( $this->list_table->$property_name );
469+
$this->assertFalse( isset( $this->list_table->$property_name ) );
470+
}
471+
472+
/**
473+
* @ticket 58896
474+
*
475+
* @covers WP_List_Table::__unset()
476+
*/
477+
public function test_should_throw_deprecation_when_unset_of_dynamic_property() {
478+
$this->expectDeprecation();
479+
$this->expectDeprecationMessage(
480+
'A property `undefined_property` is not defined. Unsetting a dynamic (undefined) property is ' .
481+
'deprecated since version 6.4.0! Instead, define the property on the class.'
482+
);
483+
unset( $this->list_table->undefined_property );
484+
}
485+
486+
/**
487+
* Data provider.
488+
*
489+
* @return array
490+
*/
491+
public function data_compat_fields() {
492+
return array(
493+
'_args' => array(
494+
'property_name' => '_args',
495+
'expected' => array(
496+
'plural' => '_wp_tests__get',
497+
'singular' => '',
498+
'ajax' => false,
499+
'screen' => null,
500+
),
501+
),
502+
'_pagination_args' => array(
503+
'property_name' => '_pagination_args',
504+
'expected' => array(),
505+
),
506+
'screen' => array(
507+
'property_name' => 'screen',
508+
'expected' => WP_Screen::class,
509+
),
510+
'_actions' => array(
511+
'property_name' => '_actions',
512+
'expected' => null,
513+
),
514+
'_pagination' => array(
515+
'property_name' => '_pagination',
516+
'expected' => null,
517+
),
518+
);
519+
}
364520
}

0 commit comments

Comments
 (0)