Skip to content

Instantly share code, notes, and snippets.

@cornobils
Created April 9, 2019 08:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cornobils/94a520b212e8c8f061202c4ea5b3fbd5 to your computer and use it in GitHub Desktop.
Save cornobils/94a520b212e8c8f061202c4ea5b3fbd5 to your computer and use it in GitHub Desktop.
[Sylius] page with product choice form

In Sylius by default product adding to cart has the following workflow - firstly you have list (grid) of products, you navigate to product by pressing the link, and there add product or productVariant to cart with ajax form. If you want for example only homepage with products as choicetype/checkboxes/radio buttons as common form, this gist is for you.

Conceptually this is overriden standard OrderItemController mostly to remove dependency to AddToCartCommandInterface and custom forms.

By default customer can add to cart only one product, and before adding to cart it cleans previous items, because user can go back with browser and buy something again. You can remove it with removing $cart->clearItems(); line in OrderItemController.

Works on Sylius 1.4.1

sylius_order:
resources:
order_item:
classes:
controller: App\ResourceController\OrderItemController
...
sylius_product:
resources:
product:
classes:
repository: App\Repository\ProductRepository
...
<?php
declare(strict_types=1);
namespace App\Model;
use Sylius\Bundle\OrderBundle\Controller\AddToCartCommandInterface;
use Sylius\Component\Order\Model\OrderInterface;
use Sylius\Component\Order\Model\OrderItemInterface;
final class AddToCartCommand
{
/**
* @var OrderInterface|null
*/
private $cart;
/**
* @var OrderItemInterface|null
*/
private $cartItem;
public function getCart(): ?OrderInterface
{
return $this->cart;
}
public function getCartItem(): ?OrderItemInterface
{
return $this->cartItem;
}
/**
* @param OrderInterface $cart
*/
public function setCart(OrderInterface $cart): void
{
$this->cart = $cart;
}
/**
* @param OrderItemInterface $cartItem
*/
public function setCartItem(OrderItemInterface $cartItem): void
{
$this->cartItem = $cartItem;
}
}
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Model\AddToCartCommand;
use Sylius\Bundle\OrderBundle\Controller\AddToCartCommandInterface;
use Sylius\Component\Order\Model\OrderInterface;
use Sylius\Component\Order\Model\OrderItemInterface;
final class AddToCartCommandFactory
{
/**
* {@inheritdoc}
*/
public function createWithCartAndCartItem()
{
return new AddToCartCommand();
}
}
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
final class HomepageOrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('cartItem', OrderItemType::class)
;
}
}
{% extends '@SyliusShop/layout.html.twig' %}
{% block content %}
{{ form_start(form) }}
{{ form_row(form.cartItem, {'label': false}) }}
<button>submit</button>
{{ form_end(form) }}
{% endblock %}
<?php
declare(strict_types=1);
namespace App\ResourceController;
use App\Model\AddToCartCommand;
use Doctrine\ORM\EntityManagerInterface;
use FOS\RestBundle\View\View;
use Sylius\Bundle\OrderBundle\Controller\AddToCartCommandInterface;
use Sylius\Bundle\ResourceBundle\Controller\RequestConfiguration;
use Sylius\Bundle\ResourceBundle\Controller\ResourceController;
use Sylius\Component\Order\CartActions;
use Sylius\Component\Order\Context\CartContextInterface;
use Sylius\Component\Order\Model\OrderInterface;
use Sylius\Component\Order\Model\OrderItemInterface;
use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface;
use Sylius\Component\Order\Modifier\OrderModifierInterface;
use Sylius\Component\Order\Repository\OrderRepositoryInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Validator\ConstraintViolationListInterface;
class OrderItemController extends ResourceController
{
public function addAction(Request $request): Response
{
$cart = $this->getCurrentCart();
$configuration = $this->requestConfigurationFactory->create($this->metadata, $request);
$this->isGrantedOr403($configuration, CartActions::ADD);
/** @var OrderItemInterface $orderItem */
$orderItem = $this->newResourceFactory->create($configuration, $this->factory);
$form = $this->getFormFactory()->create(
$configuration->getFormType(),
$this->createAddToCartCommand($cart, $orderItem),
$configuration->getFormOptions()
);
if ($request->isMethod('POST') && $form->handleRequest($request)->isValid()) {
$cart->clearItems();
/** @var AddToCartCommandInterface $addToCartCommand */
$addToCartCommand = $form->getData();
$this->getQuantityModifier()->modify($orderItem, 1);
$errors = $this->getCartItemErrors($addToCartCommand->getCartItem());
if (0 < count($errors)) {
$form = $this->getAddToCartFormWithErrors($errors, $form);
return $this->handleBadAjaxRequestView($configuration, $form);
}
$event = $this->eventDispatcher->dispatchPreEvent(CartActions::ADD, $configuration, $orderItem);
if ($event->isStopped() && !$configuration->isHtmlRequest()) {
throw new HttpException($event->getErrorCode(), $event->getMessage());
}
if ($event->isStopped()) {
$this->flashHelper->addFlashFromEvent($configuration, $event);
return $this->redirectHandler->redirectToIndex($configuration, $orderItem);
}
$this->getOrderModifier()->addToOrder($addToCartCommand->getCart(), $addToCartCommand->getCartItem());
$cartManager = $this->getCartManager();
$cartManager->persist($cart);
$cartManager->flush();
$resourceControllerEvent = $this->eventDispatcher->dispatchPostEvent(CartActions::ADD, $configuration, $orderItem);
if ($resourceControllerEvent->hasResponse()) {
return $resourceControllerEvent->getResponse();
}
$this->flashHelper->addSuccessFlash($configuration, CartActions::ADD, $orderItem);
if ($request->isXmlHttpRequest()) {
return $this->viewHandler->handle($configuration, View::create([], Response::HTTP_CREATED));
}
return $this->redirectHandler->redirectToResource($configuration, $orderItem);
}
if (!$configuration->isHtmlRequest()) {
return $this->handleBadAjaxRequestView($configuration, $form);
}
$view = View::create()
->setData([
'configuration' => $configuration,
$this->metadata->getName() => $orderItem,
'form' => $form->createView(),
])
->setTemplate($configuration->getTemplate(CartActions::ADD . '.html'))
;
return $this->viewHandler->handle($configuration, $view);
}
protected function getOrderRepository(): OrderRepositoryInterface
{
return $this->get('sylius.repository.order');
}
protected function redirectToCartSummary(RequestConfiguration $configuration): Response
{
if (null === $configuration->getParameters()->get('redirect')) {
return $this->redirectHandler->redirectToRoute($configuration, $this->getCartSummaryRoute());
}
return $this->redirectHandler->redirectToRoute($configuration, $configuration->getParameters()->get('redirect'));
}
protected function getCartSummaryRoute(): string
{
return 'sylius_cart_summary';
}
protected function getCurrentCart(): OrderInterface
{
return $this->getContext()->getCart();
}
protected function getContext(): CartContextInterface
{
return $this->get('sylius.context.cart');
}
protected function createAddToCartCommand(OrderInterface $cart, OrderItemInterface $cartItem)
{
/** @var AddToCartCommand $addCartitem */
$addCartitem = $this->get('sylius.factory.add_to_cart_command')->createWithCartAndCartItem($cart, $cartItem);
$addCartitem->setCart($cart);
$addCartitem->setCartItem($cartItem);
return $addCartitem;
}
protected function getFormFactory(): FormFactoryInterface
{
return $this->get('form.factory');
}
protected function getQuantityModifier(): OrderItemQuantityModifierInterface
{
return $this->get('sylius.order_item_quantity_modifier');
}
protected function getOrderModifier(): OrderModifierInterface
{
return $this->get('sylius.order_modifier');
}
protected function getCartManager(): EntityManagerInterface
{
return $this->get('sylius.manager.order');
}
private function getCartItemErrors(OrderItemInterface $orderItem): ConstraintViolationListInterface
{
return $this
->get('validator')
->validate($orderItem, null, $this->getParameter('sylius.form.type.order_item.validation_groups'))
;
}
private function getAddToCartFormWithErrors(ConstraintViolationListInterface $errors, FormInterface $form): FormInterface
{
foreach ($errors as $error) {
$form->get('cartItem')->get($error->getPropertyPath())->addError(new FormError($error->getMessage()));
}
return $form;
}
private function handleBadAjaxRequestView(RequestConfiguration $configuration, FormInterface $form): Response
{
return $this->viewHandler->handle(
$configuration,
View::create($form, Response::HTTP_BAD_REQUEST)->setData(['errors' => $form->getErrors(true, true)])
);
}
}
<?php
declare(strict_types=1);
namespace App\DataTransformer;
use Sylius\Component\Product\Resolver\ProductVariantResolverInterface;
use Symfony\Component\Form\DataTransformerInterface;
final class OrderItemToProductVariantTransformer implements DataTransformerInterface
{
/**
* @var ProductVariantResolverInterface
*/
private $productVariantResolver;
/**
* @param ProductVariantResolverInterface $productVariantResolver
*/
public function __construct(ProductVariantResolverInterface $productVariantResolver)
{
$this->productVariantResolver = $productVariantResolver;
}
public function transform($value)
{
// does nothing at this moment
return $value;
}
public function reverseTransform($product)
{
return $this->productVariantResolver->getVariant($product);
}
}
<?php
declare(strict_types=1);
namespace App\Form;
use App\DataTransformer\OrderItemToProductVariantTransformer;
use App\Entity\Order\OrderItem;
use App\Entity\Product\Product;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityRepository;
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
final class OrderItemType extends AbstractResourceType
{
/**
* @var ChannelContextInterface
*/
private $channelContext;
/**
* @var OrderItemToProductVariantTransformer
*/
private $orderItemToProductVariantTransformer;
/**
* @param array $validationGroups
* @param OrderItemToProductVariantTransformer $orderItemToProductVariantTransformer
* @param ChannelContextInterface $channelContext
*/
public function __construct(
array $validationGroups,
OrderItemToProductVariantTransformer $orderItemToProductVariantTransformer,
ChannelContextInterface $channelContext)
{
parent::__construct(OrderItem::class, $validationGroups);
$this->channelContext = $channelContext;
$this->orderItemToProductVariantTransformer = $orderItemToProductVariantTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('variant', EntityType::class, [
'expanded' => true,
'label' => false,
'choice_label' => 'name',
'choice_value' => 'code',
'class' => Product::class,
'query_builder' => function(EntityRepository $er) {
/** @var ProductRepository $er */
return $er->findEnabledByChannel($this->channelContext->getChannel());
}
])
->get('variant')->addModelTransformer($this->orderItemToProductVariantTransformer)
;
}
}
<?php
declare(strict_types=1);
namespace App\Repository;
use Sylius\Bundle\CoreBundle\Doctrine\ORM\ProductRepository as BaseProductRepository;
use Sylius\Component\Channel\Model\ChannelInterface;
final class ProductRepository extends BaseProductRepository
{
public function findEnabledByChannel(ChannelInterface $channel)
{
return $this->createQueryBuilder('p')
->where('p.enabled = 1')
->andWhere(':channel MEMBER OF p.channels')
->setParameter('channel', $channel)
;
}
}
services:
_defaults:
...
bind:
$validationGroups: ['sylius']
Sylius\Bundle\OrderBundle\Form\DataMapper\OrderItemQuantityDataMapper: '@sylius.form.data_mapper.order_item_quantity'
Sylius\Component\Core\Factory\CartItemFactoryInterface: '@sylius.factory.cart_item'
Sylius\Component\Product\Resolver\ProductVariantResolverInterface: '@sylius.product_variant_resolver.default'
# If you have issues with autowiring OrderItemController, put it into ResoucrController directory and exclude it as
# follows:
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,ResourceController,Kernel.php}'
...
sylius.factory.add_to_cart_command:
class: App\Factory\AddToCartCommandFactory
public: true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment