La extructura de las directorios
Instalamos composer
composer install
Generamos nuestro propio archivo.env.local y creamos la base de datos
php bin/console doctrine:database:create
Ejecutamos todos los fciheros de migracion que tengamos en nuestro sistema
php bin/console doctrine:schema:create
Para crear un nuevo proyecto
symfony new curso_principiante --webapp
Una vez instalado accedemos al directorio que nos ha generado, e instalamos maker-bundle que nos permite generar codigo
composer require symfony/maker-bundle --dev
Instalamos el siguiente bundle, que nos permite utilizar las anotaciones
composer require doctrine/annotations
Para iniciar el servidor de symfony
symfony server:start
Para acceder al servidor nos dirigimos al navegador a traves de la direccion que nos genera http://127.0.0.1:8000
Al crear una entidad nos pedira el nombre de la entidad, sus propiedades, tipo, tamaño y si puede ser nulo
php bin/console make:entity
Simpre que se cree o se modifique una entidad se debe realizar una migracion
php bin/console make:migration
php bin/console doctrine:migrations:migrate
Deberemos modificar el fichero .env, para añadir usuario, contraseña y el nombre de la base de datos quedando de la sigueinte forma
DATABASE_URL="mysql://root:@127.0.0.1:3306/curso_principiante?serverVersion=8&charset=utf8mb4"
Para crear la base de datos no dirigimos a al simbolo del sistema y ejecutamos el siguiente comando
php bin/console doctrine:database:create
Una vez creada la base de datos subimos las entidades que tengamos
php bin/console doctrine:schema:create
Para eliminar una base de datos
php bin/console doctrine:schema:drop
Para generar un controlador. Nos pedira el nombre del controlador y nos genera el controlador y la vista
php bin/console make:controller
La creacion de un crud nos crea tanto los contraladores como las vistas
php bin/console make:crud nombre_entidad
Creamos la entidad usuario
php bin/console make:user
Generamos el crud de la entidad usuario
php bin/console make:crud User
Debemos modificar el fichero UserType la parte de roles, ya que si no nos dara un fallo de conversion, la clase quedara de la siguiente forma
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\CallbackTransformer;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('username')
->add('Roles', ChoiceType::class, [
'required' => true,
'multiple' => false,
'expanded' => false,
'choices' => [
'User' => 'ROLE_USER',
'Admin' => 'ROLE_ADMIN',
],
])
->add('password');
// Data transformer
$builder->get('Roles')
->addModelTransformer(new CallbackTransformer(
function ($rolesArray) {
// transform the array to a string
return count($rolesArray)? $rolesArray[0]: null;
},
function ($rolesString) {
// transform the string back to an array
return [$rolesString];
}
));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
Para seleccionar el uusario actual desde el controlador de la entidad donde queremos que salga el usuario, por ejemplo al crear una nueva tarea para que dicha tarea tenga al usuario por defecto que la crea, en la funcion de new_tarea añadimos esta linea
$tarea -> setUsuario($security->getUser());
Para generar el registro de usuarios con su correpondiente vista
php bin/console make:registration-form
Si queremos personalizar el registro
{% extends 'base.html.twig' %}
{% block title %}Registro{% endblock %}
{% block body %}
<div class="row justify-content-center">
<div class="col-12 col-md-5">
<h2 class="text-center pt-5 mb-5">Registro</h2>
{{ form_start(registrationForm) }}
<div class="form-group">
{{ form_label(registrationForm.username) }}
{{ form_widget(registrationForm.username, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-group">
{{ form_label(registrationForm.plainPassword, 'Password') }}
{{ form_widget(registrationForm.plainPassword, {'attr': {'class': 'form-control'}}) }}
</div>
<div class="form-check ml-auto my-3">
{{ form_widget(registrationForm.agreeTerms, {'attr': {'class': 'form-check-input'} }) }}
{{ form_label(registrationForm.agreeTerms, '¿Aceptas las condiciones?', {'label_attr': {'class': 'form-check-label'} }) }}
</div>
<div class="d-grid">
<input type="submit" class="btn btn-primary" name="logeo" value="Registrarse">
</div>
{{ form_end(registrationForm) }}
</div>
</div>
{% endblock %}
Para generar el login
php bin/console make:auth
Es importante tener en cuenta que el login nos tiene que redirigir a algun ruta que debemos establecer nosotros en SecurityController o como lo hayamos llamado
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// if ($this->getUser()) {
// return $this->redirectToRoute('target_path');
// }
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
}
Si queremos personalizar el login
{% extends 'base.html.twig' %}
{% block title %}Login{% endblock %}
{% block body %}
<div class="row justify-content-center">
<div class="col-12 col-md-5">
<h2 class="text-center pt-5 mb-5">Bienvenido</h2>
<form method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
{% if app.user %}
<div class="mb-3">
You are logged in as {{ app.user.userIdentifier }}, <a href="{{ path('app_logout') }}">Logout</a>
</div>
{% endif %}
<div class="mb-4">
<label for="inputUsername">Username</label>
<input type="text" value="{{ last_username }}" name="username" id="inputUsername" class="form-control" autocomplete="username" required autofocus>
</div>
<div class="mb-4">
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required>
</div>
<div class="d-grid">
<input type="submit" class="btn btn-primary" name="logeo" value="Iniciar sesion">
</div>
<div class="my-3">
<span>¿No tienes cuenta? <a href="{{ path('app_register') }}">Crea tu cuenta</a></span>
</div>
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
{#
Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
See https://symfony.com/doc/current/security/remember_me.html
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
</div>
#}
</form>
</div>
</div>
{% endblock %}
Para denegar el acceso a una funcion en concreto para que sola puedan entrar los admin
$this -> denyAccessUnlessGranted('ROLE_ADMIN');
Para no permitir el acceso a una funcion a un usuario que no este registrado
$this -> denyAccessUnlessGranted('ROLE_USER');
Cuando queremos dar una fecha podemos dejar la fecha actual como valor por defecto, de esta forma cada vez que creemos un articulo se le asigna la fecha actual, desde el controlador de la entidad
public function __construct() {
$this -> fecha = new \DateTime();
}
Para poder mostar las fhecas debemos darle un formato desde twig
<p class="col-auto">Fecha: {{ articulo.fecha ? articulo.fecha|date('Y-m-d H:i:s') : '' }}</p>
Si queremos filtar por la fecha mas reciente podemos crear una funcion en el repositorio, y llamarla desde el controlador
public function findOrderDate(): array
{
return $this->createQueryBuilder('a')
->orderBy('a.fecha', 'DESC')
->getQuery()
->getResult()
;
}
Partimos de una entidad que tenga un campo String, para guardar la imagen, podemos seguir la documentacin de symfony. Lo primero que haremos sera modificar el campo que tenemos desde el formulario para que sea un FileType
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Form\Extension\Core\Type\FileType;
->add('imagen', FileType::class, [
'label' => 'Imagen para el articulo',
'mapped' => false,
'required' => false,
'constraints' => [
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'image/jpeg',
'image/png',
],
'mimeTypesMessage' => 'Por favor sube una imagen'
])
],
])
Lo siguiente sera modificar la funcion de creacin de articulo para gestionar la subida de la imagen a la base de datos
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\String\Slugger\SluggerInterface;
// Devemos añadir como parametro SluggerInterface $slugger
if ($form->isSubmitted() && $form->isValid()) {
// Esto debe estar dentro del condicional que valida el formulario
$imagen = $form->get('imagen')->getData();
if ($imagen) {
$originalFilename = pathinfo($imagen->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename.'-'.uniqid().'.'.$imagen->guessExtension();
try {
$imagen->move(
$this->getParameter('imagen_directory'),
$newFilename
);
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
$articulo->setImagen($newFilename);
}
$articulosRepository->save($articulo, true);
return $this->redirectToRoute('app_main', [], Response::HTTP_SEE_OTHER);
}
Creamos el directorio donde se guardaran las imagenes dentro de public y creamos el parametro en config/services.yaml
parameters:
imagen_directory: '%kernel.project_dir%/public/imagenes'
Para modifcar un campo de texto a un textArea, desde el formularpio donde este el campo a modificar
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
// ->add('contenido')
->add('contenido', TextareaType::class, [
'required' => true,
'attr' => ['rows' => 5]
])
Podemos crear una plantilla twig para que todos los formularios tengan los mismos estilos, para ello creamos un fichero en la carpeta comunes llamado _form.html.twig
<div class="row justify-content-center">
<div class="col-12 col-md-5" id="container">
<h1 class="text-center">{{ titulo }}</h1>
{{ form_start(form) }}
{% for field in form %}
<div class="form-group">
{{ form_row(field, {'attr': {'class': 'form-control', 'placeholder': field.vars.label}}) }}
{% if form_errors(field) %}
<div class="invalid-feedback">
{{ form_errors(field) }}
</div>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary my-3">Guardar</button>
{{ form_end(form) }}
</div>
</div>
En la plantilla del formulario he añadido un h1 con una variable llamado titulo, al hacer el include le pasaremos el titulo
{% include 'comunes/_form.html.twig' with {'titulo': 'Crear nuevo articulo'} %}
Para añadir recursos externos como bootstrap, nos dirigimos a la web oficial y descaargamos Bootstrap compilado y copiamos los directorios de css y js en el directorio de public
Una vez copiado realizamos las llamadas desde el archivo base en templates, quedando el archivo de la siguiente forma
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css">
{% endblock %}
</head>
<body>
<div class="container">
{% block body %}{% endblock %}
</div>
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="/js/bootstrap.min.js"></script>
{% endblock %}
</body>
</html>
Otra forma de añadir bootstrap es generandolo por node, nos dirigimos al directorio public y ejecutamos el siguiente comando
npm i bootstrap@5.3.0-alpha1
Creamos el directorio donde iran los estilos, pero usando SASS
mkdir scss
Creamos un archivo llamado custom.scss y copiamos en el fichero lo siguiente
@import "../node_modules/bootstrap/scss/bootstrap";
Una vez hecho esto ya podemos establecer el enlace al directorio css, en el directorio templates
<link rel="stylesheet" type="text/css" href="css/custom.css">
En el directorio de templates creamos un directorio donde almacenar el menu
cd templates
mkdir comunes
Creamos un fichero llamado _menu.html.twig. Una vez ralizado lo incluimos en el fichero base.html.twig de la siguiente forma y creamos un div, quedara de la siguiente forma
<body>
{% include "comunes/_menu.html.twig" %}
<div class="container">
{% block body %}{% endblock %}
</div>
</body>
Un ejemplo de menu ppodria ser algo asi
<header class="row justify-content-between align-items-center pb-3">
<h1 class="col-auto"><a class="text-dark nav-link" href="{{ path('app_main') }}">Blog</a></h1>
<nav class="col-auto navbar-expand">
<ul class="navbar-nav ml-auto">
{% if is_granted('ROLE_USER') %}
<li class="nav-item"><a class="nav-link" href="{{ path('app_articulos_new') }}">Crear articulo</a></li>
{% endif %}
{% if app.user %}
<li class="nav-item"><a class="nav-link" href="{{ path('app_logout') }}">Cerrar sesion</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{{ path('app_login') }}">Login</a></li>
{% endif %}
</ul>
</nav>
</header>
Pero tambien podemos buscar algun menu en la pagina de bootstrap
Generamos el controlador principal, MainController
php bin/console make:controller
Debemos crear las funciones correspondientes a la paginacion en el repositorio
public function paginacion($dql, $pagina, $elementosPorPagina)
{
$paginador = new Paginator($dql);
$paginador -> getQuery()
->setFirstResult($elementosPorPagina * ($pagina - 1))
->setMaxResults($elementosPorPagina);
return $paginador;
}
public function buscarTodas($pagina = 1, $elementosPorPagina = 5)
{
$query = $this->createQueryBuilder('t')
->getQuery()
;
return $this -> paginacion($query, $pagina, $elementosPorPagina);
}
Y en el maincontroller se veria asi
class MainController extends AbstractController
{
// Variables
const ELEMENTOS_POR_PAGINA = 5;
/**
* @Route("/{pagina}", name="app_main", defaults={"pagina": 1}, requirements={"pagina"="\d+"}, methods={"GET"})
*/
public function index(int $pagina, TareaRepository $tareaRepository): Response
{
return $this->render('main/index.html.twig', [
'tareas' => $tareaRepository -> buscarTodas($pagina, self::ELEMENTOS_POR_PAGINA),
'pagina' => $pagina,
]);
}
}
Para dar un foramato a la paginacion creamos una sita parcial en templates/comunes "_paginacion.html.twig"
{% set numero_total_paginas = (numero_total_elementos / elementos_por_pagina)|round(0, 'ceil') %}
{% set parametros = parametros_ruta is defined ? parametros_ruta : {} %}
{% set nombre_ruta = nombre_ruta is defined ? nombre_ruta : app.request.attributes.get('_route') %}
{% if numero_total_paginas > 1 %}
<nav>
<ul class="pagination justify-content-center">
<li class="page-item {{ pagina_actual == 1 ? 'disabled' }}">
<a class="page-link"
href="{{
path(nombre_ruta,
{
pagina: pagina_actual - 1 < 1 ? 1 : pagina_actual - 1
}|merge(parametros)
)
}}">
<span>«</span>
<span>Anterior</span>
</a>
</li>
{% for i in 1..numero_total_paginas %}
<li class="page-item {{ pagina_actual == i ? 'active' }}">
<a class="page-link"
href="{{
path(nombre_ruta,
{
pagina:i
}|merge(parametros)
)
}}">
<span>{{ i }}</span>
</a>
</li>
{% endfor %}
<li class="page-item {{ pagina_actual == numero_total_paginas ? 'disabled' }}">
<a class="page-link"
href="{{
path(nombre_ruta,
{
pagina: pagina_actual + 1 <= numero_total_paginas ? pagina_actual + 1 : pagina_actual
}|merge(parametros)
)
}}">
<span>»</span>
<span>Siguiente</span>
</a>
</li>
</ul>
</nav>
{% endif %}
y hacemos la llamada con el include desde donde queremos la paginacion
<div class="mt-3">
{{ include('comunes/_paginacion.html.twig', {
elementos_por_pagina: constant('App\\Controller\\MainController::ELEMENTOS_POR_PAGINA'),
numero_total_elementos: tareas.count,
pagina_actual: pagina,
}) }}
</div>
composer require orm-fixtures --dev
Nos genera un directorio en src llamado DataFixtures, en el eliminamos el archivo que nos ha generado y generamos el nuestro
php bin/console make:fixtures
Creamos un for con los datos a mostrar
for ($i = 0; $i < 20; $i++) {
$tarea = new Tarea();
$tarea -> setDescripcion("Tarea nueva - $i");
$manager -> persist($tarea);
}
y cargamos el archivo
php bin/console doctrine:fixtures:load
Este es un bundle que nos permitira generar una pagina de administracion muy util
composer require easycorp/easyadmin-bundle
Generamos un Dashboard, que nos permitra controlar toda la gestion, sera como nuestra pagina principal
php bin/console make:admin:dashboard
Creamos un crud por cada entidad que tengamos, nos mostrara una lista y estos cruds sera los enalces del menu
php bin/console make:admin:crud
Debemos configurar el dahsboard para mostrar los cruds que generamos, una vez generado el crud dberia quedar de la siguiente manera
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
#[Route('/admin', name: 'admin')]
public function index(): Response
{
// return parent::index();
// Option 1. You can make your dashboard redirect to some common page of your backend
//
$adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);
return $this->redirect($adminUrlGenerator->setController(ArticulosCrudController::class)->generateUrl());
}
Es posible que nos de algun fallo por trabajar con fechas si ese es elcaso debemos habilitar "PHP Intl", en el caso de xampp, nos dirigimos a config/PHP (PHP.ini) y desde ahi buscar la linea ";extension=intl" y quitarlee el punto y coma
extension=intl
Para el menu en el dashboard tenemos una funcino para ello
public function configureMenuItems(): iterable
{
yield MenuItem::linkToDashboard('Articulos', 'fa fa-home');
yield MenuItem::linkToCrud('User', 'fas fa-list', User::class);
}
Para personalizar los campos que se muestran en easyÁdmin lo haremos desde el crud
public function configureFields(string $pageName): iterable
{
return [
TextField::new('username'),
TextField::new('password'),
ChoiceField::new('roles', 'Roles')
->setChoices([
'Admin' => 'ROLE_ADMIN',
'User' => 'ROLE_USER',
])
->allowMultipleChoices(true),
];
}