Abbiamo visto in un articolo precedente come installare Symfony 6 + Sonata Admin 5 con gestione degli utenti e dei media.
Continueremo questa installazione con la configurazione delle lingue e dei fusi orari.
Installeremo intlBundle che permette di gestire la visualizzazione localizzata.
php8.1 composer.phar require sonata-project/intl-bundle
Approfittiamo di questa occasione per aggiungere un campo fuso orario e locale all'utente.
Per fare ciò, estenderemo userBundle e aggiungeremo un campo nell'admin.
Estendiamo user spostando l'entità utente in: src/Application/Sonata/UserBundle/Entity/User.php
E vi aggiungiamo un campo fuso orario e un campo locale.
<?php
namespace App\Application\Sonata\UserBundle\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;
/**
* @var string|null
*
* @ORM\Column(name="timezone", type="string", length=64, nullable=true)
*/
protected $timezone;
/**
* @var string|null
*
* @ORM\Column(name="locale", type="string", length=8, nullable=true)
*/
protected $locale;
public function getTimezone(){
return $this->timezone;
}
public function setTimezone($timezone){
$this->timezone=$timezone;
return $this;
}
public function getLocale(){
return $this->locale;
}
public function setLocale($locale){
$this->locale=$locale;
return $this;
}
}
Duplichiamo l'admin di Sonata e lo collochiamo in src/Application/Sonata/UserBundle/Admin/Model/UserAdmin.php
Aggiungiamo al suo interno il nostro campo fuso orario.
<?php
declare(strict_types=1);
/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Application\Sonata\UserBundle\Admin\Model;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\UserBundle\Form\Type\RolesMatrixType;
use Sonata\UserBundle\Model\UserManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
/**
* @phpstan-extends AbstractAdmin<\Sonata\UserBundle\Model\UserInterface>
*/
class UserAdmin extends AbstractAdmin
{
protected UserManagerInterface $userManager;
public function __construct(UserManagerInterface $userManager)
{
parent::__construct();
$this->userManager = $userManager;
}
protected function preUpdate(object $object): void
{
$this->userManager->updatePassword($object);
}
protected function configureFormOptions(array &$formOptions): void
{
$formOptions['validation_groups'] = ['Default'];
if (!$this->hasSubject() || null === $this->getSubject()->getId()) {
$formOptions['validation_groups'][] = 'Registration';
} else {
$formOptions['validation_groups'][] = 'Profile';
}
}
protected function configureListFields(ListMapper $list): void
{
$list
->addIdentifier('username')
->add('email')
->add('enabled', null, ['editable' => true])
->add('createdAt');
if ($this->isGranted('ROLE_ALLOWED_TO_SWITCH')) {
$list
->add('impersonating', FieldDescriptionInterface::TYPE_STRING, [
'virtual_field' => true,
'template' => '@SonataUser/Admin/Field/impersonating.html.twig',
]);
}
}
protected function configureDatagridFilters(DatagridMapper $filter): void
{
$filter
->add('id')
->add('username')
->add('email');
}
protected function configureShowFields(ShowMapper $show): void
{
$show
->add('username')
->add('email');
}
protected function configureFormFields(FormMapper $form): void
{
$form
->with('general', ['class' => 'col-md-4'])
->add('username')
->add('email')
->add('locale', LocaleType::class, ['required' => false])
->add('timezone', TimezoneType::class, ['required' => false])
->add('plainPassword', TextType::class, [
'required' => (!$this->hasSubject() || null === $this->getSubject()->getId()),
])
->add('enabled', null)
->end()
->with('roles', ['class' => 'col-md-8'])
->add('realRoles', RolesMatrixType::class, [
'label' => false,
'multiple' => true,
'required' => false,
])
->end();
}
protected function configureExportFields(): array
{
// Avoid sensitive properties to be exported.
return array_filter(parent::configureExportFields(), static function (string $v): bool {
return !\in_array($v, ['password', 'salt'], true);
});
}
}
Modifichiamo i riferimenti della nostra entità e del nostro admin in sonata_user.yml
sonata_user:
class:
user: App\Application\Sonata\UserBundle\Entity\User
admin:
user:
class: App\Application\Sonata\UserBundle\Admin\Model\UserAdmin
resetting:
email:
address: "test@test.com"
sender_name: Backoffice
security_acl: true
manager_type: orm # can be orm or mongodb
e registriamo la nostra estensione in doctrine.yml
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
App\Application\Sonata\UserBundle:
is_bundle: false
dir: '%kernel.project_dir%/src/Application/Sonata/UserBundle/Entity'
prefix: 'App\Application\Sonata\UserBundle\Entity'
alias: App
Aggiorniamo il nostro schema e facciamo un test.
php8.1 bin/console doctrine:schema:update --force
Il nostro nuovo campo è correttamente preso in considerazione.
Impostiamo il locale predefinito su FR
#config/packages/translation.yaml
framework:
default_locale: fr
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
- fr
Configuriamo sonata_intl
sonata_intl:
timezone:
default: Europe/Paris
locales:
fr: Europe/Paris
en_UK: Europe/London
detectors:
- sonata.intl.timezone_detector.user
- sonata.intl.timezone_detector.locale
Adesso desideriamo che le impostazioni di fuso orario e lingua predefinita del nostro utente siano prese in considerazione.
Per fare ciò, recupereremo le sue impostazioni al momento del login e le metteremo nella sessione.
Quindi, recupereremo queste informazioni ad ogni caricamento della pagina e sovraccaricheremo l'interfaccia con queste impostazioni.
Come primo passo, otteniamo le informazioni al login. Per fare ciò, specificheremo il servizio da utilizzare durante l'handler_success in security.yaml.
Impostiamo success_handler: login_success_handler
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
success_handler: login_success_handler
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
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
Quindi aggiungiamo il nostro servizio in service.yaml
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
login_success_handler:
parent: security.authentication.success_handler
class: App\EventListener\LoginSuccessHandler
E creiamo il nostro Handler in src/EventListener/LoginSuccessHandler.php
<?php
namespace App\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
class LoginSuccessHandler extends DefaultAuthenticationSuccessHandler
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
{
$locale = $token->getUser()->getLocale();
$request->getSession()->set('_locale', $locale);
$request->setLocale($locale);
$timezone = $token->getUser()->getTimezone();
$request->getSession()->set('_timezone', $timezone);
return $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));
}
}
A questo punto, abbiamo le preferenze nella sessione al momento del login. Ora aggiungiamo 2 Listeners. LocaleListener e TimezoneListener.
Li aggiungiamo ai servizi.
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
login_success_handler:
parent: security.authentication.success_handler
class: App\EventListener\LoginSuccessHandler
App\EventListener\LocaleListener:
class: App\EventListener\LocaleListener
arguments: [ "%kernel.default_locale%"]
tags:
- { name: kernel.event_subscriber }
App\EventListener\TimeZoneListener:
class: App\EventListener\TimeZoneListener
arguments: ["@twig"]
tags:
- { name: kernel.event_subscriber }
E creiamo i nostri listeners.
<?php
#src/EventListener/LocaleListener.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class LocaleListener implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct($defaultLocale = 'fr')
{
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
<?php
#src/EventListener/TimeZoneListener.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Twig\Environment;
use Twig\Extension\CoreExtension;
/**
* Class TimeZoneListener
*
* @package App\EventListener
*/
class TimeZoneListener implements EventSubscriberInterface
{
/** @var twig */
private $twig;
public function __construct( $twig)
{
$this->twig = $twig;
}
public function onKernelRequest( RequestEvent $request)
{
$request = $request->getRequest();
$timezone = $request->getSession()->get('_timezone', "Europe/Paris");
$core = $this->twig->getExtension('Twig\Extension\CoreExtension');
$core->setTimezone($timezone);
return $core;
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array(array('onKernelRequest', 18)),
);
}
}
Non ci resta che verificare che sia stato preso in considerazione.
Prima impostiamo le nostre preferenze in locale inglese e fuso orario dall'altra parte del mondo.
Non abbiamo una traduzione in vietnamita, quindi ricorriamo all'EN. Tuttavia, abbiamo la lista delle località tradotte.
L'interfaccia è in inglese e le date sono tradotte in vietnamita, e l'orario corrisponde al fuso orario di HCMC.
Adesso torniamo a FR e Europe/Paris.
La nostra interfaccia è in francese e anche le caselle di selezione.
E le nostre date sono scritte nel formato FR e l'orario è impostato su Parigi. Le 11:24 a Parigi contro le 17:24 in Vietnam.