Symfony 6 / Sonata 5: Inicializar un proyecto con Symfony 6 + Sonata Admin 5 + UserBundle + MediaBundle

Aquí están los comandos para inicializar un proyecto Symfony con un administrador seguro.

Para ejecutar Symfony 6 necesitaremos instalar php8.0 o php8.1

Para php8.0

sudo apt-get install php8.0-cli libapache2-mod-php8.0 php8.0-common php8.0-opcache php8.0-igbinary php8.0-imagick  php8.0-msgpack php8.0-readline  php8.0-memcached  php8.0-xml php8.0-mbstring php8.0-gd php8.0-mysql php8.0-curl php8.0-intl php8.0-memcache php8.0-memcached  memcached libapache2-mod-php8.0 php8.0-zip php8.0-mysql

Para php8.1

sudo apt-get install php8.1-cli libapache2-mod-php8.1 php8.1-common php8.1-opcache php8.1-igbinary php8.1-imagick  php8.1-msgpack php8.1-readline  php8.1-memcached  php8.1-xml php8.1-mbstring php8.1-gd php8.1-mysql php8.1-curl php8.1-intl php8.1-memcache php8.1-memcached  memcached libapache2-mod-php8.1 php8.1-zip php8.1-mysql

Luego instalamos el esqueleto del proyecto.

php8.1 composer.phar create-project symfony/skeleton:"6.0.x-dev" skeleton-sf6
cp composer.phar skeleton-sf6/
cd skeleton-sf6
php8.1 composer.phar require webapp

En este punto, comprobamos que todo funciona lanzando el servidor web incorporado de PHP.

php8.1 -S localhost:8000 -t public

Esto nos da la siguiente URL en nuestro navegador: http://localhost:8000/

Selection_249
Symfony 6.0.6

A continuación, instalamos el user-bundle, que es necesario para el administrador de Sonata. Instalará todas las dependencias necesarias, incluyendo Sonata/admin.
Nos aseguramos de rebajar Symfony/translation que es demasiado reciente en nuestro esqueleto para Sonata en este momento.

php8.1 composer.phar require symfony/translation-contracts:2.5
php8.1 composer.phar require sonata-project/user-bundle:5.x-dev
php8.1 bin/console assets:install

Nos encontramos con un primer problema:

 The child config "resetting" under "sonata_user" must be configured. 

Para esto, necesitamos agregar una configuración predeterminada en /config/packages/sonata_user.yaml

#####/config/packages/sonata_user.yaml
sonata_user:
    class:
        user: App\Entity\User
    resetting:
        email:
            address: "test@test.com"
            sender_name: Backoffice

Y hacemos un console/bin cache:clear

php8.1 bin/console cache:clear

Luego instalamos Sonata/admin. Al instalar doctrine-orm-admin-bundle instalaremos automáticamente admin-bundle, evitando cualquier conflicto.

php8.1 composer.phar require sonata-project/doctrine-orm-admin-bundle
php8.1 bin/console assets:install


Comprobamos nuestro sitio desde el servidor web.

php8.1 -S localhost:8000 -t public

Deberíamos obtener el siguiente error:

An exception has been thrown during the rendering of a template ("Asset manifest file "/public/build/manifest.json" does not exist.").
Il faudra installer webpack
yarn add --dev @symfony/webpack-encore
yarn add webpack-notifier --dev
yarn encore dev

Sonata admin está correctamente instalado. La autenticación aún no está configurada y todavía faltan la gestión de medios y las clasificaciones (veremos las clasificaciones en un artículo futuro).

image

Instalamos y configuramos los ACLs.

php8.1 composer.phar require symfony/acl-bundle

Configuramos sonata_user.yml

#####/config/packages/sonata_user.yaml

sonata_user:
    class:
        user: App\Entity\User
    resetting:
        email:
            address: "test@test.com"
            sender_name: Backoffice
            
    security_acl: true
    manager_type: orm # can be orm or mongodb


Configuramos security.yaml

#####/config/packages/security.yaml
security:
    enable_authenticator_manager: true
    password_hashers:
        Sonata\UserBundle\Model\UserInterface:
            algorithm: auto
    providers:
        sonata_user_bundle:
            id: sonata.user.security.user_provider
    access_decision_manager:
        strategy: unanimous   
    
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            lazy: true
            pattern: /admin(.*)
            provider: sonata_user_bundle
            context: user
            switch_user: true
            form_login:
                login_path: sonata_user_admin_security_login
                check_path: sonata_user_admin_security_check
                default_target_path: sonata_admin_dashboard
            logout:
                path: sonata_user_admin_security_logout
                target: sonata_user_admin_security_login
            remember_me:
                #secret: "%env(APP_SECRET)%"
                secret: "123456"
                lifetime: 2629746
                path: /admin


    
    access_control:
        - { path: ^/admin/login$, role: PUBLIC_ACCESS }
        - { path: ^/admin/logout$, role: PUBLIC_ACCESS }
        - { path: ^/admin/login_check$, role: PUBLIC_ACCESS }
        - { path: ^/admin/request$, role: PUBLIC_ACCESS }
        - { path: ^/admin/check-email$, role: PUBLIC_ACCESS }
        - { path: ^/admin/reset/.*$, role: PUBLIC_ACCESS }
        - { path: ^/admin/, role: ROLE_ADMIN }    
    
    role_hierarchy:
        ROLE_ADMIN:
            - ROLE_USER
            - ROLE_SONATA_ADMIN
            - ROLE_SONATA_USER_ADMIN_USER_VIEW
        ROLE_SUPER_ADMIN:
            - ROLE_ADMIN
            - ROLE_ALLOWED_TO_SWITCH        
   

Agregamos las rutas de admin.

#####/config/route.yaml
sonata_user_admin_security:
    resource: '@SonataUserBundle/Resources/config/routing/admin_security.xml'
    prefix: /admin

sonata_user_admin_resetting:
    resource: '@SonataUserBundle/Resources/config/routing/admin_resetting.xml'
    prefix: /admin/resetting

Creamos nuestra entidad de usuario.

#/src/Entity/User.php
<?php 
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Sonata\UserBundle\Entity\BaseUser;

/**
 * @ORM\Entity
 * @ORM\Table(name="user__user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected $id;
}

Actualizamos el esquema de la base de datos y creamos nuestro usuario admin.

php8.1 bin/console doctrine:schema:update --force
php8.1 bin/console sonata:user:create admin admin@test.com admin123456
php8.1 bin/console sonata:user:promote --super-admin admin

Reiniciamos nuestro servidor y probamos la URL: http://localhost:8000/admin/dashboard

php8.1 -S localhost:8000 -t public

Somos correctamente redirigidos a la pantalla de inicio de sesión.

Selection_260

El usuario está autorizado para entrar en el admin.

image-1

Ahora configuramos la gestión de medios.

Creamos 3 entidades: Galería, Item de Galería y Medio en una estructura de directorios separada para no abarrotar nuestra aplicación. Haremos lo mismo para UserBundle y ClassificationBundle.
Colocaremos todo en src/Application/Sonata/MediaBundle/Entity

<?php

declare(strict_types=1);

namespace App\Application\Sonata\MediaBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Sonata\MediaBundle\Entity\BaseGallery;

/**
 * @phpstan-extends BaseGallery<GalleryItem>
 *
 * @ORM\Entity
 * @ORM\Table(name="media__gallery")
 */
class Gallery extends BaseGallery
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private ?int $id = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}
<?php

declare(strict_types=1);

namespace App\Application\Sonata\MediaBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Sonata\MediaBundle\Entity\BaseGalleryItem;

/**
 * @ORM\Entity
 * @ORM\Table(name="media__gallery_media")
 */
class GalleryItem extends BaseGalleryItem
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private ?int $id = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}
<?php

declare(strict_types=1);

namespace App\Application\Sonata\MediaBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Sonata\MediaBundle\Entity\BaseMedia;
use App\Application\Sonata\ClassificationBundle\Entity\SonataClassificationCategory as SonataClassificationCategory;

/**
 * @ORM\Entity
 * @ORM\Table(name="media__media")
 */
class Media extends BaseMedia
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private ?int $id = null;
    
    /**
     * @var SonataClassificationCategory|null
     */
    protected ?object $category = null;

    public function getId(): ?int
    {
        return $this->id;
    }
    
    
    public function getCategory(): ?object
    {
        return $this->category;
    }
    
    public function setCategory(?object $category = null): void
    {
        $this->category = $category;
    }
}

Agregamos la configuración en sonata_media.yaml

sonata_media:
    class:
        media: App\Application\Sonata\MediaBundle\Entity\Media
        gallery: App\Application\Sonata\MediaBundle\Entity\Gallery
        gallery_item: App\Application\Sonata\MediaBundle\Entity\GalleryItem
    default_context: default
    db_driver: doctrine_orm
    providers:
        file:
            allowed_extensions: [jpg, png, jpeg, pdf, ogv, mp4, webm]
            allowed_mime_types:
                - image/pjpeg
                - image/jpeg
                - image/png
                - image/x-png
                - application/pdf
                - application/x-pdf
                - application/ogg
                - video/mp4
                - video/webm

    contexts:
        default:
            providers:
                - sonata.media.provider.dailymotion
                - sonata.media.provider.youtube
                - sonata.media.provider.image
                - sonata.media.provider.file
                - sonata.media.provider.vimeo

            formats:
                small: { width: 100 , quality: 70}
                big:   { width: 500 , quality: 70}

    cdn:
        server:
            path: /upload/media

    filesystem:
        local:
            # Directory for uploads should be writable
            directory: "%kernel.project_dir%/public/upload/media"
            create: false

Agregamos nuestra estructura de override para MediaBundle en config/packages/doctrine.yaml

doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'

        # IMPORTANT: You MUST configure your server version,
        # either here or in the DATABASE_URL env var (see .env file)
        #server_version: '13'
    orm:
        auto_generate_proxy_classes: true
        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
        auto_mapping: true
        mappings:
            App:
                is_bundle: false
                dir: '%kernel.project_dir%/src/Entity'
                prefix: 'App\Entity'
                alias: App

            App\Application\Sonata\MediaBundle:
                is_bundle: false
                dir: '%kernel.project_dir%/src/Application/Sonata/MediaBundle/Entity'
                prefix: 'App\Application\Sonata\MediaBundle\Entity'
                alias: App\Application\Sonata\MediaBundle
 
        filters:
            softdeleteable:
                class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter

Actualizamos el esquema de la base de datos.

php8.1 bin/console doctrine:schema:update --force

Configuramos nuestro sonata_admin.yml predeterminado.

sonata_admin:
    title: Backoffice
    title_logo: /bundles/sonataadmin/images/logo_title.png
    show_mosaic_button: false
    security:
        handler: sonata.admin.security.handler.role
    options:
        default_admin_route: edit
        html5_validate: false
    global_search:
        admin_route: edit
    breadcrumbs:
        child_admin_route: edit
    dashboard:
        groups:
            users:
                label: Users
                icon: <i class="fa fa-users"></i>
                on_top: true
                items:
                    - sonata.user.admin.user
            media:
                label: Media
                icon: <i class="fa fa-photo"></i>
                on_top: true
                items:
                    - sonata.media.admin.media

Y todo funciona.

image-2
image-3