PHP Sessions are used in many web application to store user’s data on the server. For instance, the majority of the PHP applications with a login page (with username and password) use Session.
The usage of PHP functions session_*
is not compatible with PSR-7, e.g. calling session_start()
will automatically add the Set-Cookie
header in the PHP output buffer. Moreover, the usage of global variables, like $_SESSION
is not recommended in a middleware architecture. All the data to generate a HTTP Response should be included in the HTTP Request.
The question is, if we need PHP Session in a PSR-7 application, how we can use it?
One possible solution is to use a library like psr7-sessions/storageless. This project works very well using a JWT approach, but it is limited to 400 bytes of data per session and it does not use the PHP Session (ext/session
). This can be a limit in many use cases, especially when migrating existing PHP applications to PSR-7 middleware architecture.
I prototyped another possible solution using the PHP Session. The idea is to use a PhpSession
class that imports and exports data to PHP Session using $_SESSION
.
UPDATE : @matthew has proposed a complete example of a PSR-7 Session Middleware here.
class PhpSession
{
protected $session;
protected $id;
public function __construct(string $id)
{
$this->id = $id;
session_id($id);
session_start([
'use_cookies' => false,
'use_only_cookies' => true
]);
$this->session = $_SESSION;
}
public function set(string $name, $value): void
{
$this->session[$name] = $value;
}
public function get(string $name)
{
return $this->session[$name] ?? null;
}
public function getId(): string
{
return $this->id;
}
public function unset(string $name): void
{
unset($this->session[$name]);
}
public function save(): void
{
$_SESSION = $this->session;
session_write_close();
}
}
This class can be injected in a PSR-7 request using an attribute. If a middleware needs Session it can retrieve as PSR-7 attribute from $request
and use it.
In order to disable the automatically output of session_start()
, I used the following configuration for Session:
session_start([
'use_cookies' => false,
'use_only_cookies' => true
]);
As suggested by Paul M.Jones in PSR-7 and Session Cookies, this configuration tells PHP not to send a cookie when it does session work. Because of that, we need to send the Set-Cookie
header manually in the HTTP Response, as requested by the PSR-7 workflow. Moreover, we need to check if a Session Cookie is present in the HTTP Request, getting the Session ID.
We can achieve this using a middleware like the follows:
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
class SessionMiddleware implements ServerMiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
$cookies = $request->getCookieParams();
$id = $cookies[session_name()] ?? bin2hex(random_bytes(16));
$session = new PhpSession($id);
$response = $delegate->process($request->withAttribute(PhpSession::class, $session));
$session->save();
return isset($cookies[session_name()]) ? $response : $response->withHeader(
'Set-Cookie',
sprintf("%s=%s; path=%s", session_name(), $id, ini_get('session.cookie_path'))
);
}
}
This SessionMiddleware
can be registered in the pipeline before the route dispatch, e.g. using Expressive you can add it in the config/pipeline.php
file, as follows:
$app->pipe(SessionMiddleware::class);
// ...
$app->pipeRoutingMiddleware();
You can also add SessionMiddleware
only for a specific route. An example is reported below:
// config/routes.php
$app->get('/', [
SessionMiddleware::class,
App\Action\HomePageAction::class
], 'home');
Note: you need to register
SessionMiddleware
in the service container, e.g. usingzend-servicemanger
you can register asinvokables
.
Using the proposed solution, we can use PHP Session as PSR-7 middleware, without the usage of the global variable $_SESSION reusing all the features of ext/session
.