DevTools

Cheatsheet Symfony

Framework PHP robusto e maduro para aplicações web enterprise

Volver a los lenguajes
Symfony
92 tarjetas encontradas
Categorías:
Versiones:

Instalação e Setup


10 cards
Crear Proyecto
composer create-project symfony/skeleton mi-proyecto
cd mi-proyecto
symfony serve

symfony/skeleton crea un proyecto mínimo. symfony serve inicia el servidor de desarrollo con HTTPS automático. Para un proyecto completo usa symfony/website-skeleton.

Maker Bundle
php bin/console make:controller HomeController
php bin/console make:entity Post
php bin/console make:form PostType
php bin/console make:crud Post

MakerBundle genera código automáticamente. make:crud crea controller, templates y form de una vez. Todos los comandos son interactivos y generan archivos listos.

Instalar Bundles
composer require symfony/mailer
composer require symfony/security-bundle
composer require doctrine/doctrine-bundle
composer require symfony/maker-bundle --dev

composer require instala y configura bundles vía Symfony Flex. Las recetas crean archivos de config automáticamente. --dev instala solo en desarrollo.

Symfony CLI
symfony new app --webapp
symfony check:requirements
symfony open:local
symfony server:log

symfony new crea un proyecto con la CLI. --webapp incluye todos los paquetes web. check:requirements valida el entorno. open:local lo abre en el navegador.

Estructura del Proyecto
src/
  Controller/
  Entity/
  Repository/
  Service/
templates/
config/
  packages/
  routes.yaml
  services.yaml
public/

src/ contiene todo el código PHP. templates/ guarda los archivos Twig. config/packages/ tiene la configuración por bundle. public/ es la raíz web.

Symfony Flex
composer recipes
composer sync-recipes
composer unpack api

Flex es el plugin que automatiza la instalación de paquetes. recipes lista las recetas aplicadas. unpack expande un pack en sus componentes individuales para mayor control.

Servidor de Desarrollo
symfony serve -d
symfony server:stop
symfony server:status
symfony serve --port=8080

-d ejecuta en daemon (background). server:stop detiene el servidor. El servidor soporta HTTPS automático y múltiples proyectos en puertos diferentes.

Configuración .env
# .env
APP_ENV=dev
APP_SECRET=a1b2c3d4
DATABASE_URL="mysql://root:@127.0.0.1:3306/app"
MAILER_DSN=smtp://localhost

.env define variables de entorno. APP_ENV controla el entorno (dev/prod/test). DATABASE_URL configura la conexión a la BD. Nunca hagas commit del .env.local.

Bin Console
php bin/console list
php bin/console debug:router
php bin/console debug:container
php bin/console about

bin/console es la consola de comandos. debug:router lista todas las rutas. debug:container muestra los servicios registrados. about exhibe la info del proyecto.

Configuración YAML
# config/packages/framework.yaml
framework:
    secret: '%env(APP_SECRET)%'
    session:
        handler_id: null
    csrf_protection: true

Cada bundle tiene su archivo en config/packages/. La sintaxis %env()% lee variables de entorno. Las configuraciones se fusionan automáticamente por entorno.

Rotas e Controllers


11 cards
Controller Básico
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class HomeController extends AbstractController
{
    public function index(): Response
    {
        return new Response('¡Hola Symfony!');
    }
}

AbstractController proporciona métodos helper como render(), json() y redirectToRoute(). Todo controller debe retornar un Response.

Tipos de Respuesta
return new Response('Texto HTML');
return $this->json(['ok' => true]);
return $this->redirectToRoute('app_home');
return $this->render('page.html.twig', $data);
return new JsonResponse($data, 201);

Response es la respuesta base. json() retorna JSON con los headers correctos. redirectToRoute() hace redirect 302. render() renderiza un template Twig.

Ruta con Prefijo
#[Route('/admin', name: 'admin_')]
class AdminController extends AbstractController
{
    #[Route('/dashboard', name: 'dashboard')]
    public function dashboard(): Response { }
    // URL final: /admin/dashboard
}

El #[Route] en la clase define un prefijo para todas las rutas del controller. El name de la clase se antepone al nombre de cada método. Evita la repetición de paths.

Ruta con Atributo
use Symfony\Component\Routing\Attribute\Route;

#[Route('/hello/{name}', name: 'app_hello')]
public function hello(string $name): Response
{
    return $this->render('hello.html.twig', [
        'name' => $name,
    ]);
}

#[Route] define la ruta con attributes de PHP 8. El name permite generar URLs y redirigir. El parámetro {name} se inyecta automáticamente en el método.

Objeto Request
use Symfony\Component\HttpFoundation\Request;

public function store(Request $request): Response
{
    $name = $request->request->get('name');
    $query = $request->query->get('page', 1);
    $all = $request->request->all();
    $ip = $request->getClientIp();
}

Request se inyecta automáticamente. request accede a datos POST. query accede a parámetros GET. El segundo argumento de get() es el valor por defecto.

Naming y Organización de Rutas
// config/routes.yaml
controllers:
    resource: ../src/Controller/
    type: attribute
    prefix: /{_locale}
    requirements:
        _locale: 'pt|en|fr'

config/routes.yaml importa las rutas de los controllers. prefix añade un prefijo global. requirements restringe el _locale a valores válidos. Convención: app_ para los nombres.

Parámetros de Ruta
#[Route('/user/{id}', requirements: ['id' => '\d+'])]
public function show(int $id): Response { }

#[Route('/blog/{slug}', defaults: ['page' => 1])]
public function post(string $slug, int $page): Response { }

requirements valida el parámetro con regex. \d+ acepta solo números. defaults define valores opcionales. El type-hint int hace cast automático.

Generar URLs
$url = $this->generateUrl('app_hello', ['name' => 'Ana']);

// con RouterInterface inyectado:
use Symfony\Component\Routing\RouterInterface;

$url = $router->generate('app_hello', ['name' => 'Ana']);

generateUrl() crea URLs a partir del nombre de la ruta. Los parámetros extra se convierten en query string. Usa siempre nombres de ruta en vez de URLs hardcoded para facilitar el mantenimiento.

Response Personalizado
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;

return new BinaryFileResponse('/path/file.pdf');

$response = new StreamedResponse(function () {
    echo 'streaming...';
});

BinaryFileResponse envía archivos con los headers correctos. StreamedResponse genera contenido progresivamente (útil para CSVs grandes). Ambos extienden Response.

Métodos HTTP
#[Route('/posts', methods: ['GET'])]
public function list(): Response { }

#[Route('/posts', methods: ['POST'])]
public function create(Request $request): Response { }

#[Route('/posts/{id}', methods: ['PUT', 'PATCH'])]
public function update(int $id): Response { }

methods restringe los verbos HTTP aceptados. Las rutas con el mismo path pero métodos diferentes se resuelven correctamente. Útil para APIs RESTful.

Redirecciones
return $this->redirectToRoute('app_home');
return $this->redirect('https://external.com');
return new RedirectResponse($url, 301);

redirectToRoute() redirige a una ruta interna. redirect() acepta una URL absoluta. RedirectResponse permite definir el código HTTP (301 permanente, 302 temporal).

Serviços e DI


10 cards
Crear Servicio
namespace App\Service;

class EmailService
{
    public function send(string $to, string $msg): void
    {
        // lógica de envío
    }
}

Un servicio es una clase PHP simple en src/Service/. Symfony lo registra automáticamente en el container. No necesita configuración manual con autowire activo.

Interface Binding
# config/services.yaml
services:
    App\Service\PaymentInterface:
        alias: App\Service\StripePayment

// en el código:
public function __construct(
    private PaymentInterface $payment,
) {}

alias liga una interface a la implementación concreta. Permite cambiar implementaciones sin alterar el código consumidor. Esencial para Dependency Inversion y testabilidad.

Service Decorator
#[AsDecorator(serviceName: 'app.notifier', priority: 10)]
class CachedNotifier implements NotifierInterface
{
    public function __construct(
        private NotifierInterface $inner,
    ) {}

    public function notify(string $msg): void
    {
        // lógica extra + $this->inner->notify($msg);
    }
}

#[AsDecorator] envuelve un servicio existente sin modificarlo. El servicio original queda disponible como $inner. priority controla el orden cuando hay múltiples decorators.

Inyección Automática
class PostController extends AbstractController
{
    public function __construct(
        private EmailService $email,
        private LoggerInterface $logger,
    ) {}

    public function index(): Response
    {
        $this->logger->info('Accedió al index');
    }
}

Constructor injection con promoted properties de PHP 8. El container resuelve las dependencias automáticamente vía type-hint. LoggerInterface se inyecta sin configuración extra.

Servicios con Tag
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: 'kernel.request')]
class MyListener
{
    public function __invoke(RequestEvent $event): void
    {
        // ejecutado en cada request
    }
}

#[AsEventListener] registra la clase como listener sin config YAML. El método __invoke se llama cuando el evento se dispara. autoconfigure detecta el atributo automáticamente.

Compiler Passes
// src/DependencyInjection/AppExtension.php
class AppExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $c): void
    {
        $loader = new YamlFileLoader($c, new FileLocator(__DIR__));
        $loader->load('services.yaml');
    }
}

Extension permite a un bundle registrar servicios programáticamente. CompilerPass modifica el container antes de compilar. Usado en bundles reutilizables para registrar servicios condicionalmente.

Config de Autowiring
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'
        exclude:
            - '../src/Entity/'
            - '../src/Kernel.php'

autowire: true inyecta dependencias por type-hint. autoconfigure: true registra tags automáticamente. exclude impide el registro de entidades y del Kernel.

Servicio Público
# config/services.yaml
services:
    App\Service\LegacyService:
        public: true

// acceder manualmente:
$service = $container->get(LegacyService::class);

Por defecto los servicios son private (solo vía injection). public: true permite el acceso directo por el container. Úsalo solo para servicios legacy o tests.

Parámetros de Config
# config/services.yaml
parameters:
    app.name: 'Mi Sitio'
    app.max_items: 50

# en el servicio:
use Symfony\Component\DependencyInjection\Attribute\Autowire;

public function __construct(
    #[Autowire('%app.name%')]
    private string $name,
) {}

parameters define valores reutilizables. #[Autowire] inyecta parámetros o valores específicos. La sintaxis %param% referencia parámetros del container.

Lazy Services
# config/services.yaml
services:
    App\Service\HeavyService:
        lazy: true

// o con atributo:
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;

#[AsDecorator('app.mailer')]
class LoggingMailer { }

lazy: true crea un proxy que solo instancia el servicio cuando se usa realmente. Reduce memoria y tiempo de arranque. Útil para servicios pesados raramente utilizados.

Doctrine ORM


11 cards
Crear Entidad
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $title;

    #[ORM\Column(type: 'text')]
    private string $content;
}

#[ORM\Entity] marca la clase como entidad. #[ORM\Id] y #[ORM\GeneratedValue] definen la clave primaria autoincremental. repositoryClass la liga al repositorio personalizado.

QueryBuilder
$qb = $this->createQueryBuilder('p')
    ->where('p.active = :active')
    ->andWhere('p.created > :date')
    ->setParameter('active', true)
    ->setParameter('date', new \DateTime('-30 days'))
    ->orderBy('p.created', 'DESC')
    ->setMaxResults(10);

$posts = $qb->getQuery()->getResult();

createQueryBuilder() construye consultas dinámicas. setParameter() previene SQL injection. setMaxResults() limita los resultados. Siempre preferible a DQL concatenado.

DQL
$dql = 'SELECT p, c FROM App\Entity\Post p
        JOIN p.comments c
        WHERE p.active = :active
        ORDER BY p.created DESC';

$query = $em->createQuery($dql)
    ->setParameter('active', true)
    ->setMaxResults(20);

$posts = $query->getResult();

DQL es el lenguaje de consultas orientado a objetos de Doctrine. JOIN carga relaciones eagerly. getResult() retorna un array de entidades. Usa getOneOrNullResult() para single.

Tipos de Columna
#[ORM\Column(type: 'string', length: 100)]
private string $name;

#[ORM\Column(type: 'integer')]
private int $age;

#[ORM\Column(type: 'boolean', options: ['default' => false])]
private bool $active;

#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $created;

#[ORM\Column(type: 'json')]
private array $metadata = [];

type define el tipo SQL mapeado. options permite defaults y nullable. json serializa arrays automáticamente. datetime_immutable es preferible a datetime.

Operaciones CRUD
$em = $this->entityManager;

// Crear / Actualizar
$em->persist($post);
$em->flush();

// Eliminar
$em->remove($post);
$em->flush();

// Leer
$post = $em->find(Post::class, $id);

EntityManager gestiona el ciclo de vida de las entidades. persist() marca para inserción/actualización. flush() ejecuta las queries SQL. find() búsqueda por clave primaria.

Eventos de Entidad
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Post
{
    #[ORM\PrePersist]
    public function onPrePersist(): void
    {
        $this->created = new \DateTimeImmutable();
    }

    #[ORM\PreUpdate]
    public function onPreUpdate(): void
    {
        $this->updated = new \DateTimeImmutable();
    }
}

#[ORM\HasLifecycleCallbacks] activa los callbacks en la entidad. #[ORM\PrePersist] se ejecuta antes de insertar. #[ORM\PreUpdate] se ejecuta antes de actualizar. Alternativa: usar Doctrine Events.

Migraciones
php bin/console doctrine:database:create
php bin/console make:migration
php bin/console doctrine:migrations:migrate
php bin/console doctrine:migrations:status
php bin/console doctrine:schema:validate

make:migration genera la migración comparando entidades con la BD. migrate aplica las migraciones pendientes. schema:validate verifica la consistencia entre el mapping y la BD.

Relaciones ManyToOne
#[ORM\Entity]
class Comment
{
    #[ORM\ManyToOne(inversedBy: 'comments')]
    #[ORM\JoinColumn(nullable: false)]
    private Post $post;
}

// en Post:
#[ORM\OneToMany(mappedBy: 'post', targetEntity: Comment::class)]
private Collection $comments;

#[ORM\ManyToOne] define el lado propietario de la relación. inversedBy apunta a la propiedad del lado opuesto. mappedBy en el OneToMany indica que es el lado inverso.

Fixtures
use Doctrine\Bundle\FixturesBundle\Fixture;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $post = new Post();
        $post->setTitle('Primer post');
        $manager->persist($post);
        $manager->flush();
    }
}
// php bin/console doctrine:fixtures:load

Fixtures pueblan la BD con datos de prueba. ObjectManager funciona como el EntityManager. doctrine:fixtures:load limpia y recarga todo. Usa --append para no limpiar.

Repository
class PostRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Post::class);
    }

    public function findActive(): array
    {
        return $this->findBy(['active' => true], ['created' => 'DESC']);
    }
}

ServiceEntityRepository es la base para repositorios. findBy() y findOneBy() son métodos mágicos heredados. Añade métodos personalizados para consultas reutilizables.

Relaciones ManyToMany
#[ORM\Entity]
class Post
{
    #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'posts')]
    #[ORM\JoinTable(name: 'post_tag')]
    private Collection $tags;

    public function __construct()
    {
        $this->tags = new ArrayCollection();
    }
}

#[ORM\ManyToMany] crea una tabla intermedia. #[ORM\JoinTable] define el nombre de la tabla pivot. Inicializa las colecciones en el constructor con ArrayCollection.

Forms e Validação


10 cards
Crear Form Type
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title')
            ->add('content', TextareaType::class)
            ->add('active', CheckboxType::class, ['required' => false]);
    }
}

AbstractType es la base de todos los formularios. buildForm() define los campos. El tipo se infiere por el nombre de la propiedad si se omite. required => false quita el atributo HTML required.

Tipos de Campo
TextType::class
TextareaType::class
EmailType::class
IntegerType::class
ChoiceType::class
EntityType::class
DateTimeType::class
CheckboxType::class
FileType::class
PasswordType::class

Cada tipo mapea a un input HTML específico. ChoiceType crea select/radio/checkbox. EntityType carga opciones de una entidad Doctrine. FileType gestiona uploads.

Formularios Anidados
class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $b, array $opts): void
    {
        $b->add('title')
          ->add('author', AuthorType::class)
          ->add('tags', CollectionType::class, [
              'entry_type' => TagType::class,
              'allow_add' => true,
              'by_reference' => false,
          ]);
    }
}

CollectionType gestiona listas de sub-formularios. allow_add permite añadir items vía JS. by_reference => false fuerza el uso de setters. entry_type define el tipo de cada item.

Usar en el Controller
$form = $this->createForm(PostType::class, $post);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    $em->persist($post);
    $em->flush();
    return $this->redirectToRoute('app_post_list');
}

return $this->render('post/form.html.twig', [
    'form' => $form,
]);

createForm() instancia el formulario ligado a una entidad. handleRequest() procesa el envío. isSubmitted() e isValid() verifican el estado antes de persistir.

Formulario sin Clase
$form = $this->createFormBuilder()
    ->add('name', TextType::class)
    ->add('email', EmailType::class)
    ->add('submit', SubmitType::class)
    ->getForm();

$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
    $data = $form->getData();
}

createFormBuilder() crea formularios inline sin FormType. Ideal para formularios simples de búsqueda o contacto. getData() retorna un array asociativo con los valores.

CSRF en Formularios
# config/packages/framework.yaml
framework:
    csrf_protection: true

// validación manual:
if (!$this->isCsrfTokenValid('delete-item', $request->get('_token'))) {
    throw new AccessDeniedException('Token inválido');
}

csrf_protection activa tokens en todos los formularios. El token se renderiza automáticamente por form_end(). Para acciones fuera de forms usa isCsrfTokenValid() manualmente.

Validación con Constraints
use Symfony\Component\Validator\Constraints as Assert;

#[Assert\NotBlank(message: 'El título es obligatorio')]
#[Assert\Length(min: 5, max: 255)]
private string $title;

#[Assert\Email]
#[Assert\NotBlank]
private string $email;

#[Assert\Range(min: 0, max: 100)]
private int $age;

Constraints validan los datos de la entidad. #[Assert\NotBlank] rechaza vacío. #[Assert\Length] limita caracteres. Las constraints se verifican automáticamente por form->isValid().

Eventos de Formulario
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;

$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
    $post = $event->getData();
    $form = $event->getForm();

    if ($post && $post->getId()) {
        $form->add('created', DateTimeType::class, ['disabled' => true]);
    }
});

FormEvents permite modificar campos dinámicamente. PRE_SET_DATA se ejecuta antes de poblar los datos. PRE_SUBMIT y POST_SUBMIT controlan el envío. Útil para campos condicionales.

Renderizar en Twig
{{ form_start(form) }}
    {{ form_row(form.title) }}
    {{ form_row(form.content) }}
    {{ form_widget(form.active) }}
    <button type="submit">Guardar</button>
{{ form_end(form) }}

{# renderizar todo de una vez: #}
{{ form(form) }}

form_start() abre la tag form con CSRF. form_row() renderiza label + widget + errores. form_end() cierra y renderiza los campos ocultos. form(form) es el atajo completo.

Subida de Archivos
$builder->add('file', FileType::class, [
    'constraints' => [
        new File([
            'maxSize' => '5M',
            'mimeTypes' => ['application/pdf', 'image/png'],
        ]),
    ],
]);

// en el controller:
$file = $form->get('file')->getData();
$newName = uniqid() . '.' . $file->guessExtension();
$file->move($this->getParameter('uploads_dir'), $newName);

FileType gestiona uploads. La constraint File valida tamaño y MIME type. move() guarda el archivo en el destino. Usa getParameter() para el directorio configurado.

Twig Templates


10 cards
Variables y Output
{{ name }}
{{ post.title }}
{{ user.email|default('N/A') }}
{{ "Hola #{name}" }}
{{ items|length }}

{{ }} imprime variables. Acceso a propiedades con punto. |default proporciona un fallback para nulos. |length cuenta elementos. Interpolación con #{} dentro de strings.

Funciones
{{ path('app_hello', {name: 'Ana'}) }}
{{ url('app_hello', {name: 'Ana'}) }}
{{ asset('css/app.css') }}
{{ csrf_token('my_form') }}
{{ include('partials/menu.html.twig') }}
{{ dump(variable) }}

path() genera una URL relativa. url() genera una URL absoluta. asset() referencia archivos estáticos con versionado. dump() muestra debug (solo en dev).

Assets
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
<script src="{{ asset('build/app.js') }}"></script>

{# con Webpack Encore: #}
{{ encore_entry_link_tags('app') }}
{{ encore_entry_script_tags('app') }}

asset() genera URLs con cache-busting. Encore integra Webpack para compilar assets. encore_entry_link_tags() y encore_entry_script_tags() insertan las tags automáticamente.

Control de Flujo
{% if user and user.isAdmin %}
    <p>Admin</p>
{% elseif user %}
    <p>Usuario</p>
{% else %}
    <p>Anónimo</p>
{% endif %}

{% for item in items %}
    <li>{{ loop.index }}. {{ item }}</li>
{% endfor %}

{% if %} soporta condiciones con operadores lógicos. {% for %} itera arrays. loop.index da la posición actual (1-based). loop.first y loop.last son booleanos útiles.

Include y Macros
{% include 'partials/header.html.twig' %}
{% include 'partials/card.html.twig' with {title: 'X'} %}

{% macro input(name, value, type) %}
    <input type="{{ type|default('text') }}"
           name="{{ name }}"
           value="{{ value }}">
{% endmacro %}

{% import _self as forms %}
{{ forms.input('email', '', 'email') }}

{% include %} inserta parciales con variables opcionales. {% macro %} define funciones reutilizables. {% import %} importa macros de otro archivo. _self referencia macros del propio template.

Debug y Dump
{{ dump(variable) }}
{{ dump() }}  {# todas las variables #}

{% debug %}

{# en la CLI: #}
php bin/console debug:twig
php bin/console lint:twig templates/

dump() inspecciona variables (solo en dev). debug:twig lista las funciones y filtros disponibles. lint:twig valida la sintaxis de todos los templates. Quita los dumps antes de producción.

Herencia de Templates
{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<body>
    {% block content %}{% endblock %}
    {% block scripts %}{% endblock %}
</body>
</html>

{# templates/page.html.twig #}
{% extends 'base.html.twig' %}
{% block content %}
    <h1>Página</h1>
{% endblock %}

{% extends %} hereda de un layout base. {% block %} define secciones sobrescribibles. parent() incluye el contenido del bloque padre. Cada template solo puede tener un extends.

Componentes Twig
{# templates/components/alert.html.twig #}
<div class="alert alert-{{ type|default('info') }}">
    {% block content %}{% endblock %}
</div>

{# uso: #}
{% component 'alert' with {type: 'danger'} %}
    {% block content %}¡Error grave!{% endblock %}
{% endcomponent %}

Twig Components (bundle extra) crean componentes reutilizables con props. Sintaxis similar a Web Components. Requiere symfony/ux-twig-component. Alternativa moderna a las macros.

Filtros
{{ name|upper }}
{{ text|lower|truncate(50) }}
{{ date|date('d/m/Y') }}
{{ price|number_format(2, ',', '.') }}
{{ html|raw }}
{{ list|join(', ') }}

|upper y |lower transforman el case. |date formatea fechas. |raw desactiva el escaping HTML (cuidado con XSS). |number_format formatea números. Los filtros se encadenan con pipe.

Traducción (i18n)
{# en el template: #}
{{ 'welcome.message'|trans({'%name%': user.name}) }}

{# translations/messages.es.yaml: #}
welcome.message: '¡Bienvenido, %name%!'

{# en el controller: #}
$translator->trans('welcome.message', ['%name%' => 'Ana']);

|trans traduce strings con placeholders. Archivos en translations/ por locale. %placeholder% se sustituye por los parámetros. trans() funciona también en PHP.

Segurança


10 cards
Configurar Security
# config/packages/security.yaml
security:
    password_hashers:
        App\Entity\User: 'auto'

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        main:
            form_login:
                login_path: app_login
                check_path: app_login

password_hashers define el algoritmo de hash. providers indica cómo cargar usuarios. firewalls protege áreas de la aplicación. form_login activa la autenticación por formulario.

Protección CSRF
<input type="hidden" name="_token"
       value="{{ csrf_token('delete-item') }}">

// validar en el controller:
if (!$this->isCsrfTokenValid('delete-item', $request->get('_token'))) {
    throw new AccessDeniedException('Token CSRF inválido');
}

csrf_token() genera el token en el template. isCsrfTokenValid() valida en el controller. El primer argumento es el ID del token (debe ser único por acción). Protección contra cross-site request forgery.

Logout
# config/packages/security.yaml
security:
    firewalls:
        main:
            logout:
                path: app_logout
                target: app_home

// controller (vacío - Symfony se encarga):
#[Route('/logout', name: 'app_logout')]
public function logout(): void
{
    throw new \LogicException('Interceptado por el firewall');
}

logout.path define la ruta de logout. target es el redirect tras el logout. El controller nunca se ejecuta — el firewall lo intercepta. La sesión se destruye automáticamente.

Entidad User
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;

#[ORM\Entity]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    private string $email;
    private string $password;
    private array $roles = [];

    public function getUserIdentifier(): string
    {
        return $this->email;
    }
}

UserInterface es obligatorio para la autenticación. PasswordAuthenticatedUserInterface añade soporte de hash. getUserIdentifier() retorna el identificador único (email). getRoles() retorna los roles.

Voters Personalizados
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return $attribute === 'EDIT' && $subject instanceof Post;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        return $subject->getAuthor() === $user;
    }
}

Voter implementa lógica de autorización custom. supports() decide si el voter trata el atributo. voteOnAttribute() retorna true/false. Úsalo con IsGranted pasando el atributo y el subject.

API Tokens
# config/packages/security.yaml
security:
    firewalls:
        api:
            pattern: ^/api
            stateless: true
            custom_authenticators:
                - App\Security\ApiTokenAuthenticator

// o con LexikJWTAuthenticationBundle:
composer require lexik/jwt-authentication-bundle

stateless: true desactiva las sesiones para APIs. custom_authenticators registra autenticadores propios. LexikJWT es la solución estándar para JWT. Ideal para SPAs y apps móviles.

Proteger Rutas
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_ADMIN')]
public function admin(): Response { }

#[IsGranted('ROLE_USER', statusCode: 403)]
public function profile(): Response { }

# o en security.yaml:
access_control:
    - { path: ^/admin, roles: ROLE_ADMIN }

#[IsGranted] protege métodos individuales. ROLE_ADMIN requiere el rol admin. statusCode personaliza el error. access_control protege por patrón de URL en YAML.

Password Hashing
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

public function register(
    Request $request,
    UserPasswordHasherInterface $hasher,
): Response {
    $user->setPassword(
        $hasher->hashPassword($user, $plainPassword)
    );

    // verificar:
    $valid = $hasher->isPasswordValid($user, $input);
}

UserPasswordHasherInterface gestiona el hashing seguro. hashPassword() crea un hash con salt automático. isPasswordValid() compara el input con el hash guardado. Nunca guardes passwords en texto plano.

Obtener el Usuario
// en el controller:
$user = $this->getUser();
$email = $user->getEmail();

// inyectar Security:
use Symfony\Bundle\SecurityBundle\Security;

public function __construct(private Security $security) {}
$user = $this->security->getUser();

// en Twig:
{{ app.user.email }}
{% if is_granted('ROLE_ADMIN') %}

getUser() retorna el usuario autenticado o null. Security inyectado permite verificar en servicios. app.user accede en Twig. is_granted() verifica permisos.

Remember Me
# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 604800  # 1 semana
                path: /
                secure: true

remember_me mantiene la sesión con una cookie persistente. lifetime define la duración en segundos. secure: true envía la cookie solo por HTTPS. El usuario necesita un checkbox en el formulario de login.

Avançado e Boas Práticas


10 cards
Cache
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

public function __construct(private CacheInterface $cache) {}

$data = $this->cache->get('unique_key', function (ItemInterface $item) {
    $item->expiresAfter(3600);
    return $this->expensiveData();
});

CacheInterface se inyecta automáticamente. get() retorna del cache o ejecuta el callback. expiresAfter() define el TTL en segundos. El cache se invalida automáticamente al expirar.

Filesystem
use Symfony\Component\Filesystem\Filesystem;

$fs = new Filesystem();
$fs->mkdir('/tmp/uploads');
$fs->copy('source.txt', 'dest.txt', true);
$fs->remove('/tmp/old');
$fs->dumpFile('config.json', $jsonContent);
$fs->exists('file.txt');

Filesystem abstrae las operaciones de archivos. mkdir() crea directorios recursivamente. dumpFile() escribe atómicamente. remove() elimina archivos y carpetas. Cross-platform y seguro.

Manejo de Errores
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

throw $this->createNotFoundException('Post no encontrado');
throw new AccessDeniedHttpException('Sin permiso');

// error controller custom:
# config/packages/framework.yaml
framework:
    error_controller: App\Controller\ErrorController::show

createNotFoundException() lanza 404. AccessDeniedHttpException lanza 403. Symfony convierte las excepciones en respuestas HTTP. error_controller personaliza las páginas de error.

Serializer
use Symfony\Component\Serializer\SerializerInterface;

$json = $serializer->serialize($post, 'json', [
    'groups' => ['post:read'],
]);

$post = $serializer->deserialize($json, Post::class, 'json');

// con Normalizer:
$data = $serializer->normalize($post, 'json');

SerializerInterface convierte objetos a JSON/XML y viceversa. groups controla qué campos se serializan. normalize() retorna un array. Esencial para APIs REST.

Logging
use Psr\Log\LoggerInterface;

public function __construct(private LoggerInterface $logger) {}

$this->logger->info('Usuario {id} autenticado', ['id' => $user->getId()]);
$this->logger->warning('Intento fallido de {email}', ['email' => $email]);
$this->logger->error('Fallo en el pagado', ['order' => $order->getId()]);

LoggerInterface (PSR-3) se inyecta automáticamente. Los placeholders {key} se sustituyen por el contexto. Niveles: debug, info, warning, error, critical. Logs en var/log/.

Buenas Prácticas
// 1. Controllers finos - lógica en Services
// 2. Type-hints en todas partes
// 3. DTOs para entrada/salida de datos
// 4. Events para desacoplar módulos
// 5. Config en YAML, no hardcoded

Mantén los controllers finos delegando en services. Usa DTOs en vez de arrays. Prefiere events para lógica transversal. Configura todo en YAML con parameters. Testea con PHPUnit y WebTestCase.

Mailer
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

public function __construct(private MailerInterface $mailer) {}

$email = (new Email())
    ->from('noreply@site.com')
    ->to('user@site.com')
    ->subject('¡Bienvenido!')
    ->html('<h1>¡Hola!</h1>')
    ->attachFromPath('/path/file.pdf');

$this->mailer->send($email);

MailerInterface envía emails. Email construye el mensaje con builder. html() define el cuerpo HTML. attachFromPath() adjunta archivos. Configura el DSN en MAILER_DSN.

Profiler y Debug
// barra de debug (dev): /_profiler
php bin/console debug:router
php bin/console debug:container --show-arguments
php bin/console debug:autowiring
php bin/console profiler:info

// en el código:
use Symfony\Component\Stopwatch\Stopwatch;
$stopwatch->start('operation');
// ...
$event = $stopwatch->stop('operation');

El Profiler muestra queries, tiempo y memoria por request. debug:autowiring lista los servicios inyectables. Stopwatch mide el tiempo de las operaciones. Indispensable para la optimización.

HTTP Client
use Symfony\Contracts\HttpClient\HttpClientInterface;

public function __construct(private HttpClientInterface $client) {}

$response = $this->client->request('GET', 'https://api.example.com/data', [
    'headers' => ['Authorization' => 'Bearer token'],
    'timeout' => 10,
]);

$statusCode = $response->getStatusCode();
$data = $response->toArray();

HttpClientInterface hace peticiones HTTP sin cURL. request() acepta método, URL y opciones. toArray() hace parse JSON automático. Soporta async, retries y rate limiting nativo.

Rate Limiting
# config/packages/rate_limiter.yaml
framework:
    rate_limiter:
        api:
            policy: sliding_window
            limit: 100
            interval: '60 minutes'

// en el controller:
use Symfony\Component\RateLimiter\RateLimiterFactory;

$limiter = $factory->create($request->getClientIp());
if (!$limiter->consume()->isAccepted()) {
    return new Response('Too Many Requests', 429);
}

rate_limiter limita peticiones por IP/usuario. sliding_window es la política más justa. consume() verifica y registra la petición. Retorna 429 cuando se excede el límite. Esencial para APIs.

Eventos e Console


10 cards
Event Listeners
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;

#[AsEventListener(event: 'kernel.request', priority: 10)]
class LocaleListener
{
    public function __invoke(RequestEvent $event): void
    {
        $request = $event->getRequest();
        $request->setLocale($request->getPreferredLanguage(['pt', 'en']));
    }
}

#[AsEventListener] registra sin config YAML. priority controla el orden de ejecución (mayor = primero). El método __invoke recibe el evento. kernel.request se dispara antes del controller.

Input y Output
protected function configure(): void
{
    $this->addArgument('file', InputArgument::REQUIRED)
         ->addOption('force', 'f', InputOption::VALUE_NONE);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
    $file = $input->getArgument('file');
    $force = $input->getOption('force');
    $output->writeln("<info>¡Hecho!</info>");
}

addArgument() define argumentos posicionales. addOption() define flags opcionales. getArgument() y getOption() leen los valores. Tags como <info> colorean el output.

Mensajes Fallidos
php bin/console messenger:failed:show
php bin/console messenger:failed:retry 1
php bin/console messenger:failed:remove 1

# config/packages/messenger.yaml
framework:
    messenger:
        failure_transport: failed
        transports:
            failed: 'doctrine://default?queue_name=failed'

failure_transport guarda los mensajes que fallan tras los retries. failed:show lista los fallos. failed:retry reprocesa. failed:remove descarta. Esencial para la monitorización.

Event Subscribers
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class LogSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'kernel.request' => 'onRequest',
            'kernel.response' => ['onResponse', -10],
        ];
    }

    public function onRequest(RequestEvent $e): void { }
    public function onResponse(ResponseEvent $e): void { }
}

EventSubscriberInterface permite escuchar múltiples eventos en una clase. getSubscribedEvents() mapea eventos a métodos. El array [método, prioridad] define el orden. Más organizado que listeners separados.

Messenger (async)
// src/Message/SendEmailMessage.php
class SendEmailMessage
{
    public function __construct(
        public string $to,
        public string $subject,
    ) {}
}

// en el controller:
$this->messageBus->dispatch(new SendEmailMessage('ana@site.com', 'Hola'));

Messenger envía mensajes para procesamiento asíncrono. El message es un DTO simple. dispatch() lo coloca en la cola. Requiere configuración de transport (Doctrine, RabbitMQ, Redis).

Dispatch de Eventos Custom
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class OrderService
{
    public function __construct(
        private EventDispatcherInterface $dispatcher,
    ) {}

    public function complete(Order $order): void
    {
        // lógica...
        $this->dispatcher->dispatch(new OrderCompletedEvent($order));
    }
}

EventDispatcherInterface emite eventos custom. Crea una clase evento que extienda Event. Los listeners reaccionan al evento sin acoplar lógica. Patrón observer para desacoplar módulos.

Eventos del Kernel
kernel.request      // antes del controller
kernel.controller   // controller resuelto
kernel.view         // el controller retornó no-Response
kernel.response     // antes de enviar la respuesta
kernel.terminate    // después de enviar la respuesta
kernel.exception    // cuando ocurre una excepción

El ciclo de vida HTTP de Symfony emite eventos en cada fase. kernel.request para pre-procesamiento. kernel.exception para error handling. kernel.terminate para tareas post-respuesta (emails, logs).

Message Handlers
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class SendEmailHandler
{
    public function __invoke(SendEmailMessage $message): void
    {
        // enviar email de forma asíncrona
        $this->mailer->send($message->to, $message->subject);
    }
}
// php bin/console messenger:consume async -vv

#[AsMessageHandler] registra el handler. El type-hint del __invoke determina qué mensaje trata. messenger:consume procesa la cola. -vv muestra logs detallados.

Comandos de Consola
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

#[AsCommand(name: 'app:clear-cache', description: 'Limpia datos antiguos')]
class ClearCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('¡Cache limpiada!');
        return Command::SUCCESS;
    }
}

#[AsCommand] registra el comando con nombre y descripción. execute() contiene la lógica. Command::SUCCESS retorna exit code 0. writeln() imprime en la terminal.

Configurar Transport
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                retry_strategy:
                    max_retries: 3
                    delay: 1000
        routing:
            App\Message\SendEmailMessage: async

transports define adónde van los mensajes. routing mapea clases a transports. retry_strategy reprocesa fallos con delay. El DSN soporta Doctrine, AMQP y Redis.