src/Controller/Back/_Setting/Shop/CatalogController.php line 65

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Back\_Setting\Shop;
  3. use App\Constants\CataloguePriceRange;
  4. use App\Constants\Setting as SettingConst;
  5. use App\Entity\Catalogue;
  6. use App\Entity\Setting;
  7. use App\Form\Type\SettingType;
  8. use App\Services\CacheService;
  9. use App\Services\Common\SettingService;
  10. use App\Services\DTV\YamlConfig\YamlReader;
  11. use App\Services\ProductService;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Exception;
  14. use JsonException;
  15. use Psr\Cache\InvalidArgumentException;
  16. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  17. use Symfony\Component\HttpFoundation\JsonResponse;
  18. use Symfony\Component\HttpFoundation\RedirectResponse;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\HttpFoundation\Response;
  21. use Symfony\Contracts\HttpClient\HttpClientInterface;
  22. class CatalogController extends AbstractController
  23. {
  24. private EntityManagerInterface $em;
  25. private HttpClientInterface $client;
  26. private CacheService $cacheService;
  27. private SettingService $settingService;
  28. private YamlReader $yamlReader;
  29. private ProductService $productService;
  30. public function __construct(
  31. EntityManagerInterface $em,
  32. HttpClientInterface $client,
  33. CacheService $cacheService,
  34. SettingService $settingService,
  35. YamlReader $yamlReader,
  36. ProductService $productService
  37. ) {
  38. $this->em = $em;
  39. $this->client = $client;
  40. $this->cacheService = $cacheService;
  41. $this->settingService = $settingService;
  42. $this->yamlReader = $yamlReader;
  43. $this->productService = $productService;
  44. }
  45. /**
  46. * Permet la création d'un catalogue au format json
  47. * Une clé relationnelle est associé à notre entité Catalogue
  48. * Les campagnes auparavant configurables dans le fichier domain.loc sont maintenant configurable dans la base
  49. * 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
  50. * automatiquement Plusieurs catalogues peuvent être créés grâce à des prototypes
  51. *
  52. * @param Request $request
  53. *
  54. * @return Response
  55. *
  56. * @throws Exception
  57. */
  58. public function edit(Request $request): Response
  59. {
  60. $catalogueSetting = $this->settingService->getSettingFromName(SettingConst::SHOP_CATALOG);
  61. $pointRate = (float)$this->yamlReader->getPoint()->getRate();
  62. $categories = [];
  63. $prices = [];
  64. $arrayHideFilters = [];
  65. $arraySelectedHideFilters = [];
  66. $arrayIncludedElements = [];
  67. $arraySpecificExclusion = [];
  68. if (!$catalogueSetting) {
  69. $catalogue = (new Catalogue());
  70. $catalogueSetting = (new Setting())
  71. ->setName(SettingConst::SHOP_CATALOG)
  72. ->setValue(SettingConst::FIELDTYPE_RELATIONAL)
  73. ->setDescription('Catalogue')
  74. ->setFieldType(SettingConst::FIELDTYPE_RELATIONAL)
  75. ->addCatalogue($catalogue);
  76. } else {
  77. $catalogues = $catalogueSetting->getCatalogues();
  78. $subDomain = $this->yamlReader->getSubDomain();
  79. /** @var Catalogue $catalogue */
  80. foreach ($catalogues as $catalogue) {
  81. $campaigns = $catalogue->getCampaigns();
  82. $types = $catalogue->getTypes();
  83. $filters = $catalogue->getFilters();
  84. $sorts = $catalogue->getSorts() ?? "[]";
  85. $includes = $catalogue->getInclude();
  86. $fees = $catalogue->getFees();
  87. $hideFilters = $catalogue->getHideFilters() ?? "[]";
  88. $specificExclusions = $catalogue->getSpecificExclusion() ?? "[]";
  89. try {
  90. $specificExclusions = json_decode($specificExclusions, true, 512, JSON_THROW_ON_ERROR);
  91. $catalogue->setCampaigns(json_decode($campaigns, true, 512, JSON_THROW_ON_ERROR));
  92. $catalogue->setTypes(json_decode($types, true, 512, JSON_THROW_ON_ERROR));
  93. $catalogue->setFilters(json_decode($filters, true, 512, JSON_THROW_ON_ERROR));
  94. $catalogue->setSorts(json_decode($sorts, true, 512, JSON_THROW_ON_ERROR));
  95. $catalogue->setInclude(json_decode($includes, true, 512, JSON_THROW_ON_ERROR));
  96. $catalogue->setHideFilters(json_decode($hideFilters, true, 512, JSON_THROW_ON_ERROR));
  97. $catalogue->setFees($fees !== null ? json_decode($fees, true, 512, JSON_THROW_ON_ERROR) : null);
  98. $slug = $catalogue->getSlug();
  99. $jsonCatalogue = $this->getParameter(
  100. 'kernel.project_dir'
  101. ) . '/data/catalogue/' . $subDomain . '/' . $slug . '.json';
  102. if (file_exists($jsonCatalogue)) {
  103. $json = file_get_contents($jsonCatalogue);
  104. $catalog = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
  105. $categories[$slug] = $catalog['categories'] ?? [];
  106. // trie des catégories ASC
  107. usort($categories[$slug], function ($a, $b) {
  108. return strcmp($a['title'], $b['title']);
  109. });
  110. $prices[$slug]['min'] = CataloguePriceRange::MIN_EUR;
  111. $prices[$slug]['max'] = CataloguePriceRange::MAX_EUR;
  112. $prices[$slug]['minSet'] = CataloguePriceRange::MIN_EUR;
  113. $prices[$slug]['maxSet'] = CataloguePriceRange::MAX_EUR;
  114. $prices[$slug]['catalogMinEur'] = isset($catalog['minPriceEur']) && is_numeric(
  115. $catalog['minPriceEur']
  116. ) ? round((float)$catalog['minPriceEur'], 2) : null;
  117. $prices[$slug]['catalogMaxEur'] = isset($catalog['maxPriceEur']) && is_numeric(
  118. $catalog['maxPriceEur']
  119. ) ? round((float)$catalog['maxPriceEur'], 2) : null;
  120. if (isset($catalogue->getInclude()['prices']['min']) && isset(
  121. $catalogue->getInclude()['prices']['max']
  122. )) {
  123. $minSet = (float)$catalogue->getInclude()['prices']['min'];
  124. $maxSet = (float)$catalogue->getInclude()['prices']['max'];
  125. $prices[$slug]['minSet'] = max(
  126. CataloguePriceRange::MIN_EUR,
  127. min(CataloguePriceRange::MAX_EUR, $minSet)
  128. );
  129. $prices[$slug]['maxSet'] = max(
  130. CataloguePriceRange::MIN_EUR,
  131. min(CataloguePriceRange::MAX_EUR, $maxSet)
  132. );
  133. }
  134. //extraction des propriétés des produits pour masquer des filtres
  135. $arrayHideFilters[$slug] = $this->extractFiltersToHide($catalog);
  136. $arraySelectedHideFilters[$slug] = $catalogue->getHideFilters() ?? [];
  137. $arrayIncludedElements[$slug] = $catalogue->getInclude() ?? [
  138. 'categories' => [],
  139. 'prices' => [],
  140. ];
  141. $arraySpecificExclusion[$slug] = $specificExclusions;
  142. }
  143. } catch (JsonException $e) {
  144. $this->addFlash(
  145. 'danger',
  146. 'Une erreur est survenue lors de la récupération des données du catalogue ' . $catalogue->getLabel(
  147. )
  148. );
  149. }
  150. }
  151. }
  152. //TODO On a pas vraiment besoin de passer par le SettingType ici, il faudra modifier ça plus tard
  153. $form = $this->createForm(SettingType::class, $catalogueSetting, [
  154. 'campaigns' => $this->getCampaigns(),
  155. ]);
  156. $form->handleRequest($request);
  157. if ($form->isSubmitted() && $form->isValid()) {
  158. $catalogues = $catalogueSetting->getCatalogues();
  159. foreach ($catalogues as $catalogue) {
  160. $campaigns = $catalogue->getCampaigns();
  161. $catalogue->setCampaigns(json_encode($campaigns, JSON_THROW_ON_ERROR));
  162. $types = $catalogue->getTypes();
  163. $catalogue->setTypes(json_encode($types, JSON_THROW_ON_ERROR));
  164. $filters = $catalogue->getFilters();
  165. $catalogue->setFilters(json_encode($filters, JSON_THROW_ON_ERROR));
  166. $sorts = $catalogue->getSorts();
  167. $catalogue->setSorts(json_encode($sorts, JSON_THROW_ON_ERROR));
  168. $includedCategories = $request->request->get('catalogues')[$catalogue->getSlug()]['categories'] ?? [];
  169. $catalogues = $request->request->get('catalogues', []);
  170. $slug = $catalogue->getSlug();
  171. $includedPrices['min'] = isset($catalogues[$slug]['minPrice']) ? (float)$catalogues[$slug]['minPrice'] : null;
  172. $includedPrices['max'] = isset($catalogues[$slug]['maxPrice']) ? (float)$catalogues[$slug]['maxPrice'] : null;
  173. if ($includedPrices['min'] !== null) {
  174. $includedPrices['min'] = max(
  175. CataloguePriceRange::MIN_EUR,
  176. min(CataloguePriceRange::MAX_EUR, $includedPrices['min'])
  177. );
  178. }
  179. if ($includedPrices['max'] !== null) {
  180. $includedPrices['max'] = max(
  181. CataloguePriceRange::MIN_EUR,
  182. min(CataloguePriceRange::MAX_EUR, $includedPrices['max'])
  183. );
  184. }
  185. $hideFilters = $request->request->get('catalogues')[$catalogue->getSlug()]['hide_filters'] ?? [];
  186. $specificExclusion = isset(
  187. $request->request->get('catalogues')[$catalogue->getSlug()]
  188. ) && ($request->request->get('catalogues')[$catalogue->getSlug(
  189. )]['specificExclusion']['skus'][0] !== "" || isset(
  190. $request->request->get(
  191. 'catalogues'
  192. )[$catalogue->getSlug()]['specificExclusion']['attributs']
  193. )) ? $request->request->get('catalogues')[$catalogue->getSlug()]['specificExclusion'] : [];
  194. $hideFiltersData = [];
  195. foreach ($hideFilters as $key => $hideFilter) {
  196. if (!is_array($hideFilter)) {
  197. $hideFiltersData[$hideFilter] = [];
  198. } else {
  199. $hideFiltersData[$key] = $hideFilter;
  200. }
  201. }
  202. $catalogue->setHideFilters(json_encode($hideFiltersData, JSON_THROW_ON_ERROR));
  203. $catalogue->setSpecificExclusion(json_encode($specificExclusion, JSON_THROW_ON_ERROR));
  204. $catalogue->setFees($this->registerFees($request, $catalogue->getSlug()));
  205. $includeData = [
  206. 'categories' => $includedCategories,
  207. 'prices' => $includedPrices,
  208. ];
  209. $catalogue->setInclude(json_encode($includeData, JSON_THROW_ON_ERROR));
  210. $this->em->persist($catalogue);
  211. }
  212. // FIX parce que le SettingType force une valeur qui n'est pas la bonne
  213. $catalogueSetting->setFieldType(SettingConst::FIELDTYPE_RELATIONAL);
  214. $this->em->persist($catalogueSetting);
  215. $this->em->flush();
  216. $this->addFlash('success', 'Les paramètres ont bien été enregistrés.');
  217. return $this->redirectToRoute('back_setting_shop_catalog_edit');
  218. }
  219. $formView = $form->createView();
  220. // The form needs decoded arrays, but Doctrine must keep text-mapped fields as strings.
  221. $this->restoreCatalogueJsonFields($catalogueSetting->getCatalogues());
  222. return $this->render('back/_setting/shop/catalog/edit.html.twig', [
  223. 'form' => $formView,
  224. 'prices' => $prices,
  225. 'priceRangeMin' => CataloguePriceRange::MIN_EUR,
  226. 'priceRangeMax' => CataloguePriceRange::MAX_EUR,
  227. 'pointRate' => $pointRate,
  228. 'categories' => $categories,
  229. 'arrayHideFilters' => $arrayHideFilters,
  230. 'arraySelectedHideFilters' => $arraySelectedHideFilters,
  231. 'arrayIncludedElements' => $arrayIncludedElements,
  232. 'arraySpecificExclusion' => $arraySpecificExclusion,
  233. ]);
  234. }
  235. /**
  236. * @param array $catalog
  237. *
  238. * @return array
  239. */
  240. public function extractFiltersToHide(array $catalog): array
  241. {
  242. $arrayHideFilters = [];
  243. // extract declinaisons
  244. foreach ($catalog['products'] as $product) {
  245. if (isset($product['declinations']) && !empty($product['declinations'] && $product['status'])) {
  246. foreach ($product['declinations'] as $declination) {
  247. $arrayHideFilters['declinations'][$declination['name']][] = $declination['value'];
  248. $arrayHideFilters['declinations'][$declination['name']] = array_unique(
  249. $arrayHideFilters['declinations'][$declination['name']]
  250. );
  251. }
  252. }
  253. }
  254. // extract catégories
  255. if (isset($catalog['categories'])) {
  256. foreach ($catalog['categories'] as $categorie) {
  257. $arrayHideFilters['categories'][] = $categorie;
  258. }
  259. }
  260. // extract localisation
  261. foreach ($catalog['products'] as $product) {
  262. if (isset($product['geo_zones']) && !empty($product['geo_zones'] && $product['status'])) {
  263. foreach ($product['geo_zones'] as $geoZone) {
  264. if ($geoZone['zone1'] !== "") {
  265. $arrayHideFilters['locations'][$geoZone['zone1']][] = $geoZone['name'];
  266. $arrayHideFilters['locations'][$geoZone['zone1']] = array_unique(
  267. $arrayHideFilters['locations'][$geoZone['zone1']]
  268. );
  269. }
  270. }
  271. }
  272. }
  273. if (!empty($arrayHideFilters['categories'])) {
  274. // trie des catégories ASC
  275. usort($arrayHideFilters['categories'], function ($a, $b) {
  276. return strcmp($a['title'], $b['title']);
  277. });
  278. }
  279. return $arrayHideFilters;
  280. }
  281. /**
  282. * Retourne les fees à enregistrer dans l'entité
  283. * A l'heure actuelle, ce n'est disponible que pour les catalogues de type gift_certificate
  284. *
  285. * @param $request
  286. * @param $slug
  287. *
  288. * @return string|null
  289. * @throws JsonException
  290. */
  291. private function registerFees($request, $slug): ?string
  292. {
  293. $formData = $request->request->get('setting')['catalogues'];
  294. $catalogueData = array_filter($formData, static function ($c) use ($slug) {
  295. return $c['slug'] === $slug;
  296. });
  297. $catalogueData = reset($catalogueData);
  298. $fees = $catalogueData['fees'] ?? null;
  299. // on s'assure d'avoir une valeur numérique
  300. $convertValue = static function ($item) {
  301. $item['value'] = (float)str_replace(',', '.', $item['value']);
  302. return $item;
  303. };
  304. if (!isset($catalogueData['types'])) {
  305. return null;
  306. }
  307. if (in_array('gift_certificate', $catalogueData['types'], true)) {
  308. $fees = array_map($convertValue, $fees);
  309. }
  310. return in_array('gift_certificate', $catalogueData['types'], true) ? json_encode(
  311. $fees,
  312. JSON_THROW_ON_ERROR
  313. ) : null;
  314. }
  315. /**
  316. * Re-encode text-mapped JSON fields after the form view is built, so any later flush
  317. * cannot persist PHP arrays as the string "Array".
  318. *
  319. * @param iterable<Catalogue> $catalogues
  320. *
  321. * @return void
  322. * @throws JsonException
  323. */
  324. private function restoreCatalogueJsonFields(iterable $catalogues): void
  325. {
  326. foreach ($catalogues as $catalogue) {
  327. $catalogue->setCampaigns($this->encodeJsonField($catalogue->getCampaigns(), '[]'));
  328. $catalogue->setTypes($this->encodeJsonField($catalogue->getTypes(), '[]'));
  329. $catalogue->setFilters($this->encodeJsonField($catalogue->getFilters(), '[]'));
  330. $catalogue->setSorts($this->encodeJsonField($catalogue->getSorts(), '[]'));
  331. $catalogue->setInclude($this->encodeJsonField($catalogue->getInclude(), '[]'));
  332. $catalogue->setHideFilters($this->encodeJsonField($catalogue->getHideFilters(), '[]'));
  333. $catalogue->setSpecificExclusion($this->encodeJsonField($catalogue->getSpecificExclusion(), '[]'));
  334. $catalogue->setFees($this->encodeJsonField($catalogue->getFees(), null));
  335. }
  336. }
  337. /**
  338. * @param mixed $value
  339. * @param string|null $default
  340. *
  341. * @return string|null
  342. * @throws JsonException
  343. */
  344. private function encodeJsonField($value, ?string $default): ?string
  345. {
  346. if ($value === null) {
  347. return $default;
  348. }
  349. if (is_array($value)) {
  350. return json_encode($value, JSON_THROW_ON_ERROR);
  351. }
  352. return is_string($value) ? $value : $default;
  353. }
  354. /**
  355. * @return array
  356. */
  357. private function getCampaigns(): array
  358. {
  359. $baseUrl = 'http://bo.37deux.com/api-bo2/';
  360. /**
  361. * @TODO : Ce code est-il toujours utile ?
  362. */ // $dataCampagnes = $this->client->request(
  363. // 'GET',
  364. // $baseUrl . 'campagnes?limit=1000000&group=dtv-campagne',
  365. // [
  366. // 'headers' => [
  367. // 'Content-Type' => 'application/json',
  368. // 'private-token' => md5('ggpojiuq'),
  369. // ],
  370. // ]
  371. // );
  372. return $this->cacheService->cacheMethodResult('getCampaigns', [], function () use ($baseUrl) {
  373. $campaigns = [];
  374. $dataCampagnes = $this->client->request(
  375. 'GET',
  376. $baseUrl . 'campagnes?limit=1000000&group=dtv-campagne',
  377. [
  378. 'headers' => [
  379. 'Content-Type' => 'application/json',
  380. 'private-token' => md5('ggpojiuq'),
  381. ],
  382. ]
  383. );
  384. try {
  385. $response = $dataCampagnes->getContent();
  386. $clientResponse = json_decode($response, true);
  387. foreach ($clientResponse as $campaign) {
  388. $campaigns[$campaign['id'] . ' : ' . $campaign['name']] = $campaign['id'];
  389. }
  390. } catch (Exception $exception) {
  391. $this->addFlash(
  392. 'danger',
  393. "Une erreur a été rencontrée lors de la tentative de connexion au bo : " . $exception->getMessage()
  394. );
  395. }
  396. return $campaigns;
  397. }, false, 3600, 'campaigns');
  398. }
  399. /**
  400. * Clear le cache des campagnes
  401. *
  402. * @return RedirectResponse
  403. * @throws InvalidArgumentException
  404. */
  405. public function clearCache(): RedirectResponse
  406. {
  407. $this->cacheService->invalidateTags(['campaigns']);
  408. return $this->redirectToRoute('back_setting_shop_catalog_edit');
  409. }
  410. /**
  411. * Returns the detail of a catalogue in JSON format.
  412. *
  413. * @param Request $request
  414. *
  415. * @return JsonResponse The JSON response containing the catalogue URL.
  416. *
  417. * @throws JsonException
  418. */
  419. public function ajaxDetail(Request $request): JsonResponse
  420. {
  421. $slug = $request->request->get('catalogue');
  422. $jsonCatalogue = $this->getParameter(
  423. 'kernel.project_dir'
  424. ) . '/data/catalogue/' . $this->yamlReader->getSubdomain() . '/' . $slug . '.json';
  425. if (file_exists($jsonCatalogue)) {
  426. $total_items = 0;
  427. $response = json_decode(file_get_contents($jsonCatalogue), true, 512, JSON_THROW_ON_ERROR);
  428. $header = '<tr><th>Id</th><th>Sku</th><th>Nom</th><th>Type</th><th>Stock</th></tr>';
  429. $items = [];
  430. foreach ($response['products'] as $product) {
  431. $dataSku = [$product['sku']];
  432. if (count($product['declinations']) > 1) {
  433. $stock = $product['totalstock'] ?? 0;
  434. $type = '<strong>Conteneur de déclinaisons :</strong><br>';
  435. foreach ($product['declinations'] as $declination) {
  436. $declinationStock = $this->productService->getRealStockProductBySku($declination['sku']);
  437. $total_items++;
  438. $type .= '- ' . $declination['sku'] . ' : ' . $declination['reference'] . ' &times; ' . $declinationStock . '<br>';
  439. $dataSku[] = $declination['sku'];
  440. }
  441. if ($stock > 0) {
  442. $color = '';
  443. } else {
  444. $color = ' text-danger ';
  445. }
  446. $items[] = '<tr class="catalog-item' . $color . '" data-sku="' . implode(
  447. ',',
  448. $dataSku
  449. ) . '">' . '<td>' . $product['id'] . '</td>' . '<td>' . $product['sku'] . '</td>' . '<td>' . $product['name'] . '</td>' . '<td>' . $type . '</td>' . '<td>' . $stock . '<td>' . '</tr>';
  450. } elseif (empty($product['product_bundle'])) {
  451. $type = 'article';
  452. $stock = $this->productService->getRealStockProductBySku($product['sku']) ?? 0;
  453. $total_items++;
  454. if ($stock > 0) {
  455. $color = '';
  456. } else {
  457. $color = ' text-danger ';
  458. }
  459. $items[] = '<tr class="catalog-item' . $color . '" data-sku="' . implode(
  460. ',',
  461. $dataSku
  462. ) . '">' . '<td>' . $product['id'] . '</td>' . '<td>' . $product['sku'] . '</td>' . '<td>' . $product['name'] . '</td>' . '<td>' . $type . '</td>' . '<td>' . $stock . '</td>' . '</tr>';
  463. } else {
  464. $type = 'Bundle<br>';
  465. $total_items++;
  466. $stock = $this->productService->getRealStockProductBySku($product['sku'], $slug);
  467. if ($stock > 0) {
  468. $color = '';
  469. } else {
  470. $color = ' text-danger ';
  471. }
  472. foreach ($product['product_bundle'] as $productBundle) {
  473. $total_items++;
  474. $type .= '- ' . $productBundle['sku'] . ', ' . $productBundle['name'] . ' (&times; ' . $productBundle['quantity_in_bundle'] . ') : ' . $productBundle['stock'] . ' en stock<br>';
  475. $dataSku[] = $productBundle['sku'];
  476. }
  477. $items[] = '<tr class="catalog-item' . $color . '" data-sku="' . implode(
  478. ',',
  479. $dataSku
  480. ) . '">' . '<td>' . $product['id'] . '</td>' . '<td>' . $product['sku'] . '</td>' . '<td>' . $product['name'] . '</td>' . '<td>' . $type . '</td>' . '<td>' . $stock . '<td>' . '</tr>';
  481. }
  482. }
  483. return new JsonResponse([
  484. 'catalogue' => $slug,
  485. 'catalogueUrl' => $jsonCatalogue,
  486. 'items' => $header . implode('', $items),
  487. 'total_hits' => $total_items,
  488. 'success' => true,
  489. ]);
  490. }
  491. return new JsonResponse([
  492. 'catalogueUrl' => $jsonCatalogue,
  493. 'success' => false,
  494. 'message' => 'Le fichier json n\'existe pas.',
  495. ]);
  496. }
  497. /**
  498. * Mise a jour de l'organisation des catalogues.
  499. **
  500. *
  501. * @param Request $request
  502. *
  503. * @return JsonResponse
  504. */
  505. public function orderingUpdate(Request $request): JsonResponse
  506. {
  507. $ordering = $request->get('ordering');
  508. if (!is_array($ordering)) {
  509. return new JsonResponse(['success' => false, 'error' => 'Données invalides.'], 400);
  510. }
  511. foreach ($ordering as $position => $catalogueId) {
  512. if (!is_numeric($catalogueId)) {
  513. continue;
  514. }
  515. $catalogue = $this->em->getRepository(Catalogue::class)->findOneBy(['id' => $catalogueId]);
  516. if ($catalogue) {
  517. $catalogue->setOrderItem($position + 1);
  518. $this->em->persist($catalogue);
  519. }
  520. }
  521. $this->em->flush();
  522. return new JsonResponse(['success' => true]);
  523. }
  524. }