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:
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
asset
en lugar de poner una ruta absoluta? Porque tal vez en producción sirvamos la aplicación en la rutablog
y 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 }
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
Nota
En producción muestra esta página:
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
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
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) }}
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 %}