Pack d'articles ajoutés au panier en un clic
This post is available in englishDans cet article on va créer une action qui va ajouter une liste de produits au panier. On va également créer la gestion des produits par pack dans le dashboard.
On va commencer par créer une entité qu'on pourra gérer dans le dashboard et qui nous permettra de sélectionner les produits qu'on veut mettre dans le 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;
}
On déclare cette ressource dans config/packages/resources.yaml
sylius_resource:
resources:
app.product_bundle:
classes:
model: App\Entity\ProductBundle
Sa grille dans 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
Ainsi que sa route dans 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
On met à jour la base de données
php bin/console doctrine:migration:diff
php bin/console doctrine:migration:migrate
Maintenant quand on se rend sur le dashboard à l'adresse admin/product-bundles/
et qu'on clique sur Create
on à ce résultat :
On voudrait avoir les titres des Produits, on va donc créer notre propre formulaire et on va en choisir les produits avec une liste de checkbox
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,
))
;
}
}
On déclare notre nouveau formulaire en tant que service dans config/services.yaml
services:
app.form.type.bundle:
class: App\Form\Type\BundleType
tags:
- { name: form.type}
arguments: ['App\Entity\ProductBundle']
Et on rajoute dans notre ressource config/packages/resources.yaml
le chemin de notre nouveau formulaire
sylius_resource:
resources:
app.product_bundle:
classes:
model: App\Entity\ProductBundle
form: App\Form\Type\BundleType # on rajoute cette ligne
Si on rafraichit la page admin/product-bundles/new
et le résultat est le suivant :
On choisit nos produits, puis on met un titre à notre pack et on clique sur Create
, ici je vais créer le pack "Special"
Maintenant on va afficher ce pack dans la 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)
);
}
}
On crée la vue qui est retourné par le 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>
On ajoute sa route dans routes.yaml
config/routes.yaml
app_page_product_bundle:
path: /bundle
methods: [GET]
controller: App\Controller\ProductBundleController::index
Le retour de notre action index de notre controller sera rendu dans un template, on crée donc celui-ci templates/ProductBundle/_index.html.twig
en faisant attention de mettre le nom de notre pack
{{ render(controller('App\\Controller\\ProductBundleController::index', {'bundleName' : 'Special'})) }}
On ajoute notre template dans le rendu par event de sylius config/packages/sylius_ui.yaml
sylius_ui:
events:
sylius.shop.homepage:
blocks:
bundle:
template: "ProductBundle/_index.html.twig"
priority: 70
priority indique l'ordre de rendu des templates appelés. On peut les trouver en inspectant le rendu HTML :
Nous avons maintenant ce résultat sur la page d'accueil
Il nous reste à créer une action qui va récupérer les produits dans la boucle. Pour l'exemple nous prendrons leur première variante (c'est à dire, pour un tee-shirt par exemple, la première taille qui va sortir donc la taille 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');
}
}
On le déclare en tant que 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
On définit sa route config/routes.yaml
app_add_bundle:
path: /bundle/{name}
methods: [POST]
defaults:
_controller: App\Controller\ProductBundleAction
On rajoute notre formulaire à notre 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>
On à maintenant le bouton "Buy this pack"
Maintenant quand on clique dessus on est redirigé vers le panier avec les produits ajoutés