Request Object & Query OR Logic
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 SubscribeBecause astronauts love to debate news, our site will have a lot of comments on production. So, let's add a search box above this table so we can find things quickly.
Open the template and, on top, I'm going to paste in a simple HTML form:
// ... lines 1 - 6 | |
{% block content_body %} | |
<div class="row"> | |
<div class="col-sm-12"> | |
<h1>Manage Comments</h1> | |
<form> | |
<div class="input-group mb-3"> | |
<input type="text" | |
name="q" | |
class="form-control" | |
placeholder="Search..." | |
> | |
<div class="input-group-append"> | |
<button type="submit" | |
class="btn btn-outline-secondary"> | |
<span class="fa fa-search"></span> | |
</button> | |
</div> | |
</div> | |
</form> | |
// ... lines 27 - 57 | |
</div> | |
</div> | |
{% endblock %} |
We're not going to use Symfony's form system because, first, we haven't learned about it yet, and second, this is a super simple form: Symfony's form system wouldn't help us much anyways.
Ok! Check this out: the form has one input field whose name is q
, and a button at the bottom. Notice that the form has no action=
: this means that the form will submit right back to this same URL. It also has no method=
, which means it will submit with a GET request instead of POST, which is exactly what you want for a search or filter form.
Let's see what it looks like: find your browser and refresh. Nice! Search for "ipsam" and hit enter. No, the search won't magically work yet. But, we can see the ?q=
at the end of the URL.
Fetching the Request Object
Back in the controller, hmm. The first question is: how can we read the ?q
query parameter? Actually, let me ask some bigger questions! How could we read POST data? Or, headers? Or the content of uploaded files?
Science! Well, actually, the request! Any time you need to read information about the request - POST data, headers, cookies, etc - you need Symfony's Request
object. How can you get it? Well... you can probably guess: add another argument with a Request
type-hint:
// ... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\Request; | |
// ... lines 7 - 9 | |
class CommentAdminController extends Controller | |
{ | |
/** | |
* @Route("/admin/comment", name="comment_admin") | |
*/ | |
public function index(CommentRepository $repository, Request $request) | |
{ | |
// ... lines 17 - 22 | |
} | |
} |
Important: get the one from HttpFoundation
- there are several, which, yea, is confusing:
// ... lines 1 - 5 | |
use Symfony\Component\HttpFoundation\Request; | |
// ... lines 7 - 25 |
So far, we know of two "magical" things you can do with controller arguments. First, if you type-hint a service class or interface, Symfony will give you that service. And second, if you type-hint an entity class, Symfony will query for that entity by using the wildcard in the route.
Well, you might think that the Request
falls into the first magic category. I mean, that the Request
is a service. Well, actually... the Request
object is not a service. And, the reasons why are technical, and honestly, not very important. The ability to type-hint a controller argument with Request
is the third "magic" trick you can do with controller arguments. So, it's (1) type-hint services, (2) type-hint entities or (3) type-hint the Request
class. There is other magic that's possible, but these are the 3 main cases.
Oh, side-note: while the Request
object is not in the service container, there is a service called RequestStack
. You can fetch it like any service and call getCurrentRequest()
to get the Request
:
public function index(RequestStack $requestStack)
{
$request = $requestStack->getCurrentRequest();
}
Anyways, the request gives us access to everything about the... um, request! Add $q = $request->query->get('q')
:
// ... lines 1 - 9 | |
class CommentAdminController extends Controller | |
{ | |
/** | |
* @Route("/admin/comment", name="comment_admin") | |
*/ | |
public function index(CommentRepository $repository, Request $request) | |
{ | |
$q = $request->query->get('q'); | |
// ... lines 18 - 22 | |
} | |
} |
This is how you read query parameters, it's like a modern $_GET
. There are other properties for almost everything else: $request->headers
for headers, $request->cookies
, $request->files
, and a few more. Basically, any time you want to use $_GET
, $_POST
, $_SERVER
or any of those global variables, use the Request
instead.
A Custom Query with OR Logic
Now that we have the search term, we need to use that to make a custom query. So, sadly, we cannot use findBy()
anymore: it's not smart enough to do queries that use the LIKE
keyword. No worries: inside CommentRepository
, add a public function called findAllWithSearch()
. Give this a nullable string argument called $term
:
// ... lines 1 - 15 | |
class CommentRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 34 | |
public function findAllWithSearch(?string $term) | |
{ | |
// ... lines 37 - 49 | |
} | |
// ... lines 51 - 79 | |
} |
I'm making this nullable because, for convenience, I want to allow this method to be called with a null
term, and we'll be smart enough to just return everything.
Above the method, add some PHP doc: this will @return
an array of Comment
objects:
// ... lines 1 - 15 | |
class CommentRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 30 | |
/** | |
* @param string|null $term | |
* @return Comment[] | |
*/ | |
public function findAllWithSearch(?string $term) | |
{ | |
// ... lines 37 - 49 | |
} | |
// ... lines 51 - 79 | |
} |
Ok: we already know how to write custom queries: $this->createQueryBuilder()
with an alias of c
:
// ... lines 1 - 15 | |
class CommentRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 30 | |
/** | |
* @param string|null $term | |
* @return Comment[] | |
*/ | |
public function findAllWithSearch(?string $term) | |
{ | |
$qb = $this->createQueryBuilder('c'); | |
// ... lines 38 - 49 | |
} | |
// ... lines 51 - 79 | |
} |
Then, if a $term
is passed, we need a WHERE clause. But, here's the tricky part: I want to search for the term on a couple of fields: I want WHERE content LIKE $term OR authorName LIKE $term
.
How can we do this? Hmm, the QueryBuilder
apparently has an orWhere()
method. Perfect, right? No! Surprise, I never use this method. Why? Imagine a complex query with various levels of AND clauses mixed with OR clauses and parenthesis. With a complex query like this, you would need to be very careful to use the parenthesis in just the right places. One mistake could lead to an OR causing many more results to be returned than you expect!
To best handle this in Doctrine, always use andWhere()
and put all the OR logic right inside: c.content LIKE :term OR c.authorName LIKE :term
. On the next line, set term
to, this looks a little odd, '%'.$term.'%'
:
// ... lines 1 - 15 | |
class CommentRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 30 | |
/** | |
* @param string|null $term | |
* @return Comment[] | |
*/ | |
public function findAllWithSearch(?string $term) | |
{ | |
$qb = $this->createQueryBuilder('c'); | |
if ($term) { | |
$qb->andWhere('c.content LIKE :term OR c.authorName LIKE :term') | |
->setParameter('term', '%' . $term . '%') | |
; | |
} | |
// ... lines 44 - 49 | |
} | |
// ... lines 51 - 79 | |
} |
By putting this all inside andWhere()
- instead of orWhere()
- all of that logic will be surrounded by a parenthesis. Later, if we add another andWhere()
, it'll logically group together properly.
Finally, in all cases, we want to return $qb->orderBy('c.createdAt',
'DESC') and ->getQuery()->getResult()
:
// ... lines 1 - 15 | |
class CommentRepository extends ServiceEntityRepository | |
{ | |
// ... lines 18 - 30 | |
/** | |
* @param string|null $term | |
* @return Comment[] | |
*/ | |
public function findAllWithSearch(?string $term) | |
{ | |
$qb = $this->createQueryBuilder('c'); | |
if ($term) { | |
$qb->andWhere('c.content LIKE :term OR c.authorName LIKE :term') | |
->setParameter('term', '%' . $term . '%') | |
; | |
} | |
return $qb | |
->orderBy('c.createdAt', 'DESC') | |
->getQuery() | |
->getResult() | |
; | |
} | |
// ... lines 51 - 79 | |
} |
Remember, getResult()
returns an array of results, and getOneOrNullResult()
returns just one row.
Phew! That looks great! Go back to the controller. Use that method: $comments = $repository->findAllWithSearch()
passing it $q
:
// ... lines 1 - 9 | |
class CommentAdminController extends Controller | |
{ | |
/** | |
* @Route("/admin/comment", name="comment_admin") | |
*/ | |
public function index(CommentRepository $repository, Request $request) | |
{ | |
$q = $request->query->get('q'); | |
$comments = $repository->findAllWithSearch($q); | |
// ... lines 19 - 22 | |
} | |
} |
Moment of truth! First, remove the ?q=
from the URL. Ok, everything looks good. Now search for something very specific, like, ahem, reprehenderit
. And, yes! A much smaller result. Try an author: Ernie
: got it!
Woo! This is great! But, we can do more! Next, let's learn about a Twig global variable that can help us fill in this input box when we search. Then, it's finally time to add a join to our custom query.
Offtopic: When I do 'symfony console make:migration' it says: This behaviour is (currently) not supported by Doctrine 2.