Buy

Being Awesome with Type-Hints

20 lines src/AppBundle/Service/MarkdownTransformer.php
... 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:

14 lines vendor/knplabs/knp-markdown-bundle/Parser/Preset/Max.php
... lines 1 - 6
/**
* Full featured Markdown Parser
*/
class Max extends MarkdownParser
{
}

Ah, this extends MarkdownParser and that does all the work:

245 lines vendor/knplabs/knp-markdown-bundle/Parser/MarkdownParser.php
... 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:

22 lines src/AppBundle/Service/MarkdownTransformer.php
... 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():

16 lines vendor/knplabs/knp-markdown-bundle/MarkdownParserInterface.php
... 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():

22 lines src/AppBundle/Service/MarkdownTransformer.php
... 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:

245 lines vendor/knplabs/knp-markdown-bundle/Parser/MarkdownParser.php
... 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!

  • 2017-01-22 weaverryan

    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!

  • 2017-01-22 Stan

    Does reflection API resolve dependencies?
    Isn't it slow? In ZF3 you can do this, but usually you creates a factory which creates objects manually.
    Is it big deal?

    UPD. Oh, nevermind. We pass it manually

  • 2016-12-23 Victor Bocharsky

    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!

  • 2016-12-23 mattxtlm

    Hey there,

    just to clear things a bit. I can type hint an interface, but it has to have the methods on it?

    I did something like this and it worked. However, phpstorm was angry about the countMessages() method.


    // in the Controller
    $mailer = new EmailService($this->get('mailer'),$this->get('swiftmailer.mailer.default.plugin.messagelogger'));
    // ...

    // in EmailService Class
    public function __construct(Swift_Mailer $mailer, Swift_Events_EventListener $logger) {
    $this->mailer = $mailer;
    $this->logger = $logger;
    }

    public function sendMail($subject, $to, $name, $body)
    {
    // ... some more code
    $messageCount = $this->logger->countMessages();
    //..

    I guess, phpstorm was angry, because it didn't find the method countMessages(). In fact, the interface is empty (no clue how this is supposed to work, anyway). But as I dropped in the 'swiftmailer.mailer.default.plugin.messagelogger' service, it worked, because the method lives there. So, I guess choosing an interface or any other higher level parent does only make sense in case a suitable method does exist?

    Cheers!