<?php
namespace App\Controller\Front;
use App\Entity\Category;
use App\Entity\Declination;
use App\Entity\Produit;
use App\Entity\Slider;
use App\Entity\ProduitDeclinationValue;
use App\Entity\Promotion;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Service\WebsiteSettingService;
use App\Entity\Pack;
use App\Entity\PackImageDeclination;
class HomeController extends AbstractController
{
use ImageTrait;
/** @var EntityManagerInterface */
private $em;
private WebsiteSettingService $websiteSettingService;
public function __construct(
EntityManagerInterface $manager,
WebsiteSettingService $websiteSettingService
)
{
$this->em = $manager;
$this->websiteSettingService = $websiteSettingService;
}
private function getNewProductDaysThreshold(): int
{
$days = (int) $this->websiteSettingService->get('newProductDays', 30);
return $days > 0 ? $days : 30;
}
private function getTopSalesPeriodThreshold(): int
{
$days = (int) $this->websiteSettingService->get('topSalesPeriod', 30);
return $days > 0 ? $days : 30;
}
private function getCatalogFilterMaxPrice(): float
{
$max = (float) $this->websiteSettingService->get('catalogFilterMaxPrice', 999999);
return $max > 0 ? $max : 999999;
}
/**
* @Route("/", name="home", options={"expose"=true})
*/
public function home(WebsiteSettingService $websiteSettingService): Response
{
$categories = $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1]);
$newProductDays = $this->getNewProductDaysThreshold();
$date = new \DateTime('now');
$date->modify(sprintf('-%d day', $newProductDays));
// get the product repository for newest product
$produits = $this->em->getRepository(Produit::class);
$sliders=$this->em->getRepository(Slider::class)->findBy(['isActive'=>true],['order'=>'asc'],3);
// Requete pour recuperer la nouvelle collection
$query = $produits->createQueryBuilder('p')
->where('p.createdAt >= :date')
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->setParameter('date', $date)
->orderBy('p.createdAt', 'DESC')
->setMaxResults(10);
$products = $query->getQuery()->getResult();
$products = array_slice($products, 0, 10);
$productsWithImage = [];
foreach ($products as $product) {
$image = $this->getDefaultImage($product);
if (!$image) {
continue;
}
$product->image = $image;
$product->aspectRatio = $this->getCategoryAspectRatio($product);
$productsWithImage[] = $product;
}
$products = $productsWithImage;
$topSalesPeriod = $this->getTopSalesPeriodThreshold();
// Requete pour recuperer les top ventes
// On va recuperer les produits crees jusqu'a 1 mois
$date = new \DateTime('now');
$date->modify(sprintf('-%d day', $topSalesPeriod));
$query = $this->em->createQueryBuilder();
$query->add('select', 'p')
->from('App\Entity\Produit', 'p')
->join('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->join('App\Entity\DocumentDeclinationProduit', 'c', 'with', 'c.produitDeclinationValue=d')
->where('p.deletedAt is null')
->andWhere('c.createdAt > = :date')
->andWhere('p.showInWebSite = 1')
->setParameter('date', $date)
->groupBy('p.id')
->orderBy('count(p.id)', 'DESC')
->setMaxResults(50);
// derniers 90 jours
$topProducts = $query->getQuery()->getResult();
// limit to the first 10 top products
$topProducts = array_slice($topProducts, 0, 10);
$topProductsWithImage = [];
foreach ($topProducts as $product) {
$image = $this->getDefaultImage($product);
if (!$image) {
continue;
}
$product->image = $image;
$product->aspectRatio = $this->getCategoryAspectRatio($product);
$topProductsWithImage[] = $product;
}
$topProducts = $topProductsWithImage;
// Maximum pour avoir une livraison gratuite
$max = (float) $websiteSettingService->get('freeDeliveryAmount', 0);
$publicDir=$this->getParameter('kernel.project_dir')."/public";
if($this->getParameter('app.under_construction')=="true")
return $this->render('front/soon.html.twig');
else
return $this->render('front/home.html.twig', array(
'sliders'=>$sliders,
'products' => $products,
'topProducts' => $topProducts,
'max' => $max,
'exist_banner1'=>file_exists($publicDir ."/images/banner/banner-9.jpg"),
'exist_banner2'=>file_exists($publicDir."/images/banner/banner-10.jpg")
));
}
/**
* @Route("/api/category_home", name="api_category_home", options={"expose"=true}, methods={"GET"})
*/
public function newCategoryHomeAPI(Request $request): Response
{
$categories = $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1], ['homepageOrder' => 'ASC'], 10, 0);
$requestedMode = strtolower(trim((string) $request->query->get('mode', '')));
$catalogListMode = in_array($requestedMode, ['product', 'declination'], true)
? $requestedMode
: strtolower(trim((string) $this->websiteSettingService->get('catalogListMode', 'product')));
if (!in_array($catalogListMode, ['product', 'declination'], true)) {
$catalogListMode = 'product';
}
$metaOnly = (bool) $request->query->get('metaOnly', false);
$categoryId = (int) $request->query->get('categoryId', 0);
if ($metaOnly) {
$data = [];
foreach ($categories as $category) {
$data[] = (object) [
'category' => $category,
'products' => [],
'loaded' => false,
];
}
return $this->jsonCachedResponse([
'res' => 'OK',
'data' => $data,
'message' => 'Catgories rcuprs avec succs.',
], 120);
}
if ($categoryId > 0) {
$category = $this->em->getRepository(Category::class)->find($categoryId);
if (!$category) {
return new JsonResponse([
'res' => 'ERROR',
'message' => 'Catégorie introuvable.',
], 404);
}
$products = $this->loadHomeCategoryProducts((int) $category->getId(), $catalogListMode, $request);
return $this->jsonCachedResponse([
'res' => 'OK',
'data' => [
(object) [
'category' => $category,
'products' => $products,
'loaded' => true,
],
],
'message' => 'Catgorie rcupre avec succs.',
], 120);
}
$data = [];
foreach ($categories as $category) {
$products = $this->loadHomeCategoryProducts((int) $category->getId(), $catalogListMode, $request);
$data[] = (object) [
'category' => $category,
'products' => $products,
'loaded' => true,
];
}
$response = [
'res' => 'OK',
'data' => $data,
'message' => 'Catgories rcuprs avec succs.',
];
return $this->jsonCachedResponse($response, 120);
}
/**
* @Route("/api/category_home_dec", name="api_category_home_dec", options={"expose"=true}, methods={"GET"})
*/
public function newCategoryHomeDecAPI(Request $request): Response
{
$categories = $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1], ['homepageOrder' => 'ASC'], 10, 0);
$data = [];
foreach ($categories as $category) {
$categoryScope = [$category];
foreach ($category->getSubCategories() as $child) {
$categoryScope[] = $child;
foreach ($child->getSubCategories() as $sub) {
$categoryScope[] = $sub;
}
}
$query = $this->em->getRepository(ProduitDeclinationValue::class)->createQueryBuilder('d');
$query->innerJoin('d.produit', 'p')
->where('p.categories in (:cats)')
->setParameter('cats', $categoryScope)
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->orderBy('p.id', 'DESC')
->addOrderBy('d.id', 'DESC');
$products = array_slice($this->buildHomePositionOneCards($query->getQuery()->getResult()), 0, 8);
$data[] = (object) [
'category' => $category,
'products' => $products,
];
}
return new JsonResponse([
'res' => 'OK',
'data' => $data,
'message' => 'Categories recuperees avec succes.',
]);
}
/**
* @Route("/home", name="home_index")
*/
public function index(): Response
{
$categories = $this->em->getRepository(Category::class)->findBy(['showInHomepage' => 1]);
$newProductDays = $this->getNewProductDaysThreshold();
$date = new \DateTime('now');
$date->modify(sprintf('-%d day', $newProductDays));
// get the product repository for newest product
$produits = $this->em->getRepository(Produit::class);
// build the query for the doctrine paginator
$query = $produits->createQueryBuilder('p')
->where('p.createdAt >= :date')
->andWhere('p.deletedAt is null')
->setParameter('date', $date)
->orderBy('p.createdAt', 'DESC')
->setMaxResults(10);
$products = $query->getQuery()->getResult();
// Recuperer les images
foreach ($products as $product) {
$product->image = $this->getDefaultImage( $product);
};
$query = $this->em->createQueryBuilder();
$query->add('select', 'p')
->from('App\Entity\Produit', 'p')
->join('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->join('App\Entity\DocumentDeclinationProduit', 'c', 'with', 'c.produitDeclinationValue=d')
->where('p.deletedAt is null')
->groupBy('p.id')
->orderBy('count(p.id)', 'DESC')
->setMaxResults(10);
$topProducts = $query->getQuery()->getResult();
// Recuperer les images
foreach ($topProducts as $product) {
$product->image = $this->getDefaultImage( $product);
}
// Maximum pour avoir une livraison gratuite
$max = (float) $websiteSettingService->get('freeDeliveryAmount', 0);
return $this->render('front/home.html.twig', array(
'products' => $products,
'topProducts' => $topProducts,
'max' => $max
));
}
/**
* @Route("/new-products", name="new_products")
*/
public function newProducts(): Response
{
return $this->render('front/product/listProducts.html.twig');
}
/**
* @Route("/new-products-dec", name="new_products_dec")
*/
public function newProductsDec(): Response
{
return $this->render('front/dec/newProductsDec.html.twig');
}
/**
* @Route("/api/new-products", name="api_new_products", options={"expose"=true}, methods={"GET"})
*/
public function newProductsAPI(Request $request): Response
{
$requestedMode = strtolower(trim((string) $request->query->get('mode', '')));
if ($requestedMode === 'declination') {
return $this->newProductsDecAPI($request);
}
$page = max(1, (int) $request->query->get('page', 1));
$pageSize = max(1, (int) $request->query->get('pageSize', 12));
$orderBy = (int) $request->query->get('orderBy', 6);
//Les Filtres de recherche
$tailles = preg_split('@,@', $request->query->get('tailles'), NULL, PREG_SPLIT_NO_EMPTY);
$couleurs = preg_split('@,@', $request->query->get('couleurs'), NULL, PREG_SPLIT_NO_EMPTY);
$maxPrice = (float) $request->query->get('maxPrice', $this->getCatalogFilterMaxPrice());
if ($maxPrice <= 0) {
$maxPrice = $this->getCatalogFilterMaxPrice();
}
$minPrice = floatval($request->query->get('minPrice')) ?: 0;
$logContent = sprintf(
"[%s] newProductsAPI params -> page:%s order:%s minPrice:%s maxPrice:%s tailles:%s couleurs:%s\n",
(new \DateTime())->format('Y-m-d H:i:s'),
$page, $orderBy, $minPrice, $maxPrice,
json_encode(array_values(array_filter($tailles))),
json_encode(array_values(array_filter($couleurs)))
);
$newProductDays = $this->getNewProductDaysThreshold();
$date = new \DateTime('now');
$date->modify(sprintf('-%d day', $newProductDays));
// build the query for the doctrine paginator
$query = $this->em->getRepository(Produit::class)->createQueryBuilder('p')
->where('p.createdAt >= :date')
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->setParameter('date', $date)
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('maxPrice', $maxPrice)
->setParameter('minPrice', $minPrice);
// Appliquer les filtres de tailles si ils sont appliqus
if( $tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles)")
->andWhere('v.declination = :declination')
->setParameter('declination', $declination)
->setParameter('tailles', $tailles);
}
// Appliquer les filtres de couleurs si ils sont appliqus seulement
if( $couleurs && !$tailles) {
$declination = $this->em->getRepository(Declination::class)->find(2);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
->andWhere('v.declination = :declination')
->setParameter('declination', $declination)
->setParameter('couleurs', $couleurs);
}
// Appliquer les filtres de tailles et couleurs appliqus les deux
if( $tailles && $couleurs) {
$declinationTaille = $this->em->getRepository(Declination::class)->find(1);
$declinationCouleur = $this->em->getRepository(Declination::class)->find(2);
// Fusionner les deux filtres en un seul
$filtres = array_merge($tailles, $couleurs);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
/* ->andWhere('v.declination = :declinationTaille OR v.declination = :declinationCouleur')
->setParameter('declinationTaille', $declinationTaille)*/
->setParameter('couleurs', $couleurs)
->setParameter('tailles', $tailles);
}
// Order by
switch ($orderBy) {
case 1:
$query->orderBy('p.name', 'ASC');
$query->addOrderBy('p.id', 'ASC');
break;
case 2:
$query->orderBy('p.name', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
case 3:
$query->orderBy('p.price_ttc', 'ASC');
$query->addOrderBy('p.id', 'ASC');
break;
case 4:
$query->orderBy('p.price_ttc', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
case 5:
$query->orderBy('p.createdAt', 'ASC');
$query->addOrderBy('p.id', 'ASC');
break;
case 6:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
default:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
}
$query->getQuery();
// load doctrine Paginator
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
// you can get total items
$totalItems = count($paginator);
// get total pages
$pagesCount = ceil($totalItems / $pageSize);
// now get one page's items:
$paginator
->getQuery()
->setFirstResult($pageSize * ($page-1)) // set the offset
->setMaxResults($pageSize); // set the limit
$pageProducts = iterator_to_array($paginator, false);
$hydratedProducts = $this->preloadProductsForCards($pageProducts);
$data = [];
foreach ($pageProducts as $pageItem) {
$productId = (int) $pageItem->getId();
if (!isset($hydratedProducts[$productId])) {
continue;
}
$data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
}
/*
* Ancien code conserve:
* $data= array();
* foreach ($paginator as $pageItem) {
* $pageItem->aspectRatio = $this->getCategoryAspectRatio($pageItem);
* array_push($data,$pageItem);
* }
* foreach ($data as $product) {
* $product->image = $this->getDefaultImage($product);
* $product->aspectRatio = $this->getCategoryAspectRatio($product);
* }
*/
// Les nombres de pages
$pages = array();
for($i=max($page-3, 1);$i<=min($page+3, $pagesCount) ;$i++){
array_push($pages,$i);
}
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'pages' => $pages,
'message' => 'Produits rcuprs avec succs.',
];
return $this->jsonCachedResponse($response, 120);
}
/**
* @Route("/api/new-products-dec", name="api_new_products_dec", options={"expose"=true}, methods={"GET"})
*/
public function newProductsDecAPI(Request $request): Response
{
$page = max(1, (int) $request->query->get('page', 1));
$pageSize = max(1, (int) $request->query->get('pageSize', 12));
$orderBy = (int) $request->query->get('orderBy', 6);
//Les Filtres de recherche
$tailles = preg_split('@,@', $request->query->get('tailles'), NULL, PREG_SPLIT_NO_EMPTY);
$couleurs = preg_split('@,@', $request->query->get('couleurs'), NULL, PREG_SPLIT_NO_EMPTY);
$maxPrice = (float) $request->query->get('maxPrice', $this->getCatalogFilterMaxPrice());
if ($maxPrice <= 0) {
$maxPrice = $this->getCatalogFilterMaxPrice();
}
$minPrice = floatval($request->query->get('minPrice')) ?: 0;
$newProductDays = $this->getNewProductDaysThreshold();
$date = new \DateTime('now');
$date->modify(sprintf('-%d day', $newProductDays));
// get the product dec repository
$produits = $this->em->getRepository(ProduitDeclinationValue::class);
// build the query for the doctrine paginator
$query = $produits->createQueryBuilder('d');
// build the query for the doctrine paginator
$query->innerJoin('App\Entity\Produit', 'p', 'with', 'd.produit=p')
->where('p.createdAt >= :date')
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->setParameter('date', $date)
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('maxPrice', $maxPrice)
->setParameter('minPrice', $minPrice)
->groupBy('p.id,v.id');
// Appliquer les filtres de tailles si ils sont ne pas appliqus
if( !$tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere('v.declination=2');
}
// Appliquer les filtres de tailles si ils sont appliqus
if( $tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles)")
->setParameter('tailles', $tailles);
}
// Appliquer les filtres de couleurs si ils sont appliqus seulement
if( $couleurs && !$tailles) {
$declination = $this->em->getRepository(Declination::class)->find(2);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
->setParameter('couleurs', $couleurs);
}
// Appliquer les filtres de tailles et couleurs appliqus les deux
if( $tailles && $couleurs) {
$declinationTaille = $this->em->getRepository(Declination::class)->find(1);
$declinationCouleur = $this->em->getRepository(Declination::class)->find(2);
// Fusionner les deux filtres en un seul
$filtres = array_merge($tailles, $couleurs);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
->setParameter('couleurs', $couleurs)
->setParameter('tailles', $tailles)
->groupBy('v.declination')
->having('v.declination=2');
}
// Order by
switch ($orderBy) {
case 1:
$query->orderBy('p.name', 'ASC');
$query->addOrderBy('d.id', 'ASC');
break;
case 2:
$query->orderBy('p.name', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
case 3:
$query->orderBy('p.price_ttc', 'ASC');
$query->addOrderBy('d.id', 'ASC');
break;
case 4:
$query->orderBy('p.price_ttc', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
case 5:
$query->orderBy('p.createdAt', 'ASC');
$query->addOrderBy('d.id', 'ASC');
break;
case 6:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
default:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
}
$this->applyDeclinationCardPreloads($query, 'd', 'p');
$query->getQuery();
// load doctrine Paginator
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
// you can get total items
$totalItems = count($paginator);
// get total pages
$pagesCount = ceil($totalItems / $pageSize);
// now get one page's items:
$paginator
->getQuery()
->setFirstResult($pageSize * ($page-1)) // set the offset
->setMaxResults($pageSize); // set the limit
$data= array();
foreach ($paginator as $pageItem) {
// do stuff with results...
$pageItem->setImage($this->getDefaultImage(null, $pageItem));
$pageItem->aspectRatio = $this->getCategoryAspectRatio($pageItem);
array_push($data,$pageItem);
}
// Les nombres de pages
$pages = array();
for($i=max($page-3, 1);$i<=min($page+3, $pagesCount) ;$i++){
array_push($pages,$i);
}
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'pages' => $pages,
'message' => 'Produits rcuprs avec succs.',
];
return new JsonResponse($response);
}
/**
* @Route("/api/top-products", name="api_top_products", options={"expose"=true}, methods={"GET"})
*/
public function topProductsAPI(Request $request): Response
{
$requestedMode = strtolower(trim((string) $request->query->get('mode', '')));
if ($requestedMode === 'declination') {
return $this->topProductsDecAPI($request);
}
$page = max(1, (int) $request->query->get('page', 1));
$pageSize = max(1, (int) $request->query->get('pageSize', 12));
$orderBy = (int) $request->query->get('orderBy', 6);
$minPrice = floatval($request->query->get('minPrice', 0));
$maxPrice = (float) $request->query->get('maxPrice', $this->getCatalogFilterMaxPrice());
if ($maxPrice <= 0) {
$maxPrice = $this->getCatalogFilterMaxPrice();
}
$days = $request->query->get('days');
$defaultDays = (int) $this->websiteSettingService->get('topSalesPeriod', 30);
if ($defaultDays <= 0) {
$defaultDays = 30;
}
$days = is_numeric($days) ? (int) $days : $defaultDays;
if ($days <= 0) {
$days = $defaultDays;
}
$date = new \DateTime('now');
$date->modify(sprintf('-%d day', $days));
$query = $this->em->createQueryBuilder();
$query->select('p')
->addSelect('COALESCE(SUM(c.quantity), 0) AS HIDDEN salesQty')
->from(Produit::class, 'p')
->join(ProduitDeclinationValue::class, 'd', 'WITH', 'd.produit = p')
->join('App\Entity\DocumentDeclinationProduit', 'c', 'WITH', 'c.produitDeclinationValue = d')
->join('c.document', 'doc')
->leftJoin('p.promotion', 'promo')
->addSelect('promo')
->where('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->andWhere('c.createdAt >= :date')
->andWhere('doc.category = :documentCategory')
->andWhere('doc.type = :documentType')
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('date', $date)
->setParameter('documentCategory', 'client')
->setParameter('documentType', 'commande')
->setParameter('minPrice', $minPrice)
->setParameter('maxPrice', $maxPrice)
->groupBy('p.id');
$query->orderBy('salesQty', 'DESC');
$query->addOrderBy('p.createdAt', 'DESC');
$query->addOrderBy('p.id', 'DESC');
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
$totalItems = count($paginator);
$pagesCount = (int) ceil($totalItems / $pageSize);
$paginator->getQuery()
->setFirstResult($pageSize * ($page - 1))
->setMaxResults($pageSize);
$pageProducts = iterator_to_array($paginator, false);
$hydratedProducts = $this->preloadProductsForCards($pageProducts);
$data = [];
foreach ($pageProducts as $product) {
$productId = (int) $product->getId();
if (!isset($hydratedProducts[$productId])) {
continue;
}
$data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
}
/*
* Ancien code conserve:
* $data = [];
* foreach ($paginator as $product) {
* $product->image = $this->getDefaultImage($product);
* $data[] = $product;
* }
*/
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'message' => 'Top ventes rcupres avec succs.'
];
return $this->jsonCachedResponse($response, 120);
}
private function topProductsDecAPI(Request $request): Response
{
$page = max(1, (int) $request->query->get('page', 1));
$pageSize = max(1, (int) $request->query->get('pageSize', 12));
$minPrice = (float) $request->query->get('minPrice', 0);
$maxPrice = (float) $request->query->get('maxPrice', $this->getCatalogFilterMaxPrice());
if ($maxPrice <= 0) {
$maxPrice = $this->getCatalogFilterMaxPrice();
}
$days = $request->query->get('days');
$defaultDays = (int) $this->websiteSettingService->get('topSalesPeriod', 30);
if ($defaultDays <= 0) {
$defaultDays = 30;
}
$days = is_numeric($days) ? (int) $days : $defaultDays;
if ($days <= 0) {
$days = $defaultDays;
}
$date = new \DateTime('now');
$date->modify(sprintf('-%d day', $days));
$qb = $this->em->createQueryBuilder();
$qb->select('d')
->addSelect('COALESCE(SUM(c.quantity), 0) AS HIDDEN salesQty')
->from(ProduitDeclinationValue::class, 'd')
->innerJoin('d.produit', 'p')
->innerJoin('App\Entity\DocumentDeclinationProduit', 'c', 'WITH', 'c.produitDeclinationValue = d')
->innerJoin('c.document', 'doc')
->where('p.deletedAt IS NULL')
->andWhere('p.showInWebSite = 1')
->andWhere('c.createdAt >= :date')
->andWhere('doc.category = :documentCategory')
->andWhere('doc.type = :documentType')
->andWhere('p.price_ttc BETWEEN :minPrice AND :maxPrice')
->setParameter('date', $date)
->setParameter('documentCategory', 'client')
->setParameter('documentType', 'commande')
->setParameter('minPrice', $minPrice)
->setParameter('maxPrice', $maxPrice)
->groupBy('d.id')
->orderBy('salesQty', 'DESC')
->addOrderBy('p.createdAt', 'DESC')
->addOrderBy('d.id', 'DESC');
$this->applyDeclinationCardPreloads($qb, 'd', 'p');
$rows = $qb->getQuery()->getResult();
$grouped = [];
$order = [];
$mixedAspectRatio = (float) $this->websiteSettingService->get('mixedAspectRatio', 0.8);
foreach ($rows as $dec) {
if (!$dec instanceof ProduitDeclinationValue) {
continue;
}
$product = $dec->getProduit();
if (!$product) {
continue;
}
$rawMainValue = null;
$sizeValue = null;
foreach ($dec->getGroupDeclinationValues() as $gdv) {
$decl = $gdv->getDeclination();
if (!$decl) {
continue;
}
if ((int) $decl->getPosition() === 1) {
$rawMainValue = $gdv->getValue();
}
if ((int) $decl->getPosition() === 2) {
$sizeValue = $gdv->getValue();
}
}
if (!$rawMainValue) {
continue;
}
$mainValue = (method_exists($rawMainValue, 'getParent') && $rawMainValue->getParent())
? $rawMainValue->getParent()
: $rawMainValue;
$key = (int) $product->getId() . '_' . (int) $mainValue->getId();
$pictures = [];
$selectedImage = null;
foreach ($dec->getPicture() as $pic) {
$imgName = $pic->getImageName();
if (!$imgName) {
continue;
}
$pictures[] = $imgName;
if ($selectedImage === null && method_exists($pic, 'getIsSelected') && $pic->getIsSelected()) {
$selectedImage = $imgName;
}
}
$img = $selectedImage ?: (!empty($pictures) ? $pictures[0] : null);
if (!$img) {
$img = $product->getImage() ?: 'no-image.jpg';
}
$hoverImage = null;
foreach ($pictures as $p) {
if ($p !== $img) {
$hoverImage = $p;
break;
}
}
if (!isset($grouped[$key])) {
$grouped[$key] = [
'id' => (int) $dec->getId(),
'idProduit' => (int) $product->getId(),
'productId' => (int) $product->getId(),
'name' => trim($product->getName() . ' ' . $mainValue->getName()),
'reference' => $product->getReference(),
'image' => $img,
'hoverImage' => $hoverImage,
'priceTTC' => (float) $product->getPriceTtc(),
'ratingScore' => (float) ($product->getRatingScore() ?? 0),
'ratingCount' => (int) ($product->getRatingCount() ?? 0),
'stock' => false,
'aspectRatio' => $mixedAspectRatio,
'colorSwatches' => [],
'activeSizes' => [],
'promo' => $product->getPromotion(),
];
$order[] = $key;
}
$qty = method_exists($dec, 'getQtyAvailableForWebsite') ? (int) $dec->getQtyAvailableForWebsite() : 0;
$sizeRow = [
'id' => (int) $dec->getId(),
'idDec' => (int) $dec->getId(),
'label' => $sizeValue ? (string) $sizeValue->getName() : (string) $dec->getName(),
'qty' => $qty,
];
$swatch = [
'id' => (int) $mainValue->getId(),
'name' => (string) $mainValue->getName(),
'code' => method_exists($mainValue, 'getCode') ? ((string) $mainValue->getCode() ?: null) : null,
'image' => $img,
'hoverImage' => $hoverImage,
'idDec' => (int) $dec->getId(),
'sizes' => [$sizeRow],
];
$existingSwatchIndex = null;
foreach ($grouped[$key]['colorSwatches'] as $index => $existingSwatch) {
if ((int) ($existingSwatch['id'] ?? 0) === (int) $swatch['id']) {
$existingSwatchIndex = $index;
break;
}
}
if ($existingSwatchIndex === null) {
$grouped[$key]['colorSwatches'][] = $swatch;
} else {
$grouped[$key]['colorSwatches'][$existingSwatchIndex]['sizes'][] = $sizeRow;
}
$grouped[$key]['activeSizes'][] = $sizeRow;
if ($qty > 0) {
$grouped[$key]['stock'] = true;
}
}
$cards = [];
foreach ($order as $key) {
$cards[] = $grouped[$key];
}
$totalItems = count($cards);
$pagesCount = (int) ceil($totalItems / $pageSize);
$data = array_slice($cards, $pageSize * ($page - 1), $pageSize);
return $this->jsonCachedResponse([
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'message' => 'Top ventes recuperees avec succes.',
], 120);
}
/**
* @Route("/api/top-rated-products", name="api_top_rated_products", options={"expose"=true}, methods={"GET"})
*/
public function topRatedProductsAPI(Request $request): Response
{
$page = max(1, (int) $request->query->get('page', 1));
$pageSize = max(1, (int) $request->query->get('pageSize', 12));
$minPrice = floatval($request->query->get('minPrice', 0));
$maxPrice = (float) $request->query->get('maxPrice', $this->getCatalogFilterMaxPrice());
if ($maxPrice <= 0) {
$maxPrice = $this->getCatalogFilterMaxPrice();
}
$query = $this->em->createQueryBuilder();
$query->select('p')
->from(Produit::class, 'p')
->leftJoin('p.promotion', 'promo')
->addSelect('promo')
->where('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->andWhere('p.ratingCount > 0')
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('minPrice', $minPrice)
->setParameter('maxPrice', $maxPrice)
->orderBy('p.ratingScore', 'DESC')
->addOrderBy('p.ratingCount', 'DESC')
->addOrderBy('p.createdAt', 'DESC')
->addOrderBy('p.id', 'DESC');
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
$totalItems = count($paginator);
$pagesCount = (int) ceil($totalItems / $pageSize);
$paginator->getQuery()
->setFirstResult($pageSize * ($page - 1))
->setMaxResults($pageSize);
$pageProducts = iterator_to_array($paginator, false);
$hydratedProducts = $this->preloadProductsForCards($pageProducts);
$data = [];
foreach ($pageProducts as $product) {
$productId = (int) $product->getId();
if (!isset($hydratedProducts[$productId])) {
continue;
}
$data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
}
/*
* Ancien code conserve:
* $data = [];
* foreach ($paginator as $product) {
* $product->image = $this->getDefaultImage($product);
* $product->aspectRatio = $this->getCategoryAspectRatio($product);
* $data[] = $product;
* }
*/
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'message' => 'Produits top rated rcuprs avec succs.'
];
return $this->jsonCachedResponse($response, 120);
}
/**
* @Route("/promotion", name="promo_products")
*/
public function promoProducts(): Response
{
return $this->render('front/product/listProducts.html.twig');
}
/**
* @Route("/promotion-dec", name="promo_products_dec")
*/
public function promoProductsDec(): Response
{
return $this->render('front/dec/promoProductsDec.html.twig');
}
/**
* @Route("/api/promotion", name="api_promo_products", options={"expose"=true}, methods={"GET"})
*/
public function promoProductsAPI(Request $request): Response
{
$requestedMode = strtolower(trim((string) $request->query->get('mode', '')));
if ($requestedMode === 'declination') {
return $this->promoProductsAPIDec($request);
}
$page = max(1, (int) $request->query->get('page', 1));
$pageSize = max(1, (int) $request->query->get('pageSize', 12));
$orderBy = (int) $request->query->get('orderBy', 6);
//Les Filtres de recherche
$tailles = preg_split('@,@', $request->query->get('tailles'), NULL, PREG_SPLIT_NO_EMPTY);
$couleurs = preg_split('@,@', $request->query->get('couleurs'), NULL, PREG_SPLIT_NO_EMPTY);
$maxPrice = (float) $request->query->get('maxPrice', $this->getCatalogFilterMaxPrice());
if ($maxPrice <= 0) {
$maxPrice = $this->getCatalogFilterMaxPrice();
}
$minPrice = floatval($request->query->get('minPrice')) ?: 0;
// Rcuprer la date encours
$date = new \DateTime('now');
$query = $this->em->createQueryBuilder();
$query->add('select', 'p')
->from('App\Entity\Produit', 'p')
->join('App\Entity\Promotion', 'c', 'with', 'p.promotion=c')
->where('p.promotion is not null')
->andWhere('c.startAt <= :date')
->andWhere('c.endAt >= :date')
->setParameter('date', $date)
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('maxPrice', $maxPrice)
->setParameter('minPrice', $minPrice);
// Appliquer les filtres de tailles si ils sont appliqus
if( $tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles)")
->andWhere('v.declination = :declination')
->setParameter('declination', $declination)
->setParameter('tailles', $tailles);
}
// Appliquer les filtres de couleurs si ils sont appliqus seulement
if( $couleurs && !$tailles) {
$declination = $this->em->getRepository(Declination::class)->find(2);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
->andWhere('v.declination = :declination')
->setParameter('declination', $declination)
->setParameter('couleurs', $couleurs);
}
// Appliquer les filtres de tailles et couleurs appliqus les deux
if( $tailles && $couleurs) {
$declinationTaille = $this->em->getRepository(Declination::class)->find(1);
$declinationCouleur = $this->em->getRepository(Declination::class)->find(2);
// Fusionner les deux filtres en un seul
$filtres = array_merge($tailles, $couleurs);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
/* ->andWhere('v.declination = :declinationTaille OR v.declination = :declinationCouleur')
->setParameter('declinationTaille', $declinationTaille)*/
->setParameter('couleurs', $couleurs)
->setParameter('tailles', $tailles);
}
// Order by
switch ($orderBy) {
case 1:
$query->orderBy('p.name', 'ASC');
$query->addOrderBy('p.id', 'ASC');
break;
case 2:
$query->orderBy('p.name', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
case 3:
$query->orderBy('p.price_ttc', 'ASC');
$query->addOrderBy('p.id', 'ASC');
break;
case 4:
$query->orderBy('p.price_ttc', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
case 5:
$query->orderBy('p.createdAt', 'ASC');
$query->addOrderBy('p.id', 'ASC');
break;
case 6:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
default:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('p.id', 'DESC');
break;
}
// load doctrine Paginator
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
// you can get total items
$totalItems = count($paginator);
// get total pages
$pagesCount = ceil($totalItems / $pageSize);
// now get one page's items:
$paginator
->getQuery()
->setFirstResult($pageSize * ($page - 1)) // set the offset
->setMaxResults($pageSize); // set the limit
$pageProducts = iterator_to_array($paginator, false);
$hydratedProducts = $this->preloadProductsForCards($pageProducts);
$data = [];
foreach ($pageProducts as $pageItem) {
$productId = (int) $pageItem->getId();
if (!isset($hydratedProducts[$productId])) {
continue;
}
$data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
}
/*
* Ancien code conserve:
* $data = array();
* foreach ($paginator as $pageItem) {
* array_push($data, $pageItem);
* }
* foreach ($data as $product) {
* $product->image = $this->getDefaultImage($product);
* }
*/
// Les nombres de pages
$pages = array();
for ($i = max($page - 3, 1); $i <= min($page + 3, $pagesCount); $i++) {
array_push($pages, $i);
}
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'pages' => $pages,
'message' => 'Produits rcuprs avec succs.',
];
return $this->jsonCachedResponse($response, 120);
}
/**
* @Route("/api/promotion-dec", name="api_promo_products_dec", options={"expose"=true}, methods={"GET"})
*/
public function promoProductsAPIDec(Request $request): Response
{
$page = max(1, (int) $request->query->get('page', 1));
$pageSize = max(1, (int) $request->query->get('pageSize', 12));
$orderBy = (int) $request->query->get('orderBy', 6);
//Les Filtres de recherche
$tailles = preg_split('@,@', $request->query->get('tailles'), NULL, PREG_SPLIT_NO_EMPTY);
$couleurs = preg_split('@,@', $request->query->get('couleurs'), NULL, PREG_SPLIT_NO_EMPTY);
$maxPrice = (float) $request->query->get('maxPrice', $this->getCatalogFilterMaxPrice());
if ($maxPrice <= 0) {
$maxPrice = $this->getCatalogFilterMaxPrice();
}
$minPrice = floatval($request->query->get('minPrice')) ?: 0;
// Rcuprer la date encours
$date = new \DateTime('now');
//$query = $this->em->createQueryBuilder();
$produits = $this->em->getRepository(ProduitDeclinationValue::class);
// build the query for the doctrine paginator
$query = $produits->createQueryBuilder('d');
$query->innerJoin('App\Entity\Produit', 'p', 'with', 'd.produit=p')
->innerJoin('App\Entity\Promotion', 'c', 'with', 'p.promotion=c')
->where('p.promotion is not null')
->andWhere('c.startAt <= :date')
->andWhere('c.endAt >= :date')
->setParameter('date', $date)
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('maxPrice', $maxPrice)
->setParameter('minPrice', $minPrice)
->groupBy('p.id,v.id');;
// Appliquer les filtres de tailles si ils sont ne pas appliqus
if( !$tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere('v.declination=2');
}
// Appliquer les filtres de tailles si ils sont appliqus
if( $tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles)")
->setParameter('tailles', $tailles);
}
// Appliquer les filtres de couleurs si ils sont appliqus seulement
if( $couleurs && !$tailles) {
$declination = $this->em->getRepository(Declination::class)->find(2);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
->setParameter('couleurs', $couleurs);
}
// Appliquer les filtres de tailles et couleurs appliqus les deux
if( $tailles && $couleurs) {
$declinationTaille = $this->em->getRepository(Declination::class)->find(1);
$declinationCouleur = $this->em->getRepository(Declination::class)->find(2);
// Fusionner les deux filtres en un seul
$filtres = array_merge($tailles, $couleurs);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
->setParameter('couleurs', $couleurs)
->setParameter('tailles', $tailles)
->groupBy('v.declination')
->having('v.declination=2');
}
// Order by
switch ($orderBy) {
case 1:
$query->orderBy('p.name', 'ASC');
$query->addOrderBy('d.id', 'ASC');
break;
case 2:
$query->orderBy('p.name', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
case 3:
$query->orderBy('p.price_ttc', 'ASC');
$query->addOrderBy('d.id', 'ASC');
break;
case 4:
$query->orderBy('p.price_ttc', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
case 5:
$query->orderBy('p.createdAt', 'ASC');
$query->addOrderBy('d.id', 'ASC');
break;
case 6:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
default:
$query->orderBy('p.createdAt', 'DESC');
$query->addOrderBy('d.id', 'DESC');
break;
}
$this->applyDeclinationCardPreloads($query, 'd', 'p');
// load doctrine Paginator
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
// you can get total items
$totalItems = count($paginator);
// get total pages
$pagesCount = ceil($totalItems / $pageSize);
// now get one page's items:
$paginator
->getQuery()
->setFirstResult($pageSize * ($page - 1)) // set the offset
->setMaxResults($pageSize); // set the limit
$data = array();
foreach ($paginator as $pageItem) {
// do stuff with results...
$pageItem->setImage($this->getDefaultImage(null, $pageItem));
array_push($data,$pageItem);
}
// Les nombres de pages
$pages = array();
for ($i = max($page - 3, 1); $i <= min($page + 3, $pagesCount); $i++) {
array_push($pages, $i);
}
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'pages' => $pages,
'message' => 'Produits rcuprs avec succs.',
];
return new JsonResponse($response);
}
/**
* Suggestions de recherche (header)
* - Produits: ref commence par / nom contient
* - Catgories: nom contient (pour la colonne "suggestions")
*
* @Route("/api/header-search-suggest", name="api_header_search_suggest", options={"expose"=true}, methods={"GET"})
*/
public function headerSearchSuggest(Request $request): JsonResponse
{
$q = trim((string) $request->query->get('q', ''));
$idCategory = (int) $request->query->get('idCategory', 0);
$normalizedQ = mb_strtolower($q);
if (mb_strlen($q) < 2) {
return new JsonResponse(['products' => [], 'categories' => [], 'references' => []]);
}
// --------- Catgories slectionnes (si filtre) + enfants ---------
$categoriesFilter = [];
if ($idCategory > 0) {
$cat = $this->em->getRepository(Category::class)->find($idCategory);
if ($cat) {
$categoriesFilter[] = $cat;
foreach ($cat->getSubCategories() as $child) {
$categoriesFilter[] = $child;
foreach ($child->getSubCategories() as $sub) {
$categoriesFilter[] = $sub;
}
}
}
}
// --------- Produits ---------
$qb = $this->em->getRepository(Produit::class)->createQueryBuilder('p');
$qb->andWhere('p.deletedAt IS NULL')
->andWhere('p.showInWebSite = 1')
->andWhere('(p.reference LIKE :refStart OR p.reference LIKE :refContains OR p.name LIKE :nameContains)')
->setParameter('refStart', $q . '%')
->setParameter('refContains', '%' . $q . '%')
->setParameter('nameContains', '%' . $q . '%')
->orderBy('p.createdAt', 'DESC')
->setMaxResults(12);
if (!empty($categoriesFilter)) {
$qb->andWhere('p.categories IN (:cats)')
->setParameter('cats', $categoriesFilter);
}
$products = $qb->getQuery()->getResult();
usort($products, function (Produit $a, Produit $b) use ($normalizedQ) {
$scoreA = $this->getSearchSuggestionScore($a, $normalizedQ);
$scoreB = $this->getSearchSuggestionScore($b, $normalizedQ);
if ($scoreA !== $scoreB) {
return $scoreA < $scoreB ? 1 : -1;
}
$dateA = $a->getCreatedAt() ? $a->getCreatedAt()->getTimestamp() : 0;
$dateB = $b->getCreatedAt() ? $b->getCreatedAt()->getTimestamp() : 0;
if ($dateA !== $dateB) {
return $dateB <=> $dateA;
}
return $b->getId() <=> $a->getId();
});
$now = new \DateTime(); // mme style que ton admin
$productsPayload = [];
$referencesPayload = [];
$seenReferences = [];
foreach ($products as $p) {
// image via ton trait
$img = $this->getDefaultImage($p);
// ===== Promo (sans changer l'existant, on enrichit) =====
$priceWithoutPromo = (float) $p->getPriceTtc();
$finalPrice = $priceWithoutPromo;
$inPromo = false;
$promoLabel = null;
$promotionType = null;
$value = null;
$promotion = $p->getPromotion();
if ($promotion) {
// on garde la logique "promo active entre start/end"
if ($promotion->getStartAt() <= $now && $promotion->getEndAt() >= $now) {
$value = (float) $promotion->getDiscountValue();
$promotionType = $promotion->getDiscountType(); // 'percent' ou autre (montant)
if ($promotionType === 'percent') {
$finalPrice = round($priceWithoutPromo * (1 - $value / 100), 3);
$promoLabel = '-' . rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.') . '%';
} else {
// remise fixe
$finalPrice = round($priceWithoutPromo - $value, 3);
if ($finalPrice < 0) { $finalPrice = 0.0; }
$promoLabel = '-' . rtrim(rtrim(number_format($value, 3, '.', ''), '0'), '.') . ' TND';
}
// scurit : promo relle
if ($finalPrice < $priceWithoutPromo && $priceWithoutPromo > 0) {
$inPromo = true;
} else {
// sinon on annule pour ne pas envoyer un "faux promo"
$finalPrice = $priceWithoutPromo;
$inPromo = false;
$promoLabel = null;
$promotionType = null;
$value = null;
}
}
}
// ===== Payload (on garde les cls existantes) =====
$productsPayload[] = [
'id' => $p->getId(),
'reference' => $p->getReference(),
'name' => $p->getName(),
'price' => $finalPrice,
'image' => $img ?: null,
'category' => $p->getCategories() ? $p->getCategories()->getName() : null,
'in_promo' => $inPromo,
'price_without_promo'=> $priceWithoutPromo,
'promo_label' => $promoLabel,
'promotionType' => $promotionType,
'value' => $value,
'aspectRatio' => $this->getMixedAspectRatioFallback(),
'is_new' => $p->isNew(),
'match_type' => $this->resolveSuggestionMatchType($p, $normalizedQ),
];
$reference = trim((string) $p->getReference());
if ($reference !== '' && !isset($seenReferences[$reference]) && str_contains(mb_strtolower($reference), $normalizedQ)) {
$seenReferences[$reference] = true;
$referencesPayload[] = [
'id' => $p->getId(),
'reference' => $reference,
'name' => $p->getName(),
];
}
}
// --------- Suggestions catgories (colonne droite) ---------
$catQb = $this->em->getRepository(Category::class)->createQueryBuilder('c');
$catQb->andWhere('c.name LIKE :cq')
->setParameter('cq', '%' . $q . '%')
->orderBy('c.name', 'ASC')
->setMaxResults(10);
$cats = $catQb->getQuery()->getResult();
$catsPayload = [];
foreach ($cats as $c) {
$catsPayload[] = [
'id' => $c->getId(),
'name' => $c->getName(),
];
}
return new JsonResponse([
'products' => $productsPayload,
'categories' => $catsPayload,
'references' => array_slice($referencesPayload, 0, 8),
]);
}
private function getSearchSuggestionScore(Produit $product, string $normalizedQ): int
{
$reference = mb_strtolower((string) ($product->getReference() ?? ''));
$name = mb_strtolower((string) ($product->getName() ?? ''));
if ($reference !== '' && str_starts_with($reference, $normalizedQ)) {
return 300;
}
if ($name !== '' && str_starts_with($name, $normalizedQ)) {
return 220;
}
if ($reference !== '' && str_contains($reference, $normalizedQ)) {
return 180;
}
if ($name !== '' && str_contains($name, $normalizedQ)) {
return 140;
}
return 100;
}
private function resolveSuggestionMatchType(Produit $product, string $normalizedQ): string
{
$reference = mb_strtolower((string) ($product->getReference() ?? ''));
if ($reference !== '' && str_contains($reference, $normalizedQ)) {
return 'reference';
}
return 'product';
}
/**
* @Route("/search/{idCategory}/{search}", name="product_search", options={"expose"=true}, methods={"GET"})
*/
public function searchProducts(Request $request, int $idCategory, string $search, EntityManagerInterface $em): Response
{
$category = null;
if ($idCategory > 0) {
$category = $em->getRepository(Category::class)->find($idCategory);
}
return $this->render('front/product/listProducts.html.twig', [
'pageMode' => 'search',
'search' => $search,
'category_id' => $idCategory,
'categorie' => $category, // optionnel
'categories' => [], // si tu ne veux plus fournir la liste
]);
}
/**
* @Route("/search-dec/{idCategory}/{search}", name="product_search_dec", options={"expose"=true}, methods={"GET"})
*/
public function searchProductsDec(Request $request,$idCategory,$search): Response
{
$categories = [];
if($idCategory) {
$category = $this->em->getRepository(Category::class)->find($idCategory);
// Rcuprer les catgories fils
array_push($categories, $category);
foreach ($category->getSubCategories() as $child) {
array_push($categories, $child);
foreach ($child->getSubCategories() as $sub) {
array_push($categories, $sub);
}
}
}
return $this->render('front/dec/searchProductsDec.html.twig', [
'idCategory' => $idCategory,
'categories' => $categories,
'search' => $search,
]);
}
/**
* @Route("/api/product-search", name="api_product_search", options={"expose"=true}, methods={"GET"})
*/
public function serachProductsAPI(Request $request): Response
{
$category_id = $request->query->get('id');
$search = $request->query->get('search');
$page = $request->query->get('page');
$orderBy = $request->query->get('orderBy');
$categories = [];
$category = $this->em->getRepository(Category::class)->find($category_id);
if ($category){
// Rcuprer kes catgories fils
array_push($categories, $category);
foreach ($category->getSubCategories() as $child) {
array_push($categories, $child);
foreach ($child->getSubCategories() as $sub) {
array_push($categories, $sub);
}
}
}
//Les Filtres de recherche
$tailles = preg_split('@,@', $request->query->get('tailles'), NULL, PREG_SPLIT_NO_EMPTY);
$couleurs = preg_split('@,@', $request->query->get('couleurs'), NULL, PREG_SPLIT_NO_EMPTY);
$maxPrice = floatval($request->query->get('maxPrice'));
$minPrice = floatval($request->query->get('minPrice'));
// get the product repository
$produits = $this->em->getRepository(Produit::class);
// build the query for the doctrine paginator
$query = $produits->createQueryBuilder('p')
->where('p.name like :search OR p.description like :search OR p.reference like :search')
->setParameter('search', '%' . $search . '%')
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('maxPrice', $maxPrice)
->setParameter('minPrice', $minPrice);
// Si catgorie est slectionne
if( $category) {
$query->andWhere('p.categories in (:cat)')
->setParameter('cat', $categories);
}
// Appliquer les filtres de tailles si ils sont appliqus
if( $tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles)")
->andWhere('v.declination = :declination')
->setParameter('declination', $declination)
->setParameter('tailles', $tailles);
}
// Appliquer les filtres de couleurs si ils sont appliqus seulement
if( $couleurs && !$tailles) {
$declination = $this->em->getRepository(Declination::class)->find(2);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
->andWhere('v.declination = :declination')
->setParameter('declination', $declination)
->setParameter('couleurs', $couleurs);
}
// Appliquer les filtres de tailles et couleurs appliqus les deux
if( $tailles && $couleurs) {
$declinationTaille = $this->em->getRepository(Declination::class)->find(1);
$declinationCouleur = $this->em->getRepository(Declination::class)->find(2);
// Fusionner les deux filtres en un seul
$filtres = array_merge($tailles, $couleurs);
$query->innerJoin('App\Entity\ProduitDeclinationValue', 'd', 'with', 'd.produit=p')
->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
/* ->andWhere('v.declination = :declinationTaille OR v.declination = :declinationCouleur')
->setParameter('declinationTaille', $declinationTaille)*/
->setParameter('couleurs', $couleurs)
->setParameter('tailles', $tailles);
}
// Order by
switch ($orderBy) {
case 1:
$query->orderBy('p.name', 'ASC');
break;
case 2:
$query->orderBy('p.name', 'DESC');
break;
case 3:
$query->orderBy('p.price_ttc', 'ASC');
break;
case 4:
$query->orderBy('p.price_ttc', 'DESC');
break;
case 5:
$query->orderBy('p.createdAt', 'ASC');
break;
case 6:
$query->orderBy('p.createdAt', 'DESC');
break;
}
$query->getQuery();
// set page size
$pageSize = $request->query->get('pageSize');;
// load doctrine Paginator
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
// you can get total items
$totalItems = count($paginator);
// get total pages
$pagesCount = ceil($totalItems / $pageSize);
// now get one page's items:
$paginator
->getQuery()
->setFirstResult($pageSize * ($page-1)) // set the offset
->setMaxResults($pageSize); // set the limit
$data= array();
foreach ($paginator as $pageItem) {
// do stuff with results...
array_push($data, $pageItem);
}
foreach ($data as $product){
$product->image = $this->getDefaultImage( $product);
}
// Les nombres de pages
$pages = array();
for($i=max($page-3, 1);$i<=min($page+3, $pagesCount) ;$i++){
array_push($pages,$i);
}
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'pages' => $pages,
'message' => 'Produits rcuprs avec succs.',
];
return new JsonResponse($response);
}
/**
* @Route("/api/product-search-dec", name="api_product_search_dec", options={"expose"=true}, methods={"GET"})
*/
public function serachProductsDecAPI(Request $request): Response
{
$category_id = $request->query->get('id');
$search = $request->query->get('search');
$page = $request->query->get('page');
$orderBy = $request->query->get('orderBy');
$categories = [];
$category = $this->em->getRepository(Category::class)->find($category_id);
if ($category){
// Rcuprer kes catgories fils
array_push($categories, $category);
foreach ($category->getSubCategories() as $child) {
array_push($categories, $child);
foreach ($child->getSubCategories() as $sub) {
array_push($categories, $sub);
}
}
}
//Les Filtres de recherche
$tailles = preg_split('@,@', $request->query->get('tailles'), NULL, PREG_SPLIT_NO_EMPTY);
$couleurs = preg_split('@,@', $request->query->get('couleurs'), NULL, PREG_SPLIT_NO_EMPTY);
$maxPrice = floatval($request->query->get('maxPrice'));
$minPrice = floatval($request->query->get('minPrice'));
// get the product dec repository
$produits = $this->em->getRepository(ProduitDeclinationValue::class);
// build the query for the doctrine paginator
$query = $produits->createQueryBuilder('d');
// build the query for the doctrine paginator
$query->innerJoin('App\Entity\Produit', 'p', 'with', 'd.produit=p')
->where('p.name like :search OR p.description like :search OR p.reference like :search')
->setParameter('search', '%' . $search . '%')
->andWhere('p.deletedAt is null')
->andWhere('p.showInWebSite = 1')
->andWhere('p.price_ttc between :minPrice and :maxPrice')
->setParameter('maxPrice', $maxPrice)
->setParameter('minPrice', $minPrice)
->groupBy('p.id,v.id');
// Si catgorie est slectionne
if( $category) {
$query->andWhere('p.categories in (:cat)')
->setParameter('cat', $categories);
}
// Appliquer les filtres de tailles si ils sont ne pas appliqus
if( !$tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere('v.declination=2');
}
// Appliquer les filtres de tailles si ils sont appliqus
if( $tailles && !$couleurs) {
$declination = $this->em->getRepository(Declination::class)->find(1);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles)")
->setParameter('tailles', $tailles);
}
// Appliquer les filtres de couleurs si ils sont appliqus seulement
if( $couleurs && !$tailles) {
$declination = $this->em->getRepository(Declination::class)->find(2);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.id in (:couleurs) or v.parent in (:couleurs)")
->setParameter('couleurs', $couleurs);
}
// Appliquer les filtres de tailles et couleurs appliqus les deux
if( $tailles && $couleurs) {
$declinationTaille = $this->em->getRepository(Declination::class)->find(1);
$declinationCouleur = $this->em->getRepository(Declination::class)->find(2);
// Fusionner les deux filtres en un seul
$filtres = array_merge($tailles, $couleurs);
$query->innerJoin('App\Entity\GroupDeclinationValue', 'g', 'with', 'g.produitDeclination= d')
->innerJoin('App\Entity\ValueDeclination', 'v', 'with', 'v= g.value')
->andWhere("v.name in (:tailles) or v.id in (:couleurs) or v.parent in (:couleurs)")
->setParameter('couleurs', $couleurs)
->setParameter('tailles', $tailles)
->groupBy('v.declination')
->having('v.declination=2');
}
// Order by
switch ($orderBy) {
case 1:
$query->orderBy('p.name', 'ASC');
break;
case 2:
$query->orderBy('p.name', 'DESC');
break;
case 3:
$query->orderBy('p.price_ttc', 'ASC');
break;
case 4:
$query->orderBy('p.price_ttc', 'DESC');
break;
case 5:
$query->orderBy('p.createdAt', 'ASC');
break;
case 6:
$query->orderBy('p.createdAt', 'DESC');
break;
}
$query->getQuery();
// set page size
$pageSize = $request->query->get('pageSize');;
// load doctrine Paginator
$paginator = new \Doctrine\ORM\Tools\Pagination\Paginator($query);
// you can get total items
$totalItems = count($paginator);
// get total pages
$pagesCount = ceil($totalItems / $pageSize);
// now get one page's items:
$paginator
->getQuery()
->setFirstResult($pageSize * ($page-1)) // set the offset
->setMaxResults($pageSize); // set the limit
$data= array();
foreach ($paginator as $pageItem) {
// do stuff with results...
$pageItem->setImage($this->getDefaultImage(null, $pageItem));
array_push($data,$pageItem);
}
// Les nombres de pages
$pages = array();
for($i=max($page-3, 1);$i<=min($page+3, $pagesCount) ;$i++){
array_push($pages,$i);
}
$response = [
'res' => 'OK',
'data' => $data,
'pagesCount' => $pagesCount,
'total' => $totalItems,
'pages' => $pages,
'message' => 'Produits rcuprs avec succs.',
];
return new JsonResponse($response);
}
/**
* @Route("/good-plan", name="pack_products")
*/
public function goodPlan(WebsiteSettingService $websiteSettingService): Response
{
$defaultMode = (string) $websiteSettingService->get('catalogListMode', 'product');
$defaultMode = in_array($defaultMode, ['product', 'declination'], true) ? $defaultMode : 'product';
return $this->render('front/pages/packProducts.html.twig', [
'defaultMode' => $defaultMode,
]);
}
/**
* @Route("/good-plan/config/{id}/{slug}", name="pack_products_config", requirements={"id"="\d+"}, defaults={"slug"=""}, options={"expose"=true})
*/
public function goodPlanConfig(Pack $pack, WebsiteSettingService $websiteSettingService): Response
{
if ($pack->getIsArchived() === true || !$pack->getShowInWebSite()) {
throw $this->createNotFoundException('Pack introuvable.');
}
$defaultMode = (string) $websiteSettingService->get('catalogListMode', 'product');
$defaultMode = in_array($defaultMode, ['product', 'declination'], true) ? $defaultMode : 'product';
return $this->render('front/pages/packConfigure.html.twig', [
'defaultMode' => $defaultMode,
'selectedPackId' => $pack->getId(),
]);
}
/**
* @Route("/api/packs", name="front_pack_list_api", methods={"GET"}, options={"expose"=true})
*/
public function packListApi(): JsonResponse
{
$packs = $this->em->getRepository(Pack::class)->createQueryBuilder('p')
->andWhere('(p.isArchived = 0 OR p.isArchived IS NULL)')
->andWhere('p.showInWebSite = 1')
->orderBy('p.createdAt', 'DESC')
->addOrderBy('p.id', 'DESC')
->getQuery()
->getResult();
$data = [];
foreach ($packs as $pack) {
// Swatches (position 1) de tous les produits du pack
$swatchesMap = [];
foreach ($pack->getItems() as $item) {
$prod = $item->getProduit();
if (!$prod) continue;
foreach ($prod->getProduitDeclinationValues() as $decVal) {
foreach ($decVal->getGroupDeclinationValues() as $gdv) {
$decl = $gdv->getDeclination();
if (!$decl || (int)$decl->getPosition() !== 1) continue;
$val = $gdv->getValue();
if (!$val) continue;
$code = method_exists($val, 'getCode') ? $val->getCode() : null;
if (!$code && method_exists($val, 'getParent') && $val->getParent() && method_exists($val->getParent(), 'getCode')) {
$code = $val->getParent()->getCode();
}
$swatchesMap[(int)$val->getId()] = [
'id' => (int)$val->getId(),
'name' => (string)$val->getName(),
'code' => (string)($code ?? ''),
];
}
}
}
// Images pack associes une valeur position 1
$packImageByPos1 = [];
$links = $this->em->getRepository(PackImageDeclination::class)->createQueryBuilder('pid')
->join('pid.file', 'f')
->join('pid.valueDeclination', 'vd')
->where('pid.pack = :pack')
->setParameter('pack', $pack)
->getQuery()
->getResult();
foreach ($links as $link) {
$val = $link->getValueDeclination();
$file = $link->getFile();
if ($val && $file && $file->getImageName()) {
$packImageByPos1[(string)$val->getId()] = (string)$file->getImageName();
}
}
$data[] = [
'id' => $pack->getId(),
'reference' => $pack->getReference(),
'name' => $pack->getName(),
'description' => $pack->getDescription(),
'price_ttc' => (float)($pack->getPriceTtc() ?? 0),
'final_price_ttc' => (float)($pack->getFinalPriceTtc() ?? 0),
'remise' => (float)($pack->getRemise() ?? 0),
'picture' => $this->getPackMainImageName($pack),
'items_count' => $pack->getItems()->count(),
'createdAt' => $pack->getCreatedAt() ? $pack->getCreatedAt()->format(\DateTimeInterface::ATOM) : null,
'swatches' => array_values($swatchesMap),
'pack_image_by_pos1' => $packImageByPos1,
];
}
return new JsonResponse(['res' => 'OK', 'data' => $data]);
}
/**
* @Route("/api/pack/{id}/configurator", name="front_pack_configurator_api", methods={"GET"}, options={"expose"=true})
*/
public function packConfiguratorApi(Pack $pack): JsonResponse
{
if ($pack->getIsArchived() === true || !$pack->getShowInWebSite()) {
return new JsonResponse(['res' => 'ERROR', 'message' => 'Pack introuvable'], 404);
}
$items = [];
foreach ($pack->getItems() as $item) {
$p = $item->getProduit();
if (!$p) continue;
$items[] = [
'product_id' => $p->getId(),
'product_reference' => $p->getReference(),
'product_name' => $p->getName(),
'product_picture' => $this->getDefaultImage($p),
];
}
$links = $this->em->getRepository(PackImageDeclination::class)->createQueryBuilder('pid')
->join('pid.file', 'f')
->join('pid.valueDeclination', 'vd')
->where('pid.pack = :pack')
->setParameter('pack', $pack)
->getQuery()
->getResult();
$packImageByPos1 = [];
foreach ($links as $link) {
$val = $link->getValueDeclination();
$file = $link->getFile();
if (!$val || !$file || !$file->getImageName()) {
continue;
}
$packImageByPos1[(string)$val->getId()] = [
'id' => (string)$val->getId(),
'name' => (string)$val->getName(),
'picture' => (string)$file->getImageName(),
];
}
return new JsonResponse([
'res' => 'OK',
'data' => [
'id' => $pack->getId(),
'reference' => $pack->getReference(),
'name' => $pack->getName(),
'description' => $pack->getDescription(),
'price_ttc' => (float)($pack->getPriceTtc() ?? 0),
'final_price_ttc' => (float)($pack->getFinalPriceTtc() ?? 0),
'remise' => (float)($pack->getRemise() ?? 0),
'picture' => $this->getPackMainImageName($pack),
'items' => $items,
'pack_image_by_pos1' => $packImageByPos1
]
]);
}
private function getPackMainImageName(Pack $pack): string
{
foreach ($pack->getPicture() as $img) {
if ($img->getIsSelected()) return (string)$img->getImageName();
}
foreach ($pack->getPicture() as $img) {
return (string)$img->getImageName();
}
return 'no-image.jpg';
}
private function preloadProductsForCards(array $products): array
{
$ids = [];
foreach ($products as $product) {
if ($product instanceof Produit && $product->getId()) {
$ids[] = (int) $product->getId();
}
}
$ids = array_values(array_unique($ids));
if (!$ids) {
return [];
}
$rows = $this->em->getRepository(Produit::class)->createQueryBuilder('p')
->leftJoin('p.promotion', 'promo')->addSelect('promo')
->leftJoin('p.picture', 'picture')->addSelect('picture')
->leftJoin('p.produitDeclinationValues', 'dec')->addSelect('dec')
->leftJoin('dec.picture', 'decPicture')->addSelect('decPicture')
->leftJoin('dec.groupDeclinationValues', 'gdv')->addSelect('gdv')
->leftJoin('gdv.declination', 'decl')->addSelect('decl')
->leftJoin('gdv.value', 'value')->addSelect('value')
->leftJoin('value.parent', 'parent')->addSelect('parent')
->leftJoin('dec.stocks', 'stock')->addSelect('stock')
->where('p.id IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->getResult();
$map = [];
foreach ($rows as $row) {
$map[(int) $row->getId()] = $row;
}
return $map;
}
private function mapHomeProductCard(Produit $product): array
{
$swatches = $this->buildProductColorSwatches($product);
return [
'id' => (int) $product->getId(),
'idProduit' => (int) $product->getId(),
'productId' => (int) $product->getId(),
'name' => (string) $product->getName(),
'reference' => (string) ($product->getReference() ?? ''),
'priceTTC' => (float) ($product->getPriceTtc() ?? 0),
'image' => $this->resolveProductPrimaryImage($product),
'hoverImage' => $this->resolveProductHoverImage($product),
'aspectRatio' => $this->getCategoryAspectRatio($product),
'promo' => $this->mapPromotionData($product->getPromotion()),
'ratingScore' => (float) ($product->getRatingScore() ?? 0),
'ratingCount' => (int) ($product->getRatingCount() ?? 0),
'colorSwatches' => $swatches,
'activeSizes' => isset($swatches[0]['sizes']) && is_array($swatches[0]['sizes']) ? $swatches[0]['sizes'] : [],
'stock' => $this->hasProductStock($product),
];
}
private function loadHomeCategoryProducts(int $categoryId, string $mode, Request $request): array
{
$limit = (int) $this->websiteSettingService->get('homepageProductsPerTab', 12);
if ($limit <= 0) {
$limit = 12;
}
$category = $this->em->getRepository(Category::class)->find($categoryId);
if (!$category) {
return [];
}
$categories = [$category];
foreach ($category->getSubCategories() as $child) {
$categories[] = $child;
foreach ($child->getSubCategories() as $sub) {
$categories[] = $sub;
}
}
if ($mode === 'declination') {
$qb = $this->em->getRepository(ProduitDeclinationValue::class)->createQueryBuilder('d')
->innerJoin('d.produit', 'p')
->where('p.categories IN (:cats)')
->andWhere('p.deletedAt IS NULL')
->andWhere('p.showInWebSite = 1')
->setParameter('cats', $categories)
->orderBy('p.createdAt', 'DESC')
->addOrderBy('d.id', 'DESC');
$this->applyDeclinationCardPreloads($qb, 'd', 'p');
$rows = $qb->getQuery()->getResult();
return array_slice($this->buildHomePositionOneCards($rows), 0, $limit);
}
$products = $this->em->getRepository(Produit::class)->createQueryBuilder('p')
->where('p.categories IN (:cats)')
->andWhere('p.deletedAt IS NULL')
->andWhere('p.showInWebSite = 1')
->setParameter('cats', $categories)
->orderBy('p.createdAt', 'DESC')
->addOrderBy('p.id', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
$hydratedProducts = $this->preloadProductsForCards($products);
$data = [];
foreach ($products as $product) {
$productId = (int) $product->getId();
if (!isset($hydratedProducts[$productId])) {
continue;
}
$data[] = $this->mapHomeProductCard($hydratedProducts[$productId]);
}
return $data;
}
private function loadHomeCategoriesDataset(array $categories, string $mode): array
{
$limit = (int) $this->websiteSettingService->get('homepageProductsPerTab', 12);
if ($limit <= 0) {
$limit = 12;
}
$dataset = [];
$categoryScopes = [];
$topCategoryIdsByDescendantId = [];
$allScopeCategories = [];
foreach ($categories as $category) {
if (!$category instanceof Category) {
continue;
}
$topCategoryId = (int) $category->getId();
$scope = [$category];
foreach ($category->getSubCategories() as $child) {
$scope[] = $child;
foreach ($child->getSubCategories() as $sub) {
$scope[] = $sub;
}
}
$dataset[$topCategoryId] = (object) [
'category' => $category,
'products' => [],
];
$categoryScopes[$topCategoryId] = $scope;
foreach ($scope as $scopeCategory) {
if (!$scopeCategory instanceof Category) {
continue;
}
$scopeCategoryId = (int) $scopeCategory->getId();
$allScopeCategories[$scopeCategoryId] = $scopeCategory;
if (!isset($topCategoryIdsByDescendantId[$scopeCategoryId])) {
$topCategoryIdsByDescendantId[$scopeCategoryId] = [];
}
$topCategoryIdsByDescendantId[$scopeCategoryId][$topCategoryId] = $topCategoryId;
}
}
if (empty($dataset)) {
return [];
}
if ($mode === 'declination') {
$rows = $this->em->getRepository(ProduitDeclinationValue::class)->createQueryBuilder('d')
->innerJoin('d.produit', 'p')
->innerJoin('p.categories', 'c')
->where('c IN (:cats)')
->andWhere('p.deletedAt IS NULL')
->andWhere('p.showInWebSite = 1')
->setParameter('cats', array_values($allScopeCategories))
->orderBy('p.createdAt', 'DESC')
->addOrderBy('d.id', 'DESC')
->getQuery()
->getResult();
$cards = $this->buildHomePositionOneCards($rows);
$productTopCategoryMap = [];
foreach ($rows as $row) {
if (!$row instanceof ProduitDeclinationValue || !$row->getProduit()) {
continue;
}
$product = $row->getProduit();
$productId = (int) $product->getId();
if (isset($productTopCategoryMap[$productId])) {
continue;
}
$matchedTopIds = [];
foreach ($product->getCategories() as $productCategory) {
$productCategoryId = (int) $productCategory->getId();
foreach ($topCategoryIdsByDescendantId[$productCategoryId] ?? [] as $topCategoryId) {
$matchedTopIds[$topCategoryId] = $topCategoryId;
}
}
$productTopCategoryMap[$productId] = array_values($matchedTopIds);
}
$seenPerTopCategory = [];
foreach ($cards as $card) {
$productId = (int) ($card['idProduit'] ?? $card['productId'] ?? 0);
if ($productId <= 0) {
continue;
}
foreach ($productTopCategoryMap[$productId] ?? [] as $topCategoryId) {
if (!isset($dataset[$topCategoryId])) {
continue;
}
if (!isset($seenPerTopCategory[$topCategoryId])) {
$seenPerTopCategory[$topCategoryId] = [];
}
$cardId = (int) ($card['id'] ?? 0);
if ($cardId > 0 && isset($seenPerTopCategory[$topCategoryId][$cardId])) {
continue;
}
if (count($dataset[$topCategoryId]->products) >= $limit) {
continue;
}
$dataset[$topCategoryId]->products[] = $card;
if ($cardId > 0) {
$seenPerTopCategory[$topCategoryId][$cardId] = true;
}
}
}
return array_values($dataset);
}
$products = $this->em->getRepository(Produit::class)->createQueryBuilder('p')
->innerJoin('p.categories', 'c')
->where('c IN (:cats)')
->andWhere('p.deletedAt IS NULL')
->andWhere('p.showInWebSite = 1')
->setParameter('cats', array_values($allScopeCategories))
->orderBy('p.createdAt', 'DESC')
->addOrderBy('p.id', 'DESC')
->getQuery()
->getResult();
$dedupedProducts = [];
foreach ($products as $product) {
if (!$product instanceof Produit) {
continue;
}
$productId = (int) $product->getId();
if (!isset($dedupedProducts[$productId])) {
$dedupedProducts[$productId] = $product;
}
}
$hydratedProducts = $this->preloadProductsForCards(array_values($dedupedProducts));
$seenPerTopCategory = [];
foreach ($dedupedProducts as $productId => $product) {
$matchedTopIds = [];
foreach ($product->getCategories() as $productCategory) {
$productCategoryId = (int) $productCategory->getId();
foreach ($topCategoryIdsByDescendantId[$productCategoryId] ?? [] as $topCategoryId) {
$matchedTopIds[$topCategoryId] = $topCategoryId;
}
}
if (empty($matchedTopIds) || !isset($hydratedProducts[$productId])) {
continue;
}
$card = $this->mapHomeProductCard($hydratedProducts[$productId]);
foreach (array_values($matchedTopIds) as $topCategoryId) {
if (!isset($dataset[$topCategoryId])) {
continue;
}
if (!isset($seenPerTopCategory[$topCategoryId])) {
$seenPerTopCategory[$topCategoryId] = [];
}
if (isset($seenPerTopCategory[$topCategoryId][$productId])) {
continue;
}
if (count($dataset[$topCategoryId]->products) >= $limit) {
continue;
}
$dataset[$topCategoryId]->products[] = $card;
$seenPerTopCategory[$topCategoryId][$productId] = true;
}
}
return array_values($dataset);
}
private function buildProductColorSwatches(Produit $product): array
{
$swatches = [];
$indexByValueId = [];
foreach ($product->getProduitDeclinationValues() as $declinationValue) {
$colorValue = null;
$sizeValue = null;
foreach ($declinationValue->getGroupDeclinationValues() as $group) {
$declination = $group->getDeclination();
if (!$declination) {
continue;
}
if ((int) $declination->getPosition() === 1) {
$rawValue = $group->getValue();
$colorValue = $rawValue && method_exists($rawValue, 'getParent') && $rawValue->getParent()
? $rawValue->getParent()
: $rawValue;
}
if ((int) $declination->getPosition() === 2) {
$sizeValue = $group->getValue();
}
}
if (!$colorValue) {
continue;
}
$valueId = (int) $colorValue->getId();
if (!isset($indexByValueId[$valueId])) {
$images = $this->resolveDeclinationImages($declinationValue, $product);
$swatches[] = [
'id' => $valueId,
'code' => (string) ($colorValue->getCode() ?: ($colorValue->getParent() ? $colorValue->getParent()->getCode() : '')),
'name' => (string) $colorValue->getName(),
'image' => $images[0],
'hoverImage' => $images[1],
'idDec' => (int) $declinationValue->getId(),
'sizes' => [],
];
$indexByValueId[$valueId] = count($swatches) - 1;
}
$label = $sizeValue ? (string) $sizeValue->getName() : (string) $declinationValue->getName();
$swatches[$indexByValueId[$valueId]]['sizes'][] = [
'id' => (int) $declinationValue->getId(),
'idDec' => (int) $declinationValue->getId(),
'label' => $label,
'qty' => (int) $declinationValue->getQtyAvailableForWebsite(),
];
}
return $swatches;
}
private function hasProductStock(Produit $product): bool
{
foreach ($product->getProduitDeclinationValues() as $declinationValue) {
if ((int) $declinationValue->getQtyAvailableForWebsite() > 0) {
return true;
}
}
return ((int) $product->getAvailableQuantityForWebsite()) > 0;
}
private function resolveProductPrimaryImage(Produit $product): string
{
foreach ($product->getPicture() as $picture) {
if ($picture->getIsSelected() && $picture->getImageName()) {
return (string) $picture->getImageName();
}
}
foreach ($product->getPicture() as $picture) {
if ($picture->getImageName()) {
return (string) $picture->getImageName();
}
}
return $this->getDefaultImage($product);
}
private function resolveProductHoverImage(Produit $product): ?string
{
$primary = $this->resolveProductPrimaryImage($product);
foreach ($product->getPicture() as $picture) {
$imageName = $picture->getImageName();
if ($imageName && $imageName !== $primary) {
return (string) $imageName;
}
}
return null;
}
private function resolveDeclinationImages(ProduitDeclinationValue $declinationValue, Produit $product): array
{
$primary = null;
$hover = null;
$pictures = [];
foreach ($declinationValue->getPicture() as $picture) {
$imageName = $picture->getImageName();
if (!$imageName) {
continue;
}
$pictures[] = (string) $imageName;
if ($primary === null && $picture->getIsSelected()) {
$primary = (string) $imageName;
}
}
if ($primary === null && isset($pictures[0])) {
$primary = $pictures[0];
}
if ($primary === null) {
$primary = $this->resolveProductPrimaryImage($product);
}
foreach ($pictures as $picture) {
if ($picture !== $primary) {
$hover = $picture;
break;
}
}
return [$primary, $hover];
}
private function mapPromotionData(?Promotion $promotion): ?array
{
if (!$promotion || !$promotion->isValid()) {
return null;
}
return [
'id' => $promotion->getId(),
'discountType' => $promotion->getDiscountType(),
'discountValue' => (float) ($promotion->getDiscountValue() ?? 0),
'isValid' => true,
];
}
private function buildHomePositionOneCards(array $rows): array
{
$cards = [];
$order = [];
$productSwatches = [];
foreach ($rows as $dec) {
if (!$dec instanceof ProduitDeclinationValue) {
continue;
}
$product = $dec->getProduit();
if (!$product) {
continue;
}
$rawMainValue = null;
$sizeValue = null;
foreach ($dec->getGroupDeclinationValues() as $gdv) {
$decl = $gdv->getDeclination();
if (!$decl) {
continue;
}
if ((int) $decl->getPosition() === 1) {
$rawMainValue = $gdv->getValue();
}
if ((int) $decl->getPosition() === 2) {
$sizeValue = $gdv->getValue();
}
}
if (!$rawMainValue) {
continue;
}
$mainValue = (method_exists($rawMainValue, 'getParent') && $rawMainValue->getParent())
? $rawMainValue->getParent()
: $rawMainValue;
$productId = (int) $product->getId();
$mainValueId = (int) $mainValue->getId();
$mainCode = method_exists($mainValue, 'getCode') ? (string) $mainValue->getCode() : '';
$mainName = (string) $mainValue->getName();
$key = $productId . '_' . $mainValueId;
$pictures = [];
$selectedImage = null;
foreach ($dec->getPicture() as $pic) {
$imgName = $pic->getImageName();
if (!$imgName) {
continue;
}
$pictures[] = $imgName;
if ($selectedImage === null && method_exists($pic, 'getIsSelected') && $pic->getIsSelected()) {
$selectedImage = $imgName;
}
}
$img = $selectedImage ?: (!empty($pictures) ? $pictures[0] : null);
if (!$img) {
$img = $product->getImage() ?: 'no-image.jpg';
}
$hoverImage = null;
foreach ($pictures as $picture) {
if ($picture !== $img) {
$hoverImage = $picture;
break;
}
}
if (!isset($productSwatches[$productId])) {
$productSwatches[$productId] = [];
}
if (!isset($productSwatches[$productId][$mainValueId])) {
$productSwatches[$productId][$mainValueId] = [
'id' => $mainValueId,
'name' => $mainName,
'code' => $mainCode !== '' ? $mainCode : null,
'image' => $img,
'hoverImage' => $hoverImage,
'idDec' => (int) $dec->getId(),
'sizes' => [],
];
}
if (!isset($cards[$key])) {
$cards[$key] = [
'id' => (int) $dec->getId(),
'idProduit' => $productId,
'productId' => $productId,
'name' => trim($product->getName() . ' ' . $mainName),
'reference' => $product->getReference(),
'image' => $img,
'hoverImage' => $hoverImage,
'priceTTC' => (float) ($product->getPriceTtc() ?? 0),
'promo' => $product->getPromotion(),
'ratingScore' => (float) ($product->getRatingScore() ?? 0),
'ratingCount' => (int) ($product->getRatingCount() ?? 0),
'stock' => false,
'aspectRatio' => $this->getCategoryAspectRatio($dec),
'colorSwatches' => [],
'activeSizes' => [],
];
$order[] = $key;
}
$qty = method_exists($dec, 'getQtyAvailableForWebsite') ? (int) $dec->getQtyAvailableForWebsite() : 0;
$label = $sizeValue ? (string) $sizeValue->getName() : (string) $dec->getName();
$sizeRow = [
'id' => (int) $dec->getId(),
'idDec' => (int) $dec->getId(),
'label' => $label,
'qty' => $qty,
];
$cards[$key]['activeSizes'][] = $sizeRow;
$productSwatches[$productId][$mainValueId]['sizes'][] = $sizeRow;
if ($qty > 0) {
$cards[$key]['stock'] = true;
}
}
$grouped = [];
foreach ($order as $cardKey) {
$productId = (int) $cards[$cardKey]['productId'];
$cards[$cardKey]['colorSwatches'] = isset($productSwatches[$productId]) ? array_values($productSwatches[$productId]) : [];
$grouped[] = $cards[$cardKey];
}
return $grouped;
}
private function applyDeclinationCardPreloads(QueryBuilder $qb, string $declinationAlias, string $productAlias): void
{
$qb->addSelect($productAlias)
->leftJoin($productAlias . '.promotion', 'promo_preload')->addSelect('promo_preload')
->leftJoin($declinationAlias . '.picture', 'decl_picture_preload')->addSelect('decl_picture_preload')
->leftJoin($declinationAlias . '.groupDeclinationValues', 'gdv_preload')->addSelect('gdv_preload')
->leftJoin('gdv_preload.declination', 'decl_preload')->addSelect('decl_preload')
->leftJoin('gdv_preload.value', 'value_preload')->addSelect('value_preload')
->leftJoin('value_preload.parent', 'value_parent_preload')->addSelect('value_parent_preload');
}
private function jsonCachedResponse(array $payload, int $ttl = 120): JsonResponse
{
$response = new JsonResponse($payload);
$response->setPublic();
$response->setMaxAge($ttl);
$response->setSharedMaxAge($ttl);
$response->headers->addCacheControlDirective('stale-while-revalidate', min(30, $ttl));
return $response;
}
/**
* @Route("/who-are-we", name="who_are_we")
*/
public function whoAreWe(): Response
{
return $this->render('front/pages/whoAreWe.html.twig');
}
/**
* @Route("/delivery-information", name="delivery_information")
*/
public function deliveryInformation(): Response
{
return $this->render('front/pages/deliveryInformation.html.twig');
}
/**
* @Route("/return-and-exchange", name="return_and_exchange")
*/
public function returnAndExchange(): Response
{
return $this->render('front/pages/returnAndExchange.html.twig');
}
/**
* @Route("/question", name="question")
*/
public function question(): Response
{
return $this->render('front/pages/question.html.twig');
}
/**
* @Route("/size-guide", name="size_guide")
*/
public function sizeGuide(): Response
{
return $this->render('front/pages/sizeGuide.html.twig');
}
/**
* @Route("/terms-of-sales", name="terms_of_sales")
*/
public function termsOfSales(): Response
{
return $this->render('front/pages/termsOfSales.html.twig');
}
/**
* @Route("/privacy-policy", name="privacy_policy")
*/
public function privacyPolicy(): Response
{
return $this->render('front/pages/privacyPolicy.html.twig');
}
/**
* @Route("/data-deletion", name="data_deletion")
*/
public function dataDeletion(): Response
{
return $this->render('front/pages/dataDeletion.html.twig');
}
/**
* @Route("/terms", name="terms")
*/
public function terms(): Response
{
return $this->render('front/pages/terms.html.twig');
}
/**
* @Route("/contact", name="contact")
*/
public function contact(): Response
{
return $this->render('front/pages/contact.html.twig');
}
private function getCategoryAspectRatio(Produit|ProduitDeclinationValue|null $product): float
{
if (!$product) {
return 0.8;
}
if ($product instanceof ProduitDeclinationValue) {
$product = $product->getProduit();
}
$category = $product->getCategories();
if ($category && $category->getAspectRatio()) {
return $category->getAspectRatio();
}
return $this->getMixedAspectRatioFallback();
}
private function getMixedAspectRatioFallback(): float
{
$ratio = (float) $this->websiteSettingService->get('mixedAspectRatio', 0.8);
return $ratio > 0 ? $ratio : 0.8;
}
}