<?php
namespace App\Controller\Back\_Setting\Shop;
use App\Constants\CataloguePriceRange;
use App\Constants\Setting as SettingConst;
use App\Entity\Catalogue;
use App\Entity\Setting;
use App\Form\Type\SettingType;
use App\Services\CacheService;
use App\Services\Common\SettingService;
use App\Services\DTV\YamlConfig\YamlReader;
use App\Services\ProductService;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use JsonException;
use Psr\Cache\InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class CatalogController extends AbstractController
{
private EntityManagerInterface $em;
private HttpClientInterface $client;
private CacheService $cacheService;
private SettingService $settingService;
private YamlReader $yamlReader;
private ProductService $productService;
public function __construct(
EntityManagerInterface $em,
HttpClientInterface $client,
CacheService $cacheService,
SettingService $settingService,
YamlReader $yamlReader,
ProductService $productService
) {
$this->em = $em;
$this->client = $client;
$this->cacheService = $cacheService;
$this->settingService = $settingService;
$this->yamlReader = $yamlReader;
$this->productService = $productService;
}
/**
* Permet la création d'un catalogue au format json
* Une clé relationnelle est associé à notre entité Catalogue
* Les campagnes auparavant configurables dans le fichier domain.loc sont maintenant configurable dans la base
* de données Les includes et excludes sont insérés en json Si la clé SHOP_CATALOG n'existe pas, on la crée
* automatiquement Plusieurs catalogues peuvent être créés grâce à des prototypes
*
* @param Request $request
*
* @return Response
*
* @throws Exception
*/
public function edit(Request $request): Response
{
$catalogueSetting = $this->settingService->getSettingFromName(SettingConst::SHOP_CATALOG);
$pointRate = (float)$this->yamlReader->getPoint()->getRate();
$categories = [];
$prices = [];
$arrayHideFilters = [];
$arraySelectedHideFilters = [];
$arrayIncludedElements = [];
$arraySpecificExclusion = [];
if (!$catalogueSetting) {
$catalogue = (new Catalogue());
$catalogueSetting = (new Setting())
->setName(SettingConst::SHOP_CATALOG)
->setValue(SettingConst::FIELDTYPE_RELATIONAL)
->setDescription('Catalogue')
->setFieldType(SettingConst::FIELDTYPE_RELATIONAL)
->addCatalogue($catalogue);
} else {
$catalogues = $catalogueSetting->getCatalogues();
$subDomain = $this->yamlReader->getSubDomain();
/** @var Catalogue $catalogue */
foreach ($catalogues as $catalogue) {
$campaigns = $catalogue->getCampaigns();
$types = $catalogue->getTypes();
$filters = $catalogue->getFilters();
$sorts = $catalogue->getSorts() ?? "[]";
$includes = $catalogue->getInclude();
$fees = $catalogue->getFees();
$hideFilters = $catalogue->getHideFilters() ?? "[]";
$specificExclusions = $catalogue->getSpecificExclusion() ?? "[]";
try {
$specificExclusions = json_decode($specificExclusions, true, 512, JSON_THROW_ON_ERROR);
$catalogue->setCampaigns(json_decode($campaigns, true, 512, JSON_THROW_ON_ERROR));
$catalogue->setTypes(json_decode($types, true, 512, JSON_THROW_ON_ERROR));
$catalogue->setFilters(json_decode($filters, true, 512, JSON_THROW_ON_ERROR));
$catalogue->setSorts(json_decode($sorts, true, 512, JSON_THROW_ON_ERROR));
$catalogue->setInclude(json_decode($includes, true, 512, JSON_THROW_ON_ERROR));
$catalogue->setHideFilters(json_decode($hideFilters, true, 512, JSON_THROW_ON_ERROR));
$catalogue->setFees($fees !== null ? json_decode($fees, true, 512, JSON_THROW_ON_ERROR) : null);
$slug = $catalogue->getSlug();
$jsonCatalogue = $this->getParameter(
'kernel.project_dir'
) . '/data/catalogue/' . $subDomain . '/' . $slug . '.json';
if (file_exists($jsonCatalogue)) {
$json = file_get_contents($jsonCatalogue);
$catalog = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$categories[$slug] = $catalog['categories'] ?? [];
// trie des catégories ASC
usort($categories[$slug], function ($a, $b) {
return strcmp($a['title'], $b['title']);
});
$prices[$slug]['min'] = CataloguePriceRange::MIN_EUR;
$prices[$slug]['max'] = CataloguePriceRange::MAX_EUR;
$prices[$slug]['minSet'] = CataloguePriceRange::MIN_EUR;
$prices[$slug]['maxSet'] = CataloguePriceRange::MAX_EUR;
$prices[$slug]['catalogMinEur'] = isset($catalog['minPriceEur']) && is_numeric(
$catalog['minPriceEur']
) ? round((float)$catalog['minPriceEur'], 2) : null;
$prices[$slug]['catalogMaxEur'] = isset($catalog['maxPriceEur']) && is_numeric(
$catalog['maxPriceEur']
) ? round((float)$catalog['maxPriceEur'], 2) : null;
if (isset($catalogue->getInclude()['prices']['min']) && isset(
$catalogue->getInclude()['prices']['max']
)) {
$minSet = (float)$catalogue->getInclude()['prices']['min'];
$maxSet = (float)$catalogue->getInclude()['prices']['max'];
$prices[$slug]['minSet'] = max(
CataloguePriceRange::MIN_EUR,
min(CataloguePriceRange::MAX_EUR, $minSet)
);
$prices[$slug]['maxSet'] = max(
CataloguePriceRange::MIN_EUR,
min(CataloguePriceRange::MAX_EUR, $maxSet)
);
}
//extraction des propriétés des produits pour masquer des filtres
$arrayHideFilters[$slug] = $this->extractFiltersToHide($catalog);
$arraySelectedHideFilters[$slug] = $catalogue->getHideFilters() ?? [];
$arrayIncludedElements[$slug] = $catalogue->getInclude() ?? [
'categories' => [],
'prices' => [],
];
$arraySpecificExclusion[$slug] = $specificExclusions;
}
} catch (JsonException $e) {
$this->addFlash(
'danger',
'Une erreur est survenue lors de la récupération des données du catalogue ' . $catalogue->getLabel(
)
);
}
}
}
//TODO On a pas vraiment besoin de passer par le SettingType ici, il faudra modifier ça plus tard
$form = $this->createForm(SettingType::class, $catalogueSetting, [
'campaigns' => $this->getCampaigns(),
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$catalogues = $catalogueSetting->getCatalogues();
foreach ($catalogues as $catalogue) {
$campaigns = $catalogue->getCampaigns();
$catalogue->setCampaigns(json_encode($campaigns, JSON_THROW_ON_ERROR));
$types = $catalogue->getTypes();
$catalogue->setTypes(json_encode($types, JSON_THROW_ON_ERROR));
$filters = $catalogue->getFilters();
$catalogue->setFilters(json_encode($filters, JSON_THROW_ON_ERROR));
$sorts = $catalogue->getSorts();
$catalogue->setSorts(json_encode($sorts, JSON_THROW_ON_ERROR));
$includedCategories = $request->request->get('catalogues')[$catalogue->getSlug()]['categories'] ?? [];
$catalogues = $request->request->get('catalogues', []);
$slug = $catalogue->getSlug();
$includedPrices['min'] = isset($catalogues[$slug]['minPrice']) ? (float)$catalogues[$slug]['minPrice'] : null;
$includedPrices['max'] = isset($catalogues[$slug]['maxPrice']) ? (float)$catalogues[$slug]['maxPrice'] : null;
if ($includedPrices['min'] !== null) {
$includedPrices['min'] = max(
CataloguePriceRange::MIN_EUR,
min(CataloguePriceRange::MAX_EUR, $includedPrices['min'])
);
}
if ($includedPrices['max'] !== null) {
$includedPrices['max'] = max(
CataloguePriceRange::MIN_EUR,
min(CataloguePriceRange::MAX_EUR, $includedPrices['max'])
);
}
$hideFilters = $request->request->get('catalogues')[$catalogue->getSlug()]['hide_filters'] ?? [];
$specificExclusion = isset(
$request->request->get('catalogues')[$catalogue->getSlug()]
) && ($request->request->get('catalogues')[$catalogue->getSlug(
)]['specificExclusion']['skus'][0] !== "" || isset(
$request->request->get(
'catalogues'
)[$catalogue->getSlug()]['specificExclusion']['attributs']
)) ? $request->request->get('catalogues')[$catalogue->getSlug()]['specificExclusion'] : [];
$hideFiltersData = [];
foreach ($hideFilters as $key => $hideFilter) {
if (!is_array($hideFilter)) {
$hideFiltersData[$hideFilter] = [];
} else {
$hideFiltersData[$key] = $hideFilter;
}
}
$catalogue->setHideFilters(json_encode($hideFiltersData, JSON_THROW_ON_ERROR));
$catalogue->setSpecificExclusion(json_encode($specificExclusion, JSON_THROW_ON_ERROR));
$catalogue->setFees($this->registerFees($request, $catalogue->getSlug()));
$includeData = [
'categories' => $includedCategories,
'prices' => $includedPrices,
];
$catalogue->setInclude(json_encode($includeData, JSON_THROW_ON_ERROR));
$this->em->persist($catalogue);
}
// FIX parce que le SettingType force une valeur qui n'est pas la bonne
$catalogueSetting->setFieldType(SettingConst::FIELDTYPE_RELATIONAL);
$this->em->persist($catalogueSetting);
$this->em->flush();
$this->addFlash('success', 'Les paramètres ont bien été enregistrés.');
return $this->redirectToRoute('back_setting_shop_catalog_edit');
}
$formView = $form->createView();
// The form needs decoded arrays, but Doctrine must keep text-mapped fields as strings.
$this->restoreCatalogueJsonFields($catalogueSetting->getCatalogues());
return $this->render('back/_setting/shop/catalog/edit.html.twig', [
'form' => $formView,
'prices' => $prices,
'priceRangeMin' => CataloguePriceRange::MIN_EUR,
'priceRangeMax' => CataloguePriceRange::MAX_EUR,
'pointRate' => $pointRate,
'categories' => $categories,
'arrayHideFilters' => $arrayHideFilters,
'arraySelectedHideFilters' => $arraySelectedHideFilters,
'arrayIncludedElements' => $arrayIncludedElements,
'arraySpecificExclusion' => $arraySpecificExclusion,
]);
}
/**
* @param array $catalog
*
* @return array
*/
public function extractFiltersToHide(array $catalog): array
{
$arrayHideFilters = [];
// extract declinaisons
foreach ($catalog['products'] as $product) {
if (isset($product['declinations']) && !empty($product['declinations'] && $product['status'])) {
foreach ($product['declinations'] as $declination) {
$arrayHideFilters['declinations'][$declination['name']][] = $declination['value'];
$arrayHideFilters['declinations'][$declination['name']] = array_unique(
$arrayHideFilters['declinations'][$declination['name']]
);
}
}
}
// extract catégories
if (isset($catalog['categories'])) {
foreach ($catalog['categories'] as $categorie) {
$arrayHideFilters['categories'][] = $categorie;
}
}
// extract localisation
foreach ($catalog['products'] as $product) {
if (isset($product['geo_zones']) && !empty($product['geo_zones'] && $product['status'])) {
foreach ($product['geo_zones'] as $geoZone) {
if ($geoZone['zone1'] !== "") {
$arrayHideFilters['locations'][$geoZone['zone1']][] = $geoZone['name'];
$arrayHideFilters['locations'][$geoZone['zone1']] = array_unique(
$arrayHideFilters['locations'][$geoZone['zone1']]
);
}
}
}
}
if (!empty($arrayHideFilters['categories'])) {
// trie des catégories ASC
usort($arrayHideFilters['categories'], function ($a, $b) {
return strcmp($a['title'], $b['title']);
});
}
return $arrayHideFilters;
}
/**
* Retourne les fees à enregistrer dans l'entité
* A l'heure actuelle, ce n'est disponible que pour les catalogues de type gift_certificate
*
* @param $request
* @param $slug
*
* @return string|null
* @throws JsonException
*/
private function registerFees($request, $slug): ?string
{
$formData = $request->request->get('setting')['catalogues'];
$catalogueData = array_filter($formData, static function ($c) use ($slug) {
return $c['slug'] === $slug;
});
$catalogueData = reset($catalogueData);
$fees = $catalogueData['fees'] ?? null;
// on s'assure d'avoir une valeur numérique
$convertValue = static function ($item) {
$item['value'] = (float)str_replace(',', '.', $item['value']);
return $item;
};
if (!isset($catalogueData['types'])) {
return null;
}
if (in_array('gift_certificate', $catalogueData['types'], true)) {
$fees = array_map($convertValue, $fees);
}
return in_array('gift_certificate', $catalogueData['types'], true) ? json_encode(
$fees,
JSON_THROW_ON_ERROR
) : null;
}
/**
* Re-encode text-mapped JSON fields after the form view is built, so any later flush
* cannot persist PHP arrays as the string "Array".
*
* @param iterable<Catalogue> $catalogues
*
* @return void
* @throws JsonException
*/
private function restoreCatalogueJsonFields(iterable $catalogues): void
{
foreach ($catalogues as $catalogue) {
$catalogue->setCampaigns($this->encodeJsonField($catalogue->getCampaigns(), '[]'));
$catalogue->setTypes($this->encodeJsonField($catalogue->getTypes(), '[]'));
$catalogue->setFilters($this->encodeJsonField($catalogue->getFilters(), '[]'));
$catalogue->setSorts($this->encodeJsonField($catalogue->getSorts(), '[]'));
$catalogue->setInclude($this->encodeJsonField($catalogue->getInclude(), '[]'));
$catalogue->setHideFilters($this->encodeJsonField($catalogue->getHideFilters(), '[]'));
$catalogue->setSpecificExclusion($this->encodeJsonField($catalogue->getSpecificExclusion(), '[]'));
$catalogue->setFees($this->encodeJsonField($catalogue->getFees(), null));
}
}
/**
* @param mixed $value
* @param string|null $default
*
* @return string|null
* @throws JsonException
*/
private function encodeJsonField($value, ?string $default): ?string
{
if ($value === null) {
return $default;
}
if (is_array($value)) {
return json_encode($value, JSON_THROW_ON_ERROR);
}
return is_string($value) ? $value : $default;
}
/**
* @return array
*/
private function getCampaigns(): array
{
$baseUrl = 'http://bo.37deux.com/api-bo2/';
/**
* @TODO : Ce code est-il toujours utile ?
*/ // $dataCampagnes = $this->client->request(
// 'GET',
// $baseUrl . 'campagnes?limit=1000000&group=dtv-campagne',
// [
// 'headers' => [
// 'Content-Type' => 'application/json',
// 'private-token' => md5('ggpojiuq'),
// ],
// ]
// );
return $this->cacheService->cacheMethodResult('getCampaigns', [], function () use ($baseUrl) {
$campaigns = [];
$dataCampagnes = $this->client->request(
'GET',
$baseUrl . 'campagnes?limit=1000000&group=dtv-campagne',
[
'headers' => [
'Content-Type' => 'application/json',
'private-token' => md5('ggpojiuq'),
],
]
);
try {
$response = $dataCampagnes->getContent();
$clientResponse = json_decode($response, true);
foreach ($clientResponse as $campaign) {
$campaigns[$campaign['id'] . ' : ' . $campaign['name']] = $campaign['id'];
}
} catch (Exception $exception) {
$this->addFlash(
'danger',
"Une erreur a été rencontrée lors de la tentative de connexion au bo : " . $exception->getMessage()
);
}
return $campaigns;
}, false, 3600, 'campaigns');
}
/**
* Clear le cache des campagnes
*
* @return RedirectResponse
* @throws InvalidArgumentException
*/
public function clearCache(): RedirectResponse
{
$this->cacheService->invalidateTags(['campaigns']);
return $this->redirectToRoute('back_setting_shop_catalog_edit');
}
/**
* Returns the detail of a catalogue in JSON format.
*
* @param Request $request
*
* @return JsonResponse The JSON response containing the catalogue URL.
*
* @throws JsonException
*/
public function ajaxDetail(Request $request): JsonResponse
{
$slug = $request->request->get('catalogue');
$jsonCatalogue = $this->getParameter(
'kernel.project_dir'
) . '/data/catalogue/' . $this->yamlReader->getSubdomain() . '/' . $slug . '.json';
if (file_exists($jsonCatalogue)) {
$total_items = 0;
$response = json_decode(file_get_contents($jsonCatalogue), true, 512, JSON_THROW_ON_ERROR);
$header = '<tr><th>Id</th><th>Sku</th><th>Nom</th><th>Type</th><th>Stock</th></tr>';
$items = [];
foreach ($response['products'] as $product) {
$dataSku = [$product['sku']];
if (count($product['declinations']) > 1) {
$stock = $product['totalstock'] ?? 0;
$type = '<strong>Conteneur de déclinaisons :</strong><br>';
foreach ($product['declinations'] as $declination) {
$declinationStock = $this->productService->getRealStockProductBySku($declination['sku']);
$total_items++;
$type .= '- ' . $declination['sku'] . ' : ' . $declination['reference'] . ' × ' . $declinationStock . '<br>';
$dataSku[] = $declination['sku'];
}
if ($stock > 0) {
$color = '';
} else {
$color = ' text-danger ';
}
$items[] = '<tr class="catalog-item' . $color . '" data-sku="' . implode(
',',
$dataSku
) . '">' . '<td>' . $product['id'] . '</td>' . '<td>' . $product['sku'] . '</td>' . '<td>' . $product['name'] . '</td>' . '<td>' . $type . '</td>' . '<td>' . $stock . '<td>' . '</tr>';
} elseif (empty($product['product_bundle'])) {
$type = 'article';
$stock = $this->productService->getRealStockProductBySku($product['sku']) ?? 0;
$total_items++;
if ($stock > 0) {
$color = '';
} else {
$color = ' text-danger ';
}
$items[] = '<tr class="catalog-item' . $color . '" data-sku="' . implode(
',',
$dataSku
) . '">' . '<td>' . $product['id'] . '</td>' . '<td>' . $product['sku'] . '</td>' . '<td>' . $product['name'] . '</td>' . '<td>' . $type . '</td>' . '<td>' . $stock . '</td>' . '</tr>';
} else {
$type = 'Bundle<br>';
$total_items++;
$stock = $this->productService->getRealStockProductBySku($product['sku'], $slug);
if ($stock > 0) {
$color = '';
} else {
$color = ' text-danger ';
}
foreach ($product['product_bundle'] as $productBundle) {
$total_items++;
$type .= '- ' . $productBundle['sku'] . ', ' . $productBundle['name'] . ' (× ' . $productBundle['quantity_in_bundle'] . ') : ' . $productBundle['stock'] . ' en stock<br>';
$dataSku[] = $productBundle['sku'];
}
$items[] = '<tr class="catalog-item' . $color . '" data-sku="' . implode(
',',
$dataSku
) . '">' . '<td>' . $product['id'] . '</td>' . '<td>' . $product['sku'] . '</td>' . '<td>' . $product['name'] . '</td>' . '<td>' . $type . '</td>' . '<td>' . $stock . '<td>' . '</tr>';
}
}
return new JsonResponse([
'catalogue' => $slug,
'catalogueUrl' => $jsonCatalogue,
'items' => $header . implode('', $items),
'total_hits' => $total_items,
'success' => true,
]);
}
return new JsonResponse([
'catalogueUrl' => $jsonCatalogue,
'success' => false,
'message' => 'Le fichier json n\'existe pas.',
]);
}
/**
* Mise a jour de l'organisation des catalogues.
**
*
* @param Request $request
*
* @return JsonResponse
*/
public function orderingUpdate(Request $request): JsonResponse
{
$ordering = $request->get('ordering');
if (!is_array($ordering)) {
return new JsonResponse(['success' => false, 'error' => 'Données invalides.'], 400);
}
foreach ($ordering as $position => $catalogueId) {
if (!is_numeric($catalogueId)) {
continue;
}
$catalogue = $this->em->getRepository(Catalogue::class)->findOneBy(['id' => $catalogueId]);
if ($catalogue) {
$catalogue->setOrderItem($position + 1);
$this->em->persist($catalogue);
}
}
$this->em->flush();
return new JsonResponse(['success' => true]);
}
}