Creating a Service

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

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

Login Subscribe

Okay, so bundles give us services and services do work. So... if we needed to write our own custom code that did work... can we create our own service class and put the logic there? Absolutely! And it's something that you're going to do all the time. It's a great way to organize your code, gives you the ability to re-use logic and allows you to write unit tests if you want. So... let's do it!

We're already doing some work. It may not look like a lot, but the logic of parsing the markdown and caching the result is work:

... lines 1 - 11
class QuestionController extends AbstractController
{
... lines 14 - 31
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache)
{
$answers = [
'Make sure your cat is sitting `purrrfectly` still ­čĄú',
'Honestly, I like furry shoes better than MY cat',
'Maybe... try saying the spell backwards?',
];
$questionText = 'I\'ve been turned into a cat, any *thoughts* on how to turn back? While I\'m **adorable**, I don\'t really care for cat food.';
$parsedQuestionText = $cache->get('markdown_'.md5($questionText), function() use ($questionText, $markdownParser) {
return $markdownParser->transformMarkdown($questionText);
});
dump($cache);
return $this->render('question/show.html.twig', [
'question' => ucwords(str_replace('-', ' ', $slug)),
'questionText' => $parsedQuestionText,
'answers' => $answers,
]);
}
}

It would be nice to move this into its own class. That would make the controller a bit easier to read and we could re-use this markdown caching logic somewhere else if we needed to, which we will later.

Creating the Service

So how do we create our very own service? Start by creating a class anywhere in src/. It doesn't matter where but I'll create a new sub-directory called Service/, which I often use when I can't think of a better directory to put my class in. Inside, add a new PHP class called, how about MarkdownHelper:

... lines 1 - 2
namespace App\Service;
class MarkdownHelper
{
... lines 7 - 12
}

And cool! PhpStorm automatically added the correct namespace to the class. Thanks!

Unlike controllers, this class has nothing to do with Symfony... it's just a class we are creating for our own purposes. And so, it doesn't need to extend a base class or implement an interface: this class will look however we want.

Let's think: we're probably going to want a function called something like parse(). It will need a string argument - how about $source - and it will return a string, which will be the finished HTML:

... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
... lines 9 - 11
}
}

Nice! Back in QuestionController, copy the three lines of logic and paste them into the new method. Let's fix a few things: return the value:

... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
$parsedQuestionText = $cache->get('markdown_'.md5($questionText), function() use ($questionText, $markdownParser) {
return $markdownParser->transformMarkdown($questionText);
});
}
}

then change $questionText to $source in three different places:

... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
return $cache->get('markdown_'.md5($source), function() use ($source, $markdownParser) {
return $markdownParser->transformMarkdown($source);
});
}
}

Your Class is Already a Service! Use it!

We still have a few undefined variables... but... I want you to ignore them for now. Because, congratulations! This may not, ya know, "work" yet, but you just created your first service! Remember: a service is just a class that does work.

Ok, so, how can we use this inside our controller? We already know the answer. If we need a service from the container, we need to add an argument with the right type-hint. But... is our service already... somehow in Symfony's container? Let's find out! At your terminal, run:

php bin/console debug:autowiring Markdown

Hmm, it only shows the two results from the bundle. But wait! At the bottom it says:

1 more concrete service would be displayed when adding the --all option.

Um... ok - let's add --all to this:

php bin/console debug:autowiring Markdown --all

And there it is! Why did we need this --all flag? Well, the "mostly-true" explanation is that, to keep this list short, Symfony hides your services from the list... because you already know they exist.

Anyways, yes! Our service is - somehow - already available in Symfony's container. We'll learn how that happened later, but the important thing now is that we can use the MarkdownHelper type-hint to get an instance of our class.

Let's do it! Back in the controller, add a 4th argument: MarkdownHelper $markdownHelper:

... lines 1 - 4
use App\Service\MarkdownHelper;
... lines 6 - 12
class QuestionController extends AbstractController
{
... lines 15 - 32
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache, MarkdownHelper $markdownHelper)
{
... lines 35 - 48
}
}

Down below, say $parsedQuestionText = $markdownHelper->parse($questionText):

... lines 1 - 12
class QuestionController extends AbstractController
{
... lines 15 - 32
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache, MarkdownHelper $markdownHelper)
{
... lines 35 - 41
$parsedQuestionText = $markdownHelper->parse($questionText);
... lines 43 - 48
}
}

Testing time! Refresh and... yea! Undefined variable coming from MarkdownHelper! Woo! I'm happy because this proves that the service was autowired into the controller. The method is blowing up... but our service is alive!

Inside of MarkdownHelper, we're trying to use the cache and markdown parser services... but we don't have access to those here:

... lines 1 - 4
class MarkdownHelper
{
public function parse(string $source): string
{
return $cache->get('markdown_'.md5($source), function() use ($source, $markdownParser) {
return $markdownParser->transformMarkdown($source);
});
}
}

How can we get them? The answer to that is "dependency injection": a threatening-sounding word for a delightfully simple concept. It's also one of the most fundamental concepts in Symfony... or really any object-oriented coding. Let's tackle it next!

Leave a comment!