Skip to content

Commit

Permalink
[HttpFoundation] Add a way to avoid the session be written at each re…
Browse files Browse the repository at this point in the history
…quest
  • Loading branch information
adrienbrault committed Sep 30, 2013
1 parent 5ebaad3 commit 191418d
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 4 deletions.
Expand Up @@ -222,6 +222,10 @@ private function addSessionSection(ArrayNodeDefinition $rootNode)
->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()
Expand Down
Expand Up @@ -333,7 +333,14 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c
$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']);
Expand All @@ -353,6 +360,8 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c
$container->findDefinition('session.storage')->getClass(),
));
}

$container->setParameter('session.metadata.update_threshold', $config['metadata_update_threshold']);
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml
Expand Up @@ -8,10 +8,13 @@
<parameter key="session.class">Symfony\Component\HttpFoundation\Session\Session</parameter>
<parameter key="session.flashbag.class">Symfony\Component\HttpFoundation\Session\Flash\FlashBag</parameter>
<parameter key="session.attribute_bag.class">Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag</parameter>
<parameter key="session.storage.metadata_bag.class">Symfony\Component\HttpFoundation\Session\Storage\MetadataBag</parameter>
<parameter key="session.metadata.storage_key">_sf2_meta</parameter>
<parameter key="session.storage.native.class">Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage</parameter>
<parameter key="session.storage.php_bridge.class">Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorage</parameter>
<parameter key="session.storage.mock_file.class">Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorage</parameter>
<parameter key="session.handler.native_file.class">Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler</parameter>
<parameter key="session.handler.write_check.class">Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler</parameter>
<parameter key="session_listener.class">Symfony\Bundle\FrameworkBundle\EventListener\SessionListener</parameter>
</parameters>

Expand All @@ -22,13 +25,20 @@
<argument type="service" id="session.flash_bag" />
</service>

<service id="session.storage.metadata_bag" class="%session.storage.metadata_bag.class%" public="false">
<argument>%session.metadata.storage_key%</argument>
<argument>%session.metadata.update_threshold%</argument>
</service>

<service id="session.storage.native" class="%session.storage.native.class%">
<argument>%session.storage.options%</argument>
<argument type="service" id="session.handler" />
<argument type="service" id="session.storage.metadata_bag" />
</service>

<service id="session.storage.php_bridge" class="%session.storage.php_bridge.class%">
<argument type="service" id="session.handler" />
<argument type="service" id="session.storage.metadata_bag" />
</service>

<service id="session.flash_bag" class="%session.flashbag.class%" public="false" />
Expand All @@ -37,12 +47,16 @@

<service id="session.storage.mock_file" class="%session.storage.mock_file.class%" public="false">
<argument>%kernel.cache_dir%/sessions</argument>
<argument>MOCKSESSID</argument>
<argument type="service" id="session.storage.metadata_bag" />
</service>

<service id="session.handler.native_file" class="%session.handler.native_file.class%" public="false">
<argument>%session.save_path%</argument>
</service>

<service id="session.handler.write_check" class="%session.handler.write_check.class%" public="false" />

<service id="session_listener" class="%session_listener.class%">
<tag name="kernel.event_subscriber" />
<argument type="service" id="service_container" />
Expand Down
@@ -0,0 +1,91 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <adrien.brault@gmail.com>
*/
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);
}
}
Expand Up @@ -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);
}

Expand All @@ -68,7 +75,11 @@ public function initialize(array &$array)

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();
}
Expand Down
@@ -0,0 +1,94 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <adrien.brault@gmail.com>
*/
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'));
}
}
Expand Up @@ -104,4 +104,36 @@ public function testClear()
{
$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]);
}
}

0 comments on commit 191418d

Please sign in to comment.