mindplay / stately
Session abstraction for PSR-7
Requires
- php: >=5.5.4
- psr/http-message: ^1
Requires (Dev)
- guzzlehttp/guzzle: ~6
- mindplay/testies: dev-master
- phpunit/php-code-coverage: 2.*@dev
- zendframework/zend-diactoros: ~1
- zendframework/zend-stratigility: ~1
This package is auto-updated.
Last update: 2021-06-05 13:50:35 UTC
README
This library implements a simple session abstraction that integrates with a PSR-7 HTTP abstraction, to replace PHP's built-in session management.
This means you will not be using $_SESSION
or the session_*()
functions.
The API approach relies on type-hinted closures as a means of creating/updating session state. In practice, this means every session variable will be an object, which means your session state will really be exposed to you as a model, not as an array or a key/value store; which is great for IDE support and for code comprehension in general - for example:
$product_id = 123; $amount = 4; $session->update(function (ShoppingCart $cart) use ($product_id, $amount) { $cart->items[] = new CartItem($product_id, $amount); });
Note that the ShoppingCart
model is automatically created, if it isn't already
present in the session data. As such, every session model is a "singleton", as
far as session state goes, though not in the sense that you can't create more
than one instance in a test-suite.
This is analogous to e.g. $_SESSION['ShoppingCart'] = new ShoppingCart()
, but
eliminates the unsafe use of arrays and strings, the need to test if the session
variable is already set, the need to type-hint when reading from $_SESSION
, and
so on.
If you don't want to auto-create a missing model, you can type-hint the argument
as e.g. ShoppingCart $cart = null
- of course, this implies you're going to
handle a missing $cart
in your callback with an if
-statement.
Also note, it's possible to use multiple arguments to obtain multiple session models in a single call.
You can also remove()
session models from the current session, or clear()
the
entire contents of the container - see the SessionContainer
interface for documentation.
Implementation
You should create SessionService
centrally (usually in a DI container) and
expose it to consumers (usually controllers) as SessionContainer
, which is
the public portion of the interface. The internal portion of the interface
includes the commit()
method, which should be called centrally, as part of
your request/response pipeline (middleware, framework or front controller),
e.g. immediately after the controller has been dispatched, before you emit
the response.
Example
Note that auto-creation implies that your session models must always have an
empty constructor - which implies it must have no hard dependencies. Try to view
this as "a good thing" - it means you won't be tempted to do things like adding
a getUser()
method for a $user_id
property, which would imply a dependency
on a UserRepository
of some sort.
This is called "feature envy", and it's an anti-pattern - you're trying to provide a more convenient way to interact with the model, which would be great, except your model is now really a service and not just a model, even if it is internally using a repository to provide that service.
The clean approach to this problem, is a separate user service, which internally
uses the session container to interact with the session model - the intention is
is not for a consumer to reach into $_SESSION
for your component's session data,
just as the expectation is not that a consumer directly accesses your session
model; but rather to encapsulate that exchange privately inside a service.
For example:
class ActiveUser { /** @var int|null active user ID (or NULL, if no User is logged in) */ public $user_id; } class UserService { private $container; private $repo; public function __construct(SessionContainer $container, UserRepository $repo) { $this->container = $container; $this->repo = $repo; } /** * @return User */ public function getUser() { $user_id = $this->container->update(function (ActiveUser $user) { return $user->user_id; }); return $user_id ? $this->repo->getUserById($user_id) : null; } }
The User
model is now conveniently exposed by the UserService
, which internally
uses the ActiveUser
model for session persistence.