feat: add migrations, mantenance tasks, database cache, blocks cache

This commit is contained in:
2025-11-13 02:24:00 +03:00
parent ab5c2f42b9
commit c0a6cb17b3
32 changed files with 948 additions and 213 deletions

View File

@@ -19,6 +19,7 @@
<script setup>
import {computed, onMounted, ref, useId} from "vue";
import {getThumb} from "@/utils/helpers.js";
const id = useId();
const model = defineModel();
@@ -26,13 +27,7 @@ const emit = defineEmits(['update:modelValue']);
const inputRef = ref(null);
const isLoaded = ref(false);
const thumb = computed(() => {
if (!model.value) return '/image/cache/no_image-100x100.png';
const extIndex = model.value.lastIndexOf('.');
const ext = model.value.substring(extIndex);
const filename = model.value.substring(0, extIndex);
return `/image/cache/${filename}-100x100${ext}`;
});
const thumb = computed(() => getThumb(model.value));
onMounted(() => {
const input = inputRef.value;

View File

@@ -1,11 +1,10 @@
<template>
<SettingsItem :label="label">
<template #default>
<OcImagePicker v-model="model"/>
<OcImagePicker v-model="model" class="tw:w-30"/>
</template>
<template #help><slot></slot></template>
</SettingsItem>
</template>
<script setup>

View File

@@ -1,5 +1,5 @@
export function getThumb(imageUrl) {
if (!imageUrl) return '/image/cache/no_image-100x100.png';
if (!imageUrl) return '/image/no_image.png';
const extIndex = imageUrl.lastIndexOf('.');
const ext = imageUrl.substring(extIndex);
const filename = imageUrl.substring(0, extIndex);

View File

@@ -2,6 +2,7 @@
use Bastion\ApplicationFactory;
use Cart\User;
use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\Http\Response as HttpResponse;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\Logger\OpenCartLogAdapter;
@@ -94,14 +95,12 @@ class ControllerExtensionModuleTgshop extends Controller
public function index(): void
{
$this->cleanUpOldAssets();
$this->migrateFromOldSettings();
$this->removeLegacyFiles();
$this->runMaintenanceTasks();
$this->injectVueJs();
$this->config();
$this->showConfigPage();
}
private function config(): void
private function showConfigPage(): void
{
$data = [];
$this->document->setTitle($this->language->get('heading_title'));
@@ -124,51 +123,8 @@ class ControllerExtensionModuleTgshop extends Controller
public function handle(): void
{
try {
$json = $this->model_setting_setting->getSetting('module_telecart');
if (! isset($json['module_telecart_settings'])) {
$json['module_telecart_settings'] = [];
}
$items = Arr::mergeArraysRecursively($json['module_telecart_settings'], [
'app' => [
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
'language_id' => (int) $this->config->get('config_language_id'),
],
'logs' => [
'path' => DIR_LOGS,
],
'database' => [
'host' => DB_HOSTNAME,
'database' => DB_DATABASE,
'username' => DB_USERNAME,
'password' => DB_PASSWORD,
'prefix' => DB_PREFIX,
'port' => (int) DB_PORT,
],
'store' => [
'oc_store_id' => 0,
'oc_default_currency' => $this->config->get('config_currency'),
'oc_config_tax' => filter_var($this->config->get('config_tax'), FILTER_VALIDATE_BOOLEAN),
],
'orders' => [
'oc_customer_group_id' => (int) $this->config->get('config_customer_group_id'),
],
'telegram' => [
'mini_app_url' => rtrim(HTTPS_CATALOG, '/') . '/image/catalog/tgshopspa/#/',
],
]);
$app = ApplicationFactory::create($items);
$app->bind(OcRegistryDecorator::class, fn() => new OcRegistryDecorator($this->registry));
$app
->withLogger(fn() => new OpenCartLogAdapter(
$this->log,
'TeleCartAdmin',
$app->getConfigValue('app.app_debug')
? LoggerInterface::LEVEL_DEBUG
: LoggerInterface::LEVEL_WARNING,
))
$this
->createApplication()
->bootAndHandleRequest();
} catch (Exception $e) {
$this->log->write('[TELECART] Error: ' . $e->getMessage());
@@ -264,68 +220,6 @@ class ControllerExtensionModuleTgshop extends Controller
return $map;
}
private function cleanUpOldAssets(): void
{
$spaPath = rtrim(DIR_IMAGE, '/') . '/catalog/tgshopspa';
$assetsPath = $spaPath . '/assets';
$manifestPath = $spaPath . '/manifest.json';
if (! file_exists($manifestPath)) {
$this->log->write('[TELECART] Файл manifest.json не найден — очистка пропущена.');
return;
}
try {
$contents = json_decode(file_get_contents($manifestPath), true, 512, JSON_THROW_ON_ERROR);
$entry = $contents['index.html'] ?? null;
if (! $entry) {
throw new RuntimeException('[TELECART] Некорректный manifest.json — отсутствует ключ index.html.');
}
$keep = [$entry['file']];
if (! empty($entry['css'])) {
foreach ($entry['css'] as $css) {
$keep[] = $css;
}
}
$deletedFiles = 0;
$keptFiles = 0;
foreach (glob($assetsPath . '/*') as $file) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
if (! in_array($ext, ['js', 'css', 'map'])) {
continue;
}
$relative = 'assets/' . basename($file);
if (in_array($relative, $keep, true)) {
$keptFiles++;
continue;
}
if (is_file($file)) {
unlink($file);
$deletedFiles++;
}
}
if ($deletedFiles > 0) {
$this->log->write(
sprintf(
'[TELECART] Очистка assets завершена. Удалено: %d, оставлено: %d',
$deletedFiles,
$keptFiles
)
);
}
} catch (JsonException $e) {
$this->log->write('[TELECART] Ошибка декодирования файла manifest.json: ' . $e->getMessage());
} catch (Exception $e) {
$this->log->write('[TELECART] Ошибка удаления старых assets: ' . $e->getMessage());
}
}
private function injectVueJs(): void
{
$appDir = rtrim(DIR_APPLICATION, '/');
@@ -340,92 +234,59 @@ class ControllerExtensionModuleTgshop extends Controller
}
}
private function migrateFromOldSettings(): void
private function createApplication(): Application
{
$legacySettings = $this->model_setting_setting->getSetting('module_tgshop');
if (! $legacySettings) {
return;
$json = $this->model_setting_setting->getSetting('module_telecart');
if (! isset($json['module_telecart_settings'])) {
$json['module_telecart_settings'] = [];
}
$newSettings = $this->model_setting_setting->getSetting('module_telecart');
static $mapLegacyToNewSettings = [
'module_tgshop_app_icon' => 'app.app_icon',
'module_tgshop_theme_light' => 'app.theme_light',
'module_tgshop_bot_token' => 'telegram.bot_token',
'module_tgshop_status' => 'app.app_enabled',
'module_tgshop_app_name' => 'app.app_name',
'module_tgshop_theme_dark' => 'app.theme_dark',
'module_tgshop_debug' => 'app.app_debug',
'module_tgshop_chat_id' => 'telegram.chat_id',
'module_tgshop_owner_notification_template' => 'telegram.owner_notification_template',
'module_tgshop_text_order_created_success' => 'texts.text_order_created_success',
'module_tgshop_enable_store' => 'store.enable_store',
'module_tgshop_yandex_metrika' => 'metrics.yandex_metrika_counter',
'module_tgshop_customer_notification_template' => 'telegram.customer_notification_template',
'module_tgshop_feature_vouchers' => 'store.feature_vouchers',
'module_tgshop_order_default_status_id' => 'orders.order_default_status_id',
'module_tgshop_feature_coupons' => 'store.feature_coupons',
'module_tgshop_text_no_more_products' => 'texts.text_no_more_products',
'module_tgshop_text_empty_cart' => 'texts.text_empty_cart',
];
if (! $newSettings) {
$data = [];
Arr::set($data, 'app.app_icon', $legacySettings['module_tgshop_app_icon']);
foreach ($mapLegacyToNewSettings as $key => $value) {
if (array_key_exists($key, $legacySettings)) {
if ($key === 'module_tgshop_status') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_debug') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_chat_id') {
$newValue = (int) $legacySettings[$key];
} elseif ($key === 'module_tgshop_enable_store') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_order_default_status_id') {
$newValue = (int) $legacySettings[$key];
} elseif ($key === 'module_tgshop_feature_vouchers') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_feature_coupons') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} else {
$newValue = $legacySettings[$key];
}
Arr::set($data, $value, $newValue);
}
}
Arr::set(
$data,
'metrics.yandex_metrika_enabled',
! empty(trim($legacySettings['module_tgshop_yandex_metrika']))
);
$this->model_setting_setting->editSetting('module_telecart', [
'module_telecart_settings' => $data,
$items = Arr::mergeArraysRecursively($json['module_telecart_settings'], [
'app' => [
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
'language_id' => (int) $this->config->get('config_language_id'),
],
'logs' => [
'path' => DIR_LOGS,
],
'database' => [
'host' => DB_HOSTNAME,
'database' => DB_DATABASE,
'username' => DB_USERNAME,
'password' => DB_PASSWORD,
'prefix' => DB_PREFIX,
'port' => (int) DB_PORT,
],
'store' => [
'oc_store_id' => 0,
'oc_default_currency' => $this->config->get('config_currency'),
'oc_config_tax' => filter_var($this->config->get('config_tax'), FILTER_VALIDATE_BOOLEAN),
],
'orders' => [
'oc_customer_group_id' => (int) $this->config->get('config_customer_group_id'),
],
'telegram' => [
'mini_app_url' => rtrim(HTTPS_CATALOG, '/') . '/image/catalog/tgshopspa/#/',
],
]);
$this->log->write('[TELECART] Выполнено обновление настроек с 1й версии модуля.');
$this->session->data['success'] = 'Выполнено обновление настроек с прошлой версии модуля.';
$app = ApplicationFactory::create($items);
$app->bind(OcRegistryDecorator::class, fn() => new OcRegistryDecorator($this->registry));
$app
->withLogger(fn() => new OpenCartLogAdapter(
$this->log,
'TeleCartAdmin',
$app->getConfigValue('app.app_debug')
? LoggerInterface::LEVEL_DEBUG
: LoggerInterface::LEVEL_INFO,
));
return $app;
}
$this->model_setting_setting->deleteSetting('module_tgshop');
}
private function removeLegacyFiles(): void
private function runMaintenanceTasks(): void
{
$legacyFilesToRemove = [
DIR_TEMPLATE . '/extension/module/tgshop_init.twig',
];
foreach ($legacyFilesToRemove as $file) {
if (file_exists($file)) {
unlink($file);
$this->log->write('[TELECART] Удалён старый файл: ' . $file);
}
}
$this->createApplication()->runMaintenanceTasks();
}
}

View File

@@ -17,7 +17,7 @@ class ApplicationFactory
{
public static function create(array $settings): Application
{
$defaultConfig = require __DIR__ . '/../src/config.php';
$defaultConfig = require __DIR__ . '/../configs/app.php';
$routes = require __DIR__ . '/routes.php';
$merged = Arr::mergeArraysRecursively($defaultConfig, $settings);

View File

@@ -0,0 +1,68 @@
<?php
namespace Bastion\Tasks;
use Exception;
use JsonException;
use Openguru\OpenCartFramework\MaintenanceTasks\BaseMaintenanceTask;
use RuntimeException;
class CleanUpOldAssets extends BaseMaintenanceTask
{
public function handle(): void
{
$spaPath = rtrim(DIR_IMAGE, '/') . '/catalog/tgshopspa';
$assetsPath = $spaPath . '/assets';
$manifestPath = $spaPath . '/manifest.json';
if (! file_exists($manifestPath)) {
return;
}
try {
$contents = json_decode(file_get_contents($manifestPath), true, 512, JSON_THROW_ON_ERROR);
$entry = $contents['index.html'] ?? null;
if (! $entry) {
throw new RuntimeException('Некорректный manifest.json — отсутствует ключ index.html.');
}
$keep = [$entry['file']];
if (! empty($entry['css'])) {
foreach ($entry['css'] as $css) {
$keep[] = $css;
}
}
$deletedFiles = 0;
$keptFiles = 0;
foreach (glob($assetsPath . '/*') as $file) {
$ext = pathinfo($file, PATHINFO_EXTENSION);
if (! in_array($ext, ['js', 'css', 'map'])) {
continue;
}
$relative = 'assets/' . basename($file);
if (in_array($relative, $keep, true)) {
$keptFiles++;
continue;
}
if (is_file($file)) {
unlink($file);
$deletedFiles++;
}
}
if ($deletedFiles > 0) {
$this->logger->info(
sprintf('Очистка assets завершена. Удалено: %d, оставлено: %d', $deletedFiles, $keptFiles)
);
}
} catch (JsonException $e) {
$this->logger->error('Ошибка декодирования файла manifest.json: ' . $e->getMessage());
} catch (Exception $e) {
$this->logger->error('Ошибка удаления старых assets: ' . $e->getMessage());
$this->logger->logException($e);
}
}
}

View File

@@ -26,7 +26,8 @@
"intervention/image": "^2.7",
"vlucas/phpdotenv": "^5.6",
"guzzlehttp/guzzle": "^7.9",
"symfony/cache": "^5.4"
"symfony/cache": "^5.4",
"doctrine/dbal": "^3.10"
},
"require-dev": {
"roave/security-advisories": "dev-latest",

View File

@@ -4,8 +4,262 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e8ed2d3d0e11eac86a27bb2972b115cd",
"content-hash": "3429e02a0f2458ebcd70b11ef4c3d0df",
"packages": [
{
"name": "doctrine/dbal",
"version": "3.10.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "65edaca19a752730f290ec2fb89d593cb40afb43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/65edaca19a752730f290ec2fb89d593cb40afb43",
"reference": "65edaca19a752730f290ec2fb89d593cb40afb43",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2",
"doctrine/deprecations": "^0.5.3|^1",
"doctrine/event-manager": "^1|^2",
"php": "^7.4 || ^8.0",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"conflict": {
"doctrine/cache": "< 1.11"
},
"require-dev": {
"doctrine/cache": "^1.11|^2.0",
"doctrine/coding-standard": "14.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "2.1.30",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "9.6.29",
"slevomat/coding-standard": "8.24.0",
"squizlabs/php_codesniffer": "4.0.0",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"bin": [
"bin/doctrine-dbal"
],
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\DBAL\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
}
],
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"keywords": [
"abstraction",
"database",
"db2",
"dbal",
"mariadb",
"mssql",
"mysql",
"oci8",
"oracle",
"pdo",
"pgsql",
"postgresql",
"queryobject",
"sasql",
"sql",
"sqlite",
"sqlserver",
"sqlsrv"
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.10.3"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
"type": "tidelift"
}
],
"time": "2025-10-09T09:05:12+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=13"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^13",
"phpstan/phpstan": "1.4.10 || 2.1.11",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.5"
},
"time": "2025-04-07T20:06:18+00:00"
},
{
"name": "doctrine/event-manager",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/event-manager.git",
"reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/95aa4cb529f1e96576f3fda9f5705ada4056a520",
"reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^0.5.3 || ^1",
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/common": "<2.9"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^10",
"phpstan/phpstan": "~1.4.10 || ^1.8.8",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"vimeo/psalm": "^4.24"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
"homepage": "https://www.doctrine-project.org/projects/event-manager.html",
"keywords": [
"event",
"event dispatcher",
"event manager",
"event system",
"events"
],
"support": {
"issues": "https://github.com/doctrine/event-manager/issues",
"source": "https://github.com/doctrine/event-manager/tree/1.2.0"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
"type": "tidelift"
}
],
"time": "2022-10-12T20:51:15+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.3",

View File

@@ -76,4 +76,12 @@ TEXT,
],
],
],
'cache' => [
'namespace' => 'telecart',
'default_lifetime' => 60 * 60 * 24,
'options' => [
'db_table' => 'telecart_cache_items',
],
],
];

View File

@@ -0,0 +1,9 @@
<?php
use Bastion\Tasks\CleanUpOldAssets;
return [
'tasks' => [
CleanUpOldAssets::class,
],
];

View File

@@ -0,0 +1,77 @@
<?php
use Openguru\OpenCartFramework\Migrations\Migration;
use Openguru\OpenCartFramework\OpenCart\Decorators\OcRegistryDecorator;
use Openguru\OpenCartFramework\Support\Arr;
return new class extends Migration {
public function up(): void
{
$opencart = $this->app->get(OcRegistryDecorator::class);
$opencart->load->model('setting/setting');
$legacySettings = $opencart->model_setting_setting->getSetting('module_tgshop');
if (! $legacySettings) {
return;
}
$newSettings = $opencart->model_setting_setting->getSetting('module_telecart');
static $mapLegacyToNewSettings = [
'module_tgshop_app_icon' => 'app.app_icon',
'module_tgshop_theme_light' => 'app.theme_light',
'module_tgshop_bot_token' => 'telegram.bot_token',
'module_tgshop_status' => 'app.app_enabled',
'module_tgshop_app_name' => 'app.app_name',
'module_tgshop_theme_dark' => 'app.theme_dark',
'module_tgshop_debug' => 'app.app_debug',
'module_tgshop_chat_id' => 'telegram.chat_id',
'module_tgshop_owner_notification_template' => 'telegram.owner_notification_template',
'module_tgshop_text_order_created_success' => 'texts.text_order_created_success',
'module_tgshop_enable_store' => 'store.enable_store',
'module_tgshop_yandex_metrika' => 'metrics.yandex_metrika_counter',
'module_tgshop_customer_notification_template' => 'telegram.customer_notification_template',
'module_tgshop_feature_vouchers' => 'store.feature_vouchers',
'module_tgshop_order_default_status_id' => 'orders.order_default_status_id',
'module_tgshop_feature_coupons' => 'store.feature_coupons',
'module_tgshop_text_no_more_products' => 'texts.text_no_more_products',
'module_tgshop_text_empty_cart' => 'texts.text_empty_cart',
];
if (! $newSettings) {
$data = [];
foreach ($mapLegacyToNewSettings as $key => $value) {
if (array_key_exists($key, $legacySettings)) {
if ($key === 'module_tgshop_status') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_debug') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_chat_id') {
$newValue = (int) $legacySettings[$key];
} elseif ($key === 'module_tgshop_enable_store') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_order_default_status_id') {
$newValue = (int) $legacySettings[$key];
} elseif ($key === 'module_tgshop_feature_vouchers') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} elseif ($key === 'module_tgshop_feature_coupons') {
$newValue = filter_var($legacySettings[$key], FILTER_VALIDATE_BOOLEAN);
} else {
$newValue = $legacySettings[$key];
}
Arr::set($data, $value, $newValue);
}
}
$opencart->model_setting_setting->editSetting('module_telecart', [
'module_telecart_settings' => $data,
]);
$this->logger->info('Выполнено обновление настроек с 1й версии модуля.');
}
$opencart->model_setting_setting->deleteSetting('module_tgshop');
}
};

View File

@@ -0,0 +1,11 @@
<?php
use Openguru\OpenCartFramework\Migrations\Migration;
use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter;
return new class extends Migration {
public function up(): void
{
$this->app->get(DoctrineDbalAdapter::class)->createTable();
}
};

View File

@@ -0,0 +1,19 @@
<?php
use Openguru\OpenCartFramework\Migrations\Migration;
return new class extends Migration {
public function up(): void
{
$legacyFilesToRemove = [
DIR_TEMPLATE . '/extension/module/tgshop_init.twig',
];
foreach ($legacyFilesToRemove as $file) {
if (file_exists($file)) {
unlink($file);
$this->logger->info('Удалён старый файл: ' . $file);
}
}
}
};

View File

@@ -11,6 +11,9 @@ use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Request;
use Openguru\OpenCartFramework\Logger\Logger;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksService;
use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksServiceProvider;
use Openguru\OpenCartFramework\Migrations\MigrationsServiceProvider;
use Openguru\OpenCartFramework\Router\Router;
use Openguru\OpenCartFramework\Support\ExecutionTimeProfiler;
@@ -160,4 +163,16 @@ class Application extends Container
{
return $this->routes;
}
public function runMaintenanceTasks(): void
{
$this->serviceProviders = array_merge($this->serviceProviders, [
MigrationsServiceProvider::class,
MaintenanceTasksServiceProvider::class,
]);
$this->boot();
$this->get(MaintenanceTasksService::class)->run();
}
}

View File

@@ -5,7 +5,10 @@ namespace Openguru\OpenCartFramework\Cache;
interface CacheInterface
{
public function get(string $key);
public function set(string $key, $value, ?int $ttlSeconds = null): void;
public function delete(string $key): void;
public function clear(): void;
}

View File

@@ -2,14 +2,32 @@
namespace Openguru\OpenCartFramework\Cache;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider;
use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter;
class CacheServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->singleton(CacheInterface::class, function () {
return new SymfonyFilesystemCache('app.cache', 0, DIR_CACHE);
$this->container->singleton(DoctrineDbalAdapter::class, function (Container $container) {
$host = $container->getConfigValue('database.host');
$username = $container->getConfigValue('database.username');
$password = $container->getConfigValue('database.password');
$port = (int) $container->getConfigValue('database.port');
$dbName = $container->getConfigValue('database.database');
$dsn = "mysql://$username:$password@$host:$port/$dbName?charset=utf8mb4";
$namespace = $container->getConfigValue('cache.namespace');
$defaultLifetime = $container->getConfigValue('cache.default_lifetime');
$options = $container->getConfigValue('cache.options');
return new DoctrineDbalAdapter($dsn, $namespace, $defaultLifetime, $options);
});
$this->container->singleton(CacheInterface::class, function (Container $container) {
return new SymfonyMySqlCache($container->get(DoctrineDbalAdapter::class));
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Openguru\OpenCartFramework\Cache;
use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter;
class SymfonyMySqlCache implements CacheInterface
{
protected DoctrineDbalAdapter $cache;
public function __construct(DoctrineDbalAdapter $adapter)
{
$this->cache = $adapter;
}
public function get(string $key)
{
$item = $this->cache->getItem($key);
return $item->isHit() ? $item->get() : null;
}
public function set(string $key, $value, ?int $ttlSeconds = null): void
{
$item = $this->cache->getItem($key);
$item->set($value);
if ($ttlSeconds) {
$item->expiresAfter($ttlSeconds);
}
$this->cache->save($item);
}
public function delete(string $key): void
{
$this->cache->deleteItem($key);
}
public function clear(): void
{
$this->cache->clear();
}
}

View File

@@ -6,6 +6,7 @@ use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider;
use Openguru\OpenCartFramework\Settings\DatabaseUserSettings;
use Openguru\OpenCartFramework\Settings\UserSettingsInterface;
use Openguru\OpenCartFramework\Support\SupportServiceProvider;
use RuntimeException;
class DependencyRegistration
@@ -16,6 +17,8 @@ class DependencyRegistration
return $container->get(DatabaseUserSettings::class);
});
$container->get(SupportServiceProvider::class)->register();
static::registerServiceProviders($container, $serviceProviders);
}

View File

@@ -34,6 +34,10 @@ class ErrorHandler
public function handleError(int $severity, string $message, string $file, int $line): bool
{
if (($severity & E_DEPRECATED|E_USER_DEPRECATED)) {
return true;
}
$this->logger->error('Handled PHP error: ' . implode(', ', compact('severity', 'message', 'file', 'line')));
// Convert warnings and notices to ErrorException
@@ -51,6 +55,7 @@ class ErrorHandler
$response = $customHandler->respond($exception);
if ($response !== null) {
$response->send();
return;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Openguru\OpenCartFramework\MaintenanceTasks;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
abstract class BaseMaintenanceTask implements MaintenanceTaskInterface
{
protected LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Openguru\OpenCartFramework\MaintenanceTasks;
interface MaintenanceTaskInterface
{
public function handle(): void;
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Openguru\OpenCartFramework\MaintenanceTasks;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\Migrations\MigratorService;
class MaintenanceTasksService
{
private MigratorService $migrator;
private LoggerInterface $logger;
private array $maintenanceTasks;
public function __construct(
MigratorService $migrator,
LoggerInterface $logger,
array $maintenanceTasks = []
) {
$this->migrator = $migrator;
$this->logger = $logger;
$this->maintenanceTasks = $maintenanceTasks;
}
public function run(): void
{
$this->migrator->migrate();
$this->performMaintenanceTasks();
}
private function performMaintenanceTasks(): void
{
foreach ($this->maintenanceTasks as $maintenanceTask) {
$startTime = microtime(true);
/** @var MaintenanceTaskInterface $instance */
$instance = $maintenanceTask();
$instance->handle();
$endTime = microtime(true);
$this->logger->info(
sprintf(
'Maintenance task %s executed by %d seconds',
get_class($instance),
$endTime - $startTime
),
);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Openguru\OpenCartFramework\MaintenanceTasks;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\Migrations\MigratorService;
class MaintenanceTasksServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->singleton(MaintenanceTasksService::class, function (Container $container) {
$config = require configs_path('maintenance.php');
$classes = $config['tasks'];
$tasks = array_map(static fn(string $class) => static fn() => $container->get($class), $classes);
return new MaintenanceTasksService(
$container->get(MigratorService::class),
$container->get(LoggerInterface::class),
$tasks,
);
});
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Openguru\OpenCartFramework\Migrations;
use Openguru\OpenCartFramework\Application;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
abstract class Migration
{
protected Application $app;
protected ConnectionInterface $database;
protected LoggerInterface $logger;
public function __construct()
{
$this->app = Application::getInstance();
$this->database = $this->app->get(ConnectionInterface::class);
$this->logger = $this->app->get(LoggerInterface::class);
}
abstract public function up(): void;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Openguru\OpenCartFramework\Migrations;
use Openguru\OpenCartFramework\Container\Container;
use Openguru\OpenCartFramework\Container\ServiceProvider;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Openguru\OpenCartFramework\Support\WorkLogsBag;
class MigrationsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->singleton(MigratorService::class, function (Container $app) {
return new MigratorService(
$app->get(ConnectionInterface::class),
$app->get(LoggerInterface::class),
database_path('migrations'),
);
});
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Openguru\OpenCartFramework\Migrations;
use Exception;
use Openguru\OpenCartFramework\Logger\LoggerInterface;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use RuntimeException;
class MigratorService
{
private ConnectionInterface $connection;
private LoggerInterface $logger;
private string $migrationsPath;
public function __construct(
ConnectionInterface $connection,
LoggerInterface $logger,
string $migrationsPath
) {
$this->connection = $connection;
$this->logger = $logger;
$this->migrationsPath = $migrationsPath;
}
public function migrate(): void
{
$this->ensureMigrationsTableExists();
$applied = $this->getAppliedMigrations();
$migrationsToApply = $this->findMigrationsToApply($applied);
$count = $this->applyMigrations($migrationsToApply);
if ($count > 0) {
$this->logger->log("$count migrations applied");
}
}
private function getMigrationsTableName(): string
{
return 'telecart_migrations';
}
private function ensureMigrationsTableExists(): void
{
$isExists = $this->connection->tableExists($this->getMigrationsTableName());
if (! $isExists) {
$this->connection->statement(
$this->createMigrationsTableSql()
);
$this->logger->log('Migrations table created.');
}
}
private function createMigrationsTableSql(): string
{
return <<<SQL
CREATE TABLE IF NOT EXISTS `{$this->getMigrationsTableName()}` (
migration VARCHAR(191) NOT NULL,
executed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (migration)
) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;
SQL;
}
private function findMigrationsToApply(array $applied): array
{
$migrations = [];
foreach (glob($this->migrationsPath . '/*.php') as $file) {
$migrations[] = basename($file);
}
return array_diff($migrations, $applied);
}
private function getAppliedMigrations(): array
{
$migrations = $this->connection->select("SELECT migration FROM {$this->getMigrationsTableName()}");
return array_column($migrations, 'migration');
}
private function applyMigrations(array $files): int
{
$count = 0;
foreach ($files as $file) {
try {
$this->connection->beginTransaction();
$migration = require $this->migrationsPath . '/' . $file;
if (! is_object($migration) || ! method_exists($migration, 'up')) {
throw new RuntimeException("Migration file $file is invalid.");
}
$migration->up();
$this->writeMigration($file);
$this->logger->log("Migrated $file");
$count++;
$this->connection->commitTransaction();
} catch (Exception $e) {
$this->connection->rollbackTransaction();
$this->logger->error("An error occurred while applying migration.");
$this->logger->logException($e);
}
}
return $count;
}
private function writeMigration(string $file): void
{
$this->connection->insert($this->getMigrationsTableName(), [
'migration' => $file,
'executed_at' => date('Y-m-d H:i:s'),
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Openguru\OpenCartFramework\Support;
use Openguru\OpenCartFramework\Container\ServiceProvider;
class SupportServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->bind(WorkLogsBag::class, function () {
return new WorkLogsBag([]);
});
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Openguru\OpenCartFramework\Support;
class WorkLogsBag
{
private array $items;
public function __construct(array $items = [])
{
$this->items = $items;
}
public function push(string $message): void
{
$this->items[] = sprintf('[%s] %s', date('Y-m-d H:i:s'), $message);
}
public function merge(array $items): void
{
$this->items = array_merge($this->items, $items);
}
public function getItems(): array
{
return $this->items;
}
public function formatAsList(): string
{
if (empty($this->items)) {
return '';
}
$list = '<ul>';
foreach ($this->items as $item) {
$list .= sprintf('<li>%s</li>', $item);
}
$list .= '</ul>';
return $list;
}
}

View File

@@ -30,6 +30,13 @@ if (! function_exists('column')) {
}
}
if (! function_exists('configs_path')) {
function configs_path(string $path = ''): string
{
return BP_BASE_PATH . '/configs/' . $path;
}
}
if (! function_exists('resources_path')) {
function resources_path(string $path = ''): string
{
@@ -37,6 +44,13 @@ if (! function_exists('resources_path')) {
}
}
if (! function_exists('database_path')) {
function database_path(string $path = ''): string
{
return BP_BASE_PATH . '/database/' . $path;
}
}
if (! function_exists('base_path')) {
function base_path(string $path = ''): string
{

View File

@@ -17,7 +17,7 @@ class ApplicationFactory
{
public static function create(array $config): Application
{
$defaultConfig = require __DIR__ . '/config.php';
$defaultConfig = require __DIR__ . '/../configs/app.php';
$routes = require __DIR__ . '/routes.php';
return (new Application(Arr::mergeArraysRecursively($defaultConfig, $config)))

View File

@@ -39,13 +39,24 @@ class BlocksService
public function process(array $block): array
{
$blockType = $block['type'];
$cacheKey = "block_$blockType";
$cacheTtlSeconds = 60;
$data = $this->cache->get($cacheKey);
if (! $data) {
$method = self::$processors[$block['type']] ?? null;
if (! $method) {
throw new RuntimeException('Processor for block type ' . $block['type'] . ' does not exist');
}
return call_user_func_array($method, [$block]);
$data = call_user_func_array($method, [$block]);
$this->cache->set($cacheKey, $data, $cacheTtlSeconds);
}
return $data;
}
private function processSlider(array $block): array