<?php
namespace Plugin\VariationSwatches42;
use Eccube\Entity\Product;
use Eccube\Event\TemplateEvent;
use Knp\Component\Pager\Pagination\PaginationInterface;
use Plugin\VariationSwatches42\Entity\ProductClassImageIndex;
use Plugin\VariationSwatches42\Entity\VariationSwatchClassConfig;
use Plugin\VariationSwatches42\Repository\ProductClassImageIndexRepository;
use Plugin\VariationSwatches42\Service\VariationSwatchService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\Persistence\ManagerRegistry;
class Event implements EventSubscriberInterface
{
/**
* @var VariationSwatchService
*/
private $variationSwatchService;
/**
* @var ManagerRegistry
*/
private $registry;
/**
* Event constructor.
*
* @param VariationSwatchService $variationSwatchService
* @param ManagerRegistry $registry
*/
public function __construct(
VariationSwatchService $variationSwatchService,
ManagerRegistry $registry
) {
$this->variationSwatchService = $variationSwatchService;
$this->registry = $registry;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
'default_frame.twig' => 'includeAssets',
'Product/detail.twig' => 'replaceClassCategoryWidget',
];
}
/**
* Include assets for variation swatches
*
* @param TemplateEvent $event
*/
public function includeAssets(TemplateEvent $event)
{
if (!$this->variationSwatchService->isEnabled()) {
return;
}
$parameters = $event->getParameters();
$classConfigs = $this->variationSwatchService->getClassConfigs();
// Build the main config object passed to JS
$config = $this->buildJsConfig($parameters, $classConfigs);
$parameters['configScript'] = sprintf(
'<script>window.variationSwatchesConfig = %s;</script>',
json_encode($config, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP)
);
// Build product-specific class name map for list pages
if (isset($parameters['pagination']) && $parameters['pagination'] instanceof PaginationInterface) {
$prodMap = $this->buildProductClassMap($parameters['pagination'], $classConfigs);
$parameters['productsScript'] = sprintf(
'<script>window.variationSwatchesProductCategories = %s;</script>',
json_encode(
$prodMap,
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
}
$event->setParameters($parameters);
$event->addAsset('@VariationSwatches42/assets.twig');
}
/**
* Builds the main configuration object for javascript.
*/
private function buildJsConfig(array $parameters, array $classConfigs)
{
$config = ['enabled' => true];
$classMap = [];
foreach ($classConfigs as $cid => $cfg) {
$classMap[$cid] = $this->convertConfigEntityToArray($cfg);
}
// On product detail page, narrow the map to only used classes
if (isset($parameters['Product']) && $parameters['Product'] instanceof Product) {
$product = $parameters['Product'];
$usedIds = [];
$idToName = [];
foreach ($product->getProductClasses() as $pc) {
if ($pc->getClassCategory1()) {
$id = $pc->getClassCategory1()->getClassName()->getId();
$usedIds[$id] = true;
$idToName[$id] = $pc->getClassCategory1()->getClassName()->getName();
}
if ($pc->getClassCategory2()) {
$id = $pc->getClassCategory2()->getClassName()->getId();
$usedIds[$id] = true;
$idToName[$id] = $pc->getClassCategory2()->getClassName()->getName();
}
}
if ($usedIds) {
$classMap = array_intersect_key($classMap, $usedIds);
$missing = array_diff_key($usedIds, $classMap);
foreach (array_keys($missing) as $mid) {
$classMap[$mid] = $this->getDefaultConfigArray($mid, $idToName[$mid] ?? ('Class '.$mid));
}
}
// Add media index mapping for the product
$config['mediaIndexMap'] = $this->buildMediaIndexMap($product);
// Add image URL mapping for product list
$config['imageUrlMap'] = $this->buildImageUrlMap($product);
}
$config['classConfigs'] = $classMap;
return $config;
}
/**
* Helper to get legacy config format.
*/
private function getCompatConfig(?int $cid, array $classConfigs)
{
if ($cid === null || !isset($classConfigs[$cid])) {
return [null, 'label', []];
}
$cfg = $classConfigs[$cid];
return [$cfg->getClassName()->getName(), $cfg->getDisplayType(), $cfg->getValuesDecoded()];
}
/**
* Helper to get appearance array.
*/
private function getAppearanceConfig($cfg)
{
return [
'shape' => $cfg->getShape(),
'size' => $cfg->getSize(),
'gap' => $cfg->getGap(),
'orientation' => $cfg->getOrientation(),
'showTooltip' => $cfg->isShowTooltip(),
'showLabel' => $cfg->isShowLabel(),
];
}
/**
* Builds product => class name map for list pages.
*/
private function buildProductClassMap($pagination, array $classConfigs)
{
$prodMap = [];
foreach ($pagination as $prod) {
if (!$prod instanceof Product) {
continue;
}
$pid = (string)$prod->getId();
$prodMap[$pid] = [];
// Map ClassName ID (e.g. size, color) to the full ClassConfig object
$classIdToConfig = [];
foreach ($prod->getProductClasses() as $pc) {
// ClassCategory1
if ($cc1 = $pc->getClassCategory1()) {
$cid = (string)$cc1->getClassName()->getId();
if (!isset($classIdToConfig[$cid])) {
$cn = $cc1->getClassName()->getName();
$classIdToConfig[$cid] = $this->findClassNameEntity($cn, $classConfigs);
}
}
// ClassCategory2
if ($cc2 = $pc->getClassCategory2()) {
$cid = (string)$cc2->getClassName()->getId();
if (!isset($classIdToConfig[$cid])) {
$cn = $cc2->getClassName()->getName();
$classIdToConfig[$cid] = $this->findClassNameEntity($cn, $classConfigs);
}
}
}
// Determine primary class for position 1 and 2
$pos1Cfg = $pos2Cfg = null;
foreach ($prod->getProductClasses() as $pc) {
if (!$pos1Cfg && $pc->getClassCategory1()) {
$cn = $pc->getClassCategory1()->getClassName()->getName();
$pos1Cfg = $this->findClassNameEntity($cn, $classConfigs);
}
if (!$pos2Cfg && $pc->getClassCategory2()) {
$cn = $pc->getClassCategory2()->getClassName()->getName();
$pos2Cfg = $this->findClassNameEntity($cn, $classConfigs);
}
if ($pos1Cfg && $pos2Cfg) {
break;
}
}
if ($pos1Cfg) {
$prodMap[$pid]['1'] = $this->getAppearanceConfig($pos1Cfg) + [
'id' => $pos1Cfg->getClassName()->getId(),
'name' => $pos1Cfg->getClassName()->getName(),
'displayType' => $pos1Cfg->getDisplayType(),
'values' => $pos1Cfg->getValuesDecoded(),
];
} elseif (
isset($prod->getProductClasses()[0]) &&
$prod->getProductClasses()[0]->getClassCategory1()
) {
$cn = $prod->getProductClasses()[0]->getClassCategory1()->getClassName();
$prodMap[$pid]['1'] = $this->getDefaultConfigArray($cn->getId(), $cn->getName());
}
if ($pos2Cfg) {
$prodMap[$pid]['2'] = $this->getAppearanceConfig($pos2Cfg) + [
'id' => $pos2Cfg->getClassName()->getId(),
'name' => $pos2Cfg->getClassName()->getName(),
'displayType' => $pos2Cfg->getDisplayType(),
'values' => $pos2Cfg->getValuesDecoded(),
];
} elseif (
isset($prod->getProductClasses()[0]) &&
$prod->getProductClasses()[0]->getClassCategory2()
) {
$cn = $prod->getProductClasses()[0]->getClassCategory2()->getClassName();
$prodMap[$pid]['2'] = $this->getDefaultConfigArray($cn->getId(), $cn->getName());
}
// Add image URL mapping for this product
$prodMap[$pid]['imageUrlMap'] = $this->buildImageUrlMap($prod);
}
return $prodMap;
}
/**
* Helper to find a ClassConfig entity by name.
* @return VariationSwatchClassConfig|null
*/
private function findClassNameEntity(string $name, array $classConfigs)
{
foreach ($classConfigs as $cfg) {
if ($cfg->getClassName()->getName() === $name) {
return $cfg;
}
}
return null;
}
/**
* @param $cfg
* @return array
*/
private function convertConfigEntityToArray($cfg)
{
return [
'id' => $cfg->getClassName()->getId(),
'name' => $cfg->getClassName()->getName(),
'displayType' => $cfg->getDisplayType(),
'shape' => $cfg->getShape(),
'size' => $cfg->getSize(),
'gap' => $cfg->getGap(),
'orientation' => $cfg->getOrientation(),
'showTooltip' => $cfg->isShowTooltip(),
'showLabel' => $cfg->isShowLabel(),
'values' => $cfg->getValuesDecoded(),
];
}
/**
* @param int $id
* @param string $name
* @return array
*/
private function getDefaultConfigArray(int $id, string $name)
{
return [
'id' => $id,
'name' => $name,
'displayType' => 'dropdown',
'shape' => 'squared',
'size' => 'small',
'gap' => 'small',
'orientation' => 'horizontal',
'showTooltip' => true,
'showLabel' => false,
'values' => [],
];
}
/**
* Build media index mapping for a product
*/
private function buildMediaIndexMap(Product $product)
{
$mediaIndexMap = [];
// Get the ProductClassImageIndex repository
$em = $this->registry->getManager();
/** @var ProductClassImageIndexRepository $imageIndexRepo */
$imageIndexRepo = $em->getRepository(ProductClassImageIndex::class);
// Get all image index configurations for this product
$imageIndexConfigs = $imageIndexRepo->findByProduct($product);
foreach ($imageIndexConfigs as $config) {
$comboKey = $config->getComboKey();
$imageIndex = $config->getImageIndex();
// Only add to map if image index is not null
if ($imageIndex !== null) {
$mediaIndexMap[$comboKey] = $imageIndex;
}
}
return $mediaIndexMap;
}
/**
* Build image URL mapping for a product
*/
private function buildImageUrlMap(Product $product)
{
$imageUrlMap = [];
// Get the ProductClassImageIndex repository
$em = $this->registry->getManager();
/** @var ProductClassImageIndexRepository $imageIndexRepo */
$imageIndexRepo = $em->getRepository(ProductClassImageIndex::class);
// Get all image index configurations for this product
$imageIndexConfigs = $imageIndexRepo->findByProduct($product);
foreach ($imageIndexConfigs as $config) {
$comboKey = $config->getComboKey();
$imageUrl = $config->getImageUrl();
// Only add to map if image URL is not null
if ($imageUrl !== null) {
$imageUrlMap[$comboKey] = $imageUrl;
}
}
return $imageUrlMap;
}
public function replaceClassCategoryWidget(TemplateEvent $event)
{
$source = $event->getSource();
$replaced = str_replace(
['{{ form_widget(form.classcategory_id1) }}', '{{ form_widget(form.classcategory_id2) }}'],
['{{ form_row(form.classcategory_id1) }}', '{{ form_row(form.classcategory_id2) }}'],
$source
);
$event->setSource($replaced);
}
}