blast-project/doctrine-session-bundle
Installation
composer require blast-project/doctrine-session-bundle
Add the bundle to config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 3/legacy):
return [
// ...
Blast\DoctrineSessionBundle\BlastDoctrineSessionBundle::class => ['all' => true],
];
Configure Doctrine Session Handler
Add this to your config/packages/blast_doctrine_session.yaml (or merge into existing config):
blast_doctrine_session:
handler_id: 'blast.doctrine.session.handler'
entity:
class: App\Entity\Session
repository: App\Repository\SessionRepository
Create Session Entity Generate a basic Doctrine entity for sessions (adjust namespace and properties as needed):
php bin/console make:entity Session
Add these fields:
id (UUID or ID, primary key)data (string, nullable)lifetime (integer, nullable)lastUsed (datetime, nullable)ipAddress (string, nullable)userAgent (string, nullable)Example entity (src/Entity/Session.php):
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: "App\Repository\SessionRepository")]
class Session
{
#[ORM\Id]
#[ORM\Column(type: "uuid", unique: true)]
#[ORM\GeneratedValue(strategy: "CUID")]
private ?string $id = null;
#[ORM\Column(type: "string", length: 255, nullable: true)]
private ?string $data;
#[ORM\Column(type: "integer", nullable: true)]
private ?int $lifetime;
#[ORM\Column(type: "datetime_immutable", nullable: true)]
private ?\DateTimeImmutable $lastUsed;
#[ORM\Column(type: "string", length: 45, nullable: true)]
private ?string $ipAddress;
#[ORM\Column(type: "text", nullable: true)]
private ?string $userAgent;
// Getters and setters...
}
Update Database Schema
php bin/console doctrine:schema:update --force
First Use Case: Replace Native Session Handler The bundle automatically replaces Symfony’s default session handler with Doctrine-based storage. Test by:
// In a controller or service
$session = $this->get('session');
$session->set('foo', 'bar');
dump($session->get('foo')); // Should output 'bar'
Session Management
session service as usual. The bundle handles serialization/deserialization of session data to/from the data field in the Session entity.
$session->start(); // Starts the session (if not already started)
$session->set('user_id', 123);
$userId = $session->get('user_id');
framework.session.lifetime). Expired sessions are automatically cleaned up via Doctrine’s SessionRepository (if implemented).Customizing Session Data
Session entity to include custom fields (e.g., rememberMeToken, loginAttempts):
#[ORM\Column(type: "boolean")]
private bool $rememberMe = false;
#[ORM\Column(type: "integer")]
private int $loginAttempts = 0;
// src/Repository/SessionRepository.php
public function findByRememberMe(bool $rememberMe): array
{
return $this->createQueryBuilder('s')
->andWhere('s.rememberMe = :rememberMe')
->setParameter('rememberMe', $rememberMe)
->getQuery()
->getResult();
}
Integration with Security
$session->set('_security_main', serialize(['ip' => $request->getClientIp()]));
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
$session = $request->getSession();
$session->set('user', $token->getUser());
$session->set('last_login_ip', $request->getClientIp());
}
Session Cleanup
// src/Command/CleanupSessionsCommand.php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\ORM\EntityManagerInterface;
class CleanupSessionsCommand extends Command
{
protected static $defaultName = 'app:cleanup-sessions';
public function __construct(private EntityManagerInterface $em)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$cutoff = new \DateTimeImmutable('-1 hour');
$sessions = $this->em->getRepository(Session::class)->findBy(['lastUsed' => $cutoff]);
foreach ($sessions as $session) {
$this->em->remove($session);
}
$this->em->flush();
$output->writeln(sprintf('Removed %d expired sessions.', count($sessions)));
return Command::SUCCESS;
}
}
SessionRepository to add a postFlush listener for cleanup:
// src/Repository/SessionRepository.php
use Doctrine\ORM\Event\LifecycleEventArgs;
public function postFlush(LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$cutoff = new \DateTimeImmutable('-1 hour');
$sessions = $em->createQuery(
'SELECT s FROM App\Entity\Session s WHERE s.lastUsed < :cutoff'
)->setParameter('cutoff', $cutoff)
->getResult();
foreach ($sessions as $session) {
$em->remove($session);
}
}
Testing Sessions
SessionTestCase or mock the session handler:
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SessionTest extends WebTestCase
{
public function testSessionStorage()
{
$client = static::createClient();
$client->request('GET', '/');
$client->getContainer()->get('session')->set('test', 'value');
$this->assertEquals('value', $client->getContainer()->get('session')->get('test'));
}
}
SessionHandlerInterface to test session logic:
$handler = $this->createMock(SessionHandlerInterface::class);
$handler->expects($this->once())
->method('read')
->with('test_session_id')
->willReturn(serialize(['foo' => 'bar']));
$this->container->set('blast.doctrine.session.handler', $handler);
Database Indexing
lastUsed, ipAddress) for performance:
#[ORM\Index(name: "idx_last_used", columns: ["lastUsed"])]
#[ORM\Index(name: "idx_ip_address", columns: ["ipAddress"])]
Session Serialization
SessionHandler:
// src/Service/CustomSessionHandler.php
namespace App\Service;
use Blast\DoctrineSessionBundle\Handler\DoctrineSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler;
class CustomSessionHandler extends DoctrineSessionHandler
{
public function read($sessionId): string|false
{
$data = parent::read($sessionId);
return $data ? gzuncompress($data) : false;
}
public function write($sessionId, $data): bool
{
return parent::write($sessionId, gzcompress($data));
}
}
config/services.yaml:
services:
App\Service\CustomSessionHandler:
arguments:
$entityManager: '@doctrine.
How can I help you explore Laravel packages today?