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