yDónde() y oDónde()
Nuestro sitio tiene un ingenioso cuadro de búsqueda que... no funciona. Si pulso "enter" para buscar "almuerzo", añade ?q=lunch al final de la URL... pero los resultados no cambian. ¡Vamos a conectar esto!
Agarrar el parámetro de consulta de búsqueda
Gira y encuentra nuestro controlador: FortuneController. Para leer el parámetro de consulta, necesitamos el objeto Request de Symfony. Añade un nuevo argumento -no importa si es el primero o el último-, escribe Request -el de Symfony-, pulsa "tab" para añadir esa declaración use, y di $request. Podemos poner el término de búsqueda aquí abajo con $searchTerm = $request->query->get('q').
| // ... lines 1 - 7 | |
| use Symfony\Component\HttpFoundation\Request; | |
| // ... lines 9 - 11 | |
| class FortuneController extends AbstractController | |
| { | |
| // ... line 14 | |
| public function index(Request $request, CategoryRepository $categoryRepository): Response | |
| { | |
| $searchTerm = $request->query->get('q'); | |
| // ... lines 18 - 26 | |
| } | |
| // ... lines 28 - 35 | |
| } |
Estamos utilizando q... sólo porque es lo que elegí en mi plantilla... puedes verlo aquí abajo en templates/base.html.twig. Esto se construye con un formulario muy simple que incluye <input type="text", name="q". Así que estamos leyendo el parámetro de consulta q y estableciéndolo en $searchTerm.
Debajo, if tenemos un $searchTerm, establecemos $categories en$categoryRepository->search() (un método que vamos a crear) y pasamos$searchTerm. Si no tenemos un $searchTerm, reutiliza la lógica de consulta que teníamos antes.
| // ... lines 1 - 14 | |
| public function index(Request $request, CategoryRepository $categoryRepository): Response | |
| { | |
| // ... line 17 | |
| if ($searchTerm) { | |
| $categories = $categoryRepository->search($searchTerm); | |
| } else { | |
| $categories = $categoryRepository->findAllOrdered(); | |
| } | |
| // ... lines 23 - 26 | |
| } | |
| // ... lines 28 - 37 |
Añadir una cláusula WHERE
¡Estupendo! ¡Vamos a crear ese método search()!
En nuestro repositorio, digamos public function search(). Tomará un argumento string $term y devolverá un array. Como la última vez, añadiré un PHPDoc que diga que devuelve un array de objetos Category[]. Elimina el @param... porque eso no añade nada.
| // ... lines 1 - 17 | |
| class CategoryRepository extends ServiceEntityRepository | |
| { | |
| // ... lines 20 - 37 | |
| /** | |
| * @return Category[] | |
| */ | |
| public function search(string $term): array | |
| { | |
| } | |
| // ... lines 45 - 87 | |
| } |
Vale: nuestra consulta empezará como antes... aunque podemos ponernos más sofisticados y returninmediatamente. Di $this->createQueryBuilder() y utiliza el mismo alias category. Es una buena idea utilizar siempre el mismo alias para una entidad: nos ayudará más adelante a reutilizar partes de un constructor de consultas.
| // ... lines 1 - 40 | |
| public function search(string $term): array | |
| { | |
| return $this->createQueryBuilder('category') | |
| // ... lines 44 - 47 | |
| } | |
| // ... lines 49 - 93 |
Para la cláusula WHERE, utiliza ->andWhere(). También existe un método where()... ¡pero creo que nunca lo he utilizado! Y... tú tampoco deberías. Utilizar andWhere()siempre está bien, aunque sea la primera cláusula WHERE... y en realidad no necesitamos la parte "y". Doctrine es lo suficientemente inteligente como para darse cuenta.
andWhere() vs where()
¿Qué tiene de malo ->where()? Bueno, si antes has añadido una cláusula WHERE a tu QueryBuilder, llamar a ->where() eliminaría eso y lo sustituiría por lo nuevo... que probablemente no es lo que quieres. ->andWhere() siempre se añade a la consulta.
Dentro di category, y como quiero buscar en la propiedad name de la entidadCategory, di category.name =. La siguiente parte es muy importante. Nunca, nunca, nunca añadas la parte dinámica directamente a tu cadena de consulta. Esto te expone a ataques de inyección SQL. Vaya. En lugar de eso, cada vez que necesites poner una parte dinámica en una consulta, pon en su lugar un marcador de posición: como :searchTerm. La palabra searchTermpodría ser cualquier cosa... y tú la rellenas diciendo->setParameter('searchTerm', $term).
| // ... lines 1 - 40 | |
| public function search(string $term): array | |
| { | |
| return $this->createQueryBuilder('category') | |
| ->andWhere('category.name = :searchTerm') | |
| ->setParameter('searchTerm', $term) | |
| // ... lines 46 - 47 | |
| } | |
| // ... lines 49 - 93 |
¡Perfecto! El final es fácil: ->getQuery() para convertir eso en un objeto Query y luego ->getResult() para ejecutar esa consulta y devolver la matriz de objetos Category.
| // ... lines 1 - 40 | |
| public function search(string $term): array | |
| { | |
| return $this->createQueryBuilder('category') | |
| ->andWhere('category.name = :searchTerm') | |
| ->setParameter('searchTerm', $term) | |
| ->getQuery() | |
| ->getResult(); | |
| } | |
| // ... lines 49 - 93 |
¡Estupendo! Si nos dirigimos y probamos esto... ¡ya lo tengo!
Hacer la consulta difusa
Pero si quitamos algunas letras y volvemos a buscar... ¡no obtenemos nada! Lo ideal es que la búsqueda sea difusa: que coincida con cualquier parte del nombre.
Y eso es fácil de hacer. Cambia nuestro ->andWhere() de = a LIKE... y aquí abajo, por searchTerm... esto parece un poco raro, pero añade un porcentaje antes y después para hacerlo difuso en ambos lados.
| // ... lines 1 - 40 | |
| public function search(string $term): array | |
| { | |
| return $this->createQueryBuilder('category') | |
| ->andWhere('category.name LIKE :searchTerm') | |
| ->setParameter('searchTerm', '%'.$term.'%') | |
| // ... lines 46 - 47 | |
| } | |
| // ... lines 49 - 93 |
Si lo probamos ahora... ¡eureka!
Cuidado con orWhere
¡Pero pongámonos más duros! Cada categoría tiene su propio icono - como fa-quote-left o el que tiene debajo fa-utensils. ¡Esto también es una cadena que se almacena en la base de datos!
¿Podríamos hacer que nuestra búsqueda también buscara en esa propiedad? ¡Por supuesto! Sólo tenemos que añadir un OR a nuestra consulta.
Aquí abajo, podrías tener la tentación de utilizar este bonito ->orWhere() pasando a category.con el nombre de esa propiedad... que... si miramos en Category rápidamente... es $iconKey. Así que category.iconKey LIKE :searchTerm.
Y sí, podríamos hacerlo. Pero ¡no lo hagas! Recomiendo no utilizar nunca orWhere(). ¿Por qué? Porque... las cosas se pueden poner raras. Imagina que tuviéramos una consulta como ésta: ->andWhere('category.name LIKE :searchTerm'), ->orWhere('category.iconKey LIKE :searchTerm') ->andWhere('category.active = true') .
¿Ves el problema? Lo que probablemente estoy intentando hacer es buscar categorías... pero sólo todas las que coincidan con categorías activas. En realidad, si el searchTermcoincide con iconKey, se devolverá un Category, esté activo o no. Si escribiéramos esto en SQL, incluiríamos paréntesis alrededor de las dos primeras partes para que se comportara. Pero cuando utilizas ->orWhere(), eso no ocurre.
Entonces, ¿cuál es la solución? Utiliza siempre andWhere()... y si necesitas un OR, ¡ponlo justo dentro! Sí, lo que pasas a andWhere() es DQL, así que podemos decirOR category.iconKey LIKE :searchTerm.
| // ... lines 1 - 40 | |
| public function search(string $term): array | |
| { | |
| return $this->createQueryBuilder('category') | |
| ->andWhere('category.name LIKE :searchTerm OR category.iconKey LIKE :searchTerm') | |
| // ... lines 45 - 47 | |
| } | |
| // ... lines 49 - 93 |
¡Y ya está! En el SQL final, Doctrine pondrá paréntesis alrededor de este WHERE.
¡Vamos a probarlo! Gira e intenta buscar "utensilios". Escribo parte de la palabra y... ¡ya está! ¡Coincidimos en el iconKey!
Ah, y para mantener la coherencia con la página de inicio normal, incluyamos->addOrderBy('category.name', 'DESC').
| // ... lines 1 - 40 | |
| public function search(string $term): array | |
| { | |
| return $this->createQueryBuilder('category') | |
| // ... lines 44 - 45 | |
| ->addOrderBy('category.name', Criteria::DESC) | |
| // ... lines 47 - 48 | |
| } | |
| // ... lines 50 - 94 |
Ahora, si vamos a la página de inicio y escribimos la letra "p" en la barra de búsqueda, ¡sí! se ordena alfabéticamente.
Y si tienes dudas sobre tu consulta, siempre puedes ir al perfilador de Doctrine para ver la versión formateada. Es exactamente lo que esperábamos.
A continuación: Vamos a ampliar nuestra consulta, para que podamos buscar en las galletas de la suerte que hay dentro de cada categoría. Para ello, necesitaremos un JOIN.
15 Comments
If anyone is slinging with PostgreSQL and wondering how to use the ILIKE operator, this code should help:
Hey @achabrzyk ,
Thank you for sharing this tip with our PostgreSQL friends :)
Cheers!
If someone wants as little 'raw' dql as possible, the where condition could also be written this way:
Hey S-H,
Thanks for sharing an alternative of building that WHERE part of the query with Doctrine Criteria - we will cover them too but a bit further in this course: https://symfonycasts.com/screencast/doctrine-queries/criteria - also will show some use cases where they can be useful. IMO this definitely looks a bit more complex than the way shown in this video :)
Cheers!
I wrote query by this example, but it does not generate or condition, no matter what.
turns out there was mistake in parantheses but I was not getting error, just wrong query.
Hey Darius,
Yeah, that happens, and it's the most complex bug to debug because you don't have an error. Glad to hear you figured it out, good job!
Cheers!
Hi! I didn't know where to ask this so I'll do it in here because is about a search bar.
I already have a search bar for all of my data and it works fine, just like in this tutorial. The problem is when I want to search through specific data that each user has. I will show you what I've tried so far.
This the function I have in my Vehicle Repository:
This is my controller:
and finally this is in my template:
It seems to be that one of my problems(or may be the only one) is that I'm not passing the 'q' argument, because the error says that I have not defined the querybuilder, meaning that either $q or $slug is no present, but I have tried both and the $q is the one missing.
I'd really appreciate any help with this matter. BTW, I love your tutorials.
Hey Octavio,
First of all, it seems that your code is fragile. The signature of the
createSearchResultsFromUser()allow both args to be null, which means you will not hit thatif ($slug && $q)in some cases. To make it work, you need to create a query builder first in that method, and only then add thatif ($slug && $q)that will modify it, instead of creating. The correct code would be something like this:But I would suggest you to improve it even more and separate those search args in different
ifstatements, like this:This way the slug and q will be independent, and you will add them only when they are not null.
Or just revisit your search business logic one more time.
Also, you have an error in the place where you call the method, you're passing those args as an array:
$queryBuilder = $vehicleRepository->createSearchResultsFromUser([$slug, $request->query->get('q')]);While it should be passed as 2 separate args:
$queryBuilder = $vehicleRepository->createSearchResultsFromUser($slug, $request->query->get('q'));I hope this helps! Sorry, but I can't help you more on it because it's a personal project question, we have bandwidth to answer tutorial questions only. Thank you for your understanding!
Cheers!
The most confusing for me is always using WHERE something IN () and second one is using UNION queries so I hope something will be in this course about it I end up using raw sql for these 2 scenarios
Hey Peter,
Yes we will cover WHERE IN() in this course, see: https://symfonycasts.com/screencast/doctrine-queries/where-in - that's a pretty simple thing, so it should not be a problem to understand that I think.
About UNION - we don't cover it in this tutorial, but with the knowledge you get in this course you should be able to leverage that MySQL function yourself as well
Cheers!
Do you know why my search is case sensitive? I worked around this by adding LOWER()
but wondering if it's just different version or another issue?
Hey
I think it's a PostgreSQL feature, and this workaround is pretty acceptable to use.
Cheers
Hello,
It depends on your query and your database server can you provide more information?
Cheers
"Houston: no signs of life"
Start the conversation!