Skip to content

No longer assume session service has a getFlashBag method #32387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from

Conversation

rpkamp
Copy link
Contributor

@rpkamp rpkamp commented Jul 5, 2019

Q A
Branch? 3.4
Bug fix? yes
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets N/A
License MIT
Doc PR N/A

In runtime the sessions service usually has a getFlashBag method, because the symfony Session class has that method, but it's not on the SessionInterface, so we must not assume it exists.
Calling it breaks custom implementation of the SessionInterface that do not have the getFlashBag method.

In runtime it usually does, because the symfony Session class has that
method, but it's not on the SessionInterface, so we must not assume it
exists. Calling it breaks custom implementation of the SessionInterface
that do not have the getFlashBag method.
@ro0NL
Copy link
Contributor

ro0NL commented Jul 5, 2019

should we add it using @method? And cleanup in 5.0 ...

@rpkamp
Copy link
Contributor Author

rpkamp commented Jul 5, 2019

That was what I wanted to propose initially, but it seems it was decided against that. See #5568 (comment)

Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im still skeptical about method_exists, that's not a contract.

What about an internal utility wrapper to get us a flashbag from a SessionInterface (FlashBag::createFromSession). Then we could additionally try getBag('flashes') by convention for a fallback.

@derrabus
Copy link
Member

derrabus commented Jul 5, 2019

im still skeptical about method_exists, that's not a contract.

Yes, I also don't like the duck typing here. But it's the most robust way I see at the moment for a bugfix release.

To improve this, we could introduce a new interface that provides getFlashBag() with 4.4 and trigger a deprecation if the session implements getFlashBag() without the interface.

Then we could additionally try getBag('flashes') by convention for a fallback.

The assumption that the flash bag is stored as flashes could be wrong.

@ro0NL
Copy link
Contributor

ro0NL commented Jul 5, 2019

for a bugfix release

Right, i wasnt aware the PR targets 3.4 :) But i dont find it robust without any additional type check for FlashBagInterface. Hence the suggested helper to tackle the boilerplate.

The assumption that the flash bag is stored as flashes could be wrong.

Not if we type check as such :)

Im thinking

function getMeAFlashBag(SessionInterface $session): ?FlashBagInterface
{
    if instanceof Session; return getFlashBag()
    if getBag(flashes) instanceof FlashBagInterface; return getBag('flashes')
    return null
}

FlashBag::createFromSession

That wont work for 3.4 since it's not final. So the drawback is we need to add an internal new symbol in 3.4. But it's the robust way IMHO that remains viable for now without feature creeping it.

@derrabus
Copy link
Member

derrabus commented Jul 5, 2019

That'd break if I had a session like this one:

class MyOwnSession implements SessionInterface
{
     //…

    public function getFlashBag(): FlashBagInterface
    {
        return $this->getBag('my_own_flashes');
    }
}

@ro0NL
Copy link
Contributor

ro0NL commented Jul 5, 2019

Fair :) 3.4 needs a patch either way. For 4.4 we could let the framework configure the intended bag name (e.g. my_own_flashbag), or fix the type system.

Copy link
Contributor

@linaori linaori left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got one minor comment, looks like a good change overall!

rpkamp added 2 commits July 6, 2019 08:53
Also fix return type for AppVariable::getSession(), which at runtime
would indeed mostly return a Session instance, but not necessarily
@rpkamp
Copy link
Contributor Author

rpkamp commented Jul 6, 2019

im still skeptical about method_exists, that's not a contract.

My point here was that the use of getFlashBag is developer knowledge that is not documented anywhere and not in any contract. That's awkward, so an awkward solution makes them acutely aware of the problem 🙂

For 4.4 we could let the framework configure the intended bag name (e.g. my_own_flashbag), or fix the type system.

I feel like fixing the type system would be the more sustainable solution in the long run, i.e., the @method on the interface, issue a user deprecation when a class is encountered that implements SessionInterface but does not have a getFlashBag method, and then in 5.0 add the method to the interface.

@ro0NL
Copy link
Contributor

ro0NL commented Jul 6, 2019

the use of getFlashBag is developer knowledge that is not documented anywhere and not in any contract

IMHO the only truth is

/**
* Gets the flashbag interface.
*
* @return FlashBagInterface
*/
public function getFlashBag()

so if anything, we expect a FlashBagInterface from calling getFlashBag(). But letting it crash at this point is fine i guess :)

I agree with #5568 (comment), we don't need it in a contract per se, having the bag name is sufficient.

@linaori
Copy link
Contributor

linaori commented Jul 6, 2019

I don't think the flash bag method will ever be added to the interface: #20258 (comment)

@rpkamp
Copy link
Contributor Author

rpkamp commented Jul 6, 2019

having the bag name is sufficient.

But what if people are implementing a SaaS and have multiple flash bags, one per tenant?

@linaori
Copy link
Contributor

linaori commented Jul 6, 2019

Not sure how this would be something per tenant, as multi tenancy is usually not recommended at application level

@rpkamp
Copy link
Contributor Author

rpkamp commented Jul 6, 2019

Fair enough. Just meant that the fact that there is one flash bag is also an assumption, and there are reasons there might be multiple, with different names.
I don't think the framework should make that impossible / require work-arounds to get it to work.

@derrabus
Copy link
Member

derrabus commented Jul 6, 2019

I don't think the flash bag method will ever be added to the interface: #20258 (comment)

No, but we can introduce a separate interface for that method and end the duck typing with 5.0.

@rpkamp
Copy link
Contributor Author

rpkamp commented Jul 6, 2019

Wouldn't a completely separate interface with a separate implementation for flash messages be nicer. as opposed to retro-fitting an interface to a flawed design?

If we add an interface for getFlashBag() to Session than all you get is a flash bag. I don't want a flash bag, I want to add / retrieve flash messages. That something somewhere is using a flash bag is fine by me, but not relevant to what I want to do (it's a leaky abstraction).

So how about something like

interface FlashMessageProvider
{
    public function add(string $type, $message): void;
    public function get(string $type, $default = []): array;
    public function getAll(): array;
    public function peek(string $type, $default = []): array;
    public function peekAll(): array;
    public function has(string $type): bool;
}

(I'm not stuck on the name, feel free to provide a better name!)

And then the default implementation could be injected a SessionInterface and then the service can add a FlashMessageBag of its own to that SessionInterface:

class SessionFlashMessageProvider
{
    private $bagName;
    public function __construct(SessionInterface $session)
    {
        $flashMessageBag = new FlashMessageBag();
        $this->bagName = $flashMessageBag->getName();
        $session->registerBag($flashMessageBag);
    }
}

The Session class itself would then no longer create and register a flash bag. so all notion of flash messages is gone from the Session class.

Later on the methods of the FlashMessageBagInterface can be simplified; it doesn't need to know about the difference between get and peek; that's a service consideration, not a storage consideration. Or maybe the FlashMessageBagInterface can be removed altogether, leaving just the default implementation for the SessionFlashMessageProvider.

So then it is using the session to store and read flash messages, but the session itself has no idea this is happening. Which, going by the current implementation, is what we wanted in the first place.

Also, if people want to store the messages elsewhere / have multiple FlashMessageBags / whatever, they are free to implement their own FlashMessageProvider and go nuts 🙂

Additionaly, as a migration path, Session::getFlashBag() would raise an E_USER_DEPRECATED stating that the new flash message service must be used instead, and internally the classes like AbstractController and AppVariable can already use the new service. So the only people who will have to change their code are the people who request call getFlashBag() from the session manually.

To return the flash bag from Session::getFlashBag() we may have to add a temporary BC layer to registerBag that checks if the bag implement FlashMessageBagInterface and keep a reference to it for when someone calls getFlashBag(). This BC layer can be removed once the getFlashBag() method is removed.

Do you think this would be viable? If so, I'll work it out some more and submit an RFC.

@derrabus
Copy link
Member

derrabus commented Jul 6, 2019

@rpkamp The interface that you’re describing is the FlashBagInterface. We have that already and I don’t see a reason why we should create a copy of it nor would I need another abstraction of it.

@rpkamp
Copy link
Contributor Author

rpkamp commented Jul 6, 2019

The interface is indeed the same, but it puts it top level, disconnected from the session (yes, they are connected by implementation, but not by concept).

The implementation right now is in the Session but pretends it's not, which is weird. If as it seems we don't want it in the session, we should not put it there, but somewhere else.

Also please note that if we introduce such a FlashMessageProvider, the FlashBagInterface can be trimmed to down, so they won't be the same anymore.

The user land code will be nicer though (IMO):

$this->flashMessageProvider->add('notice', 'Something happened!');

as apposed to

$this->session->getFlashBag()->add('notice', 'Something happened');

(which violates the law of Demeter BTW)

On the other hand, people will mostly use the AbstractController or AppVariable to interact with flash messages anyway, and for that use case it doesn't really matter how it is implemented, and adding an interface would make it already better than it is now, so maybe we should just do that.

@linaori
Copy link
Contributor

linaori commented Jul 6, 2019

There's work in progress to remove the session from the container, because it's an object bound to http context. If a separate object would be introduced for flashes, it should probably not be used as service.

@derrabus
Copy link
Member

derrabus commented Jul 6, 2019

The user land code will be nicer though (IMO):

The flash bag is already a standalone service. You don't have to take the detour via the session. Also, if you directly use the flash bag, your userland code does not need to care that the session is used to persist the flashes.

class MyService
{
    public function __construct(FlashBagInterface $flashBag)
    {
        $this->flashBag = $flashBag;
    }

    public function doSometing()
    {
        // …
        $this->flashBag->add('notice', 'Something happened');
        // …
    }
}

@nicolas-grekas nicolas-grekas added this to the 3.4 milestone Jul 8, 2019
@fabpot
Copy link
Member

fabpot commented Jul 8, 2019

What about removing access to the flash bag from the Session altogether and properly inject the FlashBagInterface (explicitly)?

@linaori
Copy link
Contributor

linaori commented Jul 8, 2019

@fabpot that would introduce the same coupling as to the @session. Imo the flash bag (and session) are stateful objects that should be passed by context, similar to how the request is used. Injecting the flash bag (as well as the session), in an event listener to set a message for example, would fail if triggered from the commandline (as in, trying to set session data in the command).

Seeing the Request is usually the point of entry for events or otherwise available in the majority of code and passed along as request related state, perhaps this should be a responsibility of the Request?

@rpkamp
Copy link
Contributor Author

rpkamp commented Jul 8, 2019

At least it would loose it's dubious state of being implemented in the Session but not wanting to be implemented there, so in my opinion it is certainly better than the status quo.

Also, moving the session to the Request seems weird, since a Request is ephemeral, but session data is not, so once you throw away the Request the session data is still there.

Injecting the flash bag (as well as the session), in an event listener to set a message for example, would fail if triggered from the commandline (as in, trying to set session data in the command).

When the flash messages are moved to the Request so the listener would set a flash message on the Request this would still fail because there is no Request. Different check, same effect.

@derrabus
Copy link
Member

derrabus commented Jul 8, 2019

I think #10557 is the ticket for this discussion. :-)

@linaori
Copy link
Contributor

linaori commented Jul 8, 2019

Also, moving the session to the Request seems weird, since a Request is ephemeral, but session data is not, so once you throw away the Request the session data is still there.

@rpkamp The session is already present: Request::getSession() : SessionInterface

@nicolas-grekas
Copy link
Member

Closing: this staled, and doesn't solve a real-world use case (or at least it's not clear which one it solves.)
@fabpot's suggestion above might be worth it, on master.
PR welcome.

@Saryon61
Copy link

When using a form authenticator (AbstractFormLoginAuthenticator), the method "onAuthenticationSuccess" output a redirection. I would like to display a message of success for the user who successfully logged in. The use of the flashbag from the authenticator would have been nice for that use case. Do you have advise to solve this use case?
Thanks in advance.

@rpkamp
Copy link
Contributor Author

rpkamp commented Oct 23, 2020

Sounds like something unrelated from this ticket @Saryon61. I would recommend you open a new ticket with your question.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants