Skip to content

Commit e1c56cb

Browse files
authored
Improve GraphQL query performance with batch loading and Redis caching (#4561)
2 parents c881e61 + 7c69205 commit e1c56cb

15 files changed

Lines changed: 338 additions & 97 deletions

src/Model/Blog/Category/BlogCategoryResolverMap.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Shopsys\FrontendApiBundle\Model\Blog\Category;
66

7+
use Overblog\DataLoader\DataLoaderInterface;
78
use Overblog\GraphQLBundle\Resolver\ResolverMap;
89
use Override;
910
use Shopsys\FrameworkBundle\Component\Domain\Domain;
@@ -21,6 +22,7 @@ public function __construct(
2122
protected readonly BlogCategoryFacade $blogCategoryFacade,
2223
protected readonly BlogArticleElasticsearchFacade $blogArticleElasticsearchFacade,
2324
protected readonly HreflangLinksFacade $hreflangLinksFacade,
25+
protected readonly DataLoaderInterface $blogCategorySlugBatchLoader,
2426
) {
2527
}
2628

@@ -42,7 +44,7 @@ protected function map(): array
4244
return $blogCategory->getParent();
4345
},
4446
'slug' => function (BlogCategory $blogCategory) {
45-
return '/' . $this->friendlyUrlFacade->getMainFriendlyUrlSlug($this->domain->getId(), 'front_blogcategory_detail', $blogCategory->getId());
47+
return $this->blogCategorySlugBatchLoader->load($blogCategory->getId());
4648
},
4749
'link' => function (BlogCategory $blogCategory) {
4850
return $this->friendlyUrlFacade->getAbsoluteUrlByRouteNameAndEntityIdOnCurrentDomain('front_blogcategory_detail', $blogCategory->getId());

src/Model/Category/ReadyCategorySeoMixBatchLoader.php

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,26 @@
77
use GraphQL\Executor\Promise\Promise;
88
use GraphQL\Executor\Promise\PromiseAdapter;
99
use Shopsys\FrameworkBundle\Component\Domain\Domain;
10-
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlFacade;
10+
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\Exception\FriendlyUrlNotFoundException;
11+
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlCacheKeyProvider;
12+
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlRepository;
1113
use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMix;
1214
use Shopsys\FrameworkBundle\Model\CategorySeo\ReadyCategorySeoMixFacade;
1315
use Shopsys\FrameworkBundle\Model\Customer\User\Role\CustomerUserRoleResolver;
16+
use Symfony\Contracts\Cache\CacheInterface;
1417

1518
class ReadyCategorySeoMixBatchLoader
1619
{
20+
protected const string ROUTE_NAME = 'front_category_seo';
21+
1722
public function __construct(
1823
protected readonly PromiseAdapter $promiseAdapter,
1924
protected readonly ReadyCategorySeoMixFacade $readyCategorySeoMixFacade,
2025
protected readonly Domain $domain,
21-
protected readonly FriendlyUrlFacade $friendlyUrlFacade,
26+
protected readonly FriendlyUrlRepository $friendlyUrlRepository,
2227
protected readonly CustomerUserRoleResolver $customerUserRoleResolver,
28+
protected readonly CacheInterface $mainFriendlyUrlSlugCache,
29+
protected readonly FriendlyUrlCacheKeyProvider $friendlyUrlCacheKeyProvider,
2330
) {
2431
}
2532

@@ -32,26 +39,87 @@ public function loadByCategoryIds(array $categoryIds): Promise
3239

3340
$canSeePrices = $this->customerUserRoleResolver->canCurrentCustomerUserSeePrices();
3441

42+
$allSeoMixIds = [];
43+
44+
foreach ($allReadyCategorySeoMixes as $readyCategorySeoMixes) {
45+
foreach ($readyCategorySeoMixes as $readyCategorySeoMix) {
46+
$allSeoMixIds[] = $readyCategorySeoMix->getId();
47+
}
48+
}
49+
50+
$slugsByEntityId = $this->loadSlugsWithCache($allSeoMixIds);
51+
3552
$result = [];
3653

3754
foreach ($allReadyCategorySeoMixes as $readyCategorySeoMixes) {
3855
$filteredMixes = $canSeePrices
3956
? $readyCategorySeoMixes
40-
: array_filter($readyCategorySeoMixes, fn (ReadyCategorySeoMix $mix) => !$mix->hasPriceBasedOrdering());
57+
: array_filter($readyCategorySeoMixes, fn (ReadyCategorySeoMix $readyCategorySeoMix) => !$readyCategorySeoMix->hasPriceBasedOrdering());
4158

4259
$result[] = array_map(
43-
fn (ReadyCategorySeoMix $readyCategorySeoMix) => [
44-
'name' => $readyCategorySeoMix->getH1(),
45-
'slug' => '/' . $this->friendlyUrlFacade->getMainFriendlyUrlSlug(
46-
$this->domain->getId(),
47-
'front_category_seo',
48-
$readyCategorySeoMix->getId(),
49-
),
50-
],
60+
function (ReadyCategorySeoMix $readyCategorySeoMix) use ($slugsByEntityId) {
61+
if (!isset($slugsByEntityId[$readyCategorySeoMix->getId()])) {
62+
throw new FriendlyUrlNotFoundException(
63+
sprintf('Main friendly URL not found for route "front_category_seo", domain ID "%d", and entity ID "%d".', $this->domain->getId(), $readyCategorySeoMix->getId()),
64+
);
65+
}
66+
67+
return [
68+
'name' => $readyCategorySeoMix->getH1(),
69+
'slug' => '/' . $slugsByEntityId[$readyCategorySeoMix->getId()],
70+
];
71+
},
5172
$filteredMixes,
5273
);
5374
}
5475

5576
return $this->promiseAdapter->all($result);
5677
}
78+
79+
/**
80+
* @param int[] $entityIds
81+
* @return array<int, string>
82+
*/
83+
protected function loadSlugsWithCache(array $entityIds): array
84+
{
85+
$domainId = $this->domain->getId();
86+
$slugsByEntityId = [];
87+
$missingEntityIds = [];
88+
89+
foreach ($entityIds as $entityId) {
90+
$cacheKey = $this->friendlyUrlCacheKeyProvider->getMainFriendlyUrlSlugCacheKey(static::ROUTE_NAME, $domainId, $entityId);
91+
$cachedSlug = $this->mainFriendlyUrlSlugCache->get($cacheKey, fn () => null);
92+
93+
if ($cachedSlug !== null) {
94+
$slugsByEntityId[$entityId] = $cachedSlug;
95+
} else {
96+
$missingEntityIds[] = $entityId;
97+
}
98+
}
99+
100+
if ($missingEntityIds !== []) {
101+
$friendlyUrls = $this->friendlyUrlRepository->getMainFriendlyUrlsByEntitiesIndexedByEntityId(
102+
$missingEntityIds,
103+
static::ROUTE_NAME,
104+
$domainId,
105+
);
106+
107+
foreach ($missingEntityIds as $entityId) {
108+
if (!isset($friendlyUrls[$entityId])) {
109+
throw new FriendlyUrlNotFoundException(
110+
sprintf('Main friendly URL not found for route "%s", domain ID "%d", and entity ID "%d".', static::ROUTE_NAME, $domainId, $entityId),
111+
);
112+
}
113+
114+
$slug = $friendlyUrls[$entityId]->getSlug();
115+
$slugsByEntityId[$entityId] = $slug;
116+
117+
$cacheKey = $this->friendlyUrlCacheKeyProvider->getMainFriendlyUrlSlugCacheKey(static::ROUTE_NAME, $domainId, $entityId);
118+
$this->mainFriendlyUrlSlugCache->delete($cacheKey);
119+
$this->mainFriendlyUrlSlugCache->get($cacheKey, fn () => $slug);
120+
}
121+
}
122+
123+
return $slugsByEntityId;
124+
}
57125
}

src/Model/Flag/FlagResolverMap.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
namespace Shopsys\FrontendApiBundle\Model\Flag;
66

7+
use Overblog\DataLoader\DataLoaderInterface;
78
use Overblog\GraphQLBundle\Resolver\ResolverMap;
89
use Override;
910
use Shopsys\FrameworkBundle\Component\Domain\Domain;
10-
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlFacade;
1111
use Shopsys\FrameworkBundle\Model\Product\Flag\Flag;
1212
use Shopsys\FrameworkBundle\Model\Seo\HreflangLinksFacade;
1313

1414
class FlagResolverMap extends ResolverMap
1515
{
1616
public function __construct(
1717
protected readonly Domain $domain,
18-
protected readonly FriendlyUrlFacade $friendlyUrlFacade,
18+
protected readonly DataLoaderInterface $flagSlugBatchLoader,
1919
protected readonly HreflangLinksFacade $hreflangLinksFacade,
2020
) {
2121
}
@@ -32,23 +32,12 @@ protected function map(): array
3232
return $flag->getName($this->domain->getLocale()) ?? '';
3333
},
3434
'slug' => function (Flag $flag) {
35-
return $this->getSlug($flag);
35+
return $this->flagSlugBatchLoader->load($flag->getId());
3636
},
3737
'hreflangLinks' => function (Flag $flag) {
3838
return $this->hreflangLinksFacade->getForFlag($flag, $this->domain->getId());
3939
},
4040
],
4141
];
4242
}
43-
44-
protected function getSlug(Flag $flag): string
45-
{
46-
$friendlyUrlSlug = $this->friendlyUrlFacade->getMainFriendlyUrlSlug(
47-
$this->domain->getId(),
48-
'front_flag_detail',
49-
$flag->getId(),
50-
);
51-
52-
return '/' . $friendlyUrlSlug;
53-
}
5443
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopsys\FrontendApiBundle\Model\FriendlyUrl;
6+
7+
use GraphQL\Executor\Promise\Promise;
8+
use GraphQL\Executor\Promise\PromiseAdapter;
9+
use Shopsys\FrameworkBundle\Component\Domain\Domain;
10+
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\Exception\FriendlyUrlNotFoundException;
11+
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlCacheKeyProvider;
12+
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlRepository;
13+
use Symfony\Contracts\Cache\CacheInterface;
14+
15+
class FriendlyUrlSlugBatchLoader
16+
{
17+
public function __construct(
18+
protected readonly PromiseAdapter $promiseAdapter,
19+
protected readonly Domain $domain,
20+
protected readonly FriendlyUrlRepository $friendlyUrlRepository,
21+
protected readonly CacheInterface $mainFriendlyUrlSlugCache,
22+
protected readonly FriendlyUrlCacheKeyProvider $friendlyUrlCacheKeyProvider,
23+
) {
24+
}
25+
26+
/**
27+
* @param int[] $entityIds
28+
*/
29+
public function loadBrandSlugs(array $entityIds): Promise
30+
{
31+
return $this->loadSlugs($entityIds, 'front_brand_detail');
32+
}
33+
34+
/**
35+
* @param int[] $entityIds
36+
*/
37+
public function loadStoreSlugs(array $entityIds): Promise
38+
{
39+
return $this->loadSlugs($entityIds, 'front_stores_detail');
40+
}
41+
42+
/**
43+
* @param int[] $entityIds
44+
*/
45+
public function loadFlagSlugs(array $entityIds): Promise
46+
{
47+
return $this->loadSlugs($entityIds, 'front_flag_detail');
48+
}
49+
50+
/**
51+
* @param int[] $entityIds
52+
*/
53+
public function loadBlogCategorySlugs(array $entityIds): Promise
54+
{
55+
return $this->loadSlugs($entityIds, 'front_blogcategory_detail');
56+
}
57+
58+
/**
59+
* @param int[] $entityIds
60+
*/
61+
public function loadCategorySlugs(array $entityIds): Promise
62+
{
63+
return $this->loadSlugs($entityIds, 'front_product_list');
64+
}
65+
66+
/**
67+
* @param int[] $entityIds
68+
*/
69+
public function loadCategorySeoSlugs(array $entityIds): Promise
70+
{
71+
return $this->loadSlugs($entityIds, 'front_category_seo');
72+
}
73+
74+
/**
75+
* @param int[] $entityIds
76+
*/
77+
public function loadProductSlugs(array $entityIds): Promise
78+
{
79+
return $this->loadSlugs($entityIds, 'front_product_detail');
80+
}
81+
82+
/**
83+
* @param int[] $entityIds
84+
*/
85+
protected function loadSlugs(array $entityIds, string $routeName): Promise
86+
{
87+
$domainId = $this->domain->getId();
88+
$slugsByEntityId = [];
89+
$missingEntityIds = [];
90+
91+
foreach ($entityIds as $entityId) {
92+
$cacheKey = $this->friendlyUrlCacheKeyProvider->getMainFriendlyUrlSlugCacheKey($routeName, $domainId, $entityId);
93+
$cachedSlug = $this->mainFriendlyUrlSlugCache->get($cacheKey, fn () => null);
94+
95+
if ($cachedSlug !== null) {
96+
$slugsByEntityId[$entityId] = $cachedSlug;
97+
} else {
98+
$missingEntityIds[] = $entityId;
99+
}
100+
}
101+
102+
if ($missingEntityIds !== []) {
103+
$friendlyUrls = $this->friendlyUrlRepository->getMainFriendlyUrlsByEntitiesIndexedByEntityId(
104+
$missingEntityIds,
105+
$routeName,
106+
$domainId,
107+
);
108+
109+
foreach ($missingEntityIds as $entityId) {
110+
if (!isset($friendlyUrls[$entityId])) {
111+
throw new FriendlyUrlNotFoundException(
112+
sprintf('Main friendly URL not found for route "%s", domain ID "%d", and entity ID "%d".', $routeName, $domainId, $entityId),
113+
);
114+
}
115+
116+
$slug = $friendlyUrls[$entityId]->getSlug();
117+
$slugsByEntityId[$entityId] = $slug;
118+
119+
$cacheKey = $this->friendlyUrlCacheKeyProvider->getMainFriendlyUrlSlugCacheKey($routeName, $domainId, $entityId);
120+
$this->mainFriendlyUrlSlugCache->delete($cacheKey);
121+
$this->mainFriendlyUrlSlugCache->get($cacheKey, fn () => $slug);
122+
}
123+
}
124+
125+
$slugs = [];
126+
127+
foreach ($entityIds as $entityId) {
128+
$slugs[] = '/' . $slugsByEntityId[$entityId];
129+
}
130+
131+
return $this->promiseAdapter->all($slugs);
132+
}
133+
}

src/Model/Resolver/Brand/BrandResolverMap.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
namespace Shopsys\FrontendApiBundle\Model\Resolver\Brand;
66

7+
use Overblog\DataLoader\DataLoaderInterface;
78
use Overblog\GraphQLBundle\Resolver\ResolverMap;
89
use Override;
910
use Shopsys\FrameworkBundle\Component\Domain\Domain;
10-
use Shopsys\FrameworkBundle\Component\Router\FriendlyUrl\FriendlyUrlFacade;
1111
use Shopsys\FrameworkBundle\Model\Product\Brand\Brand;
1212
use Shopsys\FrameworkBundle\Model\Seo\HreflangLinksFacade;
1313
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -18,7 +18,7 @@ public function __construct(
1818
protected readonly UrlGeneratorInterface $urlGenerator,
1919
protected readonly Domain $domain,
2020
protected readonly HreflangLinksFacade $hreflangLinksFacade,
21-
protected readonly FriendlyUrlFacade $friendlyUrlFacade,
21+
protected readonly DataLoaderInterface $brandSlugBatchLoader,
2222
) {
2323
}
2424

@@ -47,20 +47,9 @@ protected function map(): array
4747
return $this->hreflangLinksFacade->getForBrand($brand, $this->domain->getId());
4848
},
4949
'slug' => function (Brand $brand) {
50-
return $this->getSlug($brand);
50+
return $this->brandSlugBatchLoader->load($brand->getId());
5151
},
5252
],
5353
];
5454
}
55-
56-
protected function getSlug(Brand $brand): string
57-
{
58-
$friendlyUrlSlug = $this->friendlyUrlFacade->getMainFriendlyUrlSlug(
59-
$this->domain->getId(),
60-
'front_brand_detail',
61-
$brand->getId(),
62-
);
63-
64-
return '/' . $friendlyUrlSlug;
65-
}
6655
}

0 commit comments

Comments
 (0)