Bundle of products added to cart in one click
This post is available in frenchIn this article we will create an action that will add a list of products to the cart. We will also create the management of products by pack in the dashboard.
We will start by creating an entity that we can manage in the dashboard and which will allow us to select the products we want to put in the pack.
src/Entity/ProductBundle.php
<?php
namespace App\Entity;
use App\Entity\Product\Product;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\ProductBundleRepository")
*/
class ProductBundle implements ProductBundleInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string|null
*
* @ORM\Column(type="string")
*/
private $name;
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Product\Product")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
/**
* @return string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* @param string $name
*/
public function setName(?string $name): void
{
$this->name = $name;
}
public function countProducts(): int
{
return $this->products->count();
}
/**
* @return Collection|Product[]
*/
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->contains($product)) {
$this->products->removeElement($product);
}
return $this;
}
}
src/Repository/ProductBundleRepository.php
<?php
namespace App\Repository;
use App\Entity\ProductBundle;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method ProductBundle|null find($id, $lockMode = null, $lockVersion = null)
* @method ProductBundle|null findOneBy(array $criteria, array $orderBy = null)
* @method ProductBundle[] findAll()
* @method ProductBundle[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ProductBundleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ProductBundle::class);
}
// /**
// * @return ProductBundle[] Returns an array of ProductBundle objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('b')
->andWhere('b.exampleField = :val')
->setParameter('val', $value)
->orderBy('b.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?ProductBundle
{
return $this->createQueryBuilder('b')
->andWhere('b.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
src/Entity/ProductBundleInterface.php
<?php
namespace App\Entity;
use App\Entity\Product\Product;
use Doctrine\Common\Collections\Collection;
use Sylius\Component\Resource\Model\ResourceInterface;
interface ProductBundleInterface extends ResourceInterface
{
/**
* @return string
*/
public function getName(): ?string;
/**
* @param string $name
*/
public function setName(?string $name): void;
public function countProducts(): int;
/**
* @return Collection|Product[]
*/
public function getProducts(): Collection;
public function addProduct(Product $product): \App\Entity\ProductBundle;
public function removeProduct(Product $product): \App\Entity\ProductBundle;
}
We declare this resource in config/packages/resources.yaml
sylius_resource:
resources:
app.product_bundle:
classes:
model: App\Entity\ProductBundle
Its grid in config/packages/grids.yaml
sylius_grid:
grids:
app_admin_product_bundle:
driver:
name: doctrine/orm
options:
class: App\Entity\ProductBundle
fields:
name:
type: string
actions:
main:
create:
type: create
item:
update:
type: update
delete:
type: delete
As well as its route in config/routes.yaml
app_product_bundle:
resource: |
alias: app.product_bundle
section: admin
templates: SyliusAdminBundle:Crud
grid: app_admin_product_bundle
redirect: index
type: sylius.resource
prefix: admin
We update the database
php bin/console doctrine:migration:diff
php bin/console doctrine:migration:migrate
Now when we go to the dashboard at the address admin/product-bundles/
and we click on Create
we have this result:
We would like to have the titles of the Products, so we will create our own form and we will choose the products with a checkbox list
src/Form/Type/BundleType.php
<?php
namespace App\Form\Type;
use App\Entity\Product\Product;
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class BundleType extends AbstractResourceType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, ['label' => 'sylius.ui.name'])
->add('products', EntityType::class, array(
'class' => Product::class,
'choice_label' => 'name',
'label' => 'Product',
'expanded' => true,
'multiple' => true,
))
;
}
}
We declare our new form as a service in config/services.yaml
services:
app.form.type.bundle:
class: App\Form\Type\BundleType
tags:
- { name: form.type}
arguments: ['App\Entity\ProductBundle']
And we add in our resource config/packages/resources.yaml
the way to our new form
sylius_resource:
resources:
app.product_bundle:
classes:
model: App\Entity\ProductBundle
form: App\Form\Type\BundleType # we add this line
If we refresh the page admin/product-bundles/new
the result is as follows :
We choose our products, then we put a title to our pack and we click on Create
, here I will create the pack "Special"
Now we will display this pack in the homepage,
src/Controller/ProductBundleController.php
<?php
namespace App\Controller;
use App\Repository\ProductBundleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class ProductBundleController extends AbstractController
{
public function index($bundleName, ProductBundleRepository $bundles)
{
$bundle = $bundles->findOneBy(array('name' => $bundleName));
return $this->render('ProductBundle/_products.html.twig',
array('bundle' => $bundle)
);
}
}
We create the view which is returned by the controller templates/ProductBundle/_products.html.twig
<div class="ui very padded center aligned inverted segment">
<h2 class="ui center aligned huge header">Bundle product</h2>
<div class="ui three odd doubling cards segment centered">
{% for product in bundle.products %}
{% include '@SyliusShop/Product/_box.html.twig' %}
{% endfor %}
</div>
</div>
We add its route in routes.yaml
config/routes.yaml
app_page_product_bundle:
path: /bundle
methods: [GET]
controller: App\Controller\ProductBundleController::index
The return of our action index of our controller will be rendered in a template, so we create this one templates/ProductBundle/_index.html.twig
being careful to put the name of our pack
{{ render(controller('App\\Controller\\ProductBundleController::index', {'bundleName' : 'Special'})) }}
We add our template in the event rendering of sylius config/packages/sylius_ui.yaml
sylius_ui:
events:
sylius.shop.homepage:
blocks:
bundle:
template: "ProductBundle/_index.html.twig"
priority: 70
priority indicates the order of rendering of the called templates. They can be found by inspecting the HTML rendering:
We now have this result on the home page
We still have to create an action that will recover the products in the loop. For the example we will take their first variant (that is to say, for a T-shirt for example, the first size that will come out therefore size S)
src/Controller/ProductBundleAction.php
<?php
namespace App\Controller;
use App\Repository\ProductBundleRepository;
use Doctrine\Common\Persistence\ObjectManager;
use Sylius\Bundle\ResourceBundle\Controller\ResourceController;
use Sylius\Component\Core\Context\ShopperContextInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface;
use Sylius\Component\Order\Context\CartContextInterface;
use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class ProductBundleAction extends ResourceController
{
/** @var ProductVariantRepositoryInterface */
private $productVariantRepository;
/** @var FactoryInterface */
private $orderItemFactory;
/** @var OrderItemQuantityModifierInterface */
private $orderItemQuantityModifier;
/** @var ShopperContextInterface */
private $shopperContext;
/** @var ObjectManager */
private $orderManager;
/** @var UrlGeneratorInterface */
private $router;
/**
* QuickCheckoutAction constructor.
* @param ProductVariantRepositoryInterface $productVariantRepository
* @param FactoryInterface $orderFactory
* @param FactoryInterface $orderItemFactory
* @param OrderItemQuantityModifierInterface $orderItemQuantityModifier
* @param ShopperContextInterface $shopperContext
* @param ObjectManager $orderManager
* @param UrlGeneratorInterface $router
*/
public function __construct(ProductVariantRepositoryInterface $productVariantRepository,
FactoryInterface $orderItemFactory,
OrderItemQuantityModifierInterface $orderItemQuantityModifier,
ShopperContextInterface $shopperContext,
ObjectManager $orderManager,
UrlGeneratorInterface $router)
{
$this->productVariantRepository = $productVariantRepository;
$this->orderItemFactory = $orderItemFactory;
$this->orderItemQuantityModifier = $orderItemQuantityModifier;
$this->shopperContext = $shopperContext;
$this->orderManager = $orderManager;
$this->router = $router;
}
public function __invoke(Request $request, ProductBundleRepository $productBundleRepository): Response
{
$bundleName = $request->attributes->get('name');
$bundle = $productBundleRepository->findOneBy(array('name' => $bundleName));
/** @var OrderInterface $order */
$order = $this->getCurrentCart();
$channel = $this->shopperContext->getChannel();
foreach ($bundle->getProducts() as $product)
{
$productId = (int) $product->getVariants()->first()->getId();
/** @var ProductVariantInterface $variant */
$variant = $this->productVariantRepository->find($productId);
/** @var OrderItemInterface $orderItem */
$orderItem = $this->orderItemFactory->createNew();
$variant->setShippingRequired(false);
$orderItem->setVariant($variant);
$price = $variant->getChannelPricingForChannel($channel)->getPrice();
$orderItem->setUnitPrice($price);
$this->orderItemQuantityModifier->modify($orderItem, 1);
$order->addItem($orderItem);
}
$order->setChannel($channel);
$order->setLocaleCode($this->shopperContext->getLocaleCode());
$order->setCurrencyCode($this->shopperContext->getCurrencyCode());
$this->orderManager->persist($order);
$this->orderManager->flush();
$cartPage = $this->router->generate('sylius_shop_cart_summary');
return new RedirectResponse($cartPage);
}
protected function getCurrentCart(): \Sylius\Component\Order\Model\OrderInterface
{
return $this->getContext()->getCart();
}
protected function getContext(): CartContextInterface
{
return $this->get('sylius.context.cart');
}
}
It is declared as a service config/services.yaml
services:
App\Controller\ProductBundleAction:
arguments:
- '@sylius.repository.product_variant'
- '@sylius.factory.order_item'
- '@sylius.order_item_quantity_modifier'
- '@sylius.context.shopper'
- '@sylius.manager.order'
- '@router'
public: true
We define its route config/routes.yaml
app_add_bundle:
path: /bundle/{name}
methods: [POST]
defaults:
_controller: App\Controller\ProductBundleAction
We add our form to our template templates/ProductBundle/_products.html.twig
<div class="ui very padded center aligned inverted segment">
<h2 class="ui center aligned huge header">Bundle product</h2>
<div class="ui three odd doubling cards segment centered">
{% for product in bundle.products %}
{% include '@SyliusShop/Product/_box.html.twig' %}
{% endfor %}
</div>
<form action="{{ path('app_add_bundle', {name: 'Special' }) }}" method="post">
<button type="submit" class="ui huge yellow icon labeled button"><i class="bolt icon"></i> Buy this pack</button>
</form>
</div>
We now have the button "Buy this pack"
Now when we click on it we are redirected to the basket with the added products