Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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.

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

... 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.

Leave a comment!

8
Login or Register to join the conversation

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!

Reply
Victor Avatar Victor | SFCASTS | posted 5 years ago | edited

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!

Reply

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?

Reply

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!

Reply

Thanks for the detailed explanation! :)

Reply
Default user avatar
Default user avatar Josué Martín Hernández | posted 5 years ago

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?

Reply
Default user avatar
Default user avatar Josué Martín Hernández | Josué Martín Hernández | posted 5 years ago

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.

Reply

Hey Josue,

Oh those namespaces ;) Glad you found the problem so quick by yourself, well done!

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

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