Files
orderPRO/src/Core/Application.php
Jacek Pyziak 7ac4293df4 feat: Implement Allegro Order Sync and Status Management
- 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.
2026-03-04 23:21:35 +01:00

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')");
}
}