Plus d'un tour dans ma Stack :

l'équipement DDD pour fonder notre CSE 2.0

@stlgaits

Sylius Stack lover ❤️

Trivia & cinema quiz maniac 🎞️

Board game nerd 🎲

Ex-teacher turned developer 👩‍🏫

Staff representative @Akawaka 📣

Estelle Gaits

Sylius Key Contributor

 

@stlgaits

 

4 Sylius Key Contributors

Expertise technique Sylius, Symfony, IA et bien plus encore 🦕

Centre de service TMA/TME

Audit & conseil

@stlgaits

Registre spécial des réclamations

L'employeur doit y enregistrer les réclamations et leurs réponses dans l'ordre.

Consultable par l'inspection du travail & salariés.

Réclamations des salarié·es

 

Notifiées à l'employeur par écrit 2 jours ouvrables avant la réunion.

L'employeur a 6 jours ouvrables pour répondre à l'écrit.

1 réunion par mois minimum

 

Employeur

Délégation du personnel

Comité Social & Economique : 11 salarié·es

Objectif export PDF du registre des réclamations

Registre Spécial des Réclamations

@stlgaits

Registre Spécial des Réclamations

La tcheeeam

Cliente

Référente Sylius Stack

Chef de projet

Architecte

Référent IA

Devs

Trello specs

 

User Stories

Définition du langage métier

 

Envoyé au chef de projet

 

 

Domain Driven Design

 

Sylius Stack

Symfony UX Toolkit ?

Archi hexa DDD : deptrac

Doctrine

 

Quelle archi ? Quelle stack  ?

@stlgaits

https://github.com/deptrac/deptrac

Sylius Stack à la rescousse !

@stlgaits

CRUD

grilles paginées, triables & filtrables (custom)

actions

 

Symfony ❤️

Security : granularité des droits d'accès

Twig Components

State Machine

exports PDF avec GotenbergBundle

Scheduler & Mailer

Sylius Stack

@stlgaits

Sylius Grid Bundle

Sylius Resource Bundle

Twig Hooks

Bootstrap Admin UI

Symfony UX & Asset Mapper

Doctrine ORM & DBAL Drivers

Providers & Processors system

@stlgaits

Bounded Contexts

 

et un intrus ... !

@stlgaits

Bounded Contexts : Resource config

 

# @see https://stack.sylius.com/resource/index
sylius_resource:
    mapping:
        paths:
            - '%kernel.project_dir%/src/Admin/Infrastructure/Resource'
            - '%kernel.project_dir%/src/Session/Infrastructure/Resource'
config/packages/sylius_resource.yaml

@stlgaits

3 couches

 

4ème couche possible ?

Grid (agencement visuel)

Resource (routing & model)

Doctrine Entity

Grid Provider

Liste des réclamations

@stlgaits

Entité Doctrine

#[ORM\Entity(repositoryClass: DemandRepository::class)]
class Demand
{
    public function __construct(
        #[ORM\Column(type: Types::TEXT)]
        private string $content,
        #[ORM\ManyToOne(inversedBy: 'demands')]
        #[ORM\JoinColumn(nullable: false)]
        private Session $session,
        #[ORM\Column(enumType: DemandStatus::class, options: ['default' => DemandStatus::DRAFT])]
        private DemandStatus $status = DemandStatus::DRAFT,
    ) {}
    

@stlgaits

Ressource

 

Modèle

Routing

Opérations

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;

#[AsResource(
    section: 'admin',
    templatesDir: '@SyliusAdminUi/crud',
    routePrefix: 'admin',
    operations: [
        new Index(
            grid: DemandGrid::class,
        ),
    ],
)]
class DemandResource implements ResourceInterface
{
    public function __construct(
        public ?int $id = null,
        public string $content = '',
        public ?Session $session = null,
        public DemandStatus $status = DemandStatus::DRAFT,
    ) {}
}

// ...

@stlgaits

Grid

 

symfony console make:grid
src/Session/Infrastructure/Grid

@stlgaits

Grid

 

symfony console make:grid
#[AsGrid(
    resourceClass: Demand::class,
    name: 'app_demand',
)]
final class DemandGrid extends AbstractGrid
{
    public function __construct() {}
    
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder 
            // see https://stack.sylius.com/grid/index/field_types
            ->withFields(
                DateTimeField::create('createdAt')
                    ->setLabel('CreatedAt'),
                DateTimeField::create('publishedAt')
                    ->setLabel('PublishedAt'),
                StringField::create('content')
                    ->setLabel('Content')
                    ->setSortable(true),
                StringField::create('status')
                    ->setLabel('Status')
                    ->setSortable(true),
            )
        ;
    }
}

withFields()

 

withFilters()

 

@stlgaits

Grid

 

    use Sylius\Bundle\GridBundle\Builder\Field\FieldInterface;
    
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
      $gridBuilder->withFields(...$this->getFields());
      
    }

    /**
     * @return iterable<FieldInterface>
     */
    private function getFields(): iterable
    {
    	yield StringField::create('number')
        	->setLabel('app.ui.demand.number')
        	->setSortable(true);	

        yield StringField::create('content')
        	->setLabel('app.ui.demand.content')
        	->setSortable(true);

        yield TwigField::create('status', 'admin/demand/grid/field/status.html.twig')
        	->setLabel('app.ui.demand.status');

        yield DateTimeField::create('publishedAt')
        	->setOption('format', 'd/m/Y H:i:s')
       		->setLabel('app.ui.demand.published_at')
        	->setSortable(true)
        ;
    }

iterables 4 the win !

src/Session/Infrastructure/Grid/DemandGrid.php

Grid & Security : access control with SF AuthorizationChecker

 

use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class DemandGrid extends AbstractGrid
{
    public function __construct(private readonly AuthorizationCheckerInterface $authorizationChecker) {}
    
    private function getFields(): iterable
    {
        yield TwigField::create('status', 'admin/demand/grid/field/status.html.twig')
            ->setLabel('app.ui.demand.status')
        ;

        if ($this->authorizationChecker->isGranted('ROLE_STAFF_REPRESENTATIVE')) {
            yield DateTimeField::create('publishedAt')
                ->setOption('format', 'd/m/Y H:i:s')
                ->setLabel('app.ui.demand.published_at')
                ->setSortable(true)
            ;
        }
    }
}
src/Session/Infrastructure/Grid/DemandGrid.php
src/Session/Infrastructure/Grid/DemandGrid.php

@stlgaits

Filtrer les réclamations par statut

 

@stlgaits

Filtrer les réclamations par statut

 

@stlgaits

Filtrer les réclamations par statut

 

/**
* @return iterable<FilterInterface>
*/
private function manageFilters(): iterable
{
		yield EnumFilter::create(name: 'status', enumClass: DemandStatus::class, field: 'status')
            ->setTemplate('shared/grid/filter/enum.html.twig')
            ->addFormOption('choice_value', fn (?DemandStatus $enum) => $enum?->value)
            ->addFormOption('choice_label', fn (DemandStatus $choice): string => ucfirst($this->translator->trans('app.ui.demand.statuses.'.$choice->value)))
            ->setLabel('app.ui.demand.status')
        ;

}
src/Session/Infrastructure/Grid/DemandGrid.php

@stlgaits

EnumFilter template : upgrade to 0.11 !

 

@stlgaits

Providers & Processors

Sylius Stack // API Platform 

@stlgaits

Vue détaillée : show

Vue détaillée (show)

use App\Session\Infrastructure\Provider\DemandItemProvider;

#[AsResource(
    operations: [
        new Show(
        routeRequirements: ['id' => '\d+'], 
        provider: DemandItemProvider::class),
    ],
)]
class DemandResource implements ResourceInterface {}
src/Session/Infrastructure/Resource/DemandResource.php

Vue détaillée (show)

ItemProvider

use Sylius\Resource\Context\Context;
use Sylius\Resource\Context\Option\RequestOption;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\State\ProviderInterface;

final readonly class DemandItemProvider implements ProviderInterface
{
    public function __construct(
        private DemandRepository $demandRepository,
    ) {}

    public function provide(Operation $operation, Context $context): ?DemandResource
    {
        $request = $context->get(RequestOption::class)?->request();
        Assert::notNull($request);

        $id =  $request->attributes->get('id');

        $model = $this->demandRepository->find($id);

        return null !== $model ? DemandResource::fromEntity($model) : null;
    }
}

Vue détaillée (show)

Twig template

{% set demand = hookable_metadata.context.demand %}

 ....
 <tr>
 	<td class="five wide"><strong class="gray text">{{ 'app.ui.demand.content'|trans }}</strong></td>
 	<td>{{ demand.content|nl2br }}</td>
 </tr>
 {% if is_granted('ROLE_STAFF_REPRESENTATIVE') %}
   <tr>
       <td class="five wide"><strong class="gray text">{{ 'app.ui.demand.status'|trans }}</strong></td>
        <td>
            <span class="badge {{ demand.status.getColor() }}">
            {{ demand.status.getLabel()|trans }}
            </span>
        </td>
   </tr>
 {% endif %}
templates/admin/demand/show.html.twig

@stlgaits

Ajouter une réclamation : create

@stlgaits

Ajouter une réclamation : create

use App\Session\Infrastructure\Processor\CreateDemandProcessor;
use App\Session\Infrastructure\Type\DemandType;
use Sylius\Resource\Metadata\Create;

#[AsResource(
    formType: DemandType::class,
    operations: [
    	new Create(
            processor: CreateDemandProcessor::class,
            redirectToRoute: 'app_admin_demand_index',
     	),
    ],
)]
class DemandResource implements ResourceInterface
{
}
src/Session/Infrastructure/Resource/DemandResource.php

@stlgaits

Create Processor

final readonly class CreateDemandProcessor implements ProcessorInterface
{
    public function __construct( private DemandRepository $demandRepository) {}

    public function process(mixed $data, Operation $operation, Context $context): DemandResource
    {
        Assert::isInstanceOf($data, DemandResource::class);

        $demand = new Demand(
            $data->content,
            $data->session ?? throw new \LogicException('Demand session cannot be empty.'),
            $data->number ?? Demand::DEFAULT_NUMBER,
            $data->status ?? throw new \LogicException('Demand status cannot be empty.'),
        );

        $this->demandRepository->save($demand);

        return DemandResource::fromEntity($demand);
    }
}
Session/Infrastructure/Processor/CreateDemandProcessor.php

@stlgaits

Twig Hooks

Personnaliser les éléments autour des vues

imports:
    - { resource: '@SyliusBootstrapAdminUiBundle/config/app.php' }
    - { resource: 'sylius/' }

Twig Hooks

config/packages/sylius_bootstrap_admin_ui.yaml

@stlgaits

@stlgaits

Twig Hooks : titre dynamique

@stlgaits

@stlgaits

Twig Hooks : nouvelle commande debug

 upgrade vers Sylius Stack 0.11

symfony console sylius:debug:twig-hooks

@stlgaits

@stlgaits

Twig Hooks : titre dynamique

@stlgaits

Twig Hooks : titre dynamique

$container->extension('sylius_twig_hooks', [
	'hooks' => [
  		'sylius_admin.answer.create.content.header.title_block' => [
        	'title' => [
            	'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/header/title_block/title.html.twig',
                	'configuration' => [
                    	'title' => '@=_context.answer.getDemand().getSession().getName()',
                        'icon' => 'fluent-mdl2:pencil-reply',
                    ],
            ],
        ],
    ],
]);

AnswerResource

config/packages/sylius/twig_hooks/admin/answers/create.php

@stlgaits

Twig Hooks : titre dynamique

'configuration' => [
	'title' => '@=_context.answer.getDemand().getSession().getName()',
	'icon' => 'fluent-mdl2:pencil-reply',
],

@=_context

Symfony UX icons

Contexte Twig

answer = resource

La Sylius Stack, c'est pas que pour les backoffices !

 

 

Time vs Theory vs Assumptions

When you assume,


you make an ass out of you and me.

DOMAIN-Driven Design...

Ne présupposez pas les besoins des clients !

DOMAIN-Driven Design...

Les mots du domaine devraient rester en français quand le domaine est français

Posez des questions, appelez-vous !!!

Le besoin prime absolument sur l'archi

@stlgaits

Merci ! 

Sylius Stack DDD cookbook

https://stack.sylius.com/cookbook/ddd_architecture

 

La dernière release

https://github.com/Sylius/Stack/releases/tag/v0.11.0

@stlgaits

Twig Hooks: customiser le fil d'Ariane

return static function (ContainerConfigurator $container): void {
    $container->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.user.update.content.header' => [
                'breadcrumbs' => [
                    'template' => '@SyliusBootstrapAdminUi/shared/crud/update/content/header/breadcrumbs.html.twig',
                    'configuration' => [
                        'rendered_field' => 'email',
                    ],
                ],
            ],
         ],
    ]);
};

rendered_field

UserResource

config/packages/sylius/twig_hooks/admin/users/update.php

@stlgaits

Twig Hooks: customiser le fil d'Ariane