Skip to content

Commit 24e960e

Browse files
Copilotswissspidy
andcommitted
Add plugin dependencies support with --with-dependencies flag and install-dependencies command
Co-authored-by: swissspidy <[email protected]>
1 parent 9a3e927 commit 24e960e

File tree

3 files changed

+296
-1
lines changed

3 files changed

+296
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"plugin delete",
5050
"plugin get",
5151
"plugin install",
52+
"plugin install-dependencies",
5253
"plugin is-installed",
5354
"plugin list",
5455
"plugin path",
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
Feature: Plugin dependencies support
2+
3+
Background:
4+
Given an empty cache
5+
6+
@require-wp-6.5
7+
Scenario: Install plugin with dependencies using --with-dependencies flag
8+
Given a WP install
9+
10+
When I run `wp plugin install akismet`
11+
Then STDOUT should contain:
12+
"""
13+
Plugin installed successfully.
14+
"""
15+
16+
# Create a test plugin with dependencies
17+
And a wp-content/plugins/test-plugin/test-plugin.php file:
18+
"""
19+
<?php
20+
/**
21+
* Plugin Name: Test Plugin
22+
* Requires Plugins: akismet
23+
*/
24+
"""
25+
26+
When I run `wp plugin delete akismet --quiet`
27+
Then STDOUT should be empty
28+
29+
# Note: Testing with actual WP.org plugins that have dependencies would be better
30+
# but we'll test with a local plugin that declares dependencies
31+
When I run `wp plugin get test-plugin --field=requires_plugins`
32+
Then STDOUT should be:
33+
"""
34+
akismet
35+
"""
36+
37+
@require-wp-6.5
38+
Scenario: Install dependencies of an installed plugin
39+
Given a WP install
40+
41+
# Create a test plugin with dependencies
42+
And a wp-content/plugins/test-plugin/test-plugin.php file:
43+
"""
44+
<?php
45+
/**
46+
* Plugin Name: Test Plugin
47+
* Requires Plugins: akismet, hello
48+
*/
49+
"""
50+
51+
When I run `wp plugin install-dependencies test-plugin`
52+
Then STDOUT should contain:
53+
"""
54+
Installing 2 dependencies for 'test-plugin'
55+
"""
56+
And STDOUT should contain:
57+
"""
58+
Success:
59+
"""
60+
61+
When I run `wp plugin list --name=akismet --field=status`
62+
Then STDOUT should be:
63+
"""
64+
inactive
65+
"""
66+
67+
When I run `wp plugin list --name=hello --field=status`
68+
Then STDOUT should be:
69+
"""
70+
inactive
71+
"""
72+
73+
@require-wp-6.5
74+
Scenario: Install dependencies with activation
75+
Given a WP install
76+
77+
# Create a test plugin with dependencies
78+
And a wp-content/plugins/test-plugin/test-plugin.php file:
79+
"""
80+
<?php
81+
/**
82+
* Plugin Name: Test Plugin
83+
* Requires Plugins: hello
84+
*/
85+
"""
86+
87+
When I run `wp plugin install-dependencies test-plugin --activate`
88+
Then STDOUT should contain:
89+
"""
90+
Installing 1 dependency for 'test-plugin'
91+
"""
92+
And STDOUT should contain:
93+
"""
94+
Success:
95+
"""
96+
97+
When I run `wp plugin list --name=hello --field=status`
98+
Then STDOUT should be:
99+
"""
100+
active
101+
"""
102+
103+
@require-wp-6.5
104+
Scenario: Install plugin with no dependencies
105+
Given a WP install
106+
107+
# Create a test plugin without dependencies
108+
And a wp-content/plugins/test-plugin/test-plugin.php file:
109+
"""
110+
<?php
111+
/**
112+
* Plugin Name: Test Plugin No Deps
113+
*/
114+
"""
115+
116+
When I run `wp plugin install-dependencies test-plugin`
117+
Then STDOUT should contain:
118+
"""
119+
Success: Plugin 'test-plugin' has no dependencies.
120+
"""
121+
122+
@require-wp-6.5
123+
Scenario: Error when installing dependencies of non-existent plugin
124+
Given a WP install
125+
126+
When I try `wp plugin install-dependencies non-existent-plugin`
127+
Then STDERR should contain:
128+
"""
129+
Error:
130+
"""
131+
And the return code should be 1

src/Plugin_Command.php

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,9 @@ protected function filter_item_list( $items, $args ) {
10171017
* [--insecure]
10181018
* : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
10191019
*
1020+
* [--with-dependencies]
1021+
* : If set, the command will also install all required dependencies of the plugin as specified in the 'Requires Plugins' header.
1022+
*
10201023
* ## EXAMPLES
10211024
*
10221025
* # Install the latest version from wordpress.org and activate
@@ -1073,14 +1076,114 @@ protected function filter_item_list( $items, $args ) {
10731076
* Removing the old version of the plugin...
10741077
* Plugin updated successfully
10751078
* Success: Installed 1 of 1 plugins.
1079+
*
1080+
* # Install a plugin with all its dependencies
1081+
* $ wp plugin install my-plugin --with-dependencies
1082+
* Installing dependency: required-plugin-1 (1.2.3)
1083+
* Plugin installed successfully.
1084+
* Installing dependency: required-plugin-2 (2.0.0)
1085+
* Plugin installed successfully.
1086+
* Installing my-plugin (3.5.0)
1087+
* Plugin installed successfully.
1088+
* Success: Installed 3 of 3 plugins.
10761089
*/
10771090
public function install( $args, $assoc_args ) {
10781091

10791092
if ( ! is_dir( WP_PLUGIN_DIR ) ) {
10801093
wp_mkdir_p( WP_PLUGIN_DIR );
10811094
}
10821095

1083-
parent::install( $args, $assoc_args );
1096+
// If --with-dependencies is set, we need to handle dependencies
1097+
if ( Utils\get_flag_value( $assoc_args, 'with-dependencies', false ) ) {
1098+
$this->install_with_dependencies( $args, $assoc_args );
1099+
} else {
1100+
parent::install( $args, $assoc_args );
1101+
}
1102+
}
1103+
1104+
/**
1105+
* Installs plugins with their dependencies.
1106+
*
1107+
* @param array $args Plugin slugs to install.
1108+
* @param array $assoc_args Associative arguments.
1109+
*/
1110+
private function install_with_dependencies( $args, $assoc_args ) {
1111+
$all_to_install = [];
1112+
$installed_tracker = [];
1113+
1114+
// Remove with-dependencies from assoc_args to avoid infinite recursion
1115+
unset( $assoc_args['with-dependencies'] );
1116+
1117+
// Collect all plugins and their dependencies
1118+
foreach ( $args as $slug ) {
1119+
$this->collect_dependencies( $slug, $all_to_install, $installed_tracker );
1120+
}
1121+
1122+
if ( empty( $all_to_install ) ) {
1123+
WP_CLI::success( 'No plugins to install.' );
1124+
return;
1125+
}
1126+
1127+
// Install all collected plugins
1128+
parent::install( $all_to_install, $assoc_args );
1129+
}
1130+
1131+
/**
1132+
* Recursively collects all dependencies for a plugin.
1133+
*
1134+
* @param string $slug Plugin slug.
1135+
* @param array &$all_to_install Reference to array of all plugins to install.
1136+
* @param array &$installed_tracker Reference to array tracking what we've already processed.
1137+
*/
1138+
private function collect_dependencies( $slug, &$all_to_install, &$installed_tracker ) {
1139+
// Skip if already processed
1140+
if ( isset( $installed_tracker[ $slug ] ) ) {
1141+
return;
1142+
}
1143+
1144+
$installed_tracker[ $slug ] = true;
1145+
1146+
// Skip if it's a URL or zip file (can't get dependencies for those)
1147+
$is_remote = false !== strpos( $slug, '://' );
1148+
if ( $is_remote || ( pathinfo( $slug, PATHINFO_EXTENSION ) === 'zip' && is_file( $slug ) ) ) {
1149+
$all_to_install[] = $slug;
1150+
return;
1151+
}
1152+
1153+
// Get plugin dependencies from WordPress.org API
1154+
$dependencies = $this->get_plugin_dependencies( $slug );
1155+
1156+
// Recursively install dependencies first
1157+
if ( ! empty( $dependencies ) ) {
1158+
foreach ( $dependencies as $dependency_slug ) {
1159+
$this->collect_dependencies( $dependency_slug, $all_to_install, $installed_tracker );
1160+
}
1161+
}
1162+
1163+
// Add this plugin to the install list
1164+
$all_to_install[] = $slug;
1165+
}
1166+
1167+
/**
1168+
* Gets the dependencies for a plugin from WordPress.org API.
1169+
*
1170+
* @param string $slug Plugin slug.
1171+
* @return array Array of dependency slugs.
1172+
*/
1173+
private function get_plugin_dependencies( $slug ) {
1174+
$api = plugins_api( 'plugin_information', array( 'slug' => $slug ) );
1175+
1176+
if ( is_wp_error( $api ) ) {
1177+
WP_CLI::debug( "Could not fetch information for plugin '$slug': " . $api->get_error_message() );
1178+
return [];
1179+
}
1180+
1181+
// Check if requires_plugins field exists and is not empty
1182+
if ( ! empty( $api->requires_plugins ) && is_array( $api->requires_plugins ) ) {
1183+
return $api->requires_plugins;
1184+
}
1185+
1186+
return [];
10841187
}
10851188

10861189
/**
@@ -1377,6 +1480,66 @@ public function is_active( $args, $assoc_args ) {
13771480
$this->check_active( $plugin->file, $network_wide ) ? WP_CLI::halt( 0 ) : WP_CLI::halt( 1 );
13781481
}
13791482

1483+
/**
1484+
* Installs all dependencies of an installed plugin.
1485+
*
1486+
* This command is useful when you have a plugin installed that depends on other plugins,
1487+
* and you want to install those dependencies without activating the main plugin.
1488+
*
1489+
* ## OPTIONS
1490+
*
1491+
* <plugin>
1492+
* : The installed plugin to get dependencies for.
1493+
*
1494+
* [--activate]
1495+
* : If set, dependencies will be activated immediately after install.
1496+
*
1497+
* [--activate-network]
1498+
* : If set, dependencies will be network activated immediately after install.
1499+
*
1500+
* ## EXAMPLES
1501+
*
1502+
* # Install all dependencies of an installed plugin
1503+
* $ wp plugin install-dependencies my-plugin
1504+
* Installing dependency: required-plugin-1 (1.2.3)
1505+
* Plugin installed successfully.
1506+
* Installing dependency: required-plugin-2 (2.0.0)
1507+
* Plugin installed successfully.
1508+
* Success: Installed 2 dependencies.
1509+
*
1510+
* @subcommand install-dependencies
1511+
*/
1512+
public function install_dependencies( $args, $assoc_args ) {
1513+
$plugin = $this->fetcher->get_check( $args[0] );
1514+
$file = $plugin->file;
1515+
1516+
// Check if plugin is installed
1517+
if ( ! file_exists( WP_PLUGIN_DIR . '/' . $file ) ) {
1518+
WP_CLI::error( "Plugin '{$args[0]}' is not installed." );
1519+
}
1520+
1521+
// Get dependencies from plugin header
1522+
$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false );
1523+
$dependencies = [];
1524+
1525+
if ( ! empty( $plugin_data['RequiresPlugins'] ) ) {
1526+
// Parse the comma-separated list
1527+
$dependencies = array_map( 'trim', explode( ',', $plugin_data['RequiresPlugins'] ) );
1528+
}
1529+
1530+
if ( empty( $dependencies ) ) {
1531+
WP_CLI::success( "Plugin '{$args[0]}' has no dependencies." );
1532+
return;
1533+
}
1534+
1535+
WP_CLI::log( sprintf( "Installing %d %s for '%s'...", count( $dependencies ), Utils\pluralize( 'dependency', count( $dependencies ) ), $args[0] ) );
1536+
1537+
// Install dependencies
1538+
$this->chained_command = true;
1539+
$this->install( $dependencies, $assoc_args );
1540+
$this->chained_command = false;
1541+
}
1542+
13801543
/**
13811544
* Deletes plugin files without deactivating or uninstalling.
13821545
*

0 commit comments

Comments
 (0)