Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Routes, Controllers & Responses

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.

I gotta say, I miss the 90's. Well, not the beanie babies and... definitely not the way I dressed back then, but... the mix tapes. If you weren't a kid in the 80's or 90's, you may not know how hard it was to share your favorite tunes with your friends. Oh yea, I'm talking about a Michael Jackson, Phil Collins, Paula Abdul mashup. Perfection.

To capitalize off of that nostalgia, but with a hipster twist, we're going to create a brand new app called Mixed Vinyl: a store where users can create mix tapes - complete with Boyz || Men, Mariah Carey and Smashing Pumpkins... except pressed onto a vinyl record. Hmm, I might need to put a record player in my car.

The page we're looking at, which is super cute and changes colors when we refresh... is not a real page. It's just a way for Symfony to say "hi" and link us to the documentation. And by the way, Symfony's documentation is great, so definitely check it out as you're learning.

Routes & Controllers

Ok: every web framework in any language has the same job: to help us create pages, whether those are HTML pages, JSON API responses or ASCII art. And pretty much every framework does this in the same way: via a route & controller system. The route defines the URL for the page and points to a controller. The controller is a PHP function that builds that page.

So route + controller = page. It's math people.

Creating the Controller

We're going to build these two things... kind of in reverse. So first, let's create the controller function. In Symfony, the controller function is always a method inside of a PHP class. I'll show you: in the src/Controller/ directory, create a new PHP class. Let's call it VinylController, but the name could be anything.

<?php
namespace App\Controller;
class VinylController
{
}

And, congrats! It's our first PHP class! And guess where it lives? In the src/ directory, where all PHP classes will live. And for the most part, it doesn't matter how you organize things inside src/: you can usually put things into whatever directory you want and name the classes whatever you want. So flex your creativity.

Tip

Controllers actually do need to live in src/Controller/, unless you change some config. Most PHP classes can live anywhere in src/.

But there are two important rules. First, notice the namespace that PhpStorm added on top of the class: App\Controller. However you decide to organize your src/ directory, the namespace of a class must match the directory structure... starting with App. You can imagine that the App\ namespace points to the src/ directory. Then, if you put a file in a Controller/ sub-directory, it needs a Controller part in its namespace.

If you ever mess this up, like you typo something or forget this, you're gonna have a bad time. PHP will not be able to find the class: you'll get a "class not found" error. Oh, and the other rule is that the name of a file must match the class name inside of it, plus .php. Hence, VinylController.php. We'll follow those two rules for all files we create in src/.

Creating the Controller

Back to our job of creating a controller function. Inside, add a new public method called homepage(). And no, the name of this method doesn't matter either: try naming it after your cat: it'll work!

For now, I'm just going to put a die() statement with a message.

<?php
namespace App\Controller;
class VinylController
{
public function homepage()
{
die('Vinyl: Definitely NOT a fancy-looking frisbee!');
}
}

Creating the Route

Good start! Now that we have a controller function, let's create a route, which defines the URL to our new page and points to this controller. There are a few ways to create routes in Symfony, but almost everyone uses attributes.

Here's how it works. Right above this method, say #[]. This is the PHP 8 attribute syntax, which is a way to add configuration to your code. Start typing Route. But before you finish that, notice that PhpStorm is auto-completing it. Hit tab to let it finish.

That, nicely, completed the word Route for me. But more importantly, it added a use statement up on top. Whenever you use an attribute, you must have a corresponding use statement for it at the top of the file.

Inside Route, pass /, which will be the URL to our page.

<?php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
class VinylController
{
#[Route('/')]
public function homepage()
{
die('Vinyl: Definitely NOT a fancy-looking frisbee!');
}
}

And... done! This route defines the URL and points to this controller... simply because it's right above this controller.

Let's try it! Refresh and... congratulations! Symfony looked at the URL, saw that it matched the route - / or no slash is the same for the homepage - executed our controller and hit the die statement!

Oh, and by the way, I keep saying controller function. That's commonly just called the "controller" or the "action"... just to confuse things.

Returning a Response

Ok, so inside of the controller - or action - we can write whatever code we want to build the page, like make database queries, API calls, render a template, whatever. We are going to do all of that eventually.

The only thing that Symfony cares about is that your controller returns a Response object. Check it out: type return and then start typing Response. Woh: there are quite a few Response classes already in our code... and two are from Symfony! We want the one from HTTP foundation. HTTP foundation is one of those Symfony libraries... and it gives us nice classes for things like the Request, Response and Session. Hit tab to auto-complete and finish that.

Oh, I should have said return new response. That's better. Now hit tab. When I let Response auto-complete the first time, very importantly, PhpStorm added this use statement on top. Every time we reference a class or interface, we will need to add a use statement to the top of the file we're working in.

By letting PhpStorm auto-complete that for me, it added the use statement automatically. I'll do that every time I reference a class. Oh, and if you're still a bit new to PHP namespaces and use statements, check out our short and free PHP namespaces tutorial.

Anyways, inside of Response, we can put whatever we want to return to the user: HTML, JSON or, for now, a simple message, like the title of the Mixed vinyl we're working on: PB and jams.

<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class VinylController
{
#[Route('/')]
public function homepage()
{
return new Response('Title: "PB and Jams"');
}
}

Ok team, let's see what happens! Refresh and... PB and Jams! It may not like much, but we just built our first fully-functional Symfony page! Route + controller = profit!

And you've just learned the most foundational part of Symfony... and we're just getting started. Oh, and because our controllers always return a Response object, it's optional, but you can add a return type to this function if you want to. But that doesn't change anything: it's just a nice way to code.

... lines 1 - 9
#[Route('/')]
public function homepage(): Response
... lines 12 - 16

Next I'm feeling pretty confident. So let's create another page, but with a much fancier route that matches a wildcard pattern.

Leave a comment!

23
Login or Register to join the conversation
Benoit-L Avatar
Benoit-L Avatar Benoit-L | posted 24 days ago | edited

Hi there,

This is something I have already mentioned but I still did not find the way to the have an autocompletion with the use. It does not work on my laptop.

https://www.dropbox.com/s/qwjm5qfldtq0jc5/AutoImport.png?dl=0

Reply

Hey Benoit,

Did you have Symfony plugin installed? Please, make sure you have it installed and configured, but first - try to upgrade your PhpStorm to the latest version. Then, after all installs and upgrades - make sure to restart the PhpStorm IDE. Then it should work I think :)

Cheers!

Reply
Default user avatar
Default user avatar unknown | posted 1 month ago | edited
Comment was deleted.

Hey there,

My apologies for any inconvenience you may have experienced, but just to corroborate, is this the tutorial you got problems with? I'm asking because this tutorial was built with Symfony 6 and PHP 8

1 Reply
Matthew Avatar

You may need to run composer require annotations to get the routes to load.

Reply

Hey Matthew,

Yes, but only if you're defining your routes with PHP annotations, but in a modern Symfony app, you'd be using PHP attributes

Cheers!

Reply
HAQUE Avatar

I have created a TopicController with
#[Route('/{topic}', name: 'topic.index', methods: ['GET'])]
and then I created Photographer Controller with
#[Route('/photographers', name: 'photographer.index', method: ['GET'])]

Now if I try /anytopic , this goes to topic controller, and /photographers also goes to topic controller.

How can I force /photographers controller to go to Photographer controller?

Reply

Hey Ariful,

Good question! But the legacy workaround is simple ;) You just need to change the order of your controllers, i.e. make that "photographer.index" controller going first, it will do the trick. Or, switch to yaml config instead of PHP Attributes for that exact controllers and place them in the correct order there.

Also, since Symfony 5.1 you can set the "priority" for routes, see the blog post with example: https://symfony.com/blog/ne... - so if you're on a newer version of Symfony - this will be the best option for you.

Cheers!

Reply

I'm not quite sure where to put this.

I am creating a project in SF6, I created a couple of entities, migrated, yada yada, all good and dandy.

now, some tables have relations, and once I submit a form be it for add or edit, I get this error.


Expected argument of type
"?App\Entity\WalletProvider",
"Doctrine\Common\Collections\ArrayCollection" given at property path
"walletProvider".

it happens when I try to add anything with relationships, I get that the form doesnt send the walletProvider object as is, but I havent modified any of that code that was generated through the make:crud function in the console, any pointers?

Reply

Hey jlchafardet

Could you double check your entities relationship? It seems to me that you have a OneToMany instead of a having a ManyToOne or OneToOne. If that's not the case I'd have to see your entitie's metadata and possible the form as well.

Cheers!

Reply

it is ManyToOne :S


Entity

#[ORM\ManyToOne(targetEntity: WalletProvider::class, inversedBy: 'coins')]
#[ORM\JoinColumn(nullable: false)]
private $walletProvider;

FormType

$builder
->add('name')
->add('abreviation')
->add('coingecko_url')
->add('walletProvider', null, [
'required' => false,
'multiple' => true,
]);



Controller

#[Route('/new', name: 'app_coin_new', methods: ['GET', 'POST'])]
public function new(Request $request, CoinRepository $coinRepository): Response
{
$coin = new Coin();
$form = $this->createForm(CoinType::class, $coin);
$form->handleRequest($request);


if ($form->isSubmitted() && $form->isValid()) {


$coinRepository->add($coin);
return $this->redirectToRoute('app_coin_index', [], Response::HTTP_SEE_OTHER);
}


return $this->renderForm('coin/new.html.twig', [
'coin' => $coin,
'form' => $form,
]);
}

Reply

Ok, try this in your form type


$builder
->add('walletProvider', EntityType::class, [
'class' => WallerProvider::class,
'required' => false,
'multiple' => false, # attention to this
]);
Reply

with multiple => false works, but that goes against everything I'm interested in having... I need it to allow me to select multiple providers.

Reply

I see.. then, you need to rethink the relationship of your entities. What you need is to have multiple WalletProvider objects inside your other entity (I believe its Coin). Or, you can create an intermediate entity that will hold the collection of WalletProvider per Coin record

The key here is that your Coin entity is holding a single reference of a WalletProvider object, but what you actually want is to hold a collection of them (OneToMany relationship)

Reply

so in this case, what I should do is "start" the relationship of "OneToMany" from walletProvider, or straight up go "ManyToMany" as many wallet providers can hold many coins??

Let me see if I can explain it better.

There are many coins, just as there are more than 1 WalletProviders(at least 2 at this time but could be more in the future)

there are also "Wallets", so each Wallet can hold many coins, but 1 wallet can only belong to 1 WalletProvider.

does that makes more sense?

and to complicate it more, the app manages "users"
so each user can have 1 or more wallet, and each coin can have an "amount" that is tied to both the wallet and the user.

user 1, wallet-from-walletProvider-1,
accepts: coin1 and coin2
coin 1 balance: 22
coin 2 balance: 5

Reply

Hey jlchafardet

Ok so, you'll need a ManyToMany relationship on your Coin - WalletProvider entities
Then, the Wallet entity will have a OneToMany to the Coin entity, and a OneToOne (or ManyToOne) to the WalletProvider entity

I hope it helps. Cheers!

1 Reply

will do my best :D hopefully works the way I want it to work.

I'll let you guys know.

Reply

Great! For your User relationship it's not that easy to tell because I don't know the use-cases of your application but just keep in mind that you can add an intermediate entity that work like thin wrapper between your relationships, it's quite useful for storing extra fields and for adding behavior to the entities.

Reply

The relationships are in theory fairly simple.

Wallet Provider (Binance, KuCoin for example, they offer exchance services for cryptocurrencies[it is not really that important as the app does nothing nor has anything to do with trades, trading or the likes, we want them just because we want to know who provides the eWallet for the users.) its just there for us to know who provides the eWallet service. Each Wallet Provider can have many wallets, and support many coins.

Wallet: the eWallet itself, where the User will receive their coins, each wallet can hold many coins. but belong to a single user, and each user can only register 1 wallet.

Coin: the cryptocurrency our app will manage (we have 2, but might add more in the future), Users earn Coins for doing stuff, sometimes is Coin 1, sometimes is Coin 2, and in the future we might add Coin X.

User: the user itself, email, name, last name, favorite food, name of pet, foot size...... you get the gist of it. Each user can have ONLY 1 wallet, but can receive more than 1 coin to their Wallet.

this is the "MVP" aspect of the app.

I also have a role in user, that is not in use yet, but in a future iteration I want to be able to assign "regular users" to "staff users" to manage. example, we have 100 regular users and 3 staff users, Staff user 1, manages user 1 to 33, Staff user 2 manages user 34 to 67, and staff user 3 manages users 68 to 100.

in that iteration I will want to display in those "staff user" dashboard, a list of their "managed accounts". so they can only see what they should see. but we can get to that later :D.

did this get you a little better picture of what I want to do with the relationships? (another thing important to note is, the app itself does nothing more than parse a bunch of logs that cli tools output, and displays them on screen)

Reply
Michel D. Avatar
Michel D. Avatar Michel D. | posted 8 months ago

Hi,

I'm following the 'Symfony 6' course on SymfonyCast.
I run the commands:

symfony new mixed_vinyl
cd mixed_vinyl
symfony serve -d.

I display the symfony initial page : https://127.0.0.1:8000/
I create the 'VinylController.php' file and the route #[Route('/')] via php attribute.
I refresh the page on my browser
and I get the following error.:

Cannot rename
"C:\Symfony\mixed_vinyl\var\cache\dev\Container35B09qN\get93B0.tmp" to
"C:\Symfony\mixed_vinyl/var/cache/dev/Container35B09qN/get_Console_Command_ConfigDebug_LazyService.php":

rename(C:\Symfony\mixed_vinyl\var\cache\dev\Container35B09qN\get93B0.tmp,C:\Symfony\mixed_vinyl/var/cache/dev/Container35B09qN/get_Console_Command_ConfigDebug_LazyService.php):
Accès refusé (code: 5)

cache:clear gives the same error.
I have to remove the cache/dev directory to continue
If i add the second method with a second route and i refresh the page
I get the same type of error again

Reply

Hey Michel,

Oh, this sounds like permissions error on Windows OS. You might need to double-check the permissions for that folder, but if you say that "rm -rf var/cache/" helps only until you edit a file and then the problem returns - most probably it won't help. I'd suggest you to put your project somewhere in a public dir, I see Symfony dir it's located in the root of disk C. Try to create a project somewhere in the user directory and try again, otherwise try to google this problem, most probably some Windows users may give you some hints over the internet.

Also, it might depends on how you run the PHP. If you're using a separate server like XAMPP, I'd recommend you to try to run it without admin permissions, or the reverse if you run it without admin permissions - it might help. Or probably better to install PHP natively in your system and use it directly with Symfony CLI built-in web server.

If no luck, I'd recommend you to take a look at Docker or other virtualization tools. Or probably better take a look at Windows WSL - Windows guys say that it's the best way lately if you're on Windows OS.

I hope this helps!

Cheers!

Reply
Michel D. Avatar

It seems that the Acronis anti-ransomware app is the cause of this issue
Thanks.

Reply

Hey Michel,

Ah, yes, some antiviruses might cause such errors too, good catch!

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
    }
}