Session: Added DatabaseHandler, replaces Symfony PdoSessionHandler

This commit is contained in:
Igor Scheller 2018-09-16 14:08:09 +02:00
parent 104e4f4c43
commit 0b0890f425
11 changed files with 460 additions and 61 deletions

View File

@ -17,12 +17,14 @@ class CreateLogEntriesTable extends Migration
$table->timestamp('created_at')->nullable(); $table->timestamp('created_at')->nullable();
}); });
$this->schema->getConnection()->unprepared(' if ($this->schema->hasTable('LogEntries')) {
INSERT INTO log_entries (`id`, `level`, `message`, `created_at`) $this->schema->getConnection()->unprepared('
SELECT `id`, `level`, `message`, FROM_UNIXTIME(`timestamp`) FROM LogEntries INSERT INTO log_entries (`id`, `level`, `message`, `created_at`)
'); SELECT `id`, `level`, `message`, FROM_UNIXTIME(`timestamp`) FROM LogEntries
');
$this->schema->dropIfExists('LogEntries'); $this->schema->drop('LogEntries');
}
} }
/** /**

View File

@ -13,8 +13,7 @@ class CreateSessionsTable extends Migration
$this->schema->create('sessions', function (Blueprint $table) { $this->schema->create('sessions', function (Blueprint $table) {
$table->string('id')->unique(); $table->string('id')->unique();
$table->text('payload'); $table->text('payload');
$table->integer('last_activity'); $table->dateTime('last_activity')->useCurrent();
$table->integer('lifetime');
}); });
} }

View File

@ -76,6 +76,21 @@ class Migrate
} }
} }
/**
* Setup migration tables
*/
public function initMigration()
{
if ($this->schema->hasTable($this->table)) {
return;
}
$this->schema->create($this->table, function (Blueprint $table) {
$table->increments('id');
$table->string('migration');
});
}
/** /**
* Get all migrated migrations * Get all migrated migrations
* *
@ -155,21 +170,6 @@ class Migrate
return glob($dir . '/*_*.php'); return glob($dir . '/*_*.php');
} }
/**
* Setup migration tables
*/
protected function initMigration()
{
if ($this->schema->hasTable($this->table)) {
return;
}
$this->schema->create($this->table, function (Blueprint $table) {
$table->increments('id');
$table->string('migration');
});
}
/** /**
* Init a table query * Init a table query
* *

View File

@ -0,0 +1,75 @@
<?php
namespace Engelsystem\Http\SessionHandlers;
use SessionHandlerInterface;
abstract class AbstractHandler implements SessionHandlerInterface
{
/** @var string */
protected $name;
/** @var string */
protected $sessionPath;
/**
* Bootstrap the session handler
*
* @param string $sessionPath
* @param string $name
* @return bool
*/
public function open($sessionPath, $name): bool
{
$this->name = $name;
$this->sessionPath = $sessionPath;
return true;
}
/**
* Shutdown the session handler
*
* @return bool
*/
public function close(): bool
{
return true;
}
/**
* Remove old sessions
*
* @param int $maxLifetime
* @return bool
*/
public function gc($maxLifetime): bool
{
return true;
}
/**
* Read session data
*
* @param string $id
* @return string
*/
abstract public function read($id): string;
/**
* Write session data
*
* @param string $id
* @param string $data
* @return bool
*/
abstract public function write($id, $data): bool;
/**
* Delete a session
*
* @param string $id
* @return bool
*/
abstract public function destroy($id): bool;
}

View File

@ -0,0 +1,108 @@
<?php
namespace Engelsystem\Http\SessionHandlers;
use Engelsystem\Database\Database;
use Illuminate\Database\Query\Builder as QueryBuilder;
class DatabaseHandler extends AbstractHandler
{
/** @var Database */
protected $database;
/**
* @param Database $database
*/
public function __construct(Database $database)
{
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function read($id): string
{
$session = $this->getQuery()
->where('id', '=', $id)
->first();
return $session ? $session->payload : '';
}
/**
* {@inheritdoc}
*/
public function write($id, $data): bool
{
$values = [
'payload' => $data,
'last_activity' => $this->getCurrentTimestamp(),
];
$session = $this->getQuery()
->where('id', '=', $id)
->first();
if (!$session) {
return $this->getQuery()
->insert($values + [
'id' => $id,
]);
}
$this->getQuery()
->where('id', '=', $id)
->update($values);
// The update return can't be used directly because it won't change if the second call is in the same second
return true;
}
/**
* {@inheritdoc}
*/
public function destroy($id): bool
{
$this->getQuery()
->where('id', '=', $id)
->delete();
return true;
}
/**
* {@inheritdoc}
*/
public function gc($maxLifetime): bool
{
$timestamp = $this->getCurrentTimestamp(-$maxLifetime);
$this->getQuery()
->where('last_activity', '<', $timestamp)
->delete();
return true;
}
/**
* @return QueryBuilder
*/
protected function getQuery(): QueryBuilder
{
return $this->database
->getConnection()
->table('sessions');
}
/**
* Format the SQL timestamp
*
* @param int $diff
* @return string
*/
protected function getCurrentTimestamp(int $diff = 0): string
{
return date('Y-m-d H:i:s', strtotime(sprintf('%+d seconds', $diff)));
}
}

View File

@ -4,8 +4,8 @@ namespace Engelsystem\Http;
use Engelsystem\Config\Config; use Engelsystem\Config\Config;
use Engelsystem\Container\ServiceProvider; use Engelsystem\Container\ServiceProvider;
use Engelsystem\Http\SessionHandlers\DatabaseHandler;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
@ -45,20 +45,9 @@ class SessionServiceProvider extends ServiceProvider
$sessionConfig = $config->get('session'); $sessionConfig = $config->get('session');
$handler = null; $handler = null;
$driver = $sessionConfig['driver']; switch ($sessionConfig['driver']) {
switch ($driver) {
case 'pdo': case 'pdo':
$handler = $this->app->make(PdoSessionHandler::class, [ $handler = $this->app->make(DatabaseHandler::class);
'pdoOrDsn' => $this->app->get('db.pdo'),
'options' => [
'db_table' => 'sessions',
'db_id_col' => 'id',
'db_data_col' => 'payload',
'db_lifetime_col' => 'lifetime',
'db_time_col' => 'last_activity',
],
]);
break; break;
} }

View File

@ -0,0 +1,47 @@
<?php
namespace Engelsystem\Test\Unit;
use Engelsystem\Application;
use Engelsystem\Database\Database;
use Engelsystem\Database\Migration\Migrate;
use Engelsystem\Database\Migration\MigrationServiceProvider;
use Illuminate\Database\Capsule\Manager as CapsuleManager;
use PDO;
trait HasDatabase
{
/** @var Database */
protected $database;
/**
* Setup in memory database
*/
protected function initDatabase()
{
$dbManager = new CapsuleManager();
$dbManager->addConnection(['driver' => 'sqlite', 'database' => ':memory:']);
$connection = $dbManager->getConnection();
$connection->getPdo()->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->database = new Database($connection);
$app = new Application();
$app->instance(Database::class, $this->database);
$app->register(MigrationServiceProvider::class);
/** @var Migrate $migration */
$migration = $app->get('db.migration');
$migration->initMigration();
$this->database
->getConnection()
->table('migrations')
->insert([
['migration' => '2018_01_01_000001_import_install_sql'],
['migration' => '2018_01_01_000002_import_update_sql'],
]);
$migration->run(__DIR__ . '/../../db/migrations');
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Engelsystem\Test\Unit\Http\SessionHandlers;
use Engelsystem\Test\Unit\Http\SessionHandlers\Stub\ArrayHandler;
use PHPUnit\Framework\TestCase;
class AbstractHandlerTest extends TestCase
{
/**
* @covers \Engelsystem\Http\SessionHandlers\AbstractHandler::open
*/
public function testOpen()
{
$handler = new ArrayHandler();
$return = $handler->open('/foo/bar', '1337asd098hkl7654');
$this->assertTrue($return);
$this->assertEquals('1337asd098hkl7654', $handler->getName());
$this->assertEquals('/foo/bar', $handler->getSessionPath());
}
/**
* @covers \Engelsystem\Http\SessionHandlers\AbstractHandler::close
*/
public function testClose()
{
$handler = new ArrayHandler();
$return = $handler->close();
$this->assertTrue($return);
}
/**
* @covers \Engelsystem\Http\SessionHandlers\AbstractHandler::gc
*/
public function testGc()
{
$handler = new ArrayHandler();
$return = $handler->gc(60 * 60 * 24);
$this->assertTrue($return);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Engelsystem\Test\Unit\Http\SessionHandlers;
use Engelsystem\Http\SessionHandlers\DatabaseHandler;
use Engelsystem\Test\Unit\HasDatabase;
use PHPUnit\Framework\TestCase;
class DatabaseHandlerTest extends TestCase
{
use HasDatabase;
/**
* @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::__construct
* @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::read
* @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::getQuery
*/
public function testRead()
{
$handler = new DatabaseHandler($this->database);
$this->assertEquals('', $handler->read('foo'));
$this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', CURRENT_TIMESTAMP)");
$this->assertEquals('Lorem Ipsum', $handler->read('foo'));
}
/**
* @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::write
* @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::getCurrentTimestamp
*/
public function testWrite()
{
$handler = new DatabaseHandler($this->database);
foreach (['Lorem Ipsum', 'Dolor Sit!'] as $data) {
$this->assertTrue($handler->write('foo', $data));
$return = $this->database->select('SELECT * FROM sessions WHERE id = :id', ['id' => 'foo']);
$this->assertCount(1, $return);
$return = array_shift($return);
$this->assertEquals($data, $return->payload);
}
}
/**
* @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::destroy
*/
public function testDestroy()
{
$this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', CURRENT_TIMESTAMP)");
$this->database->insert("INSERT INTO sessions VALUES ('bar', 'Dolor Sit', CURRENT_TIMESTAMP)");
$handler = new DatabaseHandler($this->database);
$this->assertTrue($handler->destroy('batz'));
$return = $this->database->select('SELECT * FROM sessions');
$this->assertCount(2, $return);
$this->assertTrue($handler->destroy('bar'));
$return = $this->database->select('SELECT * FROM sessions');
$this->assertCount(1, $return);
$return = array_shift($return);
$this->assertEquals('foo', $return->id);
}
/**
* @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::gc
*/
public function testGc()
{
$this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', '2000-01-01 01:00')");
$this->database->insert("INSERT INTO sessions VALUES ('bar', 'Dolor Sit', '3000-01-01 01:00')");
$handler = new DatabaseHandler($this->database);
$this->assertTrue($handler->gc(60 * 60));
$return = $this->database->select('SELECT * FROM sessions');
$this->assertCount(1, $return);
$return = array_shift($return);
$this->assertEquals('bar', $return->id);
}
/**
* Prepare tests
*/
protected function setUp()
{
$this->initDatabase();
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Engelsystem\Test\Unit\Http\SessionHandlers\Stub;
use Engelsystem\Http\SessionHandlers\AbstractHandler;
class ArrayHandler extends AbstractHandler
{
/** @var string[] */
protected $content = [];
/**
* {@inheritdoc}
*/
public function read($id): string
{
if (isset($this->content[$id])) {
return $this->content[$id];
}
return '';
}
/**
* {@inheritdoc}
*/
public function write($id, $data): bool
{
$this->content[$id] = $data;
return true;
}
/**
* {@inheritdoc}
*/
public function destroy($id): bool
{
unset($this->content[$id]);
return true;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return string
*/
public function getSessionPath(): string
{
return $this->sessionPath;
}
}

View File

@ -4,12 +4,11 @@ namespace Engelsystem\Test\Unit\Http;
use Engelsystem\Config\Config; use Engelsystem\Config\Config;
use Engelsystem\Http\Request; use Engelsystem\Http\Request;
use Engelsystem\Http\SessionHandlers\DatabaseHandler;
use Engelsystem\Http\SessionServiceProvider; use Engelsystem\Http\SessionServiceProvider;
use Engelsystem\Test\Unit\ServiceProviderTest; use Engelsystem\Test\Unit\ServiceProviderTest;
use PDO;
use PHPUnit_Framework_MockObject_MockObject as MockObject; use PHPUnit_Framework_MockObject_MockObject as MockObject;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface as StorageInterface; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface as StorageInterface;
@ -26,7 +25,7 @@ class SessionServiceProviderTest extends ServiceProviderTest
$sessionStorage = $this->getMockForAbstractClass(StorageInterface::class); $sessionStorage = $this->getMockForAbstractClass(StorageInterface::class);
$sessionStorage2 = $this->getMockForAbstractClass(StorageInterface::class); $sessionStorage2 = $this->getMockForAbstractClass(StorageInterface::class);
$pdoSessionHandler = $this->getMockBuilder(PdoSessionHandler::class) $databaseHandler = $this->getMockBuilder(DatabaseHandler::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
@ -41,10 +40,6 @@ class SessionServiceProviderTest extends ServiceProviderTest
/** @var Config|MockObject $config */ /** @var Config|MockObject $config */
$config = $this->createMock(Config::class); $config = $this->createMock(Config::class);
/** @var PDO|MockObject $pdo */
$pdo = $this->getMockBuilder(PDO::class)
->disableOriginalConstructor()
->getMock();
$serviceProvider->expects($this->exactly(3)) $serviceProvider->expects($this->exactly(3))
->method('isCli') ->method('isCli')
@ -60,22 +55,10 @@ class SessionServiceProviderTest extends ServiceProviderTest
['options' => ['cookie_httponly' => true, 'name' => 'session'], 'handler' => null] ['options' => ['cookie_httponly' => true, 'name' => 'session'], 'handler' => null]
], ],
[Session::class], [Session::class],
[ [DatabaseHandler::class],
PdoSessionHandler::class,
[
'pdoOrDsn' => $pdo,
'options' => [
'db_table' => 'sessions',
'db_id_col' => 'id',
'db_data_col' => 'payload',
'db_lifetime_col' => 'lifetime',
'db_time_col' => 'last_activity',
],
]
],
[ [
NativeSessionStorage::class, NativeSessionStorage::class,
['options' => ['cookie_httponly' => true, 'name' => 'foobar'], 'handler' => $pdoSessionHandler] ['options' => ['cookie_httponly' => true, 'name' => 'foobar'], 'handler' => $databaseHandler]
], ],
[Session::class] [Session::class]
) )
@ -84,7 +67,7 @@ class SessionServiceProviderTest extends ServiceProviderTest
$session, $session,
$sessionStorage2, $sessionStorage2,
$session, $session,
$pdoSessionHandler, $databaseHandler,
$sessionStorage2, $sessionStorage2,
$session $session
); );
@ -96,14 +79,13 @@ class SessionServiceProviderTest extends ServiceProviderTest
['session', $session] ['session', $session]
); );
$app->expects($this->exactly(6)) $app->expects($this->exactly(5))
->method('get') ->method('get')
->withConsecutive( ->withConsecutive(
['request'], ['request'],
['config'], ['config'],
['request'], ['request'],
['config'], ['config'],
['db.pdo'],
['request'] ['request']
) )
->willReturnOnConsecutiveCalls( ->willReturnOnConsecutiveCalls(
@ -111,7 +93,6 @@ class SessionServiceProviderTest extends ServiceProviderTest
$config, $config,
$request, $request,
$config, $config,
$pdo,
$request $request
); );