77use WP_CLI \Dispatcher \CommandAddition ;
88use WP_CLI \Dispatcher \CommandFactory ;
99use WP_CLI \Dispatcher \DisabledCommand ;
10+ use WP_CLI \Dispatcher \LazyCommand ;
1011use WP_CLI \Dispatcher \CommandNamespace ;
1112use WP_CLI \Dispatcher \CompositeCommand ;
1213use WP_CLI \Dispatcher \RootCommand ;
@@ -470,6 +471,27 @@ private static function wp_hook_build_unique_id( $function ) {
470471 return false ;
471472 }
472473
474+ /**
475+ * Check whether a class is loaded, or can be located by a Composer autoloader, without loading it.
476+ *
477+ * @param string $class_name Class name.
478+ * @return bool
479+ */
480+ private static function is_class_loadable_without_autoloading ( $ class_name ) {
481+ if ( class_exists ( $ class_name , false ) ) {
482+ return true ;
483+ }
484+
485+ foreach ( spl_autoload_functions () ?: [] as $ autoloader ) {
486+ if ( is_array ( $ autoloader ) && $ autoloader [0 ] instanceof \Composer \Autoload \ClassLoader
487+ && $ autoloader [0 ]->findFile ( ltrim ( $ class_name , '\\' ) ) ) {
488+ return true ;
489+ }
490+ }
491+
492+ return false ;
493+ }
494+
473495 /**
474496 * Register a command to WP-CLI.
475497 *
@@ -529,8 +551,20 @@ public static function add_command( $name, $callable, $args = [] ) {
529551 return false ;
530552 }
531553
554+ // Commands registered by class name are only loaded once they are actually needed,
555+ // unless arguments are passed that require inspecting the command right away.
556+ $ is_lazy = is_string ( $ callable )
557+ && false === strpos ( $ callable , ':: ' )
558+ && ! function_exists ( $ callable )
559+ && self ::is_class_loadable_without_autoloading ( $ callable )
560+ && ! array_intersect ( [ 'shortdesc ' , 'longdesc ' , 'synopsis ' , 'when ' ], array_keys ( $ args ) )
561+ && ! getenv ( 'WP_CLI_DISABLE_LAZY_COMMANDS ' );
562+
532563 $ valid = false ;
533- if ( is_callable ( $ callable ) ) {
564+ if ( $ is_lazy ) {
565+ // Validated once the command is materialized.
566+ $ valid = true ;
567+ } elseif ( is_callable ( $ callable ) ) {
534568 $ valid = true ;
535569 } elseif ( is_string ( $ callable ) && class_exists ( $ callable ) ) {
536570 $ valid = true ;
@@ -575,7 +609,12 @@ public static function add_command( $name, $callable, $args = [] ) {
575609 while ( ! empty ( $ path ) ) {
576610 $ subcommand_name = $ path [0 ];
577611 $ parent = implode ( ' ' , $ path );
578- $ subcommand = $ command ->find_subcommand ( $ path );
612+ $ subcommand = $ command ->get_registered_subcommand ( $ subcommand_name );
613+ if ( $ subcommand ) {
614+ array_shift ( $ path );
615+ } else {
616+ $ subcommand = $ command ->find_subcommand ( $ path );
617+ }
579618
580619 // Parent not found. Defer addition or create an empty container as
581620 // needed.
@@ -614,6 +653,29 @@ public static function add_command( $name, $callable, $args = [] ) {
614653 $ command = $ subcommand ;
615654 }
616655
656+ $ existing_command = $ command ->get_registered_subcommand ( $ leaf_name );
657+
658+ if ( $ is_lazy && ! $ addition ->was_aborted () && ( ! $ existing_command || $ existing_command instanceof LazyCommand ) ) {
659+ /** @var class-string $callable */
660+ if ( $ existing_command instanceof LazyCommand ) {
661+ $ existing_command ->add_class ( $ callable );
662+ } else {
663+ $ command ->add_subcommand ( $ leaf_name , new LazyCommand ( $ command , $ leaf_name , $ callable ) );
664+ }
665+
666+ self ::debug ( "Registering lazy command: {$ name }" , 'commands ' );
667+
668+ /**
669+ * Action triggered after a command has been added to WP-CLI.
670+ */
671+ self ::do_hook ( "after_add_command: {$ name }" );
672+ return true ;
673+ }
674+
675+ if ( $ is_lazy && ! class_exists ( $ callable ) ) {
676+ self ::error ( sprintf ( 'Callable %s does not exist, and cannot be registered as `wp %s`. ' , (string ) json_encode ( $ callable ), $ name ) );
677+ }
678+
617679 $ leaf_command = CommandFactory::create ( $ leaf_name , $ callable , $ command );
618680
619681 if ( $ addition ->was_aborted () ) {
@@ -622,13 +684,16 @@ public static function add_command( $name, $callable, $args = [] ) {
622684
623685 // Only add a command namespace if the command itself does not exist yet.
624686 if ( $ leaf_command instanceof CommandNamespace
625- && array_key_exists ( $ leaf_name , $ command ->get_subcommands () ) ) {
687+ && false !== $ command ->get_registered_subcommand ( $ leaf_name ) ) {
626688 return false ;
627689 }
628690
629691 // Reattach commands attached to namespace to real command.
630692 $ subcommand_name = (array ) $ leaf_name ;
631- $ existing_command = $ command ->find_subcommand ( $ subcommand_name );
693+ $ existing_command = $ command ->get_registered_subcommand ( $ leaf_name );
694+ if ( $ existing_command instanceof LazyCommand ) {
695+ $ existing_command = $ command ->find_subcommand ( $ subcommand_name );
696+ }
632697 if ( $ existing_command instanceof CompositeCommand && $ existing_command ->can_have_subcommands () ) {
633698 if ( $ leaf_command instanceof CommandNamespace || ! $ leaf_command ->can_have_subcommands () ) {
634699 $ command_to_keep = $ existing_command ;
0 commit comments