Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Passing Values to Stimulus

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Setting a global variable is fine. But if you're using Stimulus, there's a better way. We can pass server data as a value to a Stimulus controller.

Of course, this is a Vue app. But if you look in templates/main/homepage.html.twig, we're using the symfony/ux-vue package to render this:

... lines 1 - 2
{% block body %}
<div {{ vue_component('TreasureConnectApp', {
entrypoint: path('api_entrypoint')
}) }}></div>
{% endblock %}

Behind the scenes, that activates a small Stimulus controller that starts & renders the Vue component. Any arguments that we pass here are sent to the Stimulus controller as a value... and then forwarded as props to the Vue app. So what we're going to do is "kind of" specific to Vue, but you could use this strategy to pass values to any Stimulus controller.

First in the Vue component, let's allow a new prop to be passed in called user:

... lines 1 - 26
<script setup>
... lines 28 - 32
const props = defineProps(['entrypoint', 'user'])
... lines 34 - 40

If you're not using Vue, don't worry too much about the specifics. To make sure that's getting here console.log(props.user). And initialize the data to props.user:

... lines 1 - 26
<script setup>
... lines 28 - 32
const props = defineProps(['entrypoint', 'user'])
const user = ref(props.user);
... lines 36 - 40

Next, over in base.html.twig, remove all that fancy window.user stuff:

<!DOCTYPE html>
... lines 3 - 15
{% block body %}{% endblock %}

And in homepage.html.twig, pass a new user prop set to app.user:

... lines 1 - 2
{% block body %}
<div {{ vue_component('TreasureConnectApp', {
entrypoint: path('api_entrypoint'),
user: app.user
}) }}></div>
{% endblock %}

Now if you move over and refresh, that's doesn't work? It looks like we're authenticated as... nothing?

Serializing Before Passing in the Value

If you dig a little, you'll see that we're sending the user to Stimulus as empty {}. Why? Because when you send data into Stimulus, it doesn't use the serializer to transform into JSON: it just uses json_encode(). And that's not good enough.

So, we need to serialize this ourselves. To do that, open src/Controller/MainController.php. Here's the controller that renders that template. Autowire a service called NormalizerInterface and then pass a variable into our template called userData set to $normalizer->normalize(). Oh, but we need the user! Add another argument to the controller with the fancy new #[CurrentUser] attribute, type-hint User, say $user, and then = null in case we're not authenticated. Back down below, normalization will turn the object into an array. So pass $user and then the format for the array, which is jsonld: we want all the JSON-LD fields. Finally pass the serialization context with 'groups' => 'user:read':

... lines 1 - 4
use App\Entity\User;
... lines 6 - 8
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class MainController extends AbstractController
public function homepage(NormalizerInterface $normalizer, #[CurrentUser] User $user = null): Response
return $this->render('main/homepage.html.twig', [
'userData' => $normalizer->normalize($user, 'jsonld', [
'groups' => ['user:read'],

Last step! In the template, set that user prop to userData:

... lines 1 - 2
{% block body %}
<div {{ vue_component('TreasureConnectApp', {
... line 5
user: userData,
}) }}></div>
{% endblock %}

Since the Stimulus system will run that array through json_encode() that will transform that array into JSON. When we move over and refresh.... got it! You can see the entire JSON being passed into the Stimulus controller... and then that's passed to Vue as a prop.

Spin back over and make sure to get that console.log() out of there:

... lines 1 - 26
<script setup>
... lines 28 - 33
... lines 35 - 40

CSRF Protection

We haven't actually seen it yet, but when we start making requests to our API, those requests will be authenticated thanks to the session. When using sessions with your API, you might read about needing CSRF protection. Do we need CSRF tokens?

The quick answer is: probably not. As long as you use something called SameSite cookies - which are automatic in Symfony - then your API probably doesn't need to worry about CSRF protection. But be aware of two things. First, make sure that your GET requests don't have any side effects. Don't do something silly like allow the API client to make a GET request... but then you save something to the database. Second, some older browsers - like IE 11 - don't support SameSite cookies. So by forgoing CSRF tokens, you could be allowing a small percentage of your users to be susceptible to CSRF attacks.

If you want to learn more, our API Platform 2 tutorial has a whole chapter on SameSite cookies and CSRF tokens.

Next, let's turn to the other authentication use-case: API tokens.

Leave a comment!

Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0