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