gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
We don't have a database yet... and we'll save that for a future tutorial. But to make things a bit more fun, I've created a GitHub repository - https://github.com/SymfonyCasts/vinyl-mixes - with a mixes.json
file that holds a fake database of vinyl mixes. Let's make an HTTP request from our Symfony app to this file and use that as our temporary database.
So... how can we make HTTP requests in Symfony? Well, making an HTTP request is work, and - say it with me now - "Work is done by a service". So the next question is: Is there already a service in our app that can make HTTP requests?
Let's find out! Spin over to your terminal and run:
php bin/console debug:autowiring http
to search the services for "http". We do get a bunch of results, but... nothing that looks like an HTTP client. And, that's correct. There is not currently any service in our app that can make HTTP requests.
But, we can install another package to give us one. At your terminal, type:
composer require symfony/http-client
But, before we run that, I want to show you where this command comes from. Search for "symfony http client". One of the top results is Symfony.com's documentation that teaches about an HTTP Client component. Remember: Symfony is a collection of many different libraries, called components. And this one helps us make HTTP requests!
Near the top, you'll see a section called "Installation", and there's the line from our terminal!
Anyways, if we run that... cool! Once it finishes, try that debug:autowiring
command again:
php bin/console debug:autowiring http
And... here it is! Right at the bottom: HttpClientInterface
, which
Provides flexible methods for requesting HTTP resources synchronously or asynchronously.
Woo! We just got a new service! That means that we must have just installed a new bundle, right? Because... bundles give us services? Well... go check out config/bundles.php
:
return [ | |
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], | |
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], | |
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], | |
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], | |
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], | |
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], | |
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], | |
Symfony\UX\Turbo\TurboBundle::class => ['all' => true], | |
Knp\Bundle\TimeBundle\KnpTimeBundle::class => ['all' => true], | |
]; |
Woh! There's no new bundle here! Try running
git status
Yea... that only installed a normal PHP package. Inside composer.json
, here's the new package... But it's just a "library": not a bundle.
{ | |
... lines 2 - 5 | |
"require": { | |
... lines 7 - 15 | |
"symfony/http-client": "6.1.*", | |
... lines 17 - 24 | |
}, | |
... lines 26 - 84 | |
} |
So, normally, if you install "just" a PHP library, it gives you PHP classes, but it doesn't hook into Symfony to give you new services. What we just saw is a special trick that many of the Symfony components use. The main bundle in our app is framework-bundle
. In fact, when we started our project, this was the only bundle we had. framework-bundle
is smart. When you install a new Symfony component - like the HTTP Client component - that bundle notices the new library and automatically adds the services for it.
So the new service comes from framework-bundle
... which adds that as soon as it detects that the http-client
package is installed.
Anyways, time to use the new service. The type we need is HttpClientInterface
. Head over to VinylController.php
and, up here in the browse()
action, autowire HttpClientInterface
and let's name it $httpClient
:
... lines 1 - 7 | |
use Symfony\Contracts\HttpClient\HttpClientInterface; | |
... lines 9 - 10 | |
class VinylController extends AbstractController | |
{ | |
... lines 13 - 31 | |
public function browse(HttpClientInterface $httpClient, string $slug = null): Response | |
{ | |
... lines 34 - 41 | |
} | |
... lines 43 - 67 | |
} |
Then, instead of calling $this->getMixes()
, say $response = $httpClient->
. This lists all of its methods... we probably want request()
. Pass this GET
... and then I'll paste the URL: you can copy this from the code block on this page. It's a direct link to the content of the mixes.json
file:
... lines 1 - 10 | |
class VinylController extends AbstractController | |
{ | |
... lines 13 - 31 | |
public function browse(HttpClientInterface $httpClient, string $slug = null): Response | |
{ | |
... line 34 | |
$response = $httpClient->request('GET', 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json'); | |
... lines 36 - 41 | |
} | |
... lines 43 - 67 | |
} |
Cool! So we make the request and it returns a response containing the mixes.json
data that we see here. Fortunately, this data has all of the same keys as the old data we were using down here... so we should be able to swap it in super easily. To get the mix data from the response, we can say $mixes = $response->toArray()
:
... lines 1 - 10 | |
class VinylController extends AbstractController | |
{ | |
... lines 13 - 31 | |
public function browse(HttpClientInterface $httpClient, string $slug = null): Response | |
{ | |
... line 34 | |
$response = $httpClient->request('GET', 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json'); | |
$mixes = $response->toArray(); | |
... lines 37 - 41 | |
} | |
... lines 43 - 67 | |
} |
a handy method that JSON decodes the data for us!
Moment of truth! Move over, refresh and... it works! We now have six mixes on the page. And... super cool! A new icon showed up on the web debug toolbar: "Total requests: 1". The HTTP Client service hooks into the web debug toolbar to add this, which is pretty awesome. If we click it, we can see info about the request and the response. I love that.
To celebrate this working, spin back over and remove the hardcoded getMixes()
method:
... lines 1 - 10 | |
class VinylController extends AbstractController | |
{ | |
... line 13 | |
public function homepage(): Response | |
{ | |
... lines 16 - 28 | |
} | |
... lines 30 - 31 | |
public function browse(HttpClientInterface $httpClient, string $slug = null): Response | |
{ | |
... lines 34 - 41 | |
} | |
} |
The only problem I can think of now is that, every single time someone visits our page, we're making an HTTP request to GitHub's API... and HTTP requests are slow! To make matters worse, once our site becomes super popular - which won't take long - GitHub's API will probably start rate limiting us.
To fix this, let's leverage another Symfony service: the cache service.
Hey Marc!
Hmm, that's weird - I can't think of a reason! As long as you have the http_client
enabled (which should happen automatically when you install that package) and the profiler (which you DO have if you are seeing the web debug toolbar), then it should be there. The logic that loads it is here - https://github.com/symfony/symfony/blob/0383b067e947efb3570b2fda8259e582ef3d8f2d/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L846-L848 - but that's super deep and complex, so not sure that's helpful. But it basically says what I just said: as long as profiler and http_client are enabled, that web debug toolbar should be there.
Oh, actually, there is one other possibility: there were no requests issued during the request by the HTTP client. If that happens, the web debug toolbar icon won't be shown - https://github.com/symfony/symfony/blob/0383b067e947efb3570b2fda8259e582ef3d8f2d/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig#L40 - but if you clicked any of the other icons to get into the profiler, I think you would still see an "HTTP Client" section.
Let me know if this helps!
Cheers!
Hi Ryan,
Thanks for your answer.
The fact is I tweaked http_client.html.twig to display {{ collector.requestCount }} value. It stays at 0 though dumping the result of
$response = $httpClient->request(
'GET',
'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json?'
);
via dump($response) outputs :
Symfony\Component\HttpClient\Response\TraceableResponse {#2284 ▼
-client: Symfony\Component\HttpClient\CurlHttpClient {#6639 ▶}
-response: Symfony\Component\HttpClient\Response\CurlResponse {#6264 ▶}
-content: & null
-event: Symfony\Component\Stopwatch\StopwatchEvent {#6169 ▶}
}
By the way, dumping $response->toArray() outputs the desired array.
Would you have an idea how we can get the valid response content with no request done ?
Would there be an internal cache ?
In composer.json, http-client is version 6.2.6
Kind regards,
Marc.
Hey Marc!
That's some good debugging! Hmm. A few things to check out... though I don't think these are the cause (but I need to check anyways):
A) You are autowiring the HttpClient service the same way we do in the tutorial?
B) Are you actually using the result of the response? I'm asking because the HttpClient is "async". So if you simply made a request, but never used its response, then it's possible that the response hasn't actually finished by the time the profiler collects the data. However, if you are even calling $response->toArray()
, then that makes the code "wait" for the request to finish... and so that would be enough to avoid this.
So then, what could the cause be? I'm not sure - it definitely looks weird :/
I'm interested also if:
1) Is this method being called https://github.com/symfony/symfony/blob/0383b067e947efb3570b2fda8259e582ef3d8f2d/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php#L44 ?
2) And if so, what does dd($this->clients)
show? It should show at least one HttpClient
3) If your controller, if you dump($httpClient)
, what does it look like? It should be a TraceableHttpClient
. I'm pretty sure it IS because you're getting back a TraceableResponse
.
Let me know what you find out :).
Cheers!
Thanks Ryan,
for questions A and B, I can say answer is Yes. Here's the code :
#[Route('/browse/{slug}', name: 'app_browse')]
public function browse(HttpClientInterface $httpClient, string $slug = null): Response
{
$genre = $slug ? u(str_replace('-', ' ', $slug))->title(true) : null;
$response = $httpClient->request(
'GET',
'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json',
);
dump($response->toArray());
return $this->render('vinyl/browse.html.twig',
[
'genre' => $genre,
'mixes' => $response->toArray()
]);
}
For points
I'm really sorry to bother you with this.
Thanks!
Huh... so everything, so far, seems to be working 100% correctly!
So. you have confirmed that lateCollect()
is called (2x in fact... that may be normal - I'm not sure, I'm not familiar enough with that system) - https://github.com/symfony/symfony/blob/0383b067e947efb3570b2fda8259e582ef3d8f2d/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php#L44
And we also know that $this->client
contains 1 item. So next I would be interested in what $traces
looks like - https://github.com/symfony/symfony/blob/0383b067e947efb3570b2fda8259e582ef3d8f2d/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php#L51
If there is at least 1 item in $traces
, then $this->data['request_count']
would be more than 0. And THEN we should see the icon - thanks to https://github.com/symfony/symfony/blob/0383b067e947efb3570b2fda8259e582ef3d8f2d/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig#L40
Actually, in https://github.com/symfony/symfony/blob/0383b067e947efb3570b2fda8259e582ef3d8f2d/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig#L40 - you might also try a dump(collector)
(outside of any if statements) to make sure this is being called AND what collector
looks like.
So, there is a still a mystery here... but we're circling closer and closer to it :).
Cheers!
Hi Ryan,
Thanks for your answer and sorry for the late reply.
Dumping $trace doesn't provide any info, as it gives an empty array.
The most interesting part comes from dumping collector in http_client.html.twig :
Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector {#425 ▼
#data: array:3 [▼
"request_count" => 0
"error_count" => 0
"clients" => array:2 [▼
"http_client" => array:2 [▼
"traces" => []
"error_count" => 0
]
"githubContentClient" => array:2 [▼
"traces" => array:1 [▼
0 => array:7 [▼
"method" => "GET"
"url" => "/SymfonyCasts/vinyl-mixes/main/mixes.json"
"options" => Symfony\Component\VarDumper\Cloner\Data {#433 ▶}
"content" => array:6 [▶]
"http_code" => 200
"info" => Symfony\Component\VarDumper\Cloner\Data {#437 ▶}
"curlCommand" => """
curl \
--compressed \
--request GET \
--url 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json' \
--header 'authorization: Token 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' \
--header 'accept: */*' \
--header 'user-agent: Symfony HttpClient/Curl' \
--header 'accept-encoding: gzip'
"""
]
]
"error_count" => 0
]
]
]
-clients: []
}
No clients in clients[], but clients in the #data property.
Would you have an idea ?
If it looks too complicated, we can forget about my question.
Thanks anyway Ryan !
Dang! I am at a loss! How could that clients
property be an empty array... and yet $data
got populated! Haha, indeed - without being able to play with the code first-hand, I think this will remain a mystery (though, you were an excellent debugging partner). It's possible there is some problem introduced in newer versions of Symfony... or just something bizarre with your setup for some reason. If you DO every happen to find out (maybe it goes away randomly), let me know.
Cheers!
Thanks again Ryan !
I'll let you know in case I solve this stuff...
By the way, congrats to your team : the tutorials are excellent as well as your answers !
Regards,
Marc.
Ah, I can repeat it too! This was a bug introduced unintentionally during a CVE security release. The fix has already been merged - https://github.com/symfony/symfony/pull/49301 - it looks like that should land in 6.2.7 when it's released.
Thanks Yard for speaking up - as soon as 2 users had the problem, I knew something must have been wrong!
Hi Ryan,
Thanks for these wonderful tutorials.
If Laravel is built on top of many Symfony's bundles and even both have their own cache systems I wonder how is possible that some benchmark on the internet gave a better performance to Laravel? Exactly the conclusions of many of them are: "The average loading time for websites on Laravel is about 60 milliseconds, while Symfony's load time is about 250 milliseconds"
Thank you, kind regards.
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"knplabs/knp-time-bundle": "^1.18", // v1.19.0
"symfony/asset": "6.1.*", // v6.1.0-RC1
"symfony/console": "6.1.*", // v6.1.0-RC1
"symfony/dotenv": "6.1.*", // v6.1.0-RC1
"symfony/flex": "^2", // v2.1.8
"symfony/framework-bundle": "6.1.*", // v6.1.0-RC1
"symfony/http-client": "6.1.*", // v6.1.0-RC1
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/runtime": "6.1.*", // v6.1.0-RC1
"symfony/twig-bundle": "6.1.*", // v6.1.0-RC1
"symfony/ux-turbo": "^2.0", // v2.1.1
"symfony/webpack-encore-bundle": "^1.13", // v1.14.1
"symfony/yaml": "6.1.*", // v6.1.0-RC1
"twig/extra-bundle": "^2.12|^3.0", // v3.4.0
"twig/twig": "^2.12|^3.0" // v3.4.0
},
"require-dev": {
"symfony/debug-bundle": "6.1.*", // v6.1.0-RC1
"symfony/maker-bundle": "^1.41", // v1.42.0
"symfony/stopwatch": "6.1.*", // v6.1.0-RC1
"symfony/web-profiler-bundle": "6.1.*" // v6.1.0-RC1
}
}
Hi Ryan !
Thanks for these great tutorials.
Would there be a reason for Http Client Icon not showing in the debug toolbar ?
My current Sf version is 6.2.6
Thanks,
Marc.