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 SubscribeWhen you create a reusable library, you gotta think about what extension points you want to offer your users. Right now, the user can control the two arguments to this class... but they can't control anything else, like the actual words that are used in our fake text. These are hardcoded at the bottom.
So... how could we allow the user to override these? One option that I like is to extract this code into its own class, and allow the user to override that class entirely.
Check this out: in the bundle, create a new class called KnpUWordProvider
. Give it a public function called getWordList()
that will return an array. Back in KnpUIpsum
, steal the big word list array and... return that from the new function.
... lines 1 - 4 | |
class KnpUWordProvider | |
{ | |
public function getWordList(): array | |
{ | |
return [ | |
'adorable', | |
'active', | |
'admire', | |
'adventurous', | |
... lines 14 - 140 | |
]; | |
} | |
} |
Perfect! In KnpUIpsum
, add a new constructor argument and type-hint it with KnpUWordProvider
. Make it the first argument, because it's required. Create a new property for this - $wordProvider
- then set it below: $this->wordProvider = $wordProvider
.
... lines 1 - 9 | |
class KnpUIpsum | |
{ | |
private $wordProvider; | |
... lines 13 - 17 | |
public function __construct(KnpUWordProvider $wordProvider, bool $unicornsAreReal = true, $minSunshine = 3) | |
{ | |
$this->wordProvider = $wordProvider; | |
... lines 21 - 22 | |
} | |
... lines 24 - 209 | |
} |
With all that setup, down below in the original method, just return $this->wordProvider->getWordList()
.
... lines 1 - 205 | |
private function getWordList(): array | |
{ | |
return $this->wordProvider->getWordList(); | |
} | |
... lines 210 - 211 |
Our class is now more flexible than before. Of course, in services.xml
, we need to tell Symfony to pass in that new argument! Copy the existing service node so that we can register the new provider as a service first. Call this one knpu_lorem_ipsum.knpu_word_provider
and set the class to KnpUWordProvider
. Oh, but this service does not need to be public: no one should need to use this service directly.
... lines 1 - 6 | |
<services> | |
... lines 8 - 11 | |
<service id="knpu_lorem_ipsum.knpu_word_provider" class="KnpU\LoremIpsumBundle\KnpUWordProvider" /> | |
... lines 13 - 14 | |
</services> | |
... lines 16 - 17 |
Above, we need to stop using the short service syntax. Instead, add a closing service tag. Then, add an argument with type="service"
and id="knpu_lorem_ipsum.knpu_word_provider"
.
... lines 1 - 6 | |
<services> | |
<service id="knpu_lorem_ipsum.knpu_ipsum" class="KnpU\LoremIpsumBundle\KnpUIpsum" public="true"> | |
<argument type="service" id="knpu_lorem_ipsum.knpu_word_provider" /> | |
</service> | |
... lines 11 - 14 | |
</services> | |
... lines 16 - 17 |
If you're used to configuring services in YAML, the type="service"
is equivalent to putting an @
symbol before the service id. The last change we need to make is in the extension class. These are now the second and third arguments, so use the indexes one and two.
... lines 1 - 9 | |
class KnpULoremIpsumExtension extends Extension | |
{ | |
public function load(array $configs, ContainerBuilder $container) | |
{ | |
... lines 14 - 20 | |
$definition->setArgument(1, $config['unicorns_are_real']); | |
$definition->setArgument(2, $config['min_sunshine']); | |
} | |
... lines 24 - 28 | |
} |
Phew! Unless we messed something up, it should work! Try it! Yes! We still get fresh words each time.
So... we refactored our code to be more flexible... but it's still not possible for the user to override the word provider. Here's my idea: in the Configuration
class, add a new scalar node - in other words, a string option - called word_provider
. Default this to null
, and you can add some documentation to be super cool. If the user wants to customize the word list, they will set this to the service id of their own word provider.
... lines 1 - 7 | |
class Configuration implements ConfigurationInterface | |
{ | |
public function getConfigTreeBuilder() | |
{ | |
... lines 12 - 13 | |
$rootNode | |
->children() | |
... lines 16 - 17 | |
->scalarNode('word_provider')->defaultNull()->end() | |
... lines 19 - 22 | |
} | |
} |
So, in the extension class, if the that value is not set to null, let's replace the first argument entirely: $definition->setArgument()
with 0 and $config['word_provider']
.
... lines 1 - 9 | |
class KnpULoremIpsumExtension extends Extension | |
{ | |
public function load(array $configs, ContainerBuilder $container) | |
{ | |
... lines 14 - 20 | |
if (null !== $config['word_provider']) { | |
$definition->setArgument(0, $config['word_provider']); | |
} | |
... lines 24 - 25 | |
} | |
... lines 27 - 31 | |
} |
We're not setting this config value yet, but when we refresh, great! We at least didn't break anything... though we do have a small mistake...
Anyways, let's test the system properly by creating our own, new word provider. In src/Service
, create a class called CustomWordProvider
. Make this extend the KnpUWordProvider
because I just want to add something to the core list. To override the method, go to the Code -> Generate menu, or Cmd+N on a Mac - choose "Override methods" and select getWordList()
.
... lines 1 - 6 | |
class CustomWordProvider extends KnpUWordProvider | |
{ | |
public function getWordList(): array | |
{ | |
... lines 11 - 14 | |
} | |
} |
Inside, set $words = parent::getWordList()
. Then, add the word "beach"... because we all deserve a little bit more beach in our lives. Return $words
at the bottom.
... lines 1 - 8 | |
public function getWordList(): array | |
{ | |
$words = parent::getWordList(); | |
$words[] = 'beach'; | |
return $words; | |
} | |
... lines 16 - 17 |
Thanks to the standard service configuration in our app, this class is already registered as a service. So all we need to do is go into the config/packages
directory, open knpu_lorem_ipsum.yaml
, and set word_provider
to App\Service\CustomWordProvider
.
knpu_lorem_ipsum: | |
... line 2 | |
word_provider: App\Service\CustomWordProvider |
Let's see if this thing works! Move over and refresh! Boooo!
Argument 1 passed to KnpUIpsum::__construct() must be an instance of KnpUWordProvider - because that's our type-hint - string given.
Look below in the stack-trace: this is pretty deep code, but you can actually see that something is creating a new KnpUIpsum
, but passing the string class name of our provider as the first argument... not the service!
Go back to our extension class. Here's the fix: when we set the argument to $config['word_provider']
, this of course sets that argument to the string value! To fix this in YAML, we would prefix the service id with the @
symbol. In PHP, wrap the value in a new Reference()
object. This tells Symfony that we're referring to a service.
... lines 1 - 10 | |
class KnpULoremIpsumExtension extends Extension | |
{ | |
public function load(array $configs, ContainerBuilder $container) | |
{ | |
... lines 15 - 22 | |
$definition->setArgument(0, new Reference($config['word_provider'])); | |
... lines 24 - 26 | |
} | |
... lines 28 - 32 | |
} |
Deep breath and, refresh! It works! And if you search for "beach"... yes! Let's go to the beach!
This is a great step! But there are two other nice improvements we can make: using a service alias & introducing an interface. Let's add those next.
Hey @Fabien
Since Symfony 3.4 (I believe! but definitely since Symfony4) the internal bundle structure of your application doesn't exist anymore. The reason is simple, that structure was confusing, and Symfony does not want to impose you a specific structure. So, the new standard is to put all your application code inside src/
Cheers!
I find the following part of this tutorial really interesting:
$definition->setArgument(0, new Reference($config['word_provider']));
What is the reason you can't access correctly with container->get (it throws the error: You have requested a non-existent service):
Example
$definition->setArgument(0, $container->get($config['word_provider']));
Hey Rainer S.
That's a good question! And here a pretty simple answer: We can't get real service at this step of container building, probably this services are not instantiated, so that's why we need to do a reference to service which we need here when everything will be instantiated!
Cheers!
Interesting behavior: if I put CustomWordProvider.php in src folder of the bundle (instead of App\Service folder). It doesn't work. It writes: services not found.
Hey triemli
You can't just move file to another spot, you should also change namespace and fix service definition.
Cheers!
Thank you for everything I am learning with these tutorials, but i think this course needs some tutorials about the use of templates and also overwrite it . Is that possible?
Hey Jose!
But you probably want to use and overwrite templates from third-party bundles in *your* project, so this topic does not relate to this course too much as we show how to create third-party bundles here. But we do have some courses where we explain how to override third-party templates, e.g. you can check it out in FOSUserBundle course: https://symfonycasts.com/sc...
I hope this helps.
Cheers!
No, I want to do it in my own Bundle, ie create my own Bundle as explained in this course, but make use of templates, which I think is the only thing missing in this course, together with recipes.
Thanks and I apologize for my English.
Hey Jose carlos C.!
Sorry for my slow reply! You have a great question! So let me do my best to answer it :).
When you render a template in a bundle, there are 2 things to know:
1) You should put them in the Resources/views
directory of the bundle. You can see examples of this in, for example, FOSUserBundle: https://github.com/FriendsOfSymfony/FOSUserBundle/tree/master/Resources/views
2) When you render those templates, you should use a special syntax. For example, suppose you have a Resources/views/products/checkout.html.twig
file. And, your bundle is called SymfonyCastsTemplateBundle
:). Then, when you render a template, you would use the string '@SymfonyCastsTemplate/products/checkout.html.twig'
. Yep, it's literally the @
symbol, then the name of your bundle, but without the Bundle word on the end. Symfony will know to look in the Resources/views directory of the bundle with this syntax.
And, as a bundle author, that's all you need to do. With this setup, any users of your bundle will be able to override your templates by following the standard directions - https://symfony.com/doc/current/bundles/override.html#templates - you don't need to do anything special to allow them to override the templates.
I hope this helps! If you have more questions, please let me know!
Cheers!
Thank you very much for the answer, I think with this explanation is more than enough to do it.
Thank you again!
// 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
}
}
Hey thatnk you about the tuto, what's the best practice is it better to set your code under src/ or bunde/!! I really need a raison for this!