Buy
Buy

Allowing Entire Services to be Overridden

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When 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.

Making the Word Provider Configurable

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
}

Creating our Custom Word Provider

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.

Leave a comment!

  • 2018-10-05 José Carlos ES

    Thank you very much for the answer, I think with this explanation is more than enough to do it.

    Thank you again!

  • 2018-10-05 weaverryan

    Hey José Carlos ES!

    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/FriendsO...

    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/cur... - 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!

  • 2018-09-24 José Carlos ES

    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.

  • 2018-09-24 Victor Bocharsky

    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!

  • 2018-09-22 José Carlos ES

    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?