Skip to content

[HttpKernel] Add MapSessionParameter to map session parameters to controllers #54458

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

Open
wants to merge 3 commits into
base: 7.3
Choose a base branch
from

Conversation

jtattevin
Copy link

@jtattevin jtattevin commented Apr 2, 2024

Q A
Branch? 7.3
Bug fix? no
New feature? yes
Deprecations? no
License MIT
Doc PR symfony/symfony-docs#19728

This attribute allow to map an object from/to the session. The read/write to the public properties are proxied to the session using the property name as a key, or the value defined by the attribute SessionKey. There is no prefix, so if two class use the same property, they will read/write to the same session entry, which is intended to have variable shared between context if needed.

This allow to type property and have ide autocompletion, static validation and runtime validation of type. The default value from the property is also used as a default value when reading from session.

Using this method reduce the errors related to a wrong key used or inconsistent default value. It also help seing which key are currently used and avoid conflicts

Calling isset will return if the property is defined in session but will not check if the value is null. Calling unset remove the entry from the session

For example, we can create a class like this :

class BrowsingSessionContext
{
    public string $locale = "en";

    public bool $preferDarkTheme = false;
}

and use it in a controller :

#[Route("/", name: "home")]
class DefaultController extends AbstractController
{
    public function __invoke(
        Request $request,
        #[MapSessionContext] BrowsingSessionContext $browsingSessionContext,
    ) {
        $localeSelector                 = $this
            ->createFormBuilder($browsingSessionContext)
            ->add("locale", TextType::class)
            ->add("preferDarkTheme", CheckboxType::class)
            ->add("submit", SubmitType::class)
            ->getForm()
        ;
        $localeSelector->handleRequest($request);
        if ($localeSelector->isSubmitted() && $localeSelector->isValid()) {
            return $this->redirectToRoute("home");
        }

        return $this->render("index.html.twig", [
            "value"          => $browsingSessionContext->locale,
            "localeSelector" => $localeSelector,
        ]);
    }
}

The term session context come from the idea that we get a lot of information that are often together. I avoided to use something too close of Object to avoid confusion with the SessionInterface object.

This pull request is based in HttpKernel to be close to SessionValueResolver but i'm not confident if the namespaces used are the right ones.

I'm not fond of the eval to create the anonymous class, but didn't managed to create an anonymous class without that. Also, i'm not sure if it's possible to create a proxy during container build in this context.

@carsonbot
Copy link

Hey!

I see that this is your first PR. That is great! Welcome!

Symfony has a contribution guide which I suggest you to read.

In short:

  • Always add tests
  • Keep backward compatibility (see https://symfony.com/bc).
  • Bug fixes must be submitted against the lowest maintained branch where they apply (see https://symfony.com/releases)
  • Features and deprecations must be submitted against the 7.1 branch.

Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change.

When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor!
If this PR is merged in a lower version branch, it will be merged up to all maintained branches within a few days.

I am going to sit back now and wait for the reviews.

Cheers!

Carsonbot

@carsonbot
Copy link

Hey!

Thanks for your PR. You are targeting branch "7.1" but it seems your PR description refers to branch "7.2".
Could you update the PR description or change target branch? This helps core maintainers a lot.

Cheers!

Carsonbot

@carsonbot carsonbot changed the title [WIP] [HttpKernel] Add MapSessionContext to map multiple session value to… [HttpKernel] [WIP] Add MapSessionContext to map multiple session value to… Apr 2, 2024
@jtattevin jtattevin changed the title [HttpKernel] [WIP] Add MapSessionContext to map multiple session value to… [HttpKernel] Add MapSessionContext to map multiple session value to… Apr 2, 2024
@jtattevin
Copy link
Author

jtattevin commented Apr 3, 2024

Status: needs review

@nicolas-grekas

This comment was marked as outdated.

@jtattevin
Copy link
Author

Thank for the feedback.

I didn't think about injecting the object itself, i think it would work yes and be simpler.

Initially i made it this way to migrate a project that was using directly the set/get without constant, so the goal was mainly to proxy the calls and avoid issues like reading 'locale' but writing to '_locale'. In session i wanted to have the individual keys.

I'm not sure which method would be better

@nicolas-grekas
Copy link
Member

Not using proxies is better for sure :)

@jtattevin
Copy link
Author

Update done, i've removed the magic trait and also the possibility to specify the session key for each property.
The class FQDN is the entry in the session

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

I like this idea :)

@nicolas-grekas nicolas-grekas changed the title [HttpKernel] Add MapSessionContext to map multiple session value to… [HttpKernel] Add MapSessionParameter to map session parameters to controllers Apr 4, 2024
@jtattevin
Copy link
Author

Update done, good idea to allow the interface and thank for all the feedback.

I made a change on which i'm not 100% certain if it was the right call :
If you type an interface, you need to make the parameter nullable or provide a default value.

While it is not always mandatory because if there is already a value in session, it would work without a default value, it may lead to an error later if the controller is called without setting the session before. (For example, the server restart and clear the session).

This is a case that may be complicated to detect during development, so trigger the error even if it was possible to continue without error.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

This looks good to me, I only have CS-related comments.

@jtattevin jtattevin force-pushed the MapSession branch 2 times, most recently from 2f18c59 to 8e4af59 Compare April 18, 2024 14:51
@xabbuh xabbuh modified the milestones: 7.1, 7.2 May 15, 2024
@jtattevin jtattevin force-pushed the MapSession branch 2 times, most recently from 1f956b1 to 348c194 Compare May 22, 2024 09:43
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

I like this PR because it enables more decoupling from framework-provided interfaces (SessionInterface here).

Any other opinions on the topic @symfony/mergers? Votes pending 🙏

Copy link
Member

@lyrixx lyrixx left a comment

Choose a reason for hiding this comment

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

Is it a commun thing to store object in session?
It feel a bit wrong to me.
People need to deal with BC between 2 deploys...

return [];
}

if ((!$type = $argument->getType()) || (!class_exists($type) && !interface_exists($type, false))) {
Copy link
Member

@lyrixx lyrixx Aug 22, 2024

Choose a reason for hiding this comment

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

  1. We should deal with nullable type?
  2. It does not work with union / intersection type

Copy link
Author

Choose a reason for hiding this comment

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

Allowing null allow to use an interface or to create the object only if needed.

I didn't checked for the union type, i'll update the PR

Copy link
Member

Choose a reason for hiding this comment

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

compound types aren't compatible with value resolvers IMHO, this one or any other

Copy link
Member

Choose a reason for hiding this comment

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

For the record, I tested, an it fails indeed. It tells the class "App\Foo|AppBar" does not exist

return [$value];
}

if (\is_object($value = $argument->hasDefaultValue() ? $argument->getDefaultValue() : new $type())) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why there is a if is_object here

  1. $argument->getDefaultValue() is of type object, expect if null are allowed, but this is weird?
  2. new $type is an object too

Copy link
Member

Choose a reason for hiding this comment

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

What is there is a constructor with required arguments?

Copy link
Author

Choose a reason for hiding this comment

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

The is_object check if the default value is null or not, if it's null, we don't write in session.

If you have a constructor with arguments, you'll have to provide the default value in the controller :

public function index(
        #[MapSessionParameter()] Foobar $foobar1 = null,
        #[MapSessionParameter()] Foobar $foobar2 = new Foobar("myParam"),
    ): Response

@lyrixx
Copy link
Member

lyrixx commented Aug 22, 2024

And I just tested the PR,

  1. it keeps writing to the session, event if nothing changes
class HomepageController extends AbstractController
{
    #[Route('/')]
    public function index(
        #[MapSessionParameter()] Foobar $foobar4,
    ): Response
    {
        dump($foobar4);

        return $this->render('homepage/index.html.twig', [
        ]);
    }
}


class Foobar
{
    public function __construct(
    ) {
    }
}
  1. I broken ALL FPM worker many time by playing with types. I ran symfony:server and had to restart it each time

@jtattevin
Copy link
Author

Is it a commun thing to store object in session? It feel a bit wrong to me.

The goal of this PR is not to allow storing object but reducing type error and wrong session key error (username vs userName).
If you expect to have an int in session, the property of the object or the method on the interface will require you to provide the right type.
And while the typo part is a little less present in the current version, as it depend mainly on a variable name, it may be easier to spot.

People need to deal with BC between 2 deploys...

Didn't really think about this point. I guess you can use the fact that there is support for interface to leverage this point.
In my mind you have only one or two property per object, so there should not be too much issue with BC, at least not new issues compared to calling session->get directly

@lyrixx
Copy link
Member

lyrixx commented Aug 22, 2024

My point about objects it that is encourage people to store objects in the session, and IMHO this is a terrible idea. So, even if this PR looks nice at glance, it's not a good idea.

@nicolas-grekas
Copy link
Member

But we do ourselves put value objects in the session, I see no issue, with some care of course.

@jtattevin
Copy link
Author

I've added a new commit to add support for the union/intersection type, they are considered like the interface (need to provide a default value or make them nullable).
This also fix if you used FooInterface|null $var instead of FooInterface|null $var = null

There is one point that i didn't find an easy/effective way to do : Check if the data in the session match the expected type to trigger an exception saying that the type in session was not compliant. Instead, the user will have a message like
Argument #N ($browsingSessionContext) must be of type (Foo&Bar)|null, Baz given

Is there a method like that ? (instanceof and is_a doesn't seem to handle union types)
Or can we leave the php default error ?

… to a controller argument

This attribute allow to pass an object from the session.
The read/write to the public properties are proxied to the session using the property name as a key, or the value defined by the attribute SessionKey

This allows to type property and have ide autocompletion, static validation and runtime validation of type.
The default value of the argument is also used as a default value when not defined in session.

In case of interfaces, the user will be required to provide a default value or make the parameter nullable.
This check is done even if the session may have a value already to notify quickly of the potential issue
@GromNaN GromNaN added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Apr 1, 2025
@smnandre
Copy link
Member

smnandre commented May 4, 2025

Two very genuine questions about this attribute, sorry if it has been mentionned in one of the previous threads.

First: Isn't this the only value resolver that write things? If so I guess this is something that should be written in bold in the documentation.

Second (related) question: does this mean if a controller is protected by a IsGranted attribute --as the request arguments have to be resolved before calling the authorisation request listener-- with this PR the resolver will save data in session before authorisation. In case of access denied, or any error in fact... the value is not rollback to its previous state, or did I miss something ?

@chalasr
Copy link
Member

chalasr commented May 5, 2025

First: Isn't this the only value resolver that write things? If so I guess this is something that should be written in bold in the documentation.

This is not desired to me, I would remove the Session::set() call.

@jtattevin
Copy link
Author

Two very genuine questions about this attribute, sorry if it has been mentionned in one of the previous threads.

This was not mentionned before, and for the second question, i have not a clue, so i'll dive more into this part, so thank you :)

First: Isn't this the only value resolver that write things? If so I guess this is something that should be written in bold in the documentation.

It may be the only resolver that write, it was to avoid the case where depending of if the session was already set or not, the value would save automatically or not.
If the behavior stay like this, the documentation will be updated to indicate this part.

Second (related) question: does this mean if a controller is protected by a IsGranted attribute --as the request arguments have to be resolved before calling the authorisation request listener-- with this PR the resolver will save data in session before authorisation. In case of access denied, or any error in fact... the value is not rollback to its previous state, or did I miss something ?

I don't know enough about the exact order for this process, i'll deep dive into this part to understand more.
In this case, either you already have a value, then the value is only read from session, or you don't, then a default value is written in session. It can break something if you depend on the session->has.

From all the feedback, i get that the fact that it is an object that is stored and we write automatically in the session is not the intended behavior.
We can either rollback to the original version where the class was proxied to call session->get/set instead of reading properties.
Or make the autowire attribute work only with argument that have default value or nullable with no auto-set, so the user should always be saving manually and handle creation (maybe allow to autowire the session or the setter).

@fabpot fabpot removed the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label May 17, 2025
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.

10 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy