If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOkay, 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.
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); | |
}); | |
} | |
} |
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!
Hey David,
According to the PSR4, you will need to create the same folder structure in the src/ dir to match the new namespace, i.e. if you want Some\Other\Namespace - then you will need the next file structure: src/Some/Other/Namespace/ and put there your PHP class. Abut the autoload - yeah, you might need to tweak its config in composer.json, though it depends on your current config. Most probably, you can guess it yourself, just duplicate the current config line for App namespace and do the same for yours. I suppose this one will work for you:
"autoload": {
"psr-4": {
"Some\\": "src/Some/",
"App\\": "src/"
}
},
This should work. Or, take a look at Composer docs where this all is explained in full: https://getcomposer.org/doc/01-basic-usage.md#autoloading
I hope this helps!
Cheers!
It doesn't seem to be as simple as editing composer.json to get symfony to allow different namespaces to coexist under src. In my composer.json I added the psr-4 namespace:
"autoload": {
"psr-4": {
"DMz\\" : "src/DMz/",
"App\\": "src/"
}
and I put a class DMz\Weather in a file src/DMz/Weather.php. The class' namespace declaration is <br />namespace DMz;<br />
but I get a LoaderLoadException:<br />Expected to find class "App\DMz\Weather" in file"/opt/www/new.davidmintz.org/src/DMz/Weather.php" while importing services from resource "../src/", but it was not found! Check the namespace prefix used with the resource in /opt/www/new.davidmintz.org/config/services.yaml (which is being imported from "/opt/www/new.davidmintz.org/src/Kernel.php").
and my services.yaml has this appended under the services
key:
DMz\:
resource: '../src/DMz'
and I've also tried putting this before -- rather than after -- the App\ entry, with this result:
Error: cannot redeclare class DMz\Weather because the name is already in use.```
So maybe this is too far beyond the scope of this tutorial -- if so, my apologies, feel free to ignore this.
Hey David,
It does go beyond of this tutorial, but I can give you some minor tips I think ;) Actually, you did everything correct I suppose, probably the only missing piece here is that you have to recompile Composer's autoloader. You can do it by running "composer update --lock" I think :) Or with more specific command: "composer dump-autoload", but don't forget to clear the cache after, but I'd suggest to use "composer update --lock" for simplicity.
I hope this helps!
Cheers!
Oops, I forgot to mention that I did composer dump-autoload
and I <i>think</i> I also cleared my cache but I will go back and fiddle a little more. Final word: that this is so -- not-easy? -- leads me to think that maybe the philosophy here is: if you think you need a separate namespace, maybe it's because you want to re-use that differently-namespaced-code across multiple projects, and if so, put it in a repository and require
it with composer, so it ends up living in vendor
. Could I be right?
Thanks again for indulging me!
Hey David,
Yep, mostly like that :) If you need to reuse some code - the best approach would be to create a bundle and then require it via Composer as a dependency, yeah. This way you will store that code in one centrilized place, and if you would ever need to change it - you will be able to change it in one spot and then upgrade the bundle that will change it across your projects - so, there're clearly more benefits with this approach than just copy/paste those files from project to project.
About this namespace thing - well, I think it's still should be easy to do, the biggest problem with the cache probably, i.e. when you did everything correct but still not see any changes because of the cache - this may just mislead. But this behaviour totally work too, it depends on your specific use case and what better for you. But yeah, with the weather example, especially if you want to reuse it in a few project - I'd suggest you to create a separate repo on GitHub and require it in your project.
Cheers!
OMG. I am so sorry for consuming so much of your time. Clearing the cache seems to have been the solution. Someday I will find a way to contribute something to Symfony to redeem myself! (I might be sufficiently qualified to fix typographical and minor usage errors in the English documentation!) Thanks again.
Hey David!
No problem! ;) I'm happy to hear it worked for you eventually - this means the time wan't wasted at all :)
> Someday I will find a way to contribute something to Symfony to redeem myself
This would be awesome! ❤️ Yeah, you can start with Symfony docs! It's much easier than contribute in Symfony core code, and Symfony docs really need help from devs. So, my advice - take a look at Symfony docs, you can add some good example, or add a missing information, or just fix a typo there - everything would be great :)
Cheers!
// composer.json
{
"require": {
"php": "^7.3.0 || ^8.0.0",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"sentry/sentry-symfony": "^4.0", // 4.0.3
"symfony/asset": "5.0.*", // v5.0.11
"symfony/console": "5.0.*", // v5.0.11
"symfony/debug-bundle": "5.0.*", // v5.0.11
"symfony/dotenv": "5.0.*", // v5.0.11
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.0.*", // v5.0.11
"symfony/monolog-bundle": "^3.0", // v3.6.0
"symfony/profiler-pack": "*", // v1.0.5
"symfony/routing": "5.1.*", // v5.1.11
"symfony/twig-pack": "^1.0", // v1.0.1
"symfony/var-dumper": "5.0.*", // v5.0.11
"symfony/webpack-encore-bundle": "^1.7", // v1.8.0
"symfony/yaml": "5.0.*" // v5.0.11
},
"require-dev": {
"symfony/maker-bundle": "^1.15", // v1.23.0
"symfony/profiler-pack": "^1.0" // v1.0.5
}
}
If I want to create a service class under /src but I want it to be in Some\Other\Namespace rather than App\, how do we do that (in addition to editing the autoload/psr-4 setting in composer.json)?