If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIf you watch a lot of KnpU tutorials, you know that I love to talk about how the whole point of a bundle is that it adds services to the container. But, even I have to admit that a bundle can do a lot more than that: it can add routes, controllers, translations, public assets, validation config and a bunch more!
Find your browser and Google for "Symfony bundle best practices". This is a really nice document that talks about how you're supposed to build re-usable bundles. We're following, um, most of the recommendations. It tells you the different directories where you should put different things. Some of these directories are just convention, but some are required. For example, if your bundle provides translations, they need to live in the Resources/translations
directory next to the bundle class. If you follow that rule, Symfony will automatically load them.
Here's our new goal: I want to add a route & controller to our bundle. We're going to create an optional API endpoint that returns some delightful lorem ipsum text.
Before we start, I'll open my PhpStorm preferences and, just to make this more fun, search for "Symfony" and enable the Symfony plugin. Also search for "Composer" and select the composer.json
file so that PhpStorm knows about our autoload namespaces.
Back to work! In src/
, create a Controller
directory and inside of that, a new PHP class called IpsumApiController
. We don't need to make this extend anything, but it's OK to extend AbstractController
to get some shortcuts... except what!? AbstractController
doesn't exist!
That's because the class lives in FrameworkBundle
and... remember! Our bundle does not require that! Ignore this problem for now. Instead, find our app code, open AbstractController
, copy its namespace
, and use it to add the use
statement manually to the controller.
... lines 1 - 5 | |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
... line 7 | |
class IpsumApiController extends AbstractController | |
... lines 9 - 25 |
Next, add a public function called index
. Here, we're going to use the KnpUIpsum
class to return a JSON response with some dummy text. When you create a controller in a reusable bundle, the best practice is to register your controller as a proper service and use dependency injection to get anything you need.
... lines 1 - 16 | |
public function index() | |
{ | |
... lines 19 - 22 | |
} | |
... lines 24 - 25 |
Add public function __construct()
and type-hint the first argument with KnpUIpsum
. I'll press Alt+Enter and choose Initialize Fields so that PhpStorm creates and sets a property for that.
... lines 1 - 9 | |
private $knpUIpsum; | |
... line 11 | |
public function __construct(KnpUIpsum $knpUIpsum) | |
{ | |
$this->knpUIpsum = $knpUIpsum; | |
} | |
... lines 16 - 25 |
Down below, return $this->json()
- we will not have auto-complete for that method because of the missing AbstractController
- with a paragraphs
key set to $this->knpUIpsum->getParagraphs()
and a sentences
key set to $this->knpUIpsum->getSentences()
... lines 1 - 16 | |
public function index() | |
{ | |
return $this->json([ | |
'paragraphs' => $this->knpUIpsum->getParagraphs(), | |
'sentences' => $this->knpUIpsum->getSentences(), | |
]); | |
} | |
... lines 24 - 25 |
Excellent!
Next, we need to register this as a service. In services.xml
, copy the first service, call this one ipsum_api_controller
, and set its class name. For now, don't add public="true"
or false
: we'll learn more about this in a minute. Pass one argument: the main knpu_lorem_ipsum.knpu_ipsum
service.
... lines 1 - 6 | |
<services> | |
... lines 8 - 13 | |
<service id="knpu_lorem_ipsum.ipsum_api_controller" class="KnpU\LoremIpsumBundle\Controller\IpsumApiController"> | |
<argument type="service" id="knpu_lorem_ipsum.knpu_ipsum" /> | |
</service> | |
... lines 17 - 19 | |
</services> | |
... lines 21 - 22 |
Tip
In Symfony 5, you'll need a bit more config to get your controller service working:
<service id="knpu_lorem_ipsum.ipsum_api_controller" class="KnpU\LoremIpsumBundle\Controller\IpsumApiController" public="true">
<call method="setContainer">
<argument type="service" id="Psr\Container\ContainerInterface"/>
</call>
<tag name="container.service_subscriber"/>
<argument type="service" id="knpu_lorem_ipsum.knpu_ipsum"/>
</service>
For a full explanation, see this thread: https://bit.ly/abstract-controller-tag
Perfect!
Finally, let's add some routing! In Resources/config
, create a new routes.xml
file. This could be called anything because the user will import this file manually from their app.
To fill this in, as usual, we'll cheat! Google for "Symfony Routing" and, just like we did with services, search for "XML" until you find a good example.
Copy that code and paste it into our file. Let's call the one route knpu_lorem_ipsum_api
. For controller
, copy the service id, paste, and add a single colon then index
.
<routes xmlns="http://symfony.com/schema/routing" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://symfony.com/schema/routing | |
http://symfony.com/schema/routing/routing-1.0.xsd"> | |
<route id="knpu_lorem_ipsum_api" controller="knpu_lorem_ipsum.ipsum_api_controller:index" path="/" > | |
<!-- settings --> | |
</route> | |
</routes> |
Fun fact: in Symfony 4.1, the syntax changes to a double ::
and using a single colon is deprecated. Keep a single :
for now if you want your bundle to work in Symfony 4.0.
Finally, for path
, the user will probably want something like /api/lorem-ipsum
. But instead of guessing what they want, just set this to /
, or at least, something short. We'll allow the user to choose the path prefix.
And that's it! But... how can we make sure it works? In a few minutes, we're going to write a legitimate functional test for this. But, for now, let's just test it in our app!
In the config
directory, we have a routes.yaml
file, and we could import the routes.xml
file from here. But, it's more common to go into the routes/
directory and create a separate file: knpu_lorem_ipsum.yaml
.
Add a root key - _lorem_ipsum
- this is meaningless, then resources
set to @KnpULoremIpsumBundle
and then the path to the file: /Resources/config/routes.xml
. Then, give this a prefix! How about /api/ipsum
.
_lorem_ipsum: | |
resource: '@KnpULoremIpsumBundle/Resources/config/routes.xml' | |
prefix: /api/ipsum |
Did it work? Let's find out: find your terminal tab for the application, and use the trusty old:
php bin/console debug:router
There it is! /api/ipsum/
. Copy that, find our browser, paste and.... nope. Error!
Controller ipsum_api_controller cannot be fetched from the container because it is private. Did you forget to tag the service with
controller.service_arguments
.
The error is not entirely correct for our circumstance. First, yes, at this time, controllers are the one type of service that must be public. If you're building an app, you can give it this tag, which will automatically make it public. But for a reusable bundle, in services.xml
, we need to set public="true"
.
... lines 1 - 6 | |
<services> | |
... lines 8 - 13 | |
<service id="knpu_lorem_ipsum.ipsum_api_controller" class="KnpU\LoremIpsumBundle\Controller\IpsumApiController" public="true"> | |
... line 15 | |
</service> | |
... lines 17 - 19 | |
</services> | |
... lines 21 - 22 |
Try that again! Now it works. And... you might be surprised! After all, our bundle references a class that does not exist! This is a problem... at least, a minor problem. But, because FrameworkBundle is included in our app, it does work.
But to really make things solid, let's add a proper functional test to the bundle that guarantees that this route and controller work. And when we do that, it'll become profoundly obvious that we are, yet again, not properly requiring all the dependencies we need.
I think that autowiring in the bundle is explicitly discouraged. Another way around this is to add `<call>` to each handler with method `setContainer` (which is admittedly a pain). Tbh, idk how that works exactly (are you creating a *new* container? Nesting a container? Setting something *on* a container?) but both methods don't appear to be necessary to work with bundles which appear to account for Symfony 5 such as:
https://github.com/willdura...
Does anyone have any other input on this?!
Edit
-----
I have not personally tested that bundle out on a Symfony 5 install myself; I just read the commit comments.
Hey ash-m!
Yep, you are correct :). You CAN use autowiring in your bundle, but as a best practice, it IS discouraged. The reason is that wiring things manually helps make your bundle a bit faster (on dev mode, it would be the same in prod) and makes your service config easier to read by others because it's explicit.
> Another way around this is to add `<call>` to each handler with method `setContainer` (which is admittedly a pain).
Yes, this is basically what autowiring is doing normally in this case. So, you're doing it manually. For others, there is a larger conversation about this over here: https://symfonycasts.com/sc...
> Tbh, idk how that works exactly (are you creating a *new* container? Nesting a container? Setting something *on* a container?)
It's a really cool, but complex feature :). You're creating a mini-container that has *some* services in it. You can learn about the idea here: https://symfonycasts.com/sc...
> but both methods don't appear to be necessary to work with bundles which appear to account for Symfony 5 such as: https://github.com/willdura...
If a controller doesn't extend AbstractController - https://github.com/willdura... - then none of this is needed. The AbstractController class is what tries to use this mini-service container :).
Cheers!
Thank you for the detailed reply! I am glad you mentioned the abstract controller thing because I attempted this a again using the ADR pattern and it ended up working and I didn't know why.
Hey Mindgruve
Sorry for being late to help you out but I'm glad that you could find and share the solution. Cheers!
Hey
I am facing issue in this video section i am running the command
bin/console debug:router
but its throw the error
In FileLoader.php line 168:
Unable to find file "@KnpULoremIpsumBundle/Resources/config/routes.xml" in @KnpULoremIpsumBundle/Resources/config/r
outes.xml (which is being imported from "C:\xampp\htdocs\ReusableBundleSymfony\config/routes/knpu_lorem_ipsum.yaml"
). Make sure the "KnpULoremIpsumBundle/Resources/config/routes.xml" bundle is correctly registered and loaded in th
e application kernel class. If the bundle is registered, make sure the bundle path "@KnpULoremIpsumBundle/Resources
/config/routes.xml" is not empty.
In Kernel.php line 275:
Unable to find file "@KnpULoremIpsumBundle/Resources/config/routes.xml".
debug:router [--show-controllers] [--format FORMAT] [--raw] [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no-interaction] [-e|outes--env ENV] [--no-debug] [--] <command> [<name>]
file path and code .
LoremIpsumBundle/src/Resources/config/routes.xml
`<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="knpu_lorem_ipsum_api" controller="knpu_lorem_ipsum.ipsum_api_controller:index" path="/" >
<!-- settings -->
</route>
</routes>
`
file path code
LoremIpsumBundle/src/Controller/IpsumApiController.php
`<?php
namespace KnpU\LoremIpsumBundle\Controller;
use KnpU\LoremIpsumBundle\KnpUIpsum;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class IpsumApiController extends AbstractController
{
private $knpUIpsum;
public function __construct(KnpUIpsum $knpUIpsum)
{
$this->knpUIpsum = $knpUIpsum;
}
public function index()
{
return $this->json([
'paragraphs' => $this->knpUIpsum->getParagraphs(),
'sentences' => $this->knpUIpsum->getSentences(),
]);
}
}
`
file path and code
config/routes/knpu_lorem_ipsum.yaml
`_lorem_ipsum:
resource: '@KnpULoremIpsumBundle/Resources/config/routes.xml'
prefix: '/api/ipsum'`
Hey Saif Ali Ansari!
Hmm. Before we dig in too far, this may be something simple. The error says:
> Unable to find file "@KnpULoremIpsumBundle/Resources/config/routes.xml
But you said the file is located at:
> LoremIpsumBundle/src/Resources/config/routes.xml
Try moving the Resources/ directory into LoremIpsumBundle directly - NOT inside of src/. If that doesn't help, let me know :).
Cheers!
Hey weaverryan
Thank you to reply
But i can remove from src but steel its gives same error .
i am stuck in this tutorial from last three day .
Hi Saif Ali Ansari!
Sorry for the slow reply! Ok, so let's at what's going on. First, in your config/routes/knpu_lorem_ipsum.yaml
, you have something that looks like this:
_lorem_ipsum:
resource: '@KnpULoremIpsumBundle/Resources/config/routes.xml'
prefix: /api/ipsum
And... that's great! For our purposes, the only really important part is the path: @KnpULoremIpsumBundle/Resources/config/routes.xml
.
When Symfony sees this, it looks for the location of KnpULoremIpsumBundle
and then looks for the path Resources/config/routes.xml
inside of it to find the file. If you're following the tutorial exactly (which it seems like you are), then your bundle lives in the LoremIpsumBundle/
. Directory, this means that Symfony is looking for the file in this EXACT path:
LoremIpsumBundle/Resources/config/routes.xml
Do you have a file at this path? I'm asking because, from your original message, you mentioned that your routing file lives at:
LoremIpsumBundle/src/Resources/config/routes.xml
And this is NOT the correct path. The correct path is LoremIpsumBundle/Resources/config/routes.xml
. If you DO move the file to LoremIpsumBundle/Resources/config/routes.xml
and you still get the error, then there are only 2 other possibilities I can think of:
A) The KnpULoremIpsumBundle is not registered in your config/bundles.php... or it has a different name
B) The KnpULoremIpsumBundle class does not live at LoremIpsumBundle/KnpULoremIpsumBundle.php
. And so, the "root directory" of your bundle is somewhere else.
Let me know what you find out :).
Cheers!
Hi guys, i see an error here:
"knpu_lorem_ipsum.controller.ipsum_api_controller" has no container set, did you forget to define it as a service subscriber?
In comments people write autowire is fix for an error, but the others not recommend it. So how resolve it then?
Hey triemli!
Sorry about the trouble - let's see if we can work it out :). Check out my answer here: https://symfonycasts.com/sc...
There was a subtle change in the core of Symfony, which makes this now required. Actually, I need to add a note about this to the tutorial - my fault for not doing it already!
Let me know if the solution I linked to helps - and if you want any further explanation.
Cheers!
Hello,
I'm working with Symfony 4, I created a bundle and added the routes inside the bundle /resources/config/routes.yaml, because I have to add the routes again in the global configuration, can't it be done automatically from the bundle?
Hey Jose carlos C.
You don't need to add the routes all over again. You need to tell the app to look into your route config file, in this case, your bundle route config.
One way is adding to the routes folder a reference for what you want:<br />// config/routes/my_bundle.yaml<br />_my_bundle:<br /> resource: '@MyBundle/Resources/config/routes.xml'<br /> prefix: /<br />
Hope that helps.
I just don't want it to depend on the global configuration, once it's defined in the Bundle, it doesn't need to be loaded again, so I'm looking for an option if it exists in Symfony to be able to have routes from the Bundle.
Hey Jose carlos C.
Pretty interesting question =) Of course you can use recipe system, but I think it's not suitable if you have not open source bundle. But there is another way. You can try make Custom route loader, and load routes from your bundle config file. It's described a little here https://symfony.com/doc/cur...
Cheers!
Thanks for the answer. I tried that already and it <b>doesn't work</b>. I have to include the routes.yml file back into app/config/routes.yml, https://stackoverflow.com/a/36487675/2046442 which means I have to include it out of the Bundle.
But there is good news! There is a solution that you only write once and it works for all your bundles, I found it here.
https://github.com/symfony/symfony/issues/22700#issuecomment-301151130
`
bundle_routes:
resource: "../src/Bundles/*/Resources/config/routes.yml"
`
Without telling the app where are your routes I don't see a way. But you can create your config file that register your bundle route paths on the config folder as part of your bundle installation, like most of the bundles.
If you find a different workaround, please share here. I would be interested to know.
Hey Mindgruve
Thanks for you assistance with this question =) If you interested in another workaround check my answer above =)
Cheers!
hey, thanks for this tutorial;
I still wonder though, if there is a way for Symfony (Flex?...) to auto-add the bundle's routing file (as resource) - so that I would not do it manually? can / should I do that manually in the load() method maybe? could you please just give a hint?
Hey Yaroslav,
Actually, Symfony Flex can auto-add routes.... Are we talking about Symfony 4? For example, here's the routes for EasyAdminBundle that Flex will automatically add when you require this package: https://github.com/symfony/... - i.e. Flex just copy/paste the easy_admin.yaml file with routes into "config/routes/easy_admin.yaml" in your project, and if you have good Symfony 4 application - that file will be automatically read by the application, see "configureRoutes()" in Kernel that responsible to load those files: https://github.com/symfony/...
Did you upgrade to Symfony 4 your application from 3.x? Or did you create Symfony 4 application from scratch? Probably you missed something during your update?
Cheers!
btw, I also tried to do a routes file copy with the means of composer.json of my bundle
"scripts": {
"post-install-cmd": "php -r \"copy('Resources/config/routes.yml', '../config/routes/my-bundle.yml');\"",
"post-package-install": "php -r \"copy('Resources/config/routes.yml', '../config/routes/my-bundle.yml');\"",
"post-package-uninstall": "php -r \"unlink('../config/routes/my-bundle.yml')\""
}
but none of these worked (looks like none of these was called at all, because I tried echoing and got no output)...
hi Victor,
thank you for the prompt reply. yes, I'm using Smf 4.2, and just follow this tutorial instructions (except for manual addition of bundle's resource to app routes config).
I tried install / uninstall my bundle via composer, but the routes file was never auto-copied.
should I probably create some instructions for Flex (a recipe) so that it would know what file to copy?
Hey Yaroslav,
Ah, yes, now I see the problem. You're talking about *your* bundle, sure Flex does not know nothing about it except that it's a Symfony bundle so it just includes the bundle in bundles.php and that's it. To teach Flex that when you require your bundle Flex should copy some extra files as well - you need to create a recipe for your bundle. Recipe is just a set of instructions, you can see the same EasyAdminBundle as example: https://github.com/symfony/... . But that's the official recipe, your recipe should be in https://github.com/symfony/... repository where other non-official bundles are.
Here's more info about how to create a recipe: https://github.com/symfony/...
I hope that helps!
Cheers!
hi Viktor,
great, thank you for the reply! will surely give it a try.
thank you for your support!
Hey! I have error:
Type error: Too few
arguments to function
Mwil\DummyArticleBundle\Controller\DummyArticleApiController::__construct(),
0 passed in
C:\github\create-bundle\vendor\symfony\http-kernel\Controller\ControllerResolver.php
on line 111 and exactly 1 expected
in services.xml I have registered new service for controller with argument:
<service id="mwil_dummy_article_bundle.dummy_article_api_controller" class="Mwil\DummyArticleBundle\Controller\DummyArticleApiController">
<argument type="service" id="mwil_dummy_article_bundle.mwil_dummy_article"/>
</service>
But it not passing argument to constructor in DummyArticleApiController class
{{ PROBLEM RESOLVED }} I can't believe I missed typo and I was looking for a problem for an hour ;)
Hey, I've been going crazy trying to find why this bit will not work for me.
I keep gettin the error : [ERROR 1845] Element '{http://symfony.com/schema/d...}container': No matching global declaration available for the validation root.
Now from what I've gathered so far this is due to discrepancy between the xsd defintion and the actual xml file.
However my xml file just contains the defintions from the official symfony docs and one service :
<container xmlns="http://symfony.com/schema/d..." xmlns:xsi="http://www.w3.org/2001/XMLS..." xsi:schemalocation="http://symfony.com/schema/d...
http://symfony.com/schema/d...">
<services>
<service id="ddb_stuart_api.stuart_api" class="DdB\StuartApiBundle\StuartApi" public="true"/>
</services>
</container>
Does anyone have a clue why I might be getting this error ?
Thanks in advance for any help
Turns out i'm an idiot, when including the routes.xml file in my actual application I actually included the services.xml instead, please disregard my previous message
Hey DennisdeBest!
No worries! Let's just say that making mistakes is the best way to learn - so now you're an expert in this area ;). Good job figuring out the issue.
Cheers!
Hi guys, I found a discrepancy between the name of the controller service in the example file in the script, and the name that appears in the video. In the example it is `knpu_lorem_ipsum.controller.ipsum_api_controller`, but in the script it appears as `knpu_lorem_ipsum.ipsum_api_controller`.
Hey Tom,
Thanks for mentioning it! Yes, it looks like we have a little discrepancy between the video and its scripts, in the video we missed that "controller" string between "knpu_lorem_ipsum" and "ipsum_api_controller". Probably not a big deal, but it might be good to know for others.
Cheers!
I need to create some Doctrine Entities in my bundle, to be used by the external App. How can I add and configure these Entities?
Hey Lucas,
Good question! You can create an entity in your bundle and even use Doctrine annotation for mapping. Then in your app that uses the bundle you can create an empty class in src/Entity/ dir that will extend the one from the bundle - Doctrine will see all the parents fields. Or even better to create a Trait in your bundle instead of a class, so you don't need to inherit any class in your app, just *use* the trait - this way is even more flexible, thanks to PHP traits.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"doctrine/annotations": "^1.8", // v1.8.0
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knpuniversity/lorem-ipsum-bundle": "*@dev", // dev-master
"nexylan/slack-bundle": "^2.0,<2.2", // v2.0.1
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.1.6
"symfony/asset": "^4.0", // v4.0.6
"symfony/console": "^4.0", // v4.0.6
"symfony/flex": "^1.0", // v1.18.7
"symfony/framework-bundle": "^4.0", // v4.0.6
"symfony/lts": "^4@dev", // dev-master
"symfony/twig-bundle": "^4.0", // v4.0.6
"symfony/web-server-bundle": "^4.0", // v4.0.6
"symfony/yaml": "^4.0", // v4.0.6
"weaverryan_test/lorem-ipsum-bundle": "^1.0" // v1.0.0
},
"require-dev": {
"easycorp/easy-log-handler": "^1.0.2", // v1.0.4
"sensiolabs/security-checker": "^4.1", // v4.1.8
"symfony/debug-bundle": "^3.3|^4.0", // v4.0.6
"symfony/dotenv": "^4.0", // v4.0.6
"symfony/maker-bundle": "^1.0", // v1.1.1
"symfony/monolog-bundle": "^3.0", // v3.2.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.3
"symfony/stopwatch": "^3.3|^4.0", // v4.0.6
"symfony/var-dumper": "^3.3|^4.0", // v4.0.6
"symfony/web-profiler-bundle": "^3.3|^4.0" // v4.0.6
}
}
First of all, thanks for this tutorial. Super clear, although when doing it using symfony 5 I always get stuck on this issue:
<blockquote>{{my_route}} has no container set, did you forget to define it as a service subscriber</blockquote>.
Anybody having the same issue?
Edited
Found the issue,
needed to add
<defaults autowire="true" autoconfigure="true"/>
on your service.xml file to enable auto wiring.