<?php
namespace Boldr\Shop\ShopBundle\Presenter\Product;
use Boldr\Shop\ShopBundle\CurrencyFormatter;
use Boldr\Shop\ShopBundle\Price\PriceModifierManager;
use Boldr\Shop\ShopBundle\Price\Catalog\{ CatalogPriceQuery, CatalogPriceCalculator };
use Boldr\Shop\ShopBundle\Option\OptionManager;
use Boldr\Shop\ShopBundle\Presenter\Category\CategoryPresenter;
use Boldr\Shop\ShopBundle\Presenter\Currency\CurrencyPresenter;
use Boldr\Shop\ShopBundle\Presenter\Option\OptionPresenter;
use Boldr\Shop\ShopBundle\Presenter\Price\PriceModifierView;
use Boldr\Shop\ShopBundle\Entity\{ Attribute, Product, ProductTranslation, ProductVariant, ProductAttribute, ProductImage, Customer };
use Boldr\Shop\ShopBundle\Stock\StockManagerInterface;
use Boldr\Cms\CmsBundle\Content\{ Assets, ContentManager, MediaResizer };
use Boldr\Cms\CmsBundle\Presenter\Attachment\AttachmentPresenter;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Psr\Container\ContainerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
class ProductPresenter implements ServiceSubscriberInterface
{
protected ContainerInterface $container;
public static function getSubscribedServices(): array
{
return [
CatalogPriceCalculator::class,
OptionManager::class,
MediaResizer::class,
ContentManager::class,
CurrencyFormatter::class,
CategoryPresenter::class,
CurrencyPresenter::class,
OptionPresenter::class,
PriceModifierManager::class,
StockManagerInterface::class,
EntityManagerInterface::class,
AttachmentPresenter::class,
TagAwareCacheInterface::class
];
}
// Processors
private iterable $productViewProcessors;
private iterable $productVariantViewProcessors;
private iterable $catalogProductViewProcessors;
private iterable $catalogProductVariantViewProcessors;
private iterable $attributeViewProcessors;
// Media
private array $productMediaSizes;
private string $resizedMediaFolder;
// Cache
private array $attributeViews = [];
private array $productViews = [];
private array $productVariantViews = [];
public function __construct(
ContainerInterface $container,
iterable $productViewProcessors,
iterable $productVariantViewProcessors,
iterable $catalogProductViewProcessors,
iterable $catalogProductVariantViewProcessors,
iterable $attributeViewProcessors,
string $resizedMediaFolder,
array $productMediaSizes,
) {
$this->container = $container;
$this->resizedMediaFolder = $resizedMediaFolder;
$this->productMediaSizes = $productMediaSizes;
$this->attributeViewProcessors = $attributeViewProcessors;
$this->productViewProcessors = $productViewProcessors;
$this->productVariantViewProcessors = $productVariantViewProcessors;
$this->catalogProductViewProcessors = $catalogProductViewProcessors;
$this->catalogProductVariantViewProcessors = $catalogProductVariantViewProcessors;
}
public function createProductView(Product $product, string $locale): ProductView
{
$cacheKey = $product->getId().'_'.$locale;
if (isset($this->productViews[$cacheKey]))
return $this->productViews[$cacheKey];
// Create ProductVariant views
$productVariantViews = array_map(
fn($productVariant) => $this->createProductVariantView($productVariant, $locale),
$product->getVariants()->toArray()
);
// Create Attribute views
$attributeViews = array_map(
fn($attribute) => $this->createAttributeView($attribute, $locale),
$this->getProductAttributes($product)
);
// Create category views
$categoryPresenter = $this->container->get(CategoryPresenter::class);
$categoryViews = array_map(
fn($category) => $categoryPresenter->createCategoryView($category, $locale),
$product->getCategories()->toArray()
);
$primaryImageView = $product->getPrimaryImageOptions() === null ? null : $this->createProductImageView($product->getPrimaryImageOptions(), $locale);
$defaultVariantView = $this->createProductVariantView($product->getDefaultVariant(), $locale);
$translation = $product->getTranslations()->get($locale);
$view = new ProductView(
$product->getId(),
$translation->getName(),
$this->container->get(ContentManager::class)->renderAsHtml($translation->getDescription(), new Assets),
$productVariantViews,
$defaultVariantView,
$attributeViews,
$categoryViews,
$primaryImageView
);
// Let view processors process view to add their own data
$view = array_reduce(
iterator_to_array($this->productViewProcessors),
static fn($view, $productViewProcessor) => $productViewProcessor->processProductView($view, $product, $locale),
$view
);
$this->productViews[$cacheKey] = $view;
return $view;
}
public function createProductImageView(ProductImage $productImage, string $locale): ProductImageView
{
$cacheKey = 'boldr_shop.product_image.'.$productImage->getId().'.'.$locale;
return $this->container->get(TagAwareCacheInterface::class)->get($cacheKey, function($item) use ($productImage, $locale) {
$item->tag('boldr_shop.product.'.$productImage->getProduct()->getId());
$attachmentView = $this->container->get(AttachmentPresenter::class)->createAttachmentView($productImage->getImage(), $this->productMediaSizes, $locale);
return new ProductImageView(
$attachmentView,
$productImage->getCover(),
$productImage->getPositionX(),
$productImage->getPositionY()
);
});
}
private $productAttributeCache = null;
private $productVariantAttributeCache = null;
private function initAttributeCache(): void
{
if ($this->productVariantAttributeCache === null)
{
$this->productAttributeCache = [];
$this->productVariantAttributeCache = [];
$productAttributeRepository = $this->container->get(EntityManagerInterface::class)->getRepository(ProductAttribute::class);
foreach ($productAttributeRepository->findAll() as $productAttribute)
{
if ($productAttribute->getProductVariant() !== null)
{
$this->productVariantAttributeCache[$productAttribute->getProductVariant()->getId()][] = $productAttribute;
}
else
{
$this->productAttributeCache[$productAttribute->getProduct()->getId()][] = $productAttribute;
}
}
}
}
private function getProductVariantAttributes(ProductVariant $productVariant): array
{
$this->initAttributeCache();
return $this->productVariantAttributeCache[$productVariant->getId()] ?? [];
}
private function getProductAttributes(Product $product): array
{
$this->initAttributeCache();
return $this->productAttributeCache[$product->getId()] ?? [];
}
public function createProductVariantView(ProductVariant $productVariant, string $locale): ProductVariantView
{
$cacheKey = $productVariant->getId().'_'.$locale;
if (isset($this->productVariantViews[$cacheKey]))
return $this->productVariantViews[$cacheKey];
$name = $productVariant->getTranslations()->get($locale)->getName();
if ($name === null || $name === '')
{
$name = $productVariant->getProduct()->getTranslations()->get($locale)->getName();
}
$description = null;
// $description = $productVariant->getTranslations()->get($locale)->getDescription();
if ($description === null)
{
$description = $productVariant->getProduct()->getTranslations()->get($locale)->getDescription();
}
$attributes = array_map(
fn($attribute) => $this->createAttributeView($attribute, $locale),
$this->getProductVariantAttributes($productVariant)
);
$view = new ProductVariantView(
$productVariant->getId(),
$name,
$this->container->get(ContentManager::class)->renderAsHtml($description, new Assets),
$attributes
);
// Let view processors process view to add their own data
$view = array_reduce(
iterator_to_array($this->productVariantViewProcessors),
static fn($view, $productVariantViewProcessor) => $productVariantViewProcessor->processProductVariantView($view, $productVariant, $locale),
$view
);
$this->productVariantViews[$cacheKey] = $view;
return $view;
}
public function createAttributeView(Attribute $attribute, string $locale): AttributeView
{
$cacheKey = $attribute->getId().'_'.$locale;
if (isset($this->attributeViews[$cacheKey]))
return $this->attributeViews[$cacheKey];
$view = new AttributeView(
$attribute->getId(),
$attribute->getTranslations()->get($locale)->getName()
);
$view = array_reduce(
iterator_to_array($this->attributeViewProcessors),
static fn($attributeViewProcessor) => $attributeViewProcessor->processAttributeView($view, $attribute, $locale),
$view
);
$this->attributeViews[$cacheKey] = $view;
return $view;
}
public function createCatalogProductView(Product $product, ?Customer $customer, string $currency, string $locale): CatalogProductView
{
$variants = array_map(
fn($productVariant) => $this->createCatalogProductVariantView($productVariant, [], $customer, $currency, $locale),
$product->getVariants()->toArray()
);
$view = new CatalogProductView(
$this->createProductView($product, $locale),
$variants
);
return array_reduce(
iterator_to_array($this->catalogProductViewProcessors),
static fn($catalogProductViewProcessor) => $catalogProductViewProcessor->processCatalogProductView($view, $product, $customer, $locale),
$view
);
}
public function createCatalogProductVariantView(ProductVariant $productVariant, array $options, ?Customer $customer, string $currency, string $locale): CatalogProductVariantView
{
$optionManager = $this->container->get(OptionManager::class);
$optionPresenter = $this->container->get(OptionPresenter::class);
$stockManager = $this->container->get(StockManagerInterface::class);
$optionViews = [];
// $optionViews = array_map(
// fn($option) => $optionPresenter->createOptionView($option, $productVariant, $customer, $currency, $locale),
// iterator_to_array($optionManager->getOptions($productVariant, $customer))
// );
$currencyView = $this->container->get(CurrencyPresenter::class)->createCurrencyView($currency, $locale);
$catalogPrice = $this->container->get(CatalogPriceCalculator::class)->calculateCatalogPrice(new CatalogPriceQuery($productVariant, $customer, $currency));
$basePrice = $catalogPrice->getUnitPrice();
$totalPrice = $catalogPrice->getTotalPrice();
$modifierViews = [];
foreach ($catalogPrice->getModifiers() as $modifier)
{
if ($this->container->get(PriceModifierManager::class)->displayAsSurcharge($modifier->getType(), $customer))
{
$modifierViews[] = new PriceModifierView($modifier->getType(), '', null, $modifier->getAmount());
}
if (!$this->container->get(PriceModifierManager::class)->includeInItemPrice($modifier->getType(), $customer))
{
$totalPrice -= $modifier->getAmount();
}
}
$inStock = $stockManager->isQuantityAvailable($productVariant, 1, null, $customer);
$view = new CatalogProductVariantView(
$this->createProductVariantView($productVariant, $locale),
$currencyView,
$basePrice,
$totalPrice,
$modifierViews,
$optionViews,
$inStock
);
return array_reduce(
iterator_to_array($this->catalogProductVariantViewProcessors),
static fn($catalogProductVariantViewProcessor) => $catalogProductVariantViewProcessor->processCatalogProductVariantView($view, $productVariant, $customer, $locale),
$view
);
}
}