diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 659345c20cfe6..02ad8046b5dff 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG * added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference according to [RFC 8674](https://tools.ietf.org/html/rfc8674) * made the Mime component an optional dependency + * added `Psr6SessionHandler` 5.0.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/Psr6SessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/Psr6SessionHandler.php new file mode 100644 index 0000000000000..622aa8973012a --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/Psr6SessionHandler.php @@ -0,0 +1,116 @@ + + * + * 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; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * Session handler that supports a PSR6 cache implementation. + * + * @author Tobias Nyholm + * @author Ahmed TAILOULOUTE + */ +class Psr6SessionHandler extends AbstractSessionHandler +{ + /** + * @var CacheItemPoolInterface + */ + private $cache; + + /** + * @var int Time to live in seconds + */ + private $ttl; + + /** + * @var string Key prefix for shared environments + */ + private $prefix; + + /** + * List of available options: + * * prefix: The prefix to use for the cache keys in order to avoid collision + * * ttl: The time to live in seconds. + * + * @param CacheItemPoolInterface $cache A Cache instance + * @param array $options An associative array of cache options + */ + public function __construct(CacheItemPoolInterface $cache, array $options = []) + { + $this->cache = $cache; + + if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { + throw new \InvalidArgumentException(sprintf('The following options are not supported by %s: "%s".', static::class, implode('", "', $diff))); + } + + $this->ttl = $options['ttl'] ?? null; + $this->prefix = $options['prefix'] ?? 'sf_s'; + } + + /** + * {@inheritdoc} + */ + protected function doRead(string $sessionId) + { + $item = $this->cache->getItem($this->prefix.$sessionId); + + return $item->isHit() ? $item->get() : ''; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data) + { + $item = $this->cache->getItem($this->prefix.$sessionId); + $item->set($data) + ->expiresAfter($this->ttl ?? ini_get('session.gc_maxlifetime')); + + return $this->cache->save($item); + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId) + { + return $this->cache->deleteItem($this->prefix.$sessionId); + } + + /** + * {@inheritdoc} + */ + public function close() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function gc($lifetime) + { + // not required here because cache will auto expire the records anyhow. + return true; + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + $cacheItem = $this->cache->getItem($this->prefix.$sessionId); + $cacheItem->expiresAfter((int) ($this->ttl ?? ini_get('session.gc_maxlifetime'))); + + return $this->cache->save($cacheItem); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Psr6SessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Psr6SessionHandlerTest.php new file mode 100644 index 0000000000000..747668e36bc01 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Psr6SessionHandlerTest.php @@ -0,0 +1,176 @@ + + * + * 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 PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\Psr6SessionHandler; + +/** + * @author Aaron Scherer + */ +class Psr6SessionHandlerTest extends TestCase +{ + const TTL = 100; + const PREFIX = 'pre'; + + /** + * @var Psr6SessionHandler + */ + private $handler; + + /** + * @var MockObject|CacheItemPoolInterface + */ + private $psr6; + + protected function setUp(): void + { + parent::setUp(); + + $this->psr6 = $this->getMockBuilder(Cache::class) + ->setMethods(['getItem', 'deleteItem', 'save']) + ->getMock(); + $this->handler = new Psr6SessionHandler($this->psr6, ['prefix' => self::PREFIX, 'ttl' => self::TTL]); + } + + public function testOpen() + { + $this->assertTrue($this->handler->open('foo', 'bar')); + } + + public function testClose() + { + $this->assertTrue($this->handler->close()); + } + + public function testGc() + { + $this->assertTrue($this->handler->gc(4711)); + } + + public function testReadMiss() + { + $item = $this->getItemMock(); + $item->expects($this->once()) + ->method('isHit') + ->willReturn(false); + $this->psr6->expects($this->once()) + ->method('getItem') + ->willReturn($item); + + $this->assertEquals('', $this->handler->read('foo')); + } + + public function testReadHit() + { + $item = $this->getItemMock(); + $item->expects($this->once()) + ->method('isHit') + ->willReturn(true); + $item->expects($this->once()) + ->method('get') + ->willReturn('bar'); + $this->psr6->expects($this->once()) + ->method('getItem') + ->willReturn($item); + + $this->assertEquals('bar', $this->handler->read('foo')); + } + + public function testWrite() + { + $item = $this->getItemMock(); + + $item->expects($this->once()) + ->method('set') + ->with('session value') + ->willReturnSelf(); + $item->expects($this->once()) + ->method('expiresAfter') + ->with(self::TTL) + ->willReturnSelf(); + + $this->psr6->expects($this->once()) + ->method('getItem') + ->with(self::PREFIX.'foo') + ->willReturn($item); + + $this->psr6->expects($this->once()) + ->method('save') + ->with($item) + ->willReturn(true); + + $this->assertTrue($this->handler->write('foo', 'session value')); + } + + public function testDestroy() + { + $this->psr6->expects($this->once()) + ->method('deleteItem') + ->with(self::PREFIX.'foo') + ->willReturn(true); + + $this->assertTrue($this->handler->destroy('foo')); + } + + /** + * @return MockObject + */ + private function getItemMock() + { + return $this->getMockBuilder(CacheItemInterface::class) + ->setMethods(['isHit', 'getKey', 'get', 'set', 'expiresAt', 'expiresAfter']) + ->getMock(); + } +} + +class Cache implements CacheItemPoolInterface +{ + public function getItem($key) + { + } + + public function getItems(array $keys = []) + { + } + + public function hasItem($key) + { + } + + public function clear() + { + } + + public function deleteItem($key) + { + } + + public function deleteItems(array $keys) + { + } + + public function save(CacheItemInterface $item) + { + } + + public function saveDeferred(CacheItemInterface $item) + { + } + + public function commit() + { + } +} diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index 9848c97fbb51e..28fd0259fc4f3 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -22,6 +22,7 @@ }, "require-dev": { "predis/predis": "~1.0", + "psr/cache": "~1.0", "symfony/mime": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy