@@ -65,6 +65,12 @@ class Configurator
6565 \Traversable::class,
6666 ];
6767
68+ /** @var bool whether to auto-discover extensions from installed packages */
69+ public bool $ autoDiscovery = true ;
70+
71+ /** @var string[] extension names to exclude from auto-discovery */
72+ protected array $ excludeExtensions = [];
73+
6874 protected array $ staticParameters ;
6975 protected array $ dynamicParameters = [];
7076 protected array $ services = [];
@@ -165,6 +171,26 @@ public function addServices(array $services): static
165171 }
166172
167173
174+ /**
175+ * Disables auto-discovery of extensions from installed packages.
176+ */
177+ public function disableAutoDiscovery (): static
178+ {
179+ $ this ->autoDiscovery = false ;
180+ return $ this ;
181+ }
182+
183+
184+ /**
185+ * Excludes specific extensions from auto-discovery.
186+ */
187+ public function disableExtension (string ...$ names ): static
188+ {
189+ $ this ->excludeExtensions = array_merge ($ this ->excludeExtensions , $ names );
190+ return $ this ;
191+ }
192+
193+
168194 protected function getDefaultParameters (): array
169195 {
170196 $ trace = debug_backtrace (DEBUG_BACKTRACE_IGNORE_ARGS );
@@ -244,6 +270,78 @@ public function addConfig(string|array $config): static
244270 }
245271
246272
273+ /**
274+ * Discovers extensions from installed Composer packages.
275+ * Reads extra.nette.di-extensions from vendor/composer/installed.json
276+ * @return array<string, string|array{class: class-string, args: array}>
277+ */
278+ protected function discoverExtensions (): array
279+ {
280+ $ vendorDir = $ this ->staticParameters ['vendorDir ' ] ?? null ;
281+ if (!$ vendorDir ) {
282+ return [];
283+ }
284+
285+ $ installedJson = $ vendorDir . '/composer/installed.json ' ;
286+ if (!is_file ($ installedJson )) {
287+ return [];
288+ }
289+
290+ $ installed = @json_decode (Nette \Utils \FileSystem::read ($ installedJson ), true ); // @ may not exist
291+ if (!$ installed ) {
292+ return [];
293+ }
294+
295+ $ packages = $ installed ['packages ' ] ?? $ installed ; // Composer 2.x vs 1.x format
296+ $ extensions = [];
297+
298+ foreach ($ packages as $ package ) {
299+ $ netteExtra = $ package ['extra ' ]['nette ' ] ?? null ;
300+ if (!$ netteExtra || !isset ($ netteExtra ['di-extensions ' ])) {
301+ continue ;
302+ }
303+
304+ foreach ($ netteExtra ['di-extensions ' ] as $ name => $ definition ) {
305+ // Normalize: string "Class" → ["class" => "Class", "args" => []]
306+ if (is_string ($ definition )) {
307+ $ extensions [$ name ] = $ definition ;
308+ } else {
309+ $ extensions [$ name ] = [
310+ $ definition ['class ' ],
311+ $ definition ['args ' ] ?? [],
312+ ];
313+ }
314+ }
315+ }
316+
317+ return $ extensions ;
318+ }
319+
320+
321+ /**
322+ * Returns merged extensions (discovered + default, with excludes applied).
323+ * Default extensions take precedence over discovered ones.
324+ * @return array<string, string|array>
325+ */
326+ protected function getExtensions (): array
327+ {
328+ if (!$ this ->autoDiscovery ) {
329+ $ extensions = $ this ->defaultExtensions ;
330+ } else {
331+ // Discovered extensions first, then defaults override
332+ $ discovered = $ this ->discoverExtensions ();
333+ $ extensions = array_merge ($ discovered , $ this ->defaultExtensions );
334+ }
335+
336+ // Remove excluded extensions
337+ foreach ($ this ->excludeExtensions as $ name ) {
338+ unset($ extensions [$ name ]);
339+ }
340+
341+ return $ extensions ;
342+ }
343+
344+
247345 /**
248346 * Returns system DI container.
249347 */
@@ -301,7 +399,7 @@ public function generateContainer(DI\Compiler $compiler): void
301399 $ builder = $ compiler ->getContainerBuilder ();
302400 $ builder ->addExcludedClasses ($ this ->autowireExcludedClasses );
303401
304- foreach ($ this ->defaultExtensions as $ name => $ extension ) {
402+ foreach ($ this ->getExtensions () as $ name => $ extension ) {
305403 [$ class , $ args ] = is_string ($ extension )
306404 ? [$ extension , []]
307405 : $ extension ;
@@ -331,6 +429,8 @@ protected function generateContainerKey(): array
331429 class_exists (ClassLoader::class) // composer update
332430 ? filemtime ((new \ReflectionClass (ClassLoader::class))->getFilename ())
333431 : null ,
432+ $ this ->autoDiscovery ,
433+ $ this ->excludeExtensions ,
334434 ];
335435 }
336436
0 commit comments