Creating your own Service
We know that services do work, and we know that Symfony is full of services that we can use. If you run:
php bin/console debug:autowiring
We get the dinner menu of services, where you can order any of these by adding an argument type-hinted with the matching class or interface.
We, of course, also do work in our code... hopefully. Right now, all that work is being done inside our controller, like creating the Starship data. Sure, this is hard-coded right now, but imagine if this were real work: like a complex database query. Putting the logic inside a controller is "ok"... but what if we wanted to reuse this code somewhere else? What if, on our homepage, we wanted to get a dynamic count of the Starships by grabbing this same data?
Creating the Service Class
To do that, we need to move this "work" into its own service that both controllers could then use. In the src/ directory, create a new Repository directory and a new PHP class inside called StarshipRepository.
| // ... lines 1 - 2 | |
| namespace App\Repository; | |
| class StarshipRepository | |
| { | |
| } |
Just like when we built our Starship class, this new class has absolutely nothing to do with Symfony. It's just a class that we've decided to create to organize our work. And so, Symfony doesn't care what it's called, where it lives or what it looks like. I called it StarshipRepository and put it in a Repository directory because that's a common programming name for a class whose "work" is to fetch a type of data, like Starship data.
Autowiring the New Service
Ok, before we even do anything in here, let's see if we can use this inside a controller. And, good news! Just by creating this class, it's already available for autowiring. Add a StarshipRepository $repository argument, and, to make sure it's working, dd($repository).
| // ... lines 1 - 5 | |
| use App\Repository\StarshipRepository; | |
| // ... lines 7 - 11 | |
| class StarshipApiController extends AbstractController | |
| { | |
| // ... line 14 | |
| public function getCollection(LoggerInterface $logger, StarshipRepository $repository): Response | |
| { | |
| $logger->info('Starship collection retrieved'); | |
| dd($repository); | |
| // ... lines 19 - 43 | |
| } | |
| } |
All right, spin over, click back to our endpoint, and... got it. That's so cool! Symfony saw the StarshipRepository type-hint, instantiated that object, then passed it to us. Delete the dd()... and let's move the starship data inside. Copy it... and create a new public function called, how about, findAll(). Inside, return, then paste.
| // ... lines 1 - 4 | |
| use App\Model\Starship; | |
| class StarshipRepository | |
| { | |
| public function findAll(): array | |
| { | |
| return [ | |
| new Starship( | |
| 1, | |
| 'USS LeafyCruiser (NCC-0001)', | |
| 'Garden', | |
| 'Jean-Luc Pickles', | |
| 'taken over by Q' | |
| ), | |
| new Starship( | |
| 2, | |
| 'USS Espresso (NCC-1234-C)', | |
| 'Latte', | |
| 'James T. Quick!', | |
| 'repaired', | |
| ), | |
| new Starship( | |
| 3, | |
| 'USS Wanderlust (NCC-2024-W)', | |
| 'Delta Tourist', | |
| 'Kathryn Journeyway', | |
| 'under construction', | |
| ), | |
| ]; | |
| } | |
| } |
Back over in StarshipApiController, delete that... and it's beautifully simple: $starships = $repository->findAll().
| // ... lines 1 - 4 | |
| use App\Repository\StarshipRepository; | |
| // ... lines 6 - 10 | |
| class StarshipApiController extends AbstractController | |
| { | |
| ('/api/starships') | |
| public function getCollection(LoggerInterface $logger, StarshipRepository $repository): Response | |
| { | |
| $logger->info('Starship collection retrieved'); | |
| $starships = $repository->findAll(); | |
| // ... lines 18 - 19 | |
| } | |
| } |
Done! When we try it, it still works... and now the code for fetching starships is nicely organized into its own class and reusable across our app.
Constructor Autowiring
With that victory under our belt, let's doing something harder. What if, from inside StarshipRepository, we needed access to another service to help us do our work? No problem! We can use autowiring! Let's try to autowire the logger service again.
The only difference this time is that we're not going to add the argument to findAll(). I'll explain why in a minute. Instead, add a new public function __construct() and do the auto-wiring there: private LoggerInterface $logger.
| // ... lines 1 - 5 | |
| use Psr\Log\LoggerInterface; | |
| class StarshipRepository | |
| { | |
| public function __construct(private LoggerInterface $logger) | |
| { | |
| } | |
| // ... lines 13 - 41 | |
| } |
Down below, to use it, copy the code from our controller, delete that, paste it here, and update it to $this->logger.
| // ... lines 1 - 5 | |
| use Psr\Log\LoggerInterface; | |
| class StarshipRepository | |
| { | |
| public function __construct(private LoggerInterface $logger) | |
| { | |
| } | |
| public function findAll(): array | |
| { | |
| $this->logger->info('Starship collection retrieved'); | |
| // ... lines 17 - 40 | |
| } | |
| } |
Cool! Over in the controller, we can remove that argument because we're not using it anymore.
Testing time! Refresh! No error - that's a good sign. To see if it logged something, go to /_profiler, click on the top request, Logs, and... there it is!
So let me explain why we added the service argument to the constructor. If we want to fetch a service - like the logger, a database connection, whatever, this is the correct way to use autowiring: add a __construct method inside another service. The trick we saw earlier - where we add the argument to a normal method - yeah, that's special and only works for controller methods. It's an extra convenience that was added to the system. It's a great feature, but the constructor way... that's how autowiring really works.
And this "normal" way, it even works in a controller. You could add a __construct() method with an autowirable argument and that would totally work.
The point is: if you are in a controller method, sure, add the argument to the method - it's nice! Just remember that it's a special thing that only works here. Everywhere else, autowire through the constructor.
Using the Service on another Page
Let's celebrate our new service by using it on the homepage. Open up MainController. This hardcoded $starshipCount is so 30 minutes ago. Autowire StarshipRepository $starshipRepository, then say $ships = $starshipRepository->findAll() and count them with count().
| // ... lines 1 - 4 | |
| use App\Repository\StarshipRepository; | |
| // ... lines 6 - 9 | |
| class MainController extends AbstractController | |
| { | |
| ('/') | |
| public function homepage(StarshipRepository $starshipRepository): Response | |
| { | |
| $ships = $starshipRepository->findAll(); | |
| $starshipCount = count($ships); | |
| // ... lines 17 - 22 | |
| } | |
| } |
While we're here, instead of this hardcoded $myShip array, let's grab a random Starship object. We can do that by saying $myShip equals $ships[array_rand($ships)]
| // ... lines 1 - 4 | |
| use App\Repository\StarshipRepository; | |
| // ... lines 6 - 9 | |
| class MainController extends AbstractController | |
| { | |
| ('/') | |
| public function homepage(StarshipRepository $starshipRepository): Response | |
| { | |
| $ships = $starshipRepository->findAll(); | |
| $starshipCount = count($ships); | |
| $myShip = $ships[array_rand($ships)]; | |
| // ... lines 18 - 22 | |
| } | |
| } |
Let's try it! Hunt down your browser and head to the homepage. Got it! We see the randomly changing ship down here, and the correct ship number up here... because we're multiplying it by 10 in the template.
Printing Objects in Twig
And something crazy-cool just happened! A minute ago, myShip was an associative array. But we changed it to be a Starship object. And yet, the code on our page kept working. We just accidentally saw a superpower of Twig. Head to templates/main/homepage.html.twig and scroll down to the bottom. When you say myShip.name, Twig is really smart. If myShip is an associative array, it'll grab the name key. If myShip is an object, like it is now, it will grab the name property. But even more than that, if you look at Starship, the name property is private, so we can't access it directly. Twig realizes that. It looks at the name property, sees that it's private, but also sees that there's a public getName(). And so, it calls that.
All we need to say is myShip.name... and Twig handles the details of how to fetch that, which I love.
Ok, one last tiny tweak. Instead of passing the starshipCount into our template, we can do the count inside Twig. Delete this variable, and instead, pass a ships variable.
| // ... lines 1 - 9 | |
| class MainController extends AbstractController | |
| { | |
| // ... line 12 | |
| public function homepage(StarshipRepository $starshipRepository): Response | |
| { | |
| $ships = $starshipRepository->findAll(); | |
| $myShip = $ships[array_rand($ships)]; | |
| // ... line 17 | |
| return $this->render('main/homepage.html.twig', [ | |
| 'myShip' => $myShip, | |
| 'ships' => $ships, | |
| ]); | |
| } | |
| } |
In the template, there we go, for the count, we can say ships, which is an array, and then use a Twig filter: |length.
| // ... lines 1 - 4 | |
| {% block body %} | |
| // ... lines 6 - 9 | |
| <div> | |
| Browse through {{ ships|length * 10 }} starships! | |
| {% if ships|length > 2 %} | |
| // ... lines 14 - 17 | |
| {% endif %} | |
| </div> | |
| // ... lines 20 - 42 | |
| {% endblock %} |
That feels good. Let's do the same thing down here... and change it to greater than 2. Try that out. Our site just keeps working!
Next up: let's create more pages and learn how to make routes that are even smarter.
37 Comments
Is there a difference?
1.
2.
Hey John,
Technically, the 2nd is the new PHP 8 syntax called "Constructor Property Promotion", but eventually in both examples a private Class property is created and set to $logger. If you're on a newer PHP version - it's better to use the new syntax as it requires less code to write and easier to read :)
Cheers!
How can the 2nd one work?
I'm getting Warning: Undefined property: App\Repository\StarshipRepository::$logger
~/Projects/starshop(master)$ php -v
PHP 8.3.21 (cli) (built: May 9 2025 06:27:43) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.21, Copyright (c) Zend Technologies
with Zend OPcache v8.3.21, Copyright (c), by Zend Technologies
Hey Martin,
It's difficult to say exactly what the problem is with your code without seeing it. Could you share the code snippet related to the line that shows you that warning?
Cheers!
well, I'm wondering how could the second form of constructor work without setting the private property?
$this->logger = $logger;
Undefined property: App\Repository\StarshipRepository::$logger
Hey Martin,
If you're asking about this specific code block:
The answer is the new PHP syntax, that's how PHP work. You may want to read more in the official docs: https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion
In short, that just sets the property on the class with the name of the variable you're using in the constructor method signature. I mean, that's the official PHP syntax that do that job for you instead of writing that manually like in my first example. That's it :)
I hope that helps!
Cheers!
well, why do I get the error then? I follow your code and versions, defined in composer.json
Hey Martin,
Most probably, the error is not related to the code I shared with you. Could you share the code for the specific line where you have that warning? The warning should give you the line in the file that triggers it, right? Do you see it?
Cheers!
you define logger argument public function __construct(private LoggerInterface $logger)
and at the same time access class property via $this->logger in findAll() (I'm old Java guy)
so, that's why I'm getting that error and I had to add line
$this->logger = $logger;
to constructor in order to call
$this->logger->info('Starship collection retrieved');
in findAll() method.
Hey Martin,
I'm not sure I understand how you wrote that exactly in code... so if you could share the exact code, just literally copy the code of your constructor and paste in the message - that would help a lot. But if that works for you that way - just go that way, not much important.
Cheers!
is it possible to send you screenshot?
Hey Martin,
Yes, you can upload your screenshot to a public image server like "Imgur" and then link to it in the message.
But I probably know what you mean, do you mean PhpStorm warning? I thought you were talking about PHP warning. If you have a valid syntax, like the one I shared above, and you try it and it works for you in the browser but PhpStorm still shows you some warnings complaining about that syntax and do not consider it valid - then most probably you should specify the exact PHP version in the PhpStorm settings.
Open PhpStorm Settings, and then find "PHP". Set the "PHP language level" parameter to your actual PHP version, which I see should be 8.3. Also, set the "CLI Interpreter" to the actual PHP binary path. As soon as you choose the correct PHP level there - you should not see those warnings about invalid syntax anymore.
Cheers!
oh, my bad ... please forgive me, now I see it... I forgot to write access modifier (private) in front of the LoggerInterface in the case of constructor property promotion. All of a sudden even PHPStorm is happy. Sorry again, I didn't notice it...
Hey Martin,
No problem, I'm happy to hear you were able to find the problem! Yes, that access modifier is required to make the Constructor Property Promotion work, otherwise it will be just a var inside the constructor :)
Cheers!
Hello,
In repository, findAll() function is returning an array. Because of that, when we use our variable in twig, it asks the index number of variable to get data.
my controller is as below:
` #[Route('/tasks')]
here, $tasklist is an array because of repository. repository is as below:
`<?php
namespace App\Repository;
use App\Model\Tasks;
class TaskRepository
{
}
`
so, in twig, when I used
<h3 class="card-title">{{ NumberofTask.taskheader }}</h3>,it gives key error. so instead of that, I used as this line:<h3 class="card-title">{{ NumberofTask[0].taskheader }}</h3>do you think I should continue like this or shall ı change returned array of Tasks as obje with any method? If it is, could you please share me which can be used ? In video I see that after array_rand() is used, it is changed to object from array.
Hey @Mahmut-A,
This feels a bit awkward to me. Are you listing all the tasks in this twig template? If so, using a
forwould be better:thank you so much. I was using for loop in different way. your way is easiest:)
Hi,
yes there is task list file in behind, long data file.
so each task is listed to page.
Hi ,
In the code challenge: https://symfonycasts.com/screencast/symfony/create-service/activity/763
Answer:
But "static method" - What do you mean?
public static function(){}? Or Method injection?BR.
Ruslan.
Hey Ruslan,
Yeah, about the "static method" we mean something like
public static function methodName()declaration, but that option is incorrect anyway so you had to discard it :)Method injection is meant with another option which is "By adding it as a method argument inside the controller". So in the "By declaring it as a static method in the controller" option we didn't mean method injection.
But yeah, I see what you mean... we will try to improve that option, thanks!
Cheers!
Thanks for your course.
Hey Ruslan,
You're welcome! Our challenges are polished and reviewed with a few people, so they should be correct in most cases, but if you ever notice any issues with them - feel free to let us know! Sometimes it's easy to miss something due to human factor :)
Cheers!
my
dd($repository->findAll());prints:but
$this->json($repository->findAll())prints[{},{},{}]in the browser. Any idea what could be wrong?This is repo function
my controller
Hey @Krishnadas-Pc
Do you have the Symfony Serializer installed in your project? You need something that knows how to transform Starship objects into an array
Cheers!
Did you explained adding model class in the previous videos?
Hey @Krishnadas-Pc
Yes, a couple of chapters ago.
Here: https://symfonycasts.com/screencast/symfony/json-api
Cheers!
Thanks for the quick reply. I might have missed it but able to add it my own.
Hi,
I am using a service in my environment. This service is starting a CLI command and RESPONSE is occuring in 10-12 minutes. After RESPONSE is generated, user is able to see the response in json format with the aid of controller which I did in controller. It is actually an API .
But, I need a small help. In random time, we should be able to check this RESPONSE without creating again and again. Something will check if RESPONSE is occured or not. If there is not RESPONSE then ok, it is turning icon etc. If RESPONSE exits, we should be able to fetch RESPONSE data, for example only STATUS 200.
Since my below service is working properly and creating RESPONSE, I thought that I can use this RESPONSE in another service. and new api request will trigger this new service, if there is RESPONSE it is ok, if not, it is not ok etc.
But I could not find to way, how to use this RESPONSE in other service.
Do you suggest anything about this problem for me?
thank you
`public function execDeployCommand($fromButton)
{
}
`
Hey @Mahmut-A
Perhaps there are fancy ways to handle this case, but a quick thing to try that comes to mind is to create something like a
ReportStatusentity, where you track the status of your process. Then, in another service, you check for it and print on what status it currently isCheers!
hi
I want to contribute that I found a way to write file the status as below. so another api in controller can able to check this status.
I believe and ı learned from a master:) that ı can also use mam-cache
if ($fromButton === $topology->getActionDeploy()) {
Great! Thank you for sharing it
Hi
I have two questions for these episodes:
/_profiler gives below log. I installed php8.2-intl in ubuntu. but it is same. shall ı reboot the all system? do you have any clue for this?
5:53:47.000 PM<br />deprecation Please install the "intl" PHP extension for best performance.<br />Show context Show traceIn StarshipRepository class, we defined __construct function with parameter $logger. when we use this $logger variable under findAll() function, we used
->logger(without $). maybe this is basic question but I am new for this php and symfony. Also, in StarshipApiController, we called $repository as it is. like$repository->findAll()(with $).is it because of that since we use $logger in different function or normal behavior to use without $?
Hey Mahmut,
symfony check:reqcommand to see if you need more changes in your setup for this Symfony app.private LoggerInterface $loggerinstead of doing it manually. And so, because that's the class property - you call it via$this->loggeras you do for class properties. But about$repository- that's just a method argument, i.e. a simple variable, not a class property, and so you call all variables via$.Cheers!
Hi, until the minute 1.42 does not work for me. i am getting this error:
Cannot resolve argument $respository of "App\Controller\StarshipApiController::getCollection()": Cannot determine controller argument for "App\Controller\StarshipApiController::getCollection()": the $respository argument is type-hinted with the non-existent class or interface: "App\Repository\StarshipsRepository".can you help with that?
Hey @Luis-HG
Double-check if the repository exists in the
src/Repositoryfolder. Oh, by the way, the name isStarshipRepository(singular) without ansCheers!
~/symfony7/starshop (master)$ tree src
src
├── Controller
│ ├── MainController.php
│ └── StarshipApiController.php
├── Kernel.php
├── Model
│ └── Starship.php
└── Repository
Cool, so the repository is there. The error is the extra "s" you have on its name
public function getCollection(LoggerInterface $logger, StarshipRepository $repository)Cheers!
"Houston: no signs of life"
Start the conversation!