
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
assetde 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:

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:

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
asseten lugar de poner una ruta absoluta? Porque tal vez en producción sirvamos la aplicación en la rutablogy entonces ya no nos funcionaría esta estrategia. Sin embargo, al usarasset, 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

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 }
También podemos bloquear acceso a todas los rutas de un controlador, anteponiendo la siguiente regla antes de la definición del mismo:
1
2
#[IsGranted('ROLE_ADMIN)]
final class PageController extends AbstractController
Como todos los archivos
yamles 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 alguna ruta de un controlador, usaríamos el siguiente código delante de cada una de ellos:
1
2
3
4
5
6
7
8
9
10
```php
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

Nota
En producción muestra esta página:
Para comprobarlo, modifica en archivo
.envy 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
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

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

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:

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
Antes de nada, hay que instalar un componte que permite inspeccionar de qué mime type es un archivo
1
composer require symfony/mime
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
46
47
#[Route('/admin/images', name: 'app_images')]
public function images(ManagerRegistry $doctrine, Request $request, SluggerInterface $slugger): Response
{
$image = new Image();
$form = $this->createForm(ImageFormType::class, $image);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
if ($file) {
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
// El Slugger hace que el nombre del archivo sea seguro en cuanto a
// caracteres especiales como espacios o acentos
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();
// El servidor almacena el archivo en un directorio temporal y
// debemos moverlo a su ubicación definitiva, dentro de una ruta que
// hemos definido en los parámetros de configuración (services.yaml)
// y que debe existir previamente dentro de la carpeta `public` proyecto
try {
// Primero lo movemos al directorio de imágenes
$file->move(
$this->getParameter('images_directory'), $newFilename
);
$filesystem = new Filesystem();
// Y ahora lo duplicamos en el directorio de portfolio
$filesystem->copy(
$this->getParameter('images_directory') . '/'. $newFilename,
$this->getParameter('portfolio_directory') . '/'. $newFilename, true);
} catch (FileException $e) {
return new Response("Error al subir el archivo: " . $e->getMessage());
}
// asignamos el nombre del archivo, que se llama `file`, a la entidad Image
$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()
));
}
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) }}

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.

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">»</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 %}
Para comprobarlo, modifica en archivo