diff --git a/dist/src/Entity/Order/Order.php b/dist/src/Entity/Order/Order.php new file mode 100644 index 0000000..6e94efc --- /dev/null +++ b/dist/src/Entity/Order/Order.php @@ -0,0 +1,33 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Entity\Order; + +use Doctrine\ORM\Mapping as ORM; +use MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity\PromotionCouponsAwareTrait; +use Sylius\Component\Core\Model\Order as BaseOrder; + +/** + * @ORM\Entity + * @ORM\Table(name="sylius_order") + */ +#[ORM\Entity] +#[ORM\Table(name: 'sylius_order')] +class Order extends BaseOrder implements OrderInterface +{ + use PromotionCouponsAwareTrait; + + public function __construct() + { + parent::__construct(); + $this->initializePromotionCoupons(); + } +} diff --git a/dist/src/Entity/Order/OrderInterface.php b/dist/src/Entity/Order/OrderInterface.php new file mode 100644 index 0000000..86cd62f --- /dev/null +++ b/dist/src/Entity/Order/OrderInterface.php @@ -0,0 +1,19 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Entity\Order; + +use MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity\PromotionCouponsAwareInterface; +use Sylius\Component\Core\Model\OrderInterface as BaseOrderInterface; + +interface OrderInterface extends BaseOrderInterface, PromotionCouponsAwareInterface +{ +} diff --git a/dist/src/Migrations/Version20231218092305.php b/dist/src/Migrations/Version20231218092305.php new file mode 100644 index 0000000..45cbe5f --- /dev/null +++ b/dist/src/Migrations/Version20231218092305.php @@ -0,0 +1,42 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +final class Version20231218092305 extends AbstractMigration +{ + public function getDescription(): string + { + return ''; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE monsieurbiz_advanced_promotion_order_promotion_coupon (order_id INT NOT NULL, promotion_coupon_id INT NOT NULL, INDEX IDX_5C4132AF8D9F6D38 (order_id), INDEX IDX_5C4132AF17B24436 (promotion_coupon_id), PRIMARY KEY(order_id, promotion_coupon_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE monsieurbiz_advanced_promotion_order_promotion_coupon ADD CONSTRAINT FK_5C4132AF8D9F6D38 FOREIGN KEY (order_id) REFERENCES sylius_order (id)'); + $this->addSql('ALTER TABLE monsieurbiz_advanced_promotion_order_promotion_coupon ADD CONSTRAINT FK_5C4132AF17B24436 FOREIGN KEY (promotion_coupon_id) REFERENCES sylius_promotion_coupon (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE monsieurbiz_advanced_promotion_order_promotion_coupon DROP FOREIGN KEY FK_5C4132AF8D9F6D38'); + $this->addSql('ALTER TABLE monsieurbiz_advanced_promotion_order_promotion_coupon DROP FOREIGN KEY FK_5C4132AF17B24436'); + $this->addSql('DROP TABLE monsieurbiz_advanced_promotion_order_promotion_coupon'); + } +} diff --git a/src/Entity/AfterTaxAwareTrait.php b/src/Entity/AfterTaxAwareTrait.php index 2a2851a..75a922f 100644 --- a/src/Entity/AfterTaxAwareTrait.php +++ b/src/Entity/AfterTaxAwareTrait.php @@ -19,7 +19,7 @@ trait AfterTaxAwareTrait * @ORM\Column(name="after_tax", type="boolean", nullable=false, options={"default": false}) */ #[ORM\Column(name: 'after_tax', type: 'boolean', nullable: false, options: ['default' => false])] - protected bool $afterTax = false; + private bool $afterTax = false; public function isAfterTax(): bool { diff --git a/src/Entity/PromotionCouponsAwareInterface.php b/src/Entity/PromotionCouponsAwareInterface.php new file mode 100644 index 0000000..f4fa6e5 --- /dev/null +++ b/src/Entity/PromotionCouponsAwareInterface.php @@ -0,0 +1,27 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity; + +use Doctrine\Common\Collections\Collection; +use Sylius\Component\Core\Model\PromotionCouponInterface; + +interface PromotionCouponsAwareInterface +{ + /** @return Collection */ + public function getPromotionCoupons(): Collection; + + public function hasPromotionCoupon(PromotionCouponInterface $promotionCoupon): bool; + + public function addPromotionCoupon(PromotionCouponInterface $promotionCoupon): void; + + public function removePromotionCoupon(PromotionCouponInterface $promotionCoupon): void; +} diff --git a/src/Entity/PromotionCouponsAwareTrait.php b/src/Entity/PromotionCouponsAwareTrait.php new file mode 100644 index 0000000..634a4e5 --- /dev/null +++ b/src/Entity/PromotionCouponsAwareTrait.php @@ -0,0 +1,65 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Sylius\Component\Core\Model\PromotionCouponInterface; + +trait PromotionCouponsAwareTrait +{ + /** + * @ORM\ManyToMany(targetEntity=PromotionCouponInterface::class) + * @ORM\JoinTable(name="monsieurbiz_advanced_promotion_order_promotion_coupon", + * joinColumns={@ORM\JoinColumn(name="order_id", referencedColumnName="id")}, + * inverseJoinColumns={@ORM\JoinColumn(name="promotion_coupon_id", referencedColumnName="id")} + * ) + * + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: PromotionCouponInterface::class)] + #[ORM\JoinTable(name: 'monsieurbiz_advanced_promotion_order_promotion_coupon')] + #[ORM\JoinColumn(name: 'order_id', referencedColumnName: 'id')] + #[ORM\InverseJoinColumn(name: 'promotion_coupon_id', referencedColumnName: 'id')] + private $promotionCoupons; + + private function initializePromotionCoupons(): void + { + $this->promotionCoupons = new ArrayCollection(); + } + + /** @return Collection */ + public function getPromotionCoupons(): Collection + { + return $this->promotionCoupons; + } + + public function hasPromotionCoupon(PromotionCouponInterface $promotionCoupon): bool + { + return $this->promotionCoupons->contains($promotionCoupon); + } + + public function addPromotionCoupon(PromotionCouponInterface $promotionCoupon): void + { + if (!$this->hasPromotionCoupon($promotionCoupon)) { + $this->promotionCoupons->add($promotionCoupon); + } + } + + public function removePromotionCoupon(PromotionCouponInterface $promotionCoupon): void + { + if ($this->hasPromotionCoupon($promotionCoupon)) { + $this->promotionCoupons->removeElement($promotionCoupon); + } + } +} diff --git a/src/Form/Extension/CartTypeExtension.php b/src/Form/Extension/CartTypeExtension.php new file mode 100644 index 0000000..9cc2fed --- /dev/null +++ b/src/Form/Extension/CartTypeExtension.php @@ -0,0 +1,126 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusAdvancedPromotionPlugin\Form\Extension; + +use MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity\PromotionCouponsAwareInterface; +use Sylius\Bundle\OrderBundle\Form\Type\CartType; +use Sylius\Bundle\PromotionBundle\Form\Type\PromotionCouponToCodeType; +use Sylius\Component\Core\Model\PromotionCouponInterface; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +final class CartTypeExtension extends AbstractTypeExtension +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('promotionCoupons', CollectionType::class, [ + 'entry_type' => PromotionCouponToCodeType::class, + 'entry_options' => [ + 'attr' => [ + 'form' => 'sylius_cart', + ], + 'constraints' => [ + new Assert\Callback( + [$this, 'validatePromotionEntry'], + ['monsieurbiz_advanced_promotion_coupon'] + ), + ], + ], + 'required' => false, + 'allow_add' => true, + 'allow_delete' => true, + 'button_add_label' => 'monsieurbiz_sylius_advanced_promotion.coupons.add_coupon', + 'attr' => [ + 'class' => 'monsieurbiz-coupons', + ], + ]) + ->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'removeEmptyCouponsOnPreSubmit']) + ; + } + + public function validatePromotionEntry(?PromotionCouponInterface $entry, ExecutionContextInterface $context): void + { + if (null !== $entry) { + return; + } + + $context + ->buildViolation('sylius.promotion_coupon.is_invalid') + ->addViolation() + ; + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function removeEmptyCouponsOnPreSubmit(FormEvent $event): void + { + $formData = $event->getData(); + if (!\is_array($formData)) { + return; + } + + $promotionCoupons = $formData['promotionCoupons'] ?? []; + if (!empty($promotionCoupons)) { + return; + } + + $order = $event->getForm()->getNormData(); + if (!$order instanceof PromotionCouponsAwareInterface) { + return; + } + + foreach ($order->getPromotionCoupons() as $promotionCoupon) { + if (!$promotionCoupon) { + continue; + } + $order->removePromotionCoupon($promotionCoupon); + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setNormalizer('validation_groups', fn (Options $options, array $validationGroups) => function (FormInterface $form) use ($validationGroups) { + foreach ($form->get('promotionCoupons') as $promotionCoupon) { + if ((bool) $promotionCoupon->getNormData()) { // Validate the coupon if it was sent + $validationGroups[] = 'monsieurbiz_advanced_promotion_coupon'; + + break; + } + } + + return $validationGroups; + }); + } + + public static function getExtendedTypes(): iterable + { + return [ + CartType::class, + ]; + } +} diff --git a/src/Form/Type/PromotionCouponType.php b/src/Form/Type/PromotionCouponType.php new file mode 100644 index 0000000..5c0799c --- /dev/null +++ b/src/Form/Type/PromotionCouponType.php @@ -0,0 +1,33 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusAdvancedPromotionPlugin\Form\Type; + +use Sylius\Bundle\PromotionBundle\Form\Type\PromotionCouponToCodeType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; + +final class PromotionCouponType extends AbstractType +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('promotionCoupon', PromotionCouponToCodeType::class, [ + 'label' => 'sylius.form.cart.coupon', + 'required' => false, + 'by_reference' => false, + ]) + ; + } +} diff --git a/src/Promotion/Decorator/OrderPromotionsUsageModifierDecorator.php b/src/Promotion/Decorator/OrderPromotionsUsageModifierDecorator.php new file mode 100644 index 0000000..0aaa8b8 --- /dev/null +++ b/src/Promotion/Decorator/OrderPromotionsUsageModifierDecorator.php @@ -0,0 +1,66 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusAdvancedPromotionPlugin\Promotion\Decorator; + +use MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity\PromotionCouponsAwareInterface; +use Sylius\Component\Core\Model\OrderInterface; +use Sylius\Component\Core\Promotion\Modifier\OrderPromotionsUsageModifierInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + +#[AsDecorator('sylius.promotion_usage_modifier')] +final class OrderPromotionsUsageModifierDecorator implements OrderPromotionsUsageModifierInterface +{ + public function __construct( + #[AutowireDecorated] + private readonly OrderPromotionsUsageModifierInterface $promotionProcessor, + ) { + } + + public function increment(OrderInterface $order): void + { + $this->promotionProcessor->increment($order); + if (!$order instanceof PromotionCouponsAwareInterface) { + return; + } + + // Same as Sylius but with multiple coupons + // @see Sylius\Component\Core\Promotion\Modifier\OrderPromotionsUsageModifier + foreach ($order->getPromotionCoupons() as $promotionCoupon) { + if (!$promotionCoupon) { + continue; + } + $promotionCoupon->incrementUsed(); + } + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function decrement(OrderInterface $order): void + { + $this->promotionProcessor->decrement($order); + if (!$order instanceof PromotionCouponsAwareInterface) { + return; + } + + // Same as Sylius but with multiple coupons + // @see Sylius\Component\Core\Promotion\Modifier\OrderPromotionsUsageModifier + foreach ($order->getPromotionCoupons() as $promotionCoupon) { + if (!$promotionCoupon || OrderInterface::STATE_CANCELLED === $order->getState() && !$promotionCoupon->isReusableFromCancelledOrders()) { + continue; + } + + $promotionCoupon->decrementUsed(); + } + } +} diff --git a/src/Promotion/Decorator/PromotionProcessorDecorator.php b/src/Promotion/Decorator/PromotionProcessorDecorator.php index b5d68db..841821c 100644 --- a/src/Promotion/Decorator/PromotionProcessorDecorator.php +++ b/src/Promotion/Decorator/PromotionProcessorDecorator.php @@ -13,7 +13,6 @@ use MonsieurBiz\SyliusAdvancedPromotionPlugin\Context\PromotionContextInterface; use Sylius\Component\Promotion\Model\PromotionSubjectInterface; -use Sylius\Component\Promotion\Processor\PromotionProcessor; use Sylius\Component\Promotion\Processor\PromotionProcessorInterface; use Symfony\Component\DependencyInjection\Attribute\AsDecorator; use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; @@ -23,7 +22,7 @@ final class PromotionProcessorDecorator implements PromotionProcessorInterface { public function __construct( #[AutowireDecorated] - private readonly PromotionProcessor $promotionProcessor, + private readonly PromotionProcessorInterface $promotionProcessor, private readonly PromotionContextInterface $promotionContext, ) { } diff --git a/src/Promotion/Decorator/PromotionSubjectCouponEligibilityCheckerDecorator.php b/src/Promotion/Decorator/PromotionSubjectCouponEligibilityCheckerDecorator.php new file mode 100644 index 0000000..b722ca7 --- /dev/null +++ b/src/Promotion/Decorator/PromotionSubjectCouponEligibilityCheckerDecorator.php @@ -0,0 +1,67 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusAdvancedPromotionPlugin\Promotion\Decorator; + +use MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity\PromotionCouponsAwareInterface; +use Sylius\Component\Promotion\Checker\Eligibility\PromotionCouponEligibilityCheckerInterface; +use Sylius\Component\Promotion\Checker\Eligibility\PromotionEligibilityCheckerInterface; +use Sylius\Component\Promotion\Model\PromotionInterface; +use Sylius\Component\Promotion\Model\PromotionSubjectInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + +/** + * @SuppressWarnings(PHPMD.LongClassName) + */ +#[AsDecorator('sylius.promotion_subject_coupon_eligibility_checker')] +final class PromotionSubjectCouponEligibilityCheckerDecorator implements PromotionEligibilityCheckerInterface +{ + public function __construct( + #[AutowireDecorated] + private readonly PromotionEligibilityCheckerInterface $promotionSubjectCouponEligibilityChecker, + #[Autowire('@sylius.promotion_coupon_eligibility_checker')] + private readonly PromotionCouponEligibilityCheckerInterface $promotionCouponEligibilityChecker + ) { + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function isEligible(PromotionSubjectInterface $promotionSubject, PromotionInterface $promotion): bool + { + if ($this->promotionSubjectCouponEligibilityChecker->isEligible($promotionSubject, $promotion)) { + return true; + } + + // Process our custom check if not eligible by decorated checker + if (!$promotion->isCouponBased()) { + return true; + } + + if (!$promotionSubject instanceof PromotionCouponsAwareInterface) { + return false; + } + + // Loop on order promotions with coupon to check if one is eligible + $promotionCoupons = $promotionSubject->getPromotionCoupons(); + foreach ($promotionCoupons as $promotionCoupon) { + if (!$promotionCoupon || $promotion !== $promotionCoupon->getPromotion()) { + continue; + } + + return $this->promotionCouponEligibilityChecker->isEligible($promotionSubject, $promotionCoupon); + } + + return false; + } +} diff --git a/src/Provider/ActivePromotionsByChannelProvider.php b/src/Provider/ActivePromotionsByChannelProvider.php new file mode 100644 index 0000000..8dc83b8 --- /dev/null +++ b/src/Provider/ActivePromotionsByChannelProvider.php @@ -0,0 +1,55 @@ + + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusAdvancedPromotionPlugin\Provider; + +use Doctrine\Common\Collections\ArrayCollection; +use InvalidArgumentException; +use MonsieurBiz\SyliusAdvancedPromotionPlugin\Entity\PromotionCouponsAwareInterface; +use Sylius\Component\Core\Model\OrderInterface; +use Sylius\Component\Core\Repository\PromotionRepositoryInterface; +use Sylius\Component\Promotion\Model\PromotionSubjectInterface; +use Sylius\Component\Promotion\Provider\PreQualifiedPromotionsProviderInterface; +use Sylius\Component\Resource\Exception\UnexpectedTypeException; + +final class ActivePromotionsByChannelProvider implements PreQualifiedPromotionsProviderInterface +{ + public function __construct(private PromotionRepositoryInterface $promotionRepository) + { + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getPromotions(PromotionSubjectInterface $subject): array + { + if (!$subject instanceof OrderInterface) { + throw new UnexpectedTypeException($subject, OrderInterface::class); + } + + $channel = $subject->getChannel(); + if (null === $channel) { + throw new InvalidArgumentException('Order has no channel, but it should.'); + } + + $promotionCoupons = new ArrayCollection(); + if ($subject instanceof PromotionCouponsAwareInterface) { + $promotionCoupons = $subject->getPromotionCoupons(); + } + + // We add our condition on 0 promotion coupons + if (null === $subject->getPromotionCoupon() && 0 === $promotionCoupons->count()) { + return $this->promotionRepository->findActiveNonCouponBasedByChannel($channel); + } + + return $this->promotionRepository->findActiveByChannel($channel); + } +} diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 743d965..5573d89 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -30,3 +30,7 @@ services: $promotionProcessor: '@MonsieurBiz\SyliusAdvancedPromotionPlugin\Promotion\Processor\AfterTaxPromotionProcessor' tags: - { name: 'sylius.order_processor', priority: 5 } # Tax processor is 10 + + # Change Sylius active promotion provider to manage case on multiple coupons + sylius.active_promotions_provider: + class: MonsieurBiz\SyliusAdvancedPromotionPlugin\Provider\ActivePromotionsByChannelProvider diff --git a/src/Resources/config/ui/cart.yaml b/src/Resources/config/ui/cart.yaml new file mode 100644 index 0000000..2a47a24 --- /dev/null +++ b/src/Resources/config/ui/cart.yaml @@ -0,0 +1,10 @@ + +sylius_ui: + events: + sylius.shop.cart.coupon: + blocks: + content: + enabled: false + monsieurbiz_advanced_promotion_coupons: + template: "@MonsieurBizSyliusAdvancedPromotionPlugin/Shop/Cart/Summary/_coupons.html.twig" + priority: 10 diff --git a/src/Resources/translations/messages.en.yml b/src/Resources/translations/messages.en.yml index f0cb878..203b109 100644 --- a/src/Resources/translations/messages.en.yml +++ b/src/Resources/translations/messages.en.yml @@ -2,3 +2,6 @@ monsieurbiz_sylius_advanced_promotion: promotion: applied_after_tax: Applied after tax advanced_promotion_configuration: Advanced promotion configuration + coupons: + add_coupon: Add coupon + apply_coupons: Apply coupons diff --git a/src/Resources/translations/messages.fr.yml b/src/Resources/translations/messages.fr.yml index d5722a7..fc4eb98 100644 --- a/src/Resources/translations/messages.fr.yml +++ b/src/Resources/translations/messages.fr.yml @@ -2,3 +2,6 @@ monsieurbiz_sylius_advanced_promotion: promotion: applied_after_tax: Appliqué après la taxe advanced_promotion_configuration: Configuration de la promotion avancée + coupons: + add_coupon: Ajouter un coupon + apply_coupons: Appliquer les coupons diff --git a/src/Resources/views/Shop/Cart/Summary/_coupons.html.twig b/src/Resources/views/Shop/Cart/Summary/_coupons.html.twig new file mode 100644 index 0000000..bc7e98b --- /dev/null +++ b/src/Resources/views/Shop/Cart/Summary/_coupons.html.twig @@ -0,0 +1,41 @@ +{% form_theme form '@MonsieurBizSyliusAdvancedPromotionPlugin/Shop/Form/theme.html.twig' %} + +{% if form.promotionCoupon.vars.value|default(null) %} + {% include '@SyliusShop/Cart/Summary/_coupon.html.twig' %} +{% else %} +
+
+ {{ form_widget(form.promotionCoupons) }} +
+
+ +
+
+ + +{% endif %} diff --git a/src/Resources/views/Shop/Form/theme.html.twig b/src/Resources/views/Shop/Form/theme.html.twig new file mode 100644 index 0000000..e6c661d --- /dev/null +++ b/src/Resources/views/Shop/Form/theme.html.twig @@ -0,0 +1,58 @@ +{% extends '@SyliusShop/Form/theme.html.twig' %} + +{# No changes with Sylius form #} +{# We copied it to called our `collection_item` #} +{% block collection_widget -%} + {% from '@SyliusResource/Macros/notification.html.twig' import error %} + {% import _self as self %} + {% set attr = attr|merge({'class': attr.class|default ~ ' controls collection-widget'}) %} + + {% apply spaceless %} +
+ {{ error(form.vars.errors) }} + + {% if prototypes|default is iterable %} + {% for key, subPrototype in prototypes %} + + {% endfor %} + {% endif %} + +
+ {% for child in form %} + {{ self.collection_item(child, allow_delete, button_delete_label, loop.index0) }} + {% endfor %} +
+ + {% if prototype is defined and allow_add %} + + + {{ button_add_label|trans }} + + {% endif %} +
+ {% endapply %} +{%- endblock collection_widget %} + +{# Same as Sylius one but add `form_errors` #} +{% macro collection_item(form, allow_delete, button_delete_label, index) %} + {% apply spaceless %} +
+ {{ form_widget(form) }} + {% if allow_delete %} + + + {{ button_delete_label|trans }} + + {% endif %} + {{ form_errors(form) }} {# Add this line to show error #} +
+ {% endapply %} +{% endmacro %}