<?php
namespace App\Controller;
use App\Entity\BaseProduct;
use App\Entity\BaseProductSide;
use App\Entity\BaseProductVariant;
use App\Form\BaseProductAddType;
use App\Form\BaseProductDefaultPropertiesType;
use App\Form\BaseProductEditType;
use App\Form\BaseProductVariantImageSettingsType;
use App\Form\BaseProductVariantSettingsType;
use App\Form\BaseProductVariantType;
use App\Service\Amazon\AmazonAttributes;
use App\Service\AmazonApiHelper;
use App\Service\BaseProducts\Variants;
use App\Service\BaseProductUploader;
use App\Service\EtsyApiHelper;
use App\Service\EtsyPropertyBaseProductService;
use App\Service\EtsyPropertyService;
use App\Service\S3;
use App\Service\ShipStation\ShipStationApiClient;
use App\Traits\ArrayTransformationTrait;
use App\Traits\BaseProductTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/admin/baseProduct')]
class BaseProductController extends AbstractController
{
use ArrayTransformationTrait;
use BaseProductTrait;
public function __construct(
private EntityManagerInterface $entityManager,
private Variants $variantService,
private S3 $s3,
private EtsyApiHelper $etsyApiHelper,
private EtsyPropertyService $etsyPropertyService,
private EtsyPropertyBaseProductService $etsyPropertyBaseProductService,
private ShipStationApiClient $shipStationApiClient,
)
{
}
#[Route('/list', name: 'base_product_list')]
public function list(Request $request): Response
{
$pageSize = $request->query->getInt('pageSize', 10); // Number of items per page.
$search = trim((string) $request->query->get('search', ''));
$baseProductRepo = $this->entityManager->getRepository(BaseProduct::class);
$qb = $baseProductRepo->createQueryBuilder('e')
->leftJoin('e.category', 'c')
->orderBy('e.name', 'ASC');
if ($search !== '') {
$qb->andWhere('e.name LIKE :search OR c.name LIKE :search OR e.status LIKE :search')
->setParameter('search', '%' . $search . '%');
}
$query = $qb->getQuery();
$paginator = new Paginator($query);
$page = $request->query->getInt('page', 1);
$paginator->getQuery()->setFirstResult($pageSize * ($page - 1))->setMaxResults($pageSize);
return $this->render('base_product/list.html.twig', [
'baseProducts' => $paginator->getIterator(),
'totalPages' => ceil($paginator->count() / $pageSize),
'currentPage' => $page,
'currentPageSize' => $pageSize,
'route' => 'base_product_list',
'search' => $search,
]);
}
#[Route('/add', name: 'base_product_add')]
public function add(Request $request, BaseProductUploader $uploader): Response
{
$addForm = $this->createForm(BaseProductAddType::class);
$addForm->handleRequest($request);
if ($addForm->isSubmitted() && $addForm->isValid()) {
/** @var BaseProduct $baseProduct */
$baseProduct = $addForm->getData();
$this->variantService->addBaseProduct($baseProduct);
$this->addFlash('base_product.success', 'Base Product Added');
return $this->redirectToRoute('base_product_edit', ['id' => $baseProduct->getId()]);
}
return $this->render('base_product/add.html.twig', [
'addForm' => $addForm->createView()
]);
}
#[Route('/edit/{id}', name: 'base_product_edit')]
public function edit(Request $request, BaseProduct $baseProduct, BaseProductUploader $uploader, Variants $variantService): Response
{
$editForm = $this->createForm(
BaseProductEditType::class,
$baseProduct,
['autocomplete_url' => $this->generateUrl('base_product_amazon_categories')]
);
$variantForm = $this->getVariantForm($baseProduct);
if ($variantForm instanceof RedirectResponse) {
return $variantForm;
}
$variantSettingsForm = $this->createForm(BaseProductVariantType::class, $baseProduct, [
'action' => $this->generateUrl('base_product_variant_settings', ['id' => $baseProduct->getId()]),
'baseProduct' => $baseProduct,
]);
$etsyProperties = $this->getEtsyProperties($baseProduct);
$defaultPropertiesForm = $this->getDefaultPropertiesForm($baseProduct, $etsyProperties);
$editForm->handleRequest($request);
$reload = false;
$resetForm = false;
if ($editForm->isSubmitted() && $editForm->isValid()) {
/** @var BaseProduct $baseProduct */
$baseProduct = $editForm->getData();
$hasNoError = true;
if ($baseProduct->getStatus() == 'active') {
$checkResult = $this->checkForActive($baseProduct);
if (!$checkResult['isAllowed']) {
$baseProduct->setStatus('inactive');
$this->addFlash('base_product.error', $checkResult['error']);
$hasNoError = false;
$resetForm = true;
}
}
$imageFile = $editForm->get('image')->getData();
if ($imageFile) {
list($filename, $width, $height) = $uploader->uploadBaseProduct($imageFile, $baseProduct);
$baseProduct->setImage($filename);
}
$isPersonalizable = $editForm->get('isPersonalizable')->getData();
if ($isPersonalizable === null) {
$isPersonalizable = false;
}
$baseProduct->setIsPersonalizable($isPersonalizable);
$this->variantService->matchSides($baseProduct);
$this->entityManager->persist($baseProduct);
$this->entityManager->flush();
if ($hasNoError) {
$this->addFlash('base_product.success', 'Base Product Updated');
}
$reload = true;
if ($resetForm) {
$editForm = $this->createForm(
BaseProductEditType::class,
$baseProduct,
['autocomplete_url' => $this->generateUrl('base_product_amazon_categories')]
);
}
}
return $this->render('base_product/edit.html.twig', [
'baseProduct' => $baseProduct,
'editForm' => $editForm->createView(),
'defaultProperties' => $defaultPropertiesForm->createView(),
'variantForm' => $variantForm->createView(),
'variantSettingsForm' => $variantSettingsForm->createView(),
'reload' => $reload,
]);
}
#[Route('/variantForm/{id}', name: 'base_product_variant_form')]
public function variantForm(Request $request, Variants $variantService, BaseProduct $baseProduct): Response
{
$variantAttributes = $variantService->variantAttributes(false, $baseProduct->getCategory());
$variantForm = $this->getVariantForm($baseProduct);
if ($variantForm instanceof RedirectResponse) {
return $variantForm;
}
$variantFormClone = clone $variantForm;
$variantForm->handleRequest($request);
if ($variantForm->isSubmitted() && $variantForm->isValid()) {
if ($variantForm->get('reset')->isClicked()) {
$variantForm = $variantFormClone;
$this->addFlash('base_product_variant.info', 'Variant Settings Reset');
} else {
foreach ($variantAttributes as $variantAttribute) {
$data = $variantForm->get($variantAttribute)->getData();
if (!$data) {
$data = [];
} else {
$data = explode("|", $data);
}
$variantService->saveBaseProductVariantAttributes(
$baseProduct,
$data,
$variantAttribute
);
}
$variantService->deleteObsoleteVariants($baseProduct);
// Update list of variants based on new attributes
$variantService->updateVariants($baseProduct);
$this->addFlash('base_product_variant.success', 'Variant Settings Updated');
$variantForm = $this->getVariantForm($baseProduct);
if ($variantForm instanceof RedirectResponse) {
return $variantForm;
}
$this->entityManager->refresh($baseProduct);
}
}
$variantSettingsForm = $this->createForm(BaseProductVariantType::class, $baseProduct, [
'action' => $this->generateUrl('base_product_variant_settings', ['id' => $baseProduct->getId()]),
'baseProduct' => $baseProduct,
]);
return $this->render('base_product/variantForm.html.twig', [
'baseProduct' => $baseProduct,
'variantForm' => $variantForm->createView(),
'variantSettingsForm' => $variantSettingsForm->createView(),
]);
}
#[Route('/defaultPropertiesForm/{id}', name: 'base_product_default_properties')]
public function defaultPropertiesForm(
Request $request,
EtsyPropertyBaseProductService $etsyPropertyBaseProductService,
BaseProduct $baseProduct
): Response
{
//ToDo: implement functionality
$etsyPropertyData = $this->getEtsyProperties($baseProduct);
$defaultPropertiesForm = $this->getDefaultPropertiesForm($baseProduct, $etsyPropertyData);
$defaultPropertiesForm->handleRequest($request);
if ($defaultPropertiesForm->isSubmitted() && $defaultPropertiesForm->isValid()) {
$data = $defaultPropertiesForm->getData();
$etsyProperties = $etsyPropertyData['etsyProperties'];
$etsyProperties = $this->setKeys($etsyProperties, 'property_id');
$etsyPropertyBaseProductData = [];
foreach ($data as $key => $item) {
if (! str_starts_with($key, 'etsyProperty_')) {
continue;
}
$property_id = substr($key, 13);
$newEtsyPropertyBaseProduct = [
'etsy_property_id' => $property_id,
'base_product_id' => $baseProduct,
'is_editable' => $item['is_editable'] ?? true,
'default_value' => $item['default_value'] ?? null,
'default_scale_id' => $item['default_scale_id'] ?? null,
];
if (! isset($etsyPropertyData['etsyPropertyDefaultData'][$property_id])) {
$newEtsyPropertyBaseProduct['etsy_property'] = [
'etsy_property_id' => $property_id,
'name' => $etsyProperties[$property_id]['name'],
'taxonomy_json' => json_encode($etsyProperties[$property_id]),
'is_editable' => true,
];
}
$etsyPropertyBaseProductData[] = $newEtsyPropertyBaseProduct;
}
$this->etsyPropertyBaseProductService->saveEtsyPropertiesBaseProduct($etsyPropertyBaseProductData);
}
$etsyPropertyData = $this->getEtsyProperties($baseProduct);
$defaultPropertiesForm = $this->getDefaultPropertiesForm($baseProduct, $etsyPropertyData);
return $this->render('base_product/defaultProperties.html.twig', [
'baseProduct' => $baseProduct,
'defaultProperties' => $defaultPropertiesForm->createView(),
]);
}
private function getEtsyProperties(BaseProduct $baseProduct): array
{
$store = $this->getUser()->getUserStores()->filter(function ($userStore) {
return $userStore->getType() === 'etsy';
})->first();
$category = $baseProduct->getCategory();
$etsyProperties = $this->etsyApiHelper->getTaxonomyNodes($store, $category->getEtsyTaxonomyId());
$etsyPropertyIdList = array_column($etsyProperties, 'property_id');
$etsyPropertyDefaultData = $this->etsyPropertyService->getPropertyList($etsyPropertyIdList);
$etsyPropertyDefaultData = $this->setKeys($etsyPropertyDefaultData, 'etsy_property_id');
$etsyPropertyBaseProductDefaultData = $this->etsyPropertyBaseProductService->getPropertyList(
$etsyPropertyIdList,
$baseProduct->getId(),
);
$etsyPropertyBaseProductDefaultData = $this->setKeys(
$etsyPropertyBaseProductDefaultData,
'etsy_property_id'
);
return [
'etsyProperties' => $etsyProperties,
'etsyPropertyDefaultData' => $etsyPropertyDefaultData,
'etsyPropertyBaseProductDefaultData' => $etsyPropertyBaseProductDefaultData,
];
}
private function getDefaultPropertiesForm(BaseProduct $baseProduct, array $etsyProperties): FormInterface
{
return $this->createForm(
BaseProductDefaultPropertiesType::class,
[
'baseProduct' => $baseProduct,
'etsyPropertyData' => $etsyProperties,
],
[
'data_class' => null,
'action' => $this->generateUrl(
'base_product_default_properties',
[
'id' => $baseProduct->getId(),
]
),
'attr' => [
'class' => 'default-properties-form'
],
],
);
}
#[Route('/variantSettings/{id}', name: 'base_product_variant_settings')]
public function variantSettings(Request $request, Variants $variantService, BaseProduct $baseProduct): Response
{
$variantSettingsForm = $this->createForm(BaseProductVariantType::class, $baseProduct, [
'action' => $this->generateUrl('base_product_variant_settings', ['id' => $baseProduct->getId()]),
'baseProduct' => $baseProduct,
]);
$variantSettingsForm->handleRequest($request);
if ($variantSettingsForm->isSubmitted() && $variantSettingsForm->isValid()) {
$baseProduct = $variantSettingsForm->getData();
$this->entityManager->persist($baseProduct);
$this->entityManager->flush();
$this->addFlash('base_product_variant.success', 'Variant Settings Updated');
$this->addFlash('base_product_variant.processEnded', 'Loaded');
}
return $this->render('base_product/variantSettings.html.twig', [
'baseProduct' => $baseProduct,
'variantSettingsForm' => $variantSettingsForm->createView(),
]);
}
#[Route('/variantImages/{id}', name: 'base_product_variant_images')]
public function variantImages(Request $request, Variants $variantService, BaseProduct $baseProduct): Response
{
return $this->render('base_product/variantsImages.stream.html.twig', [
'baseProduct' => $baseProduct,
]);
}
#[Route('/variant/{id}/side', name: 'base_product_variant_side')]
public function variantSide(Request $request, BaseProductUploader $uploader, BaseProductVariant $baseProductVariant): Response
{
$sideForm = $this->createForm(BaseProductVariantImageSettingsType::class, $baseProductVariant, [
'action' => $this->generateUrl('base_product_variant_side', ['id' => $baseProductVariant->getId()]),
]);
$sideForm->handleRequest($request);
if ($sideForm->isSubmitted() && $sideForm->isValid()) {
$baseProductVariant = $sideForm->getData();
foreach ($sideForm->get('baseProductSides') as $side) {
/** @var BaseProductSide $baseProductSide */
$baseProductSide = $side->getData();
$imageFile = $side->get('image')->getData();
if ($imageFile) {
list($filename, $width, $height) = $uploader->upload($imageFile, $baseProductSide);
$baseProductSide->setImage($filename);
$baseProductSide->setImageWidth($width);
$baseProductSide->setImageHeight($height);
$this->entityManager->persist($baseProductSide);
}
$svgFile = $side->get('printableAreaSvg')->getData();
if ($svgFile) {
$svgFilename = $uploader->uploadPrintableAreaSvg($svgFile, $baseProductSide);
$baseProductSide->setPrintableAreaSvg($svgFilename);
$this->entityManager->persist($baseProductSide);
} elseif ($side->get('removePrintableAreaSvg')->getData() === '1') {
$baseProductSide->setPrintableAreaSvg(null);
$baseProductSide->setPrintableAreaLeft(null);
$baseProductSide->setPrintableAreaTop(null);
$baseProductSide->setPrintableAreaWidth(null);
$baseProductSide->setPrintableAreaHeight(null);
$this->entityManager->persist($baseProductSide);
}
}
$this->entityManager->persist($baseProductVariant);
$this->entityManager->flush();
$this->addFlash('base_product_variant.success', 'Variant Settings Updated');
}
return $this->render('base_product/variantSide.html.twig', [
'baseProductVariant' => $baseProductVariant,
'sideForm' => $sideForm->createView(),
]);
}
private function getVariantForm(BaseProduct $baseProduct)
{
$options = $this->variantService->getOptionsForForm($baseProduct);
if (isset($options['illegal_char_attribute'])) {
$this->addFlash('vAttribute.error', 'Attribute name contains illegal character, please fix it.');
return $this->redirect("/attribute/edit/{$options['illegal_char_attribute']}");
}
$options['action'] = $this->generateUrl('base_product_variant_form', ['id' => $baseProduct->getId()]);
return $this->createForm(
BaseProductVariantSettingsType::class,
$this->variantService->getDataForForm($baseProduct),
$options
);
}
#[Route('/delete/{id}', name: 'base_product_delete')]
public function delete(Request $request, BaseProduct $baseProduct): Response
{
if ($baseProduct->getProducts()->count()) {
$this->addFlash('base_product.error', 'Some products are using this base, please delete all dependents first');
return $this->redirectToRoute('base_product_list');
}
foreach ($baseProduct->getProducts(true) as $product) {
foreach ($product->getProductVariants() as $pVariant) {
foreach ($pVariant->getProductSides() as $side) {
$this->entityManager->remove($side);
}
$this->entityManager->remove($pVariant);
}
$this->entityManager->remove($product);
}
$baseVariants = $baseProduct->getBaseProductVariants();
foreach ($baseVariants as $baseVariant) {
$sides = $baseVariant->getBaseProductSides();
foreach ($sides as $side) {
$image = $side->getImage();
if ($image) {
// Delete the base product side image from local storage:
$imagePath = getcwd() . $_ENV['BASE_PRODUCTS_SIDES_IMAGES_DIR'] . $image;
if (file_exists($imagePath)) unlink($imagePath);
}
$this->entityManager->remove($side);
}
$this->entityManager->remove($baseVariant);
}
// Delete the main base product image from local storage:
$mainImage = $baseProduct->getImage();
if ($mainImage) {
$imagePath = getcwd() . $_ENV['BASE_PRODUCTS_IMAGES_DIR'] . $mainImage;
if (file_exists($imagePath)) unlink($imagePath);
}
$this->entityManager->remove($baseProduct);
$this->entityManager->flush();
$this->addFlash('base_product.success', 'Base Product Deleted');
return $this->redirectToRoute('base_product_list');
}
#[Route('/image/{id}', name: 'base_product_side_image')]
public function baseImage(BaseProductSide $baseProductSide): Response
{
$image = $baseProductSide->getImage();
// $result = $this->s3->getFile('baseProducts', $image);
// return new Response($result['Body'], 200, ["Content-Type" => $result['ContentType']]);
return $this->redirect("https://$_SERVER[HTTP_HOST]" . $_ENV['BASE_PRODUCTS_SIDES_IMAGES_DIR'] . $image);
}
#[Route('/amazonCategories', name: 'base_product_amazon_categories')]
public function amazonCategories(Request $request, AmazonApiHelper $helper): Response
{
$searchTerm = $request->query->get('query');
$results = $helper->searchCategories($searchTerm);
return new JsonResponse(["results" => $results]);
}
#[Route('/edit/{id}/amazonAttributes', name: 'base_product_amazon_attributes')]
public function amazonAttributes(Request $request, BaseProduct $baseProduct, AmazonAttributes $amazonAttributes): Response
{
$groups = $amazonAttributes->getPropertyGroups($baseProduct->getAmazonCategory());
$form = $amazonAttributes->createFormForBaseProduct($baseProduct);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$baseProduct->setAmazonCategoryProperties($data);
$this->entityManager->persist($baseProduct);
$this->entityManager->flush();
$validated = $amazonAttributes->validate($baseProduct, $data);
//dump($validated);
if ($validated === true) {
return $this->redirectToRoute('base_product_amazon_attributes', ['id' => $baseProduct->getId()]);
} else {
if (is_array($validated)) {
foreach ($validated as $error) {
$form->addError(new FormError($error));
}
} elseif (is_string($validated)) {
$form->addError(new FormError($validated));
} else {
$form->addError(new FormError('Unknown Amazon Error'));
}
}
}
return $this->render('base_product/amazonAttribuntes.html.twig', [
'product' => $baseProduct,
'groups' => $groups,
'form' => $form->createView()
]);
}
}