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