Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Extensibility with Interfaces & Aliases

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $8.00

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

Login Subscribe

I want to make two other changes to the new "word provider" setup. The first is optional: it's another common method for making the word provider configurable.

Go back into our services.xml file. Right now, we set the first argument inside of the XML file, then override that argument in the extension class, if a different value is provided. Another option - and we'll talk about the advantages later - is to use a service alias.

Copy the alias we created earlier in order to enable autowiring. Create a new alias whose id is knpu_lorem_ipsum.word_provider and set the alias to the knp_word_provider service id above.

... lines 1 - 6
<services>
... lines 8 - 13
<service id="knpu_lorem_ipsum.word_provider" alias="knpu_lorem_ipsum.knpu_word_provider" public="false" />
... line 15
</services>
... lines 17 - 18

Thanks to this, there is now a new service in the container called knpu_lorem_ipsum.word_provider. But when someone references it, it actually just points to our knpu_lorem_ipsum.knpu_word_provider. Now, for the argument to KnpUIpsum, pass the alias id instead.

... lines 1 - 7
<service id="knpu_lorem_ipsum.knpu_ipsum" class="KnpU\LoremIpsumBundle\KnpUIpsum" public="true">
<argument type="service" id="knpu_lorem_ipsum.word_provider" />
</service>
... lines 11 - 18

So far, this won't change anything. But open the extension class. Instead of changing the argument, we can override the alias to point to their service id. Do this with $container->setAlias(). First pass knpu_lorem_ipsum.word_provider and set this alias to $config['word_provider']. We don't need the new Reference() here because the setAlias() method expects this to be a service ID.

... lines 1 - 10
class KnpULoremIpsumExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
... lines 15 - 21
if (null !== $config['word_provider']) {
$container->setAlias('knpu_lorem_ipsum.word_provider', $config['word_provider']);
}
... lines 25 - 26
}
... lines 28 - 32
}

And before even trying it, copy the service alias, find your terminal, and run:

php bin/console debug:container --show-private knpu_lorem_ipsum.word_provider

Yes! This is an alias to our CustomWordProvider. And that means that the first argument to KnpUIpsum will use that. Refresh to make sure it still works. It does!

There's no amazing reason to use this alias strategy versus what we had before, but there are two minor advantages. First, if we needed to reference the word provider service in multiple places - probably in services.xml - using an alias is easier, because you don't need to remember to, for example, replace 5 different arguments where the service is used. And second, if we wanted this service to be used directly by our users, creating an alias is the only way to give them a service id they can reference, even if they override the word provider to be something else.

Creating a WordProviderInterface

Ok, our setup is really, really nice. But there is one restriction we're putting on our user that we really do not need to! Open KnpUIpsum and scroll all the way to the constructor. The first argument is type-hinted with KnpUWordProvider. This means that if the user wants to create their own word provider, they must extend our original KnpUWordProvider. We are doing this... because we just want to add a new word to the list, but this should not be required! All we care about is that the service has a getWordList() method that returns an array.

In other words, this is the perfect use-case for an interface! Wooo! In the bundle, create a new PHP class. Call it WordProviderInterface and change the "kind" from class to interface.

Inside, add the getWordList() method and make it return an array. This is also the perfect place to add some documentation about what this method should do.

... lines 1 - 4
interface WordProviderInterface
{
/**
* Return an array of words to use for the fake text.
*
* @return array
*/
public function getWordList(): array;
}

With the interface done, go back to KnpUIpsum, change the type-hint to WordProviderInterface. The user can now pass anything they want, as long as it has this getWordList() method... because that is what we're using at the bottom of KnpUIpsum.

... lines 1 - 9
class KnpUIpsum
{
... lines 12 - 17
public function __construct(WordProviderInterface $wordProvider, bool $unicornsAreReal = true, $minSunshine = 3)
... lines 19 - 209
}

Then, of course, we also need to go open our provider and make sure it implements this interface: implements WordProviderInterface.

... lines 1 - 4
class KnpUWordProvider implements WordProviderInterface
{
... lines 7 - 142
}

If you try it now... not broken! And yea, our CustomWordProvider will still extend KnpUWordProvider, but that's now optional - we could just implement the interface directly.

Next, let's take a big step and move our bundle out of our code and give it it's own composer.json file!

Leave a comment!

6
Login or Register to join the conversation

At the end of the script :

And yea, our CustomUserProvider will still extend KnpUWordProvider

CustomUserProvider ? I missed something ?

1 Reply

Hey Capucine,

Not really, it's just a misprint :) Thanks for pointing this, I fixed it in https://github.com/knpunive...

Cheers!

Reply
Maik T. Avatar
Maik T. Avatar Maik T. | posted 3 years ago | edited

One more comment:

If you implement the Interface in your CustomWordProvider, $words = parent::getWordList(); does not work longer because not extending a Service wich is the parent.

Reply

Hey Maik T.!

That's correct :). This is really up to the user at this point - they can choose to make their CustomWordProvider continue to extend KnpUWordProvider or only implement the interface. But yea, then they wouldn't be able to call the parent function :). If you didn't want to extend the internal class, the user could use "service decoration". That allows you to fetch the KnpUWordProvider "words" without extending the class - the most pure solution for whoever is implementing your bundle - https://symfonycasts.com/screencast/api-platform-security/service-decoration#service-declaration-amp-decoration

Cheers!

Reply
Maik T. Avatar
Maik T. Avatar Maik T. | posted 3 years ago | edited

There is a logical Error in the script:

If you remove the Custom word Provider and refresh the site you will get a ServiceNotFoundException because the Interface is included but no Service is really set.

To fix it you can still fix it simply in the Extension class by changing following code:

`

    if(!is_null($config['word_provider'])) {
        $container->setAlias('knpu_lorem_ipsum.word_provider', $config['word_provider']);
    } else {
        $container->setAlias('knpu_lorem_ipsum.word_provider',         'knpu_lorem_ipsum.knpu_word_provider');
    }

`

Reply

Hey Maik T.!

Hmm. Are you sure that's needed? The part you added was the "else", correct? That alias was already added in the services.xml file - https://symfonycasts.com/sc...

Or am I missing something? It's totally possible :).

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built using Symfony 4, but most of the concepts apply fine to Symfony 5!

What PHP libraries does this tutorial use?

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