Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

yDónde() y oDónde()

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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.

Leave a comment!

6
Login or Register to join the conversation
S-H Avatar
S-H Avatar S-H | posted hace 2 meses | edited

If someone wants as little 'raw' dql as possible, the where condition could also be written this way:

           ->andWhere(
                $qb->expr()->orX(
                    $qb->expr()->like('category.name', ':searchTerm'),
                    $qb->expr()->like('category.iconKey', ':searchTerm'),
                )
            )
1 Reply

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!

Reply
Stanislaw Avatar
Stanislaw Avatar Stanislaw | posted hace 30 días

Do you know why my search is case sensitive? I worked around this by adding LOWER()

->andWhere('LOWER(category.name) LIKE LOWER(:searchTerm)')

but wondering if it's just different version or another issue?

Reply

Hey

I think it's a PostgreSQL feature, and this workaround is pretty acceptable to use.

Cheers

Reply
Default user avatar
Default user avatar unknown | posted hace 30 días | edited
Comment was deleted.

Hello,

It depends on your query and your database server can you provide more information?

Cheers

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "beberlei/doctrineextensions": "^1.3", // v1.3.0
        "doctrine/doctrine-bundle": "^2.7", // 2.9.1
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.15.1
        "symfony/asset": "6.2.*", // v6.2.7
        "symfony/console": "6.2.*", // v6.2.10
        "symfony/dotenv": "6.2.*", // v6.2.8
        "symfony/flex": "^2", // v2.2.5
        "symfony/framework-bundle": "6.2.*", // v6.2.10
        "symfony/proxy-manager-bridge": "6.2.*", // v6.2.7
        "symfony/runtime": "6.2.*", // v6.2.8
        "symfony/twig-bundle": "6.2.*", // v6.2.7
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "symfony/maker-bundle": "^1.47", // v1.48.0
        "symfony/stopwatch": "6.2.*", // v6.2.7
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.10
        "zenstruck/foundry": "^1.22" // v1.32.0
    }
}
userVoice