feat: cache frontpage products and categories

This commit is contained in:
2025-08-08 15:02:21 +03:00
parent 2fb841ef08
commit 5f785e82e6
7 changed files with 598 additions and 44 deletions

View File

@@ -65,6 +65,8 @@ class Controllerextensiontgshophandle extends Controller
'logs' => [
'path' => DIR_LOGS,
],
'cache_categories_main' => 60 * 10,
'cache_products_main' => 60 * 10,
]);
$app->bind(Url::class, function () {

View File

@@ -23,7 +23,8 @@
"intervention/image": "^2.7",
"rakit/validation": "^1.4",
"vlucas/phpdotenv": "^5.6",
"guzzlehttp/guzzle": "^7.9"
"guzzlehttp/guzzle": "^7.9",
"symfony/cache": "^5.4"
},
"require-dev": {
"roave/security-advisories": "dev-latest"

View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7e1f9e747364eeaf0ebae0f7af3f8d2c",
"content-hash": "329d7196ee1db3f0f4e8b3552e28f83a",
"packages": [
{
"name": "graham-campbell/result-type",
@@ -552,6 +552,55 @@
],
"time": "2024-07-20T21:41:07+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/master"
},
"time": "2016-08-06T20:24:11+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
@@ -765,6 +814,56 @@
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/log",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/1.1.4"
},
"time": "2021-05-03T11:20:27+00:00"
},
{
"name": "rakit/validation",
"version": "v1.4.0",
@@ -855,6 +954,182 @@
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "symfony/cache",
"version": "v5.4.46",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
"reference": "0fe08ee32cec2748fbfea10c52d3ee02049e0f6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/0fe08ee32cec2748fbfea10c52d3ee02049e0f6b",
"reference": "0fe08ee32cec2748fbfea10c52d3ee02049e0f6b",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/cache": "^1.0|^2.0",
"psr/log": "^1.1|^2|^3",
"symfony/cache-contracts": "^1.1.7|^2",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-php73": "^1.9",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.1|^2|^3",
"symfony/var-exporter": "^4.4|^5.0|^6.0"
},
"conflict": {
"doctrine/dbal": "<2.13.1",
"symfony/dependency-injection": "<4.4",
"symfony/http-kernel": "<4.4",
"symfony/var-dumper": "<4.4"
},
"provide": {
"psr/cache-implementation": "1.0|2.0",
"psr/simple-cache-implementation": "1.0|2.0",
"symfony/cache-implementation": "1.0|2.0"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/cache": "^1.6|^2.0",
"doctrine/dbal": "^2.13.1|^3|^4",
"predis/predis": "^1.1|^2.0",
"psr/simple-cache": "^1.0|^2.0",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/filesystem": "^4.4|^5.0|^6.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0",
"symfony/messenger": "^4.4|^5.0|^6.0",
"symfony/var-dumper": "^4.4|^5.0|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Cache\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides extended PSR-6, PSR-16 (and tags) implementations",
"homepage": "https://symfony.com",
"keywords": [
"caching",
"psr6"
],
"support": {
"source": "https://github.com/symfony/cache/tree/v5.4.46"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-11-04T11:43:55+00:00"
},
{
"name": "symfony/cache-contracts",
"version": "v2.5.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache-contracts.git",
"reference": "517c3a3619dadfa6952c4651767fcadffb4df65e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache-contracts/zipball/517c3a3619dadfa6952c4651767fcadffb4df65e",
"reference": "517c3a3619dadfa6952c4651767fcadffb4df65e",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/cache": "^1.0|^2.0|^3.0"
},
"suggest": {
"symfony/cache-implementation": ""
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "2.5-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Cache\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to caching",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/cache-contracts/tree/v2.5.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:11:13+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.5.4",
@@ -1082,6 +1357,82 @@
],
"time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/polyfill-php73",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
"reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb",
"reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php73\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.32.0",
@@ -1162,6 +1513,140 @@
],
"time": "2025-01-02T08:10:11+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v1.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/191afdcb5804db960d26d8566b7e9a2843cab3a0",
"reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0",
"shasum": ""
},
"require": {
"php": "^7.1.3"
},
"suggest": {
"psr/container": "",
"symfony/service-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v1.1.2"
},
"time": "2019-05-28T07:50:59+00:00"
},
{
"name": "symfony/var-exporter",
"version": "v5.4.45",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
"reference": "862700068db0ddfd8c5b850671e029a90246ec75"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/862700068db0ddfd8c5b850671e029a90246ec75",
"reference": "862700068db0ddfd8c5b850671e029a90246ec75",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"symfony/var-dumper": "^4.4.9|^5.0.9|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\VarExporter\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows exporting any serializable PHP data structure to plain PHP code",
"homepage": "https://symfony.com",
"keywords": [
"clone",
"construct",
"export",
"hydrate",
"instantiate",
"serialize"
],
"support": {
"source": "https://github.com/symfony/var-exporter/tree/v5.4.45"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:11:13+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.2",

View File

@@ -2,11 +2,14 @@
namespace App\Handlers;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\ImageTool\ImageToolInterface;
use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Contracts\Cache\ItemInterface;
class CategoriesHandler
{
@@ -15,41 +18,45 @@ class CategoriesHandler
private Builder $queryBuilder;
private ImageToolInterface $ocImageTool;
public function __construct(Builder $queryBuilder, ImageToolInterface $ocImageTool)
public function __construct(Builder $queryBuilder, ImageToolInterface $ocImageTool, Settings $settings)
{
$this->queryBuilder = $queryBuilder;
$this->ocImageTool = $ocImageTool;
$this->settings = $settings;
}
public function index(Request $request): JsonResponse
public function index(): JsonResponse
{
$languageId = 1;
$cache = new FilesystemAdapter();
$categoriesFlat = $this->queryBuilder->newQuery()
->select([
'categories.category_id' => 'id',
'categories.parent_id' => 'parent_id',
'categories.image' => 'image',
'descriptions.name' => 'name',
'descriptions.description' => 'description',
])
->from(db_table('category'), 'categories')
->join(
db_table('category_description') . ' AS descriptions',
function (JoinClause $join) use ($languageId) {
$join->on('categories.category_id', '=', 'descriptions.category_id')
->where('descriptions.language_id', '=', $languageId);
}
)
->where('categories.status', '=', 1)
->orderBy('parent_id')
->orderBy('sort_order')
->get();
$categories = $cache->get('main-page-categories', function (ItemInterface $item) {
$item->expiresAfter($this->settings->get('cache_categories_main', 60 * 5));
$languageId = 1;
$categories = $this->buildCategoryTree($categoriesFlat);
$categoriesFlat = $this->queryBuilder->newQuery()
->select([
'categories.category_id' => 'id',
'categories.parent_id' => 'parent_id',
'categories.image' => 'image',
'descriptions.name' => 'name',
'descriptions.description' => 'description',
])
->from(db_table('category'), 'categories')
->join(
db_table('category_description') . ' AS descriptions',
function (JoinClause $join) use ($languageId) {
$join->on('categories.category_id', '=', 'descriptions.category_id')
->where('descriptions.language_id', '=', $languageId);
}
)
->where('categories.status', '=', 1)
->orderBy('parent_id')
->orderBy('sort_order')
->get();
return new JsonResponse([
'data' => array_map(static function ($category) {
$categories = $this->buildCategoryTree($categoriesFlat);
return array_map(static function ($category) {
return [
'id' => (int)$category['id'],
'image' => $category['image'],
@@ -57,7 +64,11 @@ class CategoriesHandler
'description' => $category['description'],
'children' => $category['children'],
];
}, $categories),
}, $categories);
});
return new JsonResponse([
'data' => $categories,
]);
}

View File

@@ -15,6 +15,8 @@ use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\Support\Arr;
use Openguru\OpenCartFramework\Support\PaginationHelper;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Contracts\Cache\ItemInterface;
class ProductsHandler
{
@@ -44,20 +46,17 @@ class ProductsHandler
$this->oc = $registry;
}
public function handle(Request $request): JsonResponse
private function getProductsResponse(array $params): array
{
$page = $params['page'];
$perPage = $params['perPage'];
$categoryId = $params['categoryId'];
$search = $params['search'];
$forMainPage = $params['forMainPage'];
$featuredProducts = $params['featuredProducts'];
$mainpageProducts = $params['mainpageProducts'];
$languageId = 1;
$page = $request->get('page', 1);
$perPage = min((int)$request->get('perPage', 6), 15);
$categoryId = (int) $request->get('categoryId', 0);
$search = trim($request->get('search', ''));
$categoryName = '';
$forMainPage = $categoryId === 0;
$featuredProducts = $this->settings->get('featured_products');
$mainpageProducts = $this->settings->get('mainpage_products');
$imageWidth = 184;
$imageHeight = 245;
@@ -138,7 +137,7 @@ class ProductsHandler
];
}
return new JsonResponse([
return [
'data' => array_map(function ($product) use ($productsImagesMap, $imageWidth, $imageHeight) {
$allImages = [];
@@ -180,7 +179,62 @@ class ProductsHandler
'currentCategoryName' => $categoryName,
'hasMore' => $hasMore,
]
]);
];
}
public function handle(Request $request): JsonResponse
{
$cache = new FilesystemAdapter();
$page = (int) $request->get('page', 1);
$perPage = min((int) $request->get('perPage', 6), 15);
$categoryId = (int) $request->get('categoryId', 0);
$search = trim($request->get('search', ''));
$forMainPage = filter_var($request->get('forMainPage', false), FILTER_VALIDATE_BOOLEAN);
$featuredProducts = $this->settings->get('featured_products');
$mainpageProducts = $this->settings->get('mainpage_products');
if ($forMainPage && $page === 1) {
$response = $cache->get(
'main-page-products',
function (ItemInterface $cacheitem) use (
$page,
$perPage,
$categoryId,
$search,
$forMainPage,
$featuredProducts,
$mainpageProducts
): array {
$cacheitem->expiresAfter($this->settings->get('cache_products_main', 60 * 10));
return $this->getProductsResponse(
compact(
'page',
'perPage',
'categoryId',
'search',
'forMainPage',
'featuredProducts',
'mainpageProducts'
)
);
}
);
} else {
$response = $this->getProductsResponse(
compact(
'page',
'perPage',
'categoryId',
'search',
'forMainPage',
'featuredProducts',
'mainpageProducts'
)
);
}
return new JsonResponse($response);
}
public function show(Request $request): JsonResponse

View File

@@ -74,7 +74,7 @@ async function loadMore() {
productsStore.isLoading = true;
try {
const response = await productsStore.fetchProducts(categoryId, productsStore.page);
const response = await productsStore.fetchProducts(categoryId, productsStore.page, true);
productsStore.hasMore = response.meta.hasMore ?? false;
productsStore.products.data.push(...response.data);
productsStore.products.meta.currentCategoryName = response.meta.currentCategoryName;

View File

@@ -17,13 +17,14 @@ export const useProductsStore = defineStore('products', {
}),
actions: {
async fetchProducts(categoryId = null, page = 1) {
async fetchProducts(categoryId = null, page = 1, forMainPage = false) {
try {
this.isLoading = true;
return await ftch('products', {
categoryId: categoryId,
page: page,
search: this.search,
forMainPage: forMainPage,
});
} catch (error) {
console.error(error);