Being Awesome with Type-Hints
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.
| // ... lines 1 - 4 | |
| class MarkdownTransformer | |
| { | |
| // ... lines 7 - 8 | |
| public function __construct($markdownParser) | |
| // ... lines 10 - 18 | |
| } | 
What type of object is this $markdownParser argument? Oh, you can't tell? Well,
neither can I. With no type-hint, this could be anything! A MarkdownParser object,
a string, an octopus!
We need to add a typehint to make our code clearer... and avoid weird errors in case we accidentally pass in something else... like an octopus.
Run:
./bin/console debug:container markdown
And select markdown.parser - that's the service we're passing into MarkdownTransformer.
Ok, it's an instance of Knp\Bundle\MarkdownBundle\Parser\Preset\Max. We can use that
as the type-hint.
But hold on - I'm going to complicate things... but then we'll all learn something
cool and celebrate. Press shift+shift, type "max" and open that class:
| // ... lines 1 - 6 | |
| /** | |
| * Full featured Markdown Parser | |
| */ | |
| class Max extends MarkdownParser | |
| { | |
| } | 
Ah, this extends MarkdownParser and that does all the work:
| // ... lines 1 - 8 | |
| /** | |
| * MarkdownParser | |
| * | |
| * This class extends the original Markdown parser. | |
| * It allows to disable unwanted features to increase performances. | |
| */ | |
| class MarkdownParser extends MarkdownExtra implements MarkdownParserInterface | |
| { | |
| // ... lines 17 - 243 | |
| } | 
And this implements a MarkdownParserInterface. We could type-hint with Max,
MarkdownParser or MarkdownParserInterface: they will all work. BUT, when possible,
it's best to find a base class - or better - and interface that has the methods
on it you need, and use that.
Type-hint the argument with MarkdownParserInterface:
| // ... lines 1 - 4 | |
| use Knp\Bundle\MarkdownBundle\MarkdownParserInterface; | |
| class MarkdownTransformer | |
| { | |
| // ... lines 9 - 10 | |
| public function __construct(MarkdownParserInterface $markdownParser) | |
| // ... lines 12 - 20 | |
| } | 
Why is this the best option? Two small reasons. First, in theory, we could swap out
the $markdownParser for a different object, as long as it implemented this interface.
Second, it's really clear what methods we can call on the $markdownParser property:
only those on that interface.
But hold on a second, PhpStorm is angry about calling transform() on $this->markdownParser:
Method "transform" not found in class
MarkdownParserInterface
Weird! Open that interface. Oh, it has only one method: transformMarkdown():
| // ... lines 1 - 4 | |
| interface MarkdownParserInterface | |
| { | |
| /** | |
| * Converts text to html using markdown rules | |
| * | |
| * @param string $text plain text | |
| * | |
| * @return string rendered html | |
| */ | |
| function transformMarkdown($text); | |
| } | 
Hold on: to be clear: everything will work right now. Refresh to prove it.
The weirdness is just that we are forcing an object that implements MarkdownParserInterface
to be passed in... but then we're calling a method that's not on that interface.
Change our call to transformMarkdown():
| // ... lines 1 - 6 | |
| class MarkdownTransformer | |
| { | |
| // ... lines 9 - 15 | |
| public function parse($str) | |
| { | |
| return $this->markdownParser | |
| ->transformMarkdown($str); | |
| } | |
| } | 
Inside MarkdownParser, you can see that transformMarkdown() and transform()
do the same thing anyways:
| // ... lines 1 - 14 | |
| class MarkdownParser extends MarkdownExtra implements MarkdownParserInterface | |
| { | |
| // ... lines 17 - 114 | |
| public function transformMarkdown($text) | |
| { | |
| return parent::transform($text); | |
| } | |
| // ... lines 119 - 243 | |
| } | 
This didn't change any behavior: it just made our code more portable: our class will work
with any object that implements MarkdownParserInterface.
And if this doesn't completely make sense, do not worry. Just focus on this takeaway:
when you need an object from inside a class, use dependency injection. And when you
add the __construct() argument, type-hint it with either the class you see in debug:container
or an interface if you can find one. Both totally work.
 
             
    
         
            
Hey Stan!
I see that you figured out how it works :). I just wanted to add one thing: with Symfony's autowiring feature, you *can* choose to have Symfony use Reflection to automatically resolve each dependency: https://knpuniversity.com/s.... So, passing dependencies is totally explicit... unless you want opt into some magic (I particularly like autowiring).
Cheers!