feat: add scheduler module
This commit is contained in:
@@ -112,7 +112,7 @@ class ControllerExtensionModuleTgshop extends Controller
|
||||
$data['themes'] = self::$themes;
|
||||
$data['telecart_module_version'] = module_version();
|
||||
$data['shop_base_url'] = HTTPS_CATALOG;
|
||||
|
||||
|
||||
$data['action'] = $this->url->link(
|
||||
'extension/module/tgshop',
|
||||
'user_token=' . $this->session->data['user_token'],
|
||||
@@ -263,7 +263,7 @@ class ControllerExtensionModuleTgshop extends Controller
|
||||
|
||||
private function createLogger(bool $debug = false): Logger
|
||||
{
|
||||
$log = new Logger('TeleCart_Admin');
|
||||
$log = new Logger('TeleCart_Admin', [], [], new DateTimeZone('UTC'));
|
||||
$log->pushHandler(
|
||||
new RotatingFileHandler(
|
||||
DIR_LOGS . '/telecart.log', 14, $debug ? Logger::DEBUG : Logger::INFO
|
||||
|
||||
@@ -127,7 +127,7 @@ JS;
|
||||
|
||||
private function createLogger(bool $appDebug = false, string $app = 'TeleCart'): LoggerInterface
|
||||
{
|
||||
$log = new Logger($app);
|
||||
$log = new Logger($app, [], [], new DateTimeZone('UTC'));
|
||||
$log->pushHandler(
|
||||
new RotatingFileHandler(DIR_LOGS . '/telecart.log', 14, $appDebug ? Logger::DEBUG : Logger::INFO),
|
||||
);
|
||||
|
||||
91
module/oc_telegram_shop/upload/cli.php
Normal file → Executable file
91
module/oc_telegram_shop/upload/cli.php
Normal file → Executable file
@@ -1,18 +1,95 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Console\ApplicationFactory;
|
||||
use Console\Commands\ScheduleListCommand;
|
||||
use Console\Commands\ScheduleRunCommand;
|
||||
use Console\Commands\VersionCommand;
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\MySqlConnection;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
die("This script can only be run from CLI.\n");
|
||||
}
|
||||
|
||||
$baseDir = __DIR__;
|
||||
$debug = true;
|
||||
|
||||
$sysLibPath = rtrim(DIR_SYSTEM, '/') . '/library/oc_telegram_shop';
|
||||
$basePath = rtrim(DIR_APPLICATION, '/') . '/..';
|
||||
if (is_readable($sysLibPath . '/oc_telegram_shop.phar')) {
|
||||
require_once "phar://{$sysLibPath}/oc_telegram_shop.phar/vendor/autoload.php";
|
||||
} elseif (is_dir("$basePath/oc_telegram_shop")) {
|
||||
require_once "$basePath/oc_telegram_shop/vendor/autoload.php";
|
||||
if (is_readable($baseDir . '/oc_telegram_shop.phar')) {
|
||||
require_once "phar://{$baseDir}/oc_telegram_shop.phar/vendor/autoload.php";
|
||||
require_once $baseDir . '/../../../admin/config.php';
|
||||
} elseif (is_dir("$baseDir/oc_telegram_shop")) {
|
||||
require_once "$baseDir/oc_telegram_shop/vendor/autoload.php";
|
||||
require_once '/web/upload/admin/config.php';
|
||||
} else {
|
||||
throw new RuntimeException('Unable to locate application directory.');
|
||||
}
|
||||
}
|
||||
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
// Get Settings from Database
|
||||
$host = DB_HOSTNAME;
|
||||
$username = DB_USERNAME;
|
||||
$password = DB_PASSWORD;
|
||||
$port = (int) DB_PORT;
|
||||
$dbName = DB_DATABASE;
|
||||
$prefix = DB_PREFIX;
|
||||
$dsn = "mysql:host=$host;port=$port;dbname=$dbName";
|
||||
$pdo = new PDO($dsn, $username, $password);
|
||||
$connection = new MySqlConnection($pdo);
|
||||
$raw = $connection->select("SELECT value FROM `{$prefix}setting` WHERE `key` = 'module_telecart_settings'");
|
||||
$json = json_decode($raw[0]['value'], true, 512, JSON_THROW_ON_ERROR);
|
||||
$items = Arr::mergeArraysRecursively($json, [
|
||||
'app' => [
|
||||
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
|
||||
'language_id' => 1,
|
||||
],
|
||||
'paths' => [
|
||||
'images' => DIR_IMAGE,
|
||||
],
|
||||
'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' => 1,
|
||||
'oc_config_tax' => false,
|
||||
],
|
||||
'orders' => [
|
||||
'oc_customer_group_id' => 1,
|
||||
],
|
||||
'telegram' => [
|
||||
'mini_app_url' => rtrim(HTTPS_CATALOG, '/') . '/image/catalog/tgshopspa/#/',
|
||||
],
|
||||
]);
|
||||
|
||||
// Create logger
|
||||
$logger = new Logger('TeleCart_CLI', [], [], new DateTimeZone('UTC'));
|
||||
$logger->pushHandler(
|
||||
new RotatingFileHandler(
|
||||
DIR_LOGS . '/telecart.log', 14, $debug ? Logger::DEBUG : Logger::INFO
|
||||
),
|
||||
);
|
||||
|
||||
// Creates TeleCart application.
|
||||
$app = ApplicationFactory::create($items);
|
||||
$app->setLogger($logger);
|
||||
$app->boot();
|
||||
|
||||
// Creates Console and bind commands.
|
||||
$console = new Application('TeleCart', module_version());
|
||||
$console->add($app->get(VersionCommand::class));
|
||||
$console->add($app->get(ScheduleRunCommand::class));
|
||||
$console->add($app->get(ScheduleListCommand::class));
|
||||
$console->run();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"Openguru\\OpenCartFramework\\": "framework/",
|
||||
"App\\": "src/",
|
||||
"Bastion\\": "bastion/",
|
||||
"Console\\": "console/",
|
||||
"Tests\\": "tests/"
|
||||
},
|
||||
"files": [
|
||||
@@ -32,7 +33,9 @@
|
||||
"symfony/cache": "^5.4",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"ramsey/uuid": "^4.2",
|
||||
"symfony/http-foundation": "^5.4"
|
||||
"symfony/http-foundation": "^5.4",
|
||||
"symfony/console": "^5.4",
|
||||
"dragonmantank/cron-expression": "^3.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/sql-formatter": "^1.3",
|
||||
|
||||
@@ -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": "73829e240f399344756292ca05f62e89",
|
||||
"content-hash": "73f60d8ed1037cbbd7e6368936ed1dc7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -389,6 +389,70 @@
|
||||
],
|
||||
"time": "2022-10-12T20:51:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dragonmantank/cron-expression",
|
||||
"version": "v3.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dragonmantank/cron-expression.git",
|
||||
"reference": "1b2de7f4a468165dca07b142240733a1973e766d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/1b2de7f4a468165dca07b142240733a1973e766d",
|
||||
"reference": "1b2de7f4a468165dca07b142240733a1973e766d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2|^8.0"
|
||||
},
|
||||
"replace": {
|
||||
"mtdowling/cron-expression": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^1.12.32|^2.1.31",
|
||||
"phpunit/phpunit": "^8.5.48|^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cron\\": "src/Cron/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Chris Tankersley",
|
||||
"email": "chris@ctankersley.com",
|
||||
"homepage": "https://github.com/dragonmantank"
|
||||
}
|
||||
],
|
||||
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
|
||||
"keywords": [
|
||||
"cron",
|
||||
"schedule"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/dragonmantank/cron-expression/issues",
|
||||
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/dragonmantank",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-31T18:36:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"version": "v1.1.3",
|
||||
@@ -1912,6 +1976,105 @@
|
||||
],
|
||||
"time": "2024-09-25T14:11:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v5.4.47",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed",
|
||||
"reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/polyfill-php73": "^1.9",
|
||||
"symfony/polyfill-php80": "^1.16",
|
||||
"symfony/service-contracts": "^1.1|^2|^3",
|
||||
"symfony/string": "^5.1|^6.0"
|
||||
},
|
||||
"conflict": {
|
||||
"psr/log": ">=3",
|
||||
"symfony/dependency-injection": "<4.4",
|
||||
"symfony/dotenv": "<5.1",
|
||||
"symfony/event-dispatcher": "<4.4",
|
||||
"symfony/lock": "<4.4",
|
||||
"symfony/process": "<4.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "1.0|2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "^1|^2",
|
||||
"symfony/config": "^4.4|^5.0|^6.0",
|
||||
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
|
||||
"symfony/event-dispatcher": "^4.4|^5.0|^6.0",
|
||||
"symfony/lock": "^4.4|^5.0|^6.0",
|
||||
"symfony/process": "^4.4|^5.0|^6.0",
|
||||
"symfony/var-dumper": "^4.4|^5.0|^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "For using the console logger",
|
||||
"symfony/event-dispatcher": "",
|
||||
"symfony/lock": "",
|
||||
"symfony/process": ""
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Console\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Eases the creation of beautiful and testable command line interfaces",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"command-line",
|
||||
"console",
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v5.4.47"
|
||||
},
|
||||
"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-06T11:30:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v2.5.4",
|
||||
@@ -2142,6 +2305,173 @@
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
|
||||
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
|
||||
}
|
||||
},
|
||||
"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 for intl's grapheme_* functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"grapheme",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-06-27T09:58:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"reference": "3833d7255cc303546435cb650316bff708a1c75c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
|
||||
},
|
||||
"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 for intl's Normalizer class and related functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"intl",
|
||||
"normalizer",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.33.0",
|
||||
@@ -2532,6 +2862,92 @@
|
||||
},
|
||||
"time": "2019-05-28T07:50:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v5.4.47",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "136ca7d72f72b599f2631aca474a4f8e26719799"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799",
|
||||
"reference": "136ca7d72f72b599f2631aca474a4f8e26719799",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-intl-grapheme": "~1.0",
|
||||
"symfony/polyfill-intl-normalizer": "~1.0",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/polyfill-php80": "~1.15"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/translation-contracts": ">=3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/error-handler": "^4.4|^5.0|^6.0",
|
||||
"symfony/http-client": "^4.4|^5.0|^6.0",
|
||||
"symfony/translation-contracts": "^1.1|^2",
|
||||
"symfony/var-exporter": "^4.4|^5.0|^6.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Resources/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\String\\": ""
|
||||
},
|
||||
"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 an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"grapheme",
|
||||
"i18n",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v5.4.47"
|
||||
},
|
||||
"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-10T20:33:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation",
|
||||
"version": "v5.4.45",
|
||||
|
||||
17
module/oc_telegram_shop/upload/oc_telegram_shop/configs/schedule.php
Executable file
17
module/oc_telegram_shop/upload/oc_telegram_shop/configs/schedule.php
Executable file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/** @var \Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry $scheduler */
|
||||
|
||||
// Define your scheduled tasks here.
|
||||
// The $scheduler variable is available in this scope.
|
||||
|
||||
// Example: Running a task class every 5 minutes. Class should have execute method.
|
||||
// $scheduler->add(\My\Task\Class::class)->everyFiveMinutes();
|
||||
|
||||
// Example: Running a closure every hour
|
||||
// $scheduler->add(function () {
|
||||
// // Do something
|
||||
// }, 'my_closure_task')->everyHour();
|
||||
|
||||
// Example: Custom cron expression
|
||||
// $scheduler->add(\My\Task\Class::class)->at('0 12 * * *');
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Console;
|
||||
|
||||
use App\ServiceProviders\AppServiceProvider;
|
||||
use App\ServiceProviders\SettingsServiceProvider;
|
||||
use Openguru\OpenCartFramework\Application;
|
||||
use Openguru\OpenCartFramework\Cache\CacheServiceProvider;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
|
||||
use Openguru\OpenCartFramework\Scheduler\SchedulerServiceProvider;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseServiceProvider;
|
||||
use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider;
|
||||
|
||||
class ApplicationFactory
|
||||
{
|
||||
public static function create(array $settings): Application
|
||||
{
|
||||
$defaultConfig = require __DIR__ . '/../configs/app.php';
|
||||
|
||||
$merged = Arr::mergeArraysRecursively($defaultConfig, $settings);
|
||||
|
||||
return (new Application($merged))
|
||||
->withServiceProviders([
|
||||
SettingsServiceProvider::class,
|
||||
QueryBuilderServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
CacheServiceProvider::class,
|
||||
TelegramServiceProvider::class,
|
||||
TeleCartPulseServiceProvider::class,
|
||||
SchedulerServiceProvider::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Console\Commands;
|
||||
|
||||
use Openguru\OpenCartFramework\Container\Container;
|
||||
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
|
||||
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ScheduleListCommand extends TeleCartCommand
|
||||
{
|
||||
private SchedulerService $schedulerService;
|
||||
private Container $container;
|
||||
|
||||
protected static $defaultName = 'schedule:list';
|
||||
protected static $defaultDescription = 'List all scheduled tasks and their status';
|
||||
|
||||
public function __construct(SchedulerService $schedulerService, Container $container)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->schedulerService = $schedulerService;
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$registry = new ScheduleJobRegistry($this->container);
|
||||
|
||||
// Load schedule config
|
||||
$configFile = BP_BASE_PATH . '/configs/schedule.php';
|
||||
if (file_exists($configFile)) {
|
||||
$scheduler = $registry; // Variable name used in config file
|
||||
require $configFile;
|
||||
}
|
||||
|
||||
$jobs = $registry->getJobs();
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(['Name / Class', 'Cron Expression', 'Last Run', 'Last Failure']);
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
$id = $job->getId();
|
||||
$lastRun = $this->schedulerService->getLastRun($id);
|
||||
$lastFailure = $this->schedulerService->getLastFailure($id);
|
||||
|
||||
$lastRunText = $lastRun ? date('Y-m-d H:i:s', $lastRun) : 'Never';
|
||||
|
||||
$lastFailureText = 'None';
|
||||
if ($lastFailure) {
|
||||
$lastFailureText = date('Y-m-d H:i:s', $lastFailure['time']) . "\n" . substr($lastFailure['message'], 0, 50);
|
||||
}
|
||||
|
||||
$table->addRow([
|
||||
$job->getName(),
|
||||
// We don't have getExpression public yet on Job, assuming we might need to add it or it's not critical.
|
||||
// Wait, Job class stores expression but doesn't expose it via public getter.
|
||||
// I should add getExpression() to Job.php if I want to show it.
|
||||
// For now, let's assume we add it.
|
||||
method_exists($job, 'getExpression') ? $job->getExpression() : '???',
|
||||
$lastRunText,
|
||||
$lastFailureText
|
||||
]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Console\Commands;
|
||||
|
||||
use Openguru\OpenCartFramework\Config\SettingsInterface;
|
||||
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ScheduleRunCommand extends TeleCartCommand
|
||||
{
|
||||
private SchedulerService $scheduler;
|
||||
private SettingsInterface $settings;
|
||||
|
||||
protected static $defaultName = 'schedule:run';
|
||||
protected static $defaultDescription = 'Run scheduled commands';
|
||||
|
||||
public function __construct(SchedulerService $scheduler, SettingsInterface $settings)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->scheduler = $scheduler;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$mode = $this->settings->get('cron.mode', 'disabled');
|
||||
if ($mode !== 'system') {
|
||||
$output->writeln('<comment>Scheduler is disabled. Skipping CLI execution.</comment>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeln('<info>TeleCart Scheduler Running...</info>');
|
||||
|
||||
$result = $this->scheduler->run();
|
||||
|
||||
// Print Executed
|
||||
if (empty($result->executed)) {
|
||||
$output->writeln('No tasks executed.');
|
||||
} else {
|
||||
foreach ($result->executed as $item) {
|
||||
$output->writeln(sprintf('<info>Executed:</info> %s (%.4fs)', $item['name'], $item['duration']));
|
||||
}
|
||||
}
|
||||
|
||||
// Print Failed
|
||||
foreach ($result->failed as $item) {
|
||||
$output->writeln(sprintf('<error>Failed:</error> %s - %s', $item['name'], $item['error']));
|
||||
}
|
||||
|
||||
// Print Skipped (verbose only)
|
||||
if ($output->isVerbose()) {
|
||||
foreach ($result->skipped as $item) {
|
||||
$output->writeln(sprintf('<comment>Skipped:</comment> %s - %s', $item['name'], $item['reason']));
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
class TeleCartCommand extends Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Console\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class VersionCommand extends TeleCartCommand
|
||||
{
|
||||
protected static $defaultName = 'version';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->writeln('TeleCart Version: ' . module_version());
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
102
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Scheduler/Job.php
Executable file
102
module/oc_telegram_shop/upload/oc_telegram_shop/framework/Scheduler/Job.php
Executable file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Scheduler;
|
||||
|
||||
use Cron\CronExpression;
|
||||
use Openguru\OpenCartFramework\Container\Container;
|
||||
|
||||
class Job
|
||||
{
|
||||
private Container $container;
|
||||
|
||||
/** @var string|callable|TaskInterface */
|
||||
private $action;
|
||||
|
||||
private string $expression = '* * * * *';
|
||||
|
||||
private ?string $name;
|
||||
|
||||
public function __construct(Container $container, $action, ?string $name = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->action = $action;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function at(string $expression): self
|
||||
{
|
||||
$this->expression = $expression;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function everyMinute(): self
|
||||
{
|
||||
return $this->at('* * * * *');
|
||||
}
|
||||
|
||||
public function everyFiveMinutes(): self
|
||||
{
|
||||
return $this->at('*/5 * * * *');
|
||||
}
|
||||
|
||||
public function everyTenMinutes(): self
|
||||
{
|
||||
return $this->at('*/10 * * * *');
|
||||
}
|
||||
|
||||
public function everyFifteenMinutes(): self
|
||||
{
|
||||
return $this->at('*/15 * * * *');
|
||||
}
|
||||
|
||||
public function everyHour(): self
|
||||
{
|
||||
return $this->at('0 * * * *');
|
||||
}
|
||||
|
||||
public function dailyAt(int $hour, int $minute = 0): self
|
||||
{
|
||||
return $this->at(sprintf('%d %d * * *', $minute, $hour));
|
||||
}
|
||||
|
||||
public function isDue(): bool
|
||||
{
|
||||
return (new CronExpression($this->expression))->isDue();
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if (is_string($this->action) && class_exists($this->action)) {
|
||||
/** @var TaskInterface $task */
|
||||
$task = $this->container->get($this->action);
|
||||
$task->execute();
|
||||
} elseif (is_callable($this->action)) {
|
||||
call_user_func($this->action);
|
||||
} elseif ($this->action instanceof TaskInterface) {
|
||||
$this->action->execute();
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
if ($this->name) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
if (is_string($this->action)) {
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
return 'Closure';
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return md5($this->getName());
|
||||
}
|
||||
|
||||
public function getExpression(): string
|
||||
{
|
||||
return $this->expression;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Scheduler;
|
||||
|
||||
use Openguru\OpenCartFramework\Container\Container;
|
||||
|
||||
class ScheduleJobRegistry
|
||||
{
|
||||
private Container $container;
|
||||
|
||||
/** @var Job[] */
|
||||
private array $jobs = [];
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|callable|TaskInterface $job
|
||||
* @param string|null $name
|
||||
* @return Job
|
||||
*/
|
||||
public function add($job, ?string $name = null): Job
|
||||
{
|
||||
$newJob = new Job($this->container, $job, $name);
|
||||
$this->jobs[] = $newJob;
|
||||
|
||||
return $newJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Job[]
|
||||
*/
|
||||
public function getJobs(): array
|
||||
{
|
||||
return $this->jobs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Scheduler;
|
||||
|
||||
class SchedulerResult
|
||||
{
|
||||
public array $executed = [];
|
||||
public array $failed = [];
|
||||
public array $skipped = [];
|
||||
|
||||
public function addExecuted(string $name, float $duration): void
|
||||
{
|
||||
$this->executed[] = [
|
||||
'name' => $name,
|
||||
'duration' => $duration,
|
||||
];
|
||||
}
|
||||
|
||||
public function addFailed(string $name, string $error): void
|
||||
{
|
||||
$this->failed[] = [
|
||||
'name' => $name,
|
||||
'error' => $error,
|
||||
];
|
||||
}
|
||||
|
||||
public function addSkipped(string $name, string $reason): void
|
||||
{
|
||||
$this->skipped[] = [
|
||||
'name' => $name,
|
||||
'reason' => $reason,
|
||||
];
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'executed' => $this->executed,
|
||||
'failed' => $this->failed,
|
||||
'skipped' => $this->skipped,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Scheduler;
|
||||
|
||||
use DateTime;
|
||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||
use Openguru\OpenCartFramework\Config\SettingsInterface;
|
||||
use Openguru\OpenCartFramework\Container\Container;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
class SchedulerService
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
private CacheInterface $cache;
|
||||
private Container $container;
|
||||
private SettingsInterface $settings;
|
||||
|
||||
private const GLOBAL_LOCK_KEY = 'scheduler.global_lock';
|
||||
private const GLOBAL_LOCK_TTL = 300; // 5 minutes
|
||||
|
||||
public function __construct(LoggerInterface $logger, CacheInterface $cache, Container $container, SettingsInterface $settings)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->cache = $cache;
|
||||
$this->container = $container;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function run(): SchedulerResult
|
||||
{
|
||||
$result = new SchedulerResult();
|
||||
|
||||
$mode = $this->settings->get('cron.mode', 'disabled');
|
||||
if ($mode === 'disabled') {
|
||||
$result->addSkipped('Global', 'Scheduler is disabled');
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($this->isGlobalLocked()) {
|
||||
$result->addSkipped('Global', 'Global scheduler lock active');
|
||||
return $result;
|
||||
}
|
||||
|
||||
$this->acquireGlobalLock();
|
||||
// Since we want to run every 5 minutes, running it more frequently won't trigger jobs earlier than due,
|
||||
// but locking might prevent overlap if previous run takes > 5 mins.
|
||||
// However, updating global last run on every attempt might be useful for diagnostics,
|
||||
// but strictly speaking, we only care if tasks were processed.
|
||||
$this->updateGlobalLastRun();
|
||||
|
||||
try {
|
||||
$scheduler = new ScheduleJobRegistry($this->container);
|
||||
|
||||
$configFile = BP_BASE_PATH . '/configs/schedule.php';
|
||||
if (file_exists($configFile)) {
|
||||
require $configFile;
|
||||
} else {
|
||||
$this->logger->warning('Scheduler config file not found: ' . $configFile);
|
||||
}
|
||||
|
||||
foreach ($scheduler->getJobs() as $job) {
|
||||
$this->processJob($job, $result);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error('Scheduler run failed: ' . $e->getMessage(), ['exception' => $e]);
|
||||
$result->addFailed('Scheduler', $e->getMessage());
|
||||
} finally {
|
||||
$this->releaseGlobalLock();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function processJob(Job $job, SchedulerResult $result): void
|
||||
{
|
||||
$name = $job->getName();
|
||||
$id = $job->getId();
|
||||
|
||||
try {
|
||||
// 1. Check if due by Cron expression
|
||||
if (! $job->isDue()) {
|
||||
$result->addSkipped($name, 'Not due');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check Last Run (Prevent running multiple times in the same minute)
|
||||
if ($this->hasRanRecently($id)) {
|
||||
$result->addSkipped($name, 'Already ran recently');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Check Lock (Prevent parallel execution)
|
||||
if ($this->isJobLocked($id)) {
|
||||
$result->addSkipped($name, 'Job is locked (running)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock and Run
|
||||
$this->lockJob($id);
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$job->run();
|
||||
|
||||
$duration = microtime(true) - $startTime;
|
||||
$this->updateLastRun($id);
|
||||
|
||||
$this->logger->info("Job executed: {$name}", ['duration' => $duration]);
|
||||
$result->addExecuted($name, $duration);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$this->updateLastFailure($id, $e->getMessage());
|
||||
$this->logger->error("Job failed: {$name}", ['exception' => $e]);
|
||||
$result->addFailed($name, $e->getMessage());
|
||||
} finally {
|
||||
$this->unlockJob($id);
|
||||
}
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error("Error processing job {$name}: " . $e->getMessage());
|
||||
$result->addFailed($name, 'Processing error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function isGlobalLocked(): bool
|
||||
{
|
||||
return (bool) $this->cache->get(self::GLOBAL_LOCK_KEY);
|
||||
}
|
||||
|
||||
private function acquireGlobalLock(): void
|
||||
{
|
||||
$this->cache->set(self::GLOBAL_LOCK_KEY, 1, self::GLOBAL_LOCK_TTL);
|
||||
}
|
||||
|
||||
private function releaseGlobalLock(): void
|
||||
{
|
||||
$this->cache->delete(self::GLOBAL_LOCK_KEY);
|
||||
}
|
||||
|
||||
private function isJobLocked(string $id): bool
|
||||
{
|
||||
return (bool) $this->cache->get("scheduler.lock.{$id}");
|
||||
}
|
||||
|
||||
private function lockJob(string $id): void
|
||||
{
|
||||
// 30 minutes max execution time for a single job safe-guard
|
||||
$this->cache->set("scheduler.lock.{$id}", 1, 1800);
|
||||
}
|
||||
|
||||
private function unlockJob(string $id): void
|
||||
{
|
||||
$this->cache->delete("scheduler.lock.{$id}");
|
||||
}
|
||||
|
||||
private function hasRanRecently(string $id): bool
|
||||
{
|
||||
$lastRun = $this->getLastRun($id);
|
||||
if (! $lastRun) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lastRunDate = (new DateTime())->setTimestamp((int)$lastRun);
|
||||
$now = new DateTime();
|
||||
|
||||
return $lastRunDate->format('Y-m-d H:i') === $now->format('Y-m-d H:i');
|
||||
}
|
||||
|
||||
private function updateLastRun(string $id): void
|
||||
{
|
||||
$this->cache->set("scheduler.last_run.{$id}", time());
|
||||
}
|
||||
|
||||
private function updateGlobalLastRun(): void
|
||||
{
|
||||
$this->cache->set("scheduler.global_last_run", time());
|
||||
}
|
||||
|
||||
public function getGlobalLastRun(): ?int
|
||||
{
|
||||
$time = $this->cache->get("scheduler.global_last_run");
|
||||
return $time ? (int)$time : null;
|
||||
}
|
||||
|
||||
private function updateLastFailure(string $id, string $message): void
|
||||
{
|
||||
$this->cache->set("scheduler.last_failure.{$id}", time());
|
||||
$this->cache->set("scheduler.last_failure_msg.{$id}", $message);
|
||||
}
|
||||
|
||||
public function getLastRun(string $id): ?int
|
||||
{
|
||||
return $this->cache->get("scheduler.last_run.{$id}");
|
||||
}
|
||||
|
||||
public function getLastFailure(string $id): ?array
|
||||
{
|
||||
$time = $this->cache->get("scheduler.last_failure.{$id}");
|
||||
if (!$time) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'time' => (int) $time,
|
||||
'message' => $this->cache->get("scheduler.last_failure_msg.{$id}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Scheduler;
|
||||
|
||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||
use Openguru\OpenCartFramework\Config\SettingsInterface;
|
||||
use Openguru\OpenCartFramework\Container\Container;
|
||||
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class SchedulerServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->container->singleton(SchedulerService::class, function (Container $container) {
|
||||
return new SchedulerService(
|
||||
$container->get(LoggerInterface::class),
|
||||
$container->get(CacheInterface::class),
|
||||
$container,
|
||||
$container->get(SettingsInterface::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Scheduler;
|
||||
|
||||
interface TaskInterface
|
||||
{
|
||||
public function execute(): void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user