Zona de administración

Índice

4 Qué aprenderemos

  • A dar permisos de administrador a un usuario
  • A bloquear el acceso a determinadas páginas de nuestra web
  • A usar la función asset de twig para hacer nuestros assets accesibles independientemente de la url de nuestra aplicación.
  • A usar métodos del repositorio
  • A crear relaciones entre entidades
  • A subir imágenes al servidor
  • A mostrar imágenes

4.1 Usuario administrador

En el punto 3, vimos cómo gestionar usuarios. Hemos dado de alta algunos usuarios pero ninguno tiene el rol administrador. Vamos a dar este rol al usuario con id 1.

En pypMyAdmin ejecuta el siguiente sql:

1
UPDATE user SET roles = '["ROLE_ADMIN"]' WHERE id = 1;

Ahora comprueba que, efectivamente, posees ese rol. Para ello, inicia sesión con ese usuario y comprueba la barra del profiler de Symfony:

image-20220318091014699

4.2 Creación de la parte admin

Vamos a crear una ruta para poder añadir imágenes a la página de portada.

Creamos un nuevo controlador AdminController y la ruta /admin/images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class AdminController extends AbstractController
{
    #[Route('/admin/images', name: 'app_images')]
    public function images(): Response
    {
        return $this->render('admin/images.html.twig', []);
    }
}

La plantilla la renombramos a images.html.twig y modificamos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% extends 'base.html.twig' %}
{% block title%}Images{% endblock %}
{% block body%}
<!-- Principal Content Start -->
   <div id="images">
   	  <div class="container">
   	    <div class="col-xs-12 col-sm-8 col-sm-push-2">
       	   <h1>Images</h1>
       	   <hr>
       	</div>   
   	  </div>
   </div>
<!-- Principal Content Start -->
{% endblock %}

visitamos http://127.0.0.1:8080/admin/images y comprobamos que ha perdido los estilos:

image-20220318092357644

Esto ocurre porque la plantilla base.html.twig tiene las rutas a los assets (los assets son todos los archivos estáticos de la aplicación, como imágenes, hojas de estilos y scripts) de forma relativa:

1
<link rel="stylesheet" type="text/css" href="bootstrap/css/bootstrap.min.css">

Y cuando estamos en una ruta interna como /admin/images/ evidentemente no encuentra los estilos porque los busca en /admin/images/bootstrap/css/bootstrap.min.css. Solucionarlo es tan sencillo como hacer las rutas absolutas o, mejor aún, utilizar la función asset de twig:

1
<link rel="stylesheet" type="text/css" href="{{ asset('bootstrap/css/bootstrap.min.css') }}">

¿Por qué usamos asset en lugar de poner una ruta absoluta? Porque tal vez en producción sirvamos la aplicación en la ruta blog y entonces ya no nos funcionaría esta estrategia. Sin embargo, al usar asset, en producción se transformaría en /blog/bootstrap/css/bootstrap.min.css

Ahora cambia todas las rutas a los archivos css y javascript para incluir la función asset

image-20220318093746525

4.3 Bloquear el acceso a la parte de administración

Bloquear el acceso a toda una zona de nuestra web es tan sencillo como modificar el archivo config/packages/security.yml

1
2
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

Como todos los archivos yaml es muy sensible a errores. Si Symfony empieza a dar errores extraños después de modificar un archivo de este tipo, revísalo

Si lo que necesitamos es proteger el acceso a un controlador, usaríamos el siguiente código:

1
2
3
4
5
6
7
8
9
public function adminDashboard(): Response
{
    $this->denyAccessUnlessGranted('ROLE_ADMIN');

    // or add an optional message - seen by developers
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN');
    
    new Response("Sí que puedes entrar");
}

Si el usuario no ha iniciado sesión se redirige automáticamente a la página de login. Pero si sí lo está pero no tiene el rol ADMIN Symfony muestra una página de error

image-20220318095713134

Nota

En producción muestra esta página:

image-20220318100547163 Para comprobarlo, modifica en archivo .env y fija el entorno a producción

1
APP_ENV=prod

4.4 Gestión de imágenes

En este apartado vamos a implementar la subida de imágenes para la página de portada.

4.4.1 Entidad Category

Las imágenes pertenecen a una categoría (en la página de portada existen tres pestañas de categorías). Así que el primer paso será crear la entidad Category

Y realizamos la migración:

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

Y ahora creamos el formulario

1
php bin/console make:form CategoryForm Category

En el controlador AdminController creamos la siguiente ruta

1
2
3
4
5
#[Route('/admin/categories', name: 'app_categories')]
public function categories(): Response
{
    return $this->render('admin/categories.html.twig', []);
}

Y la plantilla admin/categories.html.twig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% extends 'base.html.twig' %}
{% block title%}Categories{% endblock %}
{% block body%}
<!-- Principal Content Start -->
   <div id="categories">
   	  <div class="container">
   	    <div class="col-xs-12 col-sm-8 col-sm-push-2">
       	   <h1>Categories</h1>
       	   <hr class='divider'>
       	</div>   
   	  </div>
   </div>
<!-- Principal Content Start -->
{% endblock %}

Ya podemos visitar la ruta /admin/categories

4.4.2 Formulario Category

Creamos el formulario para Categorías tal y como hicimos en el Formulario de Contacto

image-20220318113520367 Controlador

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[Route('/admin/categories', name: 'app_categories')]
public function categories(ManagerRegistry $doctrine, Request $request): Response
{
    $category = new Category();
    $form = $this->createForm(CategoryFormType::class, $category);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $category = $form->getData();    
        $entityManager = $doctrine->getManager();    
        $entityManager->persist($category);
        $entityManager->flush();
    }
    return $this->render('admin/categories.html.twig', array(
        'form' => $form->createView() 
    ));
}

Plantilla

image-20220322203344623

4.4.3 Listado de categorías

También vamos a mostrar una lista con todas las categorías:

image-20220318114421816

Modificamos el controlador para recuperar todas las categorías de la base de datos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[Route('/admin/categories', name: 'app_categories')]
public function categories(ManagerRegistry $doctrine, Request $request): Response
{
    $repositorio = $doctrine->getRepository(Category::class);

    $categories = $repositorio->findAll();

    $category = new Category();
    $form = $this->createForm(CategoryFormType::class, $category);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $category = $form->getData();    
        $entityManager = $doctrine->getManager();    
        $entityManager->persist($category);
        $entityManager->flush();
    }
    return $this->render('admin/categories.html.twig', array(
        'form' => $form->createView(),
        'categories' => $categories   
    ));

}

Y modificamos la plantilla para recorrer las categorías:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<hr class="divider">
<div>
	<table class="table">
		<thead>
		<tr>
			<th scope="col">#</th>
			<th scope="col">Name</th>
		</tr>
		</thead>
		<tbody>
		{% for category in categories %}
		<tr>
			<th scope="row">{{ category.id }}</th>
			<td>
				{{ category.name }}
			</td>
		</tr>
		{% endfor %}
		</tbody>
	</table>
</div>

4.4.4 Entidad Image

Como antes, vamos a crear la entidad Image que tiene una clave ajena a Category. Por ello, cuando definamos la entidad hemos de indicar que el campo Category es una Relation de tipo ManyToOne

Y realizamos la migración:

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

Al igual que hicimos con el formulario de contacto, hemos de crear el formulario y la plantilla:

En este caso hay dos peculiaridades:

  • La imagen tiene una clave ajena con la entidad Category
  • Queremos subir una imagen y almacenar la ruta a la misma en la base de datos

En primer lugar creamos el formulario:

1
php bin/console make:form ImageForm Image

En este formulario hacemos que el campo category obtenga los datos de la entidad Category

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
...
public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
        ->add('file')
        ->add('numLikes')
        ->add('numViews')
        ->add('numDownloads')
        ->add('category', EntityType::class, array(
            'class' => Category::class,
            'choice_label' => 'name'))
    ;
}

Y la plantilla:

1
2
3
4
5
6
7
8
9
<div id="images">
    <div class="container">
        <div class="col-xs-12 col-sm-8 col-sm-push-2">
            <h1>Images</h1>
            {{ form(form, {'attr': {'class':'form-horizontal'}}) }}
            <hr class="divider">
        </div>   
    </div>
</div>

Creamos el controlador:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[Route('/admin/images', name: 'app_images')]
public function images(ManagerRegistry $doctrine, Request $request): Response
{
    $image = new Image();
    $form = $this->createForm(ImageFormType::class, $image);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $image = $form->getData();    
        $entityManager = $doctrine->getManager();    
        $entityManager->persist($image);
        $entityManager->flush();
    }
    return $this->render('admin/images.html.twig', array(
        'form' => $form->createView()
    ));
}

Vamos a probar que funciona:

image-20220322174001982

Ahora vamos a añadir clases a los campos para que tenga el mismo aspecto visual que el resto de la aplicación, pero esta vez lo haremos en el propio formulario, que es otra de las opciones de las que disponemos para configurar los campos. Se puede usar cualquiera de ellas.

1
2
3
4
5
6
7
8
9
10
11
12
13
public function buildForm(FormBuilderInterface $builder, array $options): void
{
    $builder
        ->add('file')
        ->add('numLikes', null, ['attr' => ['class'=>'form-control']])
        ->add('numViews', null, ['attr' => ['class'=>'form-control']])
        ->add('numDownloads', null, ['attr' => ['class'=>'form-control']])
        ->add('category', EntityType::class, array(
            'class' => Category::class,
            'choice_label' => 'name'))
        ->add('Send', SubmitType::class, ['attr' => ['class'=>'pull-right btn btn-lg sr-button']]);
    ;
}

4.4.5 Subir imágenes al servidor

El truco para que el campo file sea de tipo input file es decirle a Symfony que no esté mapeado de tal forma que no lo guarde automáticamente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
...
$builder
    ->add('file', FileType::class,[
        'mapped' => false,
        'constraints' => [
            new File([
                'mimeTypes' => [
                    'image/jpeg',
                    'image/png',
                ],
                'mimeTypesMessage' => 'Please upload a valid image file',
            ])
        ],
    ])

Ahora ya sólo nos queda modificar el controlador:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
...
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;
...

...
if ($form->isSubmitted() && $form->isValid()) {
    $file = $form->get('file')->getData();
    if ($file) {
        $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        // this is needed to safely include the file name as part of the URL
        $safeFilename = $slugger->slug($originalFilename);
        $newFilename = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();

        // Move the file to the directory where images are stored
        try {

            $file->move(
                $this->getParameter('images_directory'), $newFilename
            );
            $filesystem = new Filesystem();
            $filesystem->copy(
                $this->getParameter('images_directory') . '/'. $newFilename, 
                $this->getParameter('portfolio_directory') . '/'.  $newFilename, true);

        } catch (FileException $e) {
            // ... handle exception if something happens during file upload
        }

        // updates the 'file$filename' property to store the PDF file name
        // instead of its contents
        $image->setFile($newFilename);
    }
    $image = $form->getData();   
    $entityManager = $doctrine->getManager();    
    $entityManager->persist($image);
    $entityManager->flush();
}
return $this->render('admin/images.html.twig', array(
            'form' => $form->createView(),
            'images' => $images   
        ));

Y definir la ruta a las imágenes en config/services.yml

1
2
3
parameters:
    images_directory: '%kernel.project_dir%/public/images/index/gallery'
    portfolio_directory: '%kernel.project_dir%/public/images/index/portfolio'

4.4.6 RETO

Crea la lista con las imágenes:

Para obtener el nombre de la imagen usa:

{{ asset('images/index/gallery/' ~ image.file) }}

image-20220322195732035

4.5 Página de portada.

Ahora ya podemos modificar la página de portada para mostrar las imágenes que se van subiendo.

Antes de nada, carga este archivo sql que contiene las instrucciones para insertar las imágenes ya existentes.

Ahora modificamos el controlador para obtener todas las categorías:

1
2
3
4
5
6
7
8
9
#[Route('/', name: 'index')]
public function index(ManagerRegistry $doctrine, Request $request): Response
{
    $repository = $doctrine->getRepository(Category::class);

    $categories = $repository->findAll();

    return $this->render('page/index.html.twig', ['categories' => $categories]);
}

Y modificar la plantilla index.html.twig. Debes eliminar todo el HTML que pinta las pestañas de las categorías y sustituirlo por la siguiente plantilla twig.

1
2
3
4
5
6
7
8
9
10
11
12
<div class="table-responsive">
  <table class="table text-center">
    <thead>
      <tr>
      {% for category in categories %}
        <td><a class="link {{loop.first ? 'active' : ''}}" href="#category{{category.id}}" data-toggle="tab">{{category.name}}</a></td>
      {% endfor %}
      </tr>
    </thead>
  </table>
  <hr>
</div>

En esta plantilla usamos loop.first para poner la clase active a la primera categoría.

image-20220322185956882

Eliminamos el siguiente HTML porque de momento no vamos a paginar las imágenes:

1
2
3
4
5
6
7
8
9
10
<nav class="text-center">
    <ul class="pagination">
        <li class="active"><a href="#">1</a></li>
        <li><a href="#">2</a></li>
        <li><a href="#">3</a></li>
        <li><a href="#" aria-label="suivant">
        <span aria-hidden="true">&raquo;</span>
        </a></li>
    </ul>
</nav>

Y ahora vamos a recorrer todas las imágenes.

Primero eliminamos todo el código repetido que pinta las tres pestañas de categorías y creamos un partial para la imagen partials/_image.html.twig:

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
31
32
33
34
35
36
37
38
<div class="col-xs-12 col-sm-6 col-md-3">
<div class="sol">
  <img class="img-responsive" src="{{ asset('images/index/portfolio/' ~ image.file) }}" alt="First category picture">
  <div class="behind">
      <div class="head text-center">
        <ul class="list-inline">
          <li>
            <a class="gallery" href="{{ asset('images/index/gallery/' ~ image.file) }}" data-toggle="tooltip" data-original-title="Quick View">
              <i class="fa fa-eye"></i>
            </a>
          </li>
          <li>
            <a href="#" data-toggle="tooltip" data-original-title="Click if you like it">
              <i class="fa fa-heart"></i>
            </a>
          </li>
          <li>
            <a href="#" data-toggle="tooltip" data-original-title="Download">
              <i class="fa fa-download"></i>
            </a>
          </li>
          <li>
            <a href="#" data-toggle="tooltip" data-original-title="More information">
              <i class="fa fa-info"></i>
            </a>
          </li>
        </ul>
      </div>
      <div class="row box-content">
        <ul class="list-inline text-center">
          <li><i class="fa fa-eye"></i> {{ image.numViews }}</li>
          <li><i class="fa fa-heart"></i> {{ image.numLikes }}</li>
          <li><i class="fa fa-download"></i> {{ image.numDownloads }}</li>
        </ul>
      </div>
  </div>
</div>
</div> 

Y modificamos index.html.twig para mostrar las tres pestañas:

1
2
3
4
5
6
7
8
9
10
<div class="tab-content">
  {% for category in categories %}
    <div id="category{{category.id}}" class="tab-pane {{loop.first ? 'active' : ''}}" >
      <div class="row popup-gallery">
      {% for image in category.images %}
      {{ include ('partials/_image.html.twig')}}
      {% endfor %}
        </div>
    </div>
{% endfor %}