Adoptar las operaciones CRUD de Entity
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 Subscribe¡Hablemos de CRUD! No, no las cosas asquerosas, sino las operaciones de creación, lectura, actualización y eliminación que son la columna vertebral de la mayoría de las secciones de administración. En lugar de perder un tiempo precioso escribiendo controladores, formularios y plantillas manualmente, Symfony tiene un ayudante de confianza que se encarga de todo lo tedioso por nosotros. Es hora de recurrir a nuestro fiable aliado, el MakerBundle.
Ve al terminal y ejecuta:
symfony console make:crud
Una vez que lo hagas, MakerBundle entrará en acción y te hará algunas preguntas. Para la entidad, vamos a usar Starship. Para el nombre del controlador, ya tenemos un StarshipController, pero como estamos tratando con cosas de administradores, vamos a llamarlo StarshipAdminController. En cuanto a las pruebas unitarias de PHP, sáltatelas por ahora.
Pulsa intro y, ¡guau! Esta vez ha creado un montón de archivos. Un controlador, un tipo de formulario y un montón de plantillas para listar, mostrar, crear y editar la entidad Starship. Este es el tipo de boilerplate que preferirías no escribir a mano cada vez.
Arreglar el problema con los Enums en la página de lista
Ahora, echemos un vistazo al interior del recién estrenado controlador. En PhpStorm, navegaré hasta StarshipAdminController en nuestro directorio src/Controller/. Lo primero que quiero cambiar es la ruta. Maker eligió una ruta razonable, pero me gusta la coherencia entre mis rutas de administración, así que cambiaré la ruta a /admin/starship:
| // ... lines 1 - 13 | |
| ('/admin/starship') | |
| final class StarshipAdminController extends AbstractController | |
| { | |
| // ... lines 17 - 80 | |
| } |
¡Perfecto!
Abre esa URL /admin/starship en el navegador. Ah, ¡un error! Dice
No se ha podido convertir un objeto de la clase
StarshipStatusEnumen una cadena.
Problema clásico. Arreglémoslo abriendo la plantilla responsable de esta ruta: starship_admin/index.html.twig. Actualmente, intenta mostrar directamente el estado de la Nave Estelar:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <h1>Starship index</h1> | |
| <table class="table"> | |
| // ... lines 9 - 22 | |
| <tbody> | |
| {% for starship in starships %} | |
| <tr> | |
| // ... lines 26 - 29 | |
| <td>{{ starship.status }}</td> | |
| // ... lines 31 - 38 | |
| </tr> | |
| // ... lines 40 - 43 | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| // ... lines 47 - 48 | |
| {% endblock %} |
Pero no es una cadena:
Si abres la entidad Starship - verás que la propiedad de estado es un StarshipStatusEnum:
| // ... lines 1 - 14 | |
| #[ORM\Entity(repositoryClass: StarshipRepository::class)] | |
| class Starship | |
| { | |
| // ... lines 18 - 33 | |
| #[ORM\Column] | |
| private ?StarshipStatusEnum $status = null; | |
| // ... lines 36 - 263 | |
| } |
Tendremos que acceder explícitamente a su valor.
Aunque MakerBundle ha hecho mucho trabajo por nosotros, parece que aún no entiende del todo los enums de PHP. Pero no temas, lo tenemos. Todo lo que tenemos que hacer es sustituir starship.status por starship.status.value en la plantilla:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <h1>Starship index</h1> | |
| <table class="table"> | |
| // ... lines 9 - 22 | |
| <tbody> | |
| {% for starship in starships %} | |
| <tr> | |
| // ... lines 26 - 29 | |
| <td>{{ starship.status.value }}</td> | |
| // ... lines 31 - 38 | |
| </tr> | |
| // ... lines 40 - 43 | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| // ... lines 47 - 48 | |
| {% endblock %} |
Tras actualizar la página, tenemos una bonita lista de todas las naves estelares de nuestra base de datos, con algunas acciones útiles que podemos realizar sobre ellas, como mostrar y editar.
Arreglar los Enums en la página Mostrar
Haz clic en el enlace Mostrar y aparecerá el mismo error. Pero ahora que somos avezados solucionadores de problemas, busquemos el controlador y la acción responsables, y apliquemos la misma corrección.
La barra de herramientas de depuración web nos dice que StarshipAdminController::show() es el culpable. Encuentra ese método... salta a la plantilla show.html.twig, y actualiza el campo a starship.status.value:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <h1>Starship</h1> | |
| <table class="table"> | |
| <tbody> | |
| // ... lines 10 - 25 | |
| <tr> | |
| <th>Status</th> | |
| <td>{{ starship.status.value }}</td> | |
| </tr> | |
| // ... lines 30 - 45 | |
| </tbody> | |
| </table> | |
| // ... lines 48 - 53 | |
| {% endblock %} |
Ahora actualiza la página. ¡Genial! Ahora podemos ver los detalles individuales de la nave estelar y, lo que es más importante, editarlos o eliminarlos.
Si hago clic en el botón Eliminar - se activa un diálogo de confirmación de JavaScript. Un detalle pequeño pero significativo para evitar eliminaciones accidentales - una característica que aprecio mucho.
Si lo cancelo y hago clic en Editar, nos encontramos con otro error similar, pero esta vez lo lanza el tema de formulario predeterminado de Symfony:
Se ha lanzado una excepción durante la representación de la plantilla ("El objeto de clase
StarshipStatusEnumno se ha podido convertir a cadena") en `form_div_layout.html.twig.
Arreglar las páginas del formulario
Busca la acción edit() en el controlador y abre la plantilla relacionada:edit.html.twig. Ahora bien, este archivo no contiene lo que necesitamos, pero sí include() otra plantilla: _form.html.twig:
| // ... lines 1 - 4 | |
| {% block body %} | |
| // ... lines 6 - 7 | |
| {{ include('starship_admin/_form.html.twig', {'button_label': 'Update'}) }} | |
| // ... lines 9 - 12 | |
| {% endblock %} |
Continúa abriéndola.
Aquí es donde se renderiza el formulario. Si abres starship_admin/new.html.twigverás que estamos incluyendo el mismo formulario tanto para acciones nuevas como para acciones de edición. La única diferencia es el button_label que pasamos como argumento al include(). El propósito de esta plantilla es evitar la duplicación de código.
Volviendo a _form.html.twig, no estamos renderizando ese campo de estado manualmente... Todo el formulario se está renderizando con esta llamada a form_widget(form):
| {{ form_start(form) }} | |
| {{ form_widget(form) }} | |
| <button class="btn">{{ button_label|default('Save') }}</button> | |
| {{ form_end(form) }} |
Por suerte, esta corrección se realiza en la clase de tipo de formulario. MakerBundle creó StarshipTypepara nosotros - ábrelo en el directorio src/Form/. El campo status aquí es el culpable, parece que adivinar el tipo de campo de formulario no funciona para los enums:
| // ... lines 1 - 9 | |
| class StarshipType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| $builder | |
| // ... lines 15 - 17 | |
| ->add('status') | |
| // ... lines 19 - 24 | |
| ; | |
| } | |
| // ... lines 27 - 33 | |
| } |
No te preocupes, podemos especificar explícitamente el tipo. Pasa EnumType::class como 2º argumento y ve a actualizar la página:
| // ... lines 1 - 6 | |
| use Symfony\Component\Form\Extension\Core\Type\EnumType; | |
| // ... lines 8 - 10 | |
| class StarshipType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| $builder | |
| // ... lines 16 - 18 | |
| ->add('status', EnumType::class) | |
| // ... lines 20 - 25 | |
| ; | |
| } | |
| // ... lines 28 - 34 | |
| } |
Otro error:
Falta la opción requerida
classpara esteEnumType.
Los mensajes de error de Symfony son bastante útiles, así que puede que ya tengas una idea del problema y de cómo solucionarlo. Pero vamos a confirmarlo. En tu terminal, ejecuta un comando ya conocido:
symfony console debug:form EnumType
Te mostrará que la opción class es necesaria para este tipo de formulario, y debe apuntar a la clase concreta Enum. En nuestro caso, seríaStarshipStatusEnum.
Añade una matriz vacía como 3er argumento, y dentro, establece la opción class enStarshipStatusEnum::class:
| // ... lines 1 - 5 | |
| use App\Entity\StarshipStatusEnum; | |
| // ... lines 7 - 11 | |
| class StarshipType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| $builder | |
| // ... lines 17 - 19 | |
| ->add('status', EnumType::class, [ | |
| 'class' => StarshipStatusEnum::class, | |
| ]) | |
| // ... lines 23 - 28 | |
| ; | |
| } | |
| // ... lines 31 - 37 | |
| } |
Vuelve a actualizar la página... y... ¡genial! El formulario se muestra correctamente, podemos editar los detalles y actualizar la entidad. ¡Todo funciona como se esperaba!
En la página de la lista, desplázate hacia abajo y encontrarás un enlace "Crear nuevo". Al hacer clic en él, aparece el mismo formulario, pero sin datos rellenados de antemano: perfecto para crear una nave estelar nueva. Esta es una de las mejores partes de la generación CRUD de Maker: un formulario que se reutiliza tanto para operaciones de creación como de actualización.
Mejorar el estilo de las páginas CRUD
Vale, seamos sinceros. El código generado ahora funciona muy bien, pero visualmente no está ganando ningún premio de diseño. Voy a mejorar rápidamente algunos estilos, pero no te preocupes, puedes copiar/pegar el mismo código de los bloques de código que aparecen debajo del vídeo.
Primero, en _form.html.twig, pegaré algunas clases CSS de Tailwind en el botón de envío:
| {{ form_start(form) }} | |
| // ... line 2 | |
| <button class="text-white bg-green-700 hover:bg-green-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer">{{ button_label|default('Save') }}</button> | |
| {{ form_end(form) }} |
Esta plantilla _delete_form.html.twig es interesante:
| <form method="post" action="{{ path('app_starship_admin_delete', {'id': starship.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');"> | |
| <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ starship.id) }}"> | |
| <button class="btn">Delete</button> | |
| </form> |
Es un parcial Twig para el botón Eliminar. Nunca querrás que las acciones de eliminar sean simples enlaces, que utilicen el método HTTP GET. En su lugar, deben utilizar el método POST.
La única forma de conseguirlo con HTML puro es utilizar un formulario. Así que MakerBundle nos genera este pequeño formulario que contiene el botón Eliminar. Para mayor protección, también incluye un token CSRF. ¡Bastante elegante!
También pegaré aquí algunas clases CSS de Tailwind al botón Eliminar:
| <form method="post" action="{{ path('app_starship_admin_delete', {'id': starship.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');"> | |
| // ... line 2 | |
| <button class="text-white bg-red-700 hover:bg-red-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer">Delete</button> | |
| </form> |
A continuación, pegaré algo de CSS y HTML para mejorar el diseño de la plantilla de edición:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="px-5 max-w-7xl mx-auto mt-4"> | |
| <a class="hover:text-gray-400 underline" href="{{ path('app_starship_admin_index') }}"> << back to list</a> | |
| <h1 class="text-[32px] font-semibold border-b border-white/10 pb-5 mb-5 mt-8">Edit Starship</h1> | |
| {{ include('starship_admin/_form.html.twig', {'button_label': 'Update'}) }} | |
| <div class="flex justify-end"> | |
| {{ include('starship_admin/_delete_form.html.twig') }} | |
| </div> | |
| </div> | |
| {% endblock %} |
Plantilla de índice:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="max-w-7xl mx-auto px-5"> | |
| <div class="flex justify-between"> | |
| <h1 class="text-4xl font-semibold mb-3 my-6 pb-4">Starship index</h1> | |
| <a class="bg-blue-500 hover:bg-blue-600 text-white self-center py-2 px-2.5 rounded-xl" href="{{ path('app_starship_admin_new') }}">Create new</a> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="table-fixed border-collapse border border-gray-400"> | |
| <thead class="bg-gray-50 dark:bg-gray-700"> | |
| <tr> | |
| <th class="p-4 text-left font-semibold text-gray-200">Id</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Name</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Class</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Captain</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Status</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">ArrivedAt</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Slug</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Created</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Updated</th> | |
| <th class="p-4 text-left font-semibold text-gray-200">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for starship in starships %} | |
| <tr class="hover:bg-blue-500/5"> | |
| <td class="border-b p-4 pl-8 border-gray-700 text-gray-400">{{ starship.id }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.name }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.class }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.captain }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.status.value }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.arrivedAt ? starship.arrivedAt|date('Y-m-d H:i:s') : '' }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.slug }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.createdAt ? starship.createdAt|date('Y-m-d H:i:s') : '' }}</td> | |
| <td class="border-b p-4 border-gray-700 text-gray-400">{{ starship.updatedAt ? starship.updatedAt|date('Y-m-d H:i:s') : '' }}</td> | |
| <td class="border-b p-4 border-gray-700 text-white"> | |
| <div class="flex gap-2"> | |
| <a class="bg-green-600 hover:bg-green-800 rounded p-1.5" href="{{ path('app_starship_admin_show', {'id': starship.id}) }}">show</a> | |
| <a class="bg-red-500 hover:bg-red-600 rounded p-1.5" href="{{ path('app_starship_admin_edit', {'id': starship.id}) }}">edit</a> | |
| </div> | |
| </td> | |
| </tr> | |
| {% else %} | |
| <tr> | |
| <td colspan="10">no records found</td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {% endblock %} |
Nueva plantilla:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="px-5 max-w-7xl mx-auto mt-4"> | |
| <a class="hover:text-gray-400 underline" href="{{ path('app_starship_admin_index') }}"> << back to list</a> | |
| <h1 class="text-[32px] font-semibold border-b border-white/10 pb-5 mb-5 mt-8">Create new Starship</h1> | |
| {{ include('starship_admin/_form.html.twig') }} | |
| </div> | |
| {% endblock %} |
Y por último la plantilla espectáculo:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="max-w-7xl mx-auto px-5 mt-4"> | |
| <a class="hover:text-gray-400 underline" href="{{ path('app_starship_admin_index') }}"> << back to list</a> | |
| <h1 class="text-[32px] font-semibold border-b border-white/10 pb-5 mb-5 mt-8">{{ starship.name }}</h1> | |
| <table class="table-fixed"> | |
| <tbody> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">Id</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.id }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">Name</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.name }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">Class</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.class }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">Captain</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.captain }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">Status</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.status.value }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">ArrivedAt</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.arrivedAt ? starship.arrivedAt|date('Y-m-d H:i:s') : '' }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">Slug</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.slug }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">CreatedAt</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.createdAt ? starship.createdAt|date('Y-m-d H:i:s') : '' }}</td> | |
| </tr> | |
| <tr> | |
| <th class="text-xs text-slate-300 font-semibold mt-2 uppercase">UpdatedAt</th> | |
| <td class="text-[22px] font-semibold pl-5">{{ starship.updatedAt ? starship.updatedAt|date('Y-m-d H:i:s') : '' }}</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <div class="flex gap-3 mt-5"> | |
| <a class="text-white bg-green-700 hover:bg-green-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer" href="{{ path('app_starship_admin_edit', {'id': starship.id}) }}">Edit</a> | |
| {{ include('starship_admin/_delete_form.html.twig') }} | |
| </div> | |
| </div> | |
| {% endblock %} |
Una vez que hayamos terminado, vuelve al navegador y actualiza la página: ¡mucho mejor! Los nuevos estilos aportan un diseño más limpio y botones más intuitivos, incluido un botón "Crear nuevo" en la parte superior para facilitar el acceso.
Lo importante es que sólo hemos hecho retoques de estilo, la funcionalidad principal sigue estando generada al 100% por el MakerBundle. Te da un muy buen comienzo, pero puedes tomar el control sobre esto si quieres también retocando el StarshipAdminController generado. Un buen comienzo podría ser añadir mensajes flash. ¡Pero depende de ti!
Aplicando globalmente el tema del formulario Symfony
El único detalle que queda - el formulario Starship claramente no está utilizando la plantilla de formulario CSS de Tailwind. Podemos aplicarla en _form.html.twig, igual que hicimos para el formulario StarshipPart. Pero, ¡espera! Prefiero no repetir esto para cada formulario de mi aplicación.
En lugar de eso, apliquemos ese tema globalmente para todos los formularios de nuestra app. ¿Cómo? En el terminal, ejecuta:
symfony console config:dump twig
Y busca la clave form_themes en la salida, en algún lugar al principio ¡Aquí está! Está configurado con el tema de formulario predeterminado de Symfony, pero podemos anularlo en la configuración.
Abre new.html.twig para la StarshipPart y comenta la etiqueta del tema del formulario:
| // ... lines 1 - 2 | |
| {#{% form_theme form 'tailwind_2_layout.html.twig' %}#} | |
| // ... lines 4 - 37 |
Ya no la necesitaremos. A continuación, copia el nombre de la plantilla del tema y ve aconfig/packages/twig.yaml.
Debajo de la clave, añade esa opción form_themes, y debajo, añade -, pega:tailwind_2_layout.html.twig:
| twig: | |
| // ... line 2 | |
| form_themes: | |
| - tailwind_2_layout.html.twig | |
| // ... lines 5 - 9 |
¡Eso es todo! Ahora, todos los formularios utilizan automáticamente el tema CSS de Tailwind. Y sí, esta configuración es una lista, así que puedes añadir más temas aquí. Eso es útil para aplicar parches y personalizaciones al tema predeterminado del formulario. Pero por ahora, mantendré las cosas sencillas.
Vuelve al navegador para asegurarte de que el formulario se ha aplicado tanto en la página nueva como en la de edición, y comprueba que nuestro formulario StarshipPart también lo sigue utilizando. Sí, tiene un aspecto estupendo, sin regresión.
Y lo mejor de todo es que, aunque hayamos establecido ese tema globalmente, puedes anularlo aplicando otro tema directamente en la plantilla a un formulario específico, como hicimos al principio.
Para terminar
En un abrir y cerrar de ojos, tenemos un controlador rico en operaciones CRUD y una base sólida que podemos personalizar a nuestro antojo. MakerBundle se encarga de las cosas aburridas, permitiéndonos centrarnos en cosas increíbles.
Go Deeper!
Si quieres un generador de administración aún más potente para tu aplicación Symfony, con operaciones CRUD ya implementadas y otras funciones geniales, echa un vistazo al curso EasyAdminBundle.
A continuación, crearemos un nuevo tipo de formulario que no se asigna a ninguna entidad. Pero por ahora, ¡disfruta de tu CRUD recién generado y ve a añadir más naves estelares a tu flota!
2 Comments
Please! Please! Include the complete code files with each file you modify. Otherwise, it becomes difficult to track the changes.
Hey @giorgiocba ,
Yes, you're totally right! Code blocks have finally added, thanks for your patience!
Cheers!
"Houston: no signs of life"
Start the conversation!