From f90b5869174f24e225331c229734542517b2b97b Mon Sep 17 00:00:00 2001 From: Martin Grossmann Date: Thu, 17 Oct 2024 10:57:23 +0200 Subject: [PATCH 1/7] moved ready category seo mix to packages --- .../CategoryWithSeoMixDeleteConfirm.js | 20 + assets/js/admin/components/index.js | 1 + .../Router/FriendlyUrl/FriendlyUrlMatcher.php | 81 +++- .../FriendlyUrl/FriendlyUrlRepository.php | 2 + .../FriendlyUrl/FriendlyUrlRouterFactory.php | 14 +- src/Controller/Admin/CategoryController.php | 12 +- .../Admin/CategorySeoController.php | 368 ++++++++++++++++++ .../CategorySeo/CategorySeoFilterFormType.php | 78 ++++ .../ReadyCategorySeoCombinationFormType.php | 98 +++++ src/Migrations/Version20241017084042.php | 117 ++++++ src/Model/AdminNavigation/SideMenuBuilder.php | 6 + src/Model/Category/CategoryFacade.php | 33 +- src/Model/CategorySeo/CategorySeoFacade.php | 181 +++++++++ .../CategorySeo/CategorySeoFiltersData.php | 28 ++ .../CategorySeoFriendlyUrlDataProvider.php | 30 ++ src/Model/CategorySeo/CategorySeoMix.php | 116 ++++++ .../ChoseCategorySeoMixCombination.php | 155 ++++++++ .../DeleteReadyCategorySeoMixFacade.php | 38 ++ ...rySeoMixCombinationIsNotValidException.php | 11 + .../ReadyCategorySeoMixNotFoundException.php | 11 + ...SeoMixUrlsContainBadDomainUrlException.php | 11 + ...lsDoNotContainMainFriendlyUrlException.php | 11 + ...NotContainUrlForCorrectDomainException.php | 11 + ...ableToFindReadyCategorySeoMixException.php | 11 + src/Model/CategorySeo/ReadyCategorySeoMix.php | 282 ++++++++++++++ .../CategorySeo/ReadyCategorySeoMixData.php | 83 ++++ .../ReadyCategorySeoMixDataFactory.php | 130 +++++++ .../CategorySeo/ReadyCategorySeoMixFacade.php | 343 ++++++++++++++++ .../ReadyCategorySeoMixFactory.php | 30 ++ .../ReadyCategorySeoMixGridFactory.php | 84 ++++ ...yCategorySeoMixParameterParameterValue.php | 80 ++++ ...rySeoMixParameterParameterValueFactory.php | 34 ++ .../ReadyCategorySeoMixRepository.php | 231 +++++++++++ .../Filter/ProductFilterConfigFactory.php | 21 + .../Filter/ProductFilterDataFactory.php | 23 ++ src/Model/Product/Flag/FlagFacade.php | 9 + src/Model/Product/Flag/FlagRepository.php | 15 + .../ProductListOrderingModeForListFacade.php | 16 +- .../RequestToOrderingModeIdConverter.php | 11 + .../Product/Parameter/ParameterFacade.php | 39 ++ .../Product/Parameter/ParameterRepository.php | 112 +++++- .../ProductOnCurrentDomainElasticFacade.php | 9 + .../Search/ProductElasticsearchRepository.php | 29 ++ src/Resources/config/services.yaml | 4 + src/Resources/translations/messages.cs.po | 100 ++++- src/Resources/translations/messages.en.po | 96 +++++ .../Admin/Content/Category/list.html.twig | 37 +- .../Admin/Content/CategorySeo/list.html.twig | 18 + .../Content/CategorySeo/listGrid.html.twig | 27 ++ .../Content/CategorySeo/newCategory.html.twig | 43 ++ .../CategorySeo/newCombinations.html.twig | 60 +++ .../Content/CategorySeo/newFilters.html.twig | 22 ++ .../CategorySeo/readyCombination.html.twig | 44 +++ .../readyCombinationEditButton.html.twig | 8 + src/Twig/CategorySeoExtension.php | 73 ++++ 55 files changed, 3516 insertions(+), 41 deletions(-) create mode 100644 assets/js/admin/components/CategoryWithSeoMixDeleteConfirm.js create mode 100644 src/Controller/Admin/CategorySeoController.php create mode 100644 src/Form/Admin/CategorySeo/CategorySeoFilterFormType.php create mode 100644 src/Form/Admin/CategorySeo/ReadyCategorySeoCombinationFormType.php create mode 100644 src/Migrations/Version20241017084042.php create mode 100644 src/Model/CategorySeo/CategorySeoFacade.php create mode 100644 src/Model/CategorySeo/CategorySeoFiltersData.php create mode 100644 src/Model/CategorySeo/CategorySeoFriendlyUrlDataProvider.php create mode 100644 src/Model/CategorySeo/CategorySeoMix.php create mode 100644 src/Model/CategorySeo/ChoseCategorySeoMixCombination.php create mode 100644 src/Model/CategorySeo/DeleteReadyCategorySeoMixFacade.php create mode 100644 src/Model/CategorySeo/Exception/ChoseCategorySeoMixCombinationIsNotValidException.php create mode 100644 src/Model/CategorySeo/Exception/ReadyCategorySeoMixNotFoundException.php create mode 100644 src/Model/CategorySeo/Exception/ReadyCategorySeoMixUrlsContainBadDomainUrlException.php create mode 100644 src/Model/CategorySeo/Exception/ReadyCategorySeoMixUrlsDoNotContainMainFriendlyUrlException.php create mode 100644 src/Model/CategorySeo/Exception/ReadyCategorySeoMixUrlsDoNotContainUrlForCorrectDomainException.php create mode 100644 src/Model/CategorySeo/Exception/UnableToFindReadyCategorySeoMixException.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMix.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixData.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixDataFactory.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixFacade.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixFactory.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixGridFactory.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValue.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValueFactory.php create mode 100644 src/Model/CategorySeo/ReadyCategorySeoMixRepository.php create mode 100644 src/Resources/views/Admin/Content/CategorySeo/list.html.twig create mode 100644 src/Resources/views/Admin/Content/CategorySeo/listGrid.html.twig create mode 100644 src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig create mode 100644 src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig create mode 100644 src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig create mode 100644 src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig create mode 100644 src/Resources/views/Admin/Content/CategorySeo/readyCombinationEditButton.html.twig create mode 100644 src/Twig/CategorySeoExtension.php diff --git a/assets/js/admin/components/CategoryWithSeoMixDeleteConfirm.js b/assets/js/admin/components/CategoryWithSeoMixDeleteConfirm.js new file mode 100644 index 0000000000..ac58178316 --- /dev/null +++ b/assets/js/admin/components/CategoryWithSeoMixDeleteConfirm.js @@ -0,0 +1,20 @@ +import Register from '../../common/utils/Register'; +import Window from '../utils/Window'; +import Translator from 'bazinga-translator'; + +export default class CategoryWithSeoMixDeleteConfirm { + static init ($container) { + $container.filterAllNodes('.js-category-with-seomix-delete-confirm').click((event) => { + // eslint-disable-next-line no-new + new Window({ + content: Translator.trans('Do you really want to remove this category even with their SEO mixes?'), + buttonCancel: true, + buttonContinue: true, + textContinue: Translator.trans('Delete category with SEO mix'), + urlContinue: $(event.currentTarget).data('delete-url') + }); + }); + } +} + +(new Register()).registerCallback(CategoryWithSeoMixDeleteConfirm.init, 'CategoryWithSeoMixDeleteConfirm.init'); diff --git a/assets/js/admin/components/index.js b/assets/js/admin/components/index.js index 45e6c04685..7525d346c3 100644 --- a/assets/js/admin/components/index.js +++ b/assets/js/admin/components/index.js @@ -3,6 +3,7 @@ import './AdvancedSearch'; import './AjaxConfirm'; import './Article'; import './CategoryDeleteConfirm'; +import './CategoryWithSeoMixDeleteConfirm'; import './CategoryTreeForm'; import './CategoryTreeSorting'; import './CharactersCounter'; diff --git a/src/Component/Router/FriendlyUrl/FriendlyUrlMatcher.php b/src/Component/Router/FriendlyUrl/FriendlyUrlMatcher.php index 85406da6a9..82405bf74e 100644 --- a/src/Component/Router/FriendlyUrl/FriendlyUrlMatcher.php +++ b/src/Component/Router/FriendlyUrl/FriendlyUrlMatcher.php @@ -5,6 +5,9 @@ namespace Shopsys\FrameworkBundle\Component\Router\FriendlyUrl; use Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig; +use Shopsys\FrameworkBundle\Model\CategorySeo\Exception\ReadyCategorySeoMixNotFoundException; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixRepository; +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RouteCollection; @@ -12,9 +15,12 @@ class FriendlyUrlMatcher { /** * @param \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlRepository $friendlyUrlRepository + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixRepository $readyCategorySeoMixRepository */ - public function __construct(protected readonly FriendlyUrlRepository $friendlyUrlRepository) - { + public function __construct( + protected readonly FriendlyUrlRepository $friendlyUrlRepository, + protected readonly ReadyCategorySeoMixRepository $readyCategorySeoMixRepository, + ) { } /** @@ -23,7 +29,7 @@ public function __construct(protected readonly FriendlyUrlRepository $friendlyUr * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig * @return array */ - public function match($pathinfo, RouteCollection $routeCollection, DomainConfig $domainConfig) + public function match(string $pathinfo, RouteCollection $routeCollection, DomainConfig $domainConfig): array { $pathWithoutSlash = substr($pathinfo, 1); $friendlyUrl = $this->friendlyUrlRepository->findByDomainIdAndSlug($domainConfig->getId(), $pathWithoutSlash); @@ -32,6 +38,18 @@ public function match($pathinfo, RouteCollection $routeCollection, DomainConfig throw new ResourceNotFoundException(); } + $matchedParameters = []; + + if ($friendlyUrl->getRedirectTo() !== null) { + $matchedParameters['_route'] = $friendlyUrl->getRouteName(); + $matchedParameters['_controller'] = RedirectController::class . '::urlRedirectAction'; + $matchedParameters['path'] = $friendlyUrl->getRedirectTo(); + $matchedParameters['permanent'] = $friendlyUrl->getRedirectCode() !== 302; + $matchedParameters['id'] = $friendlyUrl->getEntityId(); + + return $matchedParameters; + } + $route = $routeCollection->get($friendlyUrl->getRouteName()); if ($route === null) { @@ -39,15 +57,70 @@ public function match($pathinfo, RouteCollection $routeCollection, DomainConfig } $matchedParameters = $route->getDefaults(); + + if ($friendlyUrl->getRouteName() === 'front_category_seo' && $friendlyUrl->isMain() === false) { + return $this->getMatchedParametersForNonMainFrontCategorySeoFriendlyUrl($friendlyUrl, $matchedParameters); + } + + if ($friendlyUrl->getRouteName() === 'front_category_seo') { + return $this->getMatchedParametersForMainFrontCategorySeoFriendlyUrl($friendlyUrl, $matchedParameters); + } + $matchedParameters['_route'] = $friendlyUrl->getRouteName(); $matchedParameters['id'] = $friendlyUrl->getEntityId(); if (!$friendlyUrl->isMain()) { - $matchedParameters['_controller'] = 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction'; + $matchedParameters['_controller'] = RedirectController::class . '::urlRedirectAction'; $matchedParameters['route'] = $friendlyUrl->getRouteName(); $matchedParameters['permanent'] = true; } return $matchedParameters; } + + /** + * @param \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrl $friendlyUrl + * @param array $matchedParameters + * @return array + */ + protected function getMatchedParametersForMainFrontCategorySeoFriendlyUrl( + FriendlyUrl $friendlyUrl, + array $matchedParameters, + ): array { + $readyCategorySeoMixId = $friendlyUrl->getEntityId(); + $readyCategorySeoMix = $this->readyCategorySeoMixRepository->findById($readyCategorySeoMixId); + + if ($readyCategorySeoMix === null) { + throw new ReadyCategorySeoMixNotFoundException(sprintf('ReadyCategorySeoMix with ID %s not found', $readyCategorySeoMixId)); + } + + $matchedParameters['_route'] = 'front_product_list'; + $matchedParameters['id'] = $readyCategorySeoMix->getCategory()->getId(); + $matchedParameters['readyCategorySeoMixId'] = $readyCategorySeoMixId; + + return $matchedParameters; + } + + /** + * @param \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrl $friendlyUrl + * @param array $matchedParameters + * @return array + */ + protected function getMatchedParametersForNonMainFrontCategorySeoFriendlyUrl( + FriendlyUrl $friendlyUrl, + array $matchedParameters, + ): array { + $readyCategorySeoMixId = $friendlyUrl->getEntityId(); + + $matchedParameters['_controller'] = 'FrameworkBundle:Redirect:redirect'; + + // Both are necessary + $matchedParameters['route'] = $friendlyUrl->getRouteName(); + $matchedParameters['_route'] = $friendlyUrl->getRouteName(); + + $matchedParameters['id'] = $readyCategorySeoMixId; + $matchedParameters['permanent'] = true; + + return $matchedParameters; + } } diff --git a/src/Component/Router/FriendlyUrl/FriendlyUrlRepository.php b/src/Component/Router/FriendlyUrl/FriendlyUrlRepository.php index e1121ea056..1f29af0ea6 100644 --- a/src/Component/Router/FriendlyUrl/FriendlyUrlRepository.php +++ b/src/Component/Router/FriendlyUrl/FriendlyUrlRepository.php @@ -15,6 +15,7 @@ use Shopsys\FrameworkBundle\Model\Blog\Article\BlogArticle; use Shopsys\FrameworkBundle\Model\Blog\Category\BlogCategory; use Shopsys\FrameworkBundle\Model\Category\Category; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix; use Shopsys\FrameworkBundle\Model\Product\Brand\Brand; use Shopsys\FrameworkBundle\Model\Product\Flag\Flag; use Shopsys\FrameworkBundle\Model\Product\Product; @@ -281,6 +282,7 @@ public function getRouteNameToEntityMap(): array 'front_stores_detail' => $this->entityNameResolver->resolve(Store::class), 'front_flag_detail' => $this->entityNameResolver->resolve(Flag::class), 'front_page_seo' => $this->entityNameResolver->resolve(SeoPage::class), + 'front_category_seo' => $this->entityNameResolver->resolve(ReadyCategorySeoMix::class), ]; } } diff --git a/src/Component/Router/FriendlyUrl/FriendlyUrlRouterFactory.php b/src/Component/Router/FriendlyUrl/FriendlyUrlRouterFactory.php index 990b586b01..8bc008389a 100644 --- a/src/Component/Router/FriendlyUrl/FriendlyUrlRouterFactory.php +++ b/src/Component/Router/FriendlyUrl/FriendlyUrlRouterFactory.php @@ -5,29 +5,29 @@ namespace Shopsys\FrameworkBundle\Component\Router\FriendlyUrl; use Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixRepository; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Contracts\Cache\CacheInterface; class FriendlyUrlRouterFactory { - protected string $friendlyUrlRouterResourceFilepath; - /** - * @param mixed $friendlyUrlRouterResourceFilepath + * @param string $friendlyUrlRouterResourceFilepath * @param \Symfony\Component\Config\Loader\LoaderInterface $configLoader * @param \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlRepository $friendlyUrlRepository * @param \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlCacheKeyProvider $friendlyUrlCacheKeyProvider * @param \Symfony\Contracts\Cache\CacheInterface $mainFriendlyUrlSlugCache + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixRepository $readyCategorySeoMixRepository */ public function __construct( - $friendlyUrlRouterResourceFilepath, + protected string $friendlyUrlRouterResourceFilepath, protected readonly LoaderInterface $configLoader, protected readonly FriendlyUrlRepository $friendlyUrlRepository, protected readonly FriendlyUrlCacheKeyProvider $friendlyUrlCacheKeyProvider, protected readonly CacheInterface $mainFriendlyUrlSlugCache, + protected readonly ReadyCategorySeoMixRepository $readyCategorySeoMixRepository, ) { - $this->friendlyUrlRouterResourceFilepath = $friendlyUrlRouterResourceFilepath; } /** @@ -35,7 +35,7 @@ public function __construct( * @param \Symfony\Component\Routing\RequestContext $context * @return \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlRouter */ - public function createRouter(DomainConfig $domainConfig, RequestContext $context) + public function createRouter(DomainConfig $domainConfig, RequestContext $context): FriendlyUrlRouter { return new FriendlyUrlRouter( $context, @@ -46,7 +46,7 @@ public function createRouter(DomainConfig $domainConfig, RequestContext $context $this->friendlyUrlCacheKeyProvider, $this->mainFriendlyUrlSlugCache, ), - new FriendlyUrlMatcher($this->friendlyUrlRepository), + new FriendlyUrlMatcher($this->friendlyUrlRepository, $this->readyCategorySeoMixRepository), $domainConfig, $this->friendlyUrlRouterResourceFilepath, ); diff --git a/src/Controller/Admin/CategoryController.php b/src/Controller/Admin/CategoryController.php index 9a3ae41aac..b911ac7b3d 100644 --- a/src/Controller/Admin/CategoryController.php +++ b/src/Controller/Admin/CategoryController.php @@ -7,12 +7,14 @@ use Nette\Utils\Json; use Shopsys\FrameworkBundle\Component\Domain\AdminDomainFilterTabsFacade; use Shopsys\FrameworkBundle\Component\Domain\Domain; +use Shopsys\FrameworkBundle\Component\Form\FormBuilderHelper; use Shopsys\FrameworkBundle\Component\Router\Security\Annotation\CsrfProtection; use Shopsys\FrameworkBundle\Form\Admin\Category\CategoryFormType; use Shopsys\FrameworkBundle\Model\AdminNavigation\BreadcrumbOverrider; use Shopsys\FrameworkBundle\Model\Category\CategoryDataFactoryInterface; use Shopsys\FrameworkBundle\Model\Category\CategoryFacade; use Shopsys\FrameworkBundle\Model\Category\Exception\CategoryNotFoundException; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixFacade; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -25,6 +27,8 @@ class CategoryController extends AdminBaseController * @param \Shopsys\FrameworkBundle\Component\Domain\Domain $domain * @param \Shopsys\FrameworkBundle\Model\AdminNavigation\BreadcrumbOverrider $breadcrumbOverrider * @param \Shopsys\FrameworkBundle\Component\Domain\AdminDomainFilterTabsFacade $adminDomainFilterTabsFacade + * @param \Shopsys\FrameworkBundle\Component\Form\FormBuilderHelper $formBuilderHelper + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixFacade $categorySeoMixFacade */ public function __construct( protected readonly CategoryFacade $categoryFacade, @@ -32,6 +36,8 @@ public function __construct( protected readonly Domain $domain, protected readonly BreadcrumbOverrider $breadcrumbOverrider, protected readonly AdminDomainFilterTabsFacade $adminDomainFilterTabsFacade, + protected readonly FormBuilderHelper $formBuilderHelper, + protected readonly ReadyCategorySeoMixFacade $categorySeoMixFacade, ) { } @@ -141,15 +147,17 @@ public function listAction(Request $request): Response return $this->render('@ShopsysFramework/Admin/Content/Category/list.html.twig', [ 'categoriesWithPreloadedChildren' => $categoriesWithPreloadedChildren, - 'domainFilterNamespace' => $domainFilterNamespace, 'isForAllDomains' => ($selectedDomainId === null), + 'domainFilterNamespace' => $domainFilterNamespace, + 'disabledFormFields' => $this->formBuilderHelper->hasFormDisabledFields(), + 'allCategoryIdsInSeoMixes' => $this->categorySeoMixFacade->getAllCategoryIdsInSeoMixes(), ]); } /** - * @see node_modules/@shopsys/framework/js/admin/components/CategoryTreeSorting.js * @param \Symfony\Component\HttpFoundation\Request $request * @return \Symfony\Component\HttpFoundation\Response + * @see node_modules/@shopsys/framework/js/admin/components/CategoryTreeSorting.js */ #[Route(path: '/category/apply-sorting/', methods: ['post'])] public function applySortingAction(Request $request): Response diff --git a/src/Controller/Admin/CategorySeoController.php b/src/Controller/Admin/CategorySeoController.php new file mode 100644 index 0000000000..ffffbcd5b1 --- /dev/null +++ b/src/Controller/Admin/CategorySeoController.php @@ -0,0 +1,368 @@ +readyCategorySeoMixGridFactory->create( + $this->adminDomainTabsFacade->getSelectedDomainId(), + $this->adminDomainTabsFacade->getSelectedDomainConfig()->getLocale(), + ); + + return $this->render('@ShopsysFramework/Admin/Content/CategorySeo/list.html.twig', [ + 'gridView' => $grid->createView(), + ]); + } + + /** + * @return \Symfony\Component\HttpFoundation\Response + */ + #[Route(path: '/seo/category/new/category')] + public function newCategoryAction(): Response + { + $locale = $this->adminDomainTabsFacade->getSelectedDomainConfig()->getLocale(); + + $categoriesWithPreloadedChildren = $this->categoryFacade->getVisibleCategoriesWithPreloadedChildrenForDomain( + $this->adminDomainTabsFacade->getSelectedDomainId(), + $locale, + ); + + return $this->render('@ShopsysFramework/Admin/Content/CategorySeo/newCategory.html.twig', [ + 'categoriesWithPreloadedChildren' => $categoriesWithPreloadedChildren, + 'locale' => $locale, + ]); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param int $categoryId + * @return \Symfony\Component\HttpFoundation\Response + */ + #[Route(path: '/seo/category/new/filters/category/{categoryId}', requirements: ['categoryId' => '\d+'])] + public function newFiltersAction(Request $request, int $categoryId): Response + { + $locale = $this->adminDomainTabsFacade->getSelectedDomainConfig()->getLocale(); + + $category = $this->categoryFacade->getById($categoryId); + $categorySeoFiltersData = new CategorySeoFiltersData(); + + $form = $this->createCategorySeoFilterForm($category, $categorySeoFiltersData); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid() && $request->get('is_for_backlink', false) === false) { + return $this->redirect( + $this->getUrlWithCategoryIdAndAllQueryParameters( + 'admin_categoryseo_newcombinations', + $categoryId, + $request->query->all(), + false, + ), + ); + } + + return $this->render('@ShopsysFramework/Admin/Content/CategorySeo/newFilters.html.twig', [ + 'category' => $category, + 'form' => $form->createView(), + 'locale' => $locale, + ]); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param int $categoryId + * @return \Symfony\Component\HttpFoundation\Response + */ + #[Route(path: '/seo/category/new/combinations/category/{categoryId}', requirements: ['categoryId' => '\d+'])] + public function newCombinationsAction(Request $request, int $categoryId): Response + { + $locale = $this->adminDomainTabsFacade->getSelectedDomainConfig()->getLocale(); + + $category = $this->categoryFacade->getById($categoryId); + $categorySeoFiltersData = new CategorySeoFiltersData(); + + $form = $this->createCategorySeoFilterForm($category, $categorySeoFiltersData); + $form->handleRequest($request); + + $categorySeoMixes = $this->categorySeoFacade->getCategorySeoMixes( + $category, + $categorySeoFiltersData, + $this->adminDomainTabsFacade->getSelectedDomainId(), + $this->adminDomainTabsFacade->getSelectedDomainConfig()->getLocale(), + ); + + return $this->render('@ShopsysFramework/Admin/Content/CategorySeo/newCombinations.html.twig', [ + 'category' => $category, + 'form' => $form->createView(), + 'categorySeoMixes' => $categorySeoMixes, + 'categorySeoFiltersData' => $categorySeoFiltersData, + 'locale' => $locale, + 'backLink' => $this->getUrlWithCategoryIdAndAllQueryParameters( + 'admin_categoryseo_newfilters', + $categoryId, + $request->query->all(), + true, + ), + 'categorySeoFilterFormTypeAllQueries' => $request->query->all(), + 'categoryId' => $categoryId, + ]); + } + + /** + * @param \Symfony\Component\HttpFoundation\Request $request + * @param int $categoryId + * @return \Symfony\Component\HttpFoundation\Response + */ + #[Route(path: '/seo/category/new/ready-combination/category/{categoryId}', requirements: ['categoryId' => '\d+'])] + public function readyCombinationAction(Request $request, int $categoryId): Response + { + $categorySeoFilterFormTypeAllQueries = $request->get('categorySeoFilterFormTypeAllQueries'); + + $choseCategorySeoMixCombination = ChoseCategorySeoMixCombination::createFromJson( + $request->get('choseCategorySeoMixCombinationJson'), + ); + + // A little hack - when you need form sent data to create that same form - need for friendly URLs + if ($choseCategorySeoMixCombination === null) { + $sentReadyCategorySeoCombinationFormData = $request->get('ready_category_seo_combination_form'); + $choseCategorySeoMixCombination = ChoseCategorySeoMixCombination::createFromJson( + $sentReadyCategorySeoCombinationFormData['choseCategorySeoMixCombinationJson'], + ); + } + + $readyCategorySeoMixData = $this->readyCategorySeoMixDataFactory->createReadyCategorySeoMixData($choseCategorySeoMixCombination); + + $this->storeJsonsToReadyCategorySeoMixData($readyCategorySeoMixData, $categorySeoFilterFormTypeAllQueries, $choseCategorySeoMixCombination); + + $readyCategorySeoCombinationFormType = $this->createForm(ReadyCategorySeoCombinationFormType::class, $readyCategorySeoMixData, [ + 'method' => 'POST', + 'readyCategorySeoMix' => $choseCategorySeoMixCombination !== null ? $this->readyCategorySeoMixFacade->findByChoseCategorySeoMixCombination($choseCategorySeoMixCombination) : null, + ]); + + $readyCategorySeoCombinationFormType->handleRequest($request); + + if ($categorySeoFilterFormTypeAllQueries === null + && $readyCategorySeoMixData->categorySeoFilterFormTypeAllQueriesJson !== null + ) { + $categorySeoFilterFormTypeAllQueries = json_decode($readyCategorySeoMixData->categorySeoFilterFormTypeAllQueriesJson, true, 512, JSON_THROW_ON_ERROR); + } + + if ($categorySeoFilterFormTypeAllQueries !== null) { + $newCombinationsUrl = $this->getUrlWithCategoryIdAndAllQueryParameters( + 'admin_categoryseo_newcombinations', + $categoryId, + $categorySeoFilterFormTypeAllQueries, + false, + ); + } else { + $newCombinationsUrl = $this->generateUrl('admin_categoryseo_list'); + } + + if ($readyCategorySeoCombinationFormType->isSubmitted() && $readyCategorySeoCombinationFormType->isValid()) { + $this->readyCategorySeoMixDataFactory->fillValuesFromChoseCategorySeoMixCombination( + $readyCategorySeoMixData, + $choseCategorySeoMixCombination, + ); + + $selfUrl = $this->generateUrl( + 'admin_categoryseo_readycombination', + [ + 'categoryId' => $categoryId, + 'categorySeoFilterFormTypeAllQueries' => $categorySeoFilterFormTypeAllQueries, + 'choseCategorySeoMixCombinationJson' => $choseCategorySeoMixCombination->getInJson(), + ], + ); + + try { + $this->readyCategorySeoMixFacade->createOrEdit( + $choseCategorySeoMixCombination, + $readyCategorySeoMixData, + $readyCategorySeoMixData->urls, + ); + + $this->addSuccessFlashTwig( + t('SEO category has been saved'), + ['url' => $selfUrl], + ); + + return $this->redirect($newCombinationsUrl); + } catch (ReadyCategorySeoMixUrlsContainBadDomainUrlException) { + $this->addErrorFlash(t('Fill URL only for selected domain')); + } catch (ReadyCategorySeoMixUrlsDoNotContainUrlForCorrectDomainException) { + $this->addErrorFlash(t('Fill URL also for selected domain')); + } + } + + return $this->render('@ShopsysFramework/Admin/Content/CategorySeo/readyCombination.html.twig', [ + 'form' => $readyCategorySeoCombinationFormType->createView(), + 'categorySeoFilterFormTypeAllQueries' => $categorySeoFilterFormTypeAllQueries, + 'newCombinationsUrl' => $newCombinationsUrl, + 'choseCategorySeoMixCombination' => $choseCategorySeoMixCombination, + 'flagName' => $choseCategorySeoMixCombination->getFlagId() !== null ? $this->flagFacade->getById($choseCategorySeoMixCombination->getFlagId())->getName() : '', + 'parameterValueNamesIndexedByParameterNames' => $this->parameterFacade->getParameterValueNamesIndexedByParameterNames( + $choseCategorySeoMixCombination->getParameterValueIdsByParameterIds(), + ), + 'choseCategorySeoMixCombinationDomainConfig' => $this->domain->getDomainConfigById($choseCategorySeoMixCombination->getDomainId()), + ]); + } + + /** + * @param int $categoryId + * @param array $categorySeoFilterFormTypeAllQueries + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ChoseCategorySeoMixCombination $choseCategorySeoMixCombination + * @return \Symfony\Component\HttpFoundation\Response + */ + public function readyCombinationButtonAction( + int $categoryId, + array $categorySeoFilterFormTypeAllQueries, + ChoseCategorySeoMixCombination $choseCategorySeoMixCombination, + ): Response { + return $this->render('@ShopsysFramework/Admin/Content/CategorySeo/readyCombinationEditButton.html.twig', [ + 'existsReadyCategorySeoMix' => $this->readyCategorySeoMixFacade->findByChoseCategorySeoMixCombination($choseCategorySeoMixCombination) !== null, + 'categoryId' => $categoryId, + 'categorySeoFilterFormTypeAllQueries' => $categorySeoFilterFormTypeAllQueries, + 'choseCategorySeoMixCombination' => $choseCategorySeoMixCombination, + 'choseCategorySeoMixCombinationJson' => $choseCategorySeoMixCombination->getInJson(), + ]); + } + + /** + * @CsrfProtection + * @param int $id + * @return \Symfony\Component\HttpFoundation\Response + */ + #[Route(path: '/seo/category/ready-combination/delete/{id}', requirements: ['id' => '\d+'])] + public function deleteAction(int $id): Response + { + try { + $readyCategorySeoMix = $this->readyCategorySeoMixFacade->getById($id); + $this->readyCategorySeoMixFacade->delete($readyCategorySeoMix); + $this->addSuccessFlashTwig( + t('SEO combination of category with ID {{ ReadyCategorySeoMixId }} has been removed', [ + '{{ ReadyCategorySeoMixId }}' => $id, + ]), + ); + } catch (ReadyCategorySeoMixNotFoundException) { + $this->addSuccessFlashTwig( + t('SEO combination of category with ID {{ ReadyCategorySeoMixId }} has not been removed, because it was not found', [ + '{{ ReadyCategorySeoMixId }}' => $id, + ]), + ); + } + + return $this->redirectToRoute('admin_categoryseo_list'); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Category\Category $category + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoFiltersData $categorySeoFiltersData + * @return \Symfony\Component\Form\FormInterface + */ + protected function createCategorySeoFilterForm( + Category $category, + CategorySeoFiltersData $categorySeoFiltersData, + ): FormInterface { + return $this->createForm(CategorySeoFilterFormType::class, $categorySeoFiltersData, [ + 'category' => $category, + 'domainId' => $this->adminDomainTabsFacade->getSelectedDomainId(), + 'method' => 'GET', + ]); + } + + /** + * @param string $routeName + * @param int $categoryId + * @param array $categorySeoFilterFormTypeAllQueries + * @param bool $isForBackLink + * @return string + */ + protected function getUrlWithCategoryIdAndAllQueryParameters( + string $routeName, + int $categoryId, + array $categorySeoFilterFormTypeAllQueries, + bool $isForBackLink, + ): string { + return $this->generateUrl( + $routeName, + array_merge( + ['categoryId' => $categoryId], + $categorySeoFilterFormTypeAllQueries, + ['is_for_backlink' => $isForBackLink], + ), + ); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixData $readyCategorySeoMixData + * @param array|null $categorySeoFilterFormTypeAllQueries + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ChoseCategorySeoMixCombination|null $choseCategorySeoMixCombination + */ + protected function storeJsonsToReadyCategorySeoMixData( + ReadyCategorySeoMixData $readyCategorySeoMixData, + ?array $categorySeoFilterFormTypeAllQueries, + ?ChoseCategorySeoMixCombination $choseCategorySeoMixCombination, + ): void { + if (isset($categorySeoFilterFormTypeAllQueries)) { + $readyCategorySeoMixData->categorySeoFilterFormTypeAllQueriesJson = json_encode($categorySeoFilterFormTypeAllQueries, JSON_THROW_ON_ERROR); + } + + if (isset($choseCategorySeoMixCombination)) { + $readyCategorySeoMixData->choseCategorySeoMixCombinationJson = $choseCategorySeoMixCombination->getInJson(); + } + } +} diff --git a/src/Form/Admin/CategorySeo/CategorySeoFilterFormType.php b/src/Form/Admin/CategorySeo/CategorySeoFilterFormType.php new file mode 100644 index 0000000000..8103570725 --- /dev/null +++ b/src/Form/Admin/CategorySeo/CategorySeoFilterFormType.php @@ -0,0 +1,78 @@ +add('useFlags', YesNoType::class, [ + 'required' => false, + 'label' => t('By flag'), + 'data' => false, + ]) + ->add('parameters', ChoiceType::class, [ + 'label' => t('Products parameters of selected category'), + 'choices' => $this->categorySeoFacade->getParametersUsedByProductsInCategoryWithoutSlider($category, $domainId), + 'choice_label' => 'name', + 'choice_value' => 'id', + 'multiple' => true, + 'expanded' => true, + 'required' => false, + ]) + ->add('save', SubmitType::class, [ + 'label' => t('Show combinations'), + 'attr' => [ + 'class' => 'margin-top-20', + ], + ]); + } + + /** + * @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setRequired('category') + ->setAllowedTypes('category', Category::class) + ->setRequired('domainId') + ->setAllowedTypes('domainId', 'int') + ->setDefaults([ + 'data_class' => CategorySeoFiltersData::class, + 'attr' => [ + 'novalidate' => 'novalidate', + ], + ]); + } +} diff --git a/src/Form/Admin/CategorySeo/ReadyCategorySeoCombinationFormType.php b/src/Form/Admin/CategorySeo/ReadyCategorySeoCombinationFormType.php new file mode 100644 index 0000000000..0db0208465 --- /dev/null +++ b/src/Form/Admin/CategorySeo/ReadyCategorySeoCombinationFormType.php @@ -0,0 +1,98 @@ +add('urls', UrlListType::class, [ + 'required' => true, + 'route_name' => 'front_category_seo', + 'entity_id' => $readyCategorySeoMix?->getId(), + 'label' => t('URL Settings'), + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('h1', TextType::class, [ + 'label' => t('Heading (H1)'), + 'required' => true, + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('showInCategory', YesNoType::class, [ + 'label' => t('Show in the category'), + ]) + ->add('shortDescription', TextareaType::class, [ + 'label' => t('Short description of category'), + 'required' => false, + ]) + ->add('description', CKEditorType::class, [ + 'label' => t('Category description'), + 'required' => false, + ]) + ->add('title', TextType::class, [ + 'label' => t('Page title'), + 'required' => false, + 'macro' => [ + 'name' => 'seoFormRowMacros', + 'recommended_length' => 60, + ], + ]) + ->add('metaDescription', TextareaType::class, [ + 'label' => t('Meta description'), + 'required' => false, + 'macro' => [ + 'name' => 'seoFormRowMacros', + 'recommended_length' => 155, + ], + ]) + ->add('categorySeoFilterFormTypeAllQueriesJson', HiddenType::class) + ->add('choseCategorySeoMixCombinationJson', HiddenType::class) + ->add('save', SubmitType::class, [ + 'label' => t('Save'), + 'attr' => [ + 'class' => 'margin-top-20', + ], + ]); + } + + /** + * @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setRequired('readyCategorySeoMix') + ->addAllowedTypes('readyCategorySeoMix', [ReadyCategorySeoMix::class, 'null']) + ->setDefaults([ + 'data_class' => ReadyCategorySeoMixData::class, + 'attr' => ['novalidate' => 'novalidate'], + ]); + } +} diff --git a/src/Migrations/Version20241017084042.php b/src/Migrations/Version20241017084042.php new file mode 100644 index 0000000000..64ebce31e4 --- /dev/null +++ b/src/Migrations/Version20241017084042.php @@ -0,0 +1,117 @@ +isAppMigrationNotInstalledRemoveIfExists('Version20200319113341')) { + $this->sql(' + CREATE TABLE ready_category_seo_mix_parameter_parameter_values ( + ready_category_seo_mix_id INT NOT NULL, + parameter_id INT NOT NULL, + parameter_value_id INT NOT NULL, + PRIMARY KEY( + ready_category_seo_mix_id, parameter_id, + parameter_value_id + ) + )'); + $this->sql(' + CREATE INDEX IDX_428D0DF07C7FCEDE ON ready_category_seo_mix_parameter_parameter_values (ready_category_seo_mix_id)'); + $this->sql(' + CREATE INDEX IDX_428D0DF07C56DBD6 ON ready_category_seo_mix_parameter_parameter_values (parameter_id)'); + $this->sql(' + CREATE INDEX IDX_428D0DF01452663E ON ready_category_seo_mix_parameter_parameter_values (parameter_value_id)'); + $this->sql(' + CREATE TABLE ready_category_seo_mixes ( + id SERIAL NOT NULL, + category_id INT NOT NULL, + flag_id INT DEFAULT NULL, + domain_id INT NOT NULL, + chose_category_seo_mix_combination_json TEXT NOT NULL, + ordering VARCHAR(255) DEFAULT NULL, + h1 TEXT NOT NULL, + description TEXT DEFAULT NULL, + title TEXT DEFAULT NULL, + meta_description TEXT DEFAULT NULL, + PRIMARY KEY(id) + )'); + $this->sql('CREATE INDEX IDX_74803E8F12469DE2 ON ready_category_seo_mixes (category_id)'); + $this->sql('CREATE INDEX IDX_74803E8F919FE4E5 ON ready_category_seo_mixes (flag_id)'); + $this->sql(' + CREATE UNIQUE INDEX chose_category_seo_mix_combination_json ON ready_category_seo_mixes ( + chose_category_seo_mix_combination_json + )'); + $this->sql(' + ALTER TABLE + ready_category_seo_mix_parameter_parameter_values + ADD + CONSTRAINT FK_428D0DF07C7FCEDE FOREIGN KEY (ready_category_seo_mix_id) REFERENCES ready_category_seo_mixes (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->sql(' + ALTER TABLE + ready_category_seo_mix_parameter_parameter_values + ADD + CONSTRAINT FK_428D0DF07C56DBD6 FOREIGN KEY (parameter_id) REFERENCES parameters (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->sql(' + ALTER TABLE + ready_category_seo_mix_parameter_parameter_values + ADD + CONSTRAINT FK_428D0DF01452663E FOREIGN KEY (parameter_value_id) REFERENCES parameter_values (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->sql(' + ALTER TABLE + ready_category_seo_mixes + ADD + CONSTRAINT FK_74803E8F12469DE2 FOREIGN KEY (category_id) REFERENCES categories (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->sql(' + ALTER TABLE + ready_category_seo_mixes + ADD + CONSTRAINT FK_74803E8F919FE4E5 FOREIGN KEY (flag_id) REFERENCES flags (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + if ($this->isAppMigrationNotInstalledRemoveIfExists('Version20200520151921')) { + $this->sql('ALTER TABLE ready_category_seo_mixes ADD short_description TEXT DEFAULT NULL'); + } + + if ($this->isAppMigrationNotInstalledRemoveIfExists('Version20200831091231')) { + $this->sql('ALTER TABLE ready_category_seo_mixes ALTER ordering SET NOT NULL'); + } + + if ($this->isAppMigrationNotInstalledRemoveIfExists('Version20201106174818')) { + $this->sql('ALTER TABLE ready_category_seo_mixes ADD show_in_category BOOLEAN NOT NULL DEFAULT false'); + $this->sql('ALTER TABLE ready_category_seo_mixes ALTER show_in_category DROP DEFAULT'); + } + + if ($this->isAppMigrationNotInstalledRemoveIfExists('Version20211014060332')) { + $this->sql('ALTER TABLE ready_category_seo_mixes DROP CONSTRAINT FK_74803E8F12469DE2'); + $this->sql(' + ALTER TABLE + ready_category_seo_mixes + ADD + CONSTRAINT FK_74803E8F12469DE2 FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + if ($this->isAppMigrationNotInstalledRemoveIfExists('Version20220628185551')) { + $this->sql('ALTER TABLE ready_category_seo_mixes ADD uuid UUID DEFAULT NULL'); + $this->sql('UPDATE ready_category_seo_mixes SET uuid = uuid_generate_v4()'); + $this->sql('ALTER TABLE ready_category_seo_mixes ALTER uuid SET NOT NULL'); + $this->sql('CREATE UNIQUE INDEX UNIQ_74803E8FD17F50A6 ON ready_category_seo_mixes (uuid)'); + } + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + */ + public function down(Schema $schema): void + { + } +} diff --git a/src/Model/AdminNavigation/SideMenuBuilder.php b/src/Model/AdminNavigation/SideMenuBuilder.php index 04e2b0c82d..8ac8b87be0 100644 --- a/src/Model/AdminNavigation/SideMenuBuilder.php +++ b/src/Model/AdminNavigation/SideMenuBuilder.php @@ -584,6 +584,12 @@ protected function createSettingsMenu(): ItemInterface $seoPageMenu->addChild('seoPageNew', ['route' => 'admin_seopage_new', 'label' => t('New SEO page'), 'display' => false]); $seoPageMenu->addChild('seoPageEdit', ['route' => 'admin_seopage_edit', 'label' => t('Editing SEO page'), 'display' => false]); + $categorySeoMenu = $seoMenu->addChild('categorySeo', ['route' => 'admin_categoryseo_list', 'label' => t('Extended SEO categories')]); + $categorySeoMenu->addChild('new_category', ['route' => 'admin_categoryseo_newcategory', 'label' => t('Extended SEO category - category selection'), 'display' => false]); + $categorySeoMenu->addChild('new_filters', ['route' => 'admin_categoryseo_newfilters', 'label' => t('Extended SEO category - filters'), 'display' => false]); + $categorySeoMenu->addChild('new_combinations', ['route' => 'admin_categoryseo_newcombinations', 'label' => t('Extended SEO category - combinations'), 'display' => false]); + $categorySeoMenu->addChild('new_combination', ['route' => 'admin_categoryseo_readycombination', 'label' => t('Extended SEO category - set combinations with SEO values'), 'display' => false]); + $contactFormSettingsMenu = $menu->addChild('contact_form_settings', ['label' => t('Contact form')]); $contactFormSettingsMenu->addChild( 'contact_form_settings', diff --git a/src/Model/Category/CategoryFacade.php b/src/Model/Category/CategoryFacade.php index 032eb021d3..0f51429347 100644 --- a/src/Model/Category/CategoryFacade.php +++ b/src/Model/Category/CategoryFacade.php @@ -14,13 +14,15 @@ use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlFacade; use Shopsys\FrameworkBundle\Model\Category\Exception\CategoryNotFoundException; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup; +use Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData; use Shopsys\FrameworkBundle\Model\Product\Product; +use Shopsys\FrameworkBundle\Model\Product\ProductOnCurrentDomainElasticFacade; use Shopsys\FrameworkBundle\Model\Product\Recalculation\ProductRecalculationDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class CategoryFacade { - protected const INCREMENT_DUE_TO_MISSING_ROOT_CATEGORY = 1; + protected const int INCREMENT_DUE_TO_MISSING_ROOT_CATEGORY = 1; /** * @param \Doctrine\ORM\EntityManagerInterface $em @@ -36,6 +38,7 @@ class CategoryFacade * @param \Shopsys\FrameworkBundle\Model\Product\Recalculation\ProductRecalculationDispatcher $productRecalculationDispatcher * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher * @param \Shopsys\FrameworkBundle\Model\Category\CategoryParameterFacade $categoryParameterFacade + * @param \Shopsys\FrameworkBundle\Model\Product\ProductOnCurrentDomainElasticFacade $productOnCurrentDomainElasticFacade */ public function __construct( protected readonly EntityManagerInterface $em, @@ -51,6 +54,7 @@ public function __construct( protected readonly ProductRecalculationDispatcher $productRecalculationDispatcher, protected readonly EventDispatcherInterface $eventDispatcher, protected readonly CategoryParameterFacade $categoryParameterFacade, + protected readonly ProductOnCurrentDomainElasticFacade $productOnCurrentDomainElasticFacade, ) { } @@ -507,4 +511,31 @@ protected function dispatchCategoryEvent(Category $category, string $eventType): { $this->eventDispatcher->dispatch(new CategoryEvent($category), $eventType); } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData + * @return \Shopsys\FrameworkBundle\Model\Category\Category[] + */ + public function getCategoriesOfProductByFilterData(ProductFilterData $productFilterData): array + { + $categoryIds = $this->productOnCurrentDomainElasticFacade->getCategoryIdsForFilterData($productFilterData); + $categories = $this->categoryRepository->getCategoriesByIds($categoryIds); + + $categoriesIndexedByIds = []; + + foreach ($categories as $category) { + $categoriesIndexedByIds[$category->getId()] = $category; + } + + $sortedCategories = []; + + foreach ($categoryIds as $categoryId) { + if (!array_key_exists($categoryId, $categoriesIndexedByIds)) { + continue; + } + $sortedCategories[] = $categoriesIndexedByIds[$categoryId]; + } + + return $sortedCategories; + } } diff --git a/src/Model/CategorySeo/CategorySeoFacade.php b/src/Model/CategorySeo/CategorySeoFacade.php new file mode 100644 index 0000000000..68cf93454c --- /dev/null +++ b/src/Model/CategorySeo/CategorySeoFacade.php @@ -0,0 +1,181 @@ +parameterRepository->getParametersUsedByProductsInCategoryWithoutSlider($category, $domainId); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Category\Category $category + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoFiltersData $categorySeoFiltersData + * @param int $domainId + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] + */ + public function getCategorySeoMixes( + Category $category, + CategorySeoFiltersData $categorySeoFiltersData, + int $domainId, + string $locale, + ): array { + $categorySeoMixes = [new CategorySeoMix($domainId, $category)]; + + $categorySeoMixes = $this->getSeoCategoryMixesFromParameters( + $categorySeoMixes, + $category, + $categorySeoFiltersData, + $domainId, + $locale, + ); + + $categorySeoMixes = $this->getSeoCategoryMixesFromFlags( + $categorySeoMixes, + $categorySeoFiltersData, + ); + + $categorySeoMixes = $this->getSeoCategoryMixesFromOrderings( + $categorySeoMixes, + $categorySeoFiltersData, + ); + + return $categorySeoMixes; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] $categorySeoMixes + * @param \Shopsys\FrameworkBundle\Model\Category\Category $category + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoFiltersData $categorySeoFiltersData + * @param int $domainId + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] + */ + protected function getSeoCategoryMixesFromParameters( + array $categorySeoMixes, + Category $category, + CategorySeoFiltersData $categorySeoFiltersData, + int $domainId, + string $locale, + ): array { + foreach ($categorySeoFiltersData->parameters as $parameter) { + $parameterValues = $this->parameterRepository->getParameterValuesUsedByProductsInCategoryByParameter( + $category, + $parameter, + $domainId, + $locale, + ); + + $categorySeoMixes = $this->getNewSeoCategoryMixes( + $categorySeoMixes, + $parameterValues, + function (CategorySeoMix $categorySeoMix, ParameterValue $parameterValue) { + $categorySeoMix->addParameterValue($parameterValue); + }, + ); + } + + return $categorySeoMixes; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] $categorySeoMixes + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoFiltersData $categorySeoFiltersData + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] + */ + protected function getSeoCategoryMixesFromFlags( + array $categorySeoMixes, + CategorySeoFiltersData $categorySeoFiltersData, + ): array { + if ($categorySeoFiltersData->useFlags === true) { + $categorySeoMixes = $this->getNewSeoCategoryMixes( + $categorySeoMixes, + $this->flagFacade->getAll(), + function (CategorySeoMix $categorySeoMix, Flag $flag) { + $categorySeoMix->setFlag($flag); + }, + ); + } + + return $categorySeoMixes; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] $categorySeoMixes + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoFiltersData $categorySeoFiltersData + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] + */ + protected function getSeoCategoryMixesFromOrderings( + array $categorySeoMixes, + CategorySeoFiltersData $categorySeoFiltersData, + ): array { + if ($categorySeoFiltersData->useOrdering === true) { + $orderings = array_keys($this->productListOrderingModeForListFacade + ->getProductListOrderingConfig() + ->getSupportedOrderingModesNamesIndexedById()); + + $categorySeoMixes = $this->getNewSeoCategoryMixes( + $categorySeoMixes, + $orderings, + function (CategorySeoMix $categorySeoMix, string $ordering) { + $categorySeoMix->setOrdering($ordering); + }, + ); + } + + return $categorySeoMixes; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] $categorySeoMixes + * @param object[]|string[] $newValues + * @param callable $categorySeoMixCallback + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoMix[] + */ + protected function getNewSeoCategoryMixes( + array $categorySeoMixes, + array $newValues, + callable $categorySeoMixCallback, + ): array { + $newCategorySeoMixes = []; + + foreach ($newValues as $newValue) { + foreach ($categorySeoMixes as $categorySeoMix) { + $clonedCategorySeoMix = clone $categorySeoMix; + $categorySeoMixCallback($clonedCategorySeoMix, $newValue); + + $newCategorySeoMixes[] = $clonedCategorySeoMix; + } + } + + return $newCategorySeoMixes; + } +} diff --git a/src/Model/CategorySeo/CategorySeoFiltersData.php b/src/Model/CategorySeo/CategorySeoFiltersData.php new file mode 100644 index 0000000000..d3cdb268ca --- /dev/null +++ b/src/Model/CategorySeo/CategorySeoFiltersData.php @@ -0,0 +1,28 @@ +useOrdering = true; + } +} diff --git a/src/Model/CategorySeo/CategorySeoFriendlyUrlDataProvider.php b/src/Model/CategorySeo/CategorySeoFriendlyUrlDataProvider.php new file mode 100644 index 0000000000..be091ee06b --- /dev/null +++ b/src/Model/CategorySeo/CategorySeoFriendlyUrlDataProvider.php @@ -0,0 +1,30 @@ +domainId; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\Category\Category + */ + public function getCategory(): Category + { + return $this->category; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag|null + */ + public function getFlag(): ?Flag + { + return $this->flag; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Flag\Flag $flag + */ + public function setFlag(Flag $flag): void + { + $this->flag = $flag; + } + + /** + * @return string|null + */ + public function getOrdering(): ?string + { + return $this->ordering; + } + + /** + * @param string $ordering + */ + public function setOrdering(string $ordering): void + { + $this->ordering = $ordering; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterValue[] + */ + public function getParameterValues(): array + { + return $this->parameterValues; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterValue $parameterValue + */ + public function addParameterValue(ParameterValue $parameterValue): void + { + $this->parameterValues[] = $parameterValue; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Parameter\Parameter[] $parameters + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ChoseCategorySeoMixCombination + */ + public function getChoseCategorySeoMixCombination(array $parameters): ChoseCategorySeoMixCombination + { + $parameterValueIdsByParameterIds = []; + + foreach ($this->getParameterValues() as $index => $parameterValue) { + $parameterValueIdsByParameterIds[$parameters[$index]->getId()] = $parameterValue->getId(); + } + + return new ChoseCategorySeoMixCombination( + $this->getDomainId(), + $this->category->getId(), + $this->ordering, + $this->flag?->getId(), + $parameterValueIdsByParameterIds, + ); + } +} diff --git a/src/Model/CategorySeo/ChoseCategorySeoMixCombination.php b/src/Model/CategorySeo/ChoseCategorySeoMixCombination.php new file mode 100644 index 0000000000..4e6dd32e30 --- /dev/null +++ b/src/Model/CategorySeo/ChoseCategorySeoMixCombination.php @@ -0,0 +1,155 @@ +domainId, + $this->categoryId, + $this->flagId, + $this->ordering, + $this->parameterValueIdsByParameterIds, + ); + } + + /** + * @return string + */ + public function getInJson(): string + { + return json_encode($this->getInArray(), JSON_THROW_ON_ERROR); + } + + /** + * @return int + */ + public function getDomainId(): int + { + return $this->domainId; + } + + /** + * @return int + */ + public function getCategoryId(): int + { + return $this->categoryId; + } + + /** + * @return int|null + */ + public function getFlagId(): ?int + { + return $this->flagId; + } + + /** + * @return string|null + */ + public function getOrdering(): ?string + { + return $this->ordering; + } + + /** + * @return int[] + */ + public function getParameterValueIdsByParameterIds(): array + { + return $this->parameterValueIdsByParameterIds; + } + + /** + * @param int $domainId + * @param int $categoryId + * @param int|null $flagId + * @param string|null $ordering + * @param int[] $parameterValueIdsByParameterIds + * @return array + */ + public static function getChoseCategorySeoMixCombinationArray( + int $domainId, + int $categoryId, + ?int $flagId, + ?string $ordering, + array $parameterValueIdsByParameterIds, + ): array { + ksort($parameterValueIdsByParameterIds); + + return [ + 'domainId' => $domainId, + 'categoryId' => $categoryId, + 'flagId' => $flagId, + 'ordering' => $ordering, + 'parameterValueIdsByParameterIds' => $parameterValueIdsByParameterIds, + ]; + } +} diff --git a/src/Model/CategorySeo/DeleteReadyCategorySeoMixFacade.php b/src/Model/CategorySeo/DeleteReadyCategorySeoMixFacade.php new file mode 100644 index 0000000000..4398ce0682 --- /dev/null +++ b/src/Model/CategorySeo/DeleteReadyCategorySeoMixFacade.php @@ -0,0 +1,38 @@ +readyCategorySeoMixRepository->getAllWithParameter($parameter); + + if (count($readyCategorySeoMixes) === 0) { + return; + } + + foreach ($readyCategorySeoMixes as $readyCategorySeoMix) { + $this->em->remove($readyCategorySeoMix); + } + $this->em->flush(); + } +} diff --git a/src/Model/CategorySeo/Exception/ChoseCategorySeoMixCombinationIsNotValidException.php b/src/Model/CategorySeo/Exception/ChoseCategorySeoMixCombinationIsNotValidException.php new file mode 100644 index 0000000000..2e1a1c84f3 --- /dev/null +++ b/src/Model/CategorySeo/Exception/ChoseCategorySeoMixCombinationIsNotValidException.php @@ -0,0 +1,11 @@ + + * @ORM\OneToMany( + * targetEntity="Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixParameterParameterValue", + * mappedBy="readyCategorySeoMix", + * cascade={"persist" ,"remove"}, + * fetch="EXTRA_LAZY" + * ) + */ + protected $readyCategorySeoMixParameterParameterValues; + + /** + * @var string + * @ORM\Column(type="text", nullable=false) + */ + protected $h1; + + /** + * @var string|null + * @ORM\Column(type="text", nullable=true) + */ + protected $shortDescription; + + /** + * @var string|null + * @ORM\Column(type="text", nullable=true) + */ + protected $description; + + /** + * @var string|null + * @ORM\Column(type="text", nullable=true) + */ + protected $title; + + /** + * @var string|null + * @ORM\Column(type="text", nullable=true) + */ + protected $metaDescription; + + /** + * @var bool + * @ORM\Column(type="boolean") + */ + protected $showInCategory; + + /** + * @var string + * @ORM\Column(type="guid", unique=true) + */ + protected $uuid; + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixData $readyCategorySeoMixData + */ + public function __construct(ReadyCategorySeoMixData $readyCategorySeoMixData) + { + $this->readyCategorySeoMixParameterParameterValues = new ArrayCollection(); + + $this->category = $readyCategorySeoMixData->category; + $this->flag = $readyCategorySeoMixData->flag; + $this->ordering = $readyCategorySeoMixData->ordering; + $this->domainId = $readyCategorySeoMixData->domainId; + $this->choseCategorySeoMixCombinationJson = $readyCategorySeoMixData->choseCategorySeoMixCombinationJson; + + $this->h1 = $readyCategorySeoMixData->h1; + $this->shortDescription = $readyCategorySeoMixData->shortDescription; + $this->description = $readyCategorySeoMixData->description; + $this->title = $readyCategorySeoMixData->title; + $this->metaDescription = $readyCategorySeoMixData->metaDescription; + $this->showInCategory = $readyCategorySeoMixData->showInCategory; + + $this->uuid = Uuid::uuid4()->toString(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixData $readyCategorySeoMixData + */ + public function edit(ReadyCategorySeoMixData $readyCategorySeoMixData): void + { + $this->category = $readyCategorySeoMixData->category; + $this->flag = $readyCategorySeoMixData->flag; + $this->ordering = $readyCategorySeoMixData->ordering; + + $this->h1 = $readyCategorySeoMixData->h1; + $this->shortDescription = $readyCategorySeoMixData->shortDescription; + $this->description = $readyCategorySeoMixData->description; + $this->title = $readyCategorySeoMixData->title; + $this->metaDescription = $readyCategorySeoMixData->metaDescription; + $this->showInCategory = $readyCategorySeoMixData->showInCategory; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\Category\Category + */ + public function getCategory() + { + return $this->category; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag|null + */ + public function getFlag() + { + return $this->flag; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Flag\Flag $flag + */ + public function setFlag($flag) + { + $this->flag = $flag; + } + + /** + * @return string + */ + public function getOrdering() + { + return $this->ordering; + } + + /** + * @param string $ordering + */ + public function setOrdering($ordering) + { + $this->ordering = $ordering; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixParameterParameterValue[] + */ + public function getReadyCategorySeoMixParameterParameterValues() + { + return $this->readyCategorySeoMixParameterParameterValues->getValues(); + } + + /** + * @return string + */ + public function getH1() + { + return $this->h1; + } + + /** + * @return string|null + */ + public function getShortDescription() + { + return $this->shortDescription; + } + + /** + * @return string|null + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return string|null + */ + public function getTitle() + { + return $this->title; + } + + /** + * @return string|null + */ + public function getMetaDescription() + { + return $this->metaDescription; + } + + /** + * @return int + */ + public function getDomainId() + { + return $this->domainId; + } + + /** + * @return bool + */ + public function showInCategory() + { + return $this->showInCategory; + } + + /** + * @return string + */ + public function getUuid() + { + return $this->uuid; + } + + /** + * @return string|null + */ + public function getChoseCategorySeoMixCombinationJson() + { + return $this->choseCategorySeoMixCombinationJson; + } +} diff --git a/src/Model/CategorySeo/ReadyCategorySeoMixData.php b/src/Model/CategorySeo/ReadyCategorySeoMixData.php new file mode 100644 index 0000000000..f61f0d86fb --- /dev/null +++ b/src/Model/CategorySeo/ReadyCategorySeoMixData.php @@ -0,0 +1,83 @@ +createInstance(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ChoseCategorySeoMixCombination|null $choseCategorySeoMixCombination + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixData + */ + public function createReadyCategorySeoMixData( + ?ChoseCategorySeoMixCombination $choseCategorySeoMixCombination, + ): ReadyCategorySeoMixData { + $readyCategorySeoMix = null; + + if ($choseCategorySeoMixCombination !== null) { + $readyCategorySeoMix = $this->readyCategorySeoMixFacade->findByChoseCategorySeoMixCombination($choseCategorySeoMixCombination); + } + + $readyCategorySeoMixData = $this->createInstance(); + + $readyCategorySeoMixData->urls = new UrlListData(); + + if ($readyCategorySeoMix !== null) { + $this->fillValuesFromReadyCategorySeoMix($readyCategorySeoMixData, $readyCategorySeoMix); + + $mainFriendlyUrl = $this->friendlyUrlFacade->findMainFriendlyUrl( + $readyCategorySeoMix->getDomainId(), + 'front_category_seo', + $readyCategorySeoMix->getId(), + ); + $readyCategorySeoMixData->urls->mainFriendlyUrlsByDomainId[$readyCategorySeoMix->getDomainId()] = $mainFriendlyUrl; + } + + return $readyCategorySeoMixData; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixData $readyCategorySeoMixData + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ChoseCategorySeoMixCombination $choseCategorySeoMixCombination + */ + public function fillValuesFromChoseCategorySeoMixCombination( + ReadyCategorySeoMixData $readyCategorySeoMixData, + ChoseCategorySeoMixCombination $choseCategorySeoMixCombination, + ): void { + $readyCategorySeoMixData->domainId = $choseCategorySeoMixCombination->getDomainId(); + + $readyCategorySeoMixData->category = $this->categoryFacade->getById( + $choseCategorySeoMixCombination->getCategoryId(), + ); + + $readyCategorySeoMixData->flag = null; + + if ($choseCategorySeoMixCombination->getFlagId() !== null) { + $flag = $this->flagFacade->getById($choseCategorySeoMixCombination->getFlagId()); + $readyCategorySeoMixData->flag = $flag; + } + + $readyCategorySeoMixData->ordering = $choseCategorySeoMixCombination->getOrdering(); + + $readyCategorySeoMixData->readyCategorySeoMixParameterParameterValues = []; + + foreach ($choseCategorySeoMixCombination->getParameterValueIdsByParameterIds() as $parameterId => $parameterValueId) { + $readyCategorySeoMixData->readyCategorySeoMixParameterParameterValues[] = $this->readyCategorySeoMixParameterValueFactory->create( + $this->parameterFacade->getById($parameterId), + $this->parameterFacade->getParameterValueById($parameterValueId), + ); + } + + $readyCategorySeoMixData->choseCategorySeoMixCombinationJson = $choseCategorySeoMixCombination->getInJson(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixData $readyCategorySeoMixData + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix $readyCategorySeoMix + */ + public function fillValuesFromReadyCategorySeoMix( + ReadyCategorySeoMixData $readyCategorySeoMixData, + ReadyCategorySeoMix $readyCategorySeoMix, + ): void { + $readyCategorySeoMixData->h1 = $readyCategorySeoMix->getH1(); + $readyCategorySeoMixData->shortDescription = $readyCategorySeoMix->getShortDescription(); + $readyCategorySeoMixData->description = $readyCategorySeoMix->getDescription(); + $readyCategorySeoMixData->title = $readyCategorySeoMix->getTitle(); + $readyCategorySeoMixData->metaDescription = $readyCategorySeoMix->getMetaDescription(); + $readyCategorySeoMixData->showInCategory = $readyCategorySeoMix->showInCategory(); + } +} diff --git a/src/Model/CategorySeo/ReadyCategorySeoMixFacade.php b/src/Model/CategorySeo/ReadyCategorySeoMixFacade.php new file mode 100644 index 0000000000..61b3332461 --- /dev/null +++ b/src/Model/CategorySeo/ReadyCategorySeoMixFacade.php @@ -0,0 +1,343 @@ +findByChoseCategorySeoMixCombination($choseCategorySeoMixCombination); + + $this->em->beginTransaction(); + + if ($readyCategorySeoMix === null) { + $readyCategorySeoMix = $this->readyCategorySeoMixFactory->create($readyCategorySeoMixData); + $this->em->persist($readyCategorySeoMix); + $this->em->flush(); + + foreach ($readyCategorySeoMixData->readyCategorySeoMixParameterParameterValues as $readyCategorySeoMixParameterParameterValue) { + $readyCategorySeoMixParameterParameterValue->setReadyCategorySeoMix($readyCategorySeoMix); + $this->em->persist($readyCategorySeoMixParameterParameterValue); + $this->em->flush(); + } + } else { + $readyCategorySeoMix->edit($readyCategorySeoMixData); + $this->em->flush(); + } + + $this->saveReadyCategoryMixFriendlyUrls($readyCategorySeoMix, $urlListData); + + try { + $this->validateReadyCategoryMixFriendlyUrls($readyCategorySeoMix); + } catch (ReadyCategorySeoMixUrlsContainBadDomainUrlException | ReadyCategorySeoMixUrlsDoNotContainUrlForCorrectDomainException $e) { + TransactionalMasterRequestListener::setTransactionManuallyRollbacked(); + $this->em->rollback(); + + throw $e; + } + + $this->em->commit(); + + return $readyCategorySeoMix; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ChoseCategorySeoMixCombination $choseCategorySeoMixCombination + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null + */ + public function findByChoseCategorySeoMixCombination( + ChoseCategorySeoMixCombination $choseCategorySeoMixCombination, + ): ?ReadyCategorySeoMix { + return $this->readyCategorySeoMixRepository->findByChoseCategorySeoMixCombination($choseCategorySeoMixCombination); + } + + /** + * @param int $id + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null + */ + public function findById(int $id): ?ReadyCategorySeoMix + { + return $this->readyCategorySeoMixRepository->findById($id); + } + + /** + * @param int $id + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix + */ + public function getById(int $id): ReadyCategorySeoMix + { + $readyCategorySeoMix = $this->readyCategorySeoMixRepository->findById($id); + + if ($readyCategorySeoMix === null) { + throw new ReadyCategorySeoMixNotFoundException(sprintf('ReadyCategorySeoMix with ID %s not found', $id)); + } + + return $readyCategorySeoMix; + } + + /** + * @param string $uuid + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix + */ + public function getByUuid(string $uuid): ReadyCategorySeoMix + { + $readyCategorySeoMix = $this->readyCategorySeoMixRepository->findByUuid($uuid); + + if ($readyCategorySeoMix === null) { + throw new ReadyCategorySeoMixNotFoundException(sprintf('ReadyCategorySeoMix with UUID %s not found', $uuid)); + } + + return $readyCategorySeoMix; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix $readyCategorySeoMix + */ + public function delete(ReadyCategorySeoMix $readyCategorySeoMix): void + { + $this->em->remove($readyCategorySeoMix); + $this->em->flush(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix $readyCategorySeoMix + * @param \Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\UrlListData $urlListData + */ + protected function saveReadyCategoryMixFriendlyUrls( + ReadyCategorySeoMix $readyCategorySeoMix, + UrlListData $urlListData, + ): void { + $this->friendlyUrlFacade->saveUrlListFormData('front_category_seo', $readyCategorySeoMix->getId(), $urlListData); + + $mainFriendlyUrl = $this->friendlyUrlFacade->findMainFriendlyUrl($readyCategorySeoMix->getDomainId(), 'front_category_seo', $readyCategorySeoMix->getId()); + + if ($mainFriendlyUrl !== null) { + return; + } + + $readyCategoryMixAllFriendlyUrls = $this->friendlyUrlFacade->getAllByRouteNameAndEntityId('front_category_seo', $readyCategorySeoMix->getId()); + + if (count($readyCategoryMixAllFriendlyUrls) === 0) { + return; + } + + $urlListDataForMainFriendlyUrl = new UrlListData(); + $urlListDataForMainFriendlyUrl->mainFriendlyUrlsByDomainId = [ + array_shift($readyCategoryMixAllFriendlyUrls), + ]; + + $this->friendlyUrlFacade->saveUrlListFormData('front_category_seo', $readyCategorySeoMix->getId(), $urlListDataForMainFriendlyUrl); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix $readyCategorySeoMix + */ + protected function validateReadyCategoryMixFriendlyUrls(ReadyCategorySeoMix $readyCategorySeoMix): void + { + $readyCategorySeoMixAllFriendlyUrls = $this->friendlyUrlFacade->getAllByRouteNameAndEntityId('front_category_seo', $readyCategorySeoMix->getId()); + + $hasCorrectDomainUrl = false; + $hasMainFriendlyUrl = false; + + foreach ($readyCategorySeoMixAllFriendlyUrls as $friendlyUrl) { + if ($friendlyUrl->getDomainId() !== $readyCategorySeoMix->getDomainId()) { + throw new ReadyCategorySeoMixUrlsContainBadDomainUrlException('ReadyCategorySeoMix urls contain bad domain url'); + } + + if ($friendlyUrl->isMain() === true && $hasMainFriendlyUrl === false) { + $hasMainFriendlyUrl = true; + } + + $hasCorrectDomainUrl = true; + } + + if ($hasCorrectDomainUrl === false) { + throw new ReadyCategorySeoMixUrlsDoNotContainUrlForCorrectDomainException('ReadyCategorySeoMix urls do not contain url for correct domain'); + } + + if ($hasMainFriendlyUrl === false) { + throw new ReadyCategorySeoMixUrlsDoNotContainMainFriendlyUrlException('ReadyCategorySeoMix urls do not contain main FriendlyUrl'); + } + } + + /** + * @param int $categoryId + * @param array $parametersFilterData + * @param string[] $flagUuids + * @param string|null $orderingMode + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null + */ + public function findReadyCategorySeoMixByQueryInputData( + int $categoryId, + array $parametersFilterData, + array $flagUuids, + ?string $orderingMode, + ): ?ReadyCategorySeoMix { + try { + $currentDomainConfig = $this->domain->getCurrentDomainConfig(); + $this->checkPossibilityToFindReadyCategorySeoMix($parametersFilterData, $flagUuids, $orderingMode); + // From now on, we can count on the following facts: + // - Parameters have only 1 value, or values are empty, and minimalValue === maximalValue (i.e., exactly one value is selected in slider). + // - Count of flagUuids is 0 or 1. + + return $this->readyCategorySeoMixRepository->getReadyCategorySeoMixFromFilter( + $categoryId, + $this->getParameterValueIdsByParameterId($parametersFilterData, $currentDomainConfig->getLocale()), + $this->flagFacade->getFlagIdsByUuids($flagUuids), + $orderingMode, + $currentDomainConfig, + ); + } catch (UnableToFindReadyCategorySeoMixException|ParameterValueNotFoundException) { + return null; + } + } + + /** + * @param array $parametersFilterData + * @param string[] $flagUuids + * @param string|null $ordering + */ + protected function checkPossibilityToFindReadyCategorySeoMix( + array $parametersFilterData, + array $flagUuids, + ?string $ordering, + ): void { + if ($ordering === null && count($parametersFilterData) === 0 && count($flagUuids)) { + throw new UnableToFindReadyCategorySeoMixException( + 'Unable to find ReadyCategorySeoMix: it cannot have set no conditions', + ); + } + + foreach ($parametersFilterData as $parameterFilterData) { + $valuesCount = count($parameterFilterData['values']); + + if ($valuesCount === 0 && ($parameterFilterData['minimalValue'] !== $parameterFilterData['maximalValue'])) { + throw new UnableToFindReadyCategorySeoMixException( + 'Unable to find ReadyCategorySeoMix: there must be exactly one value for slider parameters selected', + ); + } + + if ($valuesCount > 1) { + throw new UnableToFindReadyCategorySeoMixException( + 'Unable to find ReadyCategorySeoMix: it cannot have more than one parameter value of one parameter', + ); + } + } + + if (count($flagUuids) > 1) { + throw new UnableToFindReadyCategorySeoMixException( + 'Unable to find ReadyCategorySeoMix: it cannot have more than one flag', + ); + } + } + + /** + * @param array $categoryIds + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix[][] + */ + public function getAllIndexedByCategoryId(array $categoryIds, DomainConfig $domainConfig): array + { + return $this->readyCategorySeoMixRepository->getAllIndexedByCategoryId($categoryIds, $domainConfig); + } + + /** + * @return array + */ + public function getAllCategoryIdsInSeoMixes(): array + { + return $this->readyCategorySeoMixRepository->getAllCategoryIdsInSeoMixes(); + } + + /** + * @param array $parametersFilterData + * @param string $currentLocale + * @return array + */ + protected function getParameterValueIdsByParameterId(array $parametersFilterData, string $currentLocale): array + { + $parameterIdsByUuids = $this->parameterFacade->getParameterIdsIndexedByUuids(array_column($parametersFilterData, 'parameter')); + $allParameterValuesUuids = array_merge(...array_column($parametersFilterData, 'values')); + $parameterValueIdsByUuids = $this->parameterFacade->getParameterValueIdsIndexedByUuids($allParameterValuesUuids); + $parameterValueIdsByParameterId = []; + + foreach ($parametersFilterData as $parameterFilterData) { + if (array_key_exists($parameterFilterData['parameter'], $parameterIdsByUuids) === false) { + throw new ParameterNotFoundException(sprintf( + 'Parameter with uuid "%s" was not found', + $parameterFilterData['parameter'], + )); + } + + $parameterId = $parameterIdsByUuids[$parameterFilterData['parameter']]; + + if (count($parameterFilterData['values']) === 0) { + // slider parameter, minimal and maximal value are the same (see checkPossibilityToFindReadyCategorySeoMix method), + // so it does not matter which one is used for grabbing the text + $text = $parameterFilterData['minimalValue']; + $parameterValueId = $this->parameterFacade->getParameterValueIdByText((string)$text, $currentLocale); + } else { + $parameterUuid = reset($parameterFilterData['values']); + + if (array_key_exists($parameterUuid, $parameterValueIdsByUuids) === false) { + throw new ParameterValueNotFoundException(sprintf( + 'Parameter value with uuid "%s" was not found', + $parameterUuid, + )); + } + + $parameterValueId = $parameterValueIdsByUuids[$parameterUuid]; + } + $parameterValueIdsByParameterId[$parameterId] = $parameterValueId; + } + + return $parameterValueIdsByParameterId; + } +} diff --git a/src/Model/CategorySeo/ReadyCategorySeoMixFactory.php b/src/Model/CategorySeo/ReadyCategorySeoMixFactory.php new file mode 100644 index 0000000000..a2fbff27a4 --- /dev/null +++ b/src/Model/CategorySeo/ReadyCategorySeoMixFactory.php @@ -0,0 +1,30 @@ +entityNameResolver->resolve(ReadyCategorySeoMix::class); + + return new $entityClassName($readyCategorySeoMixData); + } +} diff --git a/src/Model/CategorySeo/ReadyCategorySeoMixGridFactory.php b/src/Model/CategorySeo/ReadyCategorySeoMixGridFactory.php new file mode 100644 index 0000000000..03d8a5fb7f --- /dev/null +++ b/src/Model/CategorySeo/ReadyCategorySeoMixGridFactory.php @@ -0,0 +1,84 @@ +getAllByDomainIdQueryBuilder($domainId, $locale); + + $dataSource = new QueryBuilderDataSource($queryBuilder, 'rcsmId'); + + $grid = $this->gridFactory->create('ready_category_seo_mix', $dataSource); + + $grid->addColumn('categoryName', 'categoryName', t('Category name')); + $grid->addColumn('friendlyUrlSlug', 'fuSlug', t('Main URL')); + $grid->addColumn('parameters', 'rcsm.choseCategorySeoMixCombinationJson', t('Combination of parameters and their values')); + $grid->addColumn('flagName', 'flagName', t('Flag')); + $grid->addColumn('ordering', 'rcsm.ordering', t('Ordering')); + + $grid->setActionColumnClassAttribute('table-col table-col-10'); + $grid->addEditActionColumn('admin_categoryseo_readycombination', [ + 'categoryId' => 'categoryId', + 'choseCategorySeoMixCombinationJson' => 'rcsm.choseCategorySeoMixCombinationJson', + ]); + $grid->addDeleteActionColumn('admin_categoryseo_delete', ['id' => 'rcsmId']); + + $grid->setTheme('@ShopsysFramework/Admin/Content/CategorySeo/listGrid.html.twig'); + + $grid->enablePaging(); + + return $grid; + } + + /** + * @param int $domainId + * @param string $locale + * @return \Doctrine\ORM\QueryBuilder + */ + public function getAllByDomainIdQueryBuilder(int $domainId, string $locale): QueryBuilder + { + return $this->em->createQueryBuilder() + ->select('rcsm.id as rcsmId, c.id as categoryId, ct.name as categoryName, fu.slug as fuSlug, rcsm.choseCategorySeoMixCombinationJson, ft.name as flagName, rcsm.ordering') + ->from(ReadyCategorySeoMix::class, 'rcsm') + ->andWhere('rcsm.domainId = :domainId') + ->join('rcsm.category', 'c') + ->leftJoin(CategoryTranslation::class, 'ct', Join::WITH, 'ct.translatable = c and ct.locale = :locale ') + ->leftJoin(FriendlyUrl::class, 'fu', Join::WITH, 'fu.routeName = :routeName and fu.entityId = rcsm.id and fu.domainId = :domainId and fu.main = true') + ->leftJoin('rcsm.flag', 'f') + ->leftJoin(FlagTranslation::class, 'ft', Join::WITH, 'ft.translatable = f and ft.locale = :locale') + ->setParameter('locale', $locale) + ->setParameter('domainId', $domainId) + ->setParameter('routeName', 'front_category_seo') + ->orderBy('rcsm.id', 'DESC'); + } +} diff --git a/src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValue.php b/src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValue.php new file mode 100644 index 0000000000..835b7b8ba2 --- /dev/null +++ b/src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValue.php @@ -0,0 +1,80 @@ +parameter = $parameter; + $this->parameterValue = $parameterValue; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix $readyCategorySeoMix + */ + public function setReadyCategorySeoMix($readyCategorySeoMix) + { + $this->readyCategorySeoMix = $readyCategorySeoMix; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\Product\Parameter\Parameter + */ + public function getParameter() + { + return $this->parameter; + } + + /** + * @return \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterValue + */ + public function getParameterValue() + { + return $this->parameterValue; + } +} diff --git a/src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValueFactory.php b/src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValueFactory.php new file mode 100644 index 0000000000..8dfc43553a --- /dev/null +++ b/src/Model/CategorySeo/ReadyCategorySeoMixParameterParameterValueFactory.php @@ -0,0 +1,34 @@ +entityNameResolver->resolve(ReadyCategorySeoMixParameterParameterValue::class); + + return new $entityClassName($parameter, $parameterValue); + } +} diff --git a/src/Model/CategorySeo/ReadyCategorySeoMixRepository.php b/src/Model/CategorySeo/ReadyCategorySeoMixRepository.php new file mode 100644 index 0000000000..0f645c6523 --- /dev/null +++ b/src/Model/CategorySeo/ReadyCategorySeoMixRepository.php @@ -0,0 +1,231 @@ +em->getRepository(ReadyCategorySeoMix::class); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ChoseCategorySeoMixCombination $choseCategorySeoMixCombination + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null + */ + public function findByChoseCategorySeoMixCombination( + ChoseCategorySeoMixCombination $choseCategorySeoMixCombination, + ): ?ReadyCategorySeoMix { + return $this->getRepository()->findOneBy([ + 'choseCategorySeoMixCombinationJson' => $choseCategorySeoMixCombination->getInJson(), + ]); + } + + /** + * @param int $id + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null + */ + public function findById(int $id): ?ReadyCategorySeoMix + { + return $this->getRepository()->find($id); + } + + /** + * @param string $uuid + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null + */ + public function findByUuid(string $uuid): ?ReadyCategorySeoMix + { + return $this->getRepository()->findOneBy([ + 'uuid' => $uuid, + ]); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Parameter\Parameter $parameter + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix[] + */ + public function getAllWithParameter(Parameter $parameter): array + { + return $this->em->createQueryBuilder() + ->select('rcsm') + ->from(ReadyCategorySeoMix::class, 'rcsm') + ->join(ReadyCategorySeoMixParameterParameterValue::class, 'ppv', Join::WITH, 'ppv.readyCategorySeoMix = rcsm') + ->where('ppv.parameter = :parameter') + ->setParameters([ + 'parameter' => $parameter, + ]) + ->getQuery() + ->execute(); + } + + /** + * @param int $categoryId + * @param array $parameterValueIdsByParameterId + * @param int[] $flagIds + * @param string|null $ordering + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix + */ + public function getReadyCategorySeoMixFromFilter( + int $categoryId, + array $parameterValueIdsByParameterId, + array $flagIds, + ?string $ordering, + DomainConfig $domainConfig, + ): ReadyCategorySeoMix { + if (count($flagIds) === 1) { + $flagId = (int)array_shift($flagIds); + } else { + $flagId = null; + } + + $combinationArray = ChoseCategorySeoMixCombination::getChoseCategorySeoMixCombinationArray( + $domainConfig->getId(), + $categoryId, + $flagId, + $ordering, + $parameterValueIdsByParameterId, + ); + + $combinationJson = json_encode($combinationArray, JSON_THROW_ON_ERROR); + + if ($this->isJsonCombinationSeoCategory($categoryId, $domainConfig->getId(), $combinationJson) === false) { + throw new UnableToFindReadyCategorySeoMixException( + 'Unable to find ReadyCategorySeoMix: no exact match by product filter form and ordering', + ); + } + + $readyCategorySeoMix = $this->em->createQueryBuilder() + ->select('rcsm') + ->from(ReadyCategorySeoMix::class, 'rcsm') + ->andWhere('rcsm.choseCategorySeoMixCombinationJson = :combinationJson') + ->setParameter('combinationJson', $combinationJson) + ->getQuery() + ->getOneOrNullResult(); + + if ($readyCategorySeoMix === null) { + throw new UnableToFindReadyCategorySeoMixException( + 'Unable to find ReadyCategorySeoMix: no exact match by product filter form and ordering', + ); + } + + return $readyCategorySeoMix; + } + + /** + * @param int $categoryId + * @param int $domainId + * @param string $combinationJson + * @return bool + */ + protected function isJsonCombinationSeoCategory(int $categoryId, int $domainId, string $combinationJson): bool + { + $readySeoCategorySetup = $this->getReadySeoCategorySetupFromCache($categoryId, $domainId); + + return in_array($combinationJson, $readySeoCategorySetup, true); + } + + /** + * @param int $categoryId + * @param int $domainId + * @return string[] + */ + protected function getReadySeoCategorySetupFromCache(int $categoryId, int $domainId): array + { + return $this->inMemoryCache->getOrSaveValue( + self::READY_SEO_CATEGORY_SETUP_CACHE_NAMESPACE, + function () use ($categoryId, $domainId): array { + $scalarData = $this->em->createQueryBuilder() + ->select('rcsm.choseCategorySeoMixCombinationJson as json') + ->from(ReadyCategorySeoMix::class, 'rcsm') + ->where('IDENTITY(rcsm.category) = :categoryId') + ->andWhere('rcsm.domainId = :domainId') + ->setParameter('categoryId', $categoryId) + ->setParameter('domainId', $domainId) + ->getQuery()->getScalarResult(); + + $readySeoCategorySetup = []; + + foreach ($scalarData as $data) { + $readySeoCategorySetup[] = $data['json']; + } + + return $readySeoCategorySetup; + }, + $categoryId, + $domainId, + ); + } + + /** + * @param int[] $categoryIds + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix[][] + */ + public function getAllIndexedByCategoryId(array $categoryIds, DomainConfig $domainConfig): array + { + $allReadyCategorySeoMixes = array_fill_keys($categoryIds, []); + $result = $this->em->createQueryBuilder() + ->select('rcsm') + ->from(ReadyCategorySeoMix::class, 'rcsm') + ->andWhere('rcsm.category IN(:categories)') + ->andWhere('rcsm.domainId = :domainId') + ->andWhere('rcsm.showInCategory = true') + ->orderBy(OrderByCollationHelper::createOrderByForLocale('rcsm.h1', $domainConfig->getLocale()), 'asc') + ->setParameters([ + 'categories' => $categoryIds, + 'domainId' => $domainConfig->getId(), + ]) + ->getQuery() + ->execute(); + + /** @var \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix $readyCategorySeoMix */ + foreach ($result as $readyCategorySeoMix) { + $allReadyCategorySeoMixes[$readyCategorySeoMix->getCategory()->getId()][] = $readyCategorySeoMix; + } + + return $allReadyCategorySeoMixes; + } + + /** + * @return array + */ + public function getAllCategoryIdsInSeoMixes(): array + { + $result = $this->em->createQueryBuilder() + ->select('identity(rcsm.category) as categoryId') + ->from(ReadyCategorySeoMix::class, 'rcsm') + ->distinct() + ->getQuery() + ->getArrayResult(); + + return array_column($result, 'categoryId'); + } +} diff --git a/src/Model/Product/Filter/ProductFilterConfigFactory.php b/src/Model/Product/Filter/ProductFilterConfigFactory.php index 3b250179b1..8cc3427477 100644 --- a/src/Model/Product/Filter/ProductFilterConfigFactory.php +++ b/src/Model/Product/Filter/ProductFilterConfigFactory.php @@ -8,6 +8,7 @@ use Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser; use Shopsys\FrameworkBundle\Model\Product\Brand\Brand; use Shopsys\FrameworkBundle\Model\Product\Brand\BrandFacade; +use Shopsys\FrameworkBundle\Model\Product\Flag\Flag; use Shopsys\FrameworkBundle\Model\Product\Flag\FlagFacade; use Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterFacade; @@ -163,4 +164,24 @@ protected function getSortedParameterFilterChoicesForCategory( return $sortedParameterFilterChoices; } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Flag\Flag $flag + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterConfig + */ + public function createForFlag(Flag $flag, string $locale): ProductFilterConfig + { + $productFilterConfigIdsData = $this->productFilterElasticFacade->getProductFilterDataInFlag( + $flag->getId(), + $this->currentCustomerUser->getPricingGroup(), + ); + + return new ProductFilterConfig( + [], + $this->flagFacade->getVisibleFlagsByIds($productFilterConfigIdsData->getFlagIds(), $locale), + $this->brandFacade->getBrandsByIds($productFilterConfigIdsData->getBrandIds()), + $productFilterConfigIdsData->getPriceRange(), + ); + } } diff --git a/src/Model/Product/Filter/ProductFilterDataFactory.php b/src/Model/Product/Filter/ProductFilterDataFactory.php index d32b43eb22..7bba3309ba 100644 --- a/src/Model/Product/Filter/ProductFilterDataFactory.php +++ b/src/Model/Product/Filter/ProductFilterDataFactory.php @@ -4,6 +4,8 @@ namespace Shopsys\FrameworkBundle\Model\Product\Filter; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix; + class ProductFilterDataFactory { /** @@ -13,4 +15,25 @@ public function create(): ProductFilterData { return new ProductFilterData(); } + + /** + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix $readyCategorySeoMix + * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData + */ + public function updateProductFilterDataFromReadyCategorySeoMix( + ReadyCategorySeoMix $readyCategorySeoMix, + ProductFilterData $productFilterData, + ): void { + if ($readyCategorySeoMix->getFlag() !== null) { + $productFilterData->flags[] = $readyCategorySeoMix->getFlag(); + $productFilterData->flags = array_values($productFilterData->flags); + } + + foreach ($readyCategorySeoMix->getReadyCategorySeoMixParameterParameterValues() as $readyCategorySeoMixParameterParameterValue) { + $parameterFilterData = new ParameterFilterData(); + $parameterFilterData->parameter = $readyCategorySeoMixParameterParameterValue->getParameter(); + $parameterFilterData->values = [$readyCategorySeoMixParameterParameterValue->getParameterValue()]; + $productFilterData->parameters[] = $parameterFilterData; + } + } } diff --git a/src/Model/Product/Flag/FlagFacade.php b/src/Model/Product/Flag/FlagFacade.php index 82444466d9..cb4c188c40 100644 --- a/src/Model/Product/Flag/FlagFacade.php +++ b/src/Model/Product/Flag/FlagFacade.php @@ -131,4 +131,13 @@ public function getVisibleFlagsByIds(array $flagsIds, string $locale): array { return $this->flagRepository->getVisibleFlagsByIds($flagsIds, $locale); } + + /** + * @param string[] $flagUuids + * @return int[] + */ + public function getFlagIdsByUuids(array $flagUuids): array + { + return $this->flagRepository->getFlagIdsByUuids($flagUuids); + } } diff --git a/src/Model/Product/Flag/FlagRepository.php b/src/Model/Product/Flag/FlagRepository.php index e559ba59f0..136d7baeee 100644 --- a/src/Model/Product/Flag/FlagRepository.php +++ b/src/Model/Product/Flag/FlagRepository.php @@ -122,4 +122,19 @@ public function getVisibleQueryBuilder(): QueryBuilder ->select('f') ->where('f.visible = true'); } + + /** + * @param string[] $flagUuids + * @return int[] + */ + public function getFlagIdsByUuids(array $flagUuids): array + { + $queryBuilder = $this->em->createQueryBuilder() + ->select('f.id') + ->from(Flag::class, 'f') + ->where('f.uuid IN (:uuids)') + ->setParameter('uuids', $flagUuids); + + return array_column($queryBuilder->getQuery()->getArrayResult(), 'id'); + } } diff --git a/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php b/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php index 3ee135159d..dea8ae9a2d 100644 --- a/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php +++ b/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php @@ -4,17 +4,19 @@ namespace Shopsys\FrameworkBundle\Model\Product\Listing; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix; use Symfony\Component\HttpFoundation\Request; abstract class ProductListOrderingModeForListFacade { - protected const COOKIE_NAME = 'productListOrderingMode'; + protected const string COOKIE_NAME = 'productListOrderingMode'; /** * @param \Shopsys\FrameworkBundle\Model\Product\Listing\RequestToOrderingModeIdConverter $requestToOrderingModeIdConverter */ - public function __construct(protected readonly RequestToOrderingModeIdConverter $requestToOrderingModeIdConverter) - { + public function __construct( + protected readonly RequestToOrderingModeIdConverter $requestToOrderingModeIdConverter, + ) { } /** @@ -31,13 +33,17 @@ public function getProductListOrderingConfig() /** * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null $readyCategorySeoMix * @return string */ - public function getOrderingModeIdFromRequest(Request $request) - { + public function getOrderingModeIdFromRequest( + Request $request, + ?ReadyCategorySeoMix $readyCategorySeoMix = null, + ) { return $this->requestToOrderingModeIdConverter->getOrderingModeIdFromRequest( $request, $this->getProductListOrderingConfig(), + $readyCategorySeoMix, ); } diff --git a/src/Model/Product/Listing/RequestToOrderingModeIdConverter.php b/src/Model/Product/Listing/RequestToOrderingModeIdConverter.php index b14734c0c2..5a390287ed 100644 --- a/src/Model/Product/Listing/RequestToOrderingModeIdConverter.php +++ b/src/Model/Product/Listing/RequestToOrderingModeIdConverter.php @@ -4,6 +4,7 @@ namespace Shopsys\FrameworkBundle\Model\Product\Listing; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix; use Symfony\Component\HttpFoundation\Request; class RequestToOrderingModeIdConverter @@ -11,12 +12,22 @@ class RequestToOrderingModeIdConverter /** * @param \Symfony\Component\HttpFoundation\Request $request * @param \Shopsys\FrameworkBundle\Model\Product\Listing\ProductListOrderingConfig $productListOrderingConfig + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null $readyCategorySeoMix * @return string */ public function getOrderingModeIdFromRequest( Request $request, ProductListOrderingConfig $productListOrderingConfig, + ?ReadyCategorySeoMix $readyCategorySeoMix = null, ) { + if ($readyCategorySeoMix !== null) { + $readyCategorySeoMixOrderingModeId = $readyCategorySeoMix->getOrdering(); + + if ($readyCategorySeoMixOrderingModeId !== null) { + return $readyCategorySeoMixOrderingModeId; + } + } + $orderingModeId = $request->cookies->get($productListOrderingConfig->getCookieName()); if (!in_array($orderingModeId, $productListOrderingConfig->getSupportedOrderingModeIds(), true)) { diff --git a/src/Model/Product/Parameter/ParameterFacade.php b/src/Model/Product/Parameter/ParameterFacade.php index 717905192a..844b6b44f5 100644 --- a/src/Model/Product/Parameter/ParameterFacade.php +++ b/src/Model/Product/Parameter/ParameterFacade.php @@ -5,12 +5,15 @@ namespace Shopsys\FrameworkBundle\Model\Product\Parameter; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\NoResultException; use Shopsys\FrameworkBundle\Component\UploadedFile\Config\UploadedFileTypeConfig; use Shopsys\FrameworkBundle\Component\UploadedFile\UploadedFileFacade; use Shopsys\FrameworkBundle\Model\Category\Category; use Shopsys\FrameworkBundle\Model\Category\CategoryParameterRepository; +use Shopsys\FrameworkBundle\Model\CategorySeo\DeleteReadyCategorySeoMixFacade; use Shopsys\FrameworkBundle\Model\Product\Elasticsearch\Scope\ProductExportScopeConfig; use Shopsys\FrameworkBundle\Model\Product\Filter\ParameterFilterChoice; +use Shopsys\FrameworkBundle\Model\Product\Parameter\Exception\ParameterValueNotFoundException; use Shopsys\FrameworkBundle\Model\Product\Recalculation\ProductRecalculationDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -26,6 +29,7 @@ class ParameterFacade * @param \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterValueDataFactory $parameterValueDataFactory * @param \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterValueFactory $parameterValueFactory * @param \Shopsys\FrameworkBundle\Model\Product\Recalculation\ProductRecalculationDispatcher $productRecalculationDispatcher + * @param \Shopsys\FrameworkBundle\Model\CategorySeo\DeleteReadyCategorySeoMixFacade $deleteReadyCategorySeoMixFacade */ public function __construct( protected readonly EntityManagerInterface $em, @@ -37,6 +41,7 @@ public function __construct( protected readonly ParameterValueDataFactory $parameterValueDataFactory, protected readonly ParameterValueFactory $parameterValueFactory, protected readonly ProductRecalculationDispatcher $productRecalculationDispatcher, + protected readonly DeleteReadyCategorySeoMixFacade $deleteReadyCategorySeoMixFacade, ) { } @@ -142,6 +147,8 @@ public function deleteById($parameterId) { $parameter = $this->parameterRepository->getById($parameterId); + $this->deleteReadyCategorySeoMixFacade->deleteAllWithParameter($parameter); + $this->em->remove($parameter); $this->dispatchParameterEvent($parameter, ParameterEvent::DELETE); @@ -346,4 +353,36 @@ public function getCountOfParameterValuesWithoutTheirsNumericValueFilledQueryBui { return $this->parameterRepository->getCountOfParameterValuesWithoutTheirsNumericValueFilledQueryBuilder($parameter); } + + /** + * @param string[] $parameterUuids + * @return array + */ + public function getParameterIdsIndexedByUuids(array $parameterUuids): array + { + return $this->parameterRepository->getParameterIdsIndexedByUuids($parameterUuids); + } + + /** + * @param string[] $parameterValueUuids + * @return array + */ + public function getParameterValueIdsIndexedByUuids(array $parameterValueUuids): array + { + return $this->parameterRepository->getParameterValueIdsIndexedByUuids($parameterValueUuids); + } + + /** + * @param string $text + * @param string $locale + * @return int + */ + public function getParameterValueIdByText(string $text, string $locale): int + { + try { + return $this->parameterRepository->getParameterValueIdByText($text, $locale); + } catch (NoResultException) { + throw new ParameterValueNotFoundException(); + } + } } diff --git a/src/Model/Product/Parameter/ParameterRepository.php b/src/Model/Product/Parameter/ParameterRepository.php index 96ccb723da..b8ec84a921 100644 --- a/src/Model/Product/Parameter/ParameterRepository.php +++ b/src/Model/Product/Parameter/ParameterRepository.php @@ -651,8 +651,116 @@ public function existsParameterByName(string $name, string $locale, ?Parameter $ } return $queryBuilder - ->getQuery() - ->getSingleScalarResult() > 0; + ->getQuery() + ->getSingleScalarResult() > 0; + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Category\Category $category + * @param int $domainId + * @return \Shopsys\FrameworkBundle\Model\Product\Parameter\Parameter[] + */ + public function getParametersUsedByProductsInCategoryWithoutSlider(Category $category, int $domainId): array + { + $queryBuilder = $this->getParameterRepository()->createQueryBuilder('p') + ->select('p') + ->join(ProductParameterValue::class, 'ppv', Join::WITH, 'p = ppv.parameter') + ->where('p.parameterType != :parameterType') + ->setParameter('parameterType', Parameter::PARAMETER_TYPE_SLIDER) + ->orderBy('p.orderingPriority', 'DESC'); + + $this->applyCategorySeoConditions($queryBuilder, $category, $domainId); + + return $queryBuilder->getQuery()->execute(); + } + + /** + * @param \Shopsys\FrameworkBundle\Model\Category\Category $category + * @param \Shopsys\FrameworkBundle\Model\Product\Parameter\Parameter $parameter + * @param int $domainId + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterValue[] + */ + public function getParameterValuesUsedByProductsInCategoryByParameter( + Category $category, + Parameter $parameter, + int $domainId, + string $locale, + ): array { + $queryBuilder = $this->getParameterValueRepository()->createQueryBuilder('pv') + ->select('pv') + ->andWhere('ppv.parameter = :parameter') + ->setParameter('parameter', $parameter) + ->join(ProductParameterValue::class, 'ppv', Join::WITH, 'pv = ppv.value and pv.locale = :locale') + ->setParameter(':locale', $locale) + ->groupBy('pv'); + + $this->applyCategorySeoConditions($queryBuilder, $category, $domainId); + + return $queryBuilder->getQuery()->execute(); + } + + /** + * @param string[] $parameterUuids + * @return array + */ + public function getParameterIdsIndexedByUuids(array $parameterUuids): array + { + return $this->getIdsIndexedByUuids($parameterUuids, Parameter::class); + } + + /** + * @param string[] $parameterValueUuids + * @return array + */ + public function getParameterValueIdsIndexedByUuids(array $parameterValueUuids): array + { + return $this->getIdsIndexedByUuids($parameterValueUuids, ParameterValue::class); + } + + /** + * @param string $text + * @param string $locale + * @return int + */ + public function getParameterValueIdByText(string $text, string $locale): int + { + return $this->em->createQueryBuilder() + ->select('pv.id') + ->from(ParameterValue::class, 'pv') + ->where('pv.text = :text') + ->andWhere('pv.locale = :locale') + ->setParameters([ + 'text' => $text, + 'locale' => $locale, + ]) + ->getQuery()->getSingleScalarResult(); + } + + /** + * @param string[] $uuids + * @param string $entityName + * @return array + */ + protected function getIdsIndexedByUuids(array $uuids, string $entityName): array + { + $queryBuilder = $this->em->createQueryBuilder() + ->select('p.id, p.uuid') + ->from($entityName, 'p') + ->where('p.uuid IN (:uuids)') + ->setParameter('uuids', $uuids); + + if ($entityName === Parameter::class) { + $queryBuilder->orderBy('p.orderingPriority', 'DESC'); + } + + $idsIndexedByUuids = []; + + foreach ($queryBuilder->getQuery()->getArrayResult() as $idAndUuid) { + $idsIndexedByUuids[$idAndUuid['uuid']] = $idAndUuid['id']; + } + + return $idsIndexedByUuids; } /** diff --git a/src/Model/Product/ProductOnCurrentDomainElasticFacade.php b/src/Model/Product/ProductOnCurrentDomainElasticFacade.php index 677b183ccb..cfacebf19b 100644 --- a/src/Model/Product/ProductOnCurrentDomainElasticFacade.php +++ b/src/Model/Product/ProductOnCurrentDomainElasticFacade.php @@ -136,4 +136,13 @@ public function getProductFilterCountDataForFlag( $filterQuery, ); } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData + * @return int[] + */ + public function getCategoryIdsForFilterData(ProductFilterData $productFilterData): array + { + return $this->productElasticsearchRepository->getCategoryIdsForFilterData($productFilterData); + } } diff --git a/src/Model/Product/Search/ProductElasticsearchRepository.php b/src/Model/Product/Search/ProductElasticsearchRepository.php index 386b171eb7..7e8c5572d6 100644 --- a/src/Model/Product/Search/ProductElasticsearchRepository.php +++ b/src/Model/Product/Search/ProductElasticsearchRepository.php @@ -9,6 +9,7 @@ use Shopsys\FrameworkBundle\Component\Cache\InMemoryCache; use Shopsys\FrameworkBundle\Component\Elasticsearch\IndexDefinitionLoader; use Shopsys\FrameworkBundle\Model\Product\Elasticsearch\ProductIndex; +use Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData; class ProductElasticsearchRepository { @@ -231,4 +232,32 @@ protected function extractIdsFromFields(array $result): array return $ids; } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData + * @return int[] + */ + public function getCategoryIdsForFilterData(ProductFilterData $productFilterData): array + { + $result = $this->client->search( + $this->filterQueryFactory->createListableWithProductFilter($productFilterData)->setLimit(0)->getAggregationQueryForProductCountInCategories(), + ); + + return $this->extractCategoryIdsAggregation($result); + } + + /** + * @param array $productCountAggregation + * @return int[] + */ + protected function extractCategoryIdsAggregation(array $productCountAggregation): array + { + $result = []; + + foreach ($productCountAggregation['aggregations']['by_categories']['buckets'] as $categoryAggregation) { + $result[] = $categoryAggregation['key']; + } + + return $result; + } } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 25996f34e9..effc667989 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -653,6 +653,10 @@ services: Shopsys\FrameworkBundle\Model\Cart\Watcher\CartWatcher: ~ + Shopsys\FrameworkBundle\Model\CategorySeo\CategorySeoFriendlyUrlDataProvider: + tags: + - {name: shopsys.friendly_url_provider} + Shopsys\FrameworkBundle\Model\Category\CategoryDataFactoryInterface: alias: Shopsys\FrameworkBundle\Model\Category\CategoryDataFactory diff --git a/src/Resources/translations/messages.cs.po b/src/Resources/translations/messages.cs.po index 19b9382536..eb9a73e068 100644 --- a/src/Resources/translations/messages.cs.po +++ b/src/Resources/translations/messages.cs.po @@ -70,6 +70,9 @@ msgstr "Neexistují žádné jednotky, musíte nějakou vy msgid "User consent policy article for domain {{ domainName }} is not set." msgstr "Není nastaven článek správa osobních údajů pro doménu {{ domainName }}." +msgid "SEO category has been saved" +msgstr "SEO kombinace kategorie byla uložena" + msgid "Accessories" msgstr "Příslušenství" @@ -379,6 +382,9 @@ msgstr "Prům. trvání (mm:ss)" msgid "B2B data and user management" msgstr "Správa B2B údajů a uživatelů" +msgid "Back to category selection" +msgstr "Zpět na volbu kategorie" + msgid "Back to customer {{ name }}" msgstr "Zpět k zákazníkovi {{ name }}" @@ -388,6 +394,12 @@ msgstr "Zpět na nástěnku" msgid "Back to overview" msgstr "Zpět na přehled" +msgid "Back to overview of available combinations" +msgstr "Zpět na přehled možných kombinací" + +msgid "Back to parameter selection" +msgstr "Zpět na volbu parametrů" + msgid "Basic" msgstr "Základní" @@ -523,6 +535,9 @@ msgstr "Hromadné generování kupónu" msgid "But at other addresses we have a lot for you..." msgstr "Ale na jiných adresách toho pro Vás máme spousty..." +msgid "By flag" +msgstr "Dle příznaku" + msgid "CSS documentation" msgstr "CSS dokumentace" @@ -571,9 +586,15 @@ msgstr "Byla upravena kategorie {{ name }}{{ name }} deleted" msgstr "Kategorie {{ name }} byla smazána" +msgid "Category description" +msgstr "Popis kategorie" + msgid "Category guidepost on main page" msgstr "Rozcestník kategorií na titulní stránce" +msgid "Category name" +msgstr "Název kategorie" + msgid "Category with prefilled filters" msgstr "Kategorie s předvyplněnými filtry" @@ -607,6 +628,9 @@ msgstr "Vyberte náhradu" msgid "Choose the article that provides information about how user consent is obtained, managed, and withdrawn on this domain." msgstr "Vyberte článek, který poskytuje informace o tom, jak je na této doméně získáván, spravován a odvoláván souhlas se správou osobních údajů." +msgid "Choose this category and continue" +msgstr "Zvolit tuto kategorii a pokračovat" + msgid "Choose type of redirect" msgstr "Vyberte typ přesměrování" @@ -658,6 +682,9 @@ msgstr "Hodnota parametru typu barva - vše" msgid "Color parameter values - view" msgstr "Hodnota parametru typu barva - zobrazení" +msgid "Combination of parameters and their values" +msgstr "Kombinace parametrů a jejich hodnot parametrů" + msgid "Communication with customer" msgstr "Komunikace se zákazníkem" @@ -1114,6 +1141,9 @@ msgstr "Smazat" msgid "Delete and set" msgstr "Smazat a nastavit" +msgid "Delete category with SEO mix" +msgstr "Smazat kategorii i SEO mix" + msgid "Delete file" msgstr "Smazat soubor" @@ -1258,6 +1288,9 @@ msgstr "Opravdu chcete odebrat tento článek blogu?" msgid "Do you really want to remove this brand? If it is used anywhere it will be unset." msgstr "Opravdu chcete odstranit tuto značku? Pokud je někde použita, bude odnastavena." +msgid "Do you really want to remove this category even with their SEO mixes?" +msgstr "Opravdu chcete smazat tuto kategorii i s náležícími SEO mixy?" + msgid "Do you really want to remove this customer user role group?" msgstr "Opravdu chcete odstranit tuto skupinu práv" @@ -1723,6 +1756,30 @@ msgstr "Exportovat seznam e-mailů jako CSV" msgid "Ext." msgstr "Příp." +msgid "Extended SEO categories" +msgstr "Rozšířené SEO kategorie" + +msgid "Extended SEO category %categoryName% - combinations" +msgstr "Rozšířená SEO kategorie %categoryName% - kombinace" + +msgid "Extended SEO category %categoryName% - filters" +msgstr "Rozšířená SEO kategorie %categoryName% - filtry" + +msgid "Extended SEO category - category selection" +msgstr "Rozšířená SEO kategorie - volba kategorie" + +msgid "Extended SEO category - combinations" +msgstr "Rozšířená SEO kategorie - kombinace" + +msgid "Extended SEO category - editing the combination with SEO values" +msgstr "Rozšířená SEO kategorie - editace kombinace se SEO hodnotami" + +msgid "Extended SEO category - filters" +msgstr "Rozšířená SEO kategorie - filtry" + +msgid "Extended SEO category - set combinations with SEO values" +msgstr "Rozšířená SEO kategorie - kombinace s nastavenými SEO hodnotami" + msgid "External ID" msgstr "Externí ID" @@ -1801,6 +1858,12 @@ msgstr "Přehled souborů" msgid "Files uploaded:" msgstr "Soubory nahrány:" +msgid "Fill URL also for selected domain" +msgstr "Vyplňte také URL pro zvolenou doménu" + +msgid "Fill URL only for selected domain" +msgstr "Vyplňte pouze URL pro zvolenou doménu" + msgid "Fill missing settings to enable creating products. More information here." msgstr "Pro vytvoření produktu doplňte chybějící nastavení. Více informací zde." @@ -2221,6 +2284,9 @@ msgstr "Šablony e-mailů - vše" msgid "Mail templates - view" msgstr "Šablony e-mailů - zobrazení" +msgid "Main URL" +msgstr "Hlavní URL" + msgid "Main URL can't be deleted" msgstr "Hlavní URL nelze smazat" @@ -2467,6 +2533,9 @@ msgstr "Nebyly nalezeny žádné články blogu." msgid "No brands found. You have to create some first." msgstr "Nebyly nalezeny žádné značky. Nejprve musíte nějaké vytvořit." +msgid "No combinations of SEO categories found." +msgstr "Nebyly nalezeny žádné kombinace SEO kategorií." + msgid "No complaint statuses found." msgstr "Nebyly nalezeny žádné stavy reklamací." @@ -2707,6 +2776,9 @@ msgstr "Stránka s potvrzením objednávky - vše" msgid "Order submitted page setting - view" msgstr "Stránka s potvrzením objednávky - zobrazení" +msgid "Ordering" +msgstr "Řazení" + msgid "Orders" msgstr "Objednávky" @@ -3073,6 +3145,9 @@ msgstr "Zboží vyřazené z prodeje se nezobrazuje ve výpisech ani nelze vyhle msgid "Products overview" msgstr "Přehled zboží" +msgid "Products parameters of selected category" +msgstr "Parametry produktů vybrané kategorie" + msgid "Products with this flag" msgstr "Produkty s tímto příznakem" @@ -3116,7 +3191,7 @@ msgid "Quick search" msgstr "Rychlé vyhledávání" msgid "RGB Hex" -msgstr "" +msgstr "RGB Hex" msgid "Rate can't be modified. It is necessary to create new rate, remove existing one and replace it with new one." msgstr "Výši sazby nelze upravit. Je třeba vytvořit novou sazbu a starou odstranit s nahrazením za novou." @@ -3235,6 +3310,12 @@ msgstr "Nastavení SEO atributů" msgid "SEO attributes settings modified" msgstr "Natavení SEO atributů bylo upraveno" +msgid "SEO combination of category with ID {{ ReadyCategorySeoMixId }} has been removed" +msgstr "SEO kombinace kategorie s ID {{ ReadyCategorySeoMixId }} byla smazána" + +msgid "SEO combination of category with ID {{ ReadyCategorySeoMixId }} has not been removed, because it was not found" +msgstr "SEO kombinace kategorie s ID {{ ReadyCategorySeoMixId }} nebyla smazána, protože nebyla nalezena" + msgid "SEO page" msgstr "SEO stránka" @@ -3526,6 +3607,15 @@ msgstr "Krátký popis USP" msgid "Short description can be set in the main variant." msgstr "Krátký popis se nastavuje u hlavní varianty." +msgid "Short description of category" +msgstr "Krátký popis kategorie" + +msgid "Show combinations" +msgstr "Zobrazit kombinace" + +msgid "Show in the category" +msgstr "Zobrazit v rozcestníku" + msgid "Site" msgstr "Stránka" @@ -3551,7 +3641,7 @@ msgid "Slider pages" msgstr "Stránky slideru" msgid "Slug" -msgstr "" +msgstr "Slug" msgid "Some data may not be localized or serve the technical administrators of the e-shop." msgstr "Některé údaje nemusí být lokalizovány nebo slouží technickým správcům e-shopu." @@ -3856,6 +3946,9 @@ msgstr "Typ" msgid "URL" msgstr "URL" +msgid "URL Settings" +msgstr "Nastavení URL" + msgid "URL addresses" msgstr "URL adresy" @@ -4300,6 +4393,9 @@ msgstr "zboží" msgid "promo codes" msgstr "slevové kupóny" +msgid "ready category seo mixes" +msgstr "SEO kombinace kategorií" + msgid "records" msgstr "záznamů" diff --git a/src/Resources/translations/messages.en.po b/src/Resources/translations/messages.en.po index 51e850d956..74f724598c 100644 --- a/src/Resources/translations/messages.en.po +++ b/src/Resources/translations/messages.en.po @@ -70,6 +70,9 @@ msgstr "" msgid "User consent policy article for domain {{ domainName }} is not set." msgstr "" +msgid "SEO category has been saved" +msgstr "" + msgid "Accessories" msgstr "" @@ -379,6 +382,9 @@ msgstr "" msgid "B2B data and user management" msgstr "" +msgid "Back to category selection" +msgstr "" + msgid "Back to customer {{ name }}" msgstr "" @@ -388,6 +394,12 @@ msgstr "" msgid "Back to overview" msgstr "" +msgid "Back to overview of available combinations" +msgstr "" + +msgid "Back to parameter selection" +msgstr "" + msgid "Basic" msgstr "" @@ -523,6 +535,9 @@ msgstr "" msgid "But at other addresses we have a lot for you..." msgstr "" +msgid "By flag" +msgstr "" + msgid "CSS documentation" msgstr "" @@ -571,9 +586,15 @@ msgstr "" msgid "Category {{ name }} deleted" msgstr "" +msgid "Category description" +msgstr "" + msgid "Category guidepost on main page" msgstr "" +msgid "Category name" +msgstr "" + msgid "Category with prefilled filters" msgstr "" @@ -607,6 +628,9 @@ msgstr "" msgid "Choose the article that provides information about how user consent is obtained, managed, and withdrawn on this domain." msgstr "" +msgid "Choose this category and continue" +msgstr "" + msgid "Choose type of redirect" msgstr "" @@ -658,6 +682,9 @@ msgstr "" msgid "Color parameter values - view" msgstr "" +msgid "Combination of parameters and their values" +msgstr "" + msgid "Communication with customer" msgstr "" @@ -1114,6 +1141,9 @@ msgstr "" msgid "Delete and set" msgstr "" +msgid "Delete category with SEO mix" +msgstr "" + msgid "Delete file" msgstr "" @@ -1258,6 +1288,9 @@ msgstr "" msgid "Do you really want to remove this brand? If it is used anywhere it will be unset." msgstr "" +msgid "Do you really want to remove this category even with their SEO mixes?" +msgstr "" + msgid "Do you really want to remove this customer user role group?" msgstr "" @@ -1723,6 +1756,30 @@ msgstr "" msgid "Ext." msgstr "" +msgid "Extended SEO categories" +msgstr "" + +msgid "Extended SEO category %categoryName% - combinations" +msgstr "" + +msgid "Extended SEO category %categoryName% - filters" +msgstr "" + +msgid "Extended SEO category - category selection" +msgstr "" + +msgid "Extended SEO category - combinations" +msgstr "" + +msgid "Extended SEO category - editing the combination with SEO values" +msgstr "" + +msgid "Extended SEO category - filters" +msgstr "" + +msgid "Extended SEO category - set combinations with SEO values" +msgstr "" + msgid "External ID" msgstr "" @@ -1801,6 +1858,12 @@ msgstr "" msgid "Files uploaded:" msgstr "" +msgid "Fill URL also for selected domain" +msgstr "" + +msgid "Fill URL only for selected domain" +msgstr "" + msgid "Fill missing settings to enable creating products. More information here." msgstr "" @@ -2221,6 +2284,9 @@ msgstr "" msgid "Mail templates - view" msgstr "" +msgid "Main URL" +msgstr "" + msgid "Main URL can't be deleted" msgstr "" @@ -2467,6 +2533,9 @@ msgstr "" msgid "No brands found. You have to create some first." msgstr "" +msgid "No combinations of SEO categories found." +msgstr "" + msgid "No complaint statuses found." msgstr "" @@ -2707,6 +2776,9 @@ msgstr "" msgid "Order submitted page setting - view" msgstr "" +msgid "Ordering" +msgstr "" + msgid "Orders" msgstr "" @@ -3073,6 +3145,9 @@ msgstr "" msgid "Products overview" msgstr "" +msgid "Products parameters of selected category" +msgstr "" + msgid "Products with this flag" msgstr "" @@ -3235,6 +3310,12 @@ msgstr "" msgid "SEO attributes settings modified" msgstr "" +msgid "SEO combination of category with ID {{ ReadyCategorySeoMixId }} has been removed" +msgstr "" + +msgid "SEO combination of category with ID {{ ReadyCategorySeoMixId }} has not been removed, because it was not found" +msgstr "" + msgid "SEO page" msgstr "" @@ -3526,6 +3607,15 @@ msgstr "" msgid "Short description can be set in the main variant." msgstr "" +msgid "Short description of category" +msgstr "" + +msgid "Show combinations" +msgstr "" + +msgid "Show in the category" +msgstr "" + msgid "Site" msgstr "" @@ -3856,6 +3946,9 @@ msgstr "" msgid "URL" msgstr "" +msgid "URL Settings" +msgstr "" + msgid "URL addresses" msgstr "" @@ -4300,6 +4393,9 @@ msgstr "" msgid "promo codes" msgstr "" +msgid "ready category seo mixes" +msgstr "" + msgid "records" msgstr "" diff --git a/src/Resources/views/Admin/Content/Category/list.html.twig b/src/Resources/views/Admin/Content/Category/list.html.twig index 33fb0d353e..7b3eb10302 100644 --- a/src/Resources/views/Admin/Content/Category/list.html.twig +++ b/src/Resources/views/Admin/Content/Category/list.html.twig @@ -15,20 +15,18 @@ {% block main_content %} {{ render(controller('Shopsys\\FrameworkBundle\\Controller\\Admin\\DomainFilterController::domainFilterTabsAction', { namespace: domainFilterNamespace })) }} - {% macro categoryTreeItem(categoriesWithPreloadedChildren, isFirstLevel) %} + {% macro categoryTreeItem(categoriesWithPreloadedChildren, isFirstLevel, disabledFormFields, allCategoryIdsInSeoMixes) %} {% import _self as self %} -
    +
      {% for categoryWithPreloadedChildren in categoriesWithPreloadedChildren %} -
    • -
      - + {% set categoryHasSeoMix = categoryWithPreloadedChildren.category.id in allCategoryIdsInSeoMixes ? true : false %} +
    • +
      {{ categoryWithPreloadedChildren.category.name }} - + {{ icon('pencil') }} {% set csrfTokenId = constant('Shopsys\\FrameworkBundle\\Component\\Router\\Security\\RouteCsrfProtector::CSRF_TOKEN_ID_PREFIX') ~ 'admin_category_delete' %} {% set categoryDeleteUrl = url('admin_category_delete', { @@ -36,14 +34,14 @@ (constant('Shopsys\\FrameworkBundle\\Component\\Router\\Security\\RouteCsrfProtector::CSRF_TOKEN_REQUEST_PARAMETER')): csrf_token(csrfTokenId) }) %} - + {{ icon('trash') }}
      - {{ self.categoryTreeItem(categoryWithPreloadedChildren.children, false) }} + {{ self.categoryTreeItem(categoryWithPreloadedChildren.children, false, disabledFormFields, allCategoryIdsInSeoMixes) }}
    • {% endfor %}
    @@ -52,8 +50,8 @@ {% if isForAllDomains %}
    -
    - {{ self.categoryTreeItem(categoriesWithPreloadedChildren, true) }} +
    + {{ self.categoryTreeItem(categoriesWithPreloadedChildren, true, disabledFormFields, allCategoryIdsInSeoMixes) }}
    @@ -71,13 +69,16 @@ {% endblock %} {% endembed %} {% else %} -
    - {{ 'In a particular domain tab it is not possible to adjust the order and plunge of categories. Please go to the category detail or to overview of categories of all domains'|trans }} -
    + {% if disabledFormFields == false %} +
    + {{ 'In a particular domain tab it is not possible to adjust the order and plunge of categories. Please go to the category detail or to overview of categories of all domains'|trans }} + +
    + {% endif %}
    -
    - {{ self.categoryTreeItem(categoriesWithPreloadedChildren, true) }} +
    + {{ self.categoryTreeItem(categoriesWithPreloadedChildren, true, disabledFormFields, allCategoryIdsInSeoMixes) }}
    diff --git a/src/Resources/views/Admin/Content/CategorySeo/list.html.twig b/src/Resources/views/Admin/Content/CategorySeo/list.html.twig new file mode 100644 index 0000000000..57636fa0df --- /dev/null +++ b/src/Resources/views/Admin/Content/CategorySeo/list.html.twig @@ -0,0 +1,18 @@ +{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} + +{% block title %}- {{ 'Extended SEO categories'|trans }}{% endblock %} +{% block h1 %}{{ 'Extended SEO categories'|trans }}{% endblock %} + +{% block btn %} + + +{{ 'Create'|trans }} + +{% endblock %} + +{% block main_content %} + + {{ render(controller('Shopsys\\FrameworkBundle\\Controller\\Admin\\DomainController::domainTabsAction')) }} + + {{ gridView.render() }} + +{% endblock %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/listGrid.html.twig b/src/Resources/views/Admin/Content/CategorySeo/listGrid.html.twig new file mode 100644 index 0000000000..583cf7ea92 --- /dev/null +++ b/src/Resources/views/Admin/Content/CategorySeo/listGrid.html.twig @@ -0,0 +1,27 @@ +{% extends '@ShopsysFramework/Admin/Grid/Grid.html.twig' %} + +{% block grid_no_data %} + {{ 'No combinations of SEO categories found.'|trans }} +{% endblock %} + +{% block grid_pager_totalcount %} + {% set entityName = 'ready category seo mixes'|trans %} + {{ parent() }} +{% endblock %} + +{% block grid_value_cell_id_parameters %} + + {% for parameterNameParameterValuePair in getReadyCategoryMixCombinationParametersPairsIterator(row.choseCategorySeoMixCombinationJson) %} + {{ parameterNameParameterValuePair }}
    + {% endfor %} + +{% endblock %} + + +{% block grid_value_cell_id_ordering %} + {{ getOrderingNameByOrderingId(row.ordering) }} +{% endblock %} + +{% block grid_value_cell_id_friendlyUrlSlug %} + {{ row.fuSlug }} +{% endblock %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig b/src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig new file mode 100644 index 0000000000..8aa330d5c4 --- /dev/null +++ b/src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig @@ -0,0 +1,43 @@ +{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% import _self as self %} + +{% block title %}- {{ 'Extended SEO category - category selection'|trans }}{% endblock %} +{% block h1 %}{{ 'Extended SEO category - category selection'|trans }}{% endblock %} + +{% block main_content %} + + {% macro categoryTreeItem(categoriesWithPreloadedChildren, isFirstLevel, locale) %} + {% import _self as self %} +
      + {% for categoryWithPreloadedChildren in categoriesWithPreloadedChildren %} +
    • +
      + + {{ categoryWithPreloadedChildren.category.name(locale) }} + + + {{ 'Choose this category and continue'|trans }} + + +
      + {{ self.categoryTreeItem(categoryWithPreloadedChildren.children, false, locale) }} +
    • + {% endfor %} +
    + {% endmacro %} + +
    +
    +
    + {{ self.categoryTreeItem(categoriesWithPreloadedChildren, true, locale) }} +
    +
    +
    + + {% embed '@ShopsysFramework/Admin/Inline/FixedBar/fixedBar.html.twig' %} + {% block fixed_bar_content %} + {{ 'Back to overview'|trans }} + {% endblock %} + {% endembed %} + +{% endblock %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig b/src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig new file mode 100644 index 0000000000..32bb1b90da --- /dev/null +++ b/src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig @@ -0,0 +1,60 @@ +{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} + +{% set title = 'Extended SEO category %categoryName% - combinations'|trans({'%categoryName%': category.name(locale)}) %} + +{% block title %}- {{ title }}{% endblock %} +{% block h1 %}{{ title }}{% endblock %} + +{% block main_content %} + + + + + {% for parameter in categorySeoFiltersData.parameters %} + + {% endfor %} + + {% if categorySeoFiltersData.useFlags %} + + {% endif %} + + {% if categorySeoFiltersData.useOrdering %} + + {% endif %} + + + + + {% for categorySeoMix in categorySeoMixes %} + + {% for parameterValue in categorySeoMix.parameterValues %} + + {% endfor %} + + {% if categorySeoMix.flag %} + + {% endif %} + + {% if categorySeoMix.ordering %} + + {% endif %} + + + + {% endfor %} + +
    {{ parameter.name }}{{ 'Flag'|trans }}{{ 'Ordering'|trans }}{{ 'Action'|trans }}
    {{ parameterValue.text }}{{ categorySeoMix.flag.name }}{{ getOrderingNameByOrderingId(categorySeoMix.ordering) }} + {{ render(controller('Shopsys\\FrameworkBundle\\Controller\\Admin\\CategorySeoController::readyCombinationButtonAction', { + categoryId: categoryId, + categorySeoFilterFormTypeAllQueries: categorySeoFilterFormTypeAllQueries, + choseCategorySeoMixCombination: categorySeoMix.choseCategorySeoMixCombination(categorySeoFiltersData.parameters) + })) }} +
    + + {% embed '@ShopsysFramework/Admin/Inline/FixedBar/fixedBar.html.twig' %} + {% block fixed_bar_content %} + {{ 'Back to parameter selection'|trans }} + {% endblock %} + {% endembed %} + +{% endblock %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig b/src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig new file mode 100644 index 0000000000..757764b49d --- /dev/null +++ b/src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig @@ -0,0 +1,22 @@ +{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} + +{% set title = 'Extended SEO category %categoryName% - filters'|trans({'%categoryName%': category.name(locale)}) %} + +{% block title %}- {{ title }}{% endblock %} +{% block h1 %}{{ title }}{% endblock %} + +{% block main_content %} + + {{ form_start(form) }} + {{ form_errors(form) }} + + {% embed '@ShopsysFramework/Admin/Inline/FixedBar/fixedBar.html.twig' %} + {% block fixed_bar_content %} + {{ 'Back to category selection'|trans }} + {{ form_widget(form.save, { attr: { class: 'margin-top-20 margin-bottom-20' } }) }} + {% endblock %} + {% endembed %} + + {{ form_end(form) }} + +{% endblock %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig b/src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig new file mode 100644 index 0000000000..cf6337db4b --- /dev/null +++ b/src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig @@ -0,0 +1,44 @@ +{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} + +{% set title = 'Extended SEO category - editing the combination with SEO values'|trans %} + +{% block title %}- {{ title }}{% endblock %} +{% block h1 %}{{ title }}{% endblock %} + +{% block main_content %} + + {{ _self.infoLine('Flag'|trans, flagName) }} + {{ _self.infoLine('Ordering'|trans, getOrderingNameByOrderingId(choseCategorySeoMixCombination.ordering)) }} + + {% for parameterName, parameterValueName in parameterValueNamesIndexedByParameterNames %} + {{ _self.infoLine(parameterName, parameterValueName) }} + {% endfor %} + + {{ _self.infoLine('Domain'|trans, choseCategorySeoMixCombinationDomainConfig.name) }} + + {{ form_start(form) }} + {{ form_errors(form) }} + + {% embed '@ShopsysFramework/Admin/Inline/FixedBar/fixedBar.html.twig' %} + {% block fixed_bar_content %} + {{ 'Back to overview of available combinations'|trans }} + {{ form_widget(form.save, { attr: { class: 'margin-top-20 margin-bottom-20' } }) }} + {% endblock %} + {% endembed %} + + {{ form_end(form) }} + +{% endblock %} + +{% macro infoLine(label, value) %} +
    + +
    +
    + +
    +
    +
    +{% endmacro %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/readyCombinationEditButton.html.twig b/src/Resources/views/Admin/Content/CategorySeo/readyCombinationEditButton.html.twig new file mode 100644 index 0000000000..a3d093e1a9 --- /dev/null +++ b/src/Resources/views/Admin/Content/CategorySeo/readyCombinationEditButton.html.twig @@ -0,0 +1,8 @@ + diff --git a/src/Twig/CategorySeoExtension.php b/src/Twig/CategorySeoExtension.php new file mode 100644 index 0000000000..1705d7b3f9 --- /dev/null +++ b/src/Twig/CategorySeoExtension.php @@ -0,0 +1,73 @@ +getReadyCategoryMixCombinationParametersPairsIterator(...)), + new TwigFunction('getAbsoluteUrlOfReadyCategorySeoMix', $this->getAbsoluteUrlOfReadyCategorySeoMix(...)), + ]; + } + + /** + * @param string $choseCategorySeoMixCombinationJson + * @return \Generator + */ + public function getReadyCategoryMixCombinationParametersPairsIterator( + string $choseCategorySeoMixCombinationJson, + ): Generator { + $choseCategorySeoMixCombination = ChoseCategorySeoMixCombination::createFromJson($choseCategorySeoMixCombinationJson); + + foreach ($choseCategorySeoMixCombination->getParameterValueIdsByParameterIds() as $parameterId => $parameterValueId) { + yield $this->parameterFacade->getById($parameterId)->getName() . ': ' . $this->parameterFacade->getParameterValueById($parameterValueId)->getText(); + } + } + + /** + * @param int $readyCategorySeoMixId + * @return string + */ + public function getAbsoluteUrlOfReadyCategorySeoMix(int $readyCategorySeoMixId): string + { + $readyCategorySeoMix = $this->readyCategorySeoMixFacade->findById($readyCategorySeoMixId); + + if ($readyCategorySeoMix === null) { + return '#'; + } + + $readyCategorySeoMixDomainRouter = $this->domainRouterFactory->getRouter($readyCategorySeoMix->getDomainId()); + + return $readyCategorySeoMixDomainRouter->generate('front_category_seo', [ + 'id' => $readyCategorySeoMixId, + ], UrlGeneratorInterface::ABSOLUTE_URL); + } +} From d354ba6fa696b41933e726f9111f23190b9240c6 Mon Sep 17 00:00:00 2001 From: Martin Grossmann Date: Thu, 17 Oct 2024 12:17:06 +0200 Subject: [PATCH 2/7] removed obsolete ordering from request --- .../Listing/ProductListOrderingConfig.php | 45 +++++---------- .../ProductListOrderingModeForBrandFacade.php | 56 ------------------- .../ProductListOrderingModeForListFacade.php | 32 +---------- ...ProductListOrderingModeForSearchFacade.php | 56 ------------------- .../RequestToOrderingModeIdConverter.php | 39 ------------- src/Resources/config/services.yaml | 2 - 6 files changed, 14 insertions(+), 216 deletions(-) delete mode 100644 src/Model/Product/Listing/ProductListOrderingModeForBrandFacade.php delete mode 100644 src/Model/Product/Listing/ProductListOrderingModeForSearchFacade.php delete mode 100644 src/Model/Product/Listing/RequestToOrderingModeIdConverter.php diff --git a/src/Model/Product/Listing/ProductListOrderingConfig.php b/src/Model/Product/Listing/ProductListOrderingConfig.php index ed9cefd6c5..9a314bfba9 100644 --- a/src/Model/Product/Listing/ProductListOrderingConfig.php +++ b/src/Model/Product/Listing/ProductListOrderingConfig.php @@ -6,38 +6,27 @@ class ProductListOrderingConfig { - public const ORDER_BY_PRIORITY = 'priority'; - public const ORDER_BY_PRICE_DESC = 'price_desc'; - public const ORDER_BY_PRICE_ASC = 'price_asc'; - public const ORDER_BY_NAME_DESC = 'name_desc'; - public const ORDER_BY_RELEVANCE = 'relevance'; - public const ORDER_BY_NAME_ASC = 'name_asc'; - - /** - * @var string[] - */ - protected array $supportedOrderingModesNamesById; - - protected string $defaultOrderingModeId; - - protected string $cookieName; + public const string ORDER_BY_PRIORITY = 'priority'; + public const string ORDER_BY_PRICE_DESC = 'price_desc'; + public const string ORDER_BY_PRICE_ASC = 'price_asc'; + public const string ORDER_BY_NAME_DESC = 'name_desc'; + public const string ORDER_BY_RELEVANCE = 'relevance'; + public const string ORDER_BY_NAME_ASC = 'name_asc'; /** * @param string[] $supportedOrderingModesNamesById * @param string $defaultOrderingModeId - * @param string $cookieName */ - public function __construct($supportedOrderingModesNamesById, $defaultOrderingModeId, $cookieName) - { - $this->supportedOrderingModesNamesById = $supportedOrderingModesNamesById; - $this->defaultOrderingModeId = $defaultOrderingModeId; - $this->cookieName = $cookieName; + public function __construct( + protected readonly array $supportedOrderingModesNamesById, + protected readonly string $defaultOrderingModeId, + ) { } /** * @return string[] */ - public function getSupportedOrderingModesNamesIndexedById() + public function getSupportedOrderingModesNamesIndexedById(): array { return $this->supportedOrderingModesNamesById; } @@ -45,15 +34,7 @@ public function getSupportedOrderingModesNamesIndexedById() /** * @return string */ - public function getCookieName() - { - return $this->cookieName; - } - - /** - * @return string - */ - public function getDefaultOrderingModeId() + public function getDefaultOrderingModeId(): string { return $this->defaultOrderingModeId; } @@ -61,7 +42,7 @@ public function getDefaultOrderingModeId() /** * @return string[] */ - public function getSupportedOrderingModeIds() + public function getSupportedOrderingModeIds(): array { return array_keys($this->supportedOrderingModesNamesById); } diff --git a/src/Model/Product/Listing/ProductListOrderingModeForBrandFacade.php b/src/Model/Product/Listing/ProductListOrderingModeForBrandFacade.php deleted file mode 100644 index 3d2a276d6c..0000000000 --- a/src/Model/Product/Listing/ProductListOrderingModeForBrandFacade.php +++ /dev/null @@ -1,56 +0,0 @@ -getSupportedOrderingModesNamesById(), - $this->getDefaultOrderingModeId(), - static::COOKIE_NAME, - ); - } - - /** - * @param \Symfony\Component\HttpFoundation\Request $request - * @return string - */ - public function getOrderingModeIdFromRequest(Request $request) - { - return $this->requestToOrderingModeIdConverter->getOrderingModeIdFromRequest( - $request, - $this->getProductListOrderingConfig(), - ); - } - - /** - * @return array - */ - abstract protected function getSupportedOrderingModesNamesById(): array; - - /** - * @return string - */ - protected function getDefaultOrderingModeId(): string - { - return ProductListOrderingConfig::ORDER_BY_PRIORITY; - } -} diff --git a/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php b/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php index dea8ae9a2d..1ea1adadb2 100644 --- a/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php +++ b/src/Model/Product/Listing/ProductListOrderingModeForListFacade.php @@ -4,46 +4,16 @@ namespace Shopsys\FrameworkBundle\Model\Product\Listing; -use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix; -use Symfony\Component\HttpFoundation\Request; - abstract class ProductListOrderingModeForListFacade { - protected const string COOKIE_NAME = 'productListOrderingMode'; - - /** - * @param \Shopsys\FrameworkBundle\Model\Product\Listing\RequestToOrderingModeIdConverter $requestToOrderingModeIdConverter - */ - public function __construct( - protected readonly RequestToOrderingModeIdConverter $requestToOrderingModeIdConverter, - ) { - } - /** * @return \Shopsys\FrameworkBundle\Model\Product\Listing\ProductListOrderingConfig */ - public function getProductListOrderingConfig() + public function getProductListOrderingConfig(): ProductListOrderingConfig { return new ProductListOrderingConfig( $this->getSupportedOrderingModesNamesById(), $this->getDefaultOrderingModeId(), - static::COOKIE_NAME, - ); - } - - /** - * @param \Symfony\Component\HttpFoundation\Request $request - * @param \Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix|null $readyCategorySeoMix - * @return string - */ - public function getOrderingModeIdFromRequest( - Request $request, - ?ReadyCategorySeoMix $readyCategorySeoMix = null, - ) { - return $this->requestToOrderingModeIdConverter->getOrderingModeIdFromRequest( - $request, - $this->getProductListOrderingConfig(), - $readyCategorySeoMix, ); } diff --git a/src/Model/Product/Listing/ProductListOrderingModeForSearchFacade.php b/src/Model/Product/Listing/ProductListOrderingModeForSearchFacade.php deleted file mode 100644 index abd535616f..0000000000 --- a/src/Model/Product/Listing/ProductListOrderingModeForSearchFacade.php +++ /dev/null @@ -1,56 +0,0 @@ -getSupportedOrderingModesNamesById(), - $this->getDefaultOrderingModeId(), - static::COOKIE_NAME, - ); - } - - /** - * @param \Symfony\Component\HttpFoundation\Request $request - * @return string - */ - public function getOrderingModeIdFromRequest(Request $request) - { - return $this->requestToOrderingModeIdConverter->getOrderingModeIdFromRequest( - $request, - $this->getProductListOrderingConfig(), - ); - } - - /** - * @return array - */ - abstract protected function getSupportedOrderingModesNamesById(): array; - - /** - * @return string - */ - protected function getDefaultOrderingModeId(): string - { - return ProductListOrderingConfig::ORDER_BY_RELEVANCE; - } -} diff --git a/src/Model/Product/Listing/RequestToOrderingModeIdConverter.php b/src/Model/Product/Listing/RequestToOrderingModeIdConverter.php deleted file mode 100644 index 5a390287ed..0000000000 --- a/src/Model/Product/Listing/RequestToOrderingModeIdConverter.php +++ /dev/null @@ -1,39 +0,0 @@ -getOrdering(); - - if ($readyCategorySeoMixOrderingModeId !== null) { - return $readyCategorySeoMixOrderingModeId; - } - } - - $orderingModeId = $request->cookies->get($productListOrderingConfig->getCookieName()); - - if (!in_array($orderingModeId, $productListOrderingConfig->getSupportedOrderingModeIds(), true)) { - $orderingModeId = $productListOrderingConfig->getDefaultOrderingModeId(); - } - - return $orderingModeId; - } -} diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index effc667989..24fe820d74 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -892,8 +892,6 @@ services: Shopsys\FrameworkBundle\Model\Product\Flag\FlagFactoryInterface: alias: Shopsys\FrameworkBundle\Model\Product\Flag\FlagFactory - Shopsys\FrameworkBundle\Model\Product\Listing\RequestToOrderingModeIdConverter: ~ - Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterDataFactoryInterface: alias: Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterDataFactory From 751a97292855d84982135c4098a286d8a9720e5d Mon Sep 17 00:00:00 2001 From: Martin Grossmann Date: Mon, 21 Oct 2024 17:29:39 +0200 Subject: [PATCH 3/7] brands from frontend-api moved to packages --- src/Model/Product/Brand/BrandFacade.php | 20 ++++++++ src/Model/Product/Brand/BrandRepository.php | 55 +++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/Model/Product/Brand/BrandFacade.php b/src/Model/Product/Brand/BrandFacade.php index 41ff076861..91c862d604 100644 --- a/src/Model/Product/Brand/BrandFacade.php +++ b/src/Model/Product/Brand/BrandFacade.php @@ -5,6 +5,7 @@ namespace Shopsys\FrameworkBundle\Model\Product\Brand; use Doctrine\ORM\EntityManagerInterface; +use Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig; use Shopsys\FrameworkBundle\Component\Domain\Domain; use Shopsys\FrameworkBundle\Component\Image\ImageFacade; use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlFacade; @@ -167,4 +168,23 @@ public function getBrandsBySearchText(string $searchText): array { return $this->brandRepository->getBrandsBySearchText($searchText); } + + /** + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Shopsys\FrameworkBundle\Model\Product\Brand\Brand[] + */ + public function getAllWithDomainsAndTranslations(DomainConfig $domainConfig): array + { + return $this->brandRepository->getAllWithDomainsAndTranslations($domainConfig); + } + + /** + * @param int[] $brandIds + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return array + */ + public function getByIds(array $brandIds, DomainConfig $domainConfig): array + { + return $this->brandRepository->getByIds($brandIds, $domainConfig); + } } diff --git a/src/Model/Product/Brand/BrandRepository.php b/src/Model/Product/Brand/BrandRepository.php index eb54fd08d6..c68ec3a02d 100644 --- a/src/Model/Product/Brand/BrandRepository.php +++ b/src/Model/Product/Brand/BrandRepository.php @@ -5,7 +5,10 @@ namespace Shopsys\FrameworkBundle\Model\Product\Brand; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\Expr\Join; +use Doctrine\ORM\QueryBuilder; use Shopsys\FrameworkBundle\Component\Doctrine\OrderByCollationHelper; +use Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig; use Shopsys\FrameworkBundle\Component\Domain\Domain; use Shopsys\FrameworkBundle\Component\String\DatabaseSearching; use Shopsys\FrameworkBundle\Model\Product\Brand\Exception\BrandNotFoundException; @@ -110,4 +113,56 @@ public function getBrandsBySearchText(string $searchText): array return $queryBuilder->getQuery()->getResult(); } + + /** + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Shopsys\FrameworkBundle\Model\Product\Brand\Brand[] + */ + public function getAllWithDomainsAndTranslations(DomainConfig $domainConfig): array + { + return $this->getAllWithDomainsAndTranslationsQueryBuilder($domainConfig) + ->orderBy('b.name') + ->getQuery()->getResult(); + } + + /** + * @param int[] $brandIds + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return array + */ + public function getByIds(array $brandIds, DomainConfig $domainConfig): array + { + $result = $this->getAllWithDomainsAndTranslationsQueryBuilder($domainConfig) + ->andWhere('b.id IN (:brandIds)') + ->indexBy('b', 'b.id') + ->setParameter('brandIds', $brandIds) + ->getQuery()->getResult(); + + $brands = []; + + foreach ($brandIds as $brandId) { + if (isset($result[$brandId])) { + $brands[] = $result[$brandId]; + } else { + $brands[] = null; + } + } + + return $brands; + } + + /** + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Doctrine\ORM\QueryBuilder + */ + protected function getAllWithDomainsAndTranslationsQueryBuilder(DomainConfig $domainConfig): QueryBuilder + { + return $this->em->createQueryBuilder() + ->select('b, bd, bt') + ->from(Brand::class, 'b') + ->join('b.domains', 'bd', Join::WITH, 'bd.domainId = :domainId') + ->join('b.translations', 'bt', Join::WITH, 'bt.locale = :locale') + ->setParameter('domainId', $domainConfig->getId()) + ->setParameter('locale', $domainConfig->getLocale()); + } } From 2e1d84ea513a1125bb09c7beb9f371f0a8d39e7d Mon Sep 17 00:00:00 2001 From: Martin Grossmann Date: Mon, 21 Oct 2024 22:47:40 +0200 Subject: [PATCH 4/7] flag graphql queries moved to packages --- src/Model/Product/Flag/FlagData.php | 1 + src/Model/Product/Flag/FlagFacade.php | 29 +++++++++++ src/Model/Product/Flag/FlagRepository.php | 61 +++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/src/Model/Product/Flag/FlagData.php b/src/Model/Product/Flag/FlagData.php index e82e884878..112f6ac307 100644 --- a/src/Model/Product/Flag/FlagData.php +++ b/src/Model/Product/Flag/FlagData.php @@ -30,5 +30,6 @@ public function __construct() { $this->name = []; $this->visible = false; + $this->rgbColor = ''; } } diff --git a/src/Model/Product/Flag/FlagFacade.php b/src/Model/Product/Flag/FlagFacade.php index cb4c188c40..7a75870b42 100644 --- a/src/Model/Product/Flag/FlagFacade.php +++ b/src/Model/Product/Flag/FlagFacade.php @@ -140,4 +140,33 @@ public function getFlagIdsByUuids(array $flagUuids): array { return $this->flagRepository->getFlagIdsByUuids($flagUuids); } + + /** + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag[] + */ + public function getAllVisibleFlags(string $locale): array + { + return $this->flagRepository->getAllVisibleFlags($locale); + } + + /** + * @param string $uuid + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag + */ + public function getVisibleByUuid(string $uuid, string $locale): Flag + { + return $this->flagRepository->getVisibleByUuid($uuid, $locale); + } + + /** + * @param int $flagId + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag + */ + public function getVisibleFlagById(int $flagId, string $locale): Flag + { + return $this->flagRepository->getVisibleFlagById($flagId, $locale); + } } diff --git a/src/Model/Product/Flag/FlagRepository.php b/src/Model/Product/Flag/FlagRepository.php index 136d7baeee..1482939424 100644 --- a/src/Model/Product/Flag/FlagRepository.php +++ b/src/Model/Product/Flag/FlagRepository.php @@ -137,4 +137,65 @@ public function getFlagIdsByUuids(array $flagUuids): array return array_column($queryBuilder->getQuery()->getArrayResult(), 'id'); } + + /** + * @param int $flagId + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag + */ + public function getVisibleFlagById(int $flagId, string $locale): Flag + { + $flagsQueryBuilder = $this->getVisibleQueryBuilder() + ->addSelect('ft') + ->join('f.translations', 'ft', Join::WITH, 'ft.locale = :locale') + ->where('f.id = :flagId') + ->setParameter('flagId', $flagId) + ->setParameter('locale', $locale); + + $flag = $flagsQueryBuilder->getQuery()->getOneOrNullResult(); + + if ($flag === null) { + throw new FlagNotFoundException(sprintf('Flag with ID "%s" does not exist.', $flagId)); + } + + return $flag; + } + + /** + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag[] + */ + public function getAllVisibleFlags(string $locale): array + { + $flagsQueryBuilder = $this->getVisibleQueryBuilder() + ->addSelect('f') + ->join('f.translations', 'ft', Join::WITH, 'ft.locale = :locale') + ->orderBy(OrderByCollationHelper::createOrderByForLocale('ft.name', $locale), 'asc') + ->setParameter('locale', $locale); + + return $flagsQueryBuilder->getQuery()->getResult(); + } + + /** + * @param string $uuid + * @param string $locale + * @return \Shopsys\FrameworkBundle\Model\Product\Flag\Flag + */ + public function getVisibleByUuid(string $uuid, string $locale): Flag + { + $flagsQueryBuilder = $this->getVisibleQueryBuilder() + ->addSelect('ft') + ->join('f.translations', 'ft', Join::WITH, 'ft.locale = :locale') + ->setParameter('locale', $locale) + ->andWhere('f.uuid = :uuid') + ->setParameter('uuid', $uuid); + + $flag = $flagsQueryBuilder->getQuery()->getOneOrNullResult(); + + if ($flag === null) { + throw new FlagNotFoundException(sprintf('Flag with UUID "%s" does not exist.', $uuid)); + } + + return $flag; + } } From 0425015628fab7ca8dabcb87248224b7088d7ee1 Mon Sep 17 00:00:00 2001 From: Martin Grossmann Date: Mon, 21 Oct 2024 23:14:19 +0200 Subject: [PATCH 5/7] sitemap generating for ready category seo moved to framework package --- src/Model/Sitemap/SitemapFacade.php | 9 +++++++++ src/Model/Sitemap/SitemapListener.php | 8 ++++++++ src/Model/Sitemap/SitemapRepository.php | 26 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/src/Model/Sitemap/SitemapFacade.php b/src/Model/Sitemap/SitemapFacade.php index f5b06f0057..45fec13445 100644 --- a/src/Model/Sitemap/SitemapFacade.php +++ b/src/Model/Sitemap/SitemapFacade.php @@ -99,4 +99,13 @@ public function getSitemapItemsForVisibleFlags(DomainConfig $domainConfig): arra { return $this->sitemapRepository->getSitemapItemsForVisibleFlags($domainConfig); } + + /** + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Shopsys\FrameworkBundle\Model\Sitemap\SitemapItem[] + */ + public function getSitemapItemsForVisibleCategorySeoMix(DomainConfig $domainConfig): array + { + return $this->sitemapRepository->getSitemapItemsForVisibleCategorySeoMix($domainConfig); + } } diff --git a/src/Model/Sitemap/SitemapListener.php b/src/Model/Sitemap/SitemapListener.php index 92c64c9cf5..357c192520 100644 --- a/src/Model/Sitemap/SitemapListener.php +++ b/src/Model/Sitemap/SitemapListener.php @@ -113,6 +113,14 @@ public function populateSitemap(SitemapPopulateEvent $event): void 'front_flag_detail', $this->sitemapFacade->getSitemapItemsForVisibleFlags(...), ); + + $categorySeoMixSitemapItems = $this->sitemapFacade->getSitemapItemsForVisibleCategorySeoMix($domainConfig); + $this->addUrlsForSitemapItems( + $categorySeoMixSitemapItems, + $generator, + $domainConfig, + 'filtersCategories', + ); } /** diff --git a/src/Model/Sitemap/SitemapRepository.php b/src/Model/Sitemap/SitemapRepository.php index 90fcf5846d..a01f19697f 100644 --- a/src/Model/Sitemap/SitemapRepository.php +++ b/src/Model/Sitemap/SitemapRepository.php @@ -12,6 +12,7 @@ use Shopsys\FrameworkBundle\Model\Article\ArticleRepository; use Shopsys\FrameworkBundle\Model\Blog\Article\BlogArticleRepository; use Shopsys\FrameworkBundle\Model\Category\CategoryRepository; +use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix; use Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup; use Shopsys\FrameworkBundle\Model\Product\Flag\FlagRepository; use Shopsys\FrameworkBundle\Model\Product\Product; @@ -216,4 +217,29 @@ public function getSitemapItemsForVisibleFlags(DomainConfig $domainConfig): arra return $this->getSitemapItemsFromQueryBuilderWithSlugField($queryBuilder); } + + /** + * @param \Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig $domainConfig + * @return \Shopsys\FrameworkBundle\Model\Sitemap\SitemapItem[] + */ + public function getSitemapItemsForVisibleCategorySeoMix(DomainConfig $domainConfig): array + { + $queryBuilder = $this->categoryRepository->getAllVisibleByDomainIdQueryBuilder($domainConfig->getId()); + $queryBuilder + ->select('fu.slug, fu.entityId') + ->join(ReadyCategorySeoMix::class, 'rcsm', Join::WITH, 'rcsm.category = c AND rcsm.domainId = :domainId') + ->join( + FriendlyUrl::class, + 'fu', + Join::WITH, + 'fu.routeName = :categorySeoMixRouteName + AND fu.entityId = rcsm + AND fu.domainId = :domainId + AND fu.main = TRUE', + ) + ->setParameter('categorySeoMixRouteName', 'front_category_seo') + ->setParameter('domainId', $domainConfig->getId()); + + return $this->getSitemapItemsFromQueryBuilderWithSlugField($queryBuilder); + } } From 325fa279d95a0f18d5179121db6f3adf74e5fa9c Mon Sep 17 00:00:00 2001 From: Martin Grossmann Date: Thu, 24 Oct 2024 10:28:48 +0200 Subject: [PATCH 6/7] removed unecessary exclamation in extends in frameworks admin templates --- src/Resources/views/Admin/Content/CategorySeo/list.html.twig | 2 +- .../views/Admin/Content/CategorySeo/newCategory.html.twig | 2 +- .../views/Admin/Content/CategorySeo/newCombinations.html.twig | 2 +- .../views/Admin/Content/CategorySeo/newFilters.html.twig | 2 +- .../views/Admin/Content/CategorySeo/readyCombination.html.twig | 2 +- .../views/Admin/Content/ParameterValue/detail.html.twig | 2 +- src/Resources/views/Admin/Content/ParameterValue/list.html.twig | 2 +- .../views/Admin/Content/ParameterValue/listGrid.html.twig | 2 +- .../views/Admin/Content/ParameterValue/values.html.twig | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Resources/views/Admin/Content/CategorySeo/list.html.twig b/src/Resources/views/Admin/Content/CategorySeo/list.html.twig index 57636fa0df..5b37084c67 100644 --- a/src/Resources/views/Admin/Content/CategorySeo/list.html.twig +++ b/src/Resources/views/Admin/Content/CategorySeo/list.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% block title %}- {{ 'Extended SEO categories'|trans }}{% endblock %} {% block h1 %}{{ 'Extended SEO categories'|trans }}{% endblock %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig b/src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig index 8aa330d5c4..cfbbe77c12 100644 --- a/src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig +++ b/src/Resources/views/Admin/Content/CategorySeo/newCategory.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% import _self as self %} {% block title %}- {{ 'Extended SEO category - category selection'|trans }}{% endblock %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig b/src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig index 32bb1b90da..54615f6cb2 100644 --- a/src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig +++ b/src/Resources/views/Admin/Content/CategorySeo/newCombinations.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% set title = 'Extended SEO category %categoryName% - combinations'|trans({'%categoryName%': category.name(locale)}) %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig b/src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig index 757764b49d..20d4edbb54 100644 --- a/src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig +++ b/src/Resources/views/Admin/Content/CategorySeo/newFilters.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% set title = 'Extended SEO category %categoryName% - filters'|trans({'%categoryName%': category.name(locale)}) %} diff --git a/src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig b/src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig index cf6337db4b..c19796b23f 100644 --- a/src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig +++ b/src/Resources/views/Admin/Content/CategorySeo/readyCombination.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% set title = 'Extended SEO category - editing the combination with SEO values'|trans %} diff --git a/src/Resources/views/Admin/Content/ParameterValue/detail.html.twig b/src/Resources/views/Admin/Content/ParameterValue/detail.html.twig index 8461834099..78705fab66 100644 --- a/src/Resources/views/Admin/Content/ParameterValue/detail.html.twig +++ b/src/Resources/views/Admin/Content/ParameterValue/detail.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% block title %}- {{ 'Parameter value of type color'|trans }}{% endblock %} {% block h1 %}{{ 'Parameter value of type color'|trans }}{% endblock %} diff --git a/src/Resources/views/Admin/Content/ParameterValue/list.html.twig b/src/Resources/views/Admin/Content/ParameterValue/list.html.twig index c7c9efaa2f..b091f27ec4 100644 --- a/src/Resources/views/Admin/Content/ParameterValue/list.html.twig +++ b/src/Resources/views/Admin/Content/ParameterValue/list.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% block title %}- {{ 'Parameter values of type color'|trans }}{% endblock %} {% block h1 %}{{ 'Parameter values of type color'|trans }}{% endblock %} diff --git a/src/Resources/views/Admin/Content/ParameterValue/listGrid.html.twig b/src/Resources/views/Admin/Content/ParameterValue/listGrid.html.twig index bffee01afc..c2e8294b07 100644 --- a/src/Resources/views/Admin/Content/ParameterValue/listGrid.html.twig +++ b/src/Resources/views/Admin/Content/ParameterValue/listGrid.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Grid/Grid.html.twig' %} +{% extends '@ShopsysFramework/Admin/Grid/Grid.html.twig' %} {% block grid_value_cell_id_name %} {{ value }} diff --git a/src/Resources/views/Admin/Content/ParameterValue/values.html.twig b/src/Resources/views/Admin/Content/ParameterValue/values.html.twig index bfacd1fb89..fc636e7f89 100644 --- a/src/Resources/views/Admin/Content/ParameterValue/values.html.twig +++ b/src/Resources/views/Admin/Content/ParameterValue/values.html.twig @@ -1,4 +1,4 @@ -{% extends '@!ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} +{% extends '@ShopsysFramework/Admin/Layout/layoutWithPanel.html.twig' %} {% block title %}- {{ 'Convert values associated with parameter - %name%'|trans({'%name%': parameter.name}) }}{% endblock %} {% block h1 %}{{ 'Convert values associated with parameter - %name%'|trans({'%name%': parameter.name}) }}{% endblock %} From 01d348e69af1915e4b88f91be40da8270015da91 Mon Sep 17 00:00:00 2001 From: Martin Grossmann Date: Mon, 4 Nov 2024 13:13:49 +0100 Subject: [PATCH 7/7] fixed parameter group sorting --- src/Model/Product/Parameter/ParameterRepository.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Model/Product/Parameter/ParameterRepository.php b/src/Model/Product/Parameter/ParameterRepository.php index b8ec84a921..f33d8a4176 100644 --- a/src/Model/Product/Parameter/ParameterRepository.php +++ b/src/Model/Product/Parameter/ParameterRepository.php @@ -276,13 +276,15 @@ protected function getProductParameterValuesByProductSortedByOrderingPriorityAnd ->from(ProductParameterValue::class, 'ppv') ->join('ppv.parameter', 'p') ->join('p.translations', 'pt') + ->leftJoin('p.group', 'pg') ->where('ppv.product = :product_id') ->andWhere('pt.locale = :locale') ->setParameters([ 'product_id' => $product->getId(), 'locale' => $locale, ]) - ->orderBy('p.position', 'ASC') + ->orderBy('p.orderingPriority', 'DESC') + ->addOrderBy('pg.position', 'ASC') ->addOrderBy('pt.name'); }