How to build a custom session driver for Laravel 5
- 3 minsOriginally written for and posted on Foundry
Photo by Ilze Lucero on Unsplash
For a recent project built using the PHP framework Laravel a requirement was the ability to have certain resources be removed when the current session expires.
Using the built-in database session driver we maintain a pivot table linking Entities in a transient state to the current session. Later, if the current session logs in or registers an account we can store the processed entities against that account and remove them from the transients table, disassociating them from the current session.
This was brilliant and worked flawlessly-
-until the session garbage collection attempted to clean up some old sessions and didn’t know to remove their related models.
This garbage collection, which can be triggered at any time (with a default probability of 2 in 100 requests) will affect another user’s session if it causes an error. In order to prevent any foreign key errors from occurring we need to ensure that all would-be orphaned records are removed before the session itself is destroyed.
To accomplish this, we devised a custom session handler to extend the provided database session handler.
<?php | |
namespace App\Extensions; | |
// we have an Eloquent model that allows using the session table with the querybuilder methods and eloquent fluent interface- read only! | |
use App\Session; | |
// use the provided database session handler to avoid too much duplicated effort. | |
use Illuminate\Session\DatabaseSessionHandler; | |
class AppDatabaseSessionHandler extends DatabaseSessionHandler | |
{ | |
/** | |
* The destroy method can be called at any time for a single session. Ensure that our related records are removed to prevent foreign key constraint errors. | |
* | |
* {@inheritdoc} | |
*/ | |
public function destroy($sessionId) | |
{ | |
$session = $this->getQuery()->where('id', $sessionId); | |
// tidy up any orphaned records by this session going away. | |
$sessionModel = Session::find($sessionId); | |
foreach ($sessionModel->myModels as $model) { | |
$sessionModel->myModels()->detach($model->id); | |
$model->delete(); | |
} | |
$session->delete(); | |
} | |
/** | |
* Replicate the existing gc behaviour but call through to our modified destroy method instead of the default behaviour | |
* | |
* {@inheritdoc} | |
*/ | |
public function gc($lifetime) | |
{ | |
$sessions = $this->getQuery()->where('last_activity', '<=', time() - $lifetime)->get(); | |
foreach ($sessions as $session) { | |
$this->destroy($session->id); | |
} | |
} | |
} |
Registering the AppDatabaseSessionHandler
is done through a provider, which is set up the same way as the built-in DatabaseSessionHandler
.
<?php | |
namespace App\Providers; | |
use App\Extensions\AppDatabaseSessionHandler; | |
use Illuminate\Support\ServiceProvider; | |
use Illuminate\Database\ConnectionInterface; | |
use Session; | |
use Config; | |
class SessionProvider extends ServiceProvider | |
{ | |
/** | |
* Perform post-registration booting of services. | |
* | |
* @return void | |
*/ | |
public function boot(ConnectionInterface $connection) | |
{ | |
Session::extend('app-database', function($app) use ($connection) { | |
$table = Config::get('session.table'); | |
$minutes = Config::get('session.lifetime'); | |
return new AppDatabaseSessionHandler($connection, $table, $minutes); | |
}); | |
} | |
/** | |
* Register bindings in the container. | |
* | |
* @return void | |
*/ | |
public function register() | |
{ | |
// | |
} | |
} |
For our application, it relies on this functionality so it’s fine to hard-code the config into the app. Otherwise we would edit our .env
files. To make it a simpler default for the application, editing it in .env.example
would be a good idea too.
<?php | |
return [ | |
'driver' => 'app-database', | |
// etc... | |
]; |
Now when a session expires, it transparently cleans up any model that has not been committed to permanent storage. Neat!
For an alternative approach, and one that may become more valuable once the project increases in scope, would be to leverage queuing and implement a garbage collection job that looks for orphaned records and removes them from the database.