From 191418d24dd9c3dbe37eb5b566eced1b3bac1212 Mon Sep 17 00:00:00 2001 From: Adrien Brault Date: Tue, 24 Sep 2013 15:02:41 -0700 Subject: [PATCH] [HttpFoundation] Add a way to avoid the session be written at each request --- .../DependencyInjection/Configuration.php | 4 + .../FrameworkExtension.php | 11 ++- .../Resources/config/session.xml | 14 +++ .../Handler/WriteCheckSessionHandler.php | 91 ++++++++++++++++++ .../Session/Storage/MetadataBag.php | 17 +++- .../Handler/WriteCheckSessionHandlerTest.php | 94 +++++++++++++++++++ .../Tests/Session/Storage/MetadataBagTest.php | 32 +++++++ 7 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php create mode 100644 src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 51b845c724..54b5117a11 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -222,6 +222,10 @@ class Configuration implements ConfigurationInterface ->scalarNode('gc_probability')->end() ->scalarNode('gc_maxlifetime')->end() ->scalarNode('save_path')->defaultValue('%kernel.cache_dir%/sessions')->end() + ->integerNode('metadata_update_threshold') + ->defaultValue('0') + ->info('time to wait between 2 session metadata updates, it will also prevent the session handler to write if the session has not changed') + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 32487952c8..00be6076fa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -333,7 +333,14 @@ class FrameworkExtension extends Extension $container->getDefinition('session.storage.native')->replaceArgument(1, null); $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); } else { - $container->setAlias('session.handler', $config['handler_id']); + $handlerId = $config['handler_id']; + + if ($config['metadata_update_threshold'] > 0) { + $container->getDefinition('session.handler.write_check')->addArgument(new Reference($handlerId)); + $handlerId = 'session.handler.write_check'; + } + + $container->setAlias('session.handler', $handlerId); } $container->setParameter('session.save_path', $config['save_path']); @@ -353,6 +360,8 @@ class FrameworkExtension extends Extension $container->findDefinition('session.storage')->getClass(), )); } + + $container->setParameter('session.metadata.update_threshold', $config['metadata_update_threshold']); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml index de45365751..873e099ff9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml @@ -8,10 +8,13 @@ Symfony\Component\HttpFoundation\Session\Session Symfony\Component\HttpFoundation\Session\Flash\FlashBag Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag + Symfony\Component\HttpFoundation\Session\Storage\MetadataBag + _sf2_meta Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler + Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler Symfony\Bundle\FrameworkBundle\EventListener\SessionListener @@ -22,13 +25,20 @@ + + %session.storage.options% + + @@ -37,12 +47,16 @@ %kernel.cache_dir%/sessions + MOCKSESSID + %session.save_path% + + diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php new file mode 100644 index 0000000000..fc76f19a13 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/WriteCheckSessionHandler.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Wraps another SessionHandlerInterface to only write the session when it has been modified. + * + * @author Adrien Brault + */ +class WriteCheckSessionHandler implements \SessionHandlerInterface +{ + /** + * @var \SessionHandlerInterface + */ + private $wrappedSessionHandler; + + /** + * @var array sessionId => session + */ + private $readSessions; + + public function __construct(\SessionHandlerInterface $wrappedSessionHandler) + { + $this->wrappedSessionHandler = $wrappedSessionHandler; + } + + /** + * {@inheritdoc} + */ + public function close() + { + return $this->wrappedSessionHandler->close(); + } + + /** + * {@inheritdoc} + */ + public function destroy($sessionId) + { + return $this->wrappedSessionHandler->destroy($sessionId); + } + + /** + * {@inheritdoc} + */ + public function gc($maxLifetime) + { + return $this->wrappedSessionHandler->gc($maxLifetime); + } + + /** + * {@inheritdoc} + */ + public function open($savePath, $sessionId) + { + return $this->wrappedSessionHandler->open($savePath, $sessionId); + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + $session = $this->wrappedSessionHandler->read($sessionId); + + $this->readSessions[$sessionId] = $session; + + return $session; + } + + /** + * {@inheritdoc} + */ + public function write($sessionId, $sessionData) + { + if (isset($this->readSessions[$sessionId]) && $sessionData === $this->readSessions[$sessionId]) { + return true; + } + + return $this->wrappedSessionHandler->write($sessionId, $sessionData); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php b/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php index 892d004b5d..9bbdd082f1 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php @@ -48,14 +48,21 @@ class MetadataBag implements SessionBagInterface */ private $lastUsed; + /** + * @var integer + */ + private $updateThreshold; + /** * Constructor. * - * @param string $storageKey The key used to store bag in the session. + * @param string $storageKey The key used to store bag in the session. + * @param integer $updateThreshold The time to wait between two UPDATED updates */ - public function __construct($storageKey = '_sf2_meta') + public function __construct($storageKey = '_sf2_meta', $updateThreshold = 0) { $this->storageKey = $storageKey; + $this->updateThreshold = $updateThreshold; $this->meta = array(self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0); } @@ -68,7 +75,11 @@ class MetadataBag implements SessionBagInterface if (isset($array[self::CREATED])) { $this->lastUsed = $this->meta[self::UPDATED]; - $this->meta[self::UPDATED] = time(); + + $timeStamp = time(); + if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) { + $this->meta[self::UPDATED] = $timeStamp; + } } else { $this->stampCreated(); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php new file mode 100644 index 0000000000..dcfd94bd1e --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/WriteCheckSessionHandlerTest.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler; + +/** + * @author Adrien Brault + */ +class WriteCheckSessionHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function test() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('close') + ->with() + ->will($this->returnValue(true)) + ; + + $this->assertEquals(true, $writeCheckSessionHandler->close()); + } + + public function testWrite() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('write') + ->with('foo', 'bar') + ->will($this->returnValue(true)) + ; + + $this->assertEquals(true, $writeCheckSessionHandler->write('foo', 'bar')); + } + + public function testSkippedWrite() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('read') + ->with('foo') + ->will($this->returnValue('bar')) + ; + + $wrappedSessionHandlerMock + ->expects($this->never()) + ->method('write') + ; + + $this->assertEquals('bar', $writeCheckSessionHandler->read('foo')); + $this->assertEquals(true, $writeCheckSessionHandler->write('foo', 'bar')); + } + + public function testNonSkippedWrite() + { + $wrappedSessionHandlerMock = $this->getMock('SessionHandlerInterface'); + $writeCheckSessionHandler = new WriteCheckSessionHandler($wrappedSessionHandlerMock); + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('read') + ->with('foo') + ->will($this->returnValue('bar')) + ; + + $wrappedSessionHandlerMock + ->expects($this->once()) + ->method('write') + ->with('foo', 'baZZZ') + ->will($this->returnValue(true)) + ; + + $this->assertEquals('bar', $writeCheckSessionHandler->read('foo')); + $this->assertEquals(true, $writeCheckSessionHandler->write('foo', 'baZZZ')); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php index ef70281ce0..c4b5a395e9 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php @@ -104,4 +104,36 @@ class MetadataBagTest extends \PHPUnit_Framework_TestCase { $this->bag->clear(); } + + public function testSkipLastUsedUpdate() + { + $bag = new MetadataBag('', 30); + $timeStamp = time(); + + $created = $timeStamp - 15; + $sessionMetadata = array( + MetadataBag::CREATED => $created, + MetadataBag::UPDATED => $created, + MetadataBag::LIFETIME => 1000 + ); + $bag->initialize($sessionMetadata); + + $this->assertEquals($created, $sessionMetadata[MetadataBag::UPDATED]); + } + + public function testDoesNotSkipLastUsedUpdate() + { + $bag = new MetadataBag('', 30); + $timeStamp = time(); + + $created = $timeStamp - 45; + $sessionMetadata = array( + MetadataBag::CREATED => $created, + MetadataBag::UPDATED => $created, + MetadataBag::LIFETIME => 1000 + ); + $bag->initialize($sessionMetadata); + + $this->assertEquals($timeStamp, $sessionMetadata[MetadataBag::UPDATED]); + } }