Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.51% covered (success)
95.51%
596 / 624
80.00% covered (warning)
80.00%
24 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApplicationBootstrap
95.51% covered (success)
95.51%
595 / 623
80.00% covered (warning)
80.00%
24 / 30
63
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 bootstrapCoreSubsystems
100.00% covered (success)
100.00%
75 / 75
100.00% covered (success)
100.00%
1 / 1
2
 bootstrapApplicationRuntime
100.00% covered (success)
100.00%
107 / 107
100.00% covered (success)
100.00%
1 / 1
1
 getContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 dispatch
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 handleSessionSecurityValidation
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
12.72
 validateActiveSession
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
7.02
 checkImpersonationLifetime
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
19.47
 clearUserSession
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 validateLegacySession
n/a
0 / 0
n/a
0 / 0
3
 getCentralEngineApiController
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPsr17Factory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSession
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initializeDatabase
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 initializeCache
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 createRouteDispatcher
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 initializeCoreEnvironment
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
2
 initializeSecurityAndTranslation
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
1
 initializeDashboardSubsystem
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
1
 initializeClientUserRepositories
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 initializeFeatureControllers
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
 createApiDispatcher
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 initializeEngineSubsystem
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 assembleApis
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildImpersonationSubsystem
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
 buildRouteDispatcherOptions
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
1
 buildContainerServices
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 buildApplicationContainer
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 createCoreDefinitions
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
1 / 1
1
 loadDotEnv
23.08% covered (danger)
23.08%
3 / 13
0.00% covered (danger)
0.00%
0 / 1
45.87
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Bootstrap;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Access\Application\Service\AccessManagerService;
12use App\Core\Access\Domain\Repository\AccessRepositoryInterface;
13use App\Core\Access\Infrastructure\Cache\CachedAccessRepository;
14use App\Core\Access\Infrastructure\Repository\SqlAccessRepository;
15use App\Core\Access\Presentation\Api\AccessApiController;
16use App\Core\Access\Presentation\Web\AccessWebController;
17use App\Modules\Profiles\Application\Service\ProfilePermissionService;
18use App\Modules\Profiles\Domain\Repository\ProfilePermissionRepositoryInterface;
19use App\Modules\Profiles\Infrastructure\Repository\CachedProfilePermissionRepository;
20use App\Modules\Profiles\Infrastructure\Repository\SqlProfilePermissionRepository;
21use App\Modules\Profiles\Presentation\Api\ProfilePermissionApiController;
22use App\Core\Api\Domain\Repository\ApiLoggerInterface;
23use App\Core\Api\Domain\Service\PayloadSanitizer;
24use App\Core\Api\Infrastructure\Repository\SqlApiKeyRepository;
25use App\Core\Api\Infrastructure\Repository\SqlApiLoggerRepository;
26use App\Core\Api\Middleware\ApiLoggingMiddleware;
27use App\Core\Api\Middleware\ApiPayloadGuardMiddleware;
28use App\Core\Api\Middleware\ApiTokenMiddleware;
29use App\Core\Api\Middleware\CorsMiddleware;
30use App\Core\Api\Middleware\IdempotencyMiddleware;
31use App\Core\Api\Middleware\RequestIdMiddleware;
32use App\Core\Api\Presentation\ApiDispatcher;
33use App\Core\Api\Presentation\Web\OpenApiDocController;
34use App\Core\Audit\Application\Service\AuditIntegrityService;
35use App\Core\Audit\Application\Service\AuditRetentionService;
36use App\Core\Audit\Application\Service\SecurityAuditLogger;
37use App\Core\Audit\Presentation\Api\AuditApiController;
38use App\Core\Engine\Domain\Repository\AuditRepositoryInterface;
39use App\Core\Bootstrap\Configurator\BootstrapApiDispatcherFactory;
40use App\Core\Bootstrap\Configurator\BootstrapEngineContext;
41use App\Core\Bootstrap\Configurator\BootstrapEngineFactory;
42use App\Core\Bootstrap\Configurator\BootstrapRouteDispatcher;
43use App\Core\Bootstrap\Configurator\BootstrapRouteDispatcherFactory;
44use App\Core\Bootstrap\Provider\CommerceBootstrapProvider;
45use App\Core\Bootstrap\Provider\EngineComponentsBootstrapProvider;
46use App\Core\Bootstrap\Provider\PlatformFeaturesBootstrapProvider;
47use App\Core\Bootstrap\Provider\PlatformServicesBootstrapProvider;
48use App\Core\Bootstrap\Provider\UserAndAuthBootstrapProvider;
49use App\Core\Bootstrap\Configurator\ContainerConfigurator;
50use App\Core\Bootstrap\Configurator\SecurityAndTemplateConfigurator;
51use App\Core\Bootstrap\Configurator\TranslationAndMenuConfigurator;
52use App\Core\Cron\Presentation\Api\CronApiController;
53use App\Core\Database\MultiDbRouter;
54use App\Core\Engine\Application\Security\PermissionContextFactory;
55use App\Core\Engine\Application\Service\UniversalCrudService;
56use App\Modules\Currencies\Presentation\Api\CurrenciesApiController;
57use App\Modules\Currencies\Presentation\Web\CurrenciesWebController;
58use App\Modules\Dashboard\Application\Service\DashboardCalendarActionService;
59use App\Modules\Dashboard\Application\Service\DashboardWidgetsService;
60use App\Modules\Dashboard\Presentation\Api\DashboardApiController;
61use App\Modules\Dashboard\Presentation\Htmx\DashboardHtmxController;
62use App\Modules\Dashboard\Presentation\Web\DashboardWebController;
63use App\Modules\Discount\Application\Service\DiscountApplicationService;
64use App\Modules\Discount\Infrastructure\Repository\SqlDiscountRepository;
65use App\Modules\Discount\Presentation\Api\DiscountRatesApiController;
66use App\Modules\Discount\Presentation\Htmx\DiscountHtmxController;
67use App\Modules\Discount\Presentation\Web\DiscountSettingsWebController;
68use App\Core\Engine\Application\Service\DataMappingApplicationService;
69use App\Core\Engine\Application\Service\InventoryApplicationService;
70use App\Core\Engine\Application\Service\InventorySchemaEngine;
71use App\Core\Engine\Application\Service\UniversalCrudServiceInterface;
72use App\Core\Engine\Infrastructure\Repository\SqlDataMappingRepository;
73use App\Core\Engine\Infrastructure\Repository\SqlInventoryFieldRepository;
74use App\Core\Engine\Infrastructure\Repository\SqlInventoryRecordRepository;
75use App\Core\Engine\Presentation\Api\InventoryFieldsApiController;
76use App\Modules\DataMapping\Presentation\Htmx\DataMappingHtmxController;
77use App\Modules\DataMapping\Presentation\Web\DataMappingWebController;
78use App\Modules\Inventory\Presentation\Htmx\InventoryItemsHtmxController;
79use App\Modules\Inventory\Presentation\Htmx\InventorySettingsHtmxController;
80use App\Modules\Inventory\Presentation\Web\InventorySettingsWebController;
81use App\Modules\Quotes\Presentation\Web\QuoteConversionWebController;
82use App\Modules\Tax\Application\Service\TaxApplicationService;
83use App\Modules\Tax\Domain\Service\TaxDeterminationMatrix;
84use App\Modules\Tax\Domain\Service\TaxEngine;
85use App\Modules\Tax\Infrastructure\Repository\SqlTaxGroupRepository;
86use App\Modules\Tax\Infrastructure\Repository\SqlTaxRateRepository;
87use App\Modules\Tax\Infrastructure\Repository\SqlTaxRuleRepository;
88use App\Modules\Tax\Presentation\Api\TaxCalculateApiController;
89use App\Modules\Tax\Presentation\Api\TaxGroupsApiController;
90use App\Modules\Tax\Presentation\Api\TaxRatesApiController;
91use App\Modules\Tax\Presentation\Api\TaxRulesApiController;
92use App\Modules\Tax\Presentation\Htmx\TaxHtmxController;
93use App\Modules\Tax\Presentation\Web\TaxSettingsWebController;
94use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface;
95use App\Core\Engine\Infrastructure\Cache\PersistentMetadataCache;
96use App\Core\Engine\Infrastructure\Cache\RequestScopedMetadataCache;
97use App\Core\Engine\Infrastructure\Repository\SqlAuditRepository;
98use App\Core\Engine\Infrastructure\Repository\SqlMetadataRepository;
99use App\Core\Engine\Infrastructure\Settings\EngineSettings;
100use App\Core\Engine\Presentation\Api\CentralEngineApiController;
101use App\Core\Engine\Presentation\Web\CentralEngineWebController;
102use App\Core\Error\CentralErrorHandler;
103use App\Core\Event\SystemEventDispatcher;
104use App\Modules\Automation\Application\Listener\WorkflowRecordLifecycleListener;
105use App\Modules\Automation\Application\Service\WorkflowDagEngine;
106use App\Modules\Automation\Infrastructure\Repository\SqlWorkflowRepository;
107use App\Core\Instance\Application\Service\InstanceContextManager;
108use App\Core\Instance\Application\Service\InstanceContextManagerInterface;
109use App\Core\Instance\Application\Service\RemoteInstanceEngineGateway;
110use App\Core\Instance\Infrastructure\Client\RemoteInstanceApiClient;
111use App\Core\Instance\Infrastructure\Repository\SqlClientInstanceRepository;
112use App\Core\Instance\Presentation\Api\InstanceContextApiController;
113use App\Core\Instance\Presentation\Twig\TwigInstanceContextExtension;
114use App\Core\Layout\Presentation\Api\LayoutApiController;
115use App\Core\Search\Application\Service\GlobalSearchService;
116use App\Core\Search\Application\Service\SearchIndexOptimizer;
117use App\Core\Search\Application\Service\SearchQueryParser;
118use App\Core\Search\Infrastructure\Repository\SqlSearchRepository;
119use App\Core\Search\Presentation\Api\GlobalSearchApiController;
120use App\Core\Search\Presentation\Api\SearchSettingsApiController;
121use App\Core\Preference\Application\Command\ResetUserPreferencesHandler;
122use App\Core\Preference\Application\Command\SaveUserPreferenceHandler;
123use App\Core\Preference\Domain\Service\ConfigurationCascadeResolver;
124use App\Core\Preference\Infrastructure\Repository\SqlUserPreferenceRepository;
125use App\Core\Preference\Presentation\Api\UserPreferencesApiController;
126use App\Core\Preference\Presentation\Twig\UserPreferencesTwigExtension;
127use App\Core\Profile\Application\Service\ProfileManager;
128use App\Core\Profile\Domain\Model\Profile;
129use App\Core\Security\Encryption\AesGcmEncryptionService;
130use App\Core\Security\Middleware\RateLimiterMiddleware;
131use App\Core\Security\Middleware\SecurityHeadersMiddleware;
132use App\Core\Settings\Presentation\Api\SettingsApiController;
133use App\Core\Settings\SqlSettingsRepository;
134use App\Core\Translation\Middleware\LocaleDetectionMiddleware;
135use App\Core\Translation\Presentation\Api\TranslationApiController;
136use App\Modules\About\Application\DataSource\AboutLicensesDataProvider;
137use App\Modules\About\Application\DataSource\AboutRequirementsDataProvider;
138use App\Modules\About\Application\Service\SystemDiagnosticsService;
139use App\Core\Engine\Infrastructure\Repository\SqlRecordCoOwnerRepository;
140use App\Modules\About\Infrastructure\Repository\SqlSystemRequirementsRepository;
141use App\Modules\About\Presentation\Api\AboutLicensesApiController;
142use App\Modules\About\Presentation\Api\AboutRequirementsApiController;
143use App\Modules\Comments\Application\Service\CommentAttachmentService;
144use App\Modules\Comments\Application\Service\CommentHierarchyRollupService;
145use App\Modules\Comments\Application\Service\CommentMentionParserService;
146use App\Modules\Comments\Application\Service\CommentMentionsQueryService;
147use App\Modules\Comments\Application\Service\CommentService;
148use App\Modules\Comments\Infrastructure\Repository\SqlCommentRepository;
149use App\Modules\Comments\Presentation\Api\CommentsApiController;
150use App\Modules\Comments\Presentation\Htmx\CommentsHtmxController;
151use App\Modules\Comments\Presentation\Htmx\MentionsHtmxController;
152use App\Modules\Comments\Presentation\Web\CommentAttachmentDownloadWebController;
153use App\Modules\Comments\Presentation\Web\MentionsWebController;
154use App\Modules\Dav\Infrastructure\Config\AppleMobileConfigGenerator;
155use App\Modules\Dav\Infrastructure\Repository\SqlDavDeviceRepository;
156use App\Modules\Dav\Presentation\Api\DavDeviceApiController;
157use App\Modules\Dav\Presentation\Web\DavSetupWebController;
158use App\Modules\Logs\Presentation\Api\JsErrorLogApiController;
159use App\Modules\Mail\Application\Service\MailHtmlSanitizer;
160use App\Modules\Mail\Application\Service\MailDateFormatService;
161use App\Modules\Mail\Application\Service\MailSenderService;
162use App\Modules\Mail\Application\Service\MailTemplateRenderer;
163use App\Modules\Mail\Application\Service\WebmailAuthService;
164use App\Modules\Mail\Application\Service\WebmailFolderService;
165use App\Modules\Mail\Application\Service\EmailContextAssociationService;
166use App\Modules\Mail\Application\Service\WebmailMessageService;
167use App\Modules\Mail\Application\Service\WebmailSenderService;
168use App\Modules\Mail\Application\Service\WebmailSyncService;
169use App\Modules\Mail\Infrastructure\Protocol\MailProtocolDriverRegistry;
170use App\Modules\Mail\Infrastructure\Repository\SqlMailRepository;
171use App\Modules\Mail\Presentation\Api\MailApiController;
172use App\Modules\Mail\Presentation\Api\WebmailApiController;
173use App\Modules\Mail\Presentation\Api\WebmailEventsApiController;
174use App\Modules\Mail\Presentation\Htmx\WebmailHtmxController;
175use App\Modules\Mail\Presentation\Web\WebmailWebController;
176use App\Modules\Map\Application\Service\IpGeolocationService;
177use App\Modules\Map\Application\Service\MapGeocodingService;
178use App\Modules\Map\Application\Service\MapRoutingService;
179use App\Core\Template\Application\Service\TemplateCompilerService;
180use App\Core\Template\Application\Service\TemplateVariableResolver;
181use App\Modules\Pdf\Application\Service\PdfGeneratorService;
182use App\Modules\Pdf\Infrastructure\Driver\MpdfRendererDriver;
183use App\Modules\Pdf\Infrastructure\Repository\SqlPdfTemplateRepository;
184use App\Modules\Pdf\Infrastructure\Repository\SqlPdfTemplateVersionRepository;
185use App\Modules\Pdf\Presentation\Api\PdfApiController;
186use App\Modules\Pdf\Presentation\Htmx\PdfHtmxController;
187use App\Modules\Pdf\Presentation\Htmx\PdfQueueHtmxController;
188use App\Modules\Pdf\Presentation\Htmx\PdfSignatureHtmxController;
189use App\Modules\Structure\Application\Service\StructureReassignmentService;
190use App\Modules\Structure\Application\Service\StructureTreeBuilder;
191use App\Modules\Structure\Infrastructure\Repository\SqlStructureMembershipRepository;
192use App\Modules\Structure\Infrastructure\Repository\SqlStructureRepository;
193use App\Modules\Structure\Presentation\Api\OwnershipApiController;
194use App\Modules\Structure\Presentation\Api\StructureApiController;
195use App\Modules\Pdf\Presentation\Web\DocumentVerificationWebController;
196use App\Modules\Pdf\Presentation\Web\PdfWebController;
197use App\Modules\Mail\Application\Service\MailTrackingService;
198use App\Modules\Mail\Presentation\Web\MailTemplateWebController;
199use App\Modules\Mail\Presentation\Web\MailTrackingWebController;
200use App\Modules\Map\Domain\Repository\MapPointRepositoryInterface;
201use App\Modules\Map\Infrastructure\Client\AmmonlyMapApiClient;
202use App\Modules\Map\Infrastructure\GeoIp\MaxMindMmdbReader;
203use App\Modules\Map\Infrastructure\Provider\AmmonlyLocalMapProvider;
204use App\Modules\Map\Infrastructure\Provider\MapTilerMapProvider;
205use App\Modules\Map\Infrastructure\Provider\SmartMapLoadBalancer;
206use App\Modules\Map\Presentation\Api\MapApiController;
207use App\Modules\Map\Presentation\Htmx\MapHtmxController;
208use App\Modules\Map\Presentation\Web\MapWebController;
209use App\Modules\Menu\Presentation\Api\MenuApiController;
210use App\Modules\User\Application\Service\UserImpersonationService;
211use App\Modules\User\Application\Service\UserProfileService;
212use App\Modules\User\Infrastructure\Repository\SqlUserImpersonationRepository;
213use App\Modules\User\Infrastructure\Repository\SqlUserRepository;
214use App\Modules\User\Infrastructure\Repository\UserIdentityRepository;
215use App\Modules\User\Presentation\Web\ImpersonationWebController;
216use App\Core\Time\NtpTimeSyncService;
217use App\Modules\Mail\Application\Service\SystemMailSenderService;
218use App\Modules\User\Application\Service\PasswordResetService;
219use App\Modules\User\Application\Service\UserMfaDeviceService;
220use App\Modules\User\Presentation\Api\AuthApiController;
221use App\Modules\User\Presentation\Api\LocaleApiController;
222use App\Modules\User\Presentation\Api\MfaApiController;
223use App\Modules\User\Presentation\Api\UserProfileApiController;
224use App\Modules\User\Presentation\Middleware\AuthenticatedOnlyMiddleware;
225use App\Modules\User\Presentation\Middleware\GuestOnlyMiddleware;
226use App\Modules\User\Presentation\Web\AuthController;
227use App\Modules\User\Presentation\Web\Identity;
228use App\Shared\Infrastructure\Config\InstallerConfigLoader;
229use Nyholm\Psr7\Factory\Psr17Factory;
230use PDO;
231use Psr\Container\ContainerInterface;
232use Psr\Http\Message\ResponseInterface;
233use Psr\Http\Message\ServerRequestInterface;
234use Psr\Http\Server\RequestHandlerInterface;
235use Yiisoft\Cache\ArrayCache;
236use Yiisoft\Cache\Cache;
237use Yiisoft\Cache\CacheInterface;
238use Yiisoft\Cache\File\FileCache;
239use Yiisoft\Session\SessionInterface;
240use Yiisoft\Session\SessionMiddleware;
241use Yiisoft\User\CurrentUser;
242
243/**
244 * Application Bootstrap.
245 *
246 * Bootstraps and coordinates the application lifecycle, dependency resolution,
247 * middleware execution pipeline, and HTTP response delivery with API-First design.
248 *
249 * @package App\Core\Bootstrap
250 */
251final class ApplicationBootstrap
252{
253    private ContainerInterface $container;
254    private Psr17Factory $psr17Factory;
255    private SessionInterface $session;
256    private CurrentUser $currentUser;
257    private SqlUserRepository $userRepository;
258    private CentralEngineApiController $centralEngineApiController;
259    private SecurityHeadersMiddleware $securityHeadersMiddleware;
260    private SessionMiddleware $sessionMiddleware;
261    private const string LOGS_SUBPATH = '/storage/logs';
262
263    private LocaleDetectionMiddleware $localeDetectionMiddleware;
264    private BootstrapRouteDispatcher $routeDispatcher;
265    private ?SqlUserRepository $clientUserRepository = null;
266
267    /**
268     * ApplicationBootstrap constructor.
269     *
270     * @param string   $basePath Base application filesystem directory.
271     * @param PDO|null $pdo      Optional database connection instance.
272     */
273    public function __construct(string $basePath, ?PDO $pdo = null)
274    {
275        [$env, $secContext, $engineContext] = $this->bootstrapCoreSubsystems($basePath, $pdo);
276        $this->bootstrapApplicationRuntime($basePath, $env, $secContext, $engineContext);
277    }
278
279    /**
280     * Bootstraps core database, security, and engine subsystems.
281     *
282     * @param string   $basePath Base application filesystem path.
283     * @param PDO|null $pdo      Optional database connection.
284     * @return array{0: array<string, mixed>, 1: array<string, mixed>, 2: array<string, mixed>}
285     */
286    private function bootstrapCoreSubsystems(string $basePath, ?PDO $pdo): array
287    {
288        [
289            $pdo,
290            $prefix,
291            $sec,
292            $cache,
293            $cacheHandler,
294            $metadataRepo,
295            $instanceContextManager,
296            $remoteEngineGateway,
297        ] = $this->initializeCoreEnvironment($basePath, $pdo);
298
299        [
300            $i18n,
301            $authController,
302            $authApiController,
303            $mfaApiController,
304            $localeApiController,
305            $accessRepo,
306            $profileRepo,
307            $auditRepo,
308            $eventDispatcher,
309        ] = $this->initializeSecurityAndTranslation($basePath, $pdo, $prefix, $sec, [
310            'metadataRepo'           => $metadataRepo,
311            'instanceContextManager' => $instanceContextManager,
312            'cache'                  => $cache,
313        ]);
314
315        $userPrefix = ($sec->activeProfile->name === 'client') ? 'c_' : $prefix;
316        [$clientUserRepo, $clientImpersonationRepo] = $this->initializeClientUserRepositories($pdo, $sec);
317
318        [$engineFactory, $licensesApi, $requirementsApi] = $this->initializeEngineSubsystem(
319            $pdo,
320            $prefix,
321            $basePath,
322            $sec,
323            [
324                'audit_repo'               => $auditRepo,
325                'metadata_repo'            => $metadataRepo,
326                'cache'                    => $cache,
327                'event_dispatcher'         => $eventDispatcher,
328                'instance_context_manager' => $instanceContextManager,
329                'remote_engine_gateway'    => $remoteEngineGateway,
330                'access_repo'              => $accessRepo,
331                'profile_repo'             => $profileRepo,
332                'client_pdo'               => $pdo,
333                'translator'               => $i18n->translator,
334            ]
335        );
336
337        $env = [
338            'pdo'                    => $pdo,
339            'prefix'                 => $prefix,
340            'userPrefix'             => $userPrefix,
341            'sec'                    => $sec,
342            'cache'                  => $cache,
343            'cacheHandler'           => $cacheHandler,
344            'metadataRepo'           => $metadataRepo,
345            'instanceContextManager' => $instanceContextManager,
346            'remoteGateway'          => $remoteEngineGateway,
347        ];
348
349        $secContext = [
350            'i18n'                => $i18n,
351            'authController'      => $authController,
352            'authApiController'   => $authApiController,
353            'mfaApiController'    => $mfaApiController,
354            'localeApiController' => $localeApiController,
355            'accessRepo'          => $accessRepo,
356            'profileRepo'         => $profileRepo,
357            'auditRepo'           => $auditRepo,
358            'eventDispatcher'     => $eventDispatcher,
359        ];
360
361        $engineContext = [
362            'engineFactory'           => $engineFactory,
363            'licensesApi'             => $licensesApi,
364            'requirementsApi'         => $requirementsApi,
365            'clientUserRepo'          => $clientUserRepo,
366            'clientImpersonationRepo' => $clientImpersonationRepo,
367        ];
368
369        return [$env, $secContext, $engineContext];
370    }
371
372    /**
373     * Bootstraps features, commerce, route dispatcher, and application container.
374     *
375     * @param string               $basePath      Base application filesystem path.
376     * @param array<string, mixed> $env           Core environment configuration.
377     * @param array<string, mixed> $secContext    Security and translation services.
378     * @param array<string, mixed> $engineContext Engine subsystem and repositories.
379     */
380    private function bootstrapApplicationRuntime(
381        string $basePath,
382        array $env,
383        array $secContext,
384        array $engineContext
385    ): void {
386        /** @var PDO $pdo */
387        $pdo = $env['pdo'];
388        /** @var string $prefix */
389        $prefix = $env['prefix'];
390        /** @var SecurityAndTemplateConfigurator $sec */
391        $sec = $env['sec'];
392        /** @var TranslationAndMenuConfigurator $i18n */
393        $i18n = $secContext['i18n'];
394        /** @var InstanceContextManager $instanceContextManager */
395        $instanceContextManager = $env['instanceContextManager'];
396
397        $featureSetup = $this->initializeFeatureControllers(
398            $pdo,
399            $prefix,
400            $sec,
401            $i18n,
402            $instanceContextManager,
403            [
404                'cache'        => $env['cache'],
405                'accessRepo'   => $secContext['accessRepo'],
406                'metadataRepo' => $env['metadataRepo'],
407                'profileRepo'  => $secContext['profileRepo'],
408                'basePath'     => $basePath,
409            ]
410        );
411
412        $dashboard = $this->initializeDashboardSubsystem(
413            $pdo,
414            $prefix,
415            (string) $env['userPrefix'],
416            $sec,
417            $instanceContextManager
418        );
419
420        $commerceProvider = new CommerceBootstrapProvider($this->psr17Factory);
421        $commerceSetup = $commerceProvider->registerAll(
422            $pdo,
423            $prefix,
424            $sec,
425            $env['metadataRepo'],
426            $this->centralEngineApiController->getCrudService(),
427            new PermissionContextFactory($this->session, $pdo),
428            [
429                'instanceContextManager' => $instanceContextManager,
430                'clientPdo'              => $pdo,
431                'remoteGateway'          => $env['remoteGateway'],
432            ]
433        );
434
435        $apis = $this->assembleApis(
436            [
437                'auth'         => $secContext['authApiController'],
438                'mfa'          => $secContext['mfaApiController'],
439                'locale'       => $secContext['localeApiController'],
440                'licenses'     => $engineContext['licensesApi'],
441                'requirements' => $engineContext['requirementsApi'],
442                'dashboard'    => $dashboard['api'],
443            ],
444            $featureSetup['apis'],
445            $commerceSetup['apis']
446        );
447
448        $apiDispatcher = $this->createApiDispatcher(
449            $apis,
450            $i18n,
451            $engineContext['engineFactory'],
452            $sec,
453            $env['cacheHandler'],
454            $prefix,
455            $pdo
456        );
457
458        [$impersonationService, $impersonationWebController] = $this->buildImpersonationSubsystem(
459            $pdo,
460            $prefix,
461            $sec,
462            $engineContext['clientUserRepo'],
463            $engineContext['clientImpersonationRepo'],
464            $instanceContextManager
465        );
466
467        $dispatcherOptions = $this->buildRouteDispatcherOptions(
468            $env['metadataRepo'],
469            $instanceContextManager,
470            $featureSetup,
471            $commerceSetup,
472            $sec,
473            $impersonationWebController,
474            $dashboard
475        );
476
477        $this->routeDispatcher = $this->createRouteDispatcher(
478            $sec,
479            $basePath,
480            $secContext['authController'],
481            $apiDispatcher,
482            $engineContext['engineFactory'],
483            $dispatcherOptions
484        );
485
486        $containerServices = $this->buildContainerServices(
487            [
488                'sec'                   => $sec,
489                'prefix'                => $prefix,
490                'apis'                  => $apis,
491                'profile_repo'          => $secContext['profileRepo'],
492                'impersonation_service' => $impersonationService,
493                'impersonation_web'     => $impersonationWebController,
494            ],
495            $dashboard,
496            $commerceSetup
497        );
498
499        $this->container = $this->buildApplicationContainer(
500            $pdo,
501            $env['cache'],
502            $env['metadataRepo'],
503            $engineContext['engineFactory'],
504            $secContext['authController'],
505            $i18n,
506            $containerServices
507        );
508    }
509
510    public function getContainer(): ContainerInterface
511    {
512        return $this->container;
513    }
514
515    public function handle(ServerRequestInterface $request): ResponseInterface
516    {
517        $fallbackHandler = new class($this) implements RequestHandlerInterface {
518            public function __construct(private ApplicationBootstrap $app)
519            {
520            }
521
522            public function handle(ServerRequestInterface $request): ResponseInterface
523            {
524                return $this->app->dispatch($request);
525            }
526        };
527
528        $middlewareFactory = new \Yiisoft\Middleware\Dispatcher\MiddlewareFactory($this->container);
529        $dispatcher = (new \Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher($middlewareFactory))
530            ->withMiddlewares([
531                fn(ServerRequestInterface $r, RequestHandlerInterface $h): ResponseInterface => (
532                    $this->securityHeadersMiddleware->process($r, $h)
533                ),
534                fn(ServerRequestInterface $r, RequestHandlerInterface $h): ResponseInterface => (
535                    $this->sessionMiddleware->process($r, $h)
536                ),
537                fn(ServerRequestInterface $r, RequestHandlerInterface $h): ResponseInterface => (
538                    $this->localeDetectionMiddleware->process($r, $h)
539                ),
540            ]);
541
542        return $dispatcher->dispatch($request, $fallbackHandler);
543    }
544
545    public function dispatch(ServerRequestInterface $request): ResponseInterface
546    {
547        $this->handleSessionSecurityValidation($request);
548
549        return $this->routeDispatcher->dispatch($request);
550    }
551
552    private function handleSessionSecurityValidation(ServerRequestInterface $request): void
553    {
554        if ($this->session->has('user_id')) {
555            $this->validateActiveSession();
556        } elseif ($request->getAttribute('user_id') !== null) {
557            $userId = (int) $request->getAttribute('user_id');
558            $user = $this->userRepository->findById($userId);
559            if ($user !== null && $user->isActive()) {
560                $this->currentUser->login(new Identity($userId, $user->getLocale()));
561            }
562        // @codeCoverageIgnoreStart
563        } elseif (!empty($_SESSION['user_id'])) {
564            $this->validateLegacySession();
565        }
566        // @codeCoverageIgnoreEnd
567    }
568
569    private function validateActiveSession(): void
570    {
571        $userId = (int) $this->session->get('user_id');
572        $now = time();
573        $lastActivity = (int) ($this->session->get('last_activity', $now));
574        $idleTimeout = 900;
575
576        if ($now - $lastActivity > $idleTimeout) {
577            $this->clearUserSession();
578            return;
579        }
580
581        $this->session->set('last_activity', $now);
582        $this->checkImpersonationLifetime($now);
583
584        $isClientUser = $this->session->has('impersonated_client_user');
585        $userRepo = ($isClientUser && $this->clientUserRepository !== null)
586            ? $this->clientUserRepository
587            : $this->userRepository;
588
589        $user = $userRepo->findById($userId);
590        if ($user === null || !$user->isActive()) {
591            $this->clearUserSession();
592            return;
593        }
594
595        $this->currentUser->login(new Identity($userId, $user->getLocale()));
596        $userSessionData = [
597            'id'           => $user->getId(),
598            'username'     => $user->getUsername(),
599            'email'        => $user->getEmail(),
600            'is_superuser' => $user->isSuperuser(),
601            'is_active'    => true,
602        ];
603        if ($isClientUser) {
604            $userSessionData['is_client_user'] = true;
605        }
606        $this->session->set('user', $userSessionData);
607    }
608
609    private function checkImpersonationLifetime(int $now): void
610    {
611        if (!$this->session->has('impersonator_user')) {
612            return;
613        }
614
615        $startedAt = (int) $this->session->get('impersonation_started_at', $now);
616        if ($now - $startedAt > 3600) {
617            $impersonator = $this->session->get('impersonator_user');
618            if (is_array($impersonator) && !empty($impersonator['id'])) {
619                $this->session->set('user', $impersonator);
620                $this->session->set('user_id', (int) $impersonator['id']);
621            }
622            $this->session->remove('impersonator_user');
623            $this->session->remove('impersonator_user_id');
624            $this->session->remove('impersonated_client_user');
625            $this->session->remove('impersonation_started_at');
626        }
627    }
628
629    private function clearUserSession(): void
630    {
631        $this->session->remove('user_id');
632        $this->session->remove('user');
633        $this->session->remove('last_activity');
634        $this->session->remove('impersonator_user');
635        $this->session->remove('impersonator_user_id');
636        $this->session->remove('impersonated_client_user');
637        $this->session->remove('impersonation_started_at');
638        $this->currentUser->logout();
639    }
640
641    // @codeCoverageIgnoreStart
642    private function validateLegacySession(): void
643    {
644        $userId = (int) $_SESSION['user_id'];
645        $user = $this->userRepository->findById($userId);
646        if ($user === null || !$user->isActive()) {
647            unset($_SESSION['user_id'], $_SESSION['user']);
648            $this->currentUser->logout();
649            return;
650        }
651        $this->currentUser->login(new Identity($userId, $user->getLocale()));
652        $_SESSION['user'] = [
653            'id'           => $user->getId(),
654            'username'     => $user->getUsername(),
655            'email'        => $user->getEmail(),
656            'is_superuser' => $user->isSuperuser(),
657            'is_active'    => true,
658        ];
659    }
660    // @codeCoverageIgnoreEnd
661
662    public function getCentralEngineApiController(): CentralEngineApiController
663    {
664        return $this->centralEngineApiController;
665    }
666
667    public function getPsr17Factory(): Psr17Factory
668    {
669        return $this->psr17Factory;
670    }
671
672    public function getSession(): SessionInterface
673    {
674        return $this->session;
675    }
676
677    /**
678     * Initializes active database connection and resolves table prefix.
679     *
680     * @param PDO|null $pdo Injected PDO connection or null.
681     * @return array{0: PDO, 1: string} Database connection and table prefix.
682     */
683    private function initializeDatabase(?PDO $pdo): array
684    {
685        $dbConfig = InstallerConfigLoader::load();
686        $dbParams = $dbConfig['db'] ?? [];
687        $prefix = (string)($dbParams['table_prefix'] ?? $dbParams['prefix'] ?? 'a_');
688
689        // @codeCoverageIgnoreStart
690        if ($pdo === null) {
691            $router = new MultiDbRouter(['default' => $dbParams]);
692            $pdo = $router->getConnection('default');
693        }
694        // @codeCoverageIgnoreEnd
695
696        return [$pdo, $prefix];
697    }
698
699    /**
700     * Initializes PSR-16 cache handler and cache wrapper.
701     *
702     * @param string $basePath   Application base filesystem path.
703     * @param bool   $isUnitTest Whether running in a unit test environment.
704     * @param string $appEnv     Application environment name.
705     * @return array{0: Cache, 1: mixed} Cache instance and underlying handler.
706     */
707    private function initializeCache(string $basePath, bool $isUnitTest, string $appEnv): array
708    {
709        $cacheDir = $basePath . '/storage/runtime/cache';
710        if (!is_dir($cacheDir)) {
711            @mkdir($cacheDir, 0777, true);
712        }
713
714        // @codeCoverageIgnoreStart
715        $cacheHandler = ($isUnitTest || $appEnv === 'test')
716            ? new ArrayCache()
717            : (new FileCache($cacheDir))->withDirectoryLevel(0);
718        // @codeCoverageIgnoreEnd
719
720        return [new Cache($cacheHandler), $cacheHandler];
721    }
722
723    /**
724     * Instantiates and configures central route dispatcher.
725     *
726     * @param array<string, mixed> $context
727     */
728    private function createRouteDispatcher(
729        SecurityAndTemplateConfigurator $sec,
730        string $basePath,
731        AuthController $authController,
732        ApiDispatcher $apiDispatcher,
733        BootstrapEngineFactory $engineFactory,
734        array $context
735    ): BootstrapRouteDispatcher {
736        $factory = new BootstrapRouteDispatcherFactory(
737            $this->psr17Factory,
738            $this->currentUser,
739            $this->session,
740            $this->userRepository
741        );
742
743        return $factory->create(
744            $sec,
745            $basePath,
746            $authController,
747            $apiDispatcher,
748            $engineFactory,
749            $context
750        );
751    }
752
753    /**
754     * Initializes core environment, database, cache, security, metadata and instance managers.
755     *
756     * @return array{
757     *     0: PDO,
758     *     1: string,
759     *     2: SecurityAndTemplateConfigurator,
760     *     3: CacheInterface,
761     *     4: mixed,
762     *     5: MetadataRepositoryInterface,
763     *     6: InstanceContextManager,
764     *     7: RemoteInstanceEngineGateway
765     * }
766     */
767    private function initializeCoreEnvironment(string $basePath, ?PDO $pdo): array
768    {
769        $this->loadDotEnv($basePath);
770        $this->psr17Factory = new Psr17Factory();
771        CentralErrorHandler::register($basePath . self::LOGS_SUBPATH);
772
773        [$pdo, $prefix] = $this->initializeDatabase($pdo);
774        $appEnv = (string)($_ENV['APP_ENV'] ?? 'dev');
775        $appDebug = (bool)filter_var($_ENV['APP_DEBUG'] ?? true, FILTER_VALIDATE_BOOLEAN);
776        CentralErrorHandler::setDebug($appDebug);
777
778        [$cache, $cacheHandler] = $this->initializeCache($basePath, true, $appEnv);
779
780        $sec = new SecurityAndTemplateConfigurator(
781            $basePath,
782            $pdo,
783            $prefix,
784            $appEnv,
785            $appDebug,
786            $this->psr17Factory
787        );
788        $this->session = $sec->session;
789        $this->sessionMiddleware = $sec->sessionMiddleware;
790        $this->securityHeadersMiddleware = $sec->securityHeadersMiddleware;
791
792        $userPrefix = ($sec->activeProfile->name === 'client') ? 'c_' : $prefix;
793        $sqlMetadataRepo = new SqlMetadataRepository($pdo, null, null, null, $userPrefix);
794        $persistentMetadataCache = new PersistentMetadataCache($sqlMetadataRepo, $cache, 3600);
795        $metadataRepo = new RequestScopedMetadataCache($persistentMetadataCache);
796
797        $clientInstanceRepo = new SqlClientInstanceRepository($pdo, $prefix);
798        $instanceContextManager = new InstanceContextManager($clientInstanceRepo, $this->session);
799        $remoteApiClient = new RemoteInstanceApiClient();
800        $remoteEngineGateway = new RemoteInstanceEngineGateway($remoteApiClient);
801
802        return [
803            $pdo,
804            $prefix,
805            $sec,
806            $cache,
807            $cacheHandler,
808            $metadataRepo,
809            $instanceContextManager,
810            $remoteEngineGateway,
811        ];
812    }
813
814    /**
815     * Initializes translation, authentication, access repositories, and locale middleware.
816     *
817     * @param array{
818     *     metadataRepo: MetadataRepositoryInterface,
819     *     instanceContextManager: InstanceContextManager,
820     *     cache: CacheInterface
821     * } $context
822     * @return array{
823     *     0: TranslationAndMenuConfigurator,
824     *     1: AuthController,
825     *     2: AuthApiController,
826     *     3: MfaApiController,
827     *     4: LocaleApiController,
828     *     5: CachedAccessRepository,
829     *     6: CachedProfilePermissionRepository,
830     *     7: mixed,
831     *     8: mixed
832     * }
833     */
834    private function initializeSecurityAndTranslation(
835        string $basePath,
836        PDO $pdo,
837        string $prefix,
838        SecurityAndTemplateConfigurator $sec,
839        array $context
840    ): array {
841        $i18n = new TranslationAndMenuConfigurator(
842            $basePath,
843            $pdo,
844            $prefix,
845            $sec->twig,
846            $this->psr17Factory,
847            [
848                'metadataRepo'           => $context['metadataRepo'],
849                'instanceContextManager' => $context['instanceContextManager'],
850                'appProfile'             => $sec->activeProfile->name,
851            ]
852        );
853
854        $userAuthProvider = new UserAndAuthBootstrapProvider($this->psr17Factory, $this->session);
855        [
856            $authApiController,
857            $mfaApiController,
858            $authController,
859            $eventDispatcher,
860            $auditRepo,
861            $this->currentUser,
862            $this->userRepository
863        ] = $userAuthProvider->initialize($pdo, $prefix, $basePath, $sec);
864
865        $this->localeDetectionMiddleware = new LocaleDetectionMiddleware(
866            $i18n->localeContext,
867            $i18n->translator,
868            $this->session
869        );
870        $localeApiController = new LocaleApiController(
871            $i18n->localeContext,
872            $i18n->translator,
873            $this->session,
874            $this->currentUser,
875            $this->userRepository,
876            $this->psr17Factory
877        );
878        $accessRepo = new CachedAccessRepository(new SqlAccessRepository($pdo), $context['cache']);
879        $profileRepo = new CachedProfilePermissionRepository(new SqlProfilePermissionRepository($pdo));
880
881        return [
882            $i18n,
883            $authController,
884            $authApiController,
885            $mfaApiController,
886            $localeApiController,
887            $accessRepo,
888            $profileRepo,
889            $auditRepo,
890            $eventDispatcher,
891        ];
892    }
893
894    /**
895     * Initializes dashboard widgets, action service, and controllers.
896     *
897     * @return array{
898     *     0: DashboardWidgetsService,
899     *     1: DashboardCalendarActionService,
900     *     2: DashboardApiController,
901     *     3: DashboardHtmxController,
902     *     4: DashboardWebController
903     * }
904     */
905    private function initializeDashboardSubsystem(
906        PDO $pdo,
907        string $prefix,
908        string $userPrefix,
909        SecurityAndTemplateConfigurator $sec,
910        InstanceContextManager $instanceContextManager
911    ): array {
912        $userPreferenceRepo = new SqlUserPreferenceRepository($pdo, $userPrefix);
913        $widgetsService = new DashboardWidgetsService(
914            $pdo,
915            $userPreferenceRepo,
916            $instanceContextManager,
917            $pdo,
918            $prefix,
919            $sec->activeProfile->name
920        );
921        $actionService = new DashboardCalendarActionService(
922            $pdo,
923            $instanceContextManager,
924            $pdo,
925            $prefix
926        );
927        $apiController = new DashboardApiController($widgetsService, $actionService, $this->psr17Factory);
928        $htmxController = new DashboardHtmxController(
929            $widgetsService,
930            $actionService,
931            $sec->twig,
932            $this->psr17Factory
933        );
934        $webController = new DashboardWebController(
935            $widgetsService,
936            $sec->twig,
937            $this->psr17Factory,
938            $instanceContextManager
939        );
940
941        return [
942            'widgets_service' => $widgetsService,
943            'action_service'  => $actionService,
944            'api'             => $apiController,
945            'htmx'            => $htmxController,
946            'web'             => $webController,
947        ];
948    }
949
950    /**
951     * Initializes client user and impersonation repositories.
952     *
953     * @return array{0: SqlUserRepository, 1: SqlUserImpersonationRepository}
954     */
955    private function initializeClientUserRepositories(PDO $pdo, SecurityAndTemplateConfigurator $sec): array
956    {
957        $this->clientUserRepository = ($sec->activeProfile->name === 'client')
958            ? $this->userRepository
959            : new SqlUserRepository($pdo, 'c_');
960
961        return [$this->clientUserRepository, new SqlUserImpersonationRepository($pdo, 'c_')];
962    }
963
964    /**
965     * Initializes mail, preferences, user profile, DAV, structure, search, and comments.
966     *
967     * @param array<string, mixed> $options
968     * @return array<string, mixed>
969     */
970    private function initializeFeatureControllers(
971        ?PDO $pdo,
972        string $prefix,
973        SecurityAndTemplateConfigurator $sec,
974        TranslationAndMenuConfigurator $i18n,
975        InstanceContextManager $instanceContextManager,
976        array $options = []
977    ): array {
978        $platformFeaturesProvider = new PlatformFeaturesBootstrapProvider(
979            $this->psr17Factory,
980            $this->currentUser,
981            $this->session,
982            $this->userRepository
983        );
984
985        $platformServicesProvider = new PlatformServicesBootstrapProvider(
986            $this->psr17Factory,
987            $this->currentUser,
988            $this->session,
989            $this->userRepository,
990            $platformFeaturesProvider
991        );
992
993        return $platformServicesProvider->registerAll(
994            $pdo,
995            $prefix,
996            $sec,
997            $i18n,
998            $instanceContextManager,
999            $this->centralEngineApiController->getCrudService(),
1000            $options
1001        );
1002    }
1003
1004    /**
1005     * Instantiates configured API Dispatcher middleware pipeline.
1006     *
1007     * @param array<string, mixed> $apis Extra API controllers.
1008     */
1009    private function createApiDispatcher(
1010        array $apis,
1011        TranslationAndMenuConfigurator $i18n,
1012        BootstrapEngineFactory $engineFactory,
1013        SecurityAndTemplateConfigurator $sec,
1014        mixed $cacheHandler,
1015        string $prefix,
1016        PDO $pdo
1017    ): ApiDispatcher {
1018        $factory = new BootstrapApiDispatcherFactory(
1019            $this->psr17Factory,
1020            $this->currentUser,
1021            $this->session,
1022            $this->centralEngineApiController
1023        );
1024
1025        return $factory->create(
1026            $apis,
1027            $i18n,
1028            $engineFactory,
1029            $sec,
1030            $cacheHandler,
1031            $prefix,
1032            $pdo
1033        );
1034    }
1035
1036    /**
1037     * Creates and registers the central engine factory and API controller.
1038     *
1039     * @param array<string, mixed> $context
1040     * @return array{0: BootstrapEngineFactory, 1: AboutLicensesApiController, 2: AboutRequirementsApiController}
1041     */
1042    private function initializeEngineSubsystem(
1043        PDO $pdo,
1044        string $prefix,
1045        string $basePath,
1046        SecurityAndTemplateConfigurator $sec,
1047        array $context
1048    ): array {
1049        $engineProvider = new EngineComponentsBootstrapProvider($this->psr17Factory);
1050        $result = $engineProvider->initialize($pdo, $prefix, $basePath, $sec, $context);
1051        $this->centralEngineApiController = $result[0]->centralEngineApiController;
1052
1053        return $result;
1054    }
1055
1056    /**
1057     * Merges core, feature, and commerce API controllers into a unified map.
1058     *
1059     * @param array<string, mixed> $coreApis
1060     * @param array<string, mixed> $featureApis
1061     * @param array<string, mixed> $commerceApis
1062     * @return array<string, mixed>
1063     */
1064    private function assembleApis(
1065        array $coreApis,
1066        array $featureApis,
1067        array $commerceApis
1068    ): array {
1069        return array_merge($coreApis, $featureApis, $commerceApis);
1070    }
1071
1072    /**
1073     * Instantiates user impersonation domain service and web presentation controller.
1074     *
1075     * @return array{0: UserImpersonationService, 1: ImpersonationWebController}
1076     */
1077    private function buildImpersonationSubsystem(
1078        PDO $pdo,
1079        string $prefix,
1080        SecurityAndTemplateConfigurator $sec,
1081        ?SqlUserRepository $clientUserRepo,
1082        SqlUserImpersonationRepository $clientImpersonationRepo,
1083        InstanceContextManager $instanceContextManager
1084    ): array {
1085        $adminImpersonationRepo = new SqlUserImpersonationRepository($pdo, $prefix);
1086        $impersonationService = new UserImpersonationService(
1087            $adminImpersonationRepo,
1088            $this->userRepository,
1089            $clientImpersonationRepo,
1090            $clientUserRepo,
1091            $instanceContextManager
1092        );
1093        $sec->twigUserExtension->setImpersonationService($impersonationService);
1094
1095        $impersonationWebController = new ImpersonationWebController(
1096            $impersonationService,
1097            $this->userRepository,
1098            $this->session,
1099            $this->currentUser,
1100            $this->psr17Factory,
1101            $sec->twig,
1102            $clientUserRepo,
1103            $instanceContextManager,
1104            $sec->activeProfile->name,
1105            $sec->securityAuditLogger
1106        );
1107
1108        return [$impersonationService, $impersonationWebController];
1109    }
1110
1111    /**
1112     * Assembles route dispatcher options array.
1113     *
1114     * @param array<string, mixed> $featureSetup
1115     * @param array<string, mixed> $commerceSetup
1116     * @param array<string, mixed> $dashboard
1117     * @return array<string, mixed>
1118     */
1119    private function buildRouteDispatcherOptions(
1120        MetadataRepositoryInterface $metadataRepo,
1121        InstanceContextManager $instanceContextManager,
1122        array $featureSetup,
1123        array $commerceSetup,
1124        SecurityAndTemplateConfigurator $sec,
1125        ImpersonationWebController $impersonationWebController,
1126        array $dashboard
1127    ): array {
1128        return [
1129            'metadata'                        => $metadataRepo,
1130            'instance_context'                => $instanceContextManager,
1131            'dav_setup'                       => $featureSetup['dav_setup'] ?? null,
1132            'webmail_web'                     => $featureSetup['webmail_web'] ?? null,
1133            'webmail_htmx'                    => $featureSetup['webmail_htmx'] ?? null,
1134            'map_web'                         => $featureSetup['map_web'] ?? null,
1135            'map_htmx'                        => $featureSetup['map_htmx'] ?? null,
1136            'pdf_htmx'                        => $featureSetup['pdf_htmx'] ?? null,
1137            'pdf_web'                         => $featureSetup['pdf_web'] ?? null,
1138            'doc_verification'                => $featureSetup['doc_verification'] ?? null,
1139            'pdf_signature_htmx'              => $featureSetup['pdf_signature_htmx'] ?? null,
1140            'pdf_queue_htmx'                  => $featureSetup['pdf_queue_htmx'] ?? null,
1141            'mail_tracking_web'               => $featureSetup['mail_tracking_web'] ?? null,
1142            'mail_template_web'               => $featureSetup['mail_template_web'] ?? null,
1143            'calendar_rsvp_web'               => $featureSetup['calendar_rsvp_web'] ?? null,
1144            'access_web'                      => $featureSetup['access_web'] ?? null,
1145            'comments_htmx'                   => $featureSetup['comments_htmx'] ?? null,
1146            'mentions_web'                    => $featureSetup['mentions_web'] ?? null,
1147            'mentions_htmx'                   => $featureSetup['mentions_htmx'] ?? null,
1148            'comment_attachment_download_web' => $featureSetup['comment_attachment_download_web'] ?? null,
1149            'impersonation_web'               => $impersonationWebController,
1150            'dashboard_web'                   => $dashboard['web'],
1151            'dashboard_api'                   => $dashboard['api'],
1152            'dashboard_htmx'                  => $dashboard['htmx'],
1153            'tax_web'                         => $commerceSetup['tax_web'],
1154            'tax_htmx'                        => $commerceSetup['tax_htmx'],
1155            'discount_web'                    => $commerceSetup['discount_web'],
1156            'discount_htmx'                   => $commerceSetup['discount_htmx'],
1157            'inventory_web'                   => $commerceSetup['inventory_web'],
1158            'inventory_htmx'                  => $commerceSetup['inventory_htmx'],
1159            'inventory_items_htmx'            => $commerceSetup['inventory_items_htmx'],
1160            'mapping_web'                     => $commerceSetup['mapping_web'],
1161            'mapping_htmx'                    => $commerceSetup['mapping_htmx'],
1162            'quote_conversion_web'            => $commerceSetup['quote_conversion_web'],
1163            'currencies_web'                  => $commerceSetup['currencies_web'],
1164            'app_profile'                     => $sec->activeProfile->name,
1165        ];
1166    }
1167
1168    /**
1169     * Builds container service definition array.
1170     *
1171     * @param array<string, mixed> $baseServices
1172     * @param array<string, mixed> $dashboard
1173     * @param array<string, mixed> $commerce
1174     * @return array<string, mixed>
1175     */
1176    private function buildContainerServices(
1177        array $baseServices,
1178        array $dashboard,
1179        array $commerce
1180    ): array {
1181        return array_merge($baseServices, [
1182            'dashboard_widgets_service' => $dashboard['widgets_service'],
1183            'dashboard_action_service'  => $dashboard['action_service'],
1184            'dashboard_htmx'            => $dashboard['htmx'],
1185            'dashboard_web'             => $dashboard['web'],
1186            'tax_web'                   => $commerce['tax_web'],
1187            'tax_htmx'                  => $commerce['tax_htmx'],
1188            'discount_web'              => $commerce['discount_web'],
1189            'discount_htmx'             => $commerce['discount_htmx'],
1190            'inventory_web'             => $commerce['inventory_web'],
1191            'inventory_htmx'            => $commerce['inventory_htmx'],
1192            'inventory_items_htmx'      => $commerce['inventory_items_htmx'],
1193            'mapping_web'               => $commerce['mapping_web'],
1194            'mapping_htmx'              => $commerce['mapping_htmx'],
1195            'quote_conversion_web'      => $commerce['quote_conversion_web'],
1196            'currencies_web'            => $commerce['currencies_web'],
1197            'currencies_api'            => $commerce['currencies_api'],
1198        ]);
1199    }
1200
1201    /**
1202     * Configures and creates the DI container.
1203     *
1204     * @param array<string, mixed> $services
1205     */
1206    private function buildApplicationContainer(
1207        PDO $pdo,
1208        CacheInterface $cache,
1209        MetadataRepositoryInterface $metadataRepo,
1210        BootstrapEngineFactory $engineFactory,
1211        AuthController $authController,
1212        TranslationAndMenuConfigurator $i18n,
1213        array $services
1214    ): ContainerInterface {
1215        $coreDefinitions = $this->createCoreDefinitions(
1216            $pdo,
1217            $cache,
1218            $metadataRepo,
1219            $engineFactory,
1220            $authController,
1221            $i18n,
1222            $services
1223        );
1224
1225        /** @var SecurityAndTemplateConfigurator $sec */
1226        $sec = $services['sec'];
1227        $profileDefinitions = $sec->profileManager->loadDiDefinitions($sec->activeProfile);
1228        $providers = [
1229            \App\Core\Bootstrap\Provider\CoreServiceProvider::class,
1230            \App\Core\Bootstrap\Provider\SecurityServiceProvider::class,
1231            \App\Core\Bootstrap\Provider\EngineServiceProvider::class,
1232            \App\Core\Bootstrap\Provider\I18nServiceProvider::class,
1233            \App\Modules\User\Config\UserModuleServiceProvider::class,
1234            \App\Modules\Mail\Config\MailModuleServiceProvider::class,
1235            \App\Modules\About\Config\AboutModuleServiceProvider::class,
1236            \App\Modules\Menu\Config\MenuModuleServiceProvider::class,
1237            \App\Modules\Integrations\Config\IntegrationsServiceProvider::class,
1238            \App\Modules\Map\Config\MapModuleServiceProvider::class,
1239        ];
1240
1241        return ContainerConfigurator::create(
1242            array_merge($coreDefinitions, $profileDefinitions),
1243            $providers
1244        );
1245    }
1246
1247    /**
1248     * Constructs core DI container service definitions array.
1249     *
1250     * @param array<string, mixed> $services Extra services and API bundle.
1251     * @return array<string, mixed> Core container definitions.
1252     */
1253    private function createCoreDefinitions(
1254        PDO $pdo,
1255        CacheInterface $cache,
1256        MetadataRepositoryInterface $metadataRepo,
1257        BootstrapEngineFactory $engineFactory,
1258        AuthController $authController,
1259        TranslationAndMenuConfigurator $i18n,
1260        array $services
1261    ): array {
1262        /** @var SecurityAndTemplateConfigurator $sec */
1263        $sec = $services['sec'];
1264        /** @var array<string, mixed> $apis */
1265        $apis = $services['apis'];
1266        $prefix = (string)($services['prefix'] ?? 'a_');
1267
1268        return [
1269            PDO::class                             => $pdo,
1270            Psr17Factory::class                    => $this->psr17Factory,
1271            SessionInterface::class                => $this->session,
1272            CurrentUser::class                     => $this->currentUser,
1273            CacheInterface::class                  => $cache,
1274            MetadataRepositoryInterface::class     => $metadataRepo,
1275            UniversalCrudService::class            => $this->centralEngineApiController->getCrudService(),
1276            CentralEngineApiController::class      => $this->centralEngineApiController,
1277            CentralEngineWebController::class      => $engineFactory->centralEngineWebController,
1278            AuthApiController::class               => $apis['auth'],
1279            AuthController::class                  => $authController,
1280            MenuApiController::class               => $i18n->menuApiController,
1281            AboutLicensesApiController::class      => $apis['licenses'],
1282            AboutRequirementsApiController::class  => $apis['requirements'],
1283            TranslationApiController::class        => $i18n->translationApiController,
1284            SettingsApiController::class           => $i18n->settingsApiController,
1285            LayoutApiController::class             => $i18n->layoutApiController,
1286            CronApiController::class               => $i18n->cronApiController,
1287            MailApiController::class               => $apis['mail'],
1288            AuditApiController::class              => $sec->auditApiController,
1289            SecurityAuditLogger::class             => $sec->securityAuditLogger,
1290            AuditIntegrityService::class           => $sec->auditIntegrityService,
1291            AuditRetentionService::class           => $sec->auditRetentionService,
1292            ProfileManager::class                  => $sec->profileManager,
1293            Profile::class                         => $sec->activeProfile,
1294            EngineSettings::class                  => new EngineSettings($pdo, $prefix),
1295            AuditRepositoryInterface::class        => $services['audit_repo'] ?? null,
1296            DynamicDataProviderRegistry::class     => $services['data_providers'] ?? null,
1297            ApiLoggerInterface::class              => new SqlApiLoggerRepository($pdo),
1298            PayloadSanitizer::class                => new PayloadSanitizer(),
1299            UserImpersonationService::class        => $services['impersonation_service'] ?? null,
1300            ImpersonationWebController::class      => $services['impersonation_web'] ?? null,
1301            ProfilePermissionRepositoryInterface::class => $services['profile_repo'] ?? null,
1302            DashboardWidgetsService::class        => $services['dashboard_widgets_service'] ?? null,
1303            DashboardCalendarActionService::class => $services['dashboard_action_service'] ?? null,
1304            DashboardApiController::class         => $apis['dashboard'] ?? null,
1305            DashboardHtmxController::class        => $services['dashboard_htmx'] ?? null,
1306            DashboardWebController::class         => $services['dashboard_web'] ?? null,
1307            TaxSettingsWebController::class       => $services['tax_web'] ?? null,
1308            TaxHtmxController::class              => $services['tax_htmx'] ?? null,
1309            DiscountSettingsWebController::class  => $services['discount_web'] ?? null,
1310            DiscountHtmxController::class         => $services['discount_htmx'] ?? null,
1311            InventorySettingsWebController::class => $services['inventory_web'] ?? null,
1312            InventorySettingsHtmxController::class => $services['inventory_htmx'] ?? null,
1313            InventoryItemsHtmxController::class   => $services['inventory_items_htmx'] ?? null,
1314            DataMappingWebController::class       => $services['mapping_web'] ?? null,
1315            DataMappingHtmxController::class      => $services['mapping_htmx'] ?? null,
1316            QuoteConversionWebController::class   => $services['quote_conversion_web'] ?? null,
1317            CurrenciesWebController::class        => $services['currencies_web'] ?? null,
1318            CurrenciesApiController::class        => $services['currencies_api'] ?? null,
1319        ];
1320    }
1321
1322    /**
1323     * Loads environment variables from a .env file located at application root if present.
1324     *
1325     * @param string $basePath Base application filesystem path.
1326     * @return void
1327     */
1328    private function loadDotEnv(string $basePath): void
1329    {
1330        $envPath = $basePath . '/.env';
1331        if (!file_exists($envPath)) {
1332            return;
1333        }
1334
1335        $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
1336        foreach ($lines as $line) {
1337            $trimmed = trim($line);
1338            if ($trimmed === '' || str_starts_with($trimmed, '#') || !str_contains($trimmed, '=')) {
1339                continue;
1340            }
1341            [$key, $val] = explode('=', $trimmed, 2);
1342            $key = trim($key);
1343            $val = trim($val, " \t\n\r\0\x0B\"'");
1344            if ($key !== '' && !array_key_exists($key, $_ENV)) {
1345                $_ENV[$key] = $val;
1346                putenv("{$key}={$val}");
1347            }
1348        }
1349    }
1350}
1351