Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix to allow adding products with a customer option to the cart #134

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,43 @@ imports:
```yaml
brille24_customer_options:
resource: "@Brille24SyliusCustomerOptionsPlugin/Resources/config/app/routing.yml"

sylius_shop_ajax_cart_add_item:
path: ajax/cart/add
methods: [POST]
defaults:
_controller: sylius.controller.order_item::addAction
_format: json
_sylius:
factory:
method: createForProductWithCustomerOption
arguments: [expr:notFoundOnNull(service('sylius.repository.product').find($productId))]
form:
type: Sylius\Bundle\CoreBundle\Form\Type\Order\AddToCartType
options:
product: expr:notFoundOnNull(service('sylius.repository.product').find($productId))
redirect:
route: sylius_shop_cart_summary
parameters: {}
flash: sylius.cart.add_item

sylius_shop_partial_cart_add_item:
path: cart/add-item
methods: [GET]
defaults:
_controller: sylius.controller.order_item::addAction
_sylius:
template: $template
factory:
method: createForProductWithCustomerOption
arguments: [expr:notFoundOnNull(service('sylius.repository.product').find($productId))]
form:
type: Sylius\Bundle\CoreBundle\Form\Type\Order\AddToCartType
options:
product: expr:notFoundOnNull(service('sylius.repository.product').find($productId))
redirect:
route: sylius_shop_cart_summary
parameters: {}
```

* Copy the template overrides from the plugin directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,66 @@
*/
declare(strict_types=1);

namespace Brille24\SyliusCustomerOptionsPlugin\EventListener;
namespace Brille24\SyliusCustomerOptionsPlugin\Factory;

use Brille24\SyliusCustomerOptionsPlugin\Entity\OrderItemInterface;
use Brille24\SyliusCustomerOptionsPlugin\Enumerations\CustomerOptionTypeEnum;
use Brille24\SyliusCustomerOptionsPlugin\Factory\OrderItemOptionFactoryInterface;
use Brille24\SyliusCustomerOptionsPlugin\Repository\CustomerOptionRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent;
use Sylius\Component\Core\Factory\CartItemFactoryInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Order\Processor\OrderProcessorInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Product\Resolver\ProductVariantResolverInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Webmozart\Assert\Assert;

final class AddToCartListener
class CartItemFactory implements CartItemFactoryInterface
{
private CartItemFactoryInterface $decoratedFactory;
private ProductVariantResolverInterface $variantResolver;
private RequestStack $requestStack;
private OrderItemOptionFactoryInterface $orderItemOptionFactory;
private CustomerOptionRepositoryInterface $customerOptionRepository;

public function __construct(
private RequestStack $requestStack,
private EntityManagerInterface $entityManager,
private OrderItemOptionFactoryInterface $orderItemOptionFactory,
private OrderProcessorInterface $orderProcessor,
private CustomerOptionRepositoryInterface $customerOptionRepository,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we are not using constructor property promotion here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question ;) Not sure why I went this way in the original PR but we could totally switch to constructor property promotion.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be great if you could do that.

FactoryInterface $decoratedFactory,
ProductVariantResolverInterface $variantResolver,
RequestStack $requestStack,
OrderItemOptionFactoryInterface $orderItemOptionFactory,
CustomerOptionRepositoryInterface $customerOptionRepository
) {
$this->decoratedFactory = new \Sylius\Component\Core\Factory\CartItemFactory($decoratedFactory, $variantResolver);
$this->variantResolver = $variantResolver;
$this->requestStack = $requestStack;
$this->orderItemOptionFactory = $orderItemOptionFactory;
$this->customerOptionRepository = $customerOptionRepository;
}

public function createForProduct(ProductInterface $product): OrderItemInterface
{
return $this->decoratedFactory->createForProduct($product);
}

public function addItemToCart(ResourceControllerEvent $event): void
public function createForCart(OrderInterface $order): OrderItemInterface
{
/** @var OrderItemInterface $orderItem */
$orderItem = $event->getSubject();
return $this->decoratedFactory->createForCart($order);
}

// If the order is null, it's an old order item with an existing reference in the database
if ($orderItem->getOrder() === null) {
return;
}
public function createNew()
{
return $this->decoratedFactory->createNew();
}

public function createForProductWithCustomerOption(ProductInterface $product): OrderItemInterface
{
/** @var OrderItemInterface $cartItem */
$cartItem = $this->createNew();
$cartItem->setVariant($this->variantResolver->getVariant($product));

$request = $this->requestStack->getCurrentRequest();
if (!$request instanceof Request) {
return;
return $cartItem;
}

$customerOptionConfiguration = $this->getCustomerOptionsFromRequest($request);
Expand All @@ -69,28 +90,26 @@ public function addItemToCart(ResourceControllerEvent $event): void
foreach ($valueArray as $value) {
// Creating the item
$salesOrderConfiguration = $this->orderItemOptionFactory->createNewFromStrings(
$orderItem,
$cartItem,
$customerOptionCode,
$value,
$value
);

$this->entityManager->persist($salesOrderConfiguration);

$salesOrderConfigurations[] = $salesOrderConfiguration;
}
}

$orderItem->setCustomerOptionConfiguration($salesOrderConfigurations);
/** @var OrderInterface $order */
$order = $orderItem->getOrder();
$this->orderProcessor->process($order);
$cartItem->setCustomerOptionConfiguration($salesOrderConfigurations);

$this->entityManager->persist($orderItem);
$this->entityManager->flush();
return $cartItem;
}

/**
* Gets the customer options from the request
*
* @param Request $request
*
* @return array
*/
public function getCustomerOptionsFromRequest(Request $request): array
{
Expand All @@ -109,20 +128,20 @@ public function getCustomerOptionsFromRequest(Request $request): array

switch ($customerOption->getType()) {
case CustomerOptionTypeEnum::DATE:
$day = $value['day'];
$month = $value['month'];
$year = $value['year'];
$day = $value['day'];
$month = $value['month'];
$year = $value['year'];
$addToCart['customer_options'][$code] = sprintf('%d-%d-%d', $year, $month, $day);

break;
case CustomerOptionTypeEnum::DATETIME:
$date = $value['date'];
$time = $value['time'];
$day = $date['day'];
$date = $value['date'];
$time = $value['time'];
$day = $date['day'];
$month = $date['month'];
$year = $date['year'];
$year = $date['year'];

$hour = $time['hour'] ?? 0;
$hour = $time['hour'] ?? 0;
$minute = $time['minute'] ?? 0;

$addToCart['customer_options'][$code] = sprintf('%d-%d-%d %d:%d', $year, $month, $day, $hour, $minute);
Expand Down
10 changes: 0 additions & 10 deletions src/Resources/config/app/services/eventListener.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
<container xmlns="http://symfony.com/schema/dic/services">
<services>

<service class="Brille24\SyliusCustomerOptionsPlugin\EventListener\AddToCartListener" id="brille24.customer_options_plugin.event.add_to_cart_listener">
<tag name="kernel.event_listener" event="sylius.order_item.post_add" method="addItemToCart"/>
<argument type="service" id="request_stack"/>
<argument type="service" id="doctrine.orm.default_entity_manager"/>
<argument type="service" id="brille24.factory.order_item_option"/>
<argument type="service" id="sylius.order_processing.order_processor.composite"/>
<argument type="service" id="brille24.repository.customer_option"/>
</service>

<service
class="Brille24\SyliusCustomerOptionsPlugin\EventListener\CustomerOptionValueListener"
id="brille24.customer_options_plugin.event_listener.customer_option_value"
Expand Down
14 changes: 14 additions & 0 deletions src/Resources/config/app/services/factory.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,19 @@
<argument type="service" id="brille24.repository.customer_option_value"/>
<argument type="service" id="brille24.customer_options_plugin.factory.customer_option_value_price_factory" />
</service>

<service
class="Brille24\SyliusCustomerOptionsPlugin\Factory\CartItemFactory"
id="sylius.custom_factory.order_item"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a naming schema. brille24.factory.order_item would probably fit better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO I need the Sylius ID here to overwrite the service definition. Other services rely on that ID, that's why I picked it. Or am I missing something?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shochdoerfer this is a decorator. You do not need to overwrite the service ID. The decorator gets its own id. And when the service it decorates is used, the decorator is applied automatically. Or symfony replaces the old one with the decorated one automatically? Anyway, looking at the docs and recalling my experience with decorators, I am certain you do not overwrite the service ID in the config. You just need to configure what service you intend to decorate.
Have you tried using the id @mamazu suggested? I think that should work just fine.

https://symfony.com/doc/6.4/service_container/service_decoration.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seizan8 that makes sense. Can't recall why it did not work for me or it looked like it is not working.

I've fixed the issue locally already. Currently checking how to best fix the tests as things changed quite a bit.

decorates="sylius.factory.order_item"
decoration-priority="256"
public="false"
>
<argument type="service" id="sylius.custom_factory.order_item.inner"/>
<argument type="service" id="sylius.product_variant_resolver.default"/>
<argument type="service" id="request_stack"/>
<argument type="service" id="brille24.factory.order_item_option"/>
<argument type="service" id="brille24.repository.customer_option"/>
</service>
</services>
</container>
28 changes: 26 additions & 2 deletions src/Traits/OrderItemCustomerOptionCapableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,32 @@ public function equals(SyliusOrderItemInterface $item): bool
return false;
}

$product = $item instanceof self ? $item->getProduct() : null;
if (!$item instanceof self) {
return false;
}

/** @var OrderItemOptionInterface[] $itemCustomerConfiguration */
$itemCustomerConfiguration = $item->getCustomerOptionConfiguration(true);
$curItemCustomerConfiguration = $this->getCustomerOptionConfiguration(true);

// configuration array size needs to be identical
if (count($itemCustomerConfiguration) !== count($curItemCustomerConfiguration)) {
return false;
}

// configuration array keys need to match
$diff = array_diff_key($itemCustomerConfiguration, $curItemCustomerConfiguration);
if (count($diff) !== 0) {
return false;
}

// iterate over all options and compare their values
foreach ($itemCustomerConfiguration as $code => $value) {
if ($curItemCustomerConfiguration[$code]->getOptionValue() !== $value->getOptionValue()) {
return false;
}
}

return ($product instanceof ProductInterface) ? !$product->hasCustomerOptions() : true;
return true;
}
}
Loading