Liskov Takeaways & Service Alias

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

To celebrate our new system, let's see it in action. In BigFootSightingController, after the addFlash(), let's also add some duration information. But since we don't know for sure if we're using the "debuggable" version of the service, add if $bfsScore is an instance of DebuggableBigFootSightingScore, then $this->addFlash('success', sprintf(...)) with:

Btw, the scoring took %f milliseconds

Passing $bfsScore->getCalculationTime() times 1000 to convert from microseconds to milliseconds.

... lines 1 - 6
use App\Model\DebuggableBigFootSightingScore;
... lines 8 - 14
class BigFootSightingController extends AbstractController
... lines 16 - 20
public function upload(Request $request, SightingScorer $sightingScorer, EntityManagerInterface $entityManager)
{
... lines 23 - 25
if ($form->isSubmitted() && $form->isValid()) {
... lines 27 - 38
if ($bfsScore instanceof DebuggableBigFootSightingScore) {
$this->addFlash('success', sprintf(
'Btw, the scoring took %f milliseconds',
$bfsScore->getCalculationTime() * 1000
));
}
... lines 45 - 48
}
... lines 50 - 53
}
... lines 55 - 64
}

Cool! But... wait: didn't I say that instanceof is a signal that we may be breaking Liskov's principle? Yep! But I'm not too worried about it here, for a few reasons. First, this is my controller... whose job is to tie all the ugly pieces of my app together. And second, I'm using the instanceof to detect if I can add functionality... not to work-around a misbehaving subclass.

However, another solution, depending on if you really do need to substitute this class only in one environment, is to explicitly say that you require the debuggable version of the service. So instead of saying, "I allow any SightingScorer", we could say, "I specifically need a DebuggableSightingScorer".

If we did that, we wouldn't need the instanceof because we would know that that service returns a DebuggableBigFootSightingScore, which has the getCalculationTime() method on it.

... lines 1 - 21
public function upload(Request $request, DebuggableSightingScorer $sightingScorer, EntityManagerInterface $entityManager)
{
... lines 24 - 26
if ($form->isSubmitted() && $form->isValid()) {
... lines 28 - 39
$this->addFlash('success', sprintf(
'Btw, the scoring took %f milliseconds',
$bfsScore->getCalculationTime() * 1000
));
... lines 44 - 47
}
... lines 49 - 52
}
... lines 54 - 65

But... we're missing one tiny config detail in Symfony. Try to refresh the page. It breaks!

Cannot autowire service DebuggableSightingScorer: argument $scoringFactors is type-hinted iterable. You should configure its value explicitly.

Wait... we hit this error when we worked on the open-closed principle. And, in config/services.yaml, we fixed it by specifically wiring the $scoringFactors argument. Why isn't that working anymore?

Thanks to auto-registration - the feature that automatically registers all classes in src/ as a service - there is a separate service in our container called DebuggableSightingScorer. You can see it if you run:

php bin/console debug:container Sighting

Yup! There's a DebuggableSightingScorer service and a separate service for SightingScorer. This is... not what we want. Really, I want Symfony to pass us the same service, regardless of whether we type-hint DebuggableSightingScorer or SightingScorer.

We can do that by adding an alias. Inside services.yaml, say App\Service\DebuggableSightingScorer, colon, an @ symbol and then App\Service\SightingScorer.

... lines 1 - 7
services:
... lines 9 - 32
App\Service\DebuggableSightingScorer: '@App\Service\SightingScorer'
... lines 34 - 39

This says: whenever someone tries to autowire or use the DebuggableSightingScorer service, you should actually pass them the SightingScorer service... which, I know, is actually an instance of the DebuggableSightingScorer class. It can be a bit confusing.

Back at your terminal, run debug:container again:

php bin/console debug:container Sighting

It looks like there are still 2 services, but if you hit "6" to look at the "Debuggable" one, on top, it says:

This is an alias for the service App\Service\SightingScorer.

And over in the browser, when we refresh... it works again!

Liskov Principle Takeaways

So the big takeaway from Liskov's principle is this: make sure that when you have a "subtype" - a class that extends another or that implements an interface - it follows the rules of that parent type. It doesn't do anything surprising. That's it. And PHP even prevents us from most Liskov violations.

The most interesting part of Liskov for me is learning about the things that we are allowed to do. Like, you are allowed to change the return type of a method as long as you make it more specific. Or, the opposite for argument types: you can change them... as long as you make them less specific.

Okay, next up is solid principle number 4: the interface segregation principle.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4 || ^8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.12.1
        "doctrine/doctrine-bundle": "^2", // 2.3.1
        "doctrine/doctrine-migrations-bundle": "^3", // 3.1.1
        "doctrine/orm": "^2", // 2.8.4
        "knplabs/knp-time-bundle": "^1.15", // v1.16.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^6.0", // v6.1.2
        "symfony/console": "5.2.*", // v5.2.6
        "symfony/dotenv": "5.2.*", // v5.2.4
        "symfony/flex": "^1.9", // v1.12.2
        "symfony/form": "5.2.*", // v5.2.6
        "symfony/framework-bundle": "5.2.*", // v5.2.6
        "symfony/http-client": "5.2.*", // v5.2.6
        "symfony/mailer": "5.2.*", // v5.2.6
        "symfony/property-access": "5.2.*", // v5.2.4
        "symfony/property-info": "5.2.*", // v5.2.4
        "symfony/security-bundle": "5.2.*", // v5.2.6
        "symfony/serializer": "5.2.*", // v5.2.4
        "symfony/twig-bundle": "5.2.*", // v5.2.4
        "symfony/validator": "5.2.*", // v5.2.6
        "symfony/webpack-encore-bundle": "^1.6", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.5
        "twig/cssinliner-extra": "^3.3", // v3.3.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.0
        "twig/twig": "^2.12|^3.0" // v3.3.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.2", // 3.4.0
        "fakerphp/faker": "^1.13", // v1.14.1
        "symfony/debug-bundle": "^5.2", // v5.2.4
        "symfony/maker-bundle": "^1.13", // v1.30.2
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/stopwatch": "^5.2", // v5.2.4
        "symfony/var-dumper": "^5.2", // v5.2.6
        "symfony/web-profiler-bundle": "^5.2" // v5.2.6
    }
}