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 SubscribeOur MarkdownHelper
service is... sort of working:
... 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 can call it from the controller... but inside, we're trying to use two services - cache and markdown parser - that we don't have access to. How can we get those objects?
Real quick: I've said many times that there are service objects "floating around" in Symfony. But even though that's true, you can't just grab them out of thin air. There's no, like, Cache::get()
static call or something that will magically give us that object. And that's good - that's a recipe for writing bad code.
So how can we get access to services? Currently, we only know one way: by autowiring them into our controller methods:
... lines 1 - 4 | |
use App\Service\MarkdownHelper; | |
use Knp\Bundle\MarkdownBundle\MarkdownParserInterface; | |
... lines 7 - 9 | |
use Symfony\Contracts\Cache\CacheInterface; | |
... lines 11 - 12 | |
class QuestionController extends AbstractController | |
{ | |
... lines 15 - 32 | |
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache, MarkdownHelper $markdownHelper) | |
{ | |
... lines 35 - 48 | |
} | |
} |
Which we can't do here, because that's a superpower that only controllers have.
Hmm, but one idea is that we could pass the markdown parser and cache from our controller into parse()
:
... 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 | |
} | |
} |
This won't be our final solution, but let's try it!
On parse()
, add two more arguments: MarkdownParserInterface $markdownParser
and CacheInterface
- from Symfony\Contracts
- $cache
:
... lines 1 - 4 | |
use Knp\Bundle\MarkdownBundle\MarkdownParserInterface; | |
use Symfony\Contracts\Cache\CacheInterface; | |
class MarkdownHelper | |
{ | |
public function parse(string $source, MarkdownParserInterface $markdownParser, CacheInterface $cache): string | |
{ | |
... lines 12 - 14 | |
} | |
} |
Cool! This method is happy.
Back in QuestionController
, pass the two extra arguments: $markdownParser
and $cache
:
... 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, $markdownParser, $cache); | |
... lines 43 - 48 | |
} | |
} |
Ok team - let's see if it works! Find your browser and refresh. It does!
On a high level, this solution makes sense: since we can't grab service objects out of thin air in MarkdownHelper
, we pass them in. But, if you think about it, the markdown parser and cache objects aren't really "input" to the parse()
function. What I mean is, the $source
argument to parse()
makes total sense: when we call the method, we of course need to pass in the content we want parsed.
But these next two arguments don't really control how the function behaves... you would probably always pass these same values every time you called the method. No, instead of function arguments, these objects are more dependencies that the service needs in order to do its work. It's just stuff that must be available so that parse()
can do its job.
For dependencies like this - for service objects or configuration that your service simply needs, instead of passing them through the individual methods, we instead pass them through the constructor.
At the top, create a new public function __construct()
. Move the two arguments here... and delete them from parse()
:
... lines 1 - 4 | |
use Knp\Bundle\MarkdownBundle\MarkdownParserInterface; | |
use Symfony\Contracts\Cache\CacheInterface; | |
class MarkdownHelper | |
{ | |
... lines 10 - 12 | |
public function __construct(MarkdownParserInterface $markdownParser, CacheInterface $cache) | |
{ | |
... lines 15 - 16 | |
} | |
public function parse(string $source): string | |
{ | |
... lines 21 - 23 | |
} | |
} |
Before we finish this, I need to tell you that autowiring in fact works in two places. We already know that you can autowire services into your controller methods. But you can also autowire services into the __construct()
method of a service. In fact, that is the main place where autowiring is meant to work. The fact that autowiring also works for controller methods was... kind of an added feature to make life easier. And it only works for controllers - you can't add a MarkdownParserInterface
argument to parse()
and expect Symfony to autowire that because we are the ones that are calling that method and passing it arguments.
Anyways, when Symfony instantiates MarkdownHelper
, it will pass us these two arguments thanks to autowiring. What do we... do with them? Create two private properties: $markdownParser
and $cache
. Then, in the constructor, set those: $this->markdownParser = $markdownParser
and $this->cache = $cache
:
... lines 1 - 7 | |
class MarkdownHelper | |
{ | |
private $markdownParser; | |
private $cache; | |
public function __construct(MarkdownParserInterface $markdownParser, CacheInterface $cache) | |
{ | |
$this->markdownParser = $markdownParser; | |
$this->cache = $cache; | |
} | |
... lines 18 - 24 | |
} |
Basically, when the object is instantiated, we're taking those objects and storing them for later. Then, whenever we call parse()
, the two properties will already hold those objects. Let's use them: $this->cache
, and then we don't need to pass $markdownParser
to the use
because we can instead say $this->markdownParser
:
... lines 1 - 7 | |
class MarkdownHelper | |
{ | |
... lines 10 - 18 | |
public function parse(string $source): string | |
{ | |
return $this->cache->get('markdown_'.md5($source), function() use ($source) { | |
return $this->markdownParser->transformMarkdown($source); | |
}); | |
} | |
} |
I love it! This class is now a perfect service: we add our dependencies to the constructor, set them on properties, then use them below.
By the way, what we just did has a fancy name! Ooo. It's dependency injection. But don't be too impressed: it's a simple concept. Whenever you're inside a service - like MarkdownHelper
- and you realize that you need something that you don't have access to, you'll follow the same solution: add another constructor argument, create a property, set that onto the property, then use it in your methods. That is dependency injection. A big word to basically mean: if you need something, don't expect to grab it out of thin air: force Symfony to pass it to you by adding it to the constructor.
Phew! Back in QuestionController
, we can celebrate by removing the two extra arguments to parse()
:
... lines 1 - 10 | |
class QuestionController extends AbstractController | |
{ | |
... lines 13 - 30 | |
public function show($slug, MarkdownHelper $markdownHelper) | |
{ | |
... lines 33 - 39 | |
$parsedQuestionText = $markdownHelper->parse($questionText); | |
... lines 41 - 46 | |
} | |
} |
And when we move over and refresh... it works!
If this didn't feel totally comfortable yet, don't worry. The process of creating services is something that we're gonna to do over and over again. The benefit is that we now have a beautiful service - a tool - that we can use from anywhere in our app. We pass it the markdown string and it takes care of the caching and markdown processing.
Heck, in QuestionController
, we don't even need the $markdownParser
and $cache
arguments to the show()
method!
... lines 1 - 5 | |
use Knp\Bundle\MarkdownBundle\MarkdownParserInterface; | |
... lines 7 - 9 | |
use Symfony\Contracts\Cache\CacheInterface; | |
... lines 11 - 12 | |
class QuestionController extends AbstractController | |
{ | |
... lines 15 - 32 | |
public function show($slug, MarkdownParserInterface $markdownParser, CacheInterface $cache, MarkdownHelper $markdownHelper) | |
{ | |
... lines 35 - 48 | |
} | |
} |
Remove them and, on top of the class, even though it doesn't hurt anything, let's delete the two use
statements.
Next: the service container holds services! That's true! But it also holds something else: scalar configuration.
Hey, in our MarkdownHelper's constructor you can type even less -- if you are using PHP 8 -- by using contructor property promotion. https://www.php.net/manual/....
btw I am LOVING these tutorials and Symfony. I am coming from the Laminas (formerly Zend) framework where you have to write your own factories and manually register them in configuration so that the container will know how to instantiate them for you. Blaugh!
Woo for PHP 8! In our Symfony 6 tutorials starting in January, we can finally use PHP 8 (we always try to use the minimum-required version), so life will be better!
> btw I am LOVING these tutorials and Symfony
Welcome to Symfony - happy to have you :). And I see you're a fellow runner! I don't run as much anymore (I have a 5 year old), but I LOVE being in shape to get out there and just... go...
Cheers!
Somewhere along the way I must have messed up something, but I can't for the life of me figure out where. I have followed the tutorials, but when I try to run the service it is telling me that my value of the parse is null:
Return value of App\Service\MarkdownHelper::parse() must be of the type string, null returned
But, in the stacktrace I am seeing this:
MarkdownHelper->parse('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.')
in src/Controller/QuestionController.php (line 41)
Which sure looks like a string to me....
Here is my parse function
`
public function parse(string $source): string { return $this->cache->get('markdown_'.md5($source), function() use ($source) { return $this->markdownParser->transformMarkdown($source); }); }}
`
I should note that I think it has to do with the cache. When I tried to implement that before, it was pulling up blank before as well. If I remove the cache from the function, I do get it to work properly.
So I guess the question is why is the cache not working? I am on PHP 7.3.27.
Hey Tim C.
It may depend on you current cache service configuration, try to debug $this->cache
which adapter do you use?
Cheers!
Symfony\Component\Cache\Adapter\TraceableAdapter {#267 ▼
#pool: Symfony\Component\Cache\Adapter\FilesystemAdapter {#260 ▼
-createCacheItem: Closure($key, $value, $isHit) {#263 ▼
class: "Symfony\Component\Cache\CacheItem"
}
-mergeByLifetime: Closure($deferred, $namespace, &$expiredIds) {#265 ▼
class: "Symfony\Component\Cache\CacheItem"
use: {▼
$getId: Symfony\Component\Cache\Adapter\AbstractAdapter::getId($key) {#262 …}
$defaultLifetime: 0
}
}
-namespace: ""
-namespaceVersion: ""
-versioningIsEnabled: false
-deferred: []
-ids: []
#maxIdLength: null
#logger: Symfony\Bridge\Monolog\Logger {#264 ▼
#name: "cache"
#handlers: array:2 [▶]
#processors: array:1 [▶]
#microsecondTimestamps: true
#timezone: DateTimeZone {#266 ▶}
#exceptionHandler: null
}
-callbackWrapper: array:2 [▼
0 => "Symfony\Component\Cache\LockRegistry"
1 => "compute"
]
-computing: []
-marshaller: Symfony\Component\Cache\Marshaller\DefaultMarshaller {#261 ▼
-useIgbinarySerialize: false
}
-directory: "/var/www/aqua_note/var/cache/dev/pools/BnBSC5y2vY/"
-tmp: null
}
-calls: []
}
Hey Tim C.
Sorry for late reply, ok you are using filesystem adapter, so is it possible that /var/www/aqua_note/var/cache/dev/pools/BnBSC5y2vY/
is not writable? Have you tried to completely remove var/cache/dev/
?
Cheers!
Hey @disqus_1a9hby0y3a!
That's fair, though this isn't meant as an attack on Laravel, just a difference in philosophy :). In Symfony, we choose to "force" dependency injection. On the "bad" hand, that makes learning how to use Symfony harder, which is why we've added autowiring to alleviate that. In the good side, if you're getting started, it forces you into the very traditional design pattern of dependency injection. One advantage of this (that I personally really like) is that you can get a feel for "what a class does and does not do" by looking at its constructor. If I look at a class where the constructor does NOT have a MailerInterface, then I know that this service definitely does NOT send emails. If you're able to use a static method to fetch a service, then it's possible that there is a mailer call hiding way down on line 150 for your class.
So I absolutely think that you can write awesome code in any framework - definitely including Laravel! In Symfony, we've chosen to be a bit more strict. It doesn't mean that Laravel Facades == bad code. But it's harder for users to accidentally start writing worse code without the static helpers.
Anyways, thanks for the conversation - I think we'll ultimately disagree, but that's ok :). I like Laravel - we've borrowed many great ideas from it.
Cheers!
Hi colleagues, whenever I want to get a container in my constructor, I'm getting this:
The "Symfony\Component\DependencyInjection\ContainerInterface" autowiring alias is deprecated. Define it explicitly in your app if you want to keep using it.
The reasonable question, how to solve this then? What means "explicitly" in this case? Probably in future versions this way of injecting will be removed.
Hey WebAdequate,
Injecting the whole container is not a good practice anymore. If you think you still need to inject it - see this answer: https://stackoverflow.com/a... . But it's a good idea to refactor your code and inject only *direct* (more specific) dependencies you need into your services. If you need to inject a lot of services, sometimes it happens on practice - take a look at Service Subscribers: https://symfony.com/doc/mas...
I hope this helps!
Cheers!
Guys, I'm trying according with https://symfony.com/doc/current/service_container/synthetic_services.html to create a synthetic service: but i got an error:
<blockquote>The "synthetic_service" service is private, you cannot replace it.
</blockquote>
Any indeas?
I have on servises.yaml:
services:
synthetic_service:
synthetic: true```
And on kernel:
public function initializeContainer()
{
parent::initializeContainer(); // TODO: Change the autogenerated stub
$this->container->set('synthetic_service', new \Service\PasswordHasher());
}```
Thanks!
Hey triemli
do you have already defined the service synthetic_service
somewhere else? Can I see your config/services.yaml
file?
Hey triemli!
Synthetic services are pretty uncommon, but your solution makes sense to me. By definition, a synthetic service is one that you set at runtime, which means that you call $container->set(). It makes sense that it would need to be public (which allows it to be fetched via $container->get()) . Basically, if you make something private, Symfony does all kinds of build-time optimizations which simply don't make sense for a synthetic service.
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
}
}
Hey there
It looks like you're calling an unknown property
code
, you should make the call through your cache property$this->cache->get(...);
Cheers!