jenkins-bot has submitted this change and it was merged. Change subject: Session: Implement ArrayAccess ......................................................................
Session: Implement ArrayAccess Now that we dropped support for PHP 5.3.3, we can do this. The behavior of $session['foo'] when that key doesn't already exist is a little unexpected (it implicitly assigns null), but it's the best we can do. Change-Id: Ibef878867d46591a8bf542139a1719dfec3b83ab (cherry picked from commit 1aedd2df73ad2be69fb24148bfaff31c118fddba) --- M includes/session/Session.php M includes/session/SessionBackend.php M tests/phpunit/includes/session/SessionBackendTest.php M tests/phpunit/includes/session/SessionTest.php M tests/phpunit/includes/session/TestUtils.php 5 files changed, 120 insertions(+), 8 deletions(-) Approvals: Krinkle: Looks good to me, approved jenkins-bot: Verified diff --git a/includes/session/Session.php b/includes/session/Session.php index 4ad69ae..d654ff1 100644 --- a/includes/session/Session.php +++ b/includes/session/Session.php @@ -23,6 +23,7 @@ namespace MediaWiki\Session; +use Psr\Log\LoggerInterface; use User; use WebRequest; @@ -41,24 +42,28 @@ * The Session object also serves as a replacement for PHP's $_SESSION, * managing access to per-session data. * - * @todo Once we drop support for PHP 5.3.3, implementing ArrayAccess would be nice. * @ingroup Session * @since 1.27 */ -final class Session implements \Countable, \Iterator { +final class Session implements \Countable, \Iterator, \ArrayAccess { /** @var SessionBackend Session backend */ private $backend; /** @var int Session index */ private $index; + /** @var LoggerInterface */ + private $logger; + /** * @param SessionBackend $backend * @param int $index + * @param LoggerInterface $logger */ - public function __construct( SessionBackend $backend, $index ) { + public function __construct( SessionBackend $backend, $index, LoggerInterface $logger ) { $this->backend = $backend; $this->index = $index; + $this->logger = $logger; } public function __destruct() { @@ -271,7 +276,7 @@ /** * Fetch a value from the session * @param string|int $key - * @param mixed $default + * @param mixed $default Returned if $this->exists( $key ) would be false * @return mixed */ public function get( $key, $default = null ) { @@ -281,6 +286,7 @@ /** * Test if a value exists in the session + * @note Unlike isset(), null values are considered to exist. * @param string|int $key * @return bool */ @@ -419,6 +425,39 @@ return key( $data ) !== null; } + /** + * @note Despite the name, this seems to be intended to implement isset() + * rather than array_key_exists(). So do that. + */ + public function offsetExists( $offset ) { + $data = &$this->backend->getData(); + return isset( $data[$offset] ); + } + + /** + * @note This supports indirect modifications but can't mark the session + * dirty when those happen. SessionBackend::save() checks the hash of the + * data to detect such changes. + * @note Accessing a nonexistent key via this mechanism causes that key to + * be created with a null value, and does not raise a PHP warning. + */ + public function &offsetGet( $offset ) { + $data = &$this->backend->getData(); + if ( !array_key_exists( $offset, $data ) ) { + $ex = new \Exception( "Undefined index (auto-adds to session with a null value): $offset" ); + $this->logger->debug( $ex->getMessage(), array( 'exception' => $ex ) ); + } + return $data[$offset]; + } + + public function offsetSet( $offset, $value ) { + $this->set( $offset, $value ); + } + + public function offsetUnset( $offset ) { + $this->remove( $offset ); + } + /**@}*/ } diff --git a/includes/session/SessionBackend.php b/includes/session/SessionBackend.php index fe446e3..5cf7869 100644 --- a/includes/session/SessionBackend.php +++ b/includes/session/SessionBackend.php @@ -170,7 +170,7 @@ public function getSession( WebRequest $request ) { $index = ++$this->curIndex; $this->requests[$index] = $request; - $session = new Session( $this, $index ); + $session = new Session( $this, $index, $this->logger ); return $session; } diff --git a/tests/phpunit/includes/session/SessionBackendTest.php b/tests/phpunit/includes/session/SessionBackendTest.php index f08a07d..481b693 100644 --- a/tests/phpunit/includes/session/SessionBackendTest.php +++ b/tests/phpunit/includes/session/SessionBackendTest.php @@ -569,6 +569,7 @@ 'making sure it did save to backend' ); // Not marked dirty, but dirty data + // (e.g. indirect modification from ArrayAccess::offsetGet) $this->provider = $neverProvider; $this->onSessionMetadataCalled = false; $this->mergeMwGlobalArrayValue( 'wgHooks', array( 'SessionMetadata' => array( $this ) ) ); diff --git a/tests/phpunit/includes/session/SessionTest.php b/tests/phpunit/includes/session/SessionTest.php index 858996d..3c5090a 100644 --- a/tests/phpunit/includes/session/SessionTest.php +++ b/tests/phpunit/includes/session/SessionTest.php @@ -2,6 +2,7 @@ namespace MediaWiki\Session; +use Psr\Log\LogLevel; use MediaWikiTestCase; use User; @@ -16,7 +17,7 @@ \TestingAccessWrapper::newFromObject( $backend )->requests = array( -1 => 'dummy' ); \TestingAccessWrapper::newFromObject( $backend )->id = new SessionId( 'abc' ); - $session = new Session( $backend, 42 ); + $session = new Session( $backend, 42, new \TestLogger ); $priv = \TestingAccessWrapper::newFromObject( $session ); $this->assertSame( $backend, $priv->backend ); $this->assertSame( 42, $priv->index ); @@ -152,6 +153,71 @@ $this->assertFalse( $backend->dirty ); } + public function testArrayAccess() { + $logger = new \TestLogger; + $session = TestUtils::getDummySession( null, -1, $logger ); + $backend = \TestingAccessWrapper::newFromObject( $session )->backend; + + $this->assertEquals( 1, $session['foo'] ); + $this->assertEquals( 'zero', $session[0] ); + $this->assertFalse( $backend->dirty ); + + $logger->setCollect( true ); + $this->assertEquals( null, $session['null'] ); + $logger->setCollect( false ); + $this->assertFalse( $backend->dirty ); + $this->assertSame( array( + array( LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): null' ) + ), $logger->getBuffer() ); + $logger->clearBuffer(); + + $session['foo'] = 55; + $this->assertEquals( 55, $backend->data['foo'] ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + $session[1] = 'one'; + $this->assertEquals( 'one', $backend->data[1] ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + $session[1] = 'one'; + $this->assertFalse( $backend->dirty ); + + $session['bar'] = array( 'baz' => array() ); + $session['bar']['baz']['quux'] = 2; + $this->assertEquals( array( 'baz' => array( 'quux' => 2 ) ), $backend->data['bar'] ); + + $logger->setCollect( true ); + $session['bar2']['baz']['quux'] = 3; + $logger->setCollect( false ); + $this->assertEquals( array( 'baz' => array( 'quux' => 3 ) ), $backend->data['bar2'] ); + $this->assertSame( array( + array( LogLevel::DEBUG, 'Undefined index (auto-adds to session with a null value): bar2' ) + ), $logger->getBuffer() ); + $logger->clearBuffer(); + + $backend->dirty = false; + $this->assertTrue( isset( $session['foo'] ) ); + $this->assertTrue( isset( $session[1] ) ); + $this->assertFalse( isset( $session['null'] ) ); + $this->assertFalse( isset( $session['missing'] ) ); + $this->assertFalse( isset( $session[100] ) ); + $this->assertFalse( $backend->dirty ); + + unset( $session['foo'] ); + $this->assertArrayNotHasKey( 'foo', $backend->data ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + unset( $session[1] ); + $this->assertArrayNotHasKey( 1, $backend->data ); + $this->assertTrue( $backend->dirty ); + $backend->dirty = false; + + unset( $session[101] ); + $this->assertFalse( $backend->dirty ); + } + public function testClear() { $session = TestUtils::getDummySession(); $priv = \TestingAccessWrapper::newFromObject( $session ); diff --git a/tests/phpunit/includes/session/TestUtils.php b/tests/phpunit/includes/session/TestUtils.php index 1619983..cc20ab5 100644 --- a/tests/phpunit/includes/session/TestUtils.php +++ b/tests/phpunit/includes/session/TestUtils.php @@ -2,6 +2,8 @@ namespace MediaWiki\Session; +use Psr\Log\LoggerInterface; + /** * Utility functions for Session unit tests */ @@ -67,7 +69,9 @@ ); } - return $rc->newInstanceWithoutConstructor(); + $ret = $rc->newInstanceWithoutConstructor(); + \TestingAccessWrapper::newFromObject( $ret )->logger = new \TestLogger; + return $ret; } /** @@ -75,9 +79,10 @@ * construct one, use this. * @param object $backend Object to serve as the SessionBackend * @param int $index Index + * @param LoggerInterface $logger * @return Session */ - public static function getDummySession( $backend = null, $index = -1 ) { + public static function getDummySession( $backend = null, $index = -1, $logger = null ) { $rc = new \ReflectionClass( 'MediaWiki\\Session\\Session' ); if ( !method_exists( $rc, 'newInstanceWithoutConstructor' ) ) { \PHPUnit_Framework_Assert::markTestSkipped( @@ -93,6 +98,7 @@ $priv = \TestingAccessWrapper::newFromObject( $session ); $priv->backend = $backend; $priv->index = $index; + $priv->logger = $logger ?: new \TestLogger; return $session; } -- To view, visit https://gerrit.wikimedia.org/r/272827 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ibef878867d46591a8bf542139a1719dfec3b83ab Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: wmf/1.27.0-wmf.14 Gerrit-Owner: Gergő Tisza <gti...@wikimedia.org> Gerrit-Reviewer: Anomie <bjor...@wikimedia.org> Gerrit-Reviewer: Krinkle <krinklem...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits