-
-
Save weierophinney/e6c35ad73232f0e13dd205d4e8bc98d7 to your computer and use it in GitHub Desktop.
zend-expressive-session proposed changes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 = []; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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, | |
]); | |
} | |
} |
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
@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, whereisRegenerated()
indicates that this happened. (TheLazySession
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.
@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
@kynx No worries! Just wanted to ensure we're all on the same page.