src/Repository/CategoryProductRepository.php line 106

Open in your IDE?
  1. <?php
  2. namespace App\Repository;
  3. use App\Entity\Category;
  4. use Doctrine\ORM\EntityRepository;
  5. class CategoryProductRepository extends EntityRepository
  6. {
  7. public function searchProducts($query, $category, $filters=[],$sort=[],$page=1,$limit=25)
  8. {
  9. $q = $this->createQueryBuilder('cp')
  10. ->innerJoin('cp.product','p')
  11. ->where('cp.category = :category')
  12. // ->andWhere('p.parent IS NULL')
  13. ->setParameter('category', $category);
  14. if(!empty($query)){
  15. if(ctype_digit($query)){
  16. $q->andWhere(
  17. $q->expr()->eq('p.id',$query)
  18. );
  19. }else{
  20. $q->andWhere(
  21. 'p.model LIKE \'%'.$query.'%\''
  22. );
  23. }
  24. }
  25. if(empty($sort)){
  26. $q->orderBy('p.id', 'desc');
  27. }else{
  28. // foreach($sort as $s){
  29. // $q->addOrderBy('p.'.$s->property, $s->direction);
  30. // }
  31. }
  32. $q->setMaxResults($limit);
  33. $q->setFirstResult(($page-1)*$limit);
  34. $q = $q->getQuery();
  35. $products = [];
  36. $cps = $q->getResult();
  37. foreach($cps as $cp){
  38. $products[] = $cp->getProduct();
  39. }
  40. return $products;
  41. }
  42. public function searchProductsCount($query, $category, $filters=[])
  43. {
  44. $q = $this->createQueryBuilder('cp')
  45. ->select('count(p.id)')
  46. ->innerJoin('cp.product','p')
  47. ->where('cp.category = :category')
  48. // ->andWhere('p.parent IS NULL')
  49. ->setParameter('category', $category);
  50. if(!empty($query)){
  51. if(ctype_digit($query)){
  52. $q->andWhere(
  53. $q->expr()->eq('p.id',$query)
  54. );
  55. }else{
  56. $q->andWhere(
  57. 'p.model LIKE \'%'.$query.'%\''
  58. );
  59. }
  60. }
  61. $q = $q->getQuery();
  62. return $q->getSingleScalarResult();
  63. }
  64. public function getCategoriesByProduct(\App\Entity\Product $product, $onlyActive=false, $limit=null, $page=1){
  65. $q = $this->createQueryBuilder('cp')
  66. ->join('cp.product', 'p')
  67. ->where('cp.product = :product')
  68. ->setParameter('product', $product);
  69. if($onlyActive){
  70. $q->andWhere('p.active = 1');
  71. }
  72. if($limit){
  73. $q->setMaxResults($limit);
  74. $q->setFirstResult(($page-1)*$limit);
  75. }
  76. $q = $q->getQuery();
  77. $results = $q->getResult();
  78. // dump($results);
  79. $output = [];
  80. foreach($results as $result){
  81. $output[] = $result->getCategory();
  82. }
  83. return $output;
  84. }
  85. /**
  86. * Returns one representative Product per category listing entry: products that belong to
  87. * a ProductModel are collapsed into a single entry for their top-level model, so a category
  88. * page never lists the same model's variants (colors, sizes, ...) as separate rows.
  89. *
  90. * $filters maps an Attribute code to a value (or an array of values). A model/product only
  91. * matches if one of its variants carries all of the requested attribute values.
  92. */
  93. /**
  94. * @param \App\Entity\Category|int|array $category a single category (or id), or a list of
  95. * categories/ids to list products from (e.g. a
  96. * category and all of its descendants).
  97. */
  98. public function getProductsByCategory($category, $onlyActive = false, $limit = null, $page = 1, $sorts = [], $filters = []){
  99. $representatives = $this->getGroupedRepresentatives($category, $onlyActive, $filters, $sorts);
  100. if($limit){
  101. $representatives = array_slice($representatives, ($page - 1) * $limit, $limit);
  102. }
  103. $ids = array_column($representatives, 'id');
  104. if(empty($ids)){
  105. return [];
  106. }
  107. $products = $this->getEntityManager()->getRepository(\App\Entity\Product::class)
  108. ->createQueryBuilder('p')
  109. ->where('p.id IN (:ids)')
  110. ->setParameter('ids', $ids)
  111. ->getQuery()
  112. ->getResult();
  113. $productsById = [];
  114. foreach($products as $product){
  115. $productsById[$product->getId()] = $product;
  116. }
  117. $output = [];
  118. foreach($ids as $id){
  119. if(isset($productsById[$id])){
  120. $output[] = $productsById[$id];
  121. }
  122. }
  123. return $output;
  124. }
  125. /**
  126. * @param \App\Entity\Category|int|array $category see getProductsByCategory()
  127. */
  128. public function getProductCountByCategory($category, $onlyActive = false, $filters = []){
  129. return count($this->getGroupedRepresentatives($category, $onlyActive, $filters));
  130. }
  131. /**
  132. * Fetches every (product, model) pair matching the category/filters, groups them by their
  133. * top-level ProductModel (or by product id when there is no model), and picks one
  134. * representative per group: the default variant if one matches, otherwise the first match.
  135. *
  136. * @return array<int, array{id:int, isDefault:bool}> ordered list of representatives
  137. */
  138. private function getGroupedRepresentatives($category, bool $onlyActive, array $filters, array $sorts = []): array {
  139. $categories = is_iterable($category) ? (is_array($category) ? array_values($category) : iterator_to_array($category)) : [$category];
  140. if(empty($categories)){
  141. return [];
  142. }
  143. $qb = $this->createQueryBuilder('cp')
  144. ->select('p.id AS productId', 'pm.id AS modelId', 'p.isDefaultVariant AS isDefault')
  145. ->innerJoin('cp.product', 'p')
  146. ->leftJoin('p.productModel', 'pm')
  147. ->where('cp.category IN (:categories)')
  148. ->setParameter('categories', $categories);
  149. if($onlyActive){
  150. $qb->andWhere('p.status = 1');
  151. }
  152. $i = 0;
  153. foreach($filters as $code => $values){
  154. if($values === null || $values === '' || $values === []){
  155. continue;
  156. }
  157. $values = is_array($values) ? array_values($values) : [$values];
  158. $paAlias = 'pa'.$i;
  159. $attrAlias = 'attr'.$i;
  160. $qb->innerJoin('p.attributes', $paAlias)
  161. ->innerJoin($paAlias.'.attribute', $attrAlias)
  162. ->andWhere($qb->expr()->eq($attrAlias.'.code', ':attrCode'.$i))
  163. ->andWhere($qb->expr()->in($paAlias.'.value', ':attrValues'.$i))
  164. ->setParameter('attrCode'.$i, $code)
  165. ->setParameter('attrValues'.$i, $values);
  166. $i++;
  167. }
  168. if(!empty($sorts)){
  169. foreach($sorts as $field => $direction){
  170. $qb->addOrderBy('p.'.$field, $direction);
  171. }
  172. }else{
  173. $qb->addOrderBy('p.id', 'ASC');
  174. }
  175. $rows = $qb->getQuery()->getScalarResult();
  176. $modelIds = array_values(array_unique(array_filter(array_column($rows, 'modelId'), fn($id) => $id !== null)));
  177. $topModelIds = $this->resolveTopModelIds($modelIds);
  178. $groups = [];
  179. $order = [];
  180. foreach($rows as $row){
  181. $productId = (int) $row['productId'];
  182. $modelId = $row['modelId'] !== null ? (int) $row['modelId'] : null;
  183. $groupKey = $modelId !== null ? 'm'.$topModelIds[$modelId] : 'p'.$productId;
  184. if(!isset($groups[$groupKey])){
  185. $groups[$groupKey] = [];
  186. $order[] = $groupKey;
  187. }
  188. $groups[$groupKey][] = ['id' => $productId, 'isDefault' => (bool) $row['isDefault']];
  189. }
  190. $representatives = [];
  191. foreach($order as $groupKey){
  192. $chosen = null;
  193. foreach($groups[$groupKey] as $variant){
  194. if($variant['isDefault']){
  195. $chosen = $variant;
  196. break;
  197. }
  198. }
  199. $representatives[] = $chosen ?? $groups[$groupKey][0];
  200. }
  201. return $representatives;
  202. }
  203. /**
  204. * Resolves, for each given ProductModel id, the id of its top-most ancestor
  205. * (a model can itself belong to another model, e.g. a color model under a top model).
  206. *
  207. * @return array<int, int> map of modelId => topModelId
  208. */
  209. private function resolveTopModelIds(array $modelIds): array {
  210. if(empty($modelIds)){
  211. return [];
  212. }
  213. $parentOf = [];
  214. $toFetch = $modelIds;
  215. $conn = $this->getEntityManager()->getConnection();
  216. while(!empty($toFetch)){
  217. $rows = $conn->fetchAllAssociative(
  218. 'SELECT id, parent_id FROM products_models WHERE id IN (?)',
  219. [$toFetch],
  220. [\Doctrine\DBAL\Connection::PARAM_INT_ARRAY]
  221. );
  222. $toFetch = [];
  223. foreach($rows as $row){
  224. $id = (int) $row['id'];
  225. $parentId = $row['parent_id'] !== null ? (int) $row['parent_id'] : null;
  226. $parentOf[$id] = $parentId;
  227. if($parentId !== null && !array_key_exists($parentId, $parentOf)){
  228. $toFetch[] = $parentId;
  229. }
  230. }
  231. }
  232. $top = [];
  233. foreach($modelIds as $id){
  234. $current = $id;
  235. while(!empty($parentOf[$current])){
  236. $current = $parentOf[$current];
  237. }
  238. $top[$id] = $current;
  239. }
  240. return $top;
  241. }
  242. }