@@ -31,8 +31,8 @@ class Artisan extends CommandHelper
3131 public $ cli_name ;
3232
3333 /**
34- * Registered commands map
35- * @var array<string, array{instance?: object, handler?: callable, description: string}>
34+ * Registered commands map (supports multiple providers per base command)
35+ * @var array<string, array<int, array {instance?: object, handler?: callable, description: string}> >
3636 */
3737 protected static array $ commands = [];
3838
@@ -57,16 +57,22 @@ public function __construct()
5757 */
5858 public function register (string $ name , $ handler , string $ description = '' ): void
5959 {
60+ // Ensure bucket exists for this base name
61+ if (!isset (self ::$ commands [$ name ]) || !is_array (self ::$ commands [$ name ])) {
62+ self ::$ commands [$ name ] = [];
63+ }
64+
65+ // Object instance (class-based command)
6066 if (\is_object ($ handler ) && !\is_callable ($ handler )) {
61- self ::$ commands [$ name ] = [
67+ self ::$ commands [$ name ][] = [
6268 'instance ' => $ handler ,
6369 'description ' => $ description ,
6470 ];
6571 return ;
6672 }
6773
68- // Fallback to callable handler registration
69- self ::$ commands [$ name ] = [
74+ // Callable handler registration
75+ self ::$ commands [$ name ][] = [
7076 'handler ' => $ handler ,
7177 'description ' => $ description ,
7278 ];
@@ -97,63 +103,82 @@ public function run(array $argv): int
97103 return 1 ;
98104 }
99105
100- $ entry = self ::$ commands [$ base ];
106+ // Normalize entries to array of providers for this base command
107+ $ entries = self ::$ commands [$ base ];
108+ // Normalize: if entries look like a single assoc, wrap into list
109+ if (!is_array ($ entries ) || (is_array ($ entries ) && !isset ($ entries [0 ]))) {
110+ $ entries = [$ entries ];
111+ }
101112
102113 // Parse flags/options once and pass where applicable
103114 [$ positionals , $ options ] = $ this ->parseArgs ($ rawArgs );
104115
105- // If registered with a class instance, we support subcommands and flag-to-method routing
106- if (isset ($ entry ['instance ' ]) && \is_object ($ entry ['instance ' ])) {
107- $ instance = $ entry ['instance ' ];
116+ $ exitCode = 0 ;
117+ $ handled = false ;
108118
109- // Resolve primary method to call
110- $ primaryMethod = $ sub ?: 'handle ' ;
111- if (!method_exists ($ instance , $ primaryMethod )) {
112- // command instance name
113- $ instanceName = get_class ($ instance );
114- Logger::error ("Missing method \"{$ primaryMethod }() \" in {$ instanceName }. " );
115-
116- // Show small hint for available methods on the instance (public only)
117- $ hints = $ this ->introspectPublicMethods ($ instance );
118-
119- if ( !empty ($ hints )) {
120- Logger::info ("Available methods: {$ hints }\n" );
121- }
122- return 1 ;
123- }
119+ // Resolve primary once and track unresolved flags across providers
120+ $ primaryMethod = $ sub ?: 'handle ' ;
121+ $ unresolvedFlags = array_keys ($ options );
124122
125- $ exitCode = (int ) ($ this ->invokeCommandMethod ($ instance , $ primaryMethod , $ positionals , $ options ) ?? 0 );
123+ foreach ($ entries as $ entry ) {
124+ // If registered with a class instance, support subcommands and flag-to-method routing
125+ if (isset ($ entry ['instance ' ]) && \is_object ($ entry ['instance ' ])) {
126+ $ instance = $ entry ['instance ' ];
126127
127- // Route flags as methods on the same instance
128- $ invalidFlags = [];
129- foreach ($ options as $ flag => $ value ) {
130- $ method = $ this ->optionToMethodName ($ flag );
131- // Skip if this flag matches the already-run primary method
132- if ($ method === $ primaryMethod ) {
128+ // Skip instances that don't implement the requested primary method
129+ if (!method_exists ($ instance , $ primaryMethod )) {
133130 continue ;
134131 }
135- if (method_exists ($ instance , $ method )) {
136- $ this ->invokeCommandMethod ($ instance , $ method , $ positionals , $ options , $ flag );
137- } else {
138- $ invalidFlags [] = $ flag ;
132+
133+ $ result = (int ) ($ this ->invokeCommandMethod ($ instance , $ primaryMethod , $ positionals , $ options ) ?? 0 );
134+ $ exitCode = max ($ exitCode , $ result );
135+ $ handled = true ;
136+
137+ // Route flags as methods on the same instance and mark them as resolved
138+ foreach ($ unresolvedFlags as $ i => $ flag ) {
139+ $ method = $ this ->optionToMethodName ($ flag );
140+ if ($ method === $ primaryMethod ) {
141+ unset($ unresolvedFlags [$ i ]);
142+ continue ;
143+ }
144+ if (method_exists ($ instance , $ method )) {
145+ $ this ->invokeCommandMethod ($ instance , $ method , $ positionals , $ options , $ flag );
146+ unset($ unresolvedFlags [$ i ]);
147+ }
139148 }
149+
150+ continue ;
140151 }
141152
142- if (!empty ($ invalidFlags )) {
143- Logger::error ("Invalid option/method: -- " . implode (', -- ' , $ invalidFlags ) . "\n" );
153+ // Fallback: callable handler (no subcommands/flags routing)
154+ if (isset ($ entry ['handler ' ]) && \is_callable ($ entry ['handler ' ])) {
155+ $ handler = $ entry ['handler ' ];
156+ $ result = (int ) ($ handler ($ rawArgs ) ?? 0 );
157+ $ exitCode = max ($ exitCode , $ result );
158+ $ handled = true ;
159+ continue ;
144160 }
145161
146- return $ exitCode ;
162+ // Unknown provider shape; ignore but keep iterating
163+ }
164+
165+ if (!$ handled ) {
166+ if ($ sub !== null ) {
167+ Logger::error ("Command \"{$ commandInput }\" is not defined. \n\n" );
168+ } else {
169+ Logger::error ("No valid providers handled command: {$ commandInput }\n" );
170+ }
171+ return max ($ exitCode , 1 );
147172 }
148173
149- // Fallback: callable handler (no subcommands/flags routing)
150- if (isset ($ entry ['handler ' ]) && \is_callable ($ entry ['handler ' ])) {
151- $ handler = $ entry ['handler ' ];
152- return (int ) ($ handler ($ rawArgs ) ?? 0 );
174+ // Any flags not resolved by any provider are invalid
175+ $ unresolvedFlags = array_values ($ unresolvedFlags );
176+ if (!empty ($ unresolvedFlags )) {
177+ Logger::error ("Invalid option/method: -- " . implode (', -- ' , $ unresolvedFlags ) . "\n" );
178+ $ exitCode = max ($ exitCode , 1 );
153179 }
154180
155- Logger::error ("Command not properly registered: {$ commandInput }\n" );
156- return 1 ;
181+ return $ exitCode ;
157182 }
158183
159184 /**
0 commit comments