Dependency Inversion Principle

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 $8.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We've made it to the fifth and final SOLID principle: the dependency inversion principle, or DIP. This puppy has a two part definition. Ready? One:

High level modules should not depend on low level modules, both should depend on abstractions - for example, interfaces.

And part two says:

Abstractions should not depend on details. Details - meaning concrete implementations - should depend on abstractions.

Uhh... if that makes sense to you, you are awesome! And... I am jealous of you!

Simpler Definition

How would I rephrase this? Um, yikes. How about this. One:

Classes should depend on interfaces instead of concrete classes.

And two:

Those interfaces should be designed by the class that uses them, not by the classes that will implement them.

That's probably still fuzzy... but don't sweat it. This requires a real example.

Our Spam Detection System!

Here's our new problem. We've been getting so popular - no surprise - that some of our sightings are getting a lot of spam comments... like comments that say that Bigfoot is not real. Those are definitely bots!

So we need a way to determine whether or not a comment is spam based on some business logic that we've created. If you downloaded the course code from this page, then you should have a tutorial/ directory with a CommentSpamManager class inside. Copy that, then go create a new directory in src/ called Comment/... and paste the class there.

... lines 1 - 2
namespace App\Comment;
... lines 4 - 6
class CommentSpamManager
{
public function validate(Comment $comment): void
{
$content = $comment->getContent();
$badWordsOnComment = [];
$regex = implode('|', $this->spamWords());
preg_match_all("/$regex/i", $content, $badWordsOnComment);
if (count($badWordsOnComment[0]) >= 2) {
// We could throw a custom exception if needed
throw new \RuntimeException('Message detected as spam');
}
}
... lines 23 - 33
}

This class basically determines if a comment should be flagged as spam by running a regular expression on the content using a list of predefined spam words. If the content contains two or more of those words, then we consider the comment as spam and throw an exception.

If you think about the single responsibility principle, you could argue that this class already has two responsibilities: the low-level regular expression logic that looks for the spam words and a higher level business logic that decides that two spam words is the limit.

Splitting the Class

Let's pretend that we do think that these are two different responsibilities. And so, we decide to split this class into two pieces. In the Service/ directory, create a new class called RegexSpamWordHelper. Let's see: move the private spamWords() method to the new class... and then create a new public function called getMatchedSpamWords() where we pass it the string $content and return an array of the matched spam words.

... lines 1 - 4
class RegexSpamWordHelper
{
public function getMatchedSpamWords(string $content): array
{
}
private function spamWords(): array
{
return [
'follow me',
'twitter',
'facebook',
'earn money',
'SymfonyCats',
];
}
}

Next, move the regex logic itself into the class. Copy the entire contents of the existing method.... but leave it... then paste. Let's see... we don't need $comment->getContent() anymore.... it's just called $content... and the 0 index of $badWordsOnComment will contain the matches, so we can return that.

... lines 1 - 6
public function getMatchedSpamWords(string $content): array
{
$badWordsOnComment = [];
$regex = implode('|', $this->spamWords());
preg_match_all("/$regex/i", $content, $badWordsOnComment);
return $badWordsOnComment[0];
}
... lines 17 - 29

Beautiful! Now that this class is ready, let's inject it into CommentSpamManager. Add public function __construct() with RegexSpamWordHelper $spamWordHelper. I'll press Alt + Enter and select "Initialize properties" to create that property and set it.

... lines 1 - 5
use App\Service\RegexSpamWordHelper;
class CommentSpamManager
{
private RegexSpamWordHelper $spamWordHelper;
public function __construct(RegexSpamWordHelper $spamWordHelper)
{
$this->spamWordHelper = $spamWordHelper;
}
... lines 16 - 41
}

Below, now we can say $badWordsOnComment = $this->spamWordHelper->getMatchedSpamWords() and pass that $content from above. We don't need any of the logic in the middle anymore. Finally, $badWordsOnComment will contain the array of matches, so we don't need to use the 0 index anymore: just count that entire variable.

... lines 1 - 16
public function validate(Comment $comment): void
{
$content = $comment->getContent();
$badWordsOnComment = $this->spamWordHelper->getMatchedSpamWords($content);
if (count($badWordsOnComment) >= 2) {
throw new \Exception('Message detected as spam');
}
}
... lines 26 - 27

Done!

High Level and Low Level Modules

At this point, we've separated the high-level business logic - deciding how many spam words should cause a comment to be marked as spam - from the low level details: matching and finding the spam words. The dependency inversion principle doesn't necessarily tell us whether or not we should split the original logic into two classes like we just did. That's probably more the concern of the single responsibility principle.

But DIP does teach us to think about our code in terms of "high-level" modules (or classes) like CommentSpamManager - that depend on "low level" modules (or classes) like RegexSpamWordHelper. And it gives us concrete rules about how this relationship should be handled.

Next, let's refactor the relationship between these two classes to be dependency inversion principle compliant. We'll see, in real terms, exactly what changes each of the two parts of this principle want us to make.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4 || ^8.0",
        "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.12.2
        "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
    }
}