El ingenioso sistema de criterios
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 SubscribeTenemos este método superútil $ship->getParts()
: nos devuelve todas las piezas para nuestra nave estelar. Pero el año fiscal está llegando a su fin, y tenemos que planificar nuestro presupuesto. Aburrido, pero necesario: ¡nuestros jefes ferengis lo exigen! La mayoría de las piezas son baratas, como las tuercas, los tornillos y la cinta aislante que lo mantienen todo unido. Ésas no nos preocupan realmente. En cambio, quiero devolver rápidamente todas las piezas de nuestra nave que cuesten más de 50.000 créditos.
Claro, podríamos hacer una nueva consulta en nuestro controlador para todas las piezas de la nave estelar relacionadas con la nave cuyo precio sea superior a 50.000. Pero, ¿dónde está la gracia en eso? Quiero seguir con nuestro atajo fácil de $ship->getParts()
. ¿Es posible?
Añadir getExpensiveParts()
Entra en la clase Starship
y busca el método getParts()
. Cópialo, pégalo a continuación y cámbiale el nombre a getExpensiveParts()
. Por ahora, devuélvelo todo:
// ... lines 1 - 13 | |
class Starship | |
{ | |
// ... lines 16 - 160 | |
/** | |
* @return Collection<int, StarshipPart> | |
*/ | |
public function getExpensiveParts(): Collection | |
{ | |
return $this->parts; | |
} | |
// ... lines 168 - 189 | |
} |
De vuelta a nuestra plantilla de programa, dale una vuelta a esto. Cambia parts
por expensiveParts
:
// ... lines 1 - 4 | |
{% block body %} | |
// ... lines 6 - 19 | |
<div class="md:flex justify-center space-x-3 mt-5 px-4 lg:px-8"> | |
// ... lines 21 - 25 | |
<div class="space-y-5"> | |
<div class="mt-8 max-w-xl mx-auto"> | |
<div class="px-8 pt-8"> | |
// ... lines 29 - 58 | |
<h4 class="text-xs text-slate-300 font-semibold mt-2 uppercase"> | |
Expensive Parts ({{ ship.expensiveParts|length }}) | |
</h4> | |
<ul class="text-sm font-medium text-slate-400 tracking-wide"> | |
{% for part in ship.expensiveParts %} | |
// ... lines 64 - 71 | |
{% endfor %} | |
</ul> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
No hay propiedad expensiveParts
, pero esto llamará al método getExpensiveParts()
que acabamos de crear.
Filtrar lo barato:
Es hora de hacer que nuestro método devuelva sólo las partes caras. Recuerda:$this->parts
no es una matriz, sino un objeto Colección especial con algunos trucos en la manga. Uno de ellos es el método filter()
. Éste ejecuta una llamada de retorno por cada parte. Si devolvemos verdadero, incluye esa parte en la colección final. Si devolvemos false, la filtra. Así que podemos decirreturn $part->getPrice() > 50000;
:
// ... lines 1 - 13 | |
class Starship | |
{ | |
// ... lines 16 - 163 | |
public function getExpensiveParts(): Collection | |
{ | |
return $this->parts->filter(function (StarshipPart $part) { | |
return $part->getPrice() > 50000; | |
}); | |
} | |
// ... lines 170 - 191 | |
} |
¡Listo! Excepto que... esto es súper ineficiente. Seguimos buscando todas las piezas relacionadas con nuestra nave estelar y filtrándolas en PHP. Imagina que tuviéramos 50.000 piezas, pero sólo 10 de ellas costaran más de 50.000. ¡Menudo despilfarro! ¿Podemos pedirle a Doctrine que cambie la consulta para que sólo coja las piezas relacionadas con la nave estelar cuyo precio sea superior a 50.000?
El poder del objeto Criterio
Entra en el objeto Criteria
. Esta cosa es poderosa. Aunque, lo admito, también un poco críptico. Elimina nuestra lógica y utiliza en su lugar $criteria
igual aCriteria::create()->andWhere(Criteria::expr()->gt('price', 50000))
. Para utilizar esto, return $this->parts->matching($criteria);
:
// ... lines 1 - 7 | |
use Doctrine\Common\Collections\Criteria; | |
// ... lines 9 - 14 | |
class Starship | |
{ | |
// ... lines 17 - 164 | |
public function getExpensiveParts(): Collection | |
{ | |
$criteria = Criteria::create()->andWhere(Criteria::expr()->gt('price', 50000)); | |
return $this->parts->matching($criteria); | |
} | |
// ... lines 171 - 192 | |
} |
Ahora bien, si me conoces, sabes que me gusta mantener mi lógica de consulta organizada en mis clases de repositorio. Pero ahora tenemos algo de lógica de consulta dentro de nuestra entidad. ¿Es eso malo? No necesariamente, pero me gusta mantener las cosas ordenadas. Así que traslademos esta lógica de Criteria
a nuestro repositorio.
Trasladar los criterios al repositorio
Vamos a StarshipPartRepository
. En cualquier lugar de aquí, añade una función estática pública: createExpensiveCriteria()
:
// ... lines 1 - 6 | |
use Doctrine\Common\Collections\Criteria; | |
// ... lines 8 - 12 | |
class StarshipPartRepository extends ServiceEntityRepository | |
{ | |
// ... lines 15 - 19 | |
public static function createExpensiveCriteria(): Criteria | |
{ | |
return Criteria::create()->andWhere(Criteria::expr()->gt('price', 50000)); | |
} | |
// ... lines 24 - 48 | |
} |
¿Por qué estática? Por dos razones: una, porque podemos (no estamos usando la variable this
en ningún sitio dentro), y dos, porque vamos a usar este método desde la entidad Starship
y no podemos autocablear servicios en entidades, así que debe ser estático.
De vuelta en Starship
, utiliza esto. Elimina todo el contenido de Criteria
y sustitúyelo por StarshipPartRepository::createExpensiveCriteria()
:
// ... lines 1 - 4 | |
use App\Repository\StarshipPartRepository; | |
// ... lines 6 - 15 | |
class Starship | |
{ | |
// ... lines 18 - 165 | |
public function getExpensiveParts(): Collection | |
{ | |
return $this->parts->matching(StarshipPartRepository::createExpensiveCriteria()); | |
} | |
// ... lines 170 - 191 | |
} |
Combinar criterios con constructores de consultas
Todo sigue funcionando a las mil maravillas, así que demos un paso más y flexionemos nuestros músculos de desarrolladores. Vamos a crear un método que combine Criteria
conQueryBuilder
s.
Digamos que queremos obtener una lista de todas las piezas caras de cualquier Starship
. Empieza copiando el método getExpensiveParts()
de Starship
. Pégalo en StarshipPartRepository
. A continuación, devuelve $this->createQueryBuilder('sp')
. Añade un argumento $limit
, por defecto 10. Para combinar esto con un Criteria
, di addCriteria(self::createExpensiveCriteria())
. Ahora que estamos en un QueryBuilder
, podemos hacer las cosas normales, como setMaxResults($limit)
. ¿Quieres hacer un orderBy
o un andWhere
? Adelante. Y por supuesto, puedes terminar esto congetQuery()->getResult()
:
// ... lines 1 - 13 | |
class StarshipPartRepository extends ServiceEntityRepository | |
{ | |
// ... lines 16 - 25 | |
/** | |
* @return Collection<StarshipPart> | |
*/ | |
public function getExpensiveParts(int $limit = 10): Collection | |
{ | |
return $this->createQueryBuilder('sp') | |
->addCriteria(self::createExpensiveCriteria()) | |
->setMaxResults($limit) | |
->getQuery() | |
->getResult(); | |
} | |
// ... lines 37 - 61 | |
} |
Combinar Criteria
con Query Builders es una jugada poderosa.
Muy bien, ya está bien. A continuación, crearemos una página completamente nueva para listar todas las piezas. ¡Vamos camino de necesitar algunos JOINs!