Skip to content

Commit db60c57

Browse files
update
1 parent 16f3dee commit db60c57

File tree

3 files changed

+126
-164
lines changed

3 files changed

+126
-164
lines changed

Capsule/Artisan.php

Lines changed: 4 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tamedevelopers\Support\Capsule\Logger;
88
use Tamedevelopers\Support\Capsule\Manager;
99
use Tamedevelopers\Support\Capsule\Traits\ArtisanTrait;
10+
use Tamedevelopers\Support\Capsule\Traits\ArtisanDiscovery;
1011

1112
/**
1213
* Minimal artisan-like dispatcher for Tamedevelopers Support
@@ -19,7 +20,8 @@
1920
*/
2021
class Artisan
2122
{
22-
use ArtisanTrait;
23+
use ArtisanTrait,
24+
ArtisanDiscovery;
2325

2426
/**
2527
* Command registry
@@ -42,6 +44,7 @@ public function __construct()
4244
{
4345
// Ensure environment variables are loaded before accessing them
4446
Manager::startEnvIFNotStarted();
47+
4548
// Auto-discover external commands from installed packages
4649
$this->discoverExternal();
4750
}
@@ -201,167 +204,4 @@ private function renderList(): void
201204
}
202205
}
203206

204-
/**
205-
* Discover commands/providers from installed Composer packages.
206-
* Convention:
207-
* - extra.tamedevelopers.providers: string[] FQCNs implementing CommandProviderInterface
208-
* - extra.tamedevelopers.commands: array<string, string|array> e.g. "db:migrate": "Vendor\\Pkg\\MigrateCommand@handle"
209-
*/
210-
private function discoverExternal(): void
211-
{
212-
if (self::$discovered) {
213-
return;
214-
}
215-
self::$discovered = true;
216-
217-
$installedPath = $this->resolveInstalledJsonPath();
218-
if (!$installedPath || !is_file($installedPath)) {
219-
return;
220-
}
221-
222-
$json = @file_get_contents($installedPath);
223-
if ($json === false) {
224-
return;
225-
}
226-
227-
$data = json_decode($json, true);
228-
if (!is_array($data)) {
229-
return;
230-
}
231-
232-
$packages = $this->extractPackages($data);
233-
234-
foreach ($packages as $pkg) {
235-
$extra = $pkg['extra']['tamedevelopers'] ?? null;
236-
if (!$extra) {
237-
continue;
238-
}
239-
240-
// 1) Providers
241-
$providers = $extra['providers'] ?? [];
242-
foreach ((array) $providers as $fqcn) {
243-
if (\is_string($fqcn) && \class_exists($fqcn)) {
244-
try {
245-
$provider = new $fqcn();
246-
if (\method_exists($provider, 'register')) {
247-
$provider->register($this);
248-
}
249-
} catch (\Throwable $e) {
250-
// skip provider instantiation errors silently to avoid breaking CLI
251-
}
252-
}
253-
}
254-
255-
// 2) Direct command map
256-
$map = $extra['commands'] ?? [];
257-
$descriptions = $extra['descriptions'] ?? [];
258-
foreach ($map as $name => $target) {
259-
$desc = $descriptions[$name] ?? '';
260-
$this->registerFromTarget((string) $name, $target, $desc);
261-
}
262-
}
263-
}
264-
265-
/**
266-
* Handle different shapes of installed.json across Composer versions.
267-
*/
268-
private function extractPackages(array $data): array
269-
{
270-
// Composer 2: {"packages":[...]} or multi-vendor arrays
271-
if (isset($data['packages']) && is_array($data['packages'])) {
272-
return $data['packages'];
273-
}
274-
if (isset($data[0]['packages'])) {
275-
$merged = [];
276-
foreach ($data as $block) {
277-
if (isset($block['packages']) && is_array($block['packages'])) {
278-
$merged = array_merge($merged, $block['packages']);
279-
}
280-
}
281-
return $merged;
282-
}
283-
284-
// Some vendors put flat arrays
285-
if (isset($data['versions']) && is_array($data['versions'])) {
286-
$out = [];
287-
foreach ($data['versions'] as $name => $info) {
288-
if (is_array($info)) {
289-
$info['name'] = $name;
290-
$out[] = $info;
291-
}
292-
}
293-
return $out;
294-
}
295-
296-
// Fallback: maybe already an array of packages
297-
return is_array($data) ? $data : [];
298-
}
299-
300-
/**
301-
* Accepts:
302-
* - "Vendor\\Pkg\\Cmd@handle"
303-
* - "Vendor\\Pkg\\Cmd" (instance with handle|__invoke)
304-
* - ["Vendor\\Pkg\\Cmd", "method"]
305-
*/
306-
private function registerFromTarget(string $name, $target, string $description = ''): void
307-
{
308-
// "Class@method"
309-
if (is_string($target) && str_contains($target, '@')) {
310-
[$class, $method] = explode('@', $target, 2);
311-
if (class_exists($class)) {
312-
try {
313-
$instance = new $class();
314-
$this->register($name, [$instance, $method], $description);
315-
} catch (\Throwable $e) {
316-
// skip
317-
}
318-
}
319-
return;
320-
}
321-
322-
// FQCN string (assumes instance + handle/__invoke)
323-
if (is_string($target) && class_exists($target)) {
324-
try {
325-
$instance = new $target();
326-
$this->register($name, $instance, $description);
327-
} catch (\Throwable $e) {
328-
// skip
329-
}
330-
return;
331-
}
332-
333-
// ["Class", "method"]
334-
if (is_array($target) && isset($target[0], $target[1]) && is_string($target[0]) && class_exists($target[0])) {
335-
try {
336-
$instance = new $target[0]();
337-
$this->register($name, [$instance, (string) $target[1]], $description);
338-
} catch (\Throwable $e) {
339-
// skip
340-
}
341-
return;
342-
}
343-
344-
// Callable (rare via extra, but supported)
345-
if (is_callable($target)) {
346-
$this->register($name, $target, $description);
347-
}
348-
}
349-
350-
/**
351-
* Find vendor/composer/installed.json reliably relative to this package.
352-
*/
353-
private function resolveInstalledJsonPath(): ?string
354-
{
355-
// This file is .../Tamedevelopers/Support/Capsule/Artisan.php inside a project root.
356-
// We want the consumer application's vendor/composer/installed.json.
357-
$projectRoot = \dirname(__DIR__, 2); // .../Tamedevelopers/Support
358-
$vendorPath = $projectRoot . DIRECTORY_SEPARATOR . 'vendor';
359-
if (!is_dir($vendorPath)) {
360-
// Fallback for when this file is inside vendor/tamedevelopers/support
361-
$supportRoot = \dirname(__DIR__, 1); // .../support (current package root)
362-
$vendorRoot = \dirname($supportRoot, 2); // .../vendor
363-
$vendorPath = $vendorRoot;
364-
}
365-
return $vendorPath . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'installed.json';
366-
}
367207
}

Capsule/CommandProviderInterface.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*/
2222
interface CommandProviderInterface
2323
{
24+
2425
/**
2526
* Register package commands into the shared Artisan registry.
2627
*/
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tamedevelopers\Support\Capsule\Traits;
6+
7+
use Tamedevelopers\Support\Capsule\File;
8+
9+
10+
/**
11+
* @property static $discovered
12+
*/
13+
trait ArtisanDiscovery
14+
{
15+
16+
/**
17+
* Discover providers from installed Composer packages.
18+
* Convention:
19+
* - extra.tamedevelopers.providers: string[] FQCNs implementing CommandProviderInterface
20+
*/
21+
private function discoverExternal(): void
22+
{
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)) {
40+
return;
41+
}
42+
43+
$packages = $this->extractPackages($data);
44+
45+
foreach ($packages as $pkg) {
46+
$extra = $pkg['extra']['tamedevelopers'] ?? null;
47+
if (!$extra) {
48+
continue;
49+
}
50+
51+
// 1) Providers
52+
$providers = $extra['providers'] ?? [];
53+
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
62+
}
63+
}
64+
}
65+
}
66+
}
67+
68+
/**
69+
* Handle different shapes of installed.json across Composer versions.
70+
*/
71+
private function extractPackages(array $data): array
72+
{
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+
}
86+
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;
97+
}
98+
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;
117+
}
118+
return $vendorPath . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'installed.json';
119+
}
120+
121+
}

0 commit comments

Comments
 (0)