66
77use Tamedevelopers \Support \Capsule \File ;
88
9-
109/**
11- * @property static $discovered
10+ * Discovers and registers external command providers declared by packages.
11+ *
12+ * Convention in each package's composer.json:
13+ * {
14+ * "extra": {
15+ * "tamedevelopers": {
16+ * "providers": [
17+ * "Vendor\\Package\\Console\\MyCommands"
18+ * ]
19+ * }
20+ * }
21+ * }
1222 */
1323trait ArtisanDiscovery
1424{
15-
1625 /**
17- * Discover providers from installed Composer packages.
18- * Convention:
19- * - extra.tamedevelopers.providers: string[] FQCNs implementing CommandProviderInterface
26+ * Track providers that have been registered to avoid duplicate registration.
27+ */
28+ private static array $ registeredProviders = [];
29+
30+ /**
31+ * Discover providers by scanning vendor composer.json
32+ * No reliance on composer/composer installed.json or installed.php.
2033 */
2134 private function discoverExternal (): void
2235 {
23- if (self ::$ discovered ) {
24- return ;
25- }
26- self ::$ discovered = true ;
27-
28- $ installedPath = $ this ->resolveInstalledJsonPath ();
29- if (!$ installedPath || !is_file ($ installedPath )) {
30- return ;
31- }
32-
33- $ json = File::get ($ installedPath );
34- if ($ json === false ) {
35- return ;
36- }
37-
38- $ data = json_decode ($ json , true );
39- if (!is_array ($ data )) {
36+ $ vendorPath = $ this ->resolveVendorPath ();
37+ if (!$ vendorPath || !is_dir ($ vendorPath )) {
4038 return ;
4139 }
4240
43- $ packages = $ this ->extractPackages ($ data );
41+ // Scan all package composer.json files
42+ $ pattern = $ vendorPath . DIRECTORY_SEPARATOR . 'composer.json ' ;
43+ $ composerFiles = glob ($ pattern ) ?: [];
4444
45- foreach ($ packages as $ pkg ) {
46- $ extra = $ pkg ['extra ' ]['tamedevelopers ' ] ?? null ;
45+ foreach ($ composerFiles as $ composerJson ) {
46+ $ json = @File::get ($ composerJson );
47+ if ($ json === false ) {
48+ continue ;
49+ }
50+ $ meta = json_decode ($ json , true );
51+ if (!is_array ($ meta )) {
52+ continue ;
53+ }
54+ $ extra = $ meta ['extra ' ]['tamedevelopers ' ] ?? null ;
4755 if (!$ extra ) {
4856 continue ;
4957 }
50-
51- // 1) Providers
5258 $ providers = $ extra ['providers ' ] ?? [];
5359 foreach ((array ) $ providers as $ fqcn ) {
54- if (\is_string ($ fqcn ) && \class_exists ($ fqcn )) {
55- try {
56- $ provider = new $ fqcn ();
57- if (\method_exists ($ provider , 'register ' )) {
58- $ provider ->register ($ this );
59- }
60- } catch (\Throwable $ e ) {
61- // skip provider instantiation errors silently to avoid breaking CLI
60+ if (!is_string ($ fqcn ) || !class_exists ($ fqcn )) {
61+ continue ;
62+ }
63+ if (isset (self ::$ registeredProviders [$ fqcn ])) {
64+ continue ; // already registered in this process
65+ }
66+ try {
67+ $ provider = new $ fqcn ();
68+ if (method_exists ($ provider , 'register ' )) {
69+ $ provider ->register ($ this );
70+ self ::$ registeredProviders [$ fqcn ] = true ;
6271 }
72+ } catch (\Throwable $ e ) {
73+ // ignore provider instantiation/registration failures
6374 }
6475 }
6576 }
6677 }
6778
6879 /**
69- * Handle different shapes of installed.json across Composer versions .
80+ * Resolve the vendor directory path for both dev (package root) and consumer app .
7081 */
71- private function extractPackages ( array $ data ): array
82+ private function resolveVendorPath ( ): ? string
7283 {
73- // Composer 2: {"packages":[...]} or multi-vendor arrays
74- if (isset ($ data ['packages ' ]) && is_array ($ data ['packages ' ])) {
75- return $ data ['packages ' ];
76- }
77- if (isset ($ data [0 ]['packages ' ])) {
78- $ merged = [];
79- foreach ($ data as $ block ) {
80- if (isset ($ block ['packages ' ]) && is_array ($ block ['packages ' ])) {
81- $ merged = array_merge ($ merged , $ block ['packages ' ]);
82- }
83- }
84- return $ merged ;
85- }
84+ // Current file: support/Capsule/Traits/ArtisanDiscovery.php
85+ $ packageRoot = \dirname (__DIR__ , 3 ); // .../support
8686
87- // Some vendors put flat arrays
88- if (isset ($ data ['versions ' ]) && is_array ($ data ['versions ' ])) {
89- $ out = [];
90- foreach ($ data ['versions ' ] as $ name => $ info ) {
91- if (is_array ($ info )) {
92- $ info ['name ' ] = $ name ;
93- $ out [] = $ info ;
94- }
95- }
96- return $ out ;
87+ // Case 1: developing this package as the root project
88+ $ vendor = $ packageRoot . DIRECTORY_SEPARATOR . 'vendor ' ;
89+ if (is_dir ($ vendor )) {
90+ return $ vendor ;
9791 }
9892
99- // Fallback: maybe already an array of packages
100- return is_array ($ data ) ? $ data : [];
101- }
102-
103- /**
104- * Find vendor/composer/installed.json reliably relative to this package.
105- */
106- private function resolveInstalledJsonPath (): ?string
107- {
108- // This file is .../Tamedevelopers/Support/Capsule/Artisan.php inside a project root.
109- // We want the consumer application's vendor/composer/installed.json.
110- $ projectRoot = \dirname (__DIR__ , 2 ); // .../Tamedevelopers/Support
111- $ vendorPath = $ projectRoot . DIRECTORY_SEPARATOR . 'vendor ' ;
112- if (!is_dir ($ vendorPath )) {
113- // Fallback for when this file is inside vendor/tamedevelopers/support
114- $ supportRoot = \dirname (__DIR__ , 1 ); // .../support (current package root)
115- $ vendorRoot = \dirname ($ supportRoot , 2 ); // .../vendor
116- $ vendorPath = $ vendorRoot ;
93+ // Case 2: this package is installed as a dependency: project/vendor/tamedevelopers/support/...
94+ $ maybeProjectVendor = \dirname ($ packageRoot , 2 ); // .../project/vendor
95+ if (is_dir ($ maybeProjectVendor )) {
96+ return $ maybeProjectVendor ;
11797 }
118- return $ vendorPath . DIRECTORY_SEPARATOR . 'composer ' . DIRECTORY_SEPARATOR . 'installed.json ' ;
119- }
12098
121- }
99+ return null ;
100+ }
101+ }
0 commit comments