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 @mattxtlm,
Actually, type hint with interfaces makes sense when you use only those methods which are declared in that interface. If interface doesn't have methods you need, I probably need to find another in interface (abstract class or class) which declare them and which is extended by your end class. So if you can't find methods you needed in implemented interfaces or parent classes - type hint with the end class then.
Of course, it will work if you type-hinted with an interface and then pass a class which implements that interface and has some extra methods, but it will be incorrect and could cause errors in the future which are difficult to debug.
Cheers!
I have found that `transform` method actually lives in `MarkdownInterface`.
Any reason we used `MarkdownParserInterface` and renamed function to `transformMarkdown` instead, or both ways are fine?
Yo boykodev!
Ah, nice find! And nope, it really doesn't make any practice difference. If you use MarkdownParserInterface
(which is from KnpMarkdownBundle), then you're basically coupling your code to that library: we must get some instance of their interface. If you use MarkdownInterface
(from the lower-level php-markdown library), in theory that's slightly better, as you're now coupling your code to only that library (so, in theory, you could use that library directly in the future without the bundle, and you wouldn't need to change this class). So, you can see the subtle difference - it's not important, but a good thing to wrap your mind around :).
Cheers!
Simfony is telling me that it actually expects an instance of MarkdownParserInterface, detecting the Max instance from $this->get('markdown.parser') in the controller (as the argument for the MarkdownTransformer constructor) as incorrect:
"Catchable Fatal
Error: Argument 1 passed to
AppBundle\Services\MarkdownTransformer::__construct() must be an
instance of AppBundle\Services\MarkdownParserInterface, instance of
Knp\Bundle\MarkdownBundle\Parser\Preset\Max given..."
What did I missed?
Haha, my fault, it was a missed use statement:
use Knp\Bundle\MarkdownBundle\MarkdownParserInterface;
So MarkdownParserInterface can be properly namespaced in my service php definition.
Hey Josue,
Oh those namespaces ;) Glad you found the problem so quick by yourself, well done!
Cheers!
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
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!