<?php
/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Customize\Controller;
use Customize\Service\CountryWidgetService;
use Customize\Form\Type\SearchProductType;
use Eccube\Controller\ProductController as BaseProductController;
use Eccube\Repository\CategoryRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Container\ContainerInterface;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Eccube\Service\PurchaseFlow\PurchaseFlow;
use Eccube\Repository\CustomerFavoriteProductRepository;
use Eccube\Service\CartService;
use Customize\Repository\ProductRepository;
use Eccube\Repository\BaseInfoRepository;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Eccube\Repository\Master\ProductListMaxRepository;
use Eccube\Helper\PageNameHelper;
use Customize\Service\BreadcrumbService;
class ProductController extends BaseProductController
{
/**
* @var CountryWidgetService
*/
protected $countryWidgetService;
/**
* @var CategoryRepository
*/
protected $categoryRepository;
/**
* @var EntityManagerInterface
*/
protected $entityManager;
/**
* ProductController constructor.
*
* @param PurchaseFlow $cartPurchaseFlow
* @param CustomerFavoriteProductRepository $customerFavoriteProductRepository
* @param CartService $cartService
* @param ProductRepository $productRepository
* @param BaseInfoRepository $baseInfoRepository
* @param AuthenticationUtils $helper
* @param ProductListMaxRepository $productListMaxRepository
* @param PageNameHelper $pageNameHelper
* @param BreadcrumbService $breadcrumbService
* @param CountryWidgetService $countryWidgetService
* @param CategoryRepository $categoryRepository
* @param EntityManagerInterface $entityManager
*/
public function __construct(
PurchaseFlow $cartPurchaseFlow,
CustomerFavoriteProductRepository $customerFavoriteProductRepository,
CartService $cartService,
ProductRepository $productRepository,
BaseInfoRepository $baseInfoRepository,
AuthenticationUtils $helper,
ProductListMaxRepository $productListMaxRepository,
PageNameHelper $pageNameHelper,
BreadcrumbService $breadcrumbService,
CountryWidgetService $countryWidgetService,
CategoryRepository $categoryRepository,
EntityManagerInterface $entityManager
) {
parent::__construct(
$cartPurchaseFlow,
$customerFavoriteProductRepository,
$cartService,
$productRepository,
$baseInfoRepository,
$helper,
$productListMaxRepository,
$pageNameHelper,
$breadcrumbService
);
$this->countryWidgetService = $countryWidgetService;
$this->categoryRepository = $categoryRepository;
$this->entityManager = $entityManager;
}
/**
* 商品一覧画面.
*
* @Route("/products/list", name="product_list", methods={"GET"})
* @Template("Product/list.twig")
*/
public function index(Request $request, PaginatorInterface $paginator)
{
// Doctrine SQLFilter
if ($this->BaseInfo->isOptionNostockHidden()) {
$this->entityManager->getFilters()->enable('option_nostock_hidden');
}
// handleRequestは空のqueryの場合は無視するため
if ($request->getMethod() === 'GET') {
$request->query->set('pageno', $request->query->get('pageno', ''));
}
// searchForm - Sử dụng custom SearchProductType
/* @var $builder \Symfony\Component\Form\FormBuilderInterface */
$searchProductType = new \Customize\Form\Type\SearchProductType(
$this->categoryRepository,
$this->entityManager
);
$builder = $this->formFactory->createNamedBuilder('', get_class($searchProductType));
if ($request->getMethod() === 'GET') {
$builder->setMethod('GET');
}
$event = new \Eccube\Event\EventArgs(
[
'builder' => $builder,
],
$request
);
$this->eventDispatcher->dispatch($event, \Eccube\Event\EccubeEvents::FRONT_PRODUCT_INDEX_INITIALIZE);
/* @var $searchForm \Symfony\Component\Form\FormInterface */
$searchForm = $builder->getForm();
$searchForm->handleRequest($request);
// paginator
$searchData = $searchForm->getData();
// Lấy category_filter[] từ request nếu có
$categoryFilters = $request->query->get('category_filter', []);
if (!empty($categoryFilters)) {
$searchData['category_filter'] = is_array($categoryFilters) ? $categoryFilters : [$categoryFilters];
}
// Sử dụng custom ProductRepository để có logic filter theo country
$qb = $this->productRepository->getQueryBuilderBySearchData($searchData);
$event = new \Eccube\Event\EventArgs(
[
'searchData' => $searchData,
'qb' => $qb,
],
$request
);
$this->eventDispatcher->dispatch($event, \Eccube\Event\EccubeEvents::FRONT_PRODUCT_INDEX_SEARCH);
$searchData = $event->getArgument('searchData');
$query = $qb->getQuery()
->useResultCache(true, $this->eccubeConfig['eccube_result_cache_lifetime_short']);
// Xác định số sản phẩm mỗi trang: mặc định là 12
// Chỉ sử dụng giá trị từ form nếu user thực sự chọn (có trong request parameter)
$itemsPerPage = 12; // Mặc định 12 sản phẩm
$dispNumberFromRequest = $request->query->get('disp_number');
if (!empty($dispNumberFromRequest)) {
// Nếu có giá trị từ request, tìm ProductListMax entity
$selectedDispNumber = $this->entityManager->getRepository(\Eccube\Entity\Master\ProductListMax::class)->find($dispNumberFromRequest);
if ($selectedDispNumber) {
$itemsPerPage = $selectedDispNumber->getId(); // ID chính là số sản phẩm
}
}
// Bỏ qua giá trị từ searchData vì form có thể tự động set giá trị mặc định (10)
// Chỉ dùng giá trị từ request parameter nếu user thực sự chọn
/** @var SlidingPagination $pagination */
$pagination = $paginator->paginate(
$query,
!empty($searchData['pageno']) ? $searchData['pageno'] : 1,
$itemsPerPage
);
$ids = [];
foreach ($pagination as $Product) {
$ids[] = $Product->getId();
}
$ProductsAndClassCategories = $this->productRepository->findProductsWithSortedClassCategories($ids, 'p.id');
// addCart form
$forms = [];
foreach ($pagination as $Product) {
/* @var $builder \Symfony\Component\Form\FormBuilderInterface */
$builder = $this->formFactory->createNamedBuilder(
'',
\Eccube\Form\Type\AddCartType::class,
null,
[
'product' => $ProductsAndClassCategories[$Product->getId()],
'allow_extra_fields' => true,
]
);
$addCartForm = $builder->getForm();
$forms[$Product->getId()] = $addCartForm->createView();
}
$Category = $searchForm->get('category_id')->getData();
$FreeArea = '';
$ListProductAttribute = null;
// Kiểm tra nếu có sản phẩm trong pagination thì lấy FreeArea từ sản phẩm đầu tiên
if ($pagination->count() > 0) {
$firstProduct = $pagination->getItems()[0] ?? null;
if ($firstProduct) {
$FreeArea = $firstProduct->getFreeArea();
if ($FreeArea) {
$ListProductAttribute = [];
$lines = preg_split('/\r\n|\r|\n/', $FreeArea);
foreach ($lines as $line) {
if (trim($line) === '') continue;
$parts = explode(':', $line, 2);
if (count($parts) == 2) {
$ListProductAttribute[] = [trim($parts[0]), trim($parts[1])];
}
}
if (count($ListProductAttribute) === 0) {
$ListProductAttribute = null;
}
}
}
}
$categoryId = null;
$prefix = null;
if ($Category !== null) {
$categoryId = $Category->getId();
if ($categoryId !== null) {
$prefix = \Eccube\Entity\Category::getCategoryPageName($categoryId);
}
}
$pageName = $this->pageNameHelper->createPageName($prefix);
// Tạo breadcrumb
$breadcrumbs = $this->breadcrumbService->getProductListBreadcrumb($Category);
// Thêm country_categories vào result
$country_categories = $this->countryWidgetService->getCountryList();
// Lấy tất cả categories có is_searchable = 1 và group theo parent
$searchableCategories = $this->getSearchableCategoriesGrouped();
return [
'subtitle' => $this->getPageTitle($searchData),
'pageName' => $pageName,
'pagination' => $pagination,
'search_form' => $searchForm->createView(),
'forms' => $forms,
'Category' => $Category,
'ListProductAttribute' => $ListProductAttribute,
'FreeArea' => $FreeArea,
'breadcrumbs' => $breadcrumbs,
'country_categories' => $country_categories,
'searchable_categories' => $searchableCategories,
];
}
/**
* カートに追加 - Override để thêm logic kiểm tra IMEI trùng lặp
*
* @Route("/products/add_cart/{id}", name="product_add_cart", methods={"POST"}, requirements={"id" = "\d+"})
*/
public function addCart(Request $request, \Eccube\Entity\Product $Product)
{
// エラーメッセージの配列
$errorMessages = [];
if (!$this->checkVisibility($Product)) {
throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException();
}
$builder = $this->formFactory->createNamedBuilder(
'',
\Eccube\Form\Type\AddCartType::class,
null,
[
'product' => $Product,
'id_add_product_id' => false,
]
);
$event = new \Eccube\Event\EventArgs(
[
'builder' => $builder,
'Product' => $Product,
],
$request
);
$this->eventDispatcher->dispatch($event, \Eccube\Event\EccubeEvents::FRONT_PRODUCT_CART_ADD_INITIALIZE);
/* @var $form \Symfony\Component\Form\FormInterface */
$form = $builder->getForm();
$form->handleRequest($request);
if (!$form->isValid()) {
// Lưu form data và errors vào session để hiển thị lại
$this->session->set('product_detail_form_data', $request->request->all());
$this->session->set('product_detail_form_errors', $this->getFormErrors($form));
return $this->redirectToRoute('product_detail', ['id' => $Product->getId()]);
}
$addCartData = $form->getData();
// Lấy user_imei từ CartItem object
$user_imei = null;
if ($Product->hasUserImeiCategory() && $addCartData instanceof \Eccube\Entity\CartItem) {
$user_imei = $addCartData->getUserImei();
// Kiểm tra IMEI trùng lặp trong giỏ hàng
if ($user_imei && $this->cartService->isImeiDuplicate($user_imei)) {
if ($request->isXmlHttpRequest()) {
return $this->json([
'done' => false,
'error_code' => 'DUPLICATE_IMEI',
'messages' => ['front.shopping.duplicate_imei']
]);
} else {
$this->session->getFlashBag()->set('eccube.front.error', 'front.shopping.duplicate_imei');
return $this->redirectToRoute('product_detail', ['id' => $Product->getId()]);
}
}
}
log_info(
'カート追加処理開始',
[
'product_id' => $Product->getId(),
'product_class_id' => $addCartData->getProductClass()->getId(),
'quantity' => $addCartData->getQuantity(),
'user_imei' => $user_imei,
]
);
// Kiểm tra tồn kho trước khi thêm vào giỏ
$shouldAddToCart = true;
$ProductClass = $addCartData->getProductClass();
if ($ProductClass && !$ProductClass->isStockUnlimited()) {
$currentStock = (int) $ProductClass->getStock();
$requestedQty = (int) $addCartData->getQuantity();
$quantityInCart = $this->cartService->getProductClassQuantityInCart($ProductClass);
$totalRequiredQty = $quantityInCart + $requestedQty;
if ($currentStock <= 0) {
$shouldAddToCart = false;
$errorMessages[] = trans('front.shopping.out_of_stock_zero', ['%product%' => $Product->getName()]);
} elseif ($currentStock < $totalRequiredQty) {
$shouldAddToCart = false;
$errorMessages[] = trans('front.shopping.out_of_stock', ['%product%' => $Product->getName()]);
}
}
if ($shouldAddToCart) {
// カートへ追加
$this->cartService->addProduct($addCartData->getProductClass()->getId(), $addCartData->getQuantity(), $user_imei);
// 明細の正規化
$Carts = $this->cartService->getCarts();
foreach ($Carts as $Cart) {
$result = $this->purchaseFlow->validate($Cart, new \Eccube\Service\PurchaseFlow\PurchaseContext($Cart, $this->getUser()));
// 復旧不可のエラーが発生した場合は追加した明細を削除.
if ($result->hasError()) {
$this->cartService->removeProduct($addCartData->getProductClass()->getId(), $user_imei);
foreach ($result->getErrors() as $error) {
$errorMessages[] = $error->getMessage();
}
}
foreach ($result->getWarning() as $warning) {
$errorMessages[] = $warning->getMessage();
}
}
$this->cartService->save();
log_info(
'カート追加処理完了',
[
'product_id' => $Product->getId(),
'product_class_id' => $addCartData->getProductClass()->getId(),
'quantity' => $addCartData->getQuantity(),
]
);
}
$event = new \Eccube\Event\EventArgs(
[
'form' => $form,
'Product' => $Product,
],
$request
);
$this->eventDispatcher->dispatch($event, \Eccube\Event\EccubeEvents::FRONT_PRODUCT_CART_ADD_COMPLETE);
if ($event->getResponse() !== null) {
return $event->getResponse();
}
if ($request->isXmlHttpRequest()) {
// ajaxでのリクエストの場合は結果をjson形式で返す。
// 初期化
$messages = [];
$errorCode = null;
$stockLeft = null;
if (empty($errorMessages)) {
// エラーが発生していない場合
$done = true;
array_push($messages, trans('front.product.add_cart_complete'));
} else {
// エラーが発生している場合
$done = false;
$messages = $errorMessages;
// 在庫エラー用の追加情報を付与
// 直近追加を試みた商品規格の在庫とリクエスト数量から判定
$ProductClass = $addCartData->getProductClass();
if ($ProductClass && !$ProductClass->isStockUnlimited()) {
$currentStock = (int) $ProductClass->getStock();
$requestedQty = (int) $addCartData->getQuantity();
$quantityInCart = $this->cartService->getProductClassQuantityInCart($ProductClass);
$totalRequiredQty = $quantityInCart + $requestedQty;
if ($currentStock <= 0) {
$errorCode = \Customize\Constant\ErrorCodes::OUT_OF_STOCK_ZERO;
$stockLeft = 0;
} elseif ($currentStock < $totalRequiredQty) {
$errorCode = \Customize\Constant\ErrorCodes::OUT_OF_STOCK;
$stockLeft = $currentStock;
}
}
}
$response = ['done' => $done, 'messages' => $messages];
if (!$done) {
$response['error_code'] = $errorCode;
if ($stockLeft !== null) {
$response['stock_left'] = $stockLeft;
}
}
return $this->json($response);
} else {
// ajax以外でのリクエストの場合はカート画面へリダイレクト
foreach ($errorMessages as $errorMessage) {
$this->addRequestError($errorMessage);
}
return $this->redirectToRoute('cart');
}
}
/**
* Lấy tất cả lỗi từ form
*
* @param \Symfony\Component\Form\FormInterface $form
* @return array
*/
protected function getFormErrors($form)
{
$errors = [];
foreach ($form->getErrors(true) as $error) {
$errors[] = $error->getMessage();
}
return $errors;
}
/**
* Lấy tất cả categories có is_searchable = 1 và group theo parent
*
* @return array
*/
protected function getSearchableCategoriesGrouped()
{
$qb = $this->categoryRepository->createQueryBuilder('c')
->leftJoin('c.Parent', 'p')
->where('c.isSearchable = :isSearchable')
->setParameter('isSearchable', 1)
->orderBy('c.sort_no', 'DESC')
->addOrderBy('c.id', 'ASC');
$allCategories = $qb->getQuery()->getResult();
$grouped = [];
foreach ($allCategories as $category) {
$parent = $category->getParent();
if ($parent === null) {
// Category cha có is_searchable = 1
$parentId = $category->getId();
if (!isset($grouped[$parentId])) {
$grouped[$parentId] = [
'parent' => $category,
'children' => []
];
}
} else {
// Category con - lấy parent (dù parent có is_searchable hay không)
$parentId = $parent->getId();
if (!isset($grouped[$parentId])) {
// Lấy parent từ database nếu chưa có
$parentEntity = $this->categoryRepository->find($parentId);
$grouped[$parentId] = [
'parent' => $parentEntity,
'children' => []
];
}
$grouped[$parentId]['children'][] = $category;
}
}
// Sắp xếp lại theo sort_no của parent (DESC), sau đó sort children theo sort_no
uasort($grouped, function($a, $b) {
$parentSortCompare = $b['parent']->getSortNo() <=> $a['parent']->getSortNo();
if ($parentSortCompare !== 0) {
return $parentSortCompare;
}
// Nếu sort_no bằng nhau, sắp xếp theo ID
return $a['parent']->getId() <=> $b['parent']->getId();
});
// Sắp xếp children trong mỗi group
foreach ($grouped as &$group) {
usort($group['children'], function($a, $b) {
$sortCompare = $b->getSortNo() <=> $a->getSortNo();
if ($sortCompare !== 0) {
return $sortCompare;
}
return $a->getId() <=> $b->getId();
});
}
unset($group);
return $grouped;
}
}