Skip to content

Instantly share code, notes, and snippets.

@weierophinney
Last active January 29, 2021 00:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save weierophinney/e6c35ad73232f0e13dd205d4e8bc98d7 to your computer and use it in GitHub Desktop.
Save weierophinney/e6c35ad73232f0e13dd205d4e8bc98d7 to your computer and use it in GitHub Desktop.
zend-expressive-session proposed changes
<?php
namespace Zend\Expressive\Session;
/**
* Class representing a session segment (essentially, a "namespace" within the session).
*
* Other proposed methods/functionality:
*
* - setFlash(string $name, string $message) :void - on a namespace instance, to set a flash value
* - getFlash(string $name) : ?string - on a namespace instance, to retrieve a flash value
* - keepFlash() : void - on a namespace instance, persist flash values for another request
* - clearFlash() : void - on a namespace instance, to remove any existing flash values
*
* The above would require some coordination by the Session class (for purposes
* of managing which items are current, and which need to persist to next
* request).
*/
class Segment
{
use SessionDataTrait;
private $data;
private $id;
public function __construct(string $id, array $data)
{
$this->id = $id;
$this->data = $data;
}
/**
* Retrieve all data, for purposes of persistence.
*/
public function toArray() : array
{
return $this->data;
}
/**
* @param mixed $default Default value to return if value does not exist.
* @return mixed
*/
public function get(string $name, $default = null)
{
return $this->data[$name] ?? $default;
}
public function set(string $name, $value): void
{
$this->data[$name] = $value;
}
public function unset(string $name): void
{
unset($this->data[$name]);
}
public function clear() : void
{
$this->data = [];
}
}
<?php
namespace Zend\Expressive\Session;
use RuntimeException;
class Session
{
use SessionDataTrait;
private $data;
private $id;
private $segments = [];
public function __construct(string $id, array $data)
{
$this->id = $id;
$this->data = $data;
}
/**
* Retrieve all data, including segments, as a nested set of arrays, for
* purposes of persistence.
*/
public function toArray() : array
{
$data = $this->data;
foreach ($this->segments as $key => $segment) {
$segmentData = $segment->toArray();
if (empty($segmentData)) {
unset($this->data[$key]);
continue;
}
$data[$key] = $segmentData;
}
return $data;
}
/**
* @param mixed $default Default value to return if $name does not exist.
* @throws RuntimeException if $name refers to a known session segment.
*/
public function get(string $name, $default = null)
{
if (isset($this->segments[$name])) {
throw new RuntimeException(sprintf(
'Retrieving session data "%s" via get(); however, this data refers to a session segment; aborting',
$name
));
}
return $this->data[$name] ?? $default;
}
/**
* @param mixed $value
* @throws RuntimeException if $name refers to a known session segment.
*/
public function set(string $name, $value) : void
{
if (isset($this->segments[$name])) {
throw new RuntimeException(sprintf(
'Attempting to set session data "%s"; however, this data refers to a session segment; aborting',
$name
));
}
$this->data[$name] = $value;
}
/**
* @throws RuntimeException if $name refers to a known session segment.
*/
public function unset(string $name) : void
{
if (isset($this->segments[$name])) {
throw new RuntimeException(sprintf(
'Attempting to unset session data "%s"; however, this data refers to a session segment. '
. 'Use clear() on the segment instead',
$name
));
}
unset($this->data[$name]);
}
public function segment(string $name) : Segment
{
if (isset($this->segments[$name])) {
return $this->segments[$name];
}
if (array_key_exists($name, $this->data)
&& ! is_array($this->data[$name])
) {
throw new RuntimeException(sprintf(
'Cannot retrieve session segment "%s"; data exists, but as a "%s" instead of an array',
$name,
is_object($this->data['name']) ? get_class($this->data[$name]) : gettype($this->data['name'])
));
}
$this->segments[$name] = new Segment($this->data[$name]);
return $this->segments[$name];
}
public function regenerateId(): void
{
$this->id = static::generateToken();
}
}
<?php
namespace Zend\Expressive\Session;
trait SessionDataTrait
{
public static function generateToken() : string
{
return bin2hex(random_bytes(16));
}
public function getId(): string
{
return $this->id;
}
public function generateCsrfToken(string $keyName = '__csrf') : string
{
$this->data[$keyName] = static::generateToken();
return $this->data[$keyName];
}
public function validateCsrfToken(string $token, string $csrfKey = '__csrf') : bool
{
if (! isset($this->data[$csrfKey])) {
return false;
}
return $token === $this->data[$csrfKey];
}
}
<?php
namespace Zend\Expressive\Session;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
class SessionMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
$cookies = $request->getCookieParams();
$id = $cookies[session_name()] ?? Session::generateToken();
$this->startSession($id);
$session = new Session($id, $_SESSION);
$response = $delegate->process($request->withAttribute(Session::class, $session));
if ($id !== $session->getId()) {
$this->startSession($session->getId());
}
$_SESSION = $session->toArray();
session_write_close();
if (empty($_SESSION)) {
return $response;
}
return $response->withHeader(
'Set-Cookie',
sprintf(
'%s=%s; path=%s',
session_name(),
$session->getId(),
ini_get('session.cookie_path')
)
);
}
private function startSession(string $id) : void
{
session_id($id);
session_start([
'use_cookies' => false,
'use_only_cookies' => true,
]);
}
}
@weierophinney
Copy link
Author

@kynx No worries! Just wanted to ensure we're all on the same page.

@weierophinney
Copy link
Author

Full implementation (untested!) is here: https://github.com/weierophinney/zend-expressive-session

Contains:

  • Adapter-based persistence approach
  • Lazy-initialization of sessions
  • Basic session implementation, with segment support
  • Flash messages
  • CSRF tokens and validation

@weierophinney
Copy link
Author

@kynx — I've had quite a number of discussions with @Ocramius at this point, and decided to do the following:

  • Remove "identifier" awareness in the SessionInterface. regenerate() produces a new instance, where isRegenerated() indicates that this happened. (The LazySession does not return a new instance, but does reset its internal proxy to the regenerated instance the original proxy produced.) This makes identifiers an implementation detail of persistence adapters.
  • Remove all segment support. Ideally, this would be done by having separate sessions — which can be done with JWT, or by using a cache server for sessions instead of ext-session. Segments really only were needed for ext-session, and there are ways around it.
  • Move flash messages to a sub-component, with their own interface and middleware, and consume the session.
  • Likewise with CSRF.

I find that the base implementation is far simpler and more robust, while allowing for the flexibility of adding on features like flash messages and CSRF validations easily as add-ons consuming the component.

@kynx
Copy link

kynx commented Oct 5, 2017

@weierophinney fantastic! I much prefer this approach. Having regenerate() return a new instance is a particularly good touch. I’ll have a proper play with the code as soon as I can. Thanks so much for all the work on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment