This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
07.

Cosmic Queries: the Repository Class

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to talk Entity Repositories - a place where we "dock" custom queries for an entity.

In the homepage controller, we wrote a query with the query builder to find all ships. This is fine, but if we needed to use the same query elsewhere, we'd need to duplicate it. And if we wanted to change it, we'd need to do it in multiple places. Gross!

Fetching The Repository Service

Entity Repositories to the rescue! Wait, didn't make:entity create one of these? It did! To grab the repository object for an entity, try dd($em->getRepository(Starship::class)):

32 lines | src/Controller/MainController.php
// ... lines 1 - 10
class MainController extends AbstractController
{
// ... line 13
public function homepage(
EntityManagerInterface $em,
): Response {
dd($em->getRepository(Starship::class));
// ... lines 18 - 29
}
}

Back to the app and refresh. Cool! We have an App\Repository\StarshipRepository object. Go check out this class: src/Repository/StarshipRepository.php.

First, if you're curious how Doctrine knows this class is the repository for the Starship entity, jump into src/Entity/Starship.php. Ah, the #[ORM\Entity] attribute has repositoryClass: StarshipRepository::class:

110 lines | src/Entity/Starship.php
// ... lines 1 - 7
#[ORM\Entity(repositoryClass: StarshipRepository::class)]
class Starship
// ... lines 10 - 110

Each entity - like Starship - has its own repository class. It's empty to start, but we'll soon fill it with custom queries. Also, it's a service! That means we can autowire it.

In the homepage controller, remove this dd(). Let's simplify: replace EntityManagerInterface with StarshipRepository $repository:

25 lines | src/Controller/MainController.php
// ... lines 1 - 9
class MainController extends AbstractController
{
#[Route('/', name: 'app_homepage')]
public function homepage(
StarshipRepository $repository,
): Response {
// ... lines 16 - 22
}
}

This query we wrote earlier, to fetch all ships, is so common that every repository comes with a shortcut for it: findAll():

25 lines | src/Controller/MainController.php
// ... lines 1 - 9
class MainController extends AbstractController
{
// ... line 12
public function homepage(
StarshipRepository $repository,
): Response {
$ships = $repository->findAll();
// ... lines 17 - 22
}
}

Much nicer! Back in the app, refresh. It still works!

Let's also use this in StarshipController::show(). Replace EntityManagerInterface with StarshipRepository $repository:

25 lines | src/Controller/StarshipController.php
// ... lines 1 - 9
class StarshipController extends AbstractController
{
#[Route('/starships/{id<\d+>}', name: 'app_starship_show')]
public function show(int $id, StarshipRepository $repository): Response
{
// ... lines 15 - 22
}
}

Every repository also comes pre-built with a find() method! And because this is the Starship repository, we don't need to pass the entity class - just the $id:

25 lines | src/Controller/StarshipController.php
// ... lines 1 - 9
class StarshipController extends AbstractController
// ... lines 11 - 12
public function show(int $id, StarshipRepository $repository): Response
{
$ship = $repository->find($id);
// ... lines 16 - 22
}
}

Jump back to the app, refresh, and click a starship. Still works, perfect!

Custom Queries in the Repository

Back in the homepage controller, instead of finding all ships, what if we need to only find ships whose status is not completed: so just waiting or in progress. We need a custom query! But this time, instead of writing it in the controller, let's organize it in the repository.

Add a new public function findIncomplete() method that returns an array. Include a docblock so our IDE knows this will be an array of Starship objects:

59 lines | src/Repository/StarshipRepository.php
// ... lines 1 - 12
class StarshipRepository extends ServiceEntityRepository
{
// ... lines 15 - 19
/**
* @return Starship[]
*/
public function findIncomplete(): array
{
// ... lines 25 - 31
}
// ... lines 33 - 57
}

Inside, return $this->createQueryBuilder('e'). This is just an alias for the entity - we'll need it in a sec. What's cool about creating a query builder in a repository, is that you don't need to specify the select() or from() like in the controller. It's done automatically. All we need to do is add ->where('e.status != :status'). e.status is the property name on the Starship entity and :status is a placeholder for a value. Pass it a value with ->setParameter('status', StarshipStatusEnum::COMPLETED).

This silly-looking :status and the immediate setParameter('status', ...) is important. Never include the actual value in the query for two reasons. First, Doctrine can optimize the query performance slightly when using placeholders. Second, and more importantly, placeholders prevent SQL injection attacks! If you thought The Borg was bad, you'll really hate SQL injection attacks! To finish the query, add ->getQuery() and ->getResult():

59 lines | src/Repository/StarshipRepository.php
// ... lines 1 - 12
class StarshipRepository extends ServiceEntityRepository
{
// ... lines 15 - 22
public function findIncomplete(): array
{
return $this->createQueryBuilder('s')
->where('s.status != :status')
->orderBy('s.arrivedAt', 'DESC')
->setParameter('status', StarshipStatusEnum::COMPLETED)
->getQuery()
->getResult()
;
}
// ... lines 33 - 57
}

Back in the homepage controller, replace findAll() with findIncomplete():

25 lines | src/Controller/MainController.php
// ... lines 1 - 9
class MainController extends AbstractController
{
// ... line 12
public function homepage(
// ... line 14
): Response {
$ships = $repository->findIncomplete();
// ... lines 17 - 22
}
}

Spin back over. We should see this completed ship disappear. We do! The query is working! Check out the profiler: we see the query and the parameter we used.

Another Custom Query, Another Repository Method

Back in the controller, I don't like this $myShip logic. And it's not because we're faking the idea of "my ship" by just grabbing the first one. It's because, whatever the logic is, this should be in the repository so we can find "my ship" wherever we need it.

In StarshipRepository, add a new public function findMyShip() method that returns a Starship object. We can imagine that this method would take a user or something to find their ship, but for now, just return $this->findAll()[0] to get the first ship in the table:

64 lines | src/Repository/StarshipRepository.php
// ... lines 1 - 12
class StarshipRepository extends ServiceEntityRepository
{
// ... lines 15 - 33
public function findMyShip(): Starship
{
return $this->findAll()[0];
}
// ... lines 38 - 62
}

Back in the controller, replace this with $repository->findMyShip():

25 lines | src/Controller/MainController.php
// ... lines 1 - 9
class MainController extends AbstractController
{
// ... line 12
public function homepage(
// ... line 14
): Response {
// ... line 16
$myShip = $repository->findMyShip();
// ... lines 18 - 22
}
}

That just reads better! Spin over to the app and refresh. Still works! Look at the profiler: two queries! The first finds all the incomplete ships and the second is the findAll() from findMyShip(). Perfect!

Next, let's improve our fixtures and make them 100 times more fun with a library called Foundry. This will let us create a whole fleet of Starships as if we had a replicator. Let's do it!