Buy

Request Object & Query OR Logic

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

Login Subscribe

Because 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:

61 lines templates/comment_admin/index.html.twig
... 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:

25 lines src/Controller/CommentAdminController.php
... 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:

25 lines src/Controller/CommentAdminController.php
... 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'):

25 lines src/Controller/CommentAdminController.php
... 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:

81 lines src/Repository/CommentRepository.php
... 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:

81 lines src/Repository/CommentRepository.php
... 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:

81 lines src/Repository/CommentRepository.php
... 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.'%':

81 lines src/Repository/CommentRepository.php
... 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():

81 lines src/Repository/CommentRepository.php
... 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:

25 lines src/Controller/CommentAdminController.php
... 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.

Leave a comment!

  • 2018-07-04 Diego Aguiar

    Hey Carol Pelu

    If you inject your variables to the query using the QueryBuilder $qb->setParameter('name', $name);, then you are totally protected from SQL injection. You only have to be paranoid when you are doing it by your own (Executing a handmade query for example)

    Cheers!

  • 2018-07-04 Carol Pelu

    Hey!

    I have a question regarding this specific line of code:

    `->setParameter('term', '%'.$term.'%')`

    Is it safe from SQL injection?

    Because paranoya strikes often, I turned that code into

    `->setParameter('term', '%'.addcslashes($term, '%_').'%')`

    Is it ok? If it's not. Can you please tell me why?

    Looking forward to your answear and more awesome courses.

  • 2018-06-15 Victor Bocharsky

    Hey Dirk,

    Thanks for sharing it with others! Yeah, makes sense to reverse the values.

    Cheers!

  • 2018-06-14 Dirk J. Faber

    That works great, thank you! I ended up passing the variable $q from the controller to the template to add this in case a query was already made, and used app.request.attributes.get('_route') to refer back to the route the template was rendered on. Because I wanted to make a single sort-button for both ascending and descending an attribute I used an if statement to check for 'ASC' in app.request.uri. Altogether after shortening 'sortOrder' to 'o' and sortAttribute to 'a' it looks like below and works well. Thanks again!



    {% if ('p.name' in app.request.uri and 'ASC' in app.request.uri) %}
    {{ path(app.request.attributes.get('_route'), {'q': q,'a' : 'p.name', 'o' : 'DESC'}) }}
    {% else %}
    {{ path(app.request.attributes.get('_route'), {'q': q,'a' : 'p.name', 'o' : 'ASC'}) }}
    {% endif %}


  • 2018-06-14 Victor Bocharsky

    Hey Dirk,

    Actually, you can use router to generate links with extra parameters, even if route does not have it - they will be added after question mark in your URL. So in controller:


    $this->generateUrl('route_name', [
    'sortOrder' => 'DESC',
    'sortAttribute' => 'p',
    ]);

    or in Twig templates:


    {{ path('route_name', {'sortOrder' => 'DESC', 'sortAttribute' => 'p'}) }}

    If you route has some required parameters - no problem, just pass required params first and then pass extra parameters, e.g.:


    $this->generateUrl('article_show', [
    // List all required params first
    'slug' => 'article-slug',
    // Add any extra params
    'sortOrder' => 'DESC',
    'sortAttribute' => 'p.createdAt',
    ]);

    So as you see you just need to pass actual parameters values, i.e. $sortAttribute and $sortOrder to your template. But you know what? You actually may use request object in Twig templates as well, so it would be something like:


    {{ path('route_name', {'sortOrder' => app.request.query.get('sortOrder'), 'sortAttribute' => app.request.query.get('sortAttribute')}) }}

    I hope I understand you right and it makes sense for you.

    Cheers!

  • 2018-06-13 Dirk J. Faber

    Another wonderful lesson! I was wondering, what is the preferred method to create parameters in Twig when you want to have a simple anchor tag? I am asking because I have using this search query and extended it taking two more arguments, one for an attribute to sort on and one for the sort order.
    In my controller this looks like:


    $q = $request->query->get('q');
    $sortAttribute = $request->query->get('sortAttribute', 'p.createdAt');
    $sortOrder = $request->query->get('sortOrder', 'DESC');
    $toSortURL = '?q='.$q.'&'.'sortAttribute=p.';
    $sortOrderURL = '&sortOrder=';

    In twig what I have is:


    a href="{{ toSortURL }}name{{ sortOrderURL }}ASC"> Sort ASC < / a> -
    a href="{{ toSortURL }}name{{ sortOrderURL }}DESC"> Sort DESC < / a>

    Edit: I had to edit this comment because Disqus kept on rendering the HTML
    This all works well as far as I can tell, it just looks ugly and surely there is a better method, right?