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