StubObject: add magic __get() and __set()

We will soon be converting $wgUser to be a stub
object wrapping a User, and we want to be able
to access the public properties of that object
(doing so will still trigger deprecation warnings,
but it won't break). This will also work for
non-existent properties that are handled via
the __get() and __set() methods of whatever
inner object the StubObject is wrapping.

Bug: T267861
Change-Id: I4c29c615bcb107d4ef8bf4b8e48db2ecf863e5f7
This commit is contained in:
DannyS712 2021-07-02 04:08:04 +00:00
parent d28c8785e9
commit bd73c48744
3 changed files with 263 additions and 1 deletions

View file

@ -39,14 +39,18 @@ class DeprecatedGlobal extends StubObject {
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
public function _newObject() {
/* Put the caller offset for wfDeprecated as 6, as
/*
* Put the caller offset for wfDeprecated as 6, as
* that gives the function that uses this object, since:
*
* 1 = this function ( _newObject )
* 2 = StubObject::_unstub
* 3 = StubObject::_call
* 4 = StubObject::__call
* 5 = DeprecatedGlobal::<method of global called>
* 6 = Actual function using the global.
* (the same applies to _get/__get or _set/__set instead of _call/__call)
*
* Of course its theoretically possible to have other call
* sequences for this method, but that seems to be
* rather unlikely.

View file

@ -149,6 +149,50 @@ class StubObject {
return $this->_call( $name, $args );
}
/**
* Wrapper for __get(), similar to _call() above
*
* @param string $name Name of the property to get
* @return mixed
*/
public function _get( $name ) {
$this->_unstub( "__get($name)", 5 );
return $GLOBALS[$this->global]->$name;
}
/**
* Function called by PHP if no property with that name exists in this
* object.
*
* @param string $name Name of the property to get
* @return mixed
*/
public function __get( $name ) {
return $this->_get( $name );
}
/**
* Wrapper for __set(), similar to _call() above
*
* @param string $name Name of the property to set
* @param mixed $value New property value
*/
public function _set( $name, $value ) {
$this->_unstub( "__set($name)", 5 );
$GLOBALS[$this->global]->$name = $value;
}
/**
* Function called by PHP if no property with that name exists in this
* object.
*
* @param string $name Name of the property to set
* @param mixed $value New property value
*/
public function __set( $name, $value ) {
$this->_set( $name, $value );
}
/**
* This function creates a new object of the real class and replace it in
* the global variable.

View file

@ -0,0 +1,214 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Testing the magic for __get(), __set(), and __call() for our
* example global, $wgDummy, which would be an instance
* of DemoStubbed but is wrapped in a StubObject
* @author DannyS712
*
* @covers \StubObject
*/
class StubObjectTest extends MediaWikiIntegrationTestCase {
/** @var int */
private $oldErrorLevel;
protected function setUp(): void {
parent::setUp();
// Make sure deprecation notices are seen
$this->oldErrorLevel = error_reporting( -1 );
global $wgDummy;
$wgDummy = new StubObject(
'wgDummy',
[ __CLASS__, 'factory' ]
);
}
protected function tearDown(): void {
error_reporting( $this->oldErrorLevel );
parent::tearDown();
}
/**
* Static factory method for creating the underlying global, which is
* a DemoStubbed with the starting value of 5
*
* @return DemoStubbed
*/
public static function factory(): DemoStubbed {
return new DemoStubbed( 5 );
}
public function testCallMagic() {
global $wgDummy;
$this->assertInstanceOf(
StubObject::class,
$wgDummy,
'Global starts as stub object'
);
$this->assertSame(
5,
$wgDummy->getNum(),
'__call() based on id set in ::setUp()'
);
$this->assertInstanceOf(
DemoStubbed::class,
$wgDummy,
'__call() resulted in unstubbing'
);
}
public function testGetMagic() {
// StubObject::__get() returning DemoStubbed::$num
global $wgDummy;
$this->assertInstanceOf(
StubObject::class,
$wgDummy,
'Global starts as stub object'
);
$this->assertSame(
5,
$wgDummy->num,
'__get() based on id set in ::setUp()'
);
$this->assertInstanceOf(
DemoStubbed::class,
$wgDummy,
'__get() resulted in unstubbing'
);
}
public function testGetMagic_virtual() {
// StubObject::__get() calling DemoStubbed::__get()
global $wgDummy;
$this->assertInstanceOf(
StubObject::class,
$wgDummy,
'Global starts as stub object'
);
$this->assertSame(
10,
$wgDummy->doubleNum,
'__get() a virtual property based on id set in ::setUp()'
);
$this->assertInstanceOf(
DemoStubbed::class,
$wgDummy,
'__get() resulted in unstubbing'
);
}
public function testSetMagic() {
// StubObject::__set() changing DemoStubbed::$num
global $wgDummy;
$this->assertInstanceOf(
StubObject::class,
$wgDummy,
'Global starts as stub object'
);
$wgDummy->num = 100;
$this->assertInstanceOf(
DemoStubbed::class,
$wgDummy,
'__set() resulted in unstubbing'
);
$this->assertSame(
100,
$wgDummy->num,
'__set() changed the value'
);
}
public function testSetMagic_virtual() {
// StubObject::__set() calling DemoStubbed::__set()
global $wgDummy;
$this->assertInstanceOf(
StubObject::class,
$wgDummy,
'Global starts as stub object'
);
$wgDummy->doubleNum = 100;
$this->assertInstanceOf(
DemoStubbed::class,
$wgDummy,
'__set() resulted in unstubbing'
);
$this->assertSame(
50,
$wgDummy->num,
'__set() changed the value'
);
}
}
/**
* This is the object that we are stubbing so we can test the various magic methods
*/
class DemoStubbed {
/** @var int */
public $num;
/**
* @param int $num
*/
public function __construct( int $num ) {
$this->num = $num;
}
/**
* @return int
*/
public function getNum(): int {
return $this->num;
}
/**
* Magic handling for retrieving fake property "doubleNum"
*
* @param string $field
* @return mixed
*/
public function __get( $field ) {
if ( $field === 'doubleNum' ) {
return ( 2 * $this->num );
}
trigger_error( 'Inaccessible property via __get(): ' . $field, E_USER_NOTICE );
}
/**
* Magic handling for setting fake property "doubleNum"
*
* @param string $field
* @param mixed $value
*/
public function __set( $field, $value ) {
if ( $field === 'doubleNum' ) {
$this->num = (int)( $value / 2 );
return;
}
trigger_error( 'Inaccessible property via __set(): ' . $field, E_USER_NOTICE );
}
}