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