@@ -65,27 +65,20 @@ public function preAutoloadDump(Event $event): void
6565
6666 $ root = dirname (realpath ($ event ->getComposer ()->getConfig ()->get ('vendor-dir ' ))) . '/ ' ;
6767 foreach ($ extra ['plugin-paths ' ] as $ pluginsPath ) {
68- if (!is_dir ($ root . $ pluginsPath )) {
68+ $ pluginPath = $ root . $ pluginsPath ;
69+ if (!is_dir ($ pluginPath )) {
6970 continue ;
7071 }
71- foreach (new DirectoryIterator ( $ root . $ pluginsPath ) as $ fileInfo ) {
72- if (! $ fileInfo -> isDir () || $ fileInfo -> isDot ()) {
73- continue ;
74- }
72+ foreach ($ this -> findAppPlugins ( $ pluginPath ) as $ pluginName => $ pluginPath ) {
73+ $ namespace = str_replace ( ' / ' , '\\' , $ pluginName ) . '\\' ;
74+ $ testNamespace = $ namespace . ' Test \\' ;
75+ $ path = $ this -> getRelativePath ( $ pluginPath , $ root );
7576
76- $ folderName = $ fileInfo ->getFilename ();
77- if ($ folderName [0 ] === '. ' ) {
78- continue ;
77+ if (!isset ($ autoload ['psr-4 ' ][$ namespace ])) {
78+ $ autoload ['psr-4 ' ][$ namespace ] = $ path . '/src ' ;
7979 }
80-
81- $ pluginNamespace = $ folderName . '\\' ;
82- $ pluginTestNamespace = $ folderName . '\\Test \\' ;
83- $ path = $ pluginsPath . '/ ' . $ folderName . '/ ' ;
84- if (!isset ($ autoload ['psr-4 ' ][$ pluginNamespace ]) && is_dir ($ root . $ path . '/src ' )) {
85- $ autoload ['psr-4 ' ][$ pluginNamespace ] = $ path . 'src ' ;
86- }
87- if (!isset ($ devAutoload ['psr-4 ' ][$ pluginTestNamespace ]) && is_dir ($ root . $ path . '/tests ' )) {
88- $ devAutoload ['psr-4 ' ][$ pluginTestNamespace ] = $ path . 'tests ' ;
80+ if (!isset ($ devAutoload ['psr-4 ' ][$ testNamespace ]) && is_dir ($ pluginPath . '/tests ' )) {
81+ $ devAutoload ['psr-4 ' ][$ testNamespace ] = $ path . '/tests ' ;
8982 }
9083 }
9184 }
@@ -154,28 +147,102 @@ public function findPlugins(
154147
155148 foreach ($ pluginDirs as $ path ) {
156149 $ path = $ this ->getFullPath ($ path , $ vendorDir );
157- if (is_dir ($ path )) {
158- $ dir = new DirectoryIterator ($ path );
159- foreach ($ dir as $ info ) {
160- if (!$ info ->isDir () || $ info ->isDot ()) {
161- continue ;
162- }
163-
164- $ name = $ info ->getFilename ();
165- if ($ name [0 ] === '. ' ) {
166- continue ;
167- }
168-
169- $ plugins [$ name ] = $ path . DIRECTORY_SEPARATOR . $ name ;
170- }
150+ if (!is_dir ($ path )) {
151+ continue ;
171152 }
153+ $ plugins += $ this ->findAppPlugins ($ path , true );
172154 }
173155
174156 ksort ($ plugins );
175157
176158 return $ plugins ;
177159 }
178160
161+ /**
162+ * Find application plugins in a plugin path.
163+ *
164+ * Supports both `plugins/MyPlugin/src` and `plugins/Vendor/Plugin/src`.
165+ * When requested, top-level directories with no plugin children are kept
166+ * for backward compatibility.
167+ *
168+ * @param string $path The absolute plugin path.
169+ * @param bool $keepLegacyDirectories Whether to keep legacy top-level entries.
170+ * @return array<string, string>
171+ */
172+ protected function findAppPlugins (string $ path , bool $ keepLegacyDirectories = false ): array
173+ {
174+ $ plugins = [];
175+
176+ foreach (new DirectoryIterator ($ path ) as $ info ) {
177+ if ($ this ->shouldSkipDirectory ($ info )) {
178+ continue ;
179+ }
180+
181+ $ name = $ info ->getFilename ();
182+ $ pluginPath = $ info ->getPathname ();
183+ if ($ this ->isPluginDirectory ($ pluginPath )) {
184+ $ plugins [$ name ] = $ pluginPath ;
185+
186+ continue ;
187+ }
188+
189+ $ vendorPlugins = [];
190+ foreach (new DirectoryIterator ($ pluginPath ) as $ subInfo ) {
191+ if ($ this ->shouldSkipDirectory ($ subInfo )) {
192+ continue ;
193+ }
194+
195+ $ subName = $ subInfo ->getFilename ();
196+ $ subPluginPath = $ subInfo ->getPathname ();
197+ if ($ this ->isPluginDirectory ($ subPluginPath )) {
198+ $ vendorPlugins [$ name . '/ ' . $ subName ] = $ subPluginPath ;
199+ }
200+ }
201+
202+ if ($ vendorPlugins ) {
203+ $ plugins += $ vendorPlugins ;
204+
205+ continue ;
206+ }
207+ if ($ keepLegacyDirectories ) {
208+ $ plugins [$ name ] = $ pluginPath ;
209+ }
210+ }
211+
212+ return $ plugins ;
213+ }
214+
215+ /**
216+ * @param \DirectoryIterator $info Directory iterator entry.
217+ * @return bool
218+ */
219+ protected function shouldSkipDirectory (DirectoryIterator $ info ): bool
220+ {
221+ return !$ info ->isDir () || $ info ->isDot () || $ info ->getFilename ()[0 ] === '. ' ;
222+ }
223+
224+ /**
225+ * @param string $path Directory path.
226+ * @return bool
227+ */
228+ protected function isPluginDirectory (string $ path ): bool
229+ {
230+ return is_dir ($ path . DIRECTORY_SEPARATOR . 'src ' );
231+ }
232+
233+ /**
234+ * @param string $path Absolute plugin path.
235+ * @param string $root Absolute application root path.
236+ * @return string
237+ */
238+ protected function getRelativePath (string $ path , string $ root ): string
239+ {
240+ $ path = str_replace ('\\' , '/ ' , $ path );
241+ $ root = str_replace ('\\' , '/ ' , $ root );
242+
243+ return trim (substr ($ path , strlen ($ root )), '/ ' );
244+ }
245+
179246 /**
180247 * Turns relative paths in full paths.
181248 *
0 commit comments