src/Controller/BaseProductController.php line 369

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\BaseProduct;
  4. use App\Entity\BaseProductSide;
  5. use App\Entity\BaseProductVariant;
  6. use App\Form\BaseProductAddType;
  7. use App\Form\BaseProductDefaultPropertiesType;
  8. use App\Form\BaseProductEditType;
  9. use App\Form\BaseProductVariantImageSettingsType;
  10. use App\Form\BaseProductVariantSettingsType;
  11. use App\Form\BaseProductVariantType;
  12. use App\Service\Amazon\AmazonAttributes;
  13. use App\Service\AmazonApiHelper;
  14. use App\Service\BaseProducts\Variants;
  15. use App\Service\BaseProductUploader;
  16. use App\Service\EtsyApiHelper;
  17. use App\Service\EtsyPropertyBaseProductService;
  18. use App\Service\EtsyPropertyService;
  19. use App\Service\S3;
  20. use App\Service\ShipStation\ShipStationApiClient;
  21. use App\Traits\ArrayTransformationTrait;
  22. use App\Traits\BaseProductTrait;
  23. use Doctrine\ORM\EntityManagerInterface;
  24. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  25. use Symfony\Component\Form\FormError;
  26. use Symfony\Component\Form\FormInterface;
  27. use Symfony\Component\HttpFoundation\JsonResponse;
  28. use Doctrine\ORM\Tools\Pagination\Paginator;
  29. use Symfony\Component\HttpFoundation\RedirectResponse;
  30. use Symfony\Component\HttpFoundation\Request;
  31. use Symfony\Component\HttpFoundation\Response;
  32. use Symfony\Component\Routing\Annotation\Route;
  33. #[Route('/admin/baseProduct')]
  34. class BaseProductController extends AbstractController
  35. {
  36. use ArrayTransformationTrait;
  37. use BaseProductTrait;
  38. public function __construct(
  39. private EntityManagerInterface $entityManager,
  40. private Variants $variantService,
  41. private S3 $s3,
  42. private EtsyApiHelper $etsyApiHelper,
  43. private EtsyPropertyService $etsyPropertyService,
  44. private EtsyPropertyBaseProductService $etsyPropertyBaseProductService,
  45. private ShipStationApiClient $shipStationApiClient,
  46. )
  47. {
  48. }
  49. #[Route('/list', name: 'base_product_list')]
  50. public function list(Request $request): Response
  51. {
  52. $pageSize = $request->query->getInt('pageSize', 10); // Number of items per page.
  53. $search = trim((string) $request->query->get('search', ''));
  54. $baseProductRepo = $this->entityManager->getRepository(BaseProduct::class);
  55. $qb = $baseProductRepo->createQueryBuilder('e')
  56. ->leftJoin('e.category', 'c')
  57. ->orderBy('e.name', 'ASC');
  58. if ($search !== '') {
  59. $qb->andWhere('e.name LIKE :search OR c.name LIKE :search OR e.status LIKE :search')
  60. ->setParameter('search', '%' . $search . '%');
  61. }
  62. $query = $qb->getQuery();
  63. $paginator = new Paginator($query);
  64. $page = $request->query->getInt('page', 1);
  65. $paginator->getQuery()->setFirstResult($pageSize * ($page - 1))->setMaxResults($pageSize);
  66. return $this->render('base_product/list.html.twig', [
  67. 'baseProducts' => $paginator->getIterator(),
  68. 'totalPages' => ceil($paginator->count() / $pageSize),
  69. 'currentPage' => $page,
  70. 'currentPageSize' => $pageSize,
  71. 'route' => 'base_product_list',
  72. 'search' => $search,
  73. ]);
  74. }
  75. #[Route('/add', name: 'base_product_add')]
  76. public function add(Request $request, BaseProductUploader $uploader): Response
  77. {
  78. $addForm = $this->createForm(BaseProductAddType::class);
  79. $addForm->handleRequest($request);
  80. if ($addForm->isSubmitted() && $addForm->isValid()) {
  81. /** @var BaseProduct $baseProduct */
  82. $baseProduct = $addForm->getData();
  83. $this->variantService->addBaseProduct($baseProduct);
  84. $this->addFlash('base_product.success', 'Base Product Added');
  85. return $this->redirectToRoute('base_product_edit', ['id' => $baseProduct->getId()]);
  86. }
  87. return $this->render('base_product/add.html.twig', [
  88. 'addForm' => $addForm->createView()
  89. ]);
  90. }
  91. #[Route('/edit/{id}', name: 'base_product_edit')]
  92. public function edit(Request $request, BaseProduct $baseProduct, BaseProductUploader $uploader, Variants $variantService): Response
  93. {
  94. $editForm = $this->createForm(
  95. BaseProductEditType::class,
  96. $baseProduct,
  97. ['autocomplete_url' => $this->generateUrl('base_product_amazon_categories')]
  98. );
  99. $variantForm = $this->getVariantForm($baseProduct);
  100. if ($variantForm instanceof RedirectResponse) {
  101. return $variantForm;
  102. }
  103. $variantSettingsForm = $this->createForm(BaseProductVariantType::class, $baseProduct, [
  104. 'action' => $this->generateUrl('base_product_variant_settings', ['id' => $baseProduct->getId()]),
  105. 'baseProduct' => $baseProduct,
  106. ]);
  107. $etsyProperties = $this->getEtsyProperties($baseProduct);
  108. $defaultPropertiesForm = $this->getDefaultPropertiesForm($baseProduct, $etsyProperties);
  109. $editForm->handleRequest($request);
  110. $reload = false;
  111. $resetForm = false;
  112. if ($editForm->isSubmitted() && $editForm->isValid()) {
  113. /** @var BaseProduct $baseProduct */
  114. $baseProduct = $editForm->getData();
  115. $hasNoError = true;
  116. if ($baseProduct->getStatus() == 'active') {
  117. $checkResult = $this->checkForActive($baseProduct);
  118. if (!$checkResult['isAllowed']) {
  119. $baseProduct->setStatus('inactive');
  120. $this->addFlash('base_product.error', $checkResult['error']);
  121. $hasNoError = false;
  122. $resetForm = true;
  123. }
  124. }
  125. $imageFile = $editForm->get('image')->getData();
  126. if ($imageFile) {
  127. list($filename, $width, $height) = $uploader->uploadBaseProduct($imageFile, $baseProduct);
  128. $baseProduct->setImage($filename);
  129. }
  130. $isPersonalizable = $editForm->get('isPersonalizable')->getData();
  131. if ($isPersonalizable === null) {
  132. $isPersonalizable = false;
  133. }
  134. $baseProduct->setIsPersonalizable($isPersonalizable);
  135. $this->variantService->matchSides($baseProduct);
  136. $this->entityManager->persist($baseProduct);
  137. $this->entityManager->flush();
  138. if ($hasNoError) {
  139. $this->addFlash('base_product.success', 'Base Product Updated');
  140. }
  141. $reload = true;
  142. if ($resetForm) {
  143. $editForm = $this->createForm(
  144. BaseProductEditType::class,
  145. $baseProduct,
  146. ['autocomplete_url' => $this->generateUrl('base_product_amazon_categories')]
  147. );
  148. }
  149. }
  150. return $this->render('base_product/edit.html.twig', [
  151. 'baseProduct' => $baseProduct,
  152. 'editForm' => $editForm->createView(),
  153. 'defaultProperties' => $defaultPropertiesForm->createView(),
  154. 'variantForm' => $variantForm->createView(),
  155. 'variantSettingsForm' => $variantSettingsForm->createView(),
  156. 'reload' => $reload,
  157. ]);
  158. }
  159. #[Route('/variantForm/{id}', name: 'base_product_variant_form')]
  160. public function variantForm(Request $request, Variants $variantService, BaseProduct $baseProduct): Response
  161. {
  162. $variantAttributes = $variantService->variantAttributes(false, $baseProduct->getCategory());
  163. $variantForm = $this->getVariantForm($baseProduct);
  164. if ($variantForm instanceof RedirectResponse) {
  165. return $variantForm;
  166. }
  167. $variantFormClone = clone $variantForm;
  168. $variantForm->handleRequest($request);
  169. if ($variantForm->isSubmitted() && $variantForm->isValid()) {
  170. if ($variantForm->get('reset')->isClicked()) {
  171. $variantForm = $variantFormClone;
  172. $this->addFlash('base_product_variant.info', 'Variant Settings Reset');
  173. } else {
  174. foreach ($variantAttributes as $variantAttribute) {
  175. $data = $variantForm->get($variantAttribute)->getData();
  176. if (!$data) {
  177. $data = [];
  178. } else {
  179. $data = explode("|", $data);
  180. }
  181. $variantService->saveBaseProductVariantAttributes(
  182. $baseProduct,
  183. $data,
  184. $variantAttribute
  185. );
  186. }
  187. $variantService->deleteObsoleteVariants($baseProduct);
  188. // Update list of variants based on new attributes
  189. $variantService->updateVariants($baseProduct);
  190. $this->addFlash('base_product_variant.success', 'Variant Settings Updated');
  191. $variantForm = $this->getVariantForm($baseProduct);
  192. if ($variantForm instanceof RedirectResponse) {
  193. return $variantForm;
  194. }
  195. $this->entityManager->refresh($baseProduct);
  196. }
  197. }
  198. $variantSettingsForm = $this->createForm(BaseProductVariantType::class, $baseProduct, [
  199. 'action' => $this->generateUrl('base_product_variant_settings', ['id' => $baseProduct->getId()]),
  200. 'baseProduct' => $baseProduct,
  201. ]);
  202. return $this->render('base_product/variantForm.html.twig', [
  203. 'baseProduct' => $baseProduct,
  204. 'variantForm' => $variantForm->createView(),
  205. 'variantSettingsForm' => $variantSettingsForm->createView(),
  206. ]);
  207. }
  208. #[Route('/defaultPropertiesForm/{id}', name: 'base_product_default_properties')]
  209. public function defaultPropertiesForm(
  210. Request $request,
  211. EtsyPropertyBaseProductService $etsyPropertyBaseProductService,
  212. BaseProduct $baseProduct
  213. ): Response
  214. {
  215. //ToDo: implement functionality
  216. $etsyPropertyData = $this->getEtsyProperties($baseProduct);
  217. $defaultPropertiesForm = $this->getDefaultPropertiesForm($baseProduct, $etsyPropertyData);
  218. $defaultPropertiesForm->handleRequest($request);
  219. if ($defaultPropertiesForm->isSubmitted() && $defaultPropertiesForm->isValid()) {
  220. $data = $defaultPropertiesForm->getData();
  221. $etsyProperties = $etsyPropertyData['etsyProperties'];
  222. $etsyProperties = $this->setKeys($etsyProperties, 'property_id');
  223. $etsyPropertyBaseProductData = [];
  224. foreach ($data as $key => $item) {
  225. if (! str_starts_with($key, 'etsyProperty_')) {
  226. continue;
  227. }
  228. $property_id = substr($key, 13);
  229. $newEtsyPropertyBaseProduct = [
  230. 'etsy_property_id' => $property_id,
  231. 'base_product_id' => $baseProduct,
  232. 'is_editable' => $item['is_editable'] ?? true,
  233. 'default_value' => $item['default_value'] ?? null,
  234. 'default_scale_id' => $item['default_scale_id'] ?? null,
  235. ];
  236. if (! isset($etsyPropertyData['etsyPropertyDefaultData'][$property_id])) {
  237. $newEtsyPropertyBaseProduct['etsy_property'] = [
  238. 'etsy_property_id' => $property_id,
  239. 'name' => $etsyProperties[$property_id]['name'],
  240. 'taxonomy_json' => json_encode($etsyProperties[$property_id]),
  241. 'is_editable' => true,
  242. ];
  243. }
  244. $etsyPropertyBaseProductData[] = $newEtsyPropertyBaseProduct;
  245. }
  246. $this->etsyPropertyBaseProductService->saveEtsyPropertiesBaseProduct($etsyPropertyBaseProductData);
  247. }
  248. $etsyPropertyData = $this->getEtsyProperties($baseProduct);
  249. $defaultPropertiesForm = $this->getDefaultPropertiesForm($baseProduct, $etsyPropertyData);
  250. return $this->render('base_product/defaultProperties.html.twig', [
  251. 'baseProduct' => $baseProduct,
  252. 'defaultProperties' => $defaultPropertiesForm->createView(),
  253. ]);
  254. }
  255. private function getEtsyProperties(BaseProduct $baseProduct): array
  256. {
  257. $store = $this->getUser()->getUserStores()->filter(function ($userStore) {
  258. return $userStore->getType() === 'etsy';
  259. })->first();
  260. $category = $baseProduct->getCategory();
  261. $etsyProperties = $this->etsyApiHelper->getTaxonomyNodes($store, $category->getEtsyTaxonomyId());
  262. $etsyPropertyIdList = array_column($etsyProperties, 'property_id');
  263. $etsyPropertyDefaultData = $this->etsyPropertyService->getPropertyList($etsyPropertyIdList);
  264. $etsyPropertyDefaultData = $this->setKeys($etsyPropertyDefaultData, 'etsy_property_id');
  265. $etsyPropertyBaseProductDefaultData = $this->etsyPropertyBaseProductService->getPropertyList(
  266. $etsyPropertyIdList,
  267. $baseProduct->getId(),
  268. );
  269. $etsyPropertyBaseProductDefaultData = $this->setKeys(
  270. $etsyPropertyBaseProductDefaultData,
  271. 'etsy_property_id'
  272. );
  273. return [
  274. 'etsyProperties' => $etsyProperties,
  275. 'etsyPropertyDefaultData' => $etsyPropertyDefaultData,
  276. 'etsyPropertyBaseProductDefaultData' => $etsyPropertyBaseProductDefaultData,
  277. ];
  278. }
  279. private function getDefaultPropertiesForm(BaseProduct $baseProduct, array $etsyProperties): FormInterface
  280. {
  281. return $this->createForm(
  282. BaseProductDefaultPropertiesType::class,
  283. [
  284. 'baseProduct' => $baseProduct,
  285. 'etsyPropertyData' => $etsyProperties,
  286. ],
  287. [
  288. 'data_class' => null,
  289. 'action' => $this->generateUrl(
  290. 'base_product_default_properties',
  291. [
  292. 'id' => $baseProduct->getId(),
  293. ]
  294. ),
  295. 'attr' => [
  296. 'class' => 'default-properties-form'
  297. ],
  298. ],
  299. );
  300. }
  301. #[Route('/variantSettings/{id}', name: 'base_product_variant_settings')]
  302. public function variantSettings(Request $request, Variants $variantService, BaseProduct $baseProduct): Response
  303. {
  304. $variantSettingsForm = $this->createForm(BaseProductVariantType::class, $baseProduct, [
  305. 'action' => $this->generateUrl('base_product_variant_settings', ['id' => $baseProduct->getId()]),
  306. 'baseProduct' => $baseProduct,
  307. ]);
  308. $variantSettingsForm->handleRequest($request);
  309. if ($variantSettingsForm->isSubmitted() && $variantSettingsForm->isValid()) {
  310. $baseProduct = $variantSettingsForm->getData();
  311. $this->entityManager->persist($baseProduct);
  312. $this->entityManager->flush();
  313. $this->addFlash('base_product_variant.success', 'Variant Settings Updated');
  314. $this->addFlash('base_product_variant.processEnded', 'Loaded');
  315. }
  316. return $this->render('base_product/variantSettings.html.twig', [
  317. 'baseProduct' => $baseProduct,
  318. 'variantSettingsForm' => $variantSettingsForm->createView(),
  319. ]);
  320. }
  321. #[Route('/variantImages/{id}', name: 'base_product_variant_images')]
  322. public function variantImages(Request $request, Variants $variantService, BaseProduct $baseProduct): Response
  323. {
  324. return $this->render('base_product/variantsImages.stream.html.twig', [
  325. 'baseProduct' => $baseProduct,
  326. ]);
  327. }
  328. #[Route('/variant/{id}/side', name: 'base_product_variant_side')]
  329. public function variantSide(Request $request, BaseProductUploader $uploader, BaseProductVariant $baseProductVariant): Response
  330. {
  331. $sideForm = $this->createForm(BaseProductVariantImageSettingsType::class, $baseProductVariant, [
  332. 'action' => $this->generateUrl('base_product_variant_side', ['id' => $baseProductVariant->getId()]),
  333. ]);
  334. $sideForm->handleRequest($request);
  335. if ($sideForm->isSubmitted() && $sideForm->isValid()) {
  336. $baseProductVariant = $sideForm->getData();
  337. foreach ($sideForm->get('baseProductSides') as $side) {
  338. /** @var BaseProductSide $baseProductSide */
  339. $baseProductSide = $side->getData();
  340. $imageFile = $side->get('image')->getData();
  341. if ($imageFile) {
  342. list($filename, $width, $height) = $uploader->upload($imageFile, $baseProductSide);
  343. $baseProductSide->setImage($filename);
  344. $baseProductSide->setImageWidth($width);
  345. $baseProductSide->setImageHeight($height);
  346. $this->entityManager->persist($baseProductSide);
  347. }
  348. $svgFile = $side->get('printableAreaSvg')->getData();
  349. if ($svgFile) {
  350. $svgFilename = $uploader->uploadPrintableAreaSvg($svgFile, $baseProductSide);
  351. $baseProductSide->setPrintableAreaSvg($svgFilename);
  352. $this->entityManager->persist($baseProductSide);
  353. } elseif ($side->get('removePrintableAreaSvg')->getData() === '1') {
  354. $baseProductSide->setPrintableAreaSvg(null);
  355. $baseProductSide->setPrintableAreaLeft(null);
  356. $baseProductSide->setPrintableAreaTop(null);
  357. $baseProductSide->setPrintableAreaWidth(null);
  358. $baseProductSide->setPrintableAreaHeight(null);
  359. $this->entityManager->persist($baseProductSide);
  360. }
  361. }
  362. $this->entityManager->persist($baseProductVariant);
  363. $this->entityManager->flush();
  364. $this->addFlash('base_product_variant.success', 'Variant Settings Updated');
  365. }
  366. return $this->render('base_product/variantSide.html.twig', [
  367. 'baseProductVariant' => $baseProductVariant,
  368. 'sideForm' => $sideForm->createView(),
  369. ]);
  370. }
  371. private function getVariantForm(BaseProduct $baseProduct)
  372. {
  373. $options = $this->variantService->getOptionsForForm($baseProduct);
  374. if (isset($options['illegal_char_attribute'])) {
  375. $this->addFlash('vAttribute.error', 'Attribute name contains illegal character, please fix it.');
  376. return $this->redirect("/attribute/edit/{$options['illegal_char_attribute']}");
  377. }
  378. $options['action'] = $this->generateUrl('base_product_variant_form', ['id' => $baseProduct->getId()]);
  379. return $this->createForm(
  380. BaseProductVariantSettingsType::class,
  381. $this->variantService->getDataForForm($baseProduct),
  382. $options
  383. );
  384. }
  385. #[Route('/delete/{id}', name: 'base_product_delete')]
  386. public function delete(Request $request, BaseProduct $baseProduct): Response
  387. {
  388. if ($baseProduct->getProducts()->count()) {
  389. $this->addFlash('base_product.error', 'Some products are using this base, please delete all dependents first');
  390. return $this->redirectToRoute('base_product_list');
  391. }
  392. foreach ($baseProduct->getProducts(true) as $product) {
  393. foreach ($product->getProductVariants() as $pVariant) {
  394. foreach ($pVariant->getProductSides() as $side) {
  395. $this->entityManager->remove($side);
  396. }
  397. $this->entityManager->remove($pVariant);
  398. }
  399. $this->entityManager->remove($product);
  400. }
  401. $baseVariants = $baseProduct->getBaseProductVariants();
  402. foreach ($baseVariants as $baseVariant) {
  403. $sides = $baseVariant->getBaseProductSides();
  404. foreach ($sides as $side) {
  405. $image = $side->getImage();
  406. if ($image) {
  407. // Delete the base product side image from local storage:
  408. $imagePath = getcwd() . $_ENV['BASE_PRODUCTS_SIDES_IMAGES_DIR'] . $image;
  409. if (file_exists($imagePath)) unlink($imagePath);
  410. }
  411. $this->entityManager->remove($side);
  412. }
  413. $this->entityManager->remove($baseVariant);
  414. }
  415. // Delete the main base product image from local storage:
  416. $mainImage = $baseProduct->getImage();
  417. if ($mainImage) {
  418. $imagePath = getcwd() . $_ENV['BASE_PRODUCTS_IMAGES_DIR'] . $mainImage;
  419. if (file_exists($imagePath)) unlink($imagePath);
  420. }
  421. $this->entityManager->remove($baseProduct);
  422. $this->entityManager->flush();
  423. $this->addFlash('base_product.success', 'Base Product Deleted');
  424. return $this->redirectToRoute('base_product_list');
  425. }
  426. #[Route('/image/{id}', name: 'base_product_side_image')]
  427. public function baseImage(BaseProductSide $baseProductSide): Response
  428. {
  429. $image = $baseProductSide->getImage();
  430. // $result = $this->s3->getFile('baseProducts', $image);
  431. // return new Response($result['Body'], 200, ["Content-Type" => $result['ContentType']]);
  432. return $this->redirect("https://$_SERVER[HTTP_HOST]" . $_ENV['BASE_PRODUCTS_SIDES_IMAGES_DIR'] . $image);
  433. }
  434. #[Route('/amazonCategories', name: 'base_product_amazon_categories')]
  435. public function amazonCategories(Request $request, AmazonApiHelper $helper): Response
  436. {
  437. $searchTerm = $request->query->get('query');
  438. $results = $helper->searchCategories($searchTerm);
  439. return new JsonResponse(["results" => $results]);
  440. }
  441. #[Route('/edit/{id}/amazonAttributes', name: 'base_product_amazon_attributes')]
  442. public function amazonAttributes(Request $request, BaseProduct $baseProduct, AmazonAttributes $amazonAttributes): Response
  443. {
  444. $groups = $amazonAttributes->getPropertyGroups($baseProduct->getAmazonCategory());
  445. $form = $amazonAttributes->createFormForBaseProduct($baseProduct);
  446. $form->handleRequest($request);
  447. if ($form->isSubmitted() && $form->isValid()) {
  448. $data = $form->getData();
  449. $baseProduct->setAmazonCategoryProperties($data);
  450. $this->entityManager->persist($baseProduct);
  451. $this->entityManager->flush();
  452. $validated = $amazonAttributes->validate($baseProduct, $data);
  453. //dump($validated);
  454. if ($validated === true) {
  455. return $this->redirectToRoute('base_product_amazon_attributes', ['id' => $baseProduct->getId()]);
  456. } else {
  457. if (is_array($validated)) {
  458. foreach ($validated as $error) {
  459. $form->addError(new FormError($error));
  460. }
  461. } elseif (is_string($validated)) {
  462. $form->addError(new FormError($validated));
  463. } else {
  464. $form->addError(new FormError('Unknown Amazon Error'));
  465. }
  466. }
  467. }
  468. return $this->render('base_product/amazonAttribuntes.html.twig', [
  469. 'product' => $baseProduct,
  470. 'groups' => $groups,
  471. 'form' => $form->createView()
  472. ]);
  473. }
  474. }