Skip to content

Commit 2910420

Browse files
update
1 parent a0384e2 commit 2910420

File tree

2 files changed

+63
-85
lines changed

2 files changed

+63
-85
lines changed

Capsule/Artisan.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ class Artisan
3636
protected static array $commands = [];
3737

3838
/**
39-
* Guard to ensure discovery runs only once per process.
39+
* Constructor
4040
*/
41-
private static bool $discovered = false;
42-
4341
public function __construct()
4442
{
4543
// Ensure environment variables are loaded before accessing them

Capsule/Traits/ArtisanDiscovery.php

Lines changed: 62 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,116 +6,96 @@
66

77
use 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
*/
1323
trait 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

Comments
 (0)