@stlgaits
Sylius Stack lover ❤️
Trivia & cinema quiz maniac 🎞️
Board game nerd 🎲
Ex-teacher turned developer 👩🏫
Staff representative @Akawaka 📣
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
L'employeur doit y enregistrer les réclamations et leurs réponses dans l'ordre.
Consultable par l'inspection du travail & salariés.
Notifiées à l'employeur par écrit 2 jours ouvrables avant la réunion.
L'employeur a 6 jours ouvrables pour répondre à l'écrit.
Employeur
Délégation du personnel
@stlgaits
User Stories
Définition du langage métier
Envoyé au chef de projet
Sylius Stack
Symfony UX Toolkit ?
Archi hexa DDD : deptrac
Doctrine
@stlgaits
https://github.com/deptrac/deptrac
@stlgaits
CRUD
grilles paginées, triables & filtrables (custom)
actions
Security : granularité des droits d'accès
Twig Components
State Machine
exports PDF avec GotenbergBundle
Scheduler & Mailer
@stlgaits
Sylius Grid Bundle
Sylius Resource Bundle
Twig Hooks
Bootstrap Admin UI
Symfony UX & Asset Mapper
Doctrine ORM & DBAL Drivers
Providers & Processors system
@stlgaits
et un intrus ... !
@stlgaits
# @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
4ème couche possible ?
Grid (agencement visuel)
Resource (routing & model)
Doctrine Entity
Grid Provider
@stlgaits
#[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
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
symfony console make:gridsrc/Session/Infrastructure/Grid
@stlgaits
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),
)
;
}
}
@stlgaits
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)
;
}src/Session/Infrastructure/Grid/DemandGrid.php
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.phpsrc/Session/Infrastructure/Grid/DemandGrid.php
@stlgaits
@stlgaits
@stlgaits
/**
* @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
@stlgaits
Sylius Stack // API Platform
@stlgaits
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
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;
}
}
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
@stlgaits
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
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
Personnaliser les éléments autour des vues
imports:
- { resource: '@SyliusBootstrapAdminUiBundle/config/app.php' }
- { resource: 'sylius/' }
config/packages/sylius_bootstrap_admin_ui.yaml
@stlgaits
@stlgaits
@stlgaits
@stlgaits
upgrade vers Sylius Stack 0.11
symfony console sylius:debug:twig-hooks@stlgaits
@stlgaits
@stlgaits
$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
'configuration' => [
'title' => '@=_context.answer.getDemand().getSession().getName()',
'icon' => 'fluent-mdl2:pencil-reply',
],
Contexte Twig
answer = resource
When you assume,
you make an ass out of you and me.
Ne présupposez pas les besoins des clients !
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
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
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',
],
],
],
],
]);
};
UserResource
config/packages/sylius/twig_hooks/admin/users/update.php
@stlgaits