Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Real-World Stimulus Example

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.

Let's put Stimulus to the test. Here's our goal: when we click the play icon, we're going to make an Ajax request to our API endpoint... the one in SongController. This returns the URL to where this song can be played. We'll then use that in JavaScript to... play the song!

Take hello_controller.js and rename it to, how about song-controls_controller.js. Inside, just to see if this is working, in connect(), log a message. The connect() method is called whenever Stimulus sees a new matching element on the page.

import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
console.log('I just appeared into existence!');
}
}

Now, over in the template, hello isn't going to work anymore, so remove that. What I want to do is surround each song row with this controller.... so that's this song-list element. After the class, add {{ stimulus_controller('song-controls') }}.

{% extends 'base.html.twig' %}
{% block title %}Create a new Record | {{ parent() }}{% endblock %}
{% block body %}
<div class="container">
... lines 7 - 36
{% for track in tracks %}
<div class="song-list" {{ stimulus_controller('song-controls') }}>
... lines 39 - 50
</div>
{% endfor %}
... lines 53 - 55
</div>
{% endblock %}

Let's try that! Refresh, check the console and... yes! It hit our code six times! Once for each of these elements. And each element gets its own, separate controller instance.

Adding Stimulus Actions

Okay, next, when we click play, we want to run some code. To do that, we can add an action. It looks like this: on the a tag, add {{ stimulus_action() }} - another shortcut function - and pass this the controller name that you're attaching the action to - song-controls - and then a method inside of that controller that should be called when someone clicks this element. How about play.

{% extends 'base.html.twig' %}
{% block title %}Create a new Record | {{ parent() }}{% endblock %}
{% block body %}
<div class="container">
... lines 7 - 36
{% for track in tracks %}
<div class="song-list" {{ stimulus_controller('song-controls') }}>
<div class="d-flex mb-3">
<a href="#" {{ stimulus_action('song-controls', 'play') }}>
<i class="fas fa-play me-3"></i>
</a>
... lines 43 - 49
</div>
</div>
{% endfor %}
... lines 53 - 55
</div>
{% endblock %}

Cool huh? Back in song controller, we don't need the connect() method anymore: we don't need to do anything each time we notice another song-list row. But we do need a play() method.

And like with normal event listeners, this will receive an event object... and then we can say event.preventDefault() so that our browser doesn't try to follow the link click. To test, console.log('Playing!').

import { Controller } from '@hotwired/stimulus';
... lines 2 - 11
export default class extends Controller {
play(event) {
event.preventDefault();
console.log('Playing!');
}
}

Let's go see what happens! Refresh and... click. It's working. It's that easy to hook up an event listener in Stimulus. Oh, and if you inspect this element... that stimulus_action() function is just a shortcut to add a special data-action attribute that Stimulus understands.

Installing and Importing Axios

Ok, how can we make an Ajax call from inside of the play() method? Well, we could use the built-in fetch() function from JavaScript. But instead, I'm going to install a third-party library called Axios. At your terminal, install it by saying:

yarn add axios --dev

We now know what this does: it downloads this package into our node_modules directory, and adds this line to our package.json file.

Oh, and side note: you absolutely can use jQuery inside of Stimulus. I won't do it, but it works great - and you can install - and import - jQuery like any other package. We talk about that in our Stimulus tutorial.

Ok, so how do we use the axios library? By importing it!

At the top of this file, we're already importing the Controller base class from stimulus. Now import axios from 'axios'. As soon as we do that, Webpack Encore will grab the axios source code and include it in our built JavaScript files.

... lines 1 - 11
import axios from 'axios';
... lines 13 - 21

Now, down here, we can say axios.get() to make a GET request. But... what should we pass for the URL? It needs to be something like /api/songs/5... but how do we know what the "id" is for this row?

Stimulus Values

One of the coolest things about Stimulus is that it allows you to pass values from Twig into your Stimulus controller. To do that, declare which values you want to allow to passed in via a special static property: static values = {}. Inside, let's allow an infoUrl value to be passed. I totally just made up that name: I'm thinking we'll pass in the full URL to the API endpoint. Set this to the type that this will be. So, a String.

We'll learn how we pass this value from Twig into our controller in a minute. But because we have this, below, we can reference the value by saying this.infoUrlValue.

... lines 1 - 11
import axios from 'axios';
... line 13
export default class extends Controller {
static values = {
infoUrl: String
}
... line 18
play(event) {
... lines 20 - 21
console.log(this.infoUrlValue);
//axios.get()
}
}

So how do we pass that in? Back in homepage.html.twig, add a second argument to stimulus_controller(). This is an array of the values you want to pass into the controller. Pass infoUrl set to the URL.

Hmm, but we need to generate that URL. Does that route have a name yet? Nope! Add name: 'api_songs_get_one'.

<?php
... lines 3 - 10
class SongController extends AbstractController
{
#[Route('/api/songs/{id<\d+>}', methods: ['GET'], name: 'api_songs_get_one')]
public function getSong(int $id, LoggerInterface $logger): Response
{
... lines 16 - 27
}
}

Perfect. Copy that... and back in the template, set infoURl to path(), the name of the route... and then an array with any wildcards. Our route has an id wildcard.

In a real app, these tracks would probably each have a database id that we could pass. We don't have that yet... so to, kind of, fake this, I'm going to use loop.index. This is a magic Twig variable: if you're inside of a Twig for loop, you can access the index - like 1, 2, 3, 4 - by using loop.index. So we're going to use this as a fake ID. Oh, and don't forget to say id: then loop.index.

... lines 1 - 4
{% block body %}
<div class="container">
... lines 7 - 36
{% for track in tracks %}
<div class="song-list" {{ stimulus_controller('song-controls', {
infoUrl: path('api_songs_get_one', { id: loop.index })
}) }}>
... lines 41 - 52
</div>
{% endfor %}
... lines 55 - 57
</div>
{% endblock %}

Testing time! Refresh. The first thing I want you to see is that, when we pass infoUrl as the second argument to stimulus_controller, all that really does is output a very special data attribute that Stimulus knows how to read. That's how you pass a value into a controller.

Click one of the play links and... got it. Every controller object is passed its correct URL!

Making the Ajax Call

Let's celebrate by making the Ajax call! Do it with axios.get(this.infoUrlValue) - yes, I just typo'ed that, .then() and a callback using an arrow function that will receive a response argument. This will be called when the Ajax call finishes. Log the response to start. Oh, and fix to use this.infoUrlValue.

import { Controller } from '@hotwired/stimulus';
... lines 3 - 11
import axios from 'axios';
... line 13
export default class extends Controller {
... lines 15 - 18
play(event) {
event.preventDefault();
axios.get(this.infoUrlValue)
.then((response) => {
console.log(response);
});
}
}

Alrighty, refresh... then click a play link! Yes! It dumped the response... and one of its keys is data... which contains the url!

Time for our victory lap! Back in the function, we can play that audio by creating a new Audio object - this is just a normal JavaScript object - passing it response.data.url... and then calling play() on this.

import { Controller } from '@hotwired/stimulus';
... lines 2 - 11
import axios from 'axios';
export default class extends Controller {
... lines 15 - 18
play(event) {
event.preventDefault();
axios.get(this.infoUrlValue)
.then((response) => {
const audio = new Audio(response.data.url);
audio.play();
});
}
}

And now... when we hit play... finally! Music to my ears.

If you want to learn more about Stimulus - this was a bit fast - we have an entire tutorial about it... and it's great.

To finish off this tutorial, let's install one more JavaScript library. This one will instantly make our app feel like a single page app. That's next.

Leave a comment!

13
Login or Register to join the conversation
Estelle G. Avatar
Estelle G. Avatar Estelle G. | posted 4 months ago

Why did I not see that Rickroll coming !!!! Well played ^^

1 Reply

You're welcome ;)

Reply
Filip J. Avatar
Filip J. Avatar Filip J. | posted 4 months ago

Good old rickroll, and that was loud as well! :D I'm not sure if it's been reported before or not, but there is an issue with pasting text into this input field, it adds empty rows. With each paste it gets progressively worse. Something to keep in mind.

Great stuff so far, although throughout this course I've been screaming at my monitor: "Where are Models? Surely they're coming any moment now...". Also, as someone who has a background in Laravel, are there Request Validation classes in Symfony? Regex in the middle of URL path seems very unreadable in my opinion.

1 Reply

Hey Filip,

> Good old rickroll, and that was loud as well! :D I'm not sure if it's been reported before or not, but there is an issue with pasting text into this input field, it adds empty rows. With each paste it gets progressively worse. Something to keep in mind.

I think I know what exactly you're talking about... if you're talking about Disqus comment field - yes, that's a known bug in Disqus, not sure why it's not fixed yet... but so far it's only problem for Firefox - the browser you're using I suppose? :) Try it in Chrome - no new lines should be added ;) Anyway, nothing much we can do with that unfortunately... though we're not happy with Disqus comments system too, so we're going to replace it with a custom one some day, but still no specific release date. Thanks for you patience and understanding!

Unfortunately, I'm not familiar with Laravel Request Validation... but I think you can find some answers in docs: https://symfony.com/doc/cur...

I hope this helps!

Cheers!

2 Reply

Yo Filip Jakovljevic!

And... to add some more:

> "Where are Models??

Are you referring to "database models"? If so, we do them (will do them) in the 3rd course of this series. I know you need them to build anything real, but we keep them "away" so we can focus on some "fundamentals". But yes, they're coming :).

> As someone who has a background in Laravel, are there Request Validation classes in Symfony? Regex in the middle of URL path seems very unreadable in my opinion

They're... sort of 2 different things. In this tutorial, we're talking about making a route simply NOT match unless the "wildcard" is a digit. It's not quite "validation". There IS a separate validation system - it's quite different than Laravel, but would feel much more familiar. Btw, this is the regex version that Laravel uses for what we did: https://laravel.com/docs/9.... - it's not in the route, but it's still regex.

And actually, on this last point, Symfony 6.1 has a nice feature for common regex requirements: some built-in regex via constants - https://symfony.com/blog/ne... (btw, that post also shows how you can put requirements as an option, instead of sticking them right INSIDE of the route string itself).

Cheers - we appreciate the comments and questions!

2 Reply
Filip J. Avatar

Thank you guys for replying. As for the Firefox bug, I am indeed on Firefox and it doesn't bother me at all. It just seemed like something that could easily slip under the cracks and I've wanted to report it in case it wasn't already, but since it was no worries about that. :)

Here's what I was referring to by Request Validation classes. Inside of this class there is a rules method where we can store all of our rules, i.e. required, numeric, and so on. There is also another method called failedValidation where you can customize the response for any scenario you'd like, in this case we could throw a 4xx and an appropriate JSON response. Yup, I am primarily talking from a standpoint of someone who is building a RESTful API so a 404 page wouldn't work for me, but even if I wasn't, that's how I would approach this. This method also avoids regex entirely, in or outside of the route. Is the validation system similar to this?

As for the Models, I'm referring to them as a part of the MVC structure. I guess that you guys call them Entities? The "database models" can be a bit misleading since they don't have to actually interact with the DB, as far as I know, although they're very useful when it comes to doing so.

To make things simpler, I am applying for a junior developer position. I have been given a task to do in Symfony and am looking at how to approach it. I need to make a small game/program where you can type a word and get points for it. The word needs to be an actual word, they get 1 point for each letter, 3 extra points if the word is a palindrome and 2 if it's almost a palindrome. I'd obviously have a view where user can type in their word, an API route towards the controller, and from the controller I'd make a Word object to validate the word. I understand the way views and controllers work, and while I could place the entire points system directly inside of a controller that's not the MVC way and that's pretty much how I've been taught to program. Would such logic belong inside of an Entity?

Sorry for the long reply, I hope that this could be useful to some other people in the future as well.

1 Reply

Hey Filip J. !

Sorry for the slow reply!

> Here's what I was referring to by Request Validation classes. Inside of this class there is a rules method where we can store all of our rules, i.e. required, numeric, and so on.

Ah, I see! Symfony doesn't have anything that's quite like this. Or, more accurately, this is done differently in Symfony :). Let me explain it in two parts:

A) First, the regex we used in the route - even in Laravel - I would STILL use route requirements for this. And I think I didn't really show this off correctly in the tutorial. This is because the MAIN purpose of that regex is not "validation" but to make it so that the route doesn't match... which is only really needed if you have 2 routes whose URLs are very similar. For example:


#[Route('/articles/{page}')]
public function allArticles(int $page)

#[Route('/articles/{slug}')]
public function showArticle(string $slug)

See the problem? :). Both match the same pattern! This is the main use-case for adding the regex: make page only match "digits"... and then if you go to /articles/foo that will NOT match the first route, but will fall down and match the second. So, not really validation, just a trick to make two very-similar routes match. I believe you would solve this in Laravel the same way.

However, with argument types (e.g. the int before the $page) argument, if you choose to use these, then these requirements have a second purpose (which is what I was showing in the tutorial): to make sure that going to /articles/foo does NOT match the first route... so that Symfony doesn't try to pass the string foo to the int $page argument. I suppose this is a type of validation, though mostly when we talk about validation, we're talking about validating a bunch of data (either POST or JSON data) that's sent on the request (which I think is the main purpose of those request validation classes in Laravel also).

B) Ok, so how DOES Symfony do validation, specifically for an API? You can do various levels of complexity, but we would typically do this:

i) Create a class (maybe it is a Doctrine entity class) with all of the fields in your API. For example, suppose we have an Article entity with some fields like title and body.

ii) Use Symfony's serializer to "deserialize" the incoming JSON into a new instance of Article. So now, that object is populated with the incoming data. It may, at this moment, be in an "invalid" state.

iii) Add validation constraints above the properties in your Article class, like #[Assert\NotBlank].

iv) Use Symfony's validator to validate the Article object itself. This will return a ConstraintViolationList (fancy object that holds an array of "violations").

v) Transform this into JSON. You could do this by hand, or you could once again use Symfony's serialize to serialize the ConstraintViolationList into JSON.

API Platform is a great platform that has a lot of this built-in. But ultimately, this is what it's doing behind the scenes :).

> Yup, I am primarily talking from a standpoint of someone who is building a RESTful API so a 404 page wouldn't work for me

Btw, if the user goes to /api/songs/foo, then you actually DO want to trigger a 404. But you're right, you don't want a 404 "page", but rather a 404 JSON response. If you're using my requirements trick, then Symfony will trigger the "404" for you before the controller is called. But you can "hint" to Symfony that this request is a "json" request, and that it should return the 404 error as JSON and not HTML. This is not as easy as it should be, I'll admit. You need to set this little _format thing - here's an example - https://stackoverflow.com/q... - if your entire site is an API, this is pretty easy to do - I'd add that defaults: { _format: 'json' } to the route import for the src/Controllers/ directory. You'll find this in config/routes/annotations.yaml or config/routes.yaml depending on your Symfony version.

The idea is kind of cool: you throw errors (like 404 errors) just like you would in an HTML application. Then Symfony returns an HTML or JSON version based on the "format" of the request. If you want to further customize what the 404 JSON looks like, you can: https://symfony.com/doc/cur...

> As for the Models, I'm referring to them as a part of the MVC structure. I guess that you guys call them Entities? The "database models" can be a bit misleading since they don't have to actually interact with the DB, as far as I know, although they're very useful when it comes to doing so.

Correct! We usually call these "entities"... but as far as MVC goes, you're right that "models" means something bigger. So, if you want to talk to the database, those are "Doctrine entities" and we'll get to those. Beyond that, "models" is often also used to refer to "services" (we'll create our own services in the next tutorial) and "simple data-hold objects that are not Doctrine entities", which you create ad-hoc in your app if/when you find yourself passing around some array of data.. which would be more organized in a class. So as usual, the "M" in MVC is where your business logic goes, so it's largely up to you to build what you need. In Symfony, we rely heavily on "services" in this M layer.

> The word needs to be an actual word, they get 1 point for each letter, 3 extra points if the word is a palindrome and 2 if it's almost a palindrome. I'd obviously have a view where user can type in their word, an API route towards the controller, and from the controller I'd make a Word object to validate the word. I understand the way views and controllers work, and while I could place the entire points system directly inside of a controller that's not the MVC way and that's pretty much how I've been taught to program. Would such logic belong inside of an Entity?

Ah, cool! I'd clarify if they want an API endpoint or not. This can also be built with Symfony's form component. Just two different approaches.

If you have time, I'd watch our "Symfony 5 Fundamentals" tutorial - specifically the parts about services. Services are classes that "do work". So, if you find that you have a bunch of "work" being done in a controller (like calculating a score), then that is a good candidate to pull out into a service - e.g. a `WordScorer` service, which has a public function where you can pass it the "submitted word" and it returns the integer score. In this case, you may or may not need a Word class... just because... what would it hold beyond the string "word" that was submitted? Put the scoring logic into a "service" - that's the "Symfony" way :).

Cheers!

Reply
Santosh kalwar Avatar
Santosh kalwar Avatar Santosh kalwar | posted 5 months ago

What is this "infoUrlValue?" and why we have to use that instead of "infoUrl"

1 Reply

Hey Appketo,

That is "Stimulus Values" concept. Please, rewatch this video one more time if you didn't get it. But in short, you are able to pass some values from the template to the Stimulus controller via something called "values". But to get a passed Stimulus Value you need to add the "Value" prefix to it, e.g. like in this case with infoUrlValue, i.e. this.infoUrlValue.

So, you just need to get used to this "Stimulus syntax", it's standartized thing by it.

I hope it clarifies things a bit more for you!

Cheers!

Reply
Santosh kalwar Avatar
Santosh kalwar Avatar Santosh kalwar | victor | posted 5 months ago

Okay, thank you for the explanation. So something like this https://stimulus.hotwired.d...

Reply

Hey Appketo,

Yes, exactly. Looks like you found this feature described in docs, good catch!

Cheers!

2 Reply
Ruslan Avatar

Hi, How can I disable debug mode for stimulus?
In app.js I added :
import {app} from "./bootstrap";
app.debug = false;
OR
in bootstrap.js :
app.debug = false;

Is it correct way?

Thank you.

Reply

Hey Ruslan

I think that's the only way to deactivate it without running your app in production mode (env var: APP_ENV=prod)

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.0.*", // v6.0.3
        "symfony/console": "6.0.*", // v6.0.3
        "symfony/dotenv": "6.0.*", // v6.0.3
        "symfony/flex": "^2", // v2.1.5
        "symfony/framework-bundle": "6.0.*", // v6.0.4
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.3
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-turbo": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/twig": "^2.12|^3.0" // v3.3.8
    },
    "require-dev": {
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/stopwatch": "6.0.*", // v6.0.3
        "symfony/web-profiler-bundle": "6.0.*" // v6.0.3
    }
}