Приветствую всех. В данной статье поговорим об Symfony 4 и Sonata Admin.
В процессе установки я столкнулся с массой неточностей в документации и сама документация была раскидана по нескольким местам. Здесь я рассмотрю весь процесс, начиная от создания проекта и заканчивая авторизацией вместе с аутентификацией.
Отдельные части настроек взяты из официальной документации, часть бралась из комментариев на GitHub, где обсуждались проблемы при установке. Также расписаны возможные подводные камни и пути их обхода.
Создаем проект на symfony
$ composer create-project symfony/skeleton sonatademo
$ cd sonatademo
Для работы с встроенным веб сервером Symfony нужно установить Symfony Client.
Запускаем.
$ symfony serve
Переходим по ссылке http://127.0.0.1:8000/ и получим стандартное приветствие Symfony. Значит все работает корректно.
Устанавливаем Sonata Admin
$ composer require sonata-project/admin-bundle
Sonata Admin и БД
Для того, чтобы взаимодействовать с базой данных нужно установить одну из трех библиотек:
- SonataDoctrineORMAdminBundle (интеграция с Doctrine ORM);
- SonataDoctrineMongoDBAdminBundle (интеграция с Doctrine MongoDB ODM);
- SonataDoctrinePhpcrAdminBundle (интеграция с Doctrine PHPCR ODM).
В данной статья я использую SonataDoctrineORMAdminBundle
, что более чем достаточно для работы с MySQL
или Sqlite
.
Устанавливаем SonataDoctrineORMAdminBundle
.
$ composer require sonata-project/doctrine-orm-admin-bundle
Теперь настроим работу с Sqlite
.
Открываем .env файл и в секции ###> doctrine/doctrine-bundle ###
меняем DATABASE_URL
.
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
Так как Symfony Flex делает почти все работу за нас при установке, то остается сделать несколько манипуляций для получения конечного результата.
# config/packages/framework.yaml
framework:
translator: { fallbacks: ['en'] }
Выполняем
$ composer dump-env dev
Теперь переходим по ссылке http://127.0.0.1:8000/admin и видим пустой стандартный административный интерфейс.
Создание сущностей
Создадим две сущности для примера, связанные между собой связью один ко многим.
<?php
// src/Entity/City.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class City
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer", options={"unsigned":true})
*/
private $id;
/**
* @var string
*
* @ORM\Column(type="string")
*/
private $title;
/**
* @var string
*
* @ORM\Column(type="text")
*/
private $description;
/**
* @var bool
*
* @ORM\Column(type="boolean")
*/
private $isBig = false;
/**
* @ORM\OneToMany(targetEntity="Address", mappedBy="city")
*/
private $addresses;
public function __construct()
{
$this->addresses = new ArrayCollection();
}
public function getAddresses()
{
return $this->addresses;
}
/**
* @return string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @param string $title
* @return City
*/
public function setTitle(string $title): City
{
$this->title = $title;
return $this;
}
/**
* @return string
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @param string $description
* @return City
*/
public function setDescription(string $description): City
{
$this->description = $description;
return $this;
}
/**
* @return bool
*/
public function isBig(): ?bool
{
return $this->isBig;
}
/**
* @param bool $isBig
* @return City
*/
public function setIsBig(bool $isBig): City
{
$this->isBig = $isBig;
return $this;
}
public function __toString()
{
return $this->title;
}
}
<?php
// src/Entity/Address.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Address
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer", options={"unsigned":true})
*/
private $id;
/**
* @var string
*
* @ORM\Column(type="string")
*/
private $title;
/**
* @var string
*
* @ORM\Column(type="text")
*/
private $description;
/**
* @ORM\ManyToOne(targetEntity="City", inversedBy="addresses")
*/
private $city;
/**
* @return string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @param string $title
* @return Address
*/
public function setTitle(string $title): Address
{
$this->title = $title;
return $this;
}
/**
* @return string
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @param string $description
* @return Address
*/
public function setDescription(string $description): Address
{
$this->description = $description;
return $this;
}
/**
* @return City
*/
public function getCity(): ?City
{
return $this->city;
}
/**
* @param City $city
* @return Address
*/
public function setCity(City $city)
{
$this->city = $city;
return $this;
}
public function __toString()
{
return $this->title;
}
}
Синхронизируемся с БД.
bin/console doctrine:schema:create
Настройка сущностей для Sonata Admin
Нужно для каждой сущности создать отдельный файл с описанием, как Sonata Admin должна работать.
<?php
// src/Admin/CityAdmin.php
namespace App\Admin;
use App\Entity\Address;
use App\Entity\City;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Form\Type\CollectionType;
use Sonata\AdminBundle\Form\Type\ModelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
final class CityAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('title', TextType::class);
$formMapper->add('description', TextareaType::class);
$formMapper->add('isBig', CheckboxType::class);
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('title');
$datagridMapper->add('isBig');
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('title');
$listMapper->addIdentifier('isBig');
}
}
<?php
// src/Admin/AddressAdmin.php
namespace App\Admin;
use App\Entity\City;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Form\Type\ModelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
final class AddressAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('title', TextType::class);
$formMapper->add('description', TextareaType::class);
$formMapper->add('city', ModelType::class, [
'class' => City::class,
'property' => 'title',
]);
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('title');
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('title');
}
}
Эти классы нужно описать в service.yaml
.
# config/service.yaml
services:
...
App\Admin\CityAdmin:
arguments: [~, App\Entity\City, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: City }
App\Admin\AddressAdmin:
arguments: [~, App\Entity\Address, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: Address }
Если при заходе в административную часть возникают ошибки, например Unable to generate a URL for the named route
, то нужно почистить кэш приложения и попробовать снова.
bin/console cache:clear
Теперь, если перейти по ссылке http://127.0.0.1:8000/admin увидим список из двух элементов Address
и City
, где можно просматривать списки и создавать новые сущности.
Авторизация и аутентификация
Для начала создадим сущность пользователя.
<?php
// src/Entity/User.php
namespace App\Entity;
use Sonata\UserBundle\Entity\BaseUser as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* @ORM\Id
* @ORM\Column(type="integer", options={"unsigned":true})
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
}
И заодно создадим сущность группы пользователя.
<?php
// src/Entity/Group.php
namespace App\Entity;
use Sonata\UserBundle\Entity\BaseGroup as BaseGroup;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="fos_group")
*/
class Group extends BaseGroup
{
/**
* @ORM\Id
* @ORM\Column(type="integer", options={"unsigned":true})
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
}
После настроим Twig как движок шаблонов.
framework:
...
templating:
engines: ['twig']
На данный момент есть проблемы с работой Symfony Flex и FOSUserBundle. Более подробно можно узнать по ссылкам #2562, #2708 и #2801.
Пока данные проблемы не решены, нужно сделать пару дополнительных манипуляций перед установкой Sonata User Bundle
.
# config/service.yaml
services:
...
mailer:
alias: fos_user.mailer.noop
public: true
# config/packages/fos_user.yaml
fos_user:
db_driver: orm
firewall_name: main
user_class: App\Entity\User
registration:
confirmation:
enabled: false
from_email:
address: '%env(MAILER_USER_ADDRESS)%'
sender_name: '%env(MAILER_USER_NAME)%'
service:
user_manager: sonata.user.orm.user_manager
mailer: 'fos_user.mailer.noop'
group:
group_class: App\Entity\Group
group_manager: sonata.user.orm.group_manager
После чего, можно устанавливать бандл.
$ composer require sonata-project/user-bundle
Настройка ACL
В Symfony 4 ACL вынесли в отдельный бандл symfony/acl-bundle
. Поэтому его нужно отдельно установить.
composer require symfony/acl-bundle
# config/packages/sonata_user.yaml
sonata_user:
security_acl: true
manager_type: orm
# config/packages/acl.yaml
acl:
connection: default
Настройка Doctrine
# config/packages/doctrine.yaml
doctrine:
orm:
mappings:
SonataUserBundle: ~
FOSUserBundle: ~
Настройка работы с почтой
Так как в рамках этой статьи работа с почтой не рассматриватеся, то укажем заглушку вместо реального сервиса.
# config/packages/sonata_user.yaml
sonata_user:
mailer: fos_user.mailer.noop
Интеграция User Bundle в Sonata Admin
# config/routes.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
# config/packages/security.yaml
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
providers:
fos_userbundle:
id: fos_user.user_provider.username
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# -> custom firewall for the admin area of the URL
admin:
pattern: /admin(.*)
context: user
form_login:
provider: fos_userbundle
login_path: /admin/login
use_forward: false
check_path: /admin/login_check
failure_path: null
logout:
path: /admin/logout
target: /admin/login
anonymous: true
# -> end custom configuration
# default login area for standard users
# This firewall is used to handle the public login area
# This part is handled by the FOS User Bundle
main:
pattern: .*
context: user
form_login:
provider: fos_userbundle
login_path: /login
use_forward: false
check_path: /login_check
failure_path: null
logout: true
anonymous: true
access_control:
# Admin login page needs to be accessed without credential
- { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/login_check$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
# Secured part of the site
# This config requires being logged for the whole site and having the admin role for the admin part.
# Change these rules to adapt them to your needs
- { path: ^/admin/, role: [ROLE_ADMIN, ROLE_SONATA_ADMIN] }
- { path: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
# config/packages/sonata_user.yaml
sonata_user:
...
class:
user: App\Entity\User
group: App\Entity\Group
Теперь можно обновить ACL и БД.
$ bin/console acl:init
$ php bin/console doctrine:schema:update --force
Создаем супер пользователя. Указываем имя demo
и пароль demo
.
$ bin/console fos:user:create --super-admin
Please choose a username:demo
Please choose an email:demo@demo.com
Please choose a password:
Created user demo
Для корректной настройки наших Admin классов вместе с ACL нужно внести изменения в настройки.
sonata_admin:
...
security:
handler: sonata.admin.security.handler.acl
После запустить следующее:
$ bin/console sonata:admin:setup-acl
Starting ACL AdminBundle configuration
> install ACL for App\Admin\AddressAdmin
- add role: ROLE_APP\ADMIN\ADDRESSADMIN_GUEST, permissions: ["LIST"]
- add role: ROLE_APP\ADMIN\ADDRESSADMIN_STAFF, permissions: ["LIST","CREATE"]
- add role: ROLE_APP\ADMIN\ADDRESSADMIN_EDITOR, permissions: ["OPERATOR","EXPORT"]
- add role: ROLE_APP\ADMIN\ADDRESSADMIN_ADMIN, permissions: ["MASTER"]
> install ACL for App\Admin\CityAdmin
- add role: ROLE_APP\ADMIN\CITYADMIN_GUEST, permissions: ["LIST"]
- add role: ROLE_APP\ADMIN\CITYADMIN_STAFF, permissions: ["LIST","CREATE"]
- add role: ROLE_APP\ADMIN\CITYADMIN_EDITOR, permissions: ["OPERATOR","EXPORT"]
- add role: ROLE_APP\ADMIN\CITYADMIN_ADMIN, permissions: ["MASTER"]
> install ACL for sonata.user.admin.user
- add role: ROLE_SONATA_USER_ADMIN_USER_GUEST, permissions: ["LIST"]
- add role: ROLE_SONATA_USER_ADMIN_USER_STAFF, permissions: ["LIST","CREATE"]
- add role: ROLE_SONATA_USER_ADMIN_USER_EDITOR, permissions: ["OPERATOR","EXPORT"]
- add role: ROLE_SONATA_USER_ADMIN_USER_ADMIN, permissions: ["MASTER"]
> install ACL for sonata.user.admin.group
- add role: ROLE_SONATA_USER_ADMIN_GROUP_GUEST, permissions: ["LIST"]
- add role: ROLE_SONATA_USER_ADMIN_GROUP_STAFF, permissions: ["LIST","CREATE"]
- add role: ROLE_SONATA_USER_ADMIN_GROUP_EDITOR, permissions: ["OPERATOR","EXPORT"]
- add role: ROLE_SONATA_USER_ADMIN_GROUP_ADMIN, permissions: ["MASTER"]
Теперь при попытке зайти по адресу http://127.0.0.1:8000/admin нас перенаправит на http://127.0.0.1:8000/admin/login.
Вводим demo/demo и попадаем в административную часть.
Итоги
После всех манипуляций мы получили работающую административную часть Sonata Admin на Symfony 4 вместе с аутентификацией и авторизацией c помощью Sonata User + ACL.
Всем спасибо. Если будут вопросы и замечания, то я выслушаю их в комментариях.