Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.05% covered (warning)
87.05%
121 / 139
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApieServiceProvider
87.05% covered (warning)
87.05%
121 / 139
0.00% covered (danger)
0.00%
0 / 4
30.83
0.00% covered (danger)
0.00%
0 / 1
 autoTagHashmapActions
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 boot
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 register
86.02% covered (warning)
86.02%
80 / 93
0.00% covered (danger)
0.00%
0 / 1
18.88
 sanitizeConfig
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2namespace Apie\LaravelApie;
3
4use Apie\AiInstructor\AiInstructorServiceProvider;
5use Apie\ApieCommonPlugin\ApieCommonPluginServiceProvider;
6use Apie\ApieFileSystem\ApieFileSystemServiceProvider;
7use Apie\CmsApiDropdownOption\CmsDropdownServiceProvider;
8use Apie\Common\AddBasicAuthServiceProvider;
9use Apie\Common\CommonServiceProvider;
10use Apie\Common\ContextBuilders\FrameworkContextBuilder;
11use Apie\Common\Events\AddAuthenticationCookie;
12use Apie\Common\Interfaces\BoundedContextSelection;
13use Apie\Common\Interfaces\DashboardContentFactoryInterface;
14use Apie\Common\Wrappers\BoundedContextHashmapFactory;
15use Apie\Common\Wrappers\ConsoleCommandFactory as CommonConsoleCommandFactory;
16use Apie\Console\ConsoleServiceProvider;
17use Apie\Core\CoreServiceProvider;
18use Apie\Core\Session\CsrfTokenProvider;
19use Apie\DoctrineEntityConverter\DoctrineEntityConverterProvider;
20use Apie\DoctrineEntityDatalayer\Commands\ApieUpdateIdfCommand;
21use Apie\DoctrineEntityDatalayer\DoctrineEntityDatalayerServiceProvider;
22use Apie\DoctrineEntityDatalayer\EntityReindexer;
23use Apie\DoctrineEntityDatalayer\IndexStrategy\BackgroundIndexStrategy;
24use Apie\DoctrineEntityDatalayer\IndexStrategy\DirectIndexStrategy;
25use Apie\DoctrineEntityDatalayer\IndexStrategy\IndexAfterResponseIsSentStrategy;
26use Apie\DoctrineEntityDatalayer\IndexStrategy\IndexStrategyInterface;
27use Apie\Export\ExportServiceProvider;
28use Apie\Faker\FakerServiceProvider;
29use Apie\FtpServer\FtpServerServiceProvider;
30use Apie\Graphql\GraphqlServiceProvider;
31use Apie\HtmlBuilders\ErrorHandler\CmsErrorRenderer;
32use Apie\HtmlBuilders\HtmlBuilderServiceProvider;
33use Apie\LaravelApie\Config\ValidateAndSanitizeConfig;
34use Apie\LaravelApie\ContextBuilders\ApieCurrentUserContextBuilder;
35use Apie\LaravelApie\ContextBuilders\CsrfTokenContextBuilder;
36use Apie\LaravelApie\ContextBuilders\LaravelLocaleContextBuilder;
37use Apie\LaravelApie\ContextBuilders\LogoutUrlContextBuilder;
38use Apie\LaravelApie\ContextBuilders\RegisterBoundedContextActionContextBuilder;
39use Apie\LaravelApie\ContextBuilders\SessionContextBuilder;
40use Apie\LaravelApie\ErrorHandler\ApieErrorRenderer;
41use Apie\LaravelApie\ErrorHandler\Handler;
42use Apie\LaravelApie\Providers\CmsServiceProvider;
43use Apie\LaravelApie\Providers\SecurityServiceProvider;
44use Apie\LaravelApie\Wrappers\Cms\DashboardContentFactory;
45use Apie\LaravelApie\Wrappers\Core\BoundedContextSelected;
46use Apie\LaravelApie\Wrappers\Queue\BackgroundProcessPersistListener;
47use Apie\Maker\MakerServiceProvider;
48use Apie\McpServer\McpServerServiceProvider;
49use Apie\RestApi\RestApiServiceProvider;
50use Apie\SchemaGenerator\SchemaGeneratorServiceProvider;
51use Apie\Serializer\SerializerServiceProvider;
52use Apie\ServiceProviderGenerator\TagMap;
53use Apie\TypescriptClientBuilder\TypescriptClientBuilderServiceProvider;
54use Apie\Webdav\WebdavServiceProvider;
55use Illuminate\Config\Repository;
56use Illuminate\Contracts\Debug\ExceptionHandler;
57use Illuminate\Contracts\Events\Dispatcher;
58use Illuminate\Cookie\Middleware\EncryptCookies;
59use Illuminate\Support\ServiceProvider;
60use Psr\EventDispatcher\EventDispatcherInterface;
61use Psr\Http\Message\ServerRequestInterface;
62use Symfony\Component\Console\Application;
63use Symfony\Component\EventDispatcher\EventDispatcher;
64use Symfony\Component\Lock\LockFactory;
65
66class ApieServiceProvider extends ServiceProvider
67{
68    /**
69     * @var array<string, class-string<ServiceProvider>> $alreadyRegistered
70     */
71    private array $alreadyRegistered = [];
72    /**
73     * @var array<string, array<int, class-string<ServiceProvider>>> $dependencies
74     */
75    private array $dependencies = [
76        'enable_ai_instructor' => [
77            AiInstructorServiceProvider::class,
78        ],
79        'enable_basic_auth' => [
80            AddBasicAuthServiceProvider::class,
81        ],
82        'enable_common_plugin' => [
83            ApieCommonPluginServiceProvider::class,
84        ],
85        'enable_cms' => [
86            CommonServiceProvider::class,
87            HtmlBuilderServiceProvider::class, // it's important that this loads before CmsServiceProvider!!!
88            CmsServiceProvider::class,
89            SerializerServiceProvider::class,
90        ],
91        'enable_cms_dropdown' => [
92            CommonServiceProvider::class,
93            CmsDropdownServiceProvider::class,
94        ],
95        'enable_core' => [
96            CoreServiceProvider::class,
97        ],
98        'enable_console' => [
99            CommonServiceProvider::class,
100            ConsoleServiceProvider::class, // it's important that this loads after CommonServiceProvider!!!
101            SerializerServiceProvider::class,
102        ],
103        'enable_doctrine_entity_converter' => [
104            CoreServiceProvider::class,
105            DoctrineEntityConverterProvider::class,
106        ],
107        'enable_doctrine_entity_datalayer' => [
108            CoreServiceProvider::class,
109            DoctrineEntityConverterProvider::class,
110            DoctrineEntityDatalayerServiceProvider::class,
111        ],
112        'enable_export' => [
113            SerializerServiceProvider::class,
114            ExportServiceProvider::class,
115        ],
116        'enable_security' => [
117            CommonServiceProvider::class,
118            SerializerServiceProvider::class,
119            SecurityServiceProvider::class,
120        ],
121        'enable_rest_api' => [
122            CommonServiceProvider::class,
123            RestApiServiceProvider::class,
124            SchemaGeneratorServiceProvider::class,
125            SerializerServiceProvider::class,
126        ],
127        'enable_faker' => [
128            FakerServiceProvider::class,
129        ],
130        'enable_ftp' => [
131            ApieFileSystemServiceProvider::class,
132            FtpServerServiceProvider::class,
133        ],
134        'enable_graphql' => [
135            CommonServiceProvider::class,
136            SerializerServiceProvider::class,
137            GraphqlServiceProvider::class,
138        ],
139        'enable_maker' => [
140            MakerServiceProvider::class,
141        ],
142        'enable_mcp_server' => [
143            CommonServiceProvider::class,
144            SerializerServiceProvider::class,
145            McpServerServiceProvider::class,
146        ],
147        'enable_typescript_client_builder' => [
148            TypescriptClientBuilderServiceProvider::class,
149        ],
150        'enable_webdav' => [
151            ApieFileSystemServiceProvider::class,
152            WebdavServiceProvider::class,
153        ]
154    ];
155
156    private function autoTagHashmapActions(): void
157    {
158        $boundedContextConfig = config('apie.bounded_contexts');
159        $scanBoundedContextConfig = config('apie.scan_bounded_contexts');
160        $factory = new BoundedContextHashmapFactory(
161            $boundedContextConfig ?? [],
162            $scanBoundedContextConfig ?? [],
163            new EventDispatcher(),
164        );
165        $hashmap = $factory->create();
166        foreach ($hashmap as $boundedContext) {
167            foreach ($boundedContext->actions as $action) {
168                $class = $action->getDeclaringClass();
169                if (!$class->isInstantiable()) {
170                    continue;
171                }
172                $className = $class->name;
173                TagMap::register(
174                    $this->app,
175                    $className,
176                    ['apie.context']
177                );
178            }
179        }
180    }
181
182    public function boot(): void
183    {
184        $this->autoTagHashmapActions();
185        $this->loadViewsFrom(__DIR__ . '/../templates', 'apie');
186        $this->loadRoutesFrom(__DIR__.'/../resources/routes.php');
187
188        if ($this->app->runningInConsole()) {
189            $commands = [];
190            if ($this->app->bound(ApieUpdateIdfCommand::class)) {
191                $commands[] = ApieUpdateIdfCommand::class;
192            }
193            // for some reason these are not called in integration tests without re-registering them
194            foreach (TagMap::getServiceIdsWithTag($this->app, 'console.command') as $taggedCommand) {
195                $serviceId = 'apie.console.tagged.' . $taggedCommand;
196                $this->app->singleton($serviceId, function () use ($taggedCommand) {
197                    return $this->app->get($taggedCommand);
198                });
199                $commands[] = $serviceId;
200            }
201            if ($this->app->bound('apie.console.factory')) {
202                /** @var CommonConsoleCommandFactory $factory */
203                $factory = $this->app->get('apie.console.factory');
204                foreach ($factory->create($this->app->get(Application::class)) as $command) {
205                    $serviceId = 'apie.console.registered.' . $command->getName();
206                    $this->app->instance($serviceId, $command);
207                    $commands[] = $serviceId;
208                }
209            }
210            $this->commands($commands);
211        }
212
213        $this->app->booted(function () {
214            TagMap::markBooted($this->app);
215            TagMap::registerEvents($this->app);
216        });
217    }
218
219    public function register()
220    {
221        EncryptCookies::except(AddAuthenticationCookie::COOKIE_NAME);
222        $this->mergeConfigFrom(__DIR__ . '/../resources/apie.php', 'apie');
223
224        $this->app->bind(FrameworkContextBuilder::class, function () {
225            return new FrameworkContextBuilder('laravel');
226        });
227        TagMap::register($this->app, FrameworkContextBuilder::class, ['apie.core.context_builder']);
228
229        $this->app->bind(\Symfony\Contracts\Cache\CacheInterface::class, function () {
230            return new Wrappers\Cache\LaravelCache($this->app->make('cache.store'));
231        });
232
233        // add PSR-14 support if needed:
234        if (!$this->app->bound(EventDispatcherInterface::class)) {
235            $this->app->bind(EventDispatcherInterface::class, function () {
236                return new class($this->app->make(Dispatcher::class)) implements EventDispatcherInterface {
237                    public function __construct(private readonly Dispatcher $dispatcher)
238                    {
239                    }
240
241                    public function dispatch(object $event): object
242                    {
243                        $this->dispatcher->dispatch($event);
244                        return $event;
245                    }
246                };
247            });
248        }
249
250        // fix for https://github.com/laravel/framework/issues/30415
251        $this->app->extend(
252            ServerRequestInterface::class,
253            function (ServerRequestInterface $psrRequest) {
254                $route = $this->app->make('request')->route();
255                if ($route) {
256                    $parameters = $route->parameters();
257                    foreach ($parameters as $key => $value) {
258                        $psrRequest = $psrRequest->withAttribute($key, $value);
259                    }
260                }
261                return $psrRequest;
262            }
263        );
264
265        $this->app->bind(IndexStrategyInterface::class, function () {
266            $config = config();
267            if ($config->get('apie.enable_doctrine_entity_datalayer')) {
268                $type = $config->get('apie.doctrine.indexing.type', 'direct');
269                return match ($type) {
270                    'direct' => new DirectIndexStrategy($this->app->get(EntityReindexer::class)),
271                    'late' => new IndexAfterResponseIsSentStrategy($this->app->get(EntityReindexer::class)),
272                    'background' => new BackgroundIndexStrategy(),
273                    default => $this->app->get(config('apie.doctrine.indexing.service', DirectIndexStrategy::class)),
274                };
275            }
276
277            return new DirectIndexStrategy($this->app->get(EntityReindexer::class));
278        });
279
280        $this->app->bind(ApieErrorRenderer::class, function () {
281            return new ApieErrorRenderer(
282                $this->app->bound(CmsErrorRenderer::class) ? $this->app->make(CmsErrorRenderer::class) : null,
283                $this->app->make(\Apie\Common\ErrorHandler\ApiErrorRenderer::class),
284                config('apie.cms.base_url')
285            );
286        });
287
288        $this->app->extend(ExceptionHandler::class, function (ExceptionHandler $service) {
289            return new Handler($this->app, $service);
290        });
291
292        $this->app->bind(LockFactory::class, function () {
293            $config = config('apie.lock_store');
294            return new LockFactory($this->app->get($config));
295        });
296        
297        $this->app->bind(DashboardContentFactoryInterface::class, DashboardContentFactory::class);
298        $this->app->bind(BoundedContextSelection::class, BoundedContextSelected::class);
299
300        $this->alreadyRegistered = [];
301        $parsedConfig = ValidateAndSanitizeConfig::process(config('apie'));
302        foreach ($this->dependencies as $configKey => $dependencies) {
303            if ($parsedConfig[$configKey] ?? false) {
304                foreach ($dependencies as $dependency) {
305                    if (!isset($this->alreadyRegistered[$dependency])) {
306                        $this->alreadyRegistered[$dependency] = $dependency;
307                        $this->app->register($dependency);
308                    }
309                }
310            }
311        }
312
313        TagMap::register($this->app, LaravelLocaleContextBuilder::class, ['apie.core.context_builder']);
314        $this->app->tag(LaravelLocaleContextBuilder::class, ['apie.core.context_builder']);
315
316        //$this->app->bind(CsrfTokenProvider::class, CsrfTokenContextBuilder::class);
317        TagMap::register($this->app, CsrfTokenContextBuilder::class, ['apie.core.context_builder']);
318        $this->app->tag(CsrfTokenContextBuilder::class, ['apie.core.context_builder']);
319
320        // this has to be added after CsrfTokenContextBuilder!
321        $this->app->bind(SessionContextBuilder::class);
322        TagMap::register($this->app, SessionContextBuilder::class, ['apie.core.context_builder']);
323        $this->app->tag(SessionContextBuilder::class, ['apie.core.context_builder']);
324
325        $this->app->bind(ApieCurrentUserContextBuilder::class);
326        TagMap::register($this->app, ApieCurrentUserContextBuilder::class, ['apie.core.context_builder', 'kernel.event_subscriber']);
327        $this->app->tag(ApieCurrentUserContextBuilder::class, ['apie.core.context_builder', 'kernel.event_subscriber']);
328
329        $this->app->bind(LogoutUrlContextBuilder::class, function () {
330            return new LogoutUrlContextBuilder(config('apie.logout_url'));
331        });
332        TagMap::register($this->app, LogoutUrlContextBuilder::class, ['apie.core.context_builder']);
333        $this->app->tag(LogoutUrlContextBuilder::class, ['apie.core.context_builder']);
334
335        TagMap::register($this->app, RegisterBoundedContextActionContextBuilder::class, ['apie.core.context_builder']);
336        $this->app->tag(RegisterBoundedContextActionContextBuilder::class, ['apie.core.context_builder']);
337        $this->app->resolving('config', function (Repository $config) {
338            $this->sanitizeConfig($config);
339            $newParsedConfig = $config->get('apie');
340            foreach ($this->dependencies as $configKey => $dependencies) {
341                if ($newParsedConfig[$configKey] ?? false) {
342                    foreach ($dependencies as $dependency) {
343                        if (!isset($this->alreadyRegistered[$dependency])) {
344                            $this->alreadyRegistered[$dependency] = $dependency;
345                            $this->app->register($dependency);
346                        }
347                    }
348                }
349            }
350            return $config;
351        });
352
353        TagMap::register($this->app, BackgroundProcessPersistListener::class, ['kernel.event_subscriber']);
354    }
355
356    private function sanitizeConfig(Repository $config): void
357    {
358        $rawConfig = $config->get('apie');
359        $processedConfig = ValidateAndSanitizeConfig::process($rawConfig);
360
361        $config->set('apie', $processedConfig);
362    }
363}