Buy Access to Course
07.

Allowing Entire Services to be Overridden

Share this awesome video!

|

Keep on Learning!

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.

211 lines | lib/LoremIpsumBundle/src/KnpUIpsum.php
// ... 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().

211 lines | lib/LoremIpsumBundle/src/KnpUIpsum.php
// ... 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().

17 lines | src/Service/CustomWordProvider.php
// ... 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.

17 lines | src/Service/CustomWordProvider.php
// ... 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.

4 lines | config/packages/knpu_lorem_ipsum.yaml
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.