Panel de control

Índice

¿Qué aprenderemos?

  1. A crear un panel de control para nuestras entidades
  2. A usar controladores embebidos
  3. A usar servicios

2.1 Panel de control

Vamos a crear un panel de control para nuestra aplicación.

Para ello usaremos el bundle EasyAdmin de Symfony.

1
composer require easycorp/easyadmin-bundle

Una vez instalado vamos a crear nuestro Dashboard

1
php bin/console make:admin:dashboard

Que creará una nueva ruta admin y un nuevo controlador DashboardController

Si visitamos la ruta http://127.0.0.1:8080/admin verás la página de inicio

image-20221027181654194

2.2 Team

Primero configuramos la conexión en el archivo .env

1
DATABASE_URL=mysql://root:sa@127.0.0.1:3306/tienda

Y creamos la base de datos:

1
php bin/console doctrine:database:create

Vamos a empezar con los miembros del equipo

image-20221027182231943

El primer paso es crear la entidad Team.

Y generar la migración:

1
2
php bin/console make:migration
php bin/console doctrine:migrations:migrate

Y luego generar el controlador CRUD

CRUD son las iniciales de Create, Read, Update y Delete que son las operaciones básicas que se realizan con una entidad

Y por último, modificar DashboardController para que cargue por defecto el panel de control de la entidad Team

1
2
3
4
5
6
#[Route('/admin', name: 'admin')]
public function index(): Response
{
$adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);

return $this->redirect($adminUrlGenerator->setController(TeamCrudController::class)->generateUrl());

image-20221027184941326

Ahora ya podemos añadir miembros al equipo.

image-20221027185039315

Pero claro, EasyAdmin no sabe que el campo Photo debe ser de tipo File

Así que configuramos TeamCrudController para modificar los campos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace App\Controller\Admin;

use App\Entity\Team;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;

class TeamCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Team::class;
    }

    public function configureFields(string $pageName): iterable
    {
        return [
            Field::new('name'),
            ImageField::new('photo')->setUploadDir('/public/img')->setBasePath('/img/'),
            Field::new('designation')
        ];
    }
}

Lo estamos configurando para que los campos name y designation sean por defecto y el campo photo sea de tipo ImageField y además configuramos el directorio de subida y cuál es la ruta de la carpeta.

Ahora ya podemos subir una foto:

image-20221027192221148

Ahora vamos a subir todas las imágenes del equipo a la base de datos mediante el siguiente sql:

1
2
3
4
5
6
INSERT INTO `team` (`id`, `name`, `photo`, `designation`) VALUES
(1, 'Team 1', '/team-1.jpg', 'Designation'),
(2, 'Team 2', '/team-2.jpg', 'Designation'),
(3, 'Team 3', '/team-3.jpg', 'Designation'),
(4, 'Team 4', '/team-4.jpg', 'Designation'),
(5, 'Team 5', '/team-5.jpg', 'Designation');

image-20221027192959150

Ya sólo nos queda hacer el partial para mostrar a los miembros del equipo. Pasamos todo el código html a una plantilla llamada partials/_team.html.twig y en la vista index.html.twig la incluimos.

Ya solo queda conectarla con la base de datos.

Primero modificamos el controlador de la ruta index

1
2
3
4
5
6
7
#[Route('/', name: 'index')]
public function index(ManagerRegistry $doctrine): Response
{
    $repository = $doctrine->getRepository(Team::class);
    $team = $repository->findAll();
    return $this->render('page/index.html.twig', compact('team'));
}

Y ahora modificamos _team.html.twig para recorrer los miembros del equipo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    <!-- Team Start -->
    <div class="container-fluid py-5">
        <div class="container">
            <div class="border-start border-5 border-primary ps-5 mb-5" style="max-width: 600px;">
                <h6 class="text-primary text-uppercase">Team Members</h6>
                <h1 class="display-5 text-uppercase mb-0">Qualified Pets Care Professionals</h1>
            </div>
            <div class="owl-carousel team-carousel position-relative" style="padding-right: 25px;">
             {% for teamMember in team %}
                <div class="team-item">
                    <div class="position-relative overflow-hidden">
                        <img class="img-fluid w-100" src="{{ asset('img/' ~ teamMember.photo) }}" alt="">
                        <div class="team-overlay">
                            <div class="d-flex align-items-center justify-content-start">
                                <a class="btn btn-light btn-square mx-1" href="#"><i class="bi bi-twitter"></i></a>
                                <a class="btn btn-light btn-square mx-1" href="#"><i class="bi bi-facebook"></i></a>
                                <a class="btn btn-light btn-square mx-1" href="#"><i class="bi bi-linkedin"></i></a>
                            </div>
                        </div>
                    </div>
                    <div class="bg-light text-center p-4">
                        <h5 class="text-uppercase">{{ teamMember.name }}</h5>
                        <p class="m-0">{{ teamMember.designation }}</p>
                    </div>
                </div>
            {% endfor %}
            </div>
        </div>
    </div>
    <!-- Team End -->

Resulta que las fotos del equipo también aparecen en la ruta about por lo que hay que proceder de la misma forma: 1.- modificar el controlador, 2.- modificar la plantilla about.html.twig

1
2
3
4
5
6
7
#[Route('/about', name: 'about')]
public function about(ManagerRegistry $doctrine): Response
{
    $repository = $doctrine->getRepository(Team::class);
    $team = $repository->findAll();
    return $this->render('page/about.html.twig', compact('team'));
}

Pero como veis es el mismo código que en el controlador index y hemos de intentar siempre no repetirnos (DRY Don’t Repeat Yourself). Así que vamos a usar Controladores Embebidos. Creamos un método que obtenga los miembros del equipo y que además renderiza la plantilla:

1
2
3
4
5
6
public function teamTemplate(ManagerRegistry $doctrine): Response
{
    $repository = $doctrine->getRepository(Team::class);
    $team = $repository->findAll();
    return $this->render('partials/_team.html.twig',compact('team'));
}

Modificamos las plantillas index.html.twig y about.html.twig, cambiando

1
{{ include ('partials/_team.html.twig')}}

por

1
{{ render(controller('App\\Controller\\PageController::teamTemplate')) }}

Y ya podemos eliminar el código duplicado

1
2
3
4
5
6
7
8
9
10
11
#[Route('/', name: 'index')]
public function index(ManagerRegistry $doctrine): Response
{
    return $this->render('page/index.html.twig');
}

#[Route('/about', name: 'about')]
public function about(ManagerRegistry $doctrine): Response
{
    return $this->render('page/about.html.twig');
}

Procedemos de la misma manera para el controlador team

2.3 Productos

Reto

Crea el panel de control para administrar los productos. La entidad Product debe tener los atributos name, photo y price, este último de tipo float

Una vez creada la entidad, carga el siguiente SQL con los productos:

1
2
3
4
5
6
INSERT INTO `product` (`id`, `name`, `photo`, `price`) VALUES
(1, 'Producto 1', 'product-1.png', 12.45),
(2, 'Producto 2', 'product-2.png', 100),
(3, 'Producto 3', 'product-3.png', 20),
(4, 'Producto 4', 'product-4.png', 50),
(5, 'Producto 5', 'product-3.png', 34);

Y ahora crea el partial para los productos que incluirás en la ruta product e index

image-20221101195632637

En este caso vemos que también hemos de cargar los productos en dos rutas. Pero esta vez vamos a crear un servicio:

Your application is full of useful objects: a “Mailer” object might help you send emails while another object might help you save things to the database. Almost everything that your app “does” is actually done by one of these objects. And each time you install a new bundle, you get access to even more!

In Symfony, these useful objects are called services and each service lives inside a very special object called the service container. The container allows you to centralize the way objects are constructed. It makes your life easier, promotes a strong architecture and is super fast!

Un servicio es una forma de compartir código entre varias partes de la aplicación. Para usarlo sólo hay que hacer type-hinting. Vamos a verlo:

  1. Creamos el servicio en App\Services\ProductsService

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    <?php
    namespace App\Service;
       
    use App\Entity\Product;
    use Doctrine\Persistence\ManagerRegistry;
       
    class ProductsService{
        private $doctrine;
       
        public function __construct(ManagerRegistry $doctrine)
        {
            //Como hace falta acceder a ManagerRegistry lo inyectamos en el constructor
            $this->doctrine = $doctrine;
        }
        public function getProducts(): ?array{
            $repository = $this->doctrine->getRepository(Product::class);
            return $repository->findAll();
        }
    }
    

    Aquí estamos usando una característica llamada inyección de dependencias ya que Symfony se encarga automáticamente de pasarnos una referencia a la clase ManagerRegistry

  2. Y ahora lo inyectamos en todos aquellos métodos en que queramos usarlo.

    1
    2
    3
    4
    5
    6
    
    #[Route('/product', name: 'product')]
    public function product(ProductsService $productsService): Response
    {
        $products = $productsService->getProducts();
        return $this->render('product/product.html.twig', compact('products'));
    }
    

Reto

Haz lo mismo para la ruta /