Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Logout & Passing API Data to JavaScript

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.

What does it mean to "log out" of something? Like logging out of an API? Well, it's two things. First, it means invalidating whatever your token is, if possible. For example, if you have an API token, you would say to the API:

Make this API token no longer valid.

In the case of session authentication, it's basically the same: it means removing the session from the session storage.

The second part of "logging out" is making whoever is using the token "forget" it. If you had an API token in JavaScript, you would remove it from JavaScript. For session authentication, it means deleting the cookie.

Adding the Ability to Log Out

Anyways, let's add the ability to log out of our session-based authentication. Back over in SecurityController, like before, we need a route and controller, even though this controller will never be called. I'll name the method logout() and we're going to return void. You'll see why in a second. Give this a Route of /logout and name: app_logout:

... lines 1 - 10
class SecurityController extends AbstractController
... lines 13 - 26
#[Route('/logout', name: 'app_logout')]
public function logout(): void
... line 30

The reason I chose void is because we're going to throw an exception from inside the method. We've created this entirely because we need a route: Symfony's security system will intercept things before the controller is called:

... lines 1 - 10
class SecurityController extends AbstractController
... lines 13 - 26
#[Route('/logout', name: 'app_logout')]
public function logout(): void
throw new \Exception('This should never be reached!');

To activate that magic, in security.yaml, add a key called logout with path below set to that new route name: app_logout:

... lines 2 - 11
... lines 13 - 15
... lines 17 - 22
path: app_logout
... lines 25 - 50

This activates a listener that's now watching for requests to /logout. When there is a request to /logout, it will log the user out and redirect them.

All right, over here, our Vue app thinks we're not logged in, but we are: we can see it in the web debug toolbar. And if we manually go to /logout... boom! We are now logged out for real.

Getting the Current User Data in JavaScript

So we saw a moment ago that even when we are logged in and refresh, our Vue app has no idea that we're logged in. How could we fix that? One idea would be to create a /me API endpoint. Then, on load, our Vue app could make an AJAX request to that endpoint... which would either return null or the current user info. But, /me endpoints are super not RESTful. And there's a better way: dump the user information into JavaScript on page load.

Setting a Global user JavaScript Variable

There are two different ways to do this. The first is by setting a global variable. For example, in templates/base.html.twig, it doesn't really matter where, but inside the body, add a script tag. And here say window.user = and then {{ app.user|serialize }}. Serialize into jsonld and add a |raw so that it doesn't escape the output: we want raw JSON:

<!DOCTYPE html>
... lines 3 - 15
window.user = {{ app.user|serialize('jsonld')|raw }};
{% block body %}{% endblock %}

How cool is that? In a minute, we'll read that from our JavaScript. If we refresh right now and look at the source, yea! We see window.user = null. And then when we log in and refresh the page, check it out: window.user = and a huge amount of data!

Serializing to JSON-LD in Twig

But there's something mysterious going on: it has the correct fields! Look closely, it has email, username and then dragonTreasures, which is what all this stuff is. It also, correctly, does not have roles or password.

So it seems that it's correctly reading our normalization groups! But how is that even possible? We're just saying "serialize this user to jsonld". This has nothing to do with API Platform and it's not being processed by API platform. But... our normalization groups are configured in API Platform. So how did the serializer know to use those?

The answer to that, as best I can tell, is that it's working... partially by accident. During serialization, API Platform sees that we're serializing an "API resource" and so it looks up the metadata for this class.

That's cool... but it's actually not perfect... and I like to be explicit anyway. Pass a 2nd argument to serialize, which is the context and set groups to user:read:

<!DOCTYPE html>
... lines 3 - 15
window.user = {{ app.user|serialize('jsonld', {
'groups': ['user:read']
})|raw }};
... lines 22 - 23

Now, watch what happens when we refresh. Like before, the correct properties on User will be exposed. But keep an eye on the embedded dragonTreasures property. Woh, it changed! That was actually wrong before: it was including everything, not just the stuff inside the user:read group.

Reading the Dynamic Data from Vue

Ok, let's go use this global variable over in JavaScript: in TreasureConnectApp.vue. Right now, the user data always starts as null. We can change that to window.user:

... lines 1 - 26
<script setup>
... lines 28 - 32
const user = ref(window.user);
... lines 35 - 39

When we refresh... got it!

Next: if you're using Stimulus, an even better way to pass data to JavaScript is to use Stimulus values.

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