Skip to main content
Typical pitfalls
of Sylius development
Thank you!
Why do we do this?
😂😂😂
AI is not a threat for now
Q&A
Performance
N+1
Full stack mode
Headless mode
Product list
Let’s start small
{
"@context":"/api/contexts/Product",
"@id":"/api/products",
"@type":"hydra:Collection",
"hydra:totalItems":6,
"hydra:member":[
{
"@id":"/api/products/1",
"@type":"Product",
"id":1,
"name":"T-Shirt",
"price":1000
},
{
"@id":"/api/products/2",
"@type":"Product",
"id":2,
"name":"Trousers",
"price":5000
},
{
"“…“":"“…“"
}
]
}
Amount of products: 6
No associations between objects
Sample query on the left
O(1)
Order list
How many queries are executed here?
{
"@context": "/api/contexts/Order",
"@id": "/api/orders",
"@type": "hydra:Collection",
"hydra:totalItems": 3,
"hydra:member": [
{
"@id": "/api/orders/1",
"@type": "Order",
"id": 1,
"orderItems": [
"/api/order_items/1",
"/api/order_items/2"
]
},
{
“…”: “…“
}
]
}
Amount of orders: 3
Every order associated with 2 items
Sample query on the left
No total
fi
eld
O(M)
Order list with additional
fi
eld
How many queries are executed here?
{
"@context": "/api/contexts/Order",
"@id": "/api/orders",
"@type": "hydra:Collection",
"hydra:totalItems": 3,
"hydra:member": [
{
"@id": "/api/orders/1",
"@type": "Order",
"id": 1,
"orderItems": [
"/api/order_items/1",
"/api/order_items/2"
],
"total": 7000
},
{
“…”: “…“
}
]
}
Amount of orders: 3
Every order associated with 2 items
Sample query on the left
Added “total()” as a function of product
price and quantity of item
O(M*N) or O(M^2) 🚀
How to
fi
x it?
Solution 1
#[ORMOneToMany(fetch: ‘LAZY')] => 11
#[ORMOneToMany(fetch: ‘EXTRA_LAZY')] => 11
#[ORMOneToMany(fetch: ‘EAGER')] => 5 (OrderItem and Product)
#[ORMOneToMany(fetch: ‘EAGER')] (OrderItem and Product) + Serialisation groups => 4
Solution 2
final class LoadItemsAndProductsExtension implements QueryCollectionExtensionInterface,
QueryItemExtensionInterface
{
private function apply(QueryBuilder $queryBuilder): void
{
$queryBuilder
->addSelect('oi', 'p')
->join(OrderItem::class, 'oi', Join::WITH, 'oi.originOrder = o')
->join(Product::class, 'p', Join::WITH, 'oi.product = p')
;
}
}
But….
{
"@context": "/api/contexts/Order",
"@id": "/api/orders",
"@type": "hydra:Collection",
"hydra:totalItems": 3,
"hydra:member": [
{
"@id": "/api/orders/1",
"@type": "Order",
"id": 1,
"orderItems": [
"/api/order_items/1",
"/api/order_items/2"
],
"total": 7000
},
{
"@id": "/api/order_items/1",
"@type": "OrderItem",
"id": 1,
"product": "/api/products/1",
"quantity": 2,
"originOrder": "/api/orders/1",
"price": 2000
},
{
"@id": "/api/products/1",
"@type": "Product",
"id": 1,
"name": "T-Shirt",
"price": 1000
},
{
"@id": "/api/order_items/2",
"@type": "OrderItem",
"id": 2,
"product": "/api/products/2",
"quantity": 1,
"originOrder": "/api/orders/1",
"price": 5000
},
{
“…”: “…”
}
]
}
So is single query an ultimate
solution?
Concurrency from di
ff
erent
users
Sequential order number
generator
Does it need to be
sequential?
Or defer it 😉
Locking mechanism of
product variants (inventory)
What is locked?
Concurrency from the same
user
Order Processing
How does Doctrine collection
handling works?
Tests
Contract tests
Functional tests
You don’t need to do BDD
to work with Sylius
Although we ecourage you to do so
No. of Behat scenarios in our
latest successfull Sylius project:
0
No. of Behat scenarios in our
latest successfull Sylius project:
Testing documentation
Payments
Payments
Current
fl
ow
Payment after checkout
Typical request
Payment before completion
What we want to achieve
Authorization during checkout
Flexible capture moment
Core Team
is actively working on it
Resource
Resource
Regular Symfony
controllers
<?php
/*
* This
fi
le is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
*
fi
le that was distributed with this source code.
*/
declare(strict_types=1);
namespace SyliusBundleResourceBundleController;
use DoctrinePersistenceObjectManager;
use FOSRestBundleViewView;
use SyliusBundleResourceBundleEventResourceControllerEvent;
use SyliusComponentResourceExceptionDeleteHandlingException;
use SyliusComponentResourceExceptionUpdateHandlingException;
use SyliusComponentResourceFactoryFactoryInterface;
use SyliusComponentResourceMetadataMetadataInterface;
use SyliusComponentResourceModelResourceInterface;
use SyliusComponentResourceRepositoryRepositoryInterface;
use SyliusComponentResourceResourceActions;
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 RequestCon
fi
gurationFactoryInterface $requestCon
fi
gurationFactory;
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 $
fl
ashHelper;
protected AuthorizationCheckerInterface $authorizationChecker;
protected EventDispatcherInterface $eventDispatcher;
protected ?StateMachineInterface $stateMachine;
protected ResourceUpdateHandlerInterface $resourceUpdateHandler;
protected ResourceDeleteHandlerInterface $resourceDeleteHandler;
public function __construct(
MetadataInterface $metadata,
RequestCon
fi
gurationFactoryInterface $requestCon
fi
gurationFactory,
?ViewHandlerInterface $viewHandler,
RepositoryInterface $repository,
FactoryInterface $factory,
NewResourceFactoryInterface $newResourceFactory,
ObjectManager $manager,
SingleResourceProviderInterface $singleResourceProvider,
ResourcesCollectionProviderInterface $resourcesFinder,
ResourceFormFactoryInterface $resourceFormFactory,
RedirectHandlerInterface $redirectHandler,
FlashHelperInterface $
fl
ashHelper,
AuthorizationCheckerInterface $authorizationChecker,
EventDispatcherInterface $eventDispatcher,
?StateMachineInterface $stateMachine,
ResourceUpdateHandlerInterface $resourceUpdateHandler,
ResourceDeleteHandlerInterface $resourceDeleteHandler,
) {
$this->metadata = $metadata;
$this->requestCon
fi
gurationFactory = $requestCon
fi
gurationFactory;
$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->
fl
ashHelper = $
fl
ashHelper;
$this->authorizationChecker = $authorizationChecker;
$this->eventDispatcher = $eventDispatcher;
$this->stateMachine = $stateMachine;
$this->resourceUpdateHandler = $resourceUpdateHandler;
$this->resourceDeleteHandler = $resourceDeleteHandler;
}
public function showAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::SHOW);
$resource = $this->
fi
ndOr404($con
fi
guration);
$event = $this->eventDispatcher->dispatch(ResourceActions::SHOW, $con
fi
guration, $resource);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
if ($con
fi
guration->isHtmlRequest()) {
return $this->render($con
fi
guration->getTemplate(ResourceActions::SHOW . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resource' => $resource,
$this->metadata->getName() => $resource,
]);
}
return $this->createRestView($con
fi
guration, $resource);
}
public function indexAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::INDEX);
$resources = $this->resourcesCollectionProvider->get($con
fi
guration, $this->repository);
$event = $this->eventDispatcher->dispatchMultiple(ResourceActions::INDEX, $con
fi
guration, $resources);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
if ($con
fi
guration->isHtmlRequest()) {
return $this->render($con
fi
guration->getTemplate(ResourceActions::INDEX . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resources' => $resources,
$this->metadata->getPluralName() => $resources,
]);
}
return $this->createRestView($con
fi
guration, $resources);
}
public function createAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::CREATE);
$newResource = $this->newResourceFactory->create($con
fi
guration, $this->factory);
$form = $this->resourceFormFactory->create($con
fi
guration, $newResource);
$form->handleRequest($request);
if ($request->isMethod('POST') && $form->isSubmitted() && $form->isValid()) {
$newResource = $form->getData();
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::CREATE, $con
fi
guration, $newResource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $newResource);
}
if ($con
fi
guration->hasStateMachine()) {
$stateMachine = $this->getStateMachine();
$stateMachine->apply($con
fi
guration, $newResource);
}
$this->repository->add($newResource);
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::CREATE, $newResource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::CREATE, $con
fi
guration, $newResource);
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $newResource, Response::HTTP_CREATED);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $newResource);
}
if ($request->isMethod('POST') && $form->isSubmitted() && !$form->isValid()) {
$responseCode = Response::HTTP_UNPROCESSABLE_ENTITY;
}
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $form, Response::HTTP_BAD_REQUEST);
}
$initializeEvent = $this->eventDispatcher->dispatchInitializeEvent(ResourceActions::CREATE, $con
fi
guration, $newResource);
$initializeEventResponse = $initializeEvent->getResponse();
if (null !== $initializeEventResponse) {
return $initializeEventResponse;
}
return $this->render($con
fi
guration->getTemplate(ResourceActions::CREATE . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resource' => $newResource,
$this->metadata->getName() => $newResource,
'form' => $form->createView(),
], null, $responseCode ?? Response::HTTP_OK);
}
public function updateAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::UPDATE);
$resource = $this->
fi
ndOr404($con
fi
guration);
$form = $this->resourceFormFactory->create($con
fi
guration, $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, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $resource);
}
try {
$this->resourceUpdateHandler->handle($resource, $con
fi
guration, $this->manager);
} catch (UpdateHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $form, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::UPDATE, $resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE, $con
fi
guration, $resource);
if (!$con
fi
guration->isHtmlRequest()) {
if ($con
fi
guration->getParameters()->get('return_content', false)) {
return $this->createRestView($con
fi
guration, $resource, Response::HTTP_OK);
}
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $resource);
}
if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form->isSubmitted() && !$form->isValid()) {
$responseCode = Response::HTTP_UNPROCESSABLE_ENTITY;
}
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $form, Response::HTTP_BAD_REQUEST);
}
$initializeEvent = $this->eventDispatcher->dispatchInitializeEvent(ResourceActions::UPDATE, $con
fi
guration, $resource);
$initializeEventResponse = $initializeEvent->getResponse();
if (null !== $initializeEventResponse) {
return $initializeEventResponse;
}
return $this->render($con
fi
guration->getTemplate(ResourceActions::UPDATE . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resource' => $resource,
$this->metadata->getName() => $resource,
'form' => $form->createView(),
], null, $responseCode ?? Response::HTTP_OK);
}
public function deleteAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::DELETE);
$resource = $this->
fi
ndOr404($con
fi
guration);
if ($con
fi
guration->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, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $resource);
}
try {
$this->resourceDeleteHandler->handle($resource, $this->repository);
} catch (DeleteHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::DELETE, $resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::DELETE, $con
fi
guration, $resource);
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $resource);
}
public function bulkDeleteAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::BULK_DELETE);
$resources = $this->resourcesCollectionProvider->get($con
fi
guration, $this->repository);
if (
$con
fi
guration->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, $con
fi
guration, $resources);
foreach ($resources as $resource) {
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::DELETE, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $resource);
}
try {
$this->resourceDeleteHandler->handle($resource, $this->repository);
} catch (DeleteHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::DELETE, $con
fi
guration, $resource);
}
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::BULK_DELETE);
if (isset($postEvent)) {
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
}
return $this->redirectHandler->redirectToIndex($con
fi
guration);
}
public function applyStateMachineTransitionAction(Request $request): Response
{
$stateMachine = $this->getStateMachine();
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::UPDATE);
$resource = $this->
fi
ndOr404($con
fi
guration);
if ($con
fi
guration->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, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $resource);
}
if (!$stateMachine->can($con
fi
guration, $resource)) {
throw new BadRequestHttpException();
}
try {
$this->resourceUpdateHandler->handle($resource, $con
fi
guration, $this->manager);
} catch (UpdateHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $resource, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::UPDATE, $resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE, $con
fi
guration, $resource);
if (!$con
fi
guration->isHtmlRequest()) {
if ($con
fi
guration->getParameters()->get('return_content', true)) {
return $this->createRestView($con
fi
guration, $resource, Response::HTTP_OK);
}
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $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(RequestCon
fi
guration $con
fi
guration, string $permission): void
{
if (!$con
fi
guration->hasPermission()) {
return;
}
$permission = $con
fi
guration->getPermission($permission);
if (!$this->authorizationChecker->isGranted($con
fi
guration, $permission)) {
throw new AccessDeniedException();
}
}
/**
* @throws NotFoundHttpException
*/
protected function
fi
ndOr404(RequestCon
fi
guration $con
fi
guration): ResourceInterface
{
if (null === $resource = $this->singleResourceProvider->get($con
fi
guration, $this->repository)) {
throw new NotFoundHttpException(sprintf('The "%s" has not been found', $this->metadata->getHumanizedName()));
}
return $resource;
}
/**
* @param mixed $data
*/
protected function createRestView(RequestCon
fi
guration $con
fi
guration, $data, int $statusCode = null): Response
{
if (null === $this->viewHandler) {
throw new LogicException('You can not use the "non-html" request if FriendsOfSymfony Rest Bundle is not available. Try running "composer require friendsofsymfony/rest-bundle".');
}
$view = View::create($data, $statusCode);
return $this->viewHandler->handle($con
fi
guration, $view);
}
protected function getStateMachine(): StateMachineInterface
{
if (null === $this->stateMachine) {
throw new LogicException('You can not use the "state-machine" if Winzou State Machine Bundle is not available. Try running "composer require winzou/state-machine-bundle".');
}
return $this->stateMachine;
}
}
ResourceController
<?php
/*
* This
fi
le is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
*
fi
le that was distributed with this source code.
*/
declare(strict_types=1);
namespace SyliusBundleResourceBundleController;
use DoctrinePersistenceObjectManager;
use FOSRestBundleViewView;
use SyliusBundleResourceBundleEventResourceControllerEvent;
use SyliusComponentResourceExceptionDeleteHandlingException;
use SyliusComponentResourceExceptionUpdateHandlingException;
use SyliusComponentResourceFactoryFactoryInterface;
use SyliusComponentResourceMetadataMetadataInterface;
use SyliusComponentResourceModelResourceInterface;
use SyliusComponentResourceRepositoryRepositoryInterface;
use SyliusComponentResourceResourceActions;
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 RequestCon
fi
gurationFactoryInterface $requestCon
fi
gurationFactory;
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 $
fl
ashHelper;
protected AuthorizationCheckerInterface $authorizationChecker;
protected EventDispatcherInterface $eventDispatcher;
protected ?StateMachineInterface $stateMachine;
protected ResourceUpdateHandlerInterface $resourceUpdateHandler;
protected ResourceDeleteHandlerInterface $resourceDeleteHandler;
public function __construct(
MetadataInterface $metadata,
RequestCon
fi
gurationFactoryInterface $requestCon
fi
gurationFactory,
?ViewHandlerInterface $viewHandler,
RepositoryInterface $repository,
FactoryInterface $factory,
NewResourceFactoryInterface $newResourceFactory,
ObjectManager $manager,
SingleResourceProviderInterface $singleResourceProvider,
ResourcesCollectionProviderInterface $resourcesFinder,
ResourceFormFactoryInterface $resourceFormFactory,
RedirectHandlerInterface $redirectHandler,
FlashHelperInterface $
fl
ashHelper,
AuthorizationCheckerInterface $authorizationChecker,
EventDispatcherInterface $eventDispatcher,
?StateMachineInterface $stateMachine,
ResourceUpdateHandlerInterface $resourceUpdateHandler,
ResourceDeleteHandlerInterface $resourceDeleteHandler,
) {
$this->metadata = $metadata;
$this->requestCon
fi
gurationFactory = $requestCon
fi
gurationFactory;
$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->
fl
ashHelper = $
fl
ashHelper;
$this->authorizationChecker = $authorizationChecker;
$this->eventDispatcher = $eventDispatcher;
$this->stateMachine = $stateMachine;
$this->resourceUpdateHandler = $resourceUpdateHandler;
$this->resourceDeleteHandler = $resourceDeleteHandler;
}
public function showAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::SHOW);
$resource = $this->
fi
ndOr404($con
fi
guration);
$event = $this->eventDispatcher->dispatch(ResourceActions::SHOW, $con
fi
guration, $resource);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
if ($con
fi
guration->isHtmlRequest()) {
return $this->render($con
fi
guration->getTemplate(ResourceActions::SHOW . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resource' => $resource,
$this->metadata->getName() => $resource,
]);
}
return $this->createRestView($con
fi
guration, $resource);
}
public function indexAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::INDEX);
$resources = $this->resourcesCollectionProvider->get($con
fi
guration, $this->repository);
$event = $this->eventDispatcher->dispatchMultiple(ResourceActions::INDEX, $con
fi
guration, $resources);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
if ($con
fi
guration->isHtmlRequest()) {
return $this->render($con
fi
guration->getTemplate(ResourceActions::INDEX . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resources' => $resources,
$this->metadata->getPluralName() => $resources,
]);
}
return $this->createRestView($con
fi
guration, $resources);
}
public function createAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::CREATE);
$newResource = $this->newResourceFactory->create($con
fi
guration, $this->factory);
$form = $this->resourceFormFactory->create($con
fi
guration, $newResource);
$form->handleRequest($request);
if ($request->isMethod('POST') && $form->isSubmitted() && $form->isValid()) {
$newResource = $form->getData();
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::CREATE, $con
fi
guration, $newResource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $newResource);
}
if ($con
fi
guration->hasStateMachine()) {
$stateMachine = $this->getStateMachine();
$stateMachine->apply($con
fi
guration, $newResource);
}
$this->repository->add($newResource);
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::CREATE, $newResource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::CREATE, $con
fi
guration, $newResource);
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $newResource, Response::HTTP_CREATED);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $newResource);
}
if ($request->isMethod('POST') && $form->isSubmitted() && !$form->isValid()) {
$responseCode = Response::HTTP_UNPROCESSABLE_ENTITY;
}
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $form, Response::HTTP_BAD_REQUEST);
}
$initializeEvent = $this->eventDispatcher->dispatchInitializeEvent(ResourceActions::CREATE, $con
fi
guration, $newResource);
$initializeEventResponse = $initializeEvent->getResponse();
if (null !== $initializeEventResponse) {
return $initializeEventResponse;
}
return $this->render($con
fi
guration->getTemplate(ResourceActions::CREATE . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resource' => $newResource,
$this->metadata->getName() => $newResource,
'form' => $form->createView(),
], null, $responseCode ?? Response::HTTP_OK);
}
public function updateAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::UPDATE);
$resource = $this->
fi
ndOr404($con
fi
guration);
$form = $this->resourceFormFactory->create($con
fi
guration, $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, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $resource);
}
try {
$this->resourceUpdateHandler->handle($resource, $con
fi
guration, $this->manager);
} catch (UpdateHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $form, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::UPDATE, $resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE, $con
fi
guration, $resource);
if (!$con
fi
guration->isHtmlRequest()) {
if ($con
fi
guration->getParameters()->get('return_content', false)) {
return $this->createRestView($con
fi
guration, $resource, Response::HTTP_OK);
}
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $resource);
}
if (in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true) && $form->isSubmitted() && !$form->isValid()) {
$responseCode = Response::HTTP_UNPROCESSABLE_ENTITY;
}
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $form, Response::HTTP_BAD_REQUEST);
}
$initializeEvent = $this->eventDispatcher->dispatchInitializeEvent(ResourceActions::UPDATE, $con
fi
guration, $resource);
$initializeEventResponse = $initializeEvent->getResponse();
if (null !== $initializeEventResponse) {
return $initializeEventResponse;
}
return $this->render($con
fi
guration->getTemplate(ResourceActions::UPDATE . '.html'), [
'con
fi
guration' => $con
fi
guration,
'metadata' => $this->metadata,
'resource' => $resource,
$this->metadata->getName() => $resource,
'form' => $form->createView(),
], null, $responseCode ?? Response::HTTP_OK);
}
public function deleteAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::DELETE);
$resource = $this->
fi
ndOr404($con
fi
guration);
if ($con
fi
guration->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, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $resource);
}
try {
$this->resourceDeleteHandler->handle($resource, $this->repository);
} catch (DeleteHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::DELETE, $resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::DELETE, $con
fi
guration, $resource);
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $resource);
}
public function bulkDeleteAction(Request $request): Response
{
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::BULK_DELETE);
$resources = $this->resourcesCollectionProvider->get($con
fi
guration, $this->repository);
if (
$con
fi
guration->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, $con
fi
guration, $resources);
foreach ($resources as $resource) {
$event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::DELETE, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToIndex($con
fi
guration, $resource);
}
try {
$this->resourceDeleteHandler->handle($resource, $this->repository);
} catch (DeleteHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::DELETE, $con
fi
guration, $resource);
}
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::BULK_DELETE);
if (isset($postEvent)) {
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
}
return $this->redirectHandler->redirectToIndex($con
fi
guration);
}
public function applyStateMachineTransitionAction(Request $request): Response
{
$stateMachine = $this->getStateMachine();
$con
fi
guration = $this->requestCon
fi
gurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($con
fi
guration, ResourceActions::UPDATE);
$resource = $this->
fi
ndOr404($con
fi
guration);
if ($con
fi
guration->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, $con
fi
guration, $resource);
if ($event->isStopped() && !$con
fi
guration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->
fl
ashHelper->addFlashFromEvent($con
fi
guration, $event);
$eventResponse = $event->getResponse();
if (null !== $eventResponse) {
return $eventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $resource);
}
if (!$stateMachine->can($con
fi
guration, $resource)) {
throw new BadRequestHttpException();
}
try {
$this->resourceUpdateHandler->handle($resource, $con
fi
guration, $this->manager);
} catch (UpdateHandlingException $exception) {
if (!$con
fi
guration->isHtmlRequest()) {
return $this->createRestView($con
fi
guration, $resource, $exception->getApiResponseCode());
}
$this->
fl
ashHelper->addErrorFlash($con
fi
guration, $exception->getFlash());
return $this->redirectHandler->redirectToReferer($con
fi
guration);
}
if ($con
fi
guration->isHtmlRequest()) {
$this->
fl
ashHelper->addSuccessFlash($con
fi
guration, ResourceActions::UPDATE, $resource);
}
$postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE, $con
fi
guration, $resource);
if (!$con
fi
guration->isHtmlRequest()) {
if ($con
fi
guration->getParameters()->get('return_content', true)) {
return $this->createRestView($con
fi
guration, $resource, Response::HTTP_OK);
}
return $this->createRestView($con
fi
guration, null, Response::HTTP_NO_CONTENT);
}
$postEventResponse = $postEvent->getResponse();
if (null !== $postEventResponse) {
return $postEventResponse;
}
return $this->redirectHandler->redirectToResource($con
fi
guration, $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(RequestCon
fi
guration $con
fi
guration, string $permission): void
{
if (!$con
fi
guration->hasPermission()) {
return;
}
$permission = $con
fi
guration->getPermission($permission);
if (!$this->authorizationChecker->isGranted($con
fi
guration, $permission)) {
throw new AccessDeniedException();
}
}
/**
* @throws NotFoundHttpException
*/
protected function
fi
ndOr404(RequestCon
fi
guration $con
fi
guration): ResourceInterface
{
if (null === $resource = $this->singleResourceProvider->get($con
fi
guration, $this->repository)) {
throw new NotFoundHttpException(sprintf('The "%s" has not been found', $this->metadata->getHumanizedName()));
}
return $resource;
}
/**
* @param mixed $data
*/
protected function createRestView(RequestCon
fi
guration $con
fi
guration, $data, int $statusCode = null): Response
{
if (null === $this->viewHandler) {
throw new LogicException('You can not use the "non-html" request if FriendsOfSymfony Rest Bundle is not available. Try running "composer require friendsofsymfony/rest-bundle".');
}
$view = View::create($data, $statusCode);
return $this->viewHandler->handle($con
fi
guration, $view);
}
protected function getStateMachine(): StateMachineInterface
{
if (null === $this->stateMachine) {
throw new LogicException('You can not use the "state-machine" if Winzou State Machine Bundle is not available. Try running "composer require winzou/state-machine-bundle".');
}
return $this->stateMachine;
}
}
ResourceController
!
interface ProviderInterface
{
public function provide(Operation $operation, Context $context): object|iterable|null;
}
interface ProcessorInterface
{
public function process(mixed $data, Operation $operation, Context $context): mixed;
}
#[AsResource]
#[Index(grid: BookGrid::class)]
#[Create]
class Book implements ResourceInterface
{
}
$grid->addGrid(GridBuilder::create('app_user', '%app.model.user.class%')
->setLimits([10, 25, 50, 100])
->addField(
Field::create('name', 'twig')
->setLabel('Name')
->setSortable(true)
)
->addFilter(
Filter::create('name', 'string')
->setLabel('app.ui.name')
->setEnabled(true)
->setFormOptions(['type' => 'contains'])
)
->addActionGroup(MainActionGroup::create(
Action::create('create', 'create')
))
;
Is Sylius DX still great?…
Is Sylius DX still great?…
Of course
Q&A
We are hiring!
Łukasz Chruściel
Mateusz Zalewski
@lchrusciel
@mpzalewski
Commerce Weavers @commerceweavers
Thank you!