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