mindplay/stately

This package is abandoned and no longer maintained. The author suggests using the kodus/session package instead.

Session abstraction for PSR-7

dev-master 2015-08-06 13:24 UTC

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.