This course is still being released! Check back later for more chapters.
Adding a Search + the Request Object
Keep on Learning!
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 SubscribeTime for a quick, but useful, detour away from Doctrine Relations. I know Doctrine relations rock, but so will this! I want to add a search bar to our page. Trust me on this one, it's going to be good.
Pop open the index.html.twig
template. Right at the top, I'll paste in a search input:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="flex justify-end mt-6 mb-6"> | |
<div class="relative w-full max-w-md"> | |
<input type="text" | |
placeholder="Search..." | |
class="w-full p-3 pl-10 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
> | |
<svg class="absolute left-3 top-3 w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35m0 0A8.5 8.5 0 1011 19.5a8.5 8.5 0 005.65-2.85z" /> | |
</svg> | |
</div> | |
</div> | |
// ... lines 18 - 38 | |
{% endblock %} |
Nothing fancy here: just an <input type="text"
"placeholder="search", and then a smattering of classes and a swanky SVG to make it look all pretty.
To let this bad boy submit, wrap it in a form
tag. For the action, have it submit right back to this page: {{ path('app_part_index') }}
. Also, add a name="query"
and method="get"
to the form:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="flex justify-end mt-6 mb-6"> | |
<div class="relative w-full max-w-md"> | |
<form method="get" action="{{ path('app_part_index') }}"> | |
<input type="text" | |
placeholder="Search..." | |
name="query" | |
class="w-full p-3 pl-10 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
> | |
<svg class="absolute left-3 top-3 w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35m0 0A8.5 8.5 0 1011 19.5a8.5 8.5 0 005.65-2.85z" /> | |
</svg> | |
</form> | |
</div> | |
</div> | |
// ... lines 21 - 41 | |
{% endblock %} |
This way, when we submit the form, it will append the search query to the URL as a query parameter.
Getting the Request
Next, head over to PartController
. How do we read the name
query parameter from the URL? Well, that is information from the request, just like request headers or POST data. Symfony packages all of that up in a Request
object. How do we get it? In a controller, it's super easy. Add a Request
argument to your controller method.
You probably remember that you can autowire services like this. The Request
object isn't technically a service, but Symfony is cool enough to let it be autowired anyway. Grab the one from Symfony\Component\HttpFoundation\Request
. You can call it anything, but to stay sane, let's call it $request
:
// ... lines 1 - 6 | |
use Symfony\Component\HttpFoundation\Request; | |
// ... lines 8 - 10 | |
final class PartController extends AbstractController | |
{ | |
'/parts', name: 'app_part_index') | (|
public function index(StarshipPartRepository $repository, Request $request,): Response | |
{ | |
// ... lines 16 - 23 | |
} | |
} |
Set $query = $request->query->get('query')
: the first query
refers to the query parameters, and the second query
is the name of the input field. To make sure this is working, dd($query)
:
// ... lines 1 - 10 | |
final class PartController extends AbstractController | |
{ | |
'/parts', name: 'app_part_index') | (|
public function index(StarshipPartRepository $repository, Request $request,): Response | |
{ | |
$query = $request->query->get('query'); | |
dd($query); | |
// ... lines 18 - 23 | |
} | |
} |
Spin over and try it out. Look at that! It's the string "holodeck".
Enhancing the Search
Next, let's improve the findAllOrderedByPrice()
method to allow for a search. Remove the dd($query);
and pass it into the method:
// ... lines 1 - 10 | |
final class PartController extends AbstractController | |
{ | |
'/parts', name: 'app_part_index') | (|
public function index(StarshipPartRepository $repository, Request $request,): Response | |
{ | |
$query = $request->query->get('query'); | |
$parts = $repository->findAllOrderedByPrice($query); | |
// ... lines 19 - 22 | |
} | |
} |
Break this onto multiple lines and add an if
statement. I'm also going to change the return to $qb = $this->createQueryBuilder('sp')
and get rid of the getQuery()
and getResult()
: we only want the QueryBuilder
for now.
Now for the magic. If we have a search, add an andWhere()
that checks if the lower case name of our Starship part is like our search. I know it looks a bit funky, but that's because PostgreSQL is case-sensitive.
Finally, return the query result:
// ... lines 1 - 13 | |
class StarshipPartRepository extends ServiceEntityRepository | |
{ | |
// ... lines 16 - 40 | |
public function findAllOrderedByPrice(string $search = ''): array | |
{ | |
$qb = $this->createQueryBuilder('sp') | |
->orderBy('sp.price', 'DESC') | |
->innerJoin('sp.starship', 's') | |
->addSelect('s') | |
; | |
if ($search) { | |
$qb->andWhere('LOWER(sp.name) LIKE :search') | |
->setParameter('search', '%'.strtolower($search).'%'); | |
} | |
return $qb->getQuery() | |
->getResult(); | |
} | |
} |
Preserving the Search Value
You might notice that we lose our search value after a search. We don't see "holodeck" in there anymore, and that's just rude. To fix that, back in the template, add a value="{{ app.request.query.get('query') }}"
. Yup, that handy Request
object is available in any template as app.request
:
// ... lines 1 - 4 | |
{% block body %} | |
<div class="flex justify-end mt-6 mb-6"> | |
<div class="relative w-full max-w-md"> | |
<form method="get" action="{{ path('app_part_index') }}"> | |
<input type="text" | |
// ... lines 11 - 12 | |
value="{{ app.request.query.get('query') }}" | |
// ... line 14 | |
> | |
// ... lines 16 - 18 | |
</form> | |
</div> | |
</div> | |
// ... lines 22 - 42 | |
{% endblock %} |
Searching on Multiple Fields
Wouldn't it be great to also search on the parts' notes? Search for 'controls'. Right now, nothing. We really want to search on the name and the notes.
We need some OR
logic. Back in the repository, add an OR
to the andWhere()
clause:
// ... lines 1 - 13 | |
class StarshipPartRepository extends ServiceEntityRepository | |
{ | |
// ... lines 16 - 40 | |
public function findAllOrderedByPrice(string $search = ''): array | |
{ | |
// ... lines 43 - 48 | |
if ($search) { | |
$qb->andWhere('LOWER(sp.name) LIKE :search OR LOWER(sp.notes) LIKE :search') | |
->setParameter('search', '%'.$search.'%'); | |
} | |
// ... lines 53 - 55 | |
} | |
} |
You might be tempted to use orWhere()
, but that's a trap! You can't guarantee where the logical parentheses will be. Trust me, you'll thank me later. Instead, use andWhere()
and put the OR
right inside.
And there we have it! We can now search on the notes, on the name, or both. The takeaway is when you want to use orWhere()
, don't: embed the OR
inside an andWhere()
, and you'll have full control over where the logical parentheses go.
Alright, with that exciting detour complete, let's get back on track and talk about the final relationship type: many to many.