odan / session

A middleware oriented session handler for PHP and Slim 4+
https://odan.github.io/session/
MIT License
56 stars 11 forks source link

Flash usage with Slim 4 and Twig #32

Closed gRegorLove closed 1 year ago

gRegorLove commented 1 year ago

I'm having an issue where I can add flash messages from a Controller or Service class and confirm the messages are in flash storage with a debug of the getFlash() method, but then they don't appear in the Twig template.

General usage of the get and set methods is working OK. For example I can authenticate a user and store their information in the session for subsequent page views.

Environment: odan/session: 6.1.0 slim: 4.12.0 slim/twig-view: 3.2.0 php-di: 6.3.4 php-di/slim-bridge: 3.4.0

My relevant container definitions:

use Odan\Session\{
    PhpSession,
    SessionInterface,
    SessionManagerInterface
};
use Slim\Views\{
    Twig,
    TwigMiddleware
};
use Twig\TwigFunction;

SessionManagerInterface::class => function (ContainerInterface $container) {
    return $container->get(SessionInterface::class);
},

SessionInterface::class => function (ContainerInterface $container) {
    $options = $container->get('settings')['session'];
    return new PhpSession($options);
},

Twig::class => function (ContainerInterface $container) {
    # truncated start of definition
    $environment = $twig->getEnvironment();
    $session = $container->get(SessionInterface::class);
    $environment->addGlobal('flash', $session->getFlash());
    # truncated rest of this definition
},

TwigMiddleware::class => function (ContainerInterface $container) {
    return TwigMiddleware::createFromContainer($container->get(App::class), Twig::class);
},

In a Twig partial I have:

{%- set errors = flash.get('errors') -%}
{% if errors|length > 0 %}
    {% for message in errors %}
    <div class="alert alert-danger">
        {{ message|raw }}
    </div>
    {% endfor %}
{% endif %}

Then in a Controller method:

private SessionInterface $session;

private Responder $responder;

public function __construct(
    SessionInterface $session,
    Responder $responder
) {
    $this->session = $session;
    $this->responder = $responder;
}

public function index(
    ServerRequestInterface $request,
    ResponseInterface $response
) {
    $this->session->getFlash()->add('errors', 'Oops!');
    dd($this->session->getFlash()); # enable this line to debug

    return $this->responder
        ->withTemplate(
            $response,
            'account/index.twig'
        );
}

That debug (dd) call shows the message in storage:

Odan\Session\Flash Object
(
  [storage:Odan\Session\Flash:private] => Array
    (
      [_flash] => Array
        (
          [errors] => Array
            (
              [0] => Oops!
            )

        )
    )
)

But if I remove the debug call and let the Twig template load, it doesn't display the message.

I thought there might be an issue with scope of included Twig partials, so tried debugging directly in index.twig: {{ debug(flash) }} shows it's a Flash object, but it appears to be empty:

Odan\Session\Flash Object
(
    [storage:Odan\Session\Flash:private] => Array
        (
        )

    [storageKey:Odan\Session\Flash:private] => _flash
)

I've also tried this with setting a flash message on one page and redirecting to another, but the same thing happens.

I realize this might be an issue with my PHP-DI configuration, but I've tried a lot of variations and haven't had luck. I wanted to ask here in case you have any examples of Slim+PHP-DI+Twig implementations of this. I checked your slim-skeleton package and didn't see it there.

Oh, almost forgot. I did try changing the order of middleware but that doesn't seem to have an impact. Initially I had:

return function (App $app) {
    $app->addBodyParsingMiddleware();

    # truncated ...

    $app->add(SessionStartMiddleware::class);

    // The RoutingMiddleware should be added after our CORS middleware so routing is performed first
    $app->addRoutingMiddleware();

    $app->add(TwigMiddleware::class);

    $app->add(ErrorHandlerMiddleware::class);

    // The ErrorMiddleware should always be the outermost middleware
    $app->addErrorMiddleware(true, true, true);
};

And I tried moving the SessionStartMiddleware below the TwigMiddleware:

    $app->add(TwigMiddleware::class);
    $app->add(SessionStartMiddleware::class);

Thanks for any pointers you might have!

odan commented 1 year ago

The DI container is not tied to a specific HTTP request context, whereas the Session instance is specific to the HTTP context. Therefore, if you instantiate Twig before the SessionStartMiddleware initiates the actual session handler, both the session and its flash storage in Twig will be empty. This is because the session hasn't been started when the DI container is being defined.

image

To correctly load the Flash reference in Twig, you must pass the flash variable within the HTTP-specific context. Implement a middleware to achieve this:

First, remove Session (and Twig-Environment) -specific lines from the DI container definition:

$environment = $twig->getEnvironment(); $session = $container->get(SessionInterface::class); $environment->addGlobal('flash', $session->getFlash());

Next, add a middleware to set the appropriate Flash instance as a global variable in Twig:

File: src/Middleware/TwigFlashMiddleware.php

<?php

namespace App\Middleware;

use Odan\Session\SessionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Views\Twig;

final class TwigFlashMiddleware implements MiddlewareInterface
{
    private Twig $twig;
    private SessionInterface $session;

    public function __construct(Twig $twig, SessionInterface $session)
    {
        $this->twig = $twig;
        $this->session = $session;
    }

    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        $flash = $this->session->getFlash();
        $this->twig->getEnvironment()->addGlobal('flash', $flash);

        return $handler->handle($request);
    }
}

Then add the TwigFlashMiddleware::class before the SessionStartMiddleware::class.

// ...
$app->add(TwigFlashMiddleware::class);
$app->add(SessionStartMiddleware::class);
// ...

I hope this helps.

gRegorLove commented 1 year ago

Thanks so much! I'll try this out.

gRegorLove commented 1 year ago

That worked a charm!

tertek commented 3 months ago

Next, add a middleware to set the appropriate Flash instance as a global variable in Twig:

This is super helpful and should be in the docs under Twig section.