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:gridsrc/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

Plus d'un tour dans ma Stack : l'équipement DDD pour fonder notre CSE 2.0 - Sylius Meetup Bordeaux
By Estelle Gaits
Plus d'un tour dans ma Stack : l'équipement DDD pour fonder notre CSE 2.0 - Sylius Meetup Bordeaux
Vous connaissiez le « Registre Spécial des Réclamations » ? Nous, non. Quand on part de zéro pour bâtir le premier CSE de l'entreprise, mieux vaut avoir un sac bien rempli. Alors on s’est documentés, on a réfléchi et en s'attribuant des rôles (cliente, architecte, dev, chef de projet, ...), l’équipe d’akawaka a joué le jeu du forfait interne pour éprouver la Sylius Stack. Découvrez comment le Domain Driven Design nous a permis de garder le cap face à une deadline serrée. Je vous conterai ici nos aventures, points de frictions et surtout les objets légendaires qui nous ont sauvé : une State Machine pour les réclamations, des Processors pour le métier et quelques petites nouveautés pour briller en société. L’occasion pour moi également de vous faire un retour de l’autre côté du miroir et de réfléchir ainsi à la nécessité du dialogue constant entre les devs et le métier : bref, remettre le Domain au centre du Design.
- 91