If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Phew! Dependency injection, check! Registering new services, check! Delicious snack, check! Well, I hope you just had a delicious snack.
This tutorial is the start to our victory lap. We need to add caching to MarkdownTransformer
:
it should be pretty easy. Copy part of the old caching code and paste that into
the parse()
function. Remove the else
part of the if
and just return $cache->fetch()
:
... lines 1 - 6 | |
class MarkdownTransformer | |
{ | |
... lines 9 - 15 | |
public function parse($str) | |
{ | |
$cache = $this->get('doctrine_cache.providers.my_markdown_cache'); | |
$key = md5($str); | |
if ($cache->contains($key)) { | |
return $cache->fetch($key); | |
} | |
... lines 23 - 29 | |
} | |
} |
Below, assign the method call to the $str
variable and go copy the old $cache->save()
line. Return $str
and re-add the sleep()
call so that things are really slow - that
keeps it interesting:
... lines 1 - 6 | |
class MarkdownTransformer | |
{ | |
... lines 9 - 15 | |
public function parse($str) | |
{ | |
... lines 18 - 23 | |
sleep(1); | |
$str = $this->markdownParser | |
->transformMarkdown($str); | |
$cache->save($key, $str); | |
return $str; | |
} | |
} |
On top, change the $funFact
variables to $str
. Perfect!
We know this won't work: there is no get()
function in this class. And more importantly,
we don't have access to the doctrine_cache.provider.my_markdown_cache
service.
How can we get access? Dependency injection.
This time, add a second argument to the constructor called $cache
. And hmm,
we should give this a type-hint. Copy the service name and run:
./bin/console debug:container doctrine_cache.providers.my_markdown_cache
This service is an instance of ArrayCache
. But wait! Do not type-hint that. In
our earlier course on environments, we setup a cool system that uses ArrayCache
in
the dev
environment and FilesystemCache
in prod
:
... lines 1 - 7 | |
parameters: | |
locale: en | |
cache_type: file_system | |
... lines 11 - 65 | |
doctrine_cache: | |
providers: | |
my_markdown_cache: | |
type: %cache_type% | |
file_system: | |
directory: %kernel.cache_dir%/markdown_cache |
If we type-hint with ArrayCache
, this will explode in prod
because this service
will be a different class.
Let's do some digging: open up ArrayCache
:
... lines 1 - 32 | |
class ArrayCache extends CacheProvider | |
{ | |
... lines 35 - 93 | |
} |
This extends CacheProvider
:
... lines 1 - 31 | |
abstract class CacheProvider implements Cache, FlushableCache, ClearableCache, MultiGetCache | |
{ | |
... lines 34 - 276 | |
} |
That might work. But it implements several interface - one of them is just
called Cache
. Let's try that. If this isn't the right interface - meaning it
doesn't contain the methods we're using - PhpStorm will keep highlighting
those after we add the type-hint:
... lines 1 - 7 | |
class MarkdownTransformer | |
{ | |
... lines 10 - 12 | |
public function __construct(MarkdownParserInterface $markdownParser, Cache $cache) | |
{ | |
... lines 15 - 16 | |
} | |
... lines 18 - 33 | |
} |
I'll use a keyboard shortcut - option
+enter
on a Mac - and select initialize fields:
... lines 1 - 7 | |
class MarkdownTransformer | |
{ | |
... line 10 | |
private $cache; | |
public function __construct(MarkdownParserInterface $markdownParser, Cache $cache) | |
{ | |
... line 15 | |
$this->cache = $cache; | |
} | |
... lines 18 - 33 | |
} |
All this did was add the private $cache
property and set it in __construct()
.
You can also do that by hand.
Cool! Update parse()
with $cache = $this->cache
:
... lines 1 - 7 | |
class MarkdownTransformer | |
{ | |
... lines 10 - 18 | |
public function parse($str) | |
{ | |
$cache = $this->cache; | |
... lines 22 - 32 | |
} | |
} |
And look! All of the warnings went away. That was the right interface to use. Yay!
Because we added a new constructor argument, we need to update any code that instantiates
the MarkdownTransformer
. But now, that's not done by us: it's done by Symfony,
and we help it in services.yml
. Under arguments, add a comma and quotes. Copy
the service name - @doctrine_cache.providers.my_markdown_cache
and paste it here:
... lines 1 - 5 | |
services: | |
app.markdown_transformer: | |
class: AppBundle\Service\MarkdownTransformer | |
arguments: ['@markdown.parser', '@doctrine_cache.providers.my_markdown_cache'] |
That's it! That's the dependency injection pattern.
Go back to refresh. The sleep()
should make it really slow. And it is slow.
Refresh again: still slow because we setup caching to really only work in the prod
environment.
Clear the prod
cache:
./bin/console cache:clear --env=prod
And now add /app.php/
in front of the URI to use this environment. This should be
slow the first time... but then fast after. Super fast! Caching is working. And
dependency injection is behind us.
Hi Roy!
Ah, this is SUCH a great error. It's really hard to spot the problem the first time (the error from PHP is not that clear). You DID remember to add the use statement for Cache in MarkdownTransformer (forgetting this is the MOST common mistake). But, you have the wrong class. There are multiple classes from different libraries called cache. You want:
use Doctrine\Common\Cache\Cache;
If you look closely, the error says: "Hey, I'm you this ArrayCache object when MarkdownTransformer is instantiated. But, the type-hint on the argument says that you're expecting this other Sensio\Bundle\FrameworkBundle\Configuration\Cache object. That's a problem!".
Cheers!
I get this same error and I am using
use Doctrine\Common\Cache\Cache;
CRITICAL - Uncaught PHP Exception
Symfony\Component\Debug\Exception\ContextErrorException: "Notice:
Undefined property: AppBundle\Service\MarkdownTransformer::$cache" at
/home/rob/aqua_note/src/AppBundle/Service/MarkdownTransformer.php line
22
Solved it now though.
Hey robspijkerman ,
Glad you solved it! Was it a user error? Or do we have this error in code we provide to you?
Cheers!
Hello, I am doing the Level up with Services and the Container Tutorial course and for now all good, the errors that I usually found were by writing or lack of a use in any class.
But I see that the functionality of strtoupper has been lost since it does not change lowercase to uppercase the fun: fact.
Is it correct or do I have some error to correct
regards
Hey Eduardo Guglielmotti!
Ah, that's great news that any errors have been minor. Missing the use
statement is probably the #1 thing that people forget. You'll make this mistake a few hundred times... then stop making it ;). As long as you can locate the problem, it's no issue.
About the strtoupper, it's ok! This should not be in your code anymore. We originally put this code in the MarkdownTransformer.parse()
method, just so we could see things working. But then, we removed it and instead started using the MarkdownParser internally. You can see that at the very beginning of this video: https://knpuniversity.com/screencast/symfony-services/the-dreaded-dependency-injection. So, you're ok! Your fun fact should not be upper cased, but it should be running through markdown transformation.
Cheers!
If I wanted to output a twig template from my service, how would I do so? Or is that bad practice?
I can't figure out how to inject and use what I inject. Do I inject "@templating" in my services.yml? Also how do I configure the constructor and then call the template in my method?
private <variable name>;
public function __construct(<TYPE HINT> <variable name>)
{
$this-><variable name> = <variable name>
}
public function myServiceMethod()
{
// how do I render the template?
// if I was in the controller I'd use:
return $this->render('myServiceOutput.html.twig');
}
Hey Terry,
Check my example how to inject templating service into your service:
#in services.yml
services:
my_service:
class: AppBundle\Service\MyService
arguments: ['@templating']
# and then in MyService.php
namespace AppBundle\Service;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
class MyService
{
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
public function someMethod()
{
$someVar = 'some-val';
return $this->templating->render('path/to/template.html.twig', [
'someVar' => $someVar,
]);
}
}
So as you see injecting a service is very simple. Also if you use PhpStorm with configured Symfony plugin or actually any other good IDE - you will have an autocompletion for methods, so will see all methods which the EngineInterface interface has.
But if you mean outputting something in services with echo/print PHP statements - it's a bad practice. You probably need a custom Twig function or filter, check <a href="http://symfony.com/doc/current/templating/twig_extension.html">How to Write a custom Twig Extension</a>. We also have a few screencasts about custom Twig extensions, check it out:
https://knpuniversity.com/screencast/symfony2-ep3/twig-extension-listener
https://knpuniversity.com/screencast/symfony-services/create-twig-extension
Cheers!
That's exactly what I needed. I was struggling to find the EngineInterface connection. Thanks much!
Figured it out... needed to use:
$this->renderResponse
instead of
$this->render
in my service.
I remembered one of the videos where Ryan dove into the controller and "render" was just a pass through method.
I'm having issues... As you have above the method someMethod in MyService returns the $this->templating->render(...) to my controller which is called like this:
/**
* @Route("/myapp")
*/
public function myappAction()
{
$MyService= $this->get('app.MyService');
return $MyService->someMethod();
}
I was expecting the returns to bubble through back to the controller with the response object. But when I run the app, I'm getting the error message "The controller must return a response (<html><body>...".
It looks like its returning the raw html?!
Here is a screen shot:
https://s16.postimg.org/4g34v3jb9/Screenshot_1.png
ps
FYI: I'd like to attache a screenshot of the error, but it appears as though the ability to add images is turned off.
Hi Terry,
Yes, you're right. This method returns a raw HTML, but Controller::render() method returns a Response object, check it in source code to understand how it works behind the scene: https://github.com/symfony/... . To get a raw HTML is useful for rendering a mail body which you will send to user, etc.
Cheers
Hi everyone,
I have a ServiceNotFoundException on this line in GenusController
>>> $this->get('app.markdown_transformer');
"Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException:
You have requested a non-existent service "app.markdown_transformer"
- I'm using phpstorm on windows, symfony 3.3.4
- I have checked services.yml, MarkdownTransformer.php and GenusController.php nothing seems wrong
- php bin/console debug:container markdown command shows app.markdown_transformer in console
- var\cache\dev\appDevDebugProjectContainer.php does not have getApp_MarkdownTransformerService function and app.markdown_transformer in methodMap object
- I have manually deleted var/cache, nothing is changed
Any help is appreciated, thanks.
Hey Mehmet Eren Soylu!
I think I know the problem: it's due to a big "philosophical" change that we made in Symfony 3.3, and we need to add some notes to our tutorials to help guide people through them. Basically, add public: true
to your service. Like this:
services:
app.markdown_transformer:
class: ...
arguments: [...]
public: true
While you're going through the tutorial, add that to any services that you create. Then, when you're ready, here is an explanation (actually written by me, so hopefully it makes some sense!) of how we've started configuring services slightly differently in Symfony 3.3: https://symfony.com/doc/current/service_container/3.3-di-changes.html
Cheers!
I have one strange error regarding the cache System.
The test:
1.) Visit site.com/sitedoesntexist (env = prod)
2.) Receive custom 404 page
3.) Change source code of custom 404 page
4.) Clear Cache via :./bin/console cache:clear --no-warmup --env=prod // ./bin/console cache:warmup --env=prod
5.) Revisit site, *nothing* changed (expected to see the change from 3.))
6.) Remove all the contents from /var/cache/* manually
7.) Revisit the site, now I can see the source code change
I don't understand why the twig template cache doesn't get deleted. I googled the command and it should be the right for SF 3.3.6 but I see the old cached pages. Only if I delete the cache direcotry manually I see the change. Do you know why this can happen // how can I find the reason out and how do I fix it?
Hey Mike P.!
That's kind of weird, "bin/console cache:clear --env=prod" should do the work, if you make more changes to it, can you see them after clearing cache, or you have to delete the cache folder manually?
How did you customize the 404 page ?
Cheers!
Ive tried it again, now even with admin right:
pwd -> in correct directory (phpstorm)
Visit 404 page and Homepage
change /app/Resources/TwigBundle/views/Exception/error404.html.twig & views/main/homepage.html.twig (for start page)
run:
sudo ./bin/console cache:clear --env=prod && sudo ./bin/console cache:clear --env=dev
Got: [OK] Cache for the "prod" environment (debug=false) was successfully cleared.
[OK] Cache for the "dev" environment (debug=true) was successfully cleared.
But nothing changed on the site. After manually deleting the cache it works. Very strange.
Wow, this is a mystery! The bin/console cache:clear --env=prod
command actually creates a fresh, new var/cache/prod
directory, then deletes the old one, and moves this new one into its place (it's temporarily called pro_
if you look quickly). In other words, the cache directory is completely replaced when you run the command.
If you're curious, you could look for the cached Twig file and see what's inside. The Twig files cache down to PHP files, in var/cache/prod/twig
There are a bunch of different directories and files, so you would need to grep in there for some content that appears on your 404 page. What would be interesting is to see - after you run cache:clear
- is the correct, updated code inside the cached Twig template PHP file? Or does the cached Twig template PHP file still show the old code?
This is a long way of saying... this is weird.! It certainly should not work like this, and I can't think why it is!
Cheers!
Nope, totally didn't had a delicious snake :( I'll go to the supermarket to buy some chips and then get back to the course. Thanks for the idea ! :P
I just ran into an issue when clearing the production cache:
Fatal error: Class 'Symfony\Component\HttpKernel\Debug\FileLinkFormatter' not found in /Users/sanderdewijs/Documents/Projects/Symfony3/var/cache/prod/appProdProjectContainer.php on line 3207
I did a composer install (removed composer.lock first) and a composer update but the problem persists.
When learing the dev cache there are no errors. Any ideas what is causing this?
<edit> Perhaps related, if I go to the prod environment I get another error on the single Genus page:
Fatal error: Class 'Symfony\Bundle\SecurityBundle\SecurityUserValueResolver' not found in [...]appProdProjectContainer.php on line 1501
Yo sdewijs!
Wooooh. Indeed, something is NOT right here (good idea to re-run composer install and also try a fresh composer update). I *do* think we have some problem, somehow, with either our dependencies, or something else weird. What's interesting is that those 2 classes are new in Symfony 3.2. So, it's almost like your container was cached when those files DID exist... but then your vendor directory got re-set to an old version. Some things to try:
A) Put your original composer.lock file back (if you still have it - otherwise download the code from this page and use ours)
B) Run an rm -rf vendor/ to completely reset things
C) Try a composer install
... if the problem persist
D) Run an rm -rf var/cache/* and then try re-building the cache
If (D) works, then - at least from a very high level - the issue is that your cached container got compiled using Symfony 3.2 (or later) code (somehow), but then your vendor directory (somehow) was changed to only include Symfony 3.1. Rebuilding the cache *uses* the existing cache. And in this situation, your existing cache would kind of be in a "poisoned state".
Phew! Ok, let me know what (if anything) works!
Cheers!
Thanks! I needed to perform all the steps from A to D to fix the problem. Not sure about the source of the issue though. Was coding along form lesson 1 and I haven't looked in production until this step so It could have existed for a while. Thanks again for the help and creating this awesome course!
Unfortunately, I'm getting myself into a real mess with this particular part of the series. I keep getting error messages and when I correct one, I get another..... anyway, today's latest error reads:
"(1/1) AutowiringFailedException
Cannot autowire service "AppBundle\Service\MarkdownTransformer": argument "$cache" of method "__construct()" references interface "Doctrine\Common\Cache\Cache" but no such service exists. You should maybe alias this interface to one of these existing services: "2_ef8536ca925f81f3571c61ede2633674a188e643de54e41c1dc937224c39e54d", "2_1e0b21c4662523189de20fb2b1592823910640a5c2c8e5a769894fee7e2dd97b", "annotations.filesystem_cache", "annotations.cache", "doctrine_cache.providers.doctrine.orm.default_metadata_cache", "doctrine_cache.providers.doctrine.orm.default_result_cache", "doctrine_cache.providers.doctrine.orm.default_query_cache", "doctrine_cache.providers.my_markdown_cache"."
Any thoughts?
Debug is ongoing here.... using PhP Storms search function, I searched for any instance of the text "my_markdown_cache" in my project. Only one.... in services.yml.... the very line we just typed.
This is interesting as this means that I have probably inadvertently damaged/deleted wherever it was that I built my_markdown_cache.
Now what tutorial step did we do that???? Time to go hunting....
ok... so I'm getting there... I finally spotted your notes about Symfony 3.3 and stripped the new lines from services.yml
Now, things seem to be working again.
Fingers crossed for the next parts.
Yo Rich Wilx!
Good debugging :). We're in a weird spot right now while we transition from the old, more manual way of defining services to the newer, autowiring way. In this tutorial, we use a version of Symfony that does not come with any of the new autowiring features - so you were correct to disable those (if you want to follow along with the tutorial exactly). Later this year, we'll start releasing tutorials that use the new autowiring way of doing things (once Symfony 4 comes out, in Nov). We have a tutorial about this new stuff (https://knpuniversity.com/s..., but mostly, it's not a huge change... it mostly makes life easier. So, keep on as you are, then watch for tutorials with the new stuff once Symfony 4 is here!
Cheers!
Hey Ryan....
Gotta say, I'm very fragile right now.... I'm just beginning to get the hang of Symfony 3.3 and now you speak of a major version coming out in just a few weeks.
How bad is it going to be for me if I decide not to jump up to the new shiny goodness when it arrives?
-Rich
Hey Rich!
Haha, oh no! It's really not going to be bad. In fact, this is one of the best reasons to use Symfony: we don't break backwards compatibility and we practice something called the "continuous upgrade path": a system where you can migrate your code across major versions without breaking things. We'll have a tutorial out in about 2 months about upgrading to Symfony 4.
In fact, usually, when big things happen in Symfony, it's not a change to the core code, but new recommendations we make about how you should write your code. That's more or less what's happening with Symfony 4. So, let me assure you, it's going to be ok! And here's my recommendation: continue with these tutorials, completing following them in the Symfony 3 way of doing things (so, commenting-out all the autowiring stuff as you did). When Symfony 4 comes out, we will have a tutorial about learning the changes. But, it's not as much as you think, and there are basically 2 big changes
A) The directory structure of a project will change. No big deal, you will have basically the same files, but in new locations.
B) We will use autowiring everywhere. The Symfony 3 way of doing things is to wire all of your services in services.yml manually. In Symfony 4, much of this will be automated, but everything under the hood is still the same (in fact, the manual wiring will still work, if you prefer that way). So, the change from Symfony 3 to Symfony 4 will be to do less work, and let the framework take care of more tasks for you. You're actually in great shape, because you're learning it the "hard" way. In Symfony 4, you'll need to worry about less. I think it's a bit like driving a car: you're learning to drive a manual transmission now. In Symfony 4, it's an automatic transmission. It will (hopefully) be so much simpler, you'll get bored ;).
So, keep going! And we'll help you through the Symfony 4 changes when it's time. You're doing great! I know because of how well you debugged this issue.
Cheers!
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
I get the following error at the end: Catchable Fatal Error: Argument 2 passed to AppBundle\Service\MarkdownTransformer::__construct() must be an instance of Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache, instance of Doctrine\Common\Cache\ArrayCache given.
MarkdownTransformer.php:
namespace AppBundle\Service;
use Knp\Bundle\MarkdownBundle\MarkdownParserInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
class MarkdownTransformer
{
private $markdownParser;
private $cache;
public function __construct(MarkdownParserInterface $markdownParser, Cache $cache)
{
$this->markdownParser = $markdownParser;
$this->cache = $cache;
}
public function parse($str)
{
$cache = $this->cache;
$key = md5($str);
if ($cache->contains($key)) {
return $cache->fetch($key);
}
sleep(1);
$str = $this->markdownParser
->transformMarkdown($str);
$cache->save($key, $str);
return $str;
}
}
services.yml:
# Learn more about services, parameters and containers at
# http://symfony.com/doc/curr...
parameters:
# parameter_name: value
services:
app.markdown_transformer:
class: AppBundle\Service\MarkdownTransformer
arguments: ['@markdown.parser','@doctrine_cache.providers.my_markdown_cache']
When I run /bin/console debug:container doctrine_cache.providers.my_markdown_cache It shows the following line:
Class Doctrine\Common\Cache\ArrayCache