Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.51% covered (success)
90.51%
124 / 137
89.09% covered (success)
89.09%
49 / 55
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConfigurationMethodsTrait
90.51% covered (success)
90.51%
124 / 137
89.09% covered (success)
89.09%
49 / 55
95.77
0.00% covered (danger)
0.00%
0 / 1
 runtimeObject
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 setDatabase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getDb
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getServiceContainer
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAdminEmail
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTemplateSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNoReplyEmail
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMailProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRootPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
8
 getAll
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 setLdapConfig
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLdapMapping
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLdapOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLdapGroupConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLdapConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLdapServer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLdapActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isElasticsearchActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSignInWithMicrosoftActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSignInWithKeycloakActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setElasticsearch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getElasticsearch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOpenSearch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOpenSearch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setElasticsearchConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getElasticsearchConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOpenSearchConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOpenSearchConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTranslationProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTranslationProvider
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 initializeTranslationProvider
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
18.35
 add
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
7
 replaceMainReferenceUrl
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getAllowedMediaHosts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCustomCss
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPluginManager
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getPluginManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 triggerEvent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPluginConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Configuration methods for the Configuration class
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-02-24
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ;
21
22use Elastic\Elasticsearch\Client;
23use Monolog\Handler\BrowserConsoleHandler;
24use Monolog\Handler\StreamHandler;
25use Monolog\Level;
26use Monolog\Logger;
27use phpMyFAQ\Configuration\ConfigurationRepository;
28use phpMyFAQ\Configuration\ElasticsearchConfiguration;
29use phpMyFAQ\Configuration\LayoutSettings;
30use phpMyFAQ\Configuration\LdapConfiguration;
31use phpMyFAQ\Configuration\LdapSettings;
32use phpMyFAQ\Configuration\MailSettings;
33use phpMyFAQ\Configuration\OpenSearchConfiguration;
34use phpMyFAQ\Configuration\SearchSettings;
35use phpMyFAQ\Configuration\SecuritySettings;
36use phpMyFAQ\Configuration\UrlSettings;
37use phpMyFAQ\Database\DatabaseDriver;
38use phpMyFAQ\Plugin\PluginConfigurationInterface;
39use phpMyFAQ\Plugin\PluginException;
40use phpMyFAQ\Plugin\PluginManager;
41use phpMyFAQ\Translation\TranslationProviderFactory;
42use phpMyFAQ\Translation\TranslationProviderInterface;
43use Symfony\Component\DependencyInjection\ContainerInterface;
44use Symfony\Contracts\HttpClient\HttpClientInterface;
45
46/* @mago-expect lint:too-many-methods - legacy configuration facade; split into settings services is in progress */
47trait ConfigurationMethodsTrait
48{
49    /**
50     * Configuration store: string values from the faqconfig table plus the
51     * runtime objects registered under `core.*` keys.
52     *
53     * @var array<string, mixed>
54     */
55    private array $config = [];
56
57    private Logger $logger;
58
59    private PluginManager $pluginManager;
60
61    private ConfigurationRepository $configurationRepository;
62
63    private LdapSettings $ldapSettings;
64
65    /** @var array<string, mixed> LDAP runtime configuration built from ldap.php */
66    private array $ldapConfig = [];
67
68    /** @var array<int, array<string, mixed>> LDAP server definitions built from ldap.php */
69    private array $ldapServer = [];
70
71    private MailSettings $mailSettings;
72
73    private SearchSettings $searchSettings;
74
75    private SecuritySettings $securitySettings;
76
77    private LayoutSettings $layoutSettings;
78
79    private UrlSettings $urlSettings;
80
81    /**
82     * Returns the runtime object registered under the given `core.*` key,
83     * checked against the expected class, or fails loudly when it was never
84     * registered.
85     *
86     * @template T of object
87     * @param class-string<T> $expectedClass
88     * @return T
89     */
90    private function runtimeObject(string $key, string $expectedClass): object
91    {
92        /* @mago-expect analysis:mixed-assignment - the config store is mixed by design; validated below */
93        $object = $this->config[$key] ?? null;
94        if (!$object instanceof $expectedClass) {
95            throw new \LogicException(sprintf('No %s registered under "%s".', $expectedClass, $key));
96        }
97
98        return $object;
99    }
100
101    public function setDatabase(DatabaseDriver $databaseDriver): void
102    {
103        $this->config['core.database'] = $databaseDriver;
104    }
105
106    /**
107     * Sets the Monolog logger instance, logs into a normal logfile
108     */
109    public function setLogger(): void
110    {
111        $this->logger = new Logger(name: 'phpmyfaq');
112        $this->logger->pushHandler(
113            new StreamHandler(PMF_LOG_DIR, Environment::isDebugMode() ? Level::Debug : Level::Warning),
114        );
115        $this->logger->pushHandler(new BrowserConsoleHandler());
116    }
117
118    public function getLogger(): Logger
119    {
120        return $this->logger;
121    }
122
123    /**
124     * Sets one single configuration item.
125     */
126    public function set(string $key, mixed $value): bool
127    {
128        $result = $this->configurationRepository->updateConfigValue($key, (string) $value);
129        if ($result) {
130            $this->config[$key] = (string) $value;
131        }
132
133        return $result;
134    }
135
136    /**
137     * Returns the DatabaseDriver object.
138     */
139    public function getDb(): DatabaseDriver
140    {
141        return $this->runtimeObject('core.database', DatabaseDriver::class);
142    }
143
144    /**
145     * Sets the Instance object.
146     */
147    public function setInstance(Instance $instance): void
148    {
149        $this->config['core.instance'] = $instance;
150    }
151
152    /**
153     * Returns the Instance object.
154     */
155    public function getInstance(): Instance
156    {
157        return $this->runtimeObject('core.instance', Instance::class);
158    }
159
160    /**
161     * Sets the Language object.
162     */
163    public function setLanguage(Language $language): void
164    {
165        $this->config['core.language'] = $language;
166    }
167
168    /**
169     * Returns the Language object.
170     */
171    public function getLanguage(): Language
172    {
173        return $this->runtimeObject('core.language', Language::class);
174    }
175
176    /**
177     * Sets the Service Container.
178     */
179    public function setContainer(mixed $container): void
180    {
181        $this->config['core.container'] = $container;
182    }
183
184    /**
185     * Returns the registered service container, or null when none is set.
186     */
187    public function getServiceContainer(): ?\Psr\Container\ContainerInterface
188    {
189        $container = $this->config['core.container'] ?? null;
190
191        return $container instanceof \Psr\Container\ContainerInterface ? $container : null;
192    }
193
194    /**
195     * Returns the default language.
196     */
197    public function getDefaultLanguage(): string
198    {
199        if (!array_key_exists('main.language', $this->config) || $this->config['main.language'] === null) {
200            return 'en';
201        }
202
203        return str_replace(['language_', '.php'], replace: '', subject: (string) $this->config['main.language']);
204    }
205
206    /**
207     * Returns the current version
208     */
209    public function getVersion(): string
210    {
211        return (string) $this->config['main.currentVersion'];
212    }
213
214    /**
215     * Returns the title of the FAQ installation
216     */
217    public function getTitle(): string
218    {
219        return (string) $this->config['main.titleFAQ'];
220    }
221
222    /**
223     * Returns the email address of the main admin
224     */
225    public function getAdminEmail(): string
226    {
227        return (string) $this->config['main.administrationMail'];
228    }
229
230    public function getTemplateSet(): string
231    {
232        return $this->layoutSettings->getTemplateSet();
233    }
234
235    /**
236     * Returns the email address of the no-reply sender
237     */
238    public function getNoReplyEmail(): string
239    {
240        return $this->mailSettings->getNoReplyEmail();
241    }
242
243    public function getMailProvider(): string
244    {
245        return $this->mailSettings->getProvider();
246    }
247
248    /**
249     * Returns the default URL of the phpMyFAQ installation.
250     */
251    public function getDefaultUrl(): string
252    {
253        return $this->urlSettings->getDefaultUrl();
254    }
255
256    public function getRootPath(): string
257    {
258        return (string) PMF_ROOT_DIR;
259    }
260
261    /**
262     * Returns a configuration item.
263     *
264     * @param string $item Configuration item
265     */
266    public function get(string $item): mixed
267    {
268        if (!array_key_exists($item, $this->config) || $this->config[$item] === null) {
269            $this->getAll();
270        }
271
272        if (array_key_exists($item, $this->config) && $this->config[$item] !== null) {
273            return match ($this->config[$item]) {
274                'true' => true,
275                'false' => false,
276                default => $this->config[$item],
277            };
278        }
279
280        return null;
281    }
282
283    /**
284     * Fetches and returns all configuration items into an array. The result
285     * also carries the runtime objects registered under `core.*` keys.
286     *
287     * @return array<string, mixed>
288     */
289    public function getAll(): array
290    {
291        $rows = $this->configurationRepository->fetchAll();
292        foreach ($rows as $row) {
293            $this->config[(string) $row->config_name] = $row->config_value === null
294                ? null
295                : (string) $row->config_value;
296        }
297
298        return $this->config;
299    }
300
301    /**
302     * Sets the LDAP configuration.
303     */
304    public function setLdapConfig(LdapConfiguration $ldapConfiguration): void
305    {
306        $this->ldapServer = $this->ldapSettings->buildServers($ldapConfiguration);
307        $this->ldapConfig = $this->ldapSettings->buildConfig();
308    }
309
310    /**
311     * Returns the LDAP mapping configuration.
312     *
313     * @return string[]
314     */
315    public function getLdapMapping(): array
316    {
317        return $this->ldapSettings->getLdapMapping();
318    }
319
320    /**
321     * Returns the LDAP options configuration.
322     *
323     * @return array<string, mixed>
324     */
325    public function getLdapOptions(): array
326    {
327        return $this->ldapSettings->getLdapOptions();
328    }
329
330    /**
331     * Returns the LDAP group configuration.
332     *
333     * @return array<string, mixed>
334     */
335    public function getLdapGroupConfig(): array
336    {
337        return $this->ldapSettings->getLdapGroupConfig();
338    }
339
340    /**
341     * Returns the LDAP configuration.
342     *
343     * @return array<string, mixed>
344     */
345    public function getLdapConfig(): array
346    {
347        return $this->ldapConfig;
348    }
349
350    /**
351     * Returns the LDAP server(s).
352     *
353     * @return array<int, array<string, mixed>>
354     */
355    public function getLdapServer(): array
356    {
357        return $this->ldapServer;
358    }
359
360    public function isLdapActive(): bool
361    {
362        return $this->ldapSettings->isActive();
363    }
364
365    public function isElasticsearchActive(): bool
366    {
367        return $this->searchSettings->isElasticsearchActive();
368    }
369
370    public function isSignInWithMicrosoftActive(): bool
371    {
372        return $this->securitySettings->isSignInWithMicrosoftActive();
373    }
374
375    public function isSignInWithKeycloakActive(): bool
376    {
377        return $this->securitySettings->isSignInWithKeycloakActive();
378    }
379
380    /**
381     * Sets the Elasticsearch client instance.
382     */
383    public function setElasticsearch(Client $esClient): void
384    {
385        $this->config['core.elasticsearch'] = $esClient;
386    }
387
388    /**
389     * Returns the Elasticsearch client instance.
390     */
391    public function getElasticsearch(): Client
392    {
393        return $this->runtimeObject('core.elasticsearch', Client::class);
394    }
395
396    public function setOpenSearch(\OpenSearch\Client $osClient): void
397    {
398        $this->config['core.opensearch'] = $osClient;
399    }
400
401    public function getOpenSearch(): \OpenSearch\Client
402    {
403        return $this->runtimeObject('core.opensearch', \OpenSearch\Client::class);
404    }
405
406    /**
407     * Sets the Elasticsearch configuration.
408     */
409    public function setElasticsearchConfig(ElasticsearchConfiguration $elasticsearchConfiguration): void
410    {
411        $this->config['core.elasticsearchConfig'] = $elasticsearchConfiguration;
412    }
413
414    /**
415     * Returns the Elasticsearch configuration.
416     */
417    public function getElasticsearchConfig(): ElasticsearchConfiguration
418    {
419        return $this->runtimeObject('core.elasticsearchConfig', ElasticsearchConfiguration::class);
420    }
421
422    public function setOpenSearchConfig(OpenSearchConfiguration $openSearchConfiguration): void
423    {
424        $this->config['core.openSearchConfig'] = $openSearchConfiguration;
425    }
426
427    public function getOpenSearchConfig(): OpenSearchConfiguration
428    {
429        return $this->runtimeObject('core.openSearchConfig', OpenSearchConfiguration::class);
430    }
431
432    /**
433     * Sets the Translation Provider instance.
434     */
435    public function setTranslationProvider(TranslationProviderInterface $provider): void
436    {
437        $this->config['core.translationProvider'] = $provider;
438    }
439
440    /**
441     * Returns the Translation Provider instance.
442     */
443    public function getTranslationProvider(): ?TranslationProviderInterface
444    {
445        // Lazy initialization: If not set yet and configuration exists, try to initialize
446        if (
447            (
448                !array_key_exists('core.translationProvider', $this->config)
449                || $this->config['core.translationProvider'] === null
450            )
451            && $this->get('translation.provider') !== 'none'
452        ) {
453            $this->initializeTranslationProvider();
454        }
455
456        /* @mago-expect analysis:mixed-assignment - the config store is mixed by design; validated below */
457        $provider = $this->config['core.translationProvider'] ?? null;
458
459        return $provider instanceof TranslationProviderInterface ? $provider : null;
460    }
461
462    /**
463     * Initialize the translation provider based on configuration.
464     */
465    private function initializeTranslationProvider(): void
466    {
467        try {
468            // Get HTTP client from service container if available
469            /* @mago-expect analysis:mixed-assignment - the config store is mixed by design; validated below */
470            $container = $this->config['core.container'] ?? null;
471            if ($container instanceof ContainerInterface && $container->has('phpmyfaq.http-client')) {
472                $httpClient = $container->get('phpmyfaq.http-client');
473                if (!$httpClient instanceof HttpClientInterface) {
474                    return;
475                }
476
477                $provider = TranslationProviderFactory::create($this, $httpClient);
478                if ($provider !== null) {
479                    $this->config['core.translationProvider'] = $provider;
480                }
481            }
482        } catch (\Exception $e) {
483            $this->getLogger()->error('Failed to initialize translation provider: ' . $e->getMessage());
484        }
485    }
486
487    /**
488     * Adds a configuration item for the database.
489     */
490    public function add(string $name, mixed $value): bool
491    {
492        if (!array_key_exists($name, $this->config) || $this->config[$name] === null) {
493            return $this->configurationRepository->insert($name, (string) $value);
494        }
495
496        return true;
497    }
498
499    /**
500     * Deletes a configuration item for the database.
501     */
502    public function delete(string $name): bool
503    {
504        return $this->configurationRepository->delete($name);
505    }
506
507    /**
508     * Renames a configuration key for the database.
509     */
510    public function rename(string $currentKey, string $newKey): bool
511    {
512        return $this->configurationRepository->renameKey($currentKey, $newKey);
513    }
514
515    /**
516     * Updates all configuration items.
517     *
518     * @param array<string, string|null> $newConfigs Array with new configuration values
519     */
520    public function update(array $newConfigs): bool
521    {
522        $runtimeConfigs = [
523            'core.database',
524            'core.instance',
525            'core.language',
526            'core.ldapServer',
527            'core.ldapConfig',
528            'core.elasticsearch',
529            'core.opensearch',
530            'core.elasticsearchConfig',
531            'core.openSearchConfig',
532            'core.translationProvider',
533            'core.pluginManager',
534            'core.container',
535        ];
536
537        // Internal updater state that must never be writable through the bulk
538        // configuration API. The legitimate updater writes this via set() after
539        // verifying the downloaded package; allowing it here would let an admin
540        // point the updater at an arbitrary, unverified ZIP (RCE).
541        $protectedConfigs = [
542            'upgrade.lastDownloadedPackage',
543        ];
544
545        foreach ($newConfigs as $name => $value) {
546            if (
547                !(
548                    !hash_equals($name, user_string: 'main.phpMyFAQToken')
549                    && !in_array($name, $runtimeConfigs, strict: true)
550                    && !in_array($name, $protectedConfigs, strict: true)
551                )
552            ) {
553                continue;
554            }
555
556            $this->configurationRepository->updateConfigValue($name, $value ?? '');
557            if (array_key_exists($name, $this->config) && $this->config[$name] !== null) {
558                unset($this->config[$name]);
559            }
560        }
561
562        return true;
563    }
564
565    /**
566     * Updates main.referenceUrl in media objects in faqs.
567     *
568     * @param string $oldUrl Old main.referenceUrl
569     * @param string $newUrl New main.referenceUrl
570     * @return bool true|false
571     */
572    public function replaceMainReferenceUrl(string $oldUrl, string $newUrl): bool
573    {
574        $contentItems = $this->configurationRepository->getFaqDataContents();
575
576        foreach ($contentItems as $contentItem) {
577            $content = (string) $contentItem->content;
578            if (!str_contains($content, $oldUrl)) {
579                continue;
580            }
581
582            $this->configurationRepository->updateFaqDataContentById(
583                (int) $contentItem->id,
584                (string) $contentItem->lang,
585                str_replace($oldUrl, $newUrl, $content),
586            );
587        }
588
589        return true;
590    }
591
592    /**
593     * Returns an array with allowed media hosts for records
594     *
595     * @return string[]
596     */
597    public function getAllowedMediaHosts(): array
598    {
599        return $this->urlSettings->getAllowedMediaHosts();
600    }
601
602    public function getCustomCss(): string
603    {
604        return $this->layoutSettings->getCustomCss();
605    }
606
607    /**
608     * @throws PluginException
609     */
610    public function setPluginManager(): Configuration
611    {
612        $this->pluginManager = new PluginManager();
613        $this->pluginManager->loadPlugins();
614
615        $this->config['core.pluginManager'] = $this->pluginManager;
616        return $this;
617    }
618
619    public function getPluginManager(): PluginManager
620    {
621        return $this->runtimeObject('core.pluginManager', PluginManager::class);
622    }
623
624    public function triggerEvent(string $eventName, mixed $data = null): void
625    {
626        $this->pluginManager->triggerEvent($eventName, $data);
627    }
628
629    public function getPluginConfig(string $pluginName): ?PluginConfigurationInterface
630    {
631        return $this->pluginManager->getPluginConfig($pluginName);
632    }
633}