Here are the commands to initialize a Symfony project with a secure admin.
To run Symfony 6 we will need to install php8.0 or php8.1
For 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
For 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
Then we install the project's skeleton.
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
At this point, we check that everything is working by launching the PHP built-in web server.
php8.1 -S localhost:8000 -t public
This gives us the following URL in our browser: http://localhost:8000/

Next, we install the user-bundle, which is necessary for the Sonata
admin. It will install all the necessary dependencies, including
Sonata/admin.
We take care to downgrade Symfony/translation
which is too recent in our skeleton for Sonata at this time.
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
We encounter a first problem:
The child config "resetting" under "sonata_user" must be configured.
For this, we need to add a default configuration in /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
And do a console/bin cache:clear
php8.1 bin/console cache:clear
We then install Sonata/admin. By installing doctrine-orm-admin-bundle we will automatically install admin-bundle, avoiding any conflicts.
php8.1 composer.phar require sonata-project/doctrine-orm-admin-bundle
php8.1 bin/console assets:install
We check our site from the web server.
php8.1 -S localhost:8000 -t public
We should get the following 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 is correctly installed. The authentication is not configured yet, and we still miss the media management and classifications (we will see classifications in a future article).

We install and configure the ACLs.
php8.1 composer.phar require symfony/acl-bundle
We configure 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
We configure 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
We add the admin routes.
#####/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
We create our user entity.
#/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;
}
We update the database schema and create our admin user.
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
We restart our server and test the URL: http://localhost:8000/admin/dashboard
php8.1 -S localhost:8000 -t public
We are correctly redirected to the login screen.

The user is authorized to enter the admin.

We now configure the media management.
We create 3 entities: Gallery, GalleryItem, and Media in a separate
directory structure so as not to clutter our application. We will
do the same for UserBundle and ClassificationBundle.
We will
place everything in 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;
}
}
We add the configuration in 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
We add our override structure for MediaBundle in 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
We update the database schema.
php8.1 bin/console doctrine:schema:update --force
We configure our default sonata_admin.yml.
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
And everything works.

