Sessions

Melodic provides a clean session abstraction through SessionInterface, with a production-ready NativeSession that wraps PHP's built-in session handling and an ArraySession for testing. Sessions are registered as singletons through the service provider and can be injected into any controller or service.

SessionInterface

The Melodic\Session\SessionInterface defines the contract for all session implementations. Every method is explicit — there are no magic getters or hidden state.

Method Signature Description
start() start(): void Begin or resume the session
get() get(string $key, mixed $default = null): mixed Retrieve a value by key, or return the default
set() set(string $key, mixed $value): void Store a value in the session
has() has(string $key): bool Check whether a key exists in the session
remove() remove(string $key): void Remove a key from the session
destroy() destroy(): void Destroy the entire session and clear all data
regenerate() regenerate(bool $deleteOld = true): void Generate a new session ID, optionally deleting the old one
isStarted() isStarted(): bool Check whether a session is currently active
<?php
use Melodic\Session\SessionInterface;

interface SessionInterface
{
    public function start(): void;
    public function get(string $key, mixed $default = null): mixed;
    public function set(string $key, mixed $value): void;
    public function has(string $key): bool;
    public function remove(string $key): void;
    public function destroy(): void;
    public function regenerate(bool $deleteOld = true): void;
    public function isStarted(): bool;
}

NativeSession

The Melodic\Session\NativeSession class is the default production implementation. It wraps PHP's native session functions (session_start(), session_destroy(), session_regenerate_id()) and operates on the $_SESSION superglobal.

Auto-Starting

NativeSession automatically starts the session the first time you call get(), set(), has(), or remove(). You do not need to call start() manually in most cases — data access triggers it for you.

<?php
use Melodic\Session\NativeSession;

$session = new NativeSession();

// Session starts automatically on first access
$session->set('user_id', 42);
$userId = $session->get('user_id');         // 42
$session->has('user_id');                    // true

// Retrieve with a default value
$theme = $session->get('theme', 'dark');     // 'dark' (key not set)

// Remove a single key
$session->remove('user_id');

// Check session status
$session->isStarted();                      // true

Session Lifecycle

The start() method checks session_status() before calling session_start(), so calling it multiple times is safe. Destroying a session calls session_destroy() and clears the $_SESSION array.

<?php
// Explicitly start (safe to call multiple times)
$session->start();
$session->start(); // No-op, already started

// Destroy the session entirely
$session->destroy();
$session->isStarted(); // false

Safe restarts. After calling destroy(), you can call start() again or simply use get()/set() — auto-start will create a new session automatically.

ArraySession

The Melodic\Session\ArraySession class provides an in-memory session implementation backed by a plain PHP array. It is designed for unit and integration testing where you need to verify session behavior without touching the filesystem or PHP's session handler.

<?php
use Melodic\Session\ArraySession;

$session = new ArraySession();

$session->set('cart', ['item_1', 'item_2']);
$session->get('cart');    // ['item_1', 'item_2']
$session->has('cart');    // true

$session->destroy();
$session->has('cart');    // false
$session->isStarted();   // false

Testing tip. Bind ArraySession in your test container so controllers and services receive an in-memory session with no side effects.

Key Differences from NativeSession

Behavior NativeSession ArraySession
Storage $_SESSION superglobal (filesystem) In-memory array
Auto-start on access Yes (get, set, has, remove) No (sets started flag on set)
regenerate() Calls session_regenerate_id() No-op
destroy() Calls session_destroy() and clears $_SESSION Clears the internal array

SessionServiceProvider

The Melodic\Session\SessionServiceProvider registers SessionInterface as a singleton bound to NativeSession. Include it in your application bootstrap to enable session injection throughout your app.

<?php
use Melodic\Core\Application;
use Melodic\Session\SessionServiceProvider;

$app = new Application(__DIR__);
$app->loadEnvironmentConfig();

$app->services(function ($container) {
    (new SessionServiceProvider())->register($container);
});

$app->run();

Under the hood, the provider does a single call:

<?php
// SessionServiceProvider::register()
$container->singleton(SessionInterface::class, NativeSession::class);

Singleton lifetime. Because the session is registered as a singleton, the same NativeSession instance is shared across all controllers, middleware, and services within a single request.

Usage in Controllers and Services

Once the service provider is registered, you can type-hint SessionInterface in any constructor and the DI container will inject the shared instance automatically.

Controller Example

<?php
use Melodic\Controller\Controller;
use Melodic\Http\Response;
use Melodic\Session\SessionInterface;

class CartController extends Controller
{
    public function __construct(
        private readonly SessionInterface $session,
    ) {}

    public function addItem(): Response
    {
        $items = $this->session->get('cart_items', []);
        $items[] = $this->request->input('product_id');
        $this->session->set('cart_items', $items);

        return $this->json(['count' => count($items)]);
    }

    public function clear(): Response
    {
        $this->session->remove('cart_items');

        return $this->noContent();
    }
}

Service Example

<?php
use Melodic\Service\Service;
use Melodic\Session\SessionInterface;

class OnboardingService extends Service
{
    public function __construct(
        private readonly SessionInterface $session,
    ) {}

    public function markStepComplete(string $step): void
    {
        $completed = $this->session->get('onboarding_steps', []);
        $completed[] = $step;
        $this->session->set('onboarding_steps', array_unique($completed));
    }

    public function isComplete(): bool
    {
        $completed = $this->session->get('onboarding_steps', []);
        $required = ['profile', 'preferences', 'confirmation'];

        return count(array_diff($required, $completed)) === 0;
    }
}

Session Security Patterns

Session fixation and session hijacking are common attack vectors. Melodic's session interface provides the tools you need to defend against them.

Regenerate After Login

Always regenerate the session ID after a user authenticates. This prevents session fixation attacks where an attacker sets a known session ID before the user logs in.

<?php
class AuthController extends Controller
{
    public function __construct(
        private readonly SessionInterface $session,
        private readonly AuthService $auth,
    ) {}

    public function login(): Response
    {
        $user = $this->auth->attempt(
            $this->request->input('email'),
            $this->request->input('password'),
        );

        if ($user === null) {
            return $this->json(['error' => 'Invalid credentials'], 401);
        }

        // Regenerate session ID to prevent fixation
        $this->session->regenerate();

        $this->session->set('user_id', $user->id);
        $this->session->set('authenticated_at', time());

        return $this->json(['user' => $user->toArray()]);
    }
}

Always regenerate. Calling regenerate() after login is critical. Without it, an attacker who knows the pre-login session ID retains access after authentication.

Destroy on Logout

When a user logs out, destroy the session entirely rather than just removing individual keys. This ensures no stale data persists.

<?php
public function logout(): Response
{
    $this->session->destroy();

    return $this->json(['message' => 'Logged out']);
}

Testing with ArraySession

In your test suite, override the session binding so you can assert session state without a running PHP session:

<?php
use Melodic\DI\Container;
use Melodic\Session\SessionInterface;
use Melodic\Session\ArraySession;

$container = new Container();
$container->singleton(SessionInterface::class, ArraySession::class);

// Resolve your controller or service
$controller = $container->get(AuthController::class);

// After calling login, inspect the session
$session = $container->get(SessionInterface::class);
assert($session->has('user_id'));
assert($session->get('user_id') === 42);

Class Reference

Class Namespace Purpose
SessionInterface Melodic\Session Contract for all session implementations
NativeSession Melodic\Session Production implementation wrapping PHP's native sessions
ArraySession Melodic\Session In-memory implementation for testing
SessionServiceProvider Melodic\Session Registers SessionInterfaceNativeSession as a singleton