[FEATURE] add the session service

This commit is contained in:
Marco Stoll 2019-06-28 12:13:55 +02:00
parent 8b75c8ec51
commit 4c389af56b
8 changed files with 601 additions and 4 deletions

View File

@ -21,6 +21,7 @@ Currently **FF** is composed of the following features:
3. Runtime event handlers
4. Templating and Twig as a Service
5. Dispatching and Controllers
6. Sessions
More features will follow (see the Road Map section below).
@ -433,11 +434,51 @@ Each concrete controller must implement the `getTemplateRenderer()` method. If y
{
return $this->render('hello-world.html.twig', ['msg' => 'Hello, World!']);
}
}
}
# Sessions
**FF**'s built-it **Session** service in essence just wraps php's session functions in an object oriented way. It lets you
`start()`, `regenerate()`, `writeClose()`, and `destroy()` sessions as well as `get()`, `set()`, and `unset()` values.
The service can be configured using all the known `session.XYZ` options that the php session module defines. The service
defines some different default though, mostly to enhance the default security behaviour regarding session cookies.
## Defining Custom Session Save Handlers
## Observing Session Events
The **Session** service emits a number of events.
The defined events are classic life cycle events fired at distinct moments in a session's life. Observing this
kind of events lets you manipulate the session's state on your behalf.
***Example: Adding data to the session right after start***
use FF\Events;
use FF\Factories\SF;
use FF\Services\Events\EventBroker;
class MySessionObserver
{
/**
* Event handling callback
*
* @param Sessions\PostStart $event
public function onPostStart(Sessions\PostStart $event)
{
$event->getSession()->set('foo', 'bar');
}
}
// subscribe to the Sessions\PostStart event
/** @var EventBroker $eventBroker */
$eventBroker = SF::i()->get('Events\EventBroker');
$eventBroker->subscribe([new MySessionObserver, 'onPostStart'], 'Sessions\PostStart');
# Road Map
- Sessions
- Session Handlers
- Security
- CLI
- ORM

View File

@ -0,0 +1,43 @@
<?php
/**
* Definition of PostStart
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Events\Sessions;
use FF\Events\AbstractEvent;
use FF\Services\Sessions\Session;
/**
* Class PostStart
*
* @package FF\Events\Sessions
*/
class PostStart extends AbstractEvent
{
/**
* @var Session
*/
protected $session;
/**
* @param Session $session
*/
public function __construct(Session $session)
{
$this->session = $session;
}
/**
* @return Session
*/
public function getSession(): Session
{
return $this->session;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Definition of PreDestroy
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Events\Sessions;
use FF\Events\AbstractEvent;
use FF\Services\Sessions\Session;
/**
* Class PreDestroy
*
* @package FF\Events\Sessions
*/
class PreDestroy extends AbstractEvent
{
/**
* @var Session
*/
protected $session;
/**
* @param Session $session
*/
public function __construct(Session $session)
{
$this->session = $session;
}
/**
* @return Session
*/
public function getSession(): Session
{
return $this->session;
}
}

View File

@ -0,0 +1,22 @@
<?php
/**
* Definition of PreStart
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Events\Sessions;
use FF\Events\AbstractEvent;
/**
* Class PreStart
*
* @package FF\Events\Sessions
*/
class PreStart extends AbstractEvent
{
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Definition of PreWriteClose
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Events\Sessions;
use FF\Events\AbstractEvent;
use FF\Services\Sessions\Session;
/**
* Class PreWriteClose
*
* @package FF\Events\Sessions
*/
class PreWriteClose extends AbstractEvent
{
/**
* @var Session
*/
protected $session;
/**
* @param Session $session
*/
public function __construct(Session $session)
{
$this->session = $session;
}
/**
* @return Session
*/
public function getSession(): Session
{
return $this->session;
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Class SessionException
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
namespace FF\Sessions\Session\Exceptions;
/**
* Class SessionException
*
* @package FF\Services\Sessions\Exceptions
*/
class SessionException extends \RuntimeException
{
const ERROR_SESSION_START = 1;
const ERROR_SESSION_DESTROY = 2;
const ERROR_SESSION_REGENERATE = 3;
const ERROR_SESSION_SAVE_HANDLER = 4;
}

View File

@ -0,0 +1,382 @@
<?php
/**
* Definition of Session
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Services\Sessions;
use FF\Factories\Exceptions\ClassNotFoundException;
use FF\Factories\SF;
use FF\Services\AbstractService;
use FF\Services\Exceptions\ConfigurationException;
use FF\Sessions\Session\Exceptions\SessionException;
/**
* Class EventBroker
*
* Options:
*
* Accepts all options defined by the php built-in session handling.
*
* Defines the following defaults differing from php's built-in default session configuration (mostly for security
* reasons):
* - session.use_strict_mode : bool (default: true) - whether the module will use strict session id mode
* - session.cookie_httponly : bool (default: true) - marks the cookie as accessible only through the HTTP protocol
*
* Additional options:
*
* - custom_handler : string (default: '') - class identifier of a valid service class implementing the
* SessionHandlerInterface.
* Must be retrievable vie the service factory using the class
* identifier
* - fire-events : bool (default: false) - whether to fire session events
*
* Recommendations:
*
* Set a unique session name for your project:
* - session.name : string (default: 'PHPSESSID') - name of the session cookie,
* should only contain alphanumeric characters
*
* If your project solely uses a secure (SSL) https web interface, you should active the secure cookie option:
* - session.cookie_secure : bool (default: false) - whether cookies should only be sent over secure connection
*
* Do not set session.auto_start. Always call Session::start() explicitly on your demand.
* - session.auto_start : bool (default: false) - whether the session module starts a session automatically
*
* @package FF\Services\Sessions
*
* @link https://www.php.net/manual/en/session.configuration.php
*/
class Session extends AbstractService
{
const ENHANCED_CONFIG_DEFAULTS = [
'session.use_strict_mode' => '1',
'session.cookie_httponly' => '1'
];
/**
* Retrieves the session name
*
* @return string
* @see http://php.net/session_name
*/
public function getName(): string
{
return session_name();
}
/**
* Retrieves the session id
*
* @return string
* @see http://php.net/session_id
*/
public function getId(): string
{
return session_id();
}
/**
* Retrieves the current session's status
*
* @return int
* @see http://php.net/session_status
*/
public function getStatus(): int
{
return session_status();
}
/**
* Checks whether an active session exists
*
* @return bool
*/
public function isActive(): bool
{
return ($this->getStatus() == PHP_SESSION_ACTIVE);
}
/**
* Registers a session save handler
*
* Does nothing if session is already active.
*
* @param \SessionHandlerInterface $sessionHandler
* @param bool $registerShutdown
* @return $this
* @throws SessionException error while registering session save handler
* @see http://php.net/session_set_save_handler
*/
public function setSaveHandler(\SessionHandlerInterface $sessionHandler, $registerShutdown = true)
{
if ($this->isActive()) {
return $this;
}
$success = session_set_save_handler($sessionHandler, $registerShutdown);
if (!$success) {
throw new SessionException(
'error while registering session save handler',
SessionException::ERROR_SESSION_SAVE_HANDLER
);
}
return $this;
}
/**
* Starts the session
*
* Does nothing if session is already active.
*
* You may provide specialized $options that will override this service's options.
* If you will, do not use the 'session.' prefix with the keys of the $options array.
*
* @param array $options
* @return $this
* @throws SessionException error while starting session
* @fires Sessions\PreStart
* @fires Sessions\PostStart
* @see http://php.net/session_start
*/
public function start(array $options = [])
{
if ($this->isActive()) {
return $this;
}
$this->fire('Sessions\PreStart');
$success = session_start($options);
if (!$success) {
throw new SessionException('error while starting session', SessionException::ERROR_SESSION_START);
}
$this->fire('Sessions\PostStart', $this);
return $this;
}
/**
* Writes and closes any active session
*
* Does nothing id the session has not been started yet.
*
* @return $this
* @fires Sessions\PreWriteClose
* @see http://php.net/session_write_close
*/
public function writeClose()
{
if (!$this->isActive()) {
return $this;
}
$this->fire('Sessions\PreWriteClose', $this);
session_write_close();
return $this;
}
/**
* Regenerates the session id
*
* Does nothing id the session has not been started yet.
*
* @param boolean $deleteOldSession
* @return $this
* @throws SessionException error while regenerating session id
* @see http://php.net/session_regenerate_id
*/
public function regenerate($deleteOldSession = true)
{
if (!$this->isActive()) {
return $this;
}
$success = session_regenerate_id($deleteOldSession);
if (!$success) {
throw new SessionException(
'error while regenerating session id',
SessionException::ERROR_SESSION_REGENERATE
);
}
return $this;
}
/**
* Destroys the current session
*
* Clears all values from the session and marks it as outdated first.
*
* @return $this
* @throws SessionException error while destroying the session
* @fires Sessions\PreDestroy
* @see http://php.net/session_destroy
*/
public function destroy()
{
$this->fire('Sessions\PreDestroy', $this);
$this->unset()->markOutdated();
$success = session_destroy();
if (!$success) {
throw new SessionException(
'error while destroying the session',
SessionException::ERROR_SESSION_DESTROY
);
}
return $this;
}
/**
* Sends a session cookie with an outdated expiration date
*
* Does nothing if session doesn't use cookies
*
* @return $this
*/
public function markOutdated()
{
// send outdated session cookie
if (!ini_get('session.use_cookies')) {
return $this;
}
$params = session_get_cookie_params();
setcookie(
$this->getName(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
return $this;
}
/**
* Retrieves a stored session value
*
* If no value is present using $key, null is returned.
*
* @param string $key
* @return mixed
*/
public function get(string $key)
{
return $_SESSION[$key] ?? null;
}
/**
* Sets a session value
*
* @param string $key
* @param mixed $value
* @return $this
*/
public function set(string $key, $value)
{
$_SESSION[$key] = $value;
return $this;
}
/**
* Checks whether a certain session value is set
*
* Keep in mind, that null values will be treated is unset.
*
* @param string $key
* @return bool
*/
public function has(string $key): bool
{
return isset($_SESSION[$key]);
}
/**
* Clears session values
*
* If $key is set, only the value using the $key is unset.
*
* @param string $key
* @return $this
* @see http://php.net/session_unset
*/
public function unset(string $key = null)
{
if (is_null($key)) {
session_unset();
$_SESSION = [];
return $this;
}
unset($_SESSION[$key]);
return $this;
}
/**
* {@inheritDoc}
*/
protected function initialize(array $options)
{
// merge own defaults
$options = array_merge(self::ENHANCED_CONFIG_DEFAULTS, $options);
parent::initialize($options);
$this->fireEvents = $this->getOption('fire-events', false);
$errors = [];
$this->configureSessionModule($options, $errors);
// initialize session handler (if any)
if (!isset($options['custom_handler'])) {
return;
}
try {
$sessionHandler = SF::i()->get($options['custom_handler']);
if (!($sessionHandler instanceof \SessionHandlerInterface)) {
$errors[] = '[' . $options['custom_handler'] . '] is not a \SessionHandlerInterface';
}
} catch (ClassNotFoundException | ConfigurationException $exception) {
$errors[] = 'unable to retrieve [' . $options['custom_handler'] . '] from the service factory - '
. '[' . (string)$exception . ']';
}
if (!empty($errors)) {
throw new ConfigurationException($errors);
}
}
/**
* Configure php's session module
*
* @param array $options
* @param array $errors
*/
protected function configureSessionModule(array $options, array $errors)
{
foreach ($options as $key => $value) {
if (substr($key, 0, 8) != 'session.') {
continue;
}
$result = ini_set($key, (string)$value);
if ($result === false) {
$errors[] = 'error while processing option [' . $key . ']';
}
}
}
}

View File

@ -20,14 +20,14 @@ class RenderedDocument
/**
* @var string
*/
private $contents;
protected $contents;
/**
* @param string $contents
*/
public function __construct(string $contents)
{
$this->setContents($contents);
$this->contents = $contents;
}
/**