Sylius 2.0 New Features
Unveiling the Future
Łukasz Chruściel
Introduction
Timeline
v1.13 - April 2024
Symfony Work
fl
ow support
PriceHistory
Attributes & auto con
fi
guration
API not experimental anymore
v1.13 - April 2024
Symfony Work
fl
ow support
Symfony Work
fl
ow
Symfony Work
fl
ow
sylius_state_machine_abstraction:
default_adapter: symfony_workflow
Attributes
#[AsCommandDataTransformer]
#[AsDocumentationModi
fi
er]
#[AsPaymentCon
fi
gurationProvider]
Attributes in API
#[AsOrderProcessor(priority: 10)]
#[AsCartContext]
Attributes in OrderBundle
#[AsCatalogPromotionApplicatorCriteria]
#[AsCatalogPromotionPriceCalculator]
#[AsEntityObserver]
#[AsOrderItemUnitsTaxesApplicator]
#[AsOrderItemUnitsTaxesApplicator]
#[AsProductVariantMapProvider]
#[AsTaxCalculationStrategy]
#[AsUriBasedSectionResolver]
#[AsProductVariantMapProvider]
#[AsProductVariantMapProvider]
Attributes in CoreBundle
#[AsLocaleContext]
Attributes in LocaleBundle
#[AsCurrencyContext]
Attributes in CurrencyBundle
#[AsProductVariantResolver]
Attributes in ProductBundle
#[AsTaxCalculator]
Attributes in TaxationBundle
#[AsShippingCalculator]
#[AsShippingMethodResolver]
#[AsShippingMethodRuleChecker]
#[AsShippingCalculator]
Attributes in ShippingBundle
#[AsPromotionAction]
#[AsPromotionCouponEligibilityChecker]
#[AsPromotionEligibilityChecker]
#[AsPromotionRuleChecker]
Attributes in PromotionBundle
#[AsAttributeType]
Attributes in AttributeBundle
#[AsChannelContext]
#[AsRequestBasedChannelResolver]
Attributes in ChannelBundle
v1.14 - Autumn 2024
v1.14 - Autumn 2024
2.0 Compatibility layer
Resource
Attributes
Again
#[AsResource]
Operations
#[Index]
#[Show]
#[Create]
...
#[SyliusCrudRoutes]
New Controller
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace SyliusBundleResourceBundleController;
use DoctrinePersistenceObjectManager;
use FOSRestBundleViewView;
use SyliusBundleResourceBundleEventResourceControllerEvent;
use SyliusComponentResourceExceptionDeleteHandlingException;
use SyliusComponentResourceExceptionUpdateHandlingException;
use SyliusComponentResourceResourceActions;
use SyliusResourceDoctrinePersistenceRepositoryInterface;
use SyliusResourceFactoryFactoryInterface;
use SyliusResourceMetadataMetadataInterface;
use SyliusResourceModelResourceInterface;
use SymfonyComponentDependencyInjectionContainerAwareTrait;
use SymfonyComponentDependencyInjectionContainerInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelExceptionBadRequestHttpException;
use SymfonyComponentHttpKernelExceptionHttpException;
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;
use SymfonyComponentSecurityCoreExceptionAccessDeniedException;
class ResourceController
{
use ControllerTrait;
use ContainerAwareTrait;
protected MetadataInterface $metadata;
protected RequestConfigurationFactoryInterface $requestConfigurationFactory;
protected ?ViewHandlerInterface $viewHandler;
protected RepositoryInterface $repository;
protected FactoryInterface $factory;
protected NewResourceFactoryInterface $newResourceFactory;
protected ObjectManager $manager;
protected SingleResourceProviderInterface $singleResourceProvider;
protected ResourcesCollectionProviderInterface $resourcesCollectionProvider;
protected ResourceFormFactoryInterface $resourceFormFactory;
protected RedirectHandlerInterface $redirectHandler;
protected FlashHelperInterface $flashHelper;
protected AuthorizationCheckerInterface $authorizationChecker;
protected EventDispatcherInterface $eventDispatcher;
protected ?StateMachineInterface $stateMachine;
protected ResourceUpdateHandlerInterface $resourceUpdateHandler;
protected ResourceDeleteHandlerInterface $resourceDeleteHandler;
public function __construct(
MetadataInterface $metadata,
RequestConfigurationFactoryInterface $requestConfigurationFactory,
?ViewHandlerInterface $viewHandler,
RepositoryInterface $repository,
FactoryInterface $factory,
NewResourceFactoryInterface $newResourceFactory,
ObjectManager $manager,
SingleResourceProviderInterface $singleResourceProvider,
ResourcesCollectionProviderInterface $resourcesFinder,
ResourceFormFactoryInterface $resourceFormFactory,
RedirectHandlerInterface $redirectHandler,
FlashHelperInterface $flashHelper,
AuthorizationCheckerInterface $authorizationChecker,
EventDispatcherInterface $eventDispatcher,
?StateMachineInterface $stateMachine,
ResourceUpdateHandlerInterface $resourceUpdateHandler,
ResourceDeleteHandlerInterface $resourceDeleteHandler,
) {
$this->metadata = $metadata;
$this->requestConfigurationFactory = $requestConfigurationFactory;
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->factory = $factory;
$this->newResourceFactory = $newResourceFactory;
$this->manager = $manager;
$this->singleResourceProvider = $singleResourceProvider;
$this->resourcesCollectionProvider = $resourcesFinder;
$this->resourceFormFactory = $resourceFormFactory;
$this->redirectHandler = $redirectHandler;
$this->flashHelper = $flashHelper;
$this->authorizationChecker = $authorizationChecker;
$this->eventDispatcher = $eventDispatcher;
$this->stateMachine = $stateMachine;
$this->resourceUpdateHandler = $resourceUpdateHandler;
$this->resourceDeleteHandler = $resourceDeleteHandler;
}
public function showAction(Request $request): Response
{
$configuration = $this->requestConfigurationFactory->create($this->metadata,
$request);
$this->isGrantedOr403($configuration, ResourceActions::SHOW);
$resource = $this->findOr404($configuration);
$event = $this->eventDispatcher->dispatch(ResourceActions::SHOW, $configuration,
$resource);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
if ($configuration->isHtmlRequest()) {
return $this->render($configuration->getTemplate(ResourceActions::SHOW .
'.html'), [
'configuration' => $configuration,
'metadata' => $this->metadata,
'resource' => $resource,
$this->metadata->getName() => $resource,
]);
}
return $this->createRestView($configuration, $resource);
}
public function indexAction(Request $request): Response
{
$configuration = $this->requestConfigurationFactory->create($this->metadata,
}
return $this->createRestView($configuration, $resources);
}
public function createAction(Request $request): Response
{
$configuration = $this->requestConfigurationFactory->create($this->metadata,
$request);
$this->isGrantedOr403($configuration, ResourceActions::CREATE);
$newResource = $this->newResourceFactory->create($configuration, $this->factory);
$form = $this->resourceFormFactory->create($configuration, $newResource);
$form->handleRequest($request);
if ($request->isMethod('POST') && $form->isSubmitted() && $form->isValid()) {
$newResource = $form->getData();
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::CREATE,
$configuration, $newResource);
if ($event->isStopped() && !$configuration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->flashHelper->addFlashFromEvent($configuration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($configuration,
$newResource);
}
if ($configuration->hasStateMachine()) {
$stateMachine = $this->getStateMachine();
$stateMachine->apply($configuration, $newResource);
}
$this->repository->add($newResource);
if ($configuration->isHtmlRequest()) {
$this->flashHelper->addSuccessFlash($configuration,
ResourceActions::CREATE, $newResource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::CREATE,
$configuration, $newResource);
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, $newResource,
Response::HTTP_CREATED);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($configuration,
$newResource);
}
if ($request->isMethod('POST') && $form->isSubmitted() && !$form->isValid()) {
$responseCode = Response::HTTP_UNPROCESSABLE_ENTITY;
}
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, $form,
Response::HTTP_BAD_REQUEST);
}
$initializeEvent = $this->eventDispatcher-
>dispatchInitializeEvent(ResourceActions::CREATE, $configuration, $newResource);
$initializeEventResponse = $initializeEvent->getResponse();
if (null !== $initializeEventResponse) {
return $initializeEventResponse;
}
return $this->render($configuration->getTemplate(ResourceActions::CREATE .
'.html'), [
'configuration' => $configuration,
'metadata' => $this->metadata,
'resource' => $newResource,
$this->metadata->getName() => $newResource,
'form' => $form->createView(),
], null, $responseCode ?? Response::HTTP_OK);
}
public function updateAction(Request $request): Response
{
$configuration = $this->requestConfigurationFactory->create($this->metadata,
$request);
$this->isGrantedOr403($configuration, ResourceActions::UPDATE);
$resource = $this->findOr404($configuration);
$form = $this->resourceFormFactory->create($configuration, $resource);
$form->handleRequest($request);
if (
in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) &&
$form->isSubmitted() &&
$form->isValid()
) {
$resource = $form->getData();
/** @var ResourceControllerEvent $event */
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE,
$configuration, $resource);
if ($event->isStopped() && !$configuration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->flashHelper->addFlashFromEvent($configuration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToResource($configuration,
$resource);
}
try {
$this->resourceUpdateHandler->handle($resource, $configuration, $this-
>manager);
} catch (UpdateHandlingException $exception) {
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, $form, $exception-
>getApiResponseCode());
return $this->createRestView($configuration, null,
Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($configuration, $resource);
}
if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form-
>isSubmitted() && !$form->isValid()) {
$responseCode = Response::HTTP_UNPROCESSABLE_ENTITY;
}
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, $form,
Response::HTTP_BAD_REQUEST);
}
$initializeEvent = $this->eventDispatcher-
>dispatchInitializeEvent(ResourceActions::UPDATE, $configuration, $resource);
$initializeEventResponse = $initializeEvent->getResponse();
if (null !== $initializeEventResponse) {
return $initializeEventResponse;
}
return $this->render($configuration->getTemplate(ResourceActions::UPDATE .
'.html'), [
'configuration' => $configuration,
'metadata' => $this->metadata,
'resource' => $resource,
$this->metadata->getName() => $resource,
'form' => $form->createView(),
], null, $responseCode ?? Response::HTTP_OK);
}
public function deleteAction(Request $request): Response
{
$configuration = $this->requestConfigurationFactory->create($this->metadata,
$request);
$this->isGrantedOr403($configuration, ResourceActions::DELETE);
$resource = $this->findOr404($configuration);
if ($configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid((string)
$resource->getId(), (string) $request->request->get('_csrf_token'))) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
}
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::DELETE,
$configuration, $resource);
if ($event->isStopped() && !$configuration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->flashHelper->addFlashFromEvent($configuration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($configuration, $resource);
}
try {
$this->resourceDeleteHandler->handle($resource, $this->repository);
} catch (DeleteHandlingException $exception) {
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, null, $exception-
>getApiResponseCode());
}
$this->flashHelper->addErrorFlash($configuration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($configuration);
}
if ($configuration->isHtmlRequest()) {
$this->flashHelper->addSuccessFlash($configuration, ResourceActions::DELETE,
$resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::DELETE,
$configuration, $resource);
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToIndex($configuration, $resource);
}
public function bulkDeleteAction(Request $request): Response
{
$configuration = $this->requestConfigurationFactory->create($this->metadata,
$request);
$this->isGrantedOr403($configuration, ResourceActions::BULK_DELETE);
$resources = $this->resourcesCollectionProvider->get($configuration, $this-
>repository);
if (
$configuration->isCsrfProtectionEnabled() &&
!$this->isCsrfTokenValid(ResourceActions::BULK_DELETE, (string) $request-
>request->get('_csrf_token'))
) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
}
$this->eventDispatcher->dispatchMultiple(ResourceActions::BULK_DELETE,
$configuration, $resources);
foreach ($resources as $resource) {
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::DELETE,
$configuration, $resource);
if ($event->isStopped() && !$configuration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->flashHelper->addFlashFromEvent($configuration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, null, Response::HTTP_NO_CONTENT);
}
$this->flashHelper->addSuccessFlash($configuration, ResourceActions::BULK_DELETE);
if (isset($postEvent)) {
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
}
return $this->redirectHandler->redirectToIndex($configuration);
}
public function applyStateMachineTransitionAction(Request $request): Response
{
$stateMachine = $this->getStateMachine();
$configuration = $this->requestConfigurationFactory->create($this->metadata,
$request);
$this->isGrantedOr403($configuration, ResourceActions::UPDATE);
$resource = $this->findOr404($configuration);
if ($configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid((string)
$resource->getId(), $request->get('_csrf_token'))) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid CSRF token.');
}
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE,
$configuration, $resource);
if ($event->isStopped() && !$configuration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->flashHelper->addFlashFromEvent($configuration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToResource($configuration, $resource);
}
if (!$stateMachine->can($configuration, $resource)) {
throw new BadRequestHttpException();
}
try {
$this->resourceUpdateHandler->handle($resource, $configuration, $this-
>manager);
} catch (UpdateHandlingException $exception) {
if (!$configuration->isHtmlRequest()) {
return $this->createRestView($configuration, $resource, $exception-
>getApiResponseCode());
}
$this->flashHelper->addErrorFlash($configuration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($configuration);
}
if ($configuration->isHtmlRequest()) {
$this->flashHelper->addSuccessFlash($configuration, ResourceActions::UPDATE,
$resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE,
$configuration, $resource);
if (!$configuration->isHtmlRequest()) {
if ($configuration->getParameters()->get('return_content', true)) {
return $this->createRestView($configuration, $resource, Response::HTTP_OK);
}
return $this->createRestView($configuration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($configuration, $resource);
}
/**
* @return mixed
*/
protected function getParameter(string $name)
{
if (!$this->container instanceof ContainerInterface) {
throw new RuntimeException(sprintf(
'Container passed to "%s" has to implements "%s".',
self::class,
ContainerInterface::class,
));
}
return $this->container->getParameter($name);
}
/**
* @throws AccessDeniedException
*/
protected function isGrantedOr403(RequestConfiguration $configuration, string
$permission): void
{
if (!$configuration->hasPermission()) {
return;
}
$permission = $configuration->getPermission($permission);
if (!$this->authorizationChecker->isGranted($configuration, $permission)) {
throw new AccessDeniedException();
}
}
/**
* @throws NotFoundHttpException
*/
protected function findOr404(RequestConfiguration $configuration): ResourceInterface
{
if (null === $resource = $this->singleResourceProvider->get($configuration, $this-
>repository)) {
throw new NotFoundHttpException(sprintf('The "%s" has not been found', $this-
>metadata->getHumanizedName()));
}
return $resource;
}
Old Architecture
ResourceController -> 582+456 LoC
New Controller
<?php
final class MainController
{
public function __construct(
private HttpOperationInitiatorInterface $operationInitiator,
private RequestContextInitiatorInterface $requestContextInitiator,
private ProviderInterface $provider,
private ProcessorInterface $processor,
) {
}
public function __invoke(Request $request): Response
{
$operation = $this->operationInitiator->initializeOperation($request);
if (null === $operation) {
throw new RuntimeException('Operation should not be null.');
}
$context = $this->requestContextInitiator->initializeContext($request);
if (null === $operation->canWrite()) {
$operation = $operation->withWrite(!$request->isMethodSafe());
}
$data = $this->provider->provide($operation, $context);
$valid = $request->attributes->getBoolean('is_valid', true);
if (!$valid) {
$operation = $operation->withWrite(false);
}
return $this->processor->process($data, $operation, $context);
}
}
Brand New Architecture
Providers
Processors
Factories
Responders
Metadata
Ground up rework
Fully backward compatible
Improved debugger
sylius:debug:resource
Grid
New
fl
avours
PHP Con
fi
g
<?php
return static function (GridConfig $grid) {
$grid->addGrid(
GridBuilder::create('app_ticket_php')
->addField(StringField::create('email'))
);
};
Service based
final class TicketGrid extends AbstractGrid
{
public static function getName(): string
{
return 'app_ticket';
}
public function buildGrid(GridBuilderInterface $gridBuilder): void
{
$gridBuilder
->addField(
StringField::create('email')
)
;
}
}
make:grid
Attributes
#[Index(grid: 'app_test')]
v2.0 - SyliusCon 2024
Payments
API-
fi
rst
Architecture
Messenger
Work
fl
ow
UI
Admin
Sylius TwigHooks
https://syliusdev-demobap.bunnyenv.com/admin/
Shop
API
Symfony compatibility
Symfony compatibility
Work
fl
ow vs WinzouStateMachine
Clock vs Sylius/Calendar
Symfony v7.0
New folder structure
src/
Addressing/
Model/
Symfony/
Doctrine/
Anything more?
Version compatibility
V1
V2
V1 V2
V1
Magento
Sylius
Release cycle
Symfony +1 year
Thank you!

Unveiling the Future: Sylius 2.0 New Features

  • 1.
    Sylius 2.0 NewFeatures Unveiling the Future Łukasz Chruściel
  • 2.
  • 3.
  • 4.
  • 5.
    Symfony Work fl ow support PriceHistory Attributes& auto con fi guration API not experimental anymore v1.13 - April 2024
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
    v1.14 - Autumn2024 2.0 Compatibility layer
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
    * For thefull copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace SyliusBundleResourceBundleController; use DoctrinePersistenceObjectManager; use FOSRestBundleViewView; use SyliusBundleResourceBundleEventResourceControllerEvent; use SyliusComponentResourceExceptionDeleteHandlingException; use SyliusComponentResourceExceptionUpdateHandlingException; use SyliusComponentResourceResourceActions; use SyliusResourceDoctrinePersistenceRepositoryInterface; use SyliusResourceFactoryFactoryInterface; use SyliusResourceMetadataMetadataInterface; use SyliusResourceModelResourceInterface; use SymfonyComponentDependencyInjectionContainerAwareTrait; use SymfonyComponentDependencyInjectionContainerInterface; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse; use SymfonyComponentHttpKernelExceptionBadRequestHttpException; use SymfonyComponentHttpKernelExceptionHttpException; use SymfonyComponentHttpKernelExceptionNotFoundHttpException; use SymfonyComponentSecurityCoreExceptionAccessDeniedException; class ResourceController { use ControllerTrait; use ContainerAwareTrait; protected MetadataInterface $metadata; protected RequestConfigurationFactoryInterface $requestConfigurationFactory; protected ?ViewHandlerInterface $viewHandler; protected RepositoryInterface $repository; protected FactoryInterface $factory; protected NewResourceFactoryInterface $newResourceFactory; protected ObjectManager $manager; protected SingleResourceProviderInterface $singleResourceProvider; protected ResourcesCollectionProviderInterface $resourcesCollectionProvider; protected ResourceFormFactoryInterface $resourceFormFactory; protected RedirectHandlerInterface $redirectHandler; protected FlashHelperInterface $flashHelper; protected AuthorizationCheckerInterface $authorizationChecker; protected EventDispatcherInterface $eventDispatcher; protected ?StateMachineInterface $stateMachine; protected ResourceUpdateHandlerInterface $resourceUpdateHandler; protected ResourceDeleteHandlerInterface $resourceDeleteHandler; public function __construct( MetadataInterface $metadata, RequestConfigurationFactoryInterface $requestConfigurationFactory, ?ViewHandlerInterface $viewHandler, RepositoryInterface $repository, FactoryInterface $factory, NewResourceFactoryInterface $newResourceFactory, ObjectManager $manager, SingleResourceProviderInterface $singleResourceProvider, ResourcesCollectionProviderInterface $resourcesFinder, ResourceFormFactoryInterface $resourceFormFactory, RedirectHandlerInterface $redirectHandler, FlashHelperInterface $flashHelper, AuthorizationCheckerInterface $authorizationChecker, EventDispatcherInterface $eventDispatcher, ?StateMachineInterface $stateMachine, ResourceUpdateHandlerInterface $resourceUpdateHandler, ResourceDeleteHandlerInterface $resourceDeleteHandler, ) { $this->metadata = $metadata; $this->requestConfigurationFactory = $requestConfigurationFactory; $this->viewHandler = $viewHandler; $this->repository = $repository; $this->factory = $factory; $this->newResourceFactory = $newResourceFactory; $this->manager = $manager; $this->singleResourceProvider = $singleResourceProvider; $this->resourcesCollectionProvider = $resourcesFinder; $this->resourceFormFactory = $resourceFormFactory; $this->redirectHandler = $redirectHandler; $this->flashHelper = $flashHelper; $this->authorizationChecker = $authorizationChecker; $this->eventDispatcher = $eventDispatcher; $this->stateMachine = $stateMachine; $this->resourceUpdateHandler = $resourceUpdateHandler; $this->resourceDeleteHandler = $resourceDeleteHandler; } public function showAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); $this->isGrantedOr403($configuration, ResourceActions::SHOW); $resource = $this->findOr404($configuration); $event = $this->eventDispatcher->dispatch(ResourceActions::SHOW, $configuration, $resource); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } if ($configuration->isHtmlRequest()) { return $this->render($configuration->getTemplate(ResourceActions::SHOW . '.html'), [ 'configuration' => $configuration, 'metadata' => $this->metadata, 'resource' => $resource, $this->metadata->getName() => $resource, ]); } return $this->createRestView($configuration, $resource); } public function indexAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, } return $this->createRestView($configuration, $resources); } public function createAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); $this->isGrantedOr403($configuration, ResourceActions::CREATE); $newResource = $this->newResourceFactory->create($configuration, $this->factory); $form = $this->resourceFormFactory->create($configuration, $newResource); $form->handleRequest($request); if ($request->isMethod('POST') && $form->isSubmitted() && $form->isValid()) { $newResource = $form->getData(); $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::CREATE, $configuration, $newResource); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } if ($event->isStopped()) { $this->flashHelper->addFlashFromEvent($configuration, $event); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } return $this->redirectHandler->redirectToIndex($configuration, $newResource); } if ($configuration->hasStateMachine()) { $stateMachine = $this->getStateMachine(); $stateMachine->apply($configuration, $newResource); } $this->repository->add($newResource); if ($configuration->isHtmlRequest()) { $this->flashHelper->addSuccessFlash($configuration, ResourceActions::CREATE, $newResource); } $postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::CREATE, $configuration, $newResource); if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, $newResource, Response::HTTP_CREATED); } $postEventResponse = $postEvent->getResponse(); if (null !== $postEventResponse) { return $postEventResponse; } return $this->redirectHandler->redirectToResource($configuration, $newResource); } if ($request->isMethod('POST') && $form->isSubmitted() && !$form->isValid()) { $responseCode = Response::HTTP_UNPROCESSABLE_ENTITY; } if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, $form, Response::HTTP_BAD_REQUEST); } $initializeEvent = $this->eventDispatcher- >dispatchInitializeEvent(ResourceActions::CREATE, $configuration, $newResource); $initializeEventResponse = $initializeEvent->getResponse(); if (null !== $initializeEventResponse) { return $initializeEventResponse; } return $this->render($configuration->getTemplate(ResourceActions::CREATE . '.html'), [ 'configuration' => $configuration, 'metadata' => $this->metadata, 'resource' => $newResource, $this->metadata->getName() => $newResource, 'form' => $form->createView(), ], null, $responseCode ?? Response::HTTP_OK); } public function updateAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); $this->isGrantedOr403($configuration, ResourceActions::UPDATE); $resource = $this->findOr404($configuration); $form = $this->resourceFormFactory->create($configuration, $resource); $form->handleRequest($request); if ( in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form->isSubmitted() && $form->isValid() ) { $resource = $form->getData(); /** @var ResourceControllerEvent $event */ $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE, $configuration, $resource); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } if ($event->isStopped()) { $this->flashHelper->addFlashFromEvent($configuration, $event); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } return $this->redirectHandler->redirectToResource($configuration, $resource); } try { $this->resourceUpdateHandler->handle($resource, $configuration, $this- >manager); } catch (UpdateHandlingException $exception) { if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, $form, $exception- >getApiResponseCode()); return $this->createRestView($configuration, null, Response::HTTP_NO_CONTENT); } $postEventResponse = $postEvent->getResponse(); if (null !== $postEventResponse) { return $postEventResponse; } return $this->redirectHandler->redirectToResource($configuration, $resource); } if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form- >isSubmitted() && !$form->isValid()) { $responseCode = Response::HTTP_UNPROCESSABLE_ENTITY; } if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, $form, Response::HTTP_BAD_REQUEST); } $initializeEvent = $this->eventDispatcher- >dispatchInitializeEvent(ResourceActions::UPDATE, $configuration, $resource); $initializeEventResponse = $initializeEvent->getResponse(); if (null !== $initializeEventResponse) { return $initializeEventResponse; } return $this->render($configuration->getTemplate(ResourceActions::UPDATE . '.html'), [ 'configuration' => $configuration, 'metadata' => $this->metadata, 'resource' => $resource, $this->metadata->getName() => $resource, 'form' => $form->createView(), ], null, $responseCode ?? Response::HTTP_OK); } public function deleteAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); $this->isGrantedOr403($configuration, ResourceActions::DELETE); $resource = $this->findOr404($configuration); if ($configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid((string) $resource->getId(), (string) $request->request->get('_csrf_token'))) { throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); } $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::DELETE, $configuration, $resource); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } if ($event->isStopped()) { $this->flashHelper->addFlashFromEvent($configuration, $event); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } return $this->redirectHandler->redirectToIndex($configuration, $resource); } try { $this->resourceDeleteHandler->handle($resource, $this->repository); } catch (DeleteHandlingException $exception) { if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, null, $exception- >getApiResponseCode()); } $this->flashHelper->addErrorFlash($configuration, $exception->getFlash()); return $this->redirectHandler->redirectToReferer($configuration); } if ($configuration->isHtmlRequest()) { $this->flashHelper->addSuccessFlash($configuration, ResourceActions::DELETE, $resource); } $postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::DELETE, $configuration, $resource); if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, null, Response::HTTP_NO_CONTENT); } $postEventResponse = $postEvent->getResponse(); if (null !== $postEventResponse) { return $postEventResponse; } return $this->redirectHandler->redirectToIndex($configuration, $resource); } public function bulkDeleteAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); $this->isGrantedOr403($configuration, ResourceActions::BULK_DELETE); $resources = $this->resourcesCollectionProvider->get($configuration, $this- >repository); if ( $configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid(ResourceActions::BULK_DELETE, (string) $request- >request->get('_csrf_token')) ) { throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); } $this->eventDispatcher->dispatchMultiple(ResourceActions::BULK_DELETE, $configuration, $resources); foreach ($resources as $resource) { $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::DELETE, $configuration, $resource); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } if ($event->isStopped()) { $this->flashHelper->addFlashFromEvent($configuration, $event); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, null, Response::HTTP_NO_CONTENT); } $this->flashHelper->addSuccessFlash($configuration, ResourceActions::BULK_DELETE); if (isset($postEvent)) { $postEventResponse = $postEvent->getResponse(); if (null !== $postEventResponse) { return $postEventResponse; } } return $this->redirectHandler->redirectToIndex($configuration); } public function applyStateMachineTransitionAction(Request $request): Response { $stateMachine = $this->getStateMachine(); $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); $this->isGrantedOr403($configuration, ResourceActions::UPDATE); $resource = $this->findOr404($configuration); if ($configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid((string) $resource->getId(), $request->get('_csrf_token'))) { throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid CSRF token.'); } $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE, $configuration, $resource); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } if ($event->isStopped()) { $this->flashHelper->addFlashFromEvent($configuration, $event); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } return $this->redirectHandler->redirectToResource($configuration, $resource); } if (!$stateMachine->can($configuration, $resource)) { throw new BadRequestHttpException(); } try { $this->resourceUpdateHandler->handle($resource, $configuration, $this- >manager); } catch (UpdateHandlingException $exception) { if (!$configuration->isHtmlRequest()) { return $this->createRestView($configuration, $resource, $exception- >getApiResponseCode()); } $this->flashHelper->addErrorFlash($configuration, $exception->getFlash()); return $this->redirectHandler->redirectToReferer($configuration); } if ($configuration->isHtmlRequest()) { $this->flashHelper->addSuccessFlash($configuration, ResourceActions::UPDATE, $resource); } $postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE, $configuration, $resource); if (!$configuration->isHtmlRequest()) { if ($configuration->getParameters()->get('return_content', true)) { return $this->createRestView($configuration, $resource, Response::HTTP_OK); } return $this->createRestView($configuration, null, Response::HTTP_NO_CONTENT); } $postEventResponse = $postEvent->getResponse(); if (null !== $postEventResponse) { return $postEventResponse; } return $this->redirectHandler->redirectToResource($configuration, $resource); } /** * @return mixed */ protected function getParameter(string $name) { if (!$this->container instanceof ContainerInterface) { throw new RuntimeException(sprintf( 'Container passed to "%s" has to implements "%s".', self::class, ContainerInterface::class, )); } return $this->container->getParameter($name); } /** * @throws AccessDeniedException */ protected function isGrantedOr403(RequestConfiguration $configuration, string $permission): void { if (!$configuration->hasPermission()) { return; } $permission = $configuration->getPermission($permission); if (!$this->authorizationChecker->isGranted($configuration, $permission)) { throw new AccessDeniedException(); } } /** * @throws NotFoundHttpException */ protected function findOr404(RequestConfiguration $configuration): ResourceInterface { if (null === $resource = $this->singleResourceProvider->get($configuration, $this- >repository)) { throw new NotFoundHttpException(sprintf('The "%s" has not been found', $this- >metadata->getHumanizedName())); } return $resource; } Old Architecture ResourceController -> 582+456 LoC
  • 30.
    New Controller <?php final classMainController { public function __construct( private HttpOperationInitiatorInterface $operationInitiator, private RequestContextInitiatorInterface $requestContextInitiator, private ProviderInterface $provider, private ProcessorInterface $processor, ) { } public function __invoke(Request $request): Response { $operation = $this->operationInitiator->initializeOperation($request); if (null === $operation) { throw new RuntimeException('Operation should not be null.'); } $context = $this->requestContextInitiator->initializeContext($request); if (null === $operation->canWrite()) { $operation = $operation->withWrite(!$request->isMethodSafe()); } $data = $this->provider->provide($operation, $context); $valid = $request->attributes->getBoolean('is_valid', true); if (!$valid) { $operation = $operation->withWrite(false); } return $this->processor->process($data, $operation, $context); } }
  • 31.
  • 32.
    Ground up rework Fullybackward compatible
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
    PHP Con fi g <?php return staticfunction (GridConfig $grid) { $grid->addGrid( GridBuilder::create('app_ticket_php') ->addField(StringField::create('email')) ); };
  • 38.
    Service based final classTicketGrid extends AbstractGrid { public static function getName(): string { return 'app_ticket'; } public function buildGrid(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addField( StringField::create('email') ) ; } }
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 45.
  • 46.
  • 48.
  • 49.
  • 53.
  • 54.
  • 55.
  • 57.
  • 59.
  • 60.
    Symfony compatibility Work fl ow vsWinzouStateMachine Clock vs Sylius/Calendar Symfony v7.0
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.