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 SubscribeTo 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-hintediterable
. 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!
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.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"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.18.7
"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
}
}