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 SubscribeWe know that bundles give us services and services do work. Ok. But what if we need to write our own custom code that does work? Should we... put that into our own service class? Absolutely! And it's a great way to organize your code.
We are already doing some work in our app. In the browse()
action:
... lines 1 - 12 | |
class VinylController extends AbstractController | |
{ | |
... lines 15 - 32 | |
'/browse/{slug}', name: 'app_browse') ( | |
public function browse(HttpClientInterface $httpClient, CacheInterface $cache, string $slug = null): Response | |
{ | |
$genre = $slug ? u(str_replace('-', ' ', $slug))->title(true) : null; | |
$mixes = $cache->get('mixes_data', function(CacheItemInterface $cacheItem) use ($httpClient) { | |
$cacheItem->expiresAfter(5); | |
$response = $httpClient->request('GET', 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json'); | |
return $response->toArray(); | |
}); | |
return $this->render('vinyl/browse.html.twig', [ | |
'genre' => $genre, | |
'mixes' => $mixes, | |
]); | |
} | |
} |
we make an HTTP request and cache the result. Putting this logic in our controller is fine. But by moving it into its own service class, it'll make the purpose of the code more clear, allow us to reuse it from multiple places... and even enable us to unit test that code if we want to.
That sounds amazing, so let's do it! How do we create a service? In the src/
directory, create a new PHP class wherever you want. It seriously doesn't matter what directories or subdirectories you create in src/
: do whatever feels good for you.
For this example, I'll create a Service/
directory - though again, you could call that PizzaParty
or Repository
- and inside of that, a new PHP class. Let's call it... how about MixRepository
. "Repository" is a pretty common name for a service that returns data. Notice that when I create this, PhpStorm automatically adds the correct namespace. It doesn't matter how we organize our classes inside of src/
... as long as our namespace matches the directory:
namespace App\Service; | |
... lines 4 - 5 | |
class MixRepository | |
{ | |
... lines 9 - 17 | |
} |
One important thing about service classes: they have nothing to do with Symfony. Our controller class is a Symfony concept. But MixRepository
is a class we're creating to organize our own code. That means... there are no rules! We don't need to extend a base class or implement an interface. We can make this class look and feel however we want. The power!
With that in mind, let's create a new public function
called, how about, findAll()
that will return
an array
of all of the mixes in our system. Back in VinylController
, copy all of the logic that fetches the mixes and paste that here:
... lines 1 - 4 | |
use Psr\Cache\CacheItemInterface; | |
class MixRepository | |
{ | |
public function findAll(): array | |
{ | |
$mixes = $cache->get('mixes_data', function(CacheItemInterface $cacheItem) use ($httpClient) { | |
$cacheItem->expiresAfter(5); | |
$response = $httpClient->request('GET', 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json'); | |
return $response->toArray(); | |
}); | |
} | |
} |
PhpStorm will ask if we want to add a use
statement for the CacheItemInterface
. We totally do! Then, instead of creating a $mixes
variable, just return
:
... lines 1 - 4 | |
use Psr\Cache\CacheItemInterface; | |
class MixRepository | |
{ | |
public function findAll(): array | |
{ | |
return $cache->get('mixes_data', function(CacheItemInterface $cacheItem) use ($httpClient) { | |
$cacheItem->expiresAfter(5); | |
$response = $httpClient->request('GET', 'https://raw.githubusercontent.com/SymfonyCasts/vinyl-mixes/main/mixes.json'); | |
return $response->toArray(); | |
}); | |
} | |
} |
There are some undefined variables in this class... and those will be a problem. But ignore them for a minute: I first want to see if we can use our shiny new MixRepository
.
Head into VinylController
. Let's think: we somehow need to tell Symfony's service container about our new service so that we can then autowire it in the same way we're autowiring core services like HtttpClientInterface
and CacheInterface
.
Whelp, I have a surprise! Spin over to your terminal and run:
php bin/console debug:autowiring --all
Scroll up to the top and... amaze! MixRepository
is already a service in the container! Let me explain two things here. First, the --all
flag is not that important. It basically tells this command to show you the core services like $httpClient
and $cache
, plus our own services like MixRepository
.
Second, the container... somehow already saw our repository class and recognized it as a service. We'll learn how that happened in a few minutes... but for now, it's enough to know that our new MixRepository
is already inside the container and its service id is the full class name. That means we can autowire it!
Back over in our controller, add a third argument type-hinted with MixRepository
- hit tab to add the use
statement - and call it... how about $mixRepository
:
... lines 1 - 4 | |
use App\Service\MixRepository; | |
... lines 6 - 12 | |
class VinylController extends AbstractController | |
{ | |
... lines 15 - 33 | |
public function browse(HttpClientInterface $httpClient, CacheInterface $cache, MixRepository $mixRepository, string $slug = null): Response | |
{ | |
... lines 36 - 43 | |
} | |
} |
Then, down here, we don't need any of this $mixes
code anymore. Replace it with $mixes = $mixRepository->findAll()
:
... lines 1 - 4 | |
use App\Service\MixRepository; | |
... lines 6 - 12 | |
class VinylController extends AbstractController | |
{ | |
... lines 15 - 33 | |
public function browse(HttpClientInterface $httpClient, CacheInterface $cache, MixRepository $mixRepository, string $slug = null): Response | |
{ | |
... lines 36 - 37 | |
$mixes = $mixRepository->findAll(); | |
... lines 39 - 43 | |
} | |
} |
How nice is that? Will it work? Let's find out! Refresh and... it does! Ok, working in this case means that we get an Undefined variable $cache
message coming from MixRepository
. But the fact that our code got here means that autowiring MixRepository
worked: the container saw this, instantiated MixRepository
and passed it to us so that we could use it.
So, we created a service and made it available for autowiring! We are so cool! But our new service needs the $httpClient
and $cache
services in order to do its job. How do we get those? The answer is one of the most important concepts in Symfony and object-oriented coding in general: dependency injection. Let's talk about that next.
"Houston: no signs of life"
Start the conversation!
// 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
}
}