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 Subscribe¡Añadamos más cosas a esta página! ¿Qué tal el número medio de galletas de la suerte impresas para esta categoría? Para ello, vuelve a nuestra consulta: vive en countNumberPrintedForCategory()
.
... lines 1 - 17 | |
class FortuneCookieRepository extends ServiceEntityRepository | |
{ | |
... lines 20 - 24 | |
public function countNumberPrintedForCategory(Category $category): int | |
{ | |
$result = $this->createQueryBuilder('fortuneCookie') | |
->select('SUM(fortuneCookie.numberPrinted) AS fortunesPrinted') | |
->andWhere('fortuneCookie.category = :category') | |
->setParameter('category', $category) | |
->getQuery() | |
->getSingleScalarResult(); | |
return (int) $result; | |
} | |
... lines 36 - 78 | |
} |
Para obtener la media, podemos añadir una coma y utilizar la función AVG()
. O podemos utilizar addSelect()
... que me parece un poco mejor. Queremos el AVG()
defortuneCookie.numberPrinted
aliasado a fortunesAverage
.
Esta vez no he utilizado la palabra AS
... sólo para demostrar que la palabraAS
es opcional. De hecho, toda la parte fortunesAverage
o AS fortunesPrinted
es opcional. Pero dándole un nombre a cada una, podemos controlar las claves de la matriz de resultados final, que veremos en un minuto.
... lines 1 - 24 | |
public function countNumberPrintedForCategory(Category $category): int | |
{ | |
$result = $this->createQueryBuilder('fortuneCookie') | |
... line 28 | |
->addSelect('AVG(fortuneCookie.numberPrinted) fortunesAverage') | |
... lines 30 - 35 | |
} | |
... lines 37 - 81 |
Ya que estamos aquí, en lugar de imprimir el nombre del objeto $category
, veamos si podemos obtener el nombre de la categoría dentro de esta consulta. Diré->addSelect('category.name')
.
Si ves un problema con esto, ¡tienes razón! Pero ignorémoslo y avancemos a ciegas! dd($result)
al final.
... lines 1 - 24 | |
public function countNumberPrintedForCategory(Category $category): int | |
{ | |
$result = $this->createQueryBuilder('fortuneCookie') | |
... lines 28 - 29 | |
->addSelect('category.name') | |
... lines 31 - 34 | |
dd($result); | |
... lines 36 - 37 | |
} | |
... lines 39 - 83 |
Antes, esto sólo devolvía el entero fortunesPrinted
. Pero ahora, estamos seleccionando tres cosas. ¿Qué nos devolverá ahora?
La respuesta es... ¡un error gigantesco!
'categoría' no está definida.
Sí, he hecho referencia a category
... pero nunca nos hemos unido a ella. Añadámoslo. Estamos consultando desde la entidad FortuneCookie
, y ésta tiene una propiedad category
, que es un ManyToOne
. Así que nos estamos uniendo a un objeto. Hazlo con ->innerJoin()
pasando a fortuneCookie.category
y dándole el aliascategory
.
... lines 1 - 24 | |
public function countNumberPrintedForCategory(Category $category): int | |
{ | |
$result = $this->createQueryBuilder('fortuneCookie') | |
... lines 28 - 30 | |
->innerJoin('fortuneCookie.category', 'category') | |
... lines 32 - 38 | |
} | |
... lines 40 - 84 |
Si ahora vamos a actualizar la página... éste es el error que esperaba:
La consulta devolvió una fila que contenía varias columnas.
Este ->getSingleScalarResult()
es perfecto cuando devuelves una sola fila y una sola columna. En cuanto devuelvas varias columnas,->getSingleScalarResult()
no funcionará. Para solucionarlo, cambia a ->getSingleResult()
.
... lines 1 - 24 | |
public function countNumberPrintedForCategory(Category $category): array | |
{ | |
$result = $this->createQueryBuilder('fortuneCookie') | |
... lines 28 - 34 | |
->getSingleResult(); | |
... lines 36 - 38 | |
} | |
... lines 40 - 84 |
Esto básicamente dice
Dame la única fila de datos de la base de datos.
Inténtalo de nuevo. ¡Eso es lo que queremos! ¡Devuelve exactamente las tres columnas que hemos seleccionado!
Y ahora... tenemos que cambiar un poco este método. Actualiza el retorno int
a un array
... y, aquí abajo, quita el (int)
por completo y devuelve $result
. También podemos quitar el dd()
... y podrías poner el return
aquí arriba si quisieras.
... lines 1 - 24 | |
public function countNumberPrintedForCategory(Category $category): array | |
{ | |
... lines 27 - 36 | |
return $result; | |
} | |
... lines 39 - 83 |
¡Nuestro método está listo! Ahora vamos a arreglar el controlador. Este$fortunesPrinted
ya no está bien. Cámbialo por $result
en su lugar. Luego... léelo abajo con - $result['fortunesPrinted']
. Copia eso, pégalo, y envía una variable fortunesAverage
a la plantilla ajustada a la clave fortunesAverage
. Pasa también categoryName
ajustada a $result['name']
.
... lines 1 - 12 | |
class FortuneController extends AbstractController | |
{ | |
... lines 15 - 30 | |
public function showCategory(int $id, CategoryRepository $categoryRepository, FortuneCookieRepository $fortuneCookieRepository): Response | |
{ | |
... lines 33 - 38 | |
return $this->render('fortune/showCategory.html.twig',[ | |
'category' => $category, | |
'fortunesPrinted' => $result['fortunesPrinted'], | |
'fortunesAverage' => $result['fortunesAverage'], | |
'categoryName' => $result['name'], | |
]); | |
} | |
} |
¡Hora de la plantilla! En showCategory.html.twig
, tenemos acceso a todo el objeto$category
... que es como estamos imprimiendo category.name
. Pero ahora, también tenemos una variable categoryName
. Sustituye category.name
por categoryName
.
... lines 1 - 2 | |
{% block body %} | |
... lines 4 - 5 | |
<h1 class="text-3xl p-5 text-center my-4 font-semibold"><span class="fa {{ category.iconKey }}"></span> {{ categoryName }} Fortunes</h1> | |
... lines 7 - 38 | |
{% endblock %} |
No hay... ninguna razón real para hacerlo: sólo estoy demostrando que podemos obtener datos adicionales en nuestra nueva consulta. Aunque, si también hubiéramos seleccionado iconKey
, entonces podríamos evitar por completo la consulta del objeto Category
. Sin embargo, aunque eso podría hacer que nuestra página fuera un poco más rápida, es casi definitivamente una exageración y hace que nuestro código sea más confuso. ¡Utilizar objetos es lo mejor!
Vale, a continuación, para el "Historial de impresión", pulsa "enter" y añade{{ fortunesAverage|number_format }}
y luego average
.
... lines 1 - 2 | |
{% block body %} | |
... lines 4 - 9 | |
<thead class="bg-slate-500 text-white"> | |
... lines 11 - 14 | |
<th class="border p-4"> | |
Print History ({{ fortunesPrinted|number_format }} total, {{ fortunesAverage|number_format }} average) | |
</th> | |
... line 18 | |
</thead> | |
... lines 20 - 38 | |
{% endblock %} |
Fantástico. ¡Inténtalo de nuevo! Si no me he equivocado... ¡ya está! ¡Todo funciona! Tenemos dos consultas: una para el category
que está unido afortune_cookies
y la que acabamos de hacer que coge el SUM
, AVG
, y el name
también con un JOIN
. ¡Me encanta!
Recibir objetos de entidad completos de Doctrine es la situación ideal porque... es muy agradable trabajar con objetos. Pero al fin y al cabo, si necesitas consultar datos o columnas específicos, puedes hacerlo perfectamente. Y como acabamos de ver, Doctrine devolverá una matriz asociativa.
Sin embargo, podemos ir un paso más allá y pedirle a Doctrine que nos devuelva esos datos concretos dentro de un objeto. Hablemos de ello a continuación.
// 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
}
}
I had an error and had to add
->groupBy('category.name')
to make it work.