- Added AllegroOrderSyncStateRepository for managing sync state with Allegro orders. - Introduced AllegroOrdersSyncService to handle the synchronization of orders from Allegro. - Created AllegroStatusDiscoveryService to discover and store order statuses from Allegro. - Developed AllegroStatusMappingRepository for managing status mappings between Allegro and OrderPro. - Implemented AllegroStatusSyncService to facilitate status synchronization. - Added CronSettingsController for managing cron job settings related to Allegro integration.
343 lines
10 KiB
PHP
343 lines
10 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Core;
|
|
|
|
use App\Core\Database\ConnectionFactory;
|
|
use App\Core\Database\Migrator;
|
|
use App\Core\Http\Request;
|
|
use App\Core\Http\Response;
|
|
use App\Core\I18n\Translator;
|
|
use App\Core\Routing\Router;
|
|
use App\Core\Support\Logger;
|
|
use App\Core\Support\Session;
|
|
use App\Core\View\Template;
|
|
use App\Modules\Auth\AuthService;
|
|
use App\Modules\Cron\AllegroOrdersImportHandler;
|
|
use App\Modules\Cron\AllegroStatusSyncHandler;
|
|
use App\Modules\Cron\AllegroTokenRefreshHandler;
|
|
use App\Modules\Cron\CronRepository;
|
|
use App\Modules\Cron\CronRunner;
|
|
use App\Modules\Orders\OrderImportRepository;
|
|
use App\Modules\Orders\OrdersRepository;
|
|
use App\Modules\Settings\AllegroApiClient;
|
|
use App\Modules\Settings\AllegroIntegrationRepository;
|
|
use App\Modules\Settings\AllegroOrderImportService;
|
|
use App\Modules\Settings\AllegroOrdersSyncService;
|
|
use App\Modules\Settings\AllegroOrderSyncStateRepository;
|
|
use App\Modules\Settings\AllegroOAuthClient;
|
|
use App\Modules\Settings\AllegroStatusSyncService;
|
|
use App\Modules\Settings\AllegroStatusMappingRepository;
|
|
use App\Modules\Settings\OrderStatusRepository;
|
|
use App\Modules\Users\UserRepository;
|
|
use Throwable;
|
|
use PDO;
|
|
|
|
final class Application
|
|
{
|
|
private Router $router;
|
|
private Template $template;
|
|
private AuthService $authService;
|
|
private UserRepository $userRepository;
|
|
private OrdersRepository $ordersRepository;
|
|
private OrderStatusRepository $orderStatusRepository;
|
|
private Migrator $migrator;
|
|
private PDO $db;
|
|
private Logger $logger;
|
|
private Translator $translator;
|
|
|
|
/**
|
|
* @param array<string, array<string, mixed>> $config
|
|
*/
|
|
public function __construct(
|
|
private readonly string $basePath,
|
|
private readonly array $config
|
|
) {
|
|
$this->router = new Router();
|
|
$this->translator = new Translator(
|
|
(string) $this->config('app.lang_path'),
|
|
(string) $this->config('app.locale', 'pl')
|
|
);
|
|
$this->template = new Template((string) $this->config('app.view_path'), $this->translator);
|
|
$this->db = ConnectionFactory::make((array) $this->config('database', []));
|
|
$this->userRepository = new UserRepository($this->db);
|
|
$this->ordersRepository = new OrdersRepository($this->db);
|
|
$this->orderStatusRepository = new OrderStatusRepository($this->db);
|
|
$this->migrator = new Migrator(
|
|
$this->db,
|
|
(string) $this->config('app.migrations_path', $this->basePath('database/migrations'))
|
|
);
|
|
$this->authService = new AuthService($this->userRepository);
|
|
$this->logger = new Logger((string) $this->config('app.log_path'));
|
|
}
|
|
|
|
public function boot(): void
|
|
{
|
|
$this->prepareDirectories();
|
|
$this->configureSession();
|
|
$this->registerErrorHandlers();
|
|
|
|
$routes = require $this->basePath . '/routes/web.php';
|
|
$routes($this);
|
|
}
|
|
|
|
public function run(): void
|
|
{
|
|
$request = Request::capture();
|
|
$this->maybeRunCronOnWeb($request);
|
|
$response = $this->router->dispatch($request);
|
|
$response->send();
|
|
}
|
|
|
|
public function basePath(string $path = ''): string
|
|
{
|
|
if ($path === '') {
|
|
return $this->basePath;
|
|
}
|
|
|
|
return $this->basePath . '/' . ltrim($path, '/');
|
|
}
|
|
|
|
public function router(): Router
|
|
{
|
|
return $this->router;
|
|
}
|
|
|
|
public function template(): Template
|
|
{
|
|
return $this->template;
|
|
}
|
|
|
|
public function auth(): AuthService
|
|
{
|
|
return $this->authService;
|
|
}
|
|
|
|
public function logger(): Logger
|
|
{
|
|
return $this->logger;
|
|
}
|
|
|
|
public function users(): UserRepository
|
|
{
|
|
return $this->userRepository;
|
|
}
|
|
|
|
public function orderStatuses(): OrderStatusRepository
|
|
{
|
|
return $this->orderStatusRepository;
|
|
}
|
|
|
|
public function orders(): OrdersRepository
|
|
{
|
|
return $this->ordersRepository;
|
|
}
|
|
|
|
public function db(): PDO
|
|
{
|
|
return $this->db;
|
|
}
|
|
|
|
public function migrator(): Migrator
|
|
{
|
|
return $this->migrator;
|
|
}
|
|
|
|
public function translator(): Translator
|
|
{
|
|
return $this->translator;
|
|
}
|
|
|
|
public function config(string $key, mixed $default = null): mixed
|
|
{
|
|
$segments = explode('.', $key);
|
|
$value = $this->config;
|
|
|
|
foreach ($segments as $segment) {
|
|
if (!is_array($value) || !array_key_exists($segment, $value)) {
|
|
return $default;
|
|
}
|
|
|
|
$value = $value[$segment];
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
private function prepareDirectories(): void
|
|
{
|
|
$required = [
|
|
$this->basePath('storage/logs'),
|
|
$this->basePath('storage/sessions'),
|
|
$this->basePath('storage/cache'),
|
|
$this->basePath('storage/tmp'),
|
|
];
|
|
|
|
foreach ($required as $directory) {
|
|
if (!is_dir($directory)) {
|
|
mkdir($directory, 0775, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function configureSession(): void
|
|
{
|
|
$sessionName = (string) $this->config('app.session.name', 'orderpro_session');
|
|
$sessionPath = (string) $this->config('app.session.path', $this->basePath('storage/sessions'));
|
|
|
|
if (is_dir($sessionPath)) {
|
|
session_save_path($sessionPath);
|
|
}
|
|
|
|
session_name($sessionName);
|
|
Session::start();
|
|
}
|
|
|
|
private function registerErrorHandlers(): void
|
|
{
|
|
$debug = (bool) $this->config('app.debug', false);
|
|
|
|
if ($debug) {
|
|
ini_set('display_errors', '1');
|
|
error_reporting(E_ALL);
|
|
} else {
|
|
ini_set('display_errors', '0');
|
|
error_reporting(E_ALL);
|
|
}
|
|
|
|
set_exception_handler(function (Throwable $exception) use ($debug): void {
|
|
$this->logger->error('Unhandled exception', [
|
|
'message' => $exception->getMessage(),
|
|
'file' => $exception->getFile(),
|
|
'line' => $exception->getLine(),
|
|
]);
|
|
|
|
$message = $debug ? $exception->getMessage() : 'Internal server error';
|
|
Response::html($message, 500)->send();
|
|
});
|
|
|
|
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
|
|
$this->logger->error('PHP error', [
|
|
'severity' => $severity,
|
|
'message' => $message,
|
|
'file' => $file,
|
|
'line' => $line,
|
|
]);
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
private function maybeRunCronOnWeb(Request $request): void
|
|
{
|
|
$path = $request->path();
|
|
if ($path === '/health' || str_starts_with($path, '/assets/')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$repository = new CronRepository($this->db);
|
|
$runOnWeb = $repository->getBoolSetting(
|
|
'cron_run_on_web',
|
|
(bool) $this->config('app.cron.run_on_web_default', false)
|
|
);
|
|
if (!$runOnWeb) {
|
|
return;
|
|
}
|
|
|
|
$webLimit = $repository->getIntSetting(
|
|
'cron_web_limit',
|
|
(int) $this->config('app.cron.web_limit_default', 5),
|
|
1,
|
|
100
|
|
);
|
|
|
|
if ($this->isWebCronThrottled(10)) {
|
|
return;
|
|
}
|
|
if (!$this->acquireWebCronLock()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$integrationRepository = new AllegroIntegrationRepository(
|
|
$this->db,
|
|
(string) $this->config('app.integrations.secret', '')
|
|
);
|
|
$oauthClient = new AllegroOAuthClient();
|
|
$apiClient = new AllegroApiClient();
|
|
$statusMappingRepository = new AllegroStatusMappingRepository($this->db);
|
|
$orderImportService = new AllegroOrderImportService(
|
|
$integrationRepository,
|
|
$oauthClient,
|
|
$apiClient,
|
|
new OrderImportRepository($this->db),
|
|
$statusMappingRepository
|
|
);
|
|
$ordersSyncService = new AllegroOrdersSyncService(
|
|
$integrationRepository,
|
|
new AllegroOrderSyncStateRepository($this->db),
|
|
$oauthClient,
|
|
$apiClient,
|
|
$orderImportService
|
|
);
|
|
|
|
$runner = new CronRunner(
|
|
$repository,
|
|
$this->logger,
|
|
[
|
|
'allegro_token_refresh' => new AllegroTokenRefreshHandler(
|
|
$integrationRepository,
|
|
$oauthClient
|
|
),
|
|
'allegro_orders_import' => new AllegroOrdersImportHandler(
|
|
$ordersSyncService
|
|
),
|
|
'allegro_status_sync' => new AllegroStatusSyncHandler(
|
|
new AllegroStatusSyncService(
|
|
$repository,
|
|
$ordersSyncService
|
|
)
|
|
),
|
|
]
|
|
);
|
|
$runner->run($webLimit);
|
|
} finally {
|
|
$this->releaseWebCronLock();
|
|
}
|
|
} catch (Throwable $exception) {
|
|
$this->logger->error('Web cron run failed', [
|
|
'message' => $exception->getMessage(),
|
|
'path' => $path,
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function isWebCronThrottled(int $minIntervalSeconds): bool
|
|
{
|
|
$safeInterval = max(1, $minIntervalSeconds);
|
|
$now = time();
|
|
$lastRunAt = isset($_SESSION['cron_web_last_run_at']) ? (int) $_SESSION['cron_web_last_run_at'] : 0;
|
|
|
|
if ($lastRunAt > 0 && ($now - $lastRunAt) < $safeInterval) {
|
|
return true;
|
|
}
|
|
|
|
$_SESSION['cron_web_last_run_at'] = $now;
|
|
return false;
|
|
}
|
|
|
|
private function acquireWebCronLock(): bool
|
|
{
|
|
$statement = $this->db->query("SELECT GET_LOCK('orderpro_web_cron_lock', 0)");
|
|
$value = $statement !== false ? $statement->fetchColumn() : false;
|
|
|
|
return (string) $value === '1';
|
|
}
|
|
|
|
private function releaseWebCronLock(): void
|
|
{
|
|
$this->db->query("DO RELEASE_LOCK('orderpro_web_cron_lock')");
|
|
}
|
|
}
|