Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.33% covered (success)
99.33%
297 / 299
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
PlatformServicesBootstrapProvider
99.33% covered (success)
99.33%
296 / 298
80.00% covered (warning)
80.00%
8 / 10
15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerAll
100.00% covered (success)
100.00%
75 / 75
100.00% covered (success)
100.00%
1 / 1
1
 initMailAndContextServices
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 initPreferenceAndProfileServices
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
3
 initDavServices
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 initPlatformFeatures
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 initStructureAndAccessServices
97.50% covered (success)
97.50%
39 / 40
0.00% covered (danger)
0.00%
0 / 1
3
 initSearchAndProfilePermissionServices
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
1
 initCommentsServices
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
1 / 1
1
 initCalendarRsvpController
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
2.00
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\Provider;
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\Core\Bootstrap\Configurator\SecurityAndTemplateConfigurator;
18use App\Core\Bootstrap\Configurator\TranslationAndMenuConfigurator;
19use App\Core\Engine\Application\Security\PermissionContextFactory;
20use App\Core\Engine\Application\Service\UniversalCrudService;
21use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface;
22use App\Core\Instance\Application\Service\InstanceContextManager;
23use App\Core\Instance\Application\Service\InstanceContextManagerInterface;
24use App\Core\Instance\Presentation\Api\InstanceContextApiController;
25use App\Core\Instance\Presentation\Twig\TwigInstanceContextExtension;
26use App\Modules\Menu\Domain\Repository\MenuRepositoryInterface;
27use App\Core\Preference\Application\Command\ResetUserPreferencesHandler;
28use App\Core\Preference\Application\Command\SaveUserPreferenceHandler;
29use App\Core\Preference\Domain\Service\ConfigurationCascadeResolver;
30use App\Core\Preference\Infrastructure\Repository\SqlUserPreferenceRepository;
31use App\Core\Preference\Presentation\Api\UserPreferencesApiController;
32use App\Core\Preference\Presentation\Twig\UserPreferencesTwigExtension;
33use App\Core\Engine\Infrastructure\Repository\SqlMetadataRepository;
34use App\Core\Search\Application\Service\GlobalSearchService;
35use App\Core\Search\Application\Service\SearchQueryParser;
36use App\Core\Search\Infrastructure\Repository\SqlSearchRepository;
37use App\Core\Search\Presentation\Api\GlobalSearchApiController;
38use App\Core\Search\Presentation\Api\SearchSettingsApiController;
39use App\Core\Security\Encryption\AesGcmEncryptionService;
40use App\Core\Settings\SqlSettingsRepository;
41use App\Modules\Calendar\Application\Service\CalendarIcsGenerator;
42use App\Modules\Calendar\Application\Service\CalendarInvitationService;
43use App\Modules\Calendar\Application\Service\CalendarRsvpTokenService;
44use App\Modules\Calendar\Presentation\Web\CalendarRsvpWebController;
45use App\Modules\Comments\Application\Service\CommentAttachmentService;
46use App\Modules\Comments\Application\Service\CommentHierarchyRollupService;
47use App\Modules\Comments\Application\Service\CommentMentionParserService;
48use App\Modules\Comments\Application\Service\CommentMentionsQueryService;
49use App\Modules\Comments\Application\Service\CommentService;
50use App\Modules\Comments\Infrastructure\Repository\SqlCommentRepository;
51use App\Modules\Comments\Presentation\Api\CommentsApiController;
52use App\Modules\Comments\Presentation\Htmx\CommentsHtmxController;
53use App\Modules\Comments\Presentation\Htmx\MentionsHtmxController;
54use App\Modules\Comments\Presentation\Web\CommentAttachmentDownloadWebController;
55use App\Modules\Comments\Presentation\Web\MentionsWebController;
56use App\Modules\Dav\Infrastructure\Config\AppleMobileConfigGenerator;
57use App\Modules\Dav\Infrastructure\Repository\SqlDavDeviceRepository;
58use App\Modules\Dav\Presentation\Api\DavDeviceApiController;
59use App\Modules\Dav\Presentation\Web\DavSetupWebController;
60use App\Modules\Mail\Application\Service\MailSenderService;
61use App\Modules\Mail\Application\Service\MailTemplateRenderer;
62use App\Modules\Mail\Infrastructure\Repository\SqlMailRepository;
63use App\Modules\Mail\Presentation\Api\MailApiController;
64use App\Modules\Profiles\Application\Service\ProfilePermissionService;
65use App\Modules\Profiles\Domain\Repository\ProfilePermissionRepositoryInterface;
66use App\Modules\Profiles\Infrastructure\Repository\CachedProfilePermissionRepository;
67use App\Modules\Profiles\Infrastructure\Repository\SqlProfilePermissionRepository;
68use App\Modules\Profiles\Presentation\Api\ProfilePermissionApiController;
69use App\Modules\Structure\Application\Service\StructureReassignmentService;
70use App\Modules\Structure\Application\Service\StructureTreeBuilder;
71use App\Modules\Structure\Infrastructure\Repository\SqlStructureMembershipRepository;
72use App\Modules\Structure\Infrastructure\Repository\SqlStructureRepository;
73use App\Modules\Structure\Presentation\Api\OwnershipApiController;
74use App\Modules\Structure\Presentation\Api\StructureApiController;
75use App\Modules\User\Application\Service\UserProfileService;
76use App\Modules\User\Infrastructure\Repository\SqlUserRepository;
77use App\Modules\User\Presentation\Api\UserProfileApiController;
78use Nyholm\Psr7\Factory\Psr17Factory;
79use PDO;
80use Yiisoft\Cache\ArrayCache;
81use Yiisoft\Cache\Cache;
82use Yiisoft\Cache\CacheInterface;
83use Yiisoft\Session\SessionInterface;
84use Yiisoft\User\CurrentUser;
85
86/**
87 * Bootstrap provider encapsulating platform features, collaboration, structure and search subsystems.
88 */
89final readonly class PlatformServicesBootstrapProvider
90{
91    public function __construct(
92        private Psr17Factory $psr17Factory,
93        private CurrentUser $currentUser,
94        private SessionInterface $session,
95        private SqlUserRepository $userRepository,
96        private PlatformFeaturesBootstrapProvider $platformFeaturesProvider
97    ) {
98    }
99
100    /**
101     * Initializes mail, preferences, user profile, DAV, structure, search and comments.
102     *
103     * @param array{
104     *     cache?: ?CacheInterface,
105     *     accessRepo?: ?AccessRepositoryInterface,
106     *     metadataRepo?: ?MetadataRepositoryInterface,
107     *     profileRepo?: ?ProfilePermissionRepositoryInterface,
108     *     basePath?: ?string
109     * } $options Optional dependencies.
110     * @return array<string, mixed>
111     */
112    public function registerAll(
113        ?PDO $pdo,
114        string $prefix,
115        SecurityAndTemplateConfigurator $sec,
116        TranslationAndMenuConfigurator $i18n,
117        InstanceContextManager $instanceContextManager,
118        UniversalCrudService $crudService,
119        array $options = []
120    ): array {
121        $cache = $options['cache'] ?? new Cache(new ArrayCache());
122        $accessRepo = $options['accessRepo'] ?? null;
123        $metadataRepo = $options['metadataRepo'] ?? new SqlMetadataRepository($pdo);
124        $profileRepo = $options['profileRepo'] ?? null;
125        $basePath = $options['basePath'] ?? null;
126
127        $mailAndContext = $this->initMailAndContextServices($pdo, $prefix, $sec, $instanceContextManager);
128        $prefAndProfile = $this->initPreferenceAndProfileServices($pdo, $prefix, $sec, $i18n);
129        $davServices = $this->initDavServices($pdo, $prefix);
130        $features = $this->initPlatformFeatures($pdo, $sec, $crudService, $cache, $mailAndContext['sql_mail_repo']);
131
132        $structAndAccess = $this->initStructureAndAccessServices(
133            $pdo,
134            $prefix,
135            $sec,
136            $instanceContextManager,
137            $cache,
138            $accessRepo
139        );
140
141        $searchAndProfile = $this->initSearchAndProfilePermissionServices(
142            $pdo,
143            $prefix,
144            $metadataRepo,
145            $prefAndProfile['user_pref_repo'],
146            $profileRepo,
147            $structAndAccess['access_repo'],
148            $options['menuRepo'] ?? null,
149            $sec->activeProfile->name,
150            $instanceContextManager
151        );
152
153        $commentsServices = $this->initCommentsServices(
154            $pdo,
155            $prefix,
156            $sec,
157            $searchAndProfile['search_service'],
158            $basePath
159        );
160
161        $calendarRsvpController = $this->initCalendarRsvpController(
162            $pdo,
163            $prefix,
164            $sec,
165            $mailAndContext['sql_mail_repo']
166        );
167
168        return [
169            'apis' => array_merge([
170                'mail'                => $mailAndContext['mail_api'],
171                'instance'            => $mailAndContext['instance_api'],
172                'preferences'         => $prefAndProfile['pref_api'],
173                'profile'             => $prefAndProfile['profile_api'],
174                'dav_device'          => $davServices['dav_device_api'],
175                'ownership'           => $structAndAccess['ownership_api'],
176                'structure'           => $structAndAccess['structure_api'],
177                'access'              => $structAndAccess['access_api'],
178                'profile_permissions' => $searchAndProfile['profile_api'],
179                'search'              => $searchAndProfile['search_api'],
180                'search_settings'     => $searchAndProfile['search_settings_api'],
181                'comments'            => $commentsServices['comments_api'],
182            ], $features['webmail']['apis'], $features['maps']['apis'], $features['pdf']['apis']),
183            'dav_setup'                       => $davServices['dav_setup_web'],
184            'access_web'                      => $structAndAccess['access_web'],
185            'webmail_web'                     => $features['webmail']['webmail_web'],
186            'webmail_htmx'                    => $features['webmail']['webmail_htmx'],
187            'map_web'                         => $features['maps']['map_web'],
188            'map_htmx'                        => $features['maps']['map_htmx'],
189            'pdf_htmx'                        => $features['pdf']['pdf_htmx'],
190            'pdf_web'                         => $features['pdf']['pdf_web'],
191            'doc_verification'                => $features['pdf']['doc_verification'],
192            'pdf_signature_htmx'              => $features['pdf']['pdf_signature_htmx'],
193            'pdf_queue_htmx'                  => $features['pdf']['pdf_queue_htmx'],
194            'mail_tracking_web'               => $features['pdf']['mail_tracking_web'],
195            'mail_template_web'               => $features['pdf']['mail_template_web'] ?? null,
196            'comments_htmx'                   => $commentsServices['comments_htmx'],
197            'mentions_web'                    => $commentsServices['mentions_web'],
198            'mentions_htmx'                   => $commentsServices['mentions_htmx'],
199            'comment_attachment_download_web' => $commentsServices['attachment_download_web'],
200            'calendar_rsvp_web'               => $calendarRsvpController,
201        ];
202    }
203
204    /**
205     * @return array{
206     *     sql_mail_repo: SqlMailRepository,
207     *     mail_api: MailApiController,
208     *     instance_api: InstanceContextApiController
209     * }
210     */
211    private function initMailAndContextServices(
212        ?PDO $pdo,
213        string $prefix,
214        SecurityAndTemplateConfigurator $sec,
215        InstanceContextManager $instanceContextManager
216    ): array {
217        $sqlMailRepo = new SqlMailRepository($pdo, $prefix);
218        $mailSender = new MailSenderService($sqlMailRepo, new MailTemplateRenderer(), new AesGcmEncryptionService());
219        $mailApiController = new MailApiController($this->psr17Factory, $sqlMailRepo, $mailSender);
220
221        $instanceContextApiController = new InstanceContextApiController(
222            $instanceContextManager,
223            $this->psr17Factory
224        );
225        $sec->twig->addExtension(new TwigInstanceContextExtension($instanceContextManager));
226
227        return [
228            'sql_mail_repo' => $sqlMailRepo,
229            'mail_api'      => $mailApiController,
230            'instance_api'  => $instanceContextApiController,
231        ];
232    }
233
234    /**
235     * @return array{
236     *     user_pref_repo: SqlUserPreferenceRepository,
237     *     pref_api: UserPreferencesApiController,
238     *     profile_api: UserProfileApiController
239     * }
240     */
241    private function initPreferenceAndProfileServices(
242        ?PDO $pdo,
243        string $prefix,
244        SecurityAndTemplateConfigurator $sec,
245        TranslationAndMenuConfigurator $i18n
246    ): array {
247        $userPrefix = (isset($sec->activeProfile) && $sec->activeProfile->name === 'client') ? 'c_' : $prefix;
248        $userPreferenceRepo = new SqlUserPreferenceRepository($pdo, $userPrefix);
249        $cascadeResolver = new ConfigurationCascadeResolver(
250            $userPreferenceRepo,
251            new SqlSettingsRepository($i18n->settingsApiController, $prefix)
252        );
253        $userPrefApiController = new UserPreferencesApiController(
254            $this->psr17Factory,
255            $userPreferenceRepo,
256            $cascadeResolver,
257            new SaveUserPreferenceHandler($userPreferenceRepo),
258            new ResetUserPreferencesHandler($userPreferenceRepo)
259        );
260        $sec->twig->addExtension(new UserPreferencesTwigExtension($cascadeResolver, $userPreferenceRepo));
261
262        $userProfileService = new UserProfileService($this->userRepository);
263        $userProfileApiController = new UserProfileApiController(
264            $userProfileService,
265            $this->session,
266            $this->currentUser,
267            $this->psr17Factory
268        );
269
270        return [
271            'user_pref_repo' => $userPreferenceRepo,
272            'pref_api'       => $userPrefApiController,
273            'profile_api'    => $userProfileApiController,
274        ];
275    }
276
277    /**
278     * @return array{dav_device_api: DavDeviceApiController, dav_setup_web: DavSetupWebController}
279     */
280    private function initDavServices(?PDO $pdo, string $prefix): array
281    {
282        $davDeviceRepo = new SqlDavDeviceRepository($pdo, $prefix);
283        $davDeviceApiController = new DavDeviceApiController(
284            $davDeviceRepo,
285            $this->session,
286            $this->currentUser,
287            $this->psr17Factory
288        );
289
290        $davSetupWebController = new DavSetupWebController(
291            $davDeviceRepo,
292            $this->userRepository,
293            new AppleMobileConfigGenerator(),
294            $this->psr17Factory
295        );
296
297        return [
298            'dav_device_api' => $davDeviceApiController,
299            'dav_setup_web'  => $davSetupWebController,
300        ];
301    }
302
303    /**
304     * @return array{maps: array<string, mixed>, pdf: array<string, mixed>, webmail: array<string, mixed>}
305     */
306    private function initPlatformFeatures(
307        ?PDO $pdo,
308        SecurityAndTemplateConfigurator $sec,
309        UniversalCrudService $crudService,
310        CacheInterface $cache,
311        SqlMailRepository $sqlMailRepo
312    ): array {
313        $mapSetup = $this->platformFeaturesProvider->initializeMaps($pdo, $sec);
314        $pdfSetup = $this->platformFeaturesProvider->initializePdf($pdo, $sec, $crudService);
315        $webmailSetup = $this->platformFeaturesProvider->initializeWebmail(
316            $pdo,
317            $sec,
318            $cache,
319            $sqlMailRepo,
320            $pdfSetup['generator'] ?? null,
321            $pdfSetup['template_repo'] ?? null
322        );
323
324        return [
325            'maps'    => $mapSetup,
326            'pdf'     => $pdfSetup,
327            'webmail' => $webmailSetup,
328        ];
329    }
330
331    /**
332     * @return array{
333     *     access_repo: AccessRepositoryInterface,
334     *     structure_repo: SqlStructureRepository,
335     *     ownership_api: OwnershipApiController,
336     *     structure_api: StructureApiController,
337     *     access_api: AccessApiController,
338     *     access_web: AccessWebController
339     * }
340     */
341    private function initStructureAndAccessServices(
342        ?PDO $pdo,
343        string $prefix,
344        SecurityAndTemplateConfigurator $sec,
345        InstanceContextManager $instanceContextManager,
346        CacheInterface $cache,
347        ?AccessRepositoryInterface $accessRepo
348    ): array {
349        $structurePrefix = (isset($sec->activeProfile) && $sec->activeProfile->name === 'client')
350            ? 'c_'
351            : $prefix;
352        $membershipRepo = new SqlStructureMembershipRepository($pdo, $structurePrefix);
353        $structureRepo = new SqlStructureRepository($pdo, $membershipRepo, $structurePrefix);
354        $structureTreeBuilder = new StructureTreeBuilder($structureRepo);
355        $reassignService = new StructureReassignmentService($pdo, $structureRepo, $membershipRepo, $structurePrefix);
356
357        $ownershipApiController = new OwnershipApiController(
358            $this->psr17Factory,
359            $this->userRepository,
360            $structureRepo
361        );
362        $structureApiController = new StructureApiController(
363            $this->psr17Factory,
364            $membershipRepo,
365            $structureTreeBuilder,
366            $reassignService,
367            $structureRepo
368        );
369
370        $effectiveAccessRepo = $accessRepo ?? new CachedAccessRepository(new SqlAccessRepository($pdo), $cache);
371        $accessManager = new AccessManagerService($effectiveAccessRepo, $pdo);
372        $accessContextFactory = new PermissionContextFactory($this->session, $pdo);
373        $accessApiController = new AccessApiController($accessManager, $accessContextFactory, $this->psr17Factory);
374        $accessWebController = new AccessWebController(
375            $accessManager,
376            $instanceContextManager,
377            $structureRepo,
378            $this->userRepository,
379            $accessContextFactory,
380            $sec->twig,
381            $this->psr17Factory
382        );
383
384        return [
385            'access_repo'    => $effectiveAccessRepo,
386            'structure_repo' => $structureRepo,
387            'ownership_api'  => $ownershipApiController,
388            'structure_api'  => $structureApiController,
389            'access_api'     => $accessApiController,
390            'access_web'     => $accessWebController,
391        ];
392    }
393
394    /**
395     * @return array{
396     *     search_service: GlobalSearchService,
397     *     profile_api: ProfilePermissionApiController,
398     *     search_api: GlobalSearchApiController,
399     *     search_settings_api: SearchSettingsApiController
400     * }
401     */
402    private function initSearchAndProfilePermissionServices(
403        ?PDO $pdo,
404        string $prefix,
405        MetadataRepositoryInterface $metadataRepo,
406        SqlUserPreferenceRepository $userPreferenceRepo,
407        ?ProfilePermissionRepositoryInterface $profileRepo,
408        AccessRepositoryInterface $accessRepo,
409        ?MenuRepositoryInterface $menuRepo = null,
410        string $appProfile = 'admin',
411        ?InstanceContextManagerInterface $instanceContextManager = null
412    ): array {
413        $effectiveProfileRepo = $profileRepo ?? new CachedProfilePermissionRepository(
414            new SqlProfilePermissionRepository($pdo)
415        );
416        $profilePermissionService = new ProfilePermissionService($effectiveProfileRepo, $metadataRepo, $pdo);
417        $profileContextFactory = new PermissionContextFactory($this->session, $pdo);
418        $profileApiController = new ProfilePermissionApiController(
419            $profilePermissionService,
420            $profileContextFactory,
421            $this->psr17Factory
422        );
423
424        $searchQueryParser = new SearchQueryParser();
425        $sqlSearchRepo = new SqlSearchRepository($pdo, $searchQueryParser);
426        $globalSearchService = new GlobalSearchService(
427            $metadataRepo,
428            $sqlSearchRepo,
429            $userPreferenceRepo,
430            $effectiveProfileRepo,
431            $accessRepo,
432            $pdo,
433            $prefix,
434            $menuRepo,
435            $appProfile,
436            $instanceContextManager
437        );
438
439        return [
440            'search_service'      => $globalSearchService,
441            'profile_api'         => $profileApiController,
442            'search_api'          => new GlobalSearchApiController(
443                $globalSearchService,
444                $profileContextFactory,
445                $this->psr17Factory
446            ),
447            'search_settings_api' => new SearchSettingsApiController(
448                $globalSearchService,
449                $profileContextFactory,
450                $this->psr17Factory
451            ),
452        ];
453    }
454
455    /**
456     * @return array{
457     *     comments_api: CommentsApiController,
458     *     comments_htmx: CommentsHtmxController,
459     *     mentions_web: MentionsWebController,
460     *     mentions_htmx: MentionsHtmxController,
461     *     attachment_download_web: CommentAttachmentDownloadWebController
462     * }
463     */
464    private function initCommentsServices(
465        ?PDO $pdo,
466        string $prefix,
467        SecurityAndTemplateConfigurator $sec,
468        GlobalSearchService $globalSearchService,
469        ?string $basePath
470    ): array {
471        $baseDir = $basePath ?? dirname(__DIR__, 4);
472        $commentsRepo = new SqlCommentRepository($pdo);
473        $hierarchyRollup = new CommentHierarchyRollupService($pdo);
474        $mentionParser = new CommentMentionParserService();
475        $commentAttachmentService = new CommentAttachmentService($commentsRepo, $baseDir);
476        $commentService = new CommentService(
477            $commentsRepo,
478            $hierarchyRollup,
479            $commentAttachmentService,
480            $mentionParser
481        );
482        $commentsContextFactory = new PermissionContextFactory($this->session, $pdo);
483        $commentMentionsQueryService = new CommentMentionsQueryService(
484            $pdo,
485            $mentionParser,
486            null,
487            $prefix
488        );
489        $commentsApiController = new CommentsApiController(
490            $commentService,
491            $this->psr17Factory,
492            $this->userRepository,
493            $globalSearchService,
494            $commentMentionsQueryService,
495            $pdo
496        );
497        $commentsHtmxController = new CommentsHtmxController(
498            $commentService,
499            $sec->twig,
500            $this->psr17Factory,
501            $commentsContextFactory,
502            $commentsApiController
503        );
504        $mentionsWebController = new MentionsWebController(
505            $sec->twig,
506            $commentMentionsQueryService,
507            $this->psr17Factory
508        );
509        $mentionsHtmxController = new MentionsHtmxController(
510            $sec->twig,
511            $commentMentionsQueryService,
512            $this->psr17Factory
513        );
514        $downloadWebController = new CommentAttachmentDownloadWebController(
515            $commentService,
516            $commentAttachmentService
517        );
518
519        return [
520            'comments_api'            => $commentsApiController,
521            'comments_htmx'           => $commentsHtmxController,
522            'mentions_web'            => $mentionsWebController,
523            'mentions_htmx'           => $mentionsHtmxController,
524            'attachment_download_web' => $downloadWebController,
525        ];
526    }
527
528    private function initCalendarRsvpController(
529        ?PDO $pdo,
530        string $prefix,
531        SecurityAndTemplateConfigurator $sec,
532        SqlMailRepository $sqlMailRepo
533    ): ?CalendarRsvpWebController {
534        if ($pdo === null) {
535            return null;
536        }
537
538        $icsGen = new CalendarIcsGenerator();
539        $tokenSvc = new CalendarRsvpTokenService();
540        $invSvc = new CalendarInvitationService(
541            $icsGen,
542            $tokenSvc,
543            $sec->twig,
544            $sqlMailRepo,
545            $pdo,
546            $prefix
547        );
548
549        return new CalendarRsvpWebController(
550            $this->psr17Factory,
551            $tokenSvc,
552            $invSvc,
553            $icsGen,
554            $sec->twig
555        );
556    }
557}
558