vendor/boldr/shop-bundle/src/Presenter/Product/ProductPresenter.php line 84

Open in your IDE?
  1. <?php
  2. namespace Boldr\Shop\ShopBundle\Presenter\Product;
  3. use Boldr\Shop\ShopBundle\CurrencyFormatter;
  4. use Boldr\Shop\ShopBundle\Price\PriceModifierManager;
  5. use Boldr\Shop\ShopBundle\Price\Catalog\{ CatalogPriceQuery, CatalogPriceCalculator };
  6. use Boldr\Shop\ShopBundle\Option\OptionManager;
  7. use Boldr\Shop\ShopBundle\Presenter\Category\CategoryPresenter;
  8. use Boldr\Shop\ShopBundle\Presenter\Currency\CurrencyPresenter;
  9. use Boldr\Shop\ShopBundle\Presenter\Option\OptionPresenter;
  10. use Boldr\Shop\ShopBundle\Presenter\Price\PriceModifierView;
  11. use Boldr\Shop\ShopBundle\Entity\{ Attribute, Product, ProductTranslation, ProductVariant, ProductAttribute, ProductImage, Customer };
  12. use Boldr\Shop\ShopBundle\Stock\StockManagerInterface;
  13. use Boldr\Cms\CmsBundle\Content\{ Assets, ContentManager, MediaResizer };
  14. use Boldr\Cms\CmsBundle\Presenter\Attachment\AttachmentPresenter;
  15. use Symfony\Contracts\Service\ServiceSubscriberInterface;
  16. use Psr\Container\ContainerInterface;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. use Symfony\Contracts\Cache\TagAwareCacheInterface;
  19. class ProductPresenter implements ServiceSubscriberInterface
  20. {
  21. protected ContainerInterface $container;
  22. public static function getSubscribedServices(): array
  23. {
  24. return [
  25. CatalogPriceCalculator::class,
  26. OptionManager::class,
  27. MediaResizer::class,
  28. ContentManager::class,
  29. CurrencyFormatter::class,
  30. CategoryPresenter::class,
  31. CurrencyPresenter::class,
  32. OptionPresenter::class,
  33. PriceModifierManager::class,
  34. StockManagerInterface::class,
  35. EntityManagerInterface::class,
  36. AttachmentPresenter::class,
  37. TagAwareCacheInterface::class
  38. ];
  39. }
  40. // Processors
  41. private iterable $productViewProcessors;
  42. private iterable $productVariantViewProcessors;
  43. private iterable $catalogProductViewProcessors;
  44. private iterable $catalogProductVariantViewProcessors;
  45. private iterable $attributeViewProcessors;
  46. // Media
  47. private array $productMediaSizes;
  48. private string $resizedMediaFolder;
  49. // Cache
  50. private array $attributeViews = [];
  51. private array $productViews = [];
  52. private array $productVariantViews = [];
  53. public function __construct(
  54. ContainerInterface $container,
  55. iterable $productViewProcessors,
  56. iterable $productVariantViewProcessors,
  57. iterable $catalogProductViewProcessors,
  58. iterable $catalogProductVariantViewProcessors,
  59. iterable $attributeViewProcessors,
  60. string $resizedMediaFolder,
  61. array $productMediaSizes,
  62. ) {
  63. $this->container = $container;
  64. $this->resizedMediaFolder = $resizedMediaFolder;
  65. $this->productMediaSizes = $productMediaSizes;
  66. $this->attributeViewProcessors = $attributeViewProcessors;
  67. $this->productViewProcessors = $productViewProcessors;
  68. $this->productVariantViewProcessors = $productVariantViewProcessors;
  69. $this->catalogProductViewProcessors = $catalogProductViewProcessors;
  70. $this->catalogProductVariantViewProcessors = $catalogProductVariantViewProcessors;
  71. }
  72. public function createProductView(Product $product, string $locale): ProductView
  73. {
  74. $cacheKey = $product->getId().'_'.$locale;
  75. if (isset($this->productViews[$cacheKey]))
  76. return $this->productViews[$cacheKey];
  77. // Create ProductVariant views
  78. $productVariantViews = array_map(
  79. fn($productVariant) => $this->createProductVariantView($productVariant, $locale),
  80. $product->getVariants()->toArray()
  81. );
  82. // Create Attribute views
  83. $attributeViews = array_map(
  84. fn($attribute) => $this->createAttributeView($attribute, $locale),
  85. $this->getProductAttributes($product)
  86. );
  87. // Create category views
  88. $categoryPresenter = $this->container->get(CategoryPresenter::class);
  89. $categoryViews = array_map(
  90. fn($category) => $categoryPresenter->createCategoryView($category, $locale),
  91. $product->getCategories()->toArray()
  92. );
  93. $primaryImageView = $product->getPrimaryImageOptions() === null ? null : $this->createProductImageView($product->getPrimaryImageOptions(), $locale);
  94. $defaultVariantView = $this->createProductVariantView($product->getDefaultVariant(), $locale);
  95. $translation = $product->getTranslations()->get($locale);
  96. $view = new ProductView(
  97. $product->getId(),
  98. $translation->getName(),
  99. $this->container->get(ContentManager::class)->renderAsHtml($translation->getDescription(), new Assets),
  100. $productVariantViews,
  101. $defaultVariantView,
  102. $attributeViews,
  103. $categoryViews,
  104. $primaryImageView
  105. );
  106. // Let view processors process view to add their own data
  107. $view = array_reduce(
  108. iterator_to_array($this->productViewProcessors),
  109. static fn($view, $productViewProcessor) => $productViewProcessor->processProductView($view, $product, $locale),
  110. $view
  111. );
  112. $this->productViews[$cacheKey] = $view;
  113. return $view;
  114. }
  115. public function createProductImageView(ProductImage $productImage, string $locale): ProductImageView
  116. {
  117. $cacheKey = 'boldr_shop.product_image.'.$productImage->getId().'.'.$locale;
  118. return $this->container->get(TagAwareCacheInterface::class)->get($cacheKey, function($item) use ($productImage, $locale) {
  119. $item->tag('boldr_shop.product.'.$productImage->getProduct()->getId());
  120. $attachmentView = $this->container->get(AttachmentPresenter::class)->createAttachmentView($productImage->getImage(), $this->productMediaSizes, $locale);
  121. return new ProductImageView(
  122. $attachmentView,
  123. $productImage->getCover(),
  124. $productImage->getPositionX(),
  125. $productImage->getPositionY()
  126. );
  127. });
  128. }
  129. private $productAttributeCache = null;
  130. private $productVariantAttributeCache = null;
  131. private function initAttributeCache(): void
  132. {
  133. if ($this->productVariantAttributeCache === null)
  134. {
  135. $this->productAttributeCache = [];
  136. $this->productVariantAttributeCache = [];
  137. $productAttributeRepository = $this->container->get(EntityManagerInterface::class)->getRepository(ProductAttribute::class);
  138. foreach ($productAttributeRepository->findAll() as $productAttribute)
  139. {
  140. if ($productAttribute->getProductVariant() !== null)
  141. {
  142. $this->productVariantAttributeCache[$productAttribute->getProductVariant()->getId()][] = $productAttribute;
  143. }
  144. else
  145. {
  146. $this->productAttributeCache[$productAttribute->getProduct()->getId()][] = $productAttribute;
  147. }
  148. }
  149. }
  150. }
  151. private function getProductVariantAttributes(ProductVariant $productVariant): array
  152. {
  153. $this->initAttributeCache();
  154. return $this->productVariantAttributeCache[$productVariant->getId()] ?? [];
  155. }
  156. private function getProductAttributes(Product $product): array
  157. {
  158. $this->initAttributeCache();
  159. return $this->productAttributeCache[$product->getId()] ?? [];
  160. }
  161. public function createProductVariantView(ProductVariant $productVariant, string $locale): ProductVariantView
  162. {
  163. $cacheKey = $productVariant->getId().'_'.$locale;
  164. if (isset($this->productVariantViews[$cacheKey]))
  165. return $this->productVariantViews[$cacheKey];
  166. $name = $productVariant->getTranslations()->get($locale)->getName();
  167. if ($name === null || $name === '')
  168. {
  169. $name = $productVariant->getProduct()->getTranslations()->get($locale)->getName();
  170. }
  171. $description = null;
  172. // $description = $productVariant->getTranslations()->get($locale)->getDescription();
  173. if ($description === null)
  174. {
  175. $description = $productVariant->getProduct()->getTranslations()->get($locale)->getDescription();
  176. }
  177. $attributes = array_map(
  178. fn($attribute) => $this->createAttributeView($attribute, $locale),
  179. $this->getProductVariantAttributes($productVariant)
  180. );
  181. $view = new ProductVariantView(
  182. $productVariant->getId(),
  183. $name,
  184. $this->container->get(ContentManager::class)->renderAsHtml($description, new Assets),
  185. $attributes
  186. );
  187. // Let view processors process view to add their own data
  188. $view = array_reduce(
  189. iterator_to_array($this->productVariantViewProcessors),
  190. static fn($view, $productVariantViewProcessor) => $productVariantViewProcessor->processProductVariantView($view, $productVariant, $locale),
  191. $view
  192. );
  193. $this->productVariantViews[$cacheKey] = $view;
  194. return $view;
  195. }
  196. public function createAttributeView(Attribute $attribute, string $locale): AttributeView
  197. {
  198. $cacheKey = $attribute->getId().'_'.$locale;
  199. if (isset($this->attributeViews[$cacheKey]))
  200. return $this->attributeViews[$cacheKey];
  201. $view = new AttributeView(
  202. $attribute->getId(),
  203. $attribute->getTranslations()->get($locale)->getName()
  204. );
  205. $view = array_reduce(
  206. iterator_to_array($this->attributeViewProcessors),
  207. static fn($attributeViewProcessor) => $attributeViewProcessor->processAttributeView($view, $attribute, $locale),
  208. $view
  209. );
  210. $this->attributeViews[$cacheKey] = $view;
  211. return $view;
  212. }
  213. public function createCatalogProductView(Product $product, ?Customer $customer, string $currency, string $locale): CatalogProductView
  214. {
  215. $variants = array_map(
  216. fn($productVariant) => $this->createCatalogProductVariantView($productVariant, [], $customer, $currency, $locale),
  217. $product->getVariants()->toArray()
  218. );
  219. $view = new CatalogProductView(
  220. $this->createProductView($product, $locale),
  221. $variants
  222. );
  223. return array_reduce(
  224. iterator_to_array($this->catalogProductViewProcessors),
  225. static fn($catalogProductViewProcessor) => $catalogProductViewProcessor->processCatalogProductView($view, $product, $customer, $locale),
  226. $view
  227. );
  228. }
  229. public function createCatalogProductVariantView(ProductVariant $productVariant, array $options, ?Customer $customer, string $currency, string $locale): CatalogProductVariantView
  230. {
  231. $optionManager = $this->container->get(OptionManager::class);
  232. $optionPresenter = $this->container->get(OptionPresenter::class);
  233. $stockManager = $this->container->get(StockManagerInterface::class);
  234. $optionViews = [];
  235. // $optionViews = array_map(
  236. // fn($option) => $optionPresenter->createOptionView($option, $productVariant, $customer, $currency, $locale),
  237. // iterator_to_array($optionManager->getOptions($productVariant, $customer))
  238. // );
  239. $currencyView = $this->container->get(CurrencyPresenter::class)->createCurrencyView($currency, $locale);
  240. $catalogPrice = $this->container->get(CatalogPriceCalculator::class)->calculateCatalogPrice(new CatalogPriceQuery($productVariant, $customer, $currency));
  241. $basePrice = $catalogPrice->getUnitPrice();
  242. $totalPrice = $catalogPrice->getTotalPrice();
  243. $modifierViews = [];
  244. foreach ($catalogPrice->getModifiers() as $modifier)
  245. {
  246. if ($this->container->get(PriceModifierManager::class)->displayAsSurcharge($modifier->getType(), $customer))
  247. {
  248. $modifierViews[] = new PriceModifierView($modifier->getType(), '', null, $modifier->getAmount());
  249. }
  250. if (!$this->container->get(PriceModifierManager::class)->includeInItemPrice($modifier->getType(), $customer))
  251. {
  252. $totalPrice -= $modifier->getAmount();
  253. }
  254. }
  255. $inStock = $stockManager->isQuantityAvailable($productVariant, 1, null, $customer);
  256. $view = new CatalogProductVariantView(
  257. $this->createProductVariantView($productVariant, $locale),
  258. $currencyView,
  259. $basePrice,
  260. $totalPrice,
  261. $modifierViews,
  262. $optionViews,
  263. $inStock
  264. );
  265. return array_reduce(
  266. iterator_to_array($this->catalogProductVariantViewProcessors),
  267. static fn($catalogProductVariantViewProcessor) => $catalogProductVariantViewProcessor->processCatalogProductVariantView($view, $productVariant, $customer, $locale),
  268. $view
  269. );
  270. }
  271. }