diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 51b845c724..3cadf0d5a8 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('seconds 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 915269919c..efc2e968d4 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -342,7 +342,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']);
@@ -362,6 +369,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.metadata.storage_key%
+ %session.metadata.update_threshold%
+
+
%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 8f87884e70..c502c38ae6 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php
@@ -100,4 +100,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]);
+ }
}