File: //usr/local/wp/vendor/wp-cli/scaffold-command/src/Scaffold_Command.php
<?php
use WP_CLI\Utils;
use WP_CLI\Inflector;
/**
* Generates code for post types, taxonomies, plugins, child themes, etc.
*
* ## EXAMPLES
*
* # Generate a new plugin with unit tests.
* $ wp scaffold plugin sample-plugin
* Success: Created plugin files.
* Success: Created test files.
*
* # Generate theme based on _s.
* $ wp scaffold _s sample-theme --theme_name="Sample Theme" --author="John Doe"
* Success: Created theme 'Sample Theme'.
*
* # Generate code for post type registration in given theme.
* $ wp scaffold post-type movie --label=Movie --theme=simple-life
* Success: Created '/var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php'.
*
* @package wp-cli
*/
class Scaffold_Command extends WP_CLI_Command {
/**
* Generates PHP code for registering a custom post type.
*
* ## OPTIONS
*
* <slug>
* : The internal name of the post type.
*
* [--label=<label>]
* : The text used to translate the update messages.
*
* [--textdomain=<textdomain>]
* : The textdomain to use for the labels.
*
* [--dashicon=<dashicon>]
* : The dashicon to use in the menu.
*
* [--theme]
* : Create a file in the active theme directory, instead of sending to
* STDOUT. Specify a theme with `--theme=<theme>` to have the file placed in that theme.
*
* [--plugin=<plugin>]
* : Create a file in the given plugin's directory, instead of sending to STDOUT.
*
* [--raw]
* : Just generate the `register_post_type()` call and nothing else.
*
* [--force]
* : Overwrite files that already exist.
*
* ## EXAMPLES
*
* # Generate a 'movie' post type for the 'simple-life' theme
* $ wp scaffold post-type movie --label=Movie --theme=simple-life
* Success: Created '/var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php'.
*
* @subcommand post-type
*
* @alias cpt
*/
public function post_type( $args, $assoc_args ) {
if ( strlen( $args[0] ) > 20 ) {
WP_CLI::error( 'Post type slugs cannot exceed 20 characters in length.' );
}
$defaults = [
'textdomain' => '',
'dashicon' => 'admin-post',
];
$templates = [
'post_type.mustache',
'post_type_extended.mustache',
];
$this->scaffold( $args[0], $assoc_args, $defaults, '/post-types/', $templates );
}
/**
* Generates PHP code for registering a custom taxonomy.
*
* ## OPTIONS
*
* <slug>
* : The internal name of the taxonomy.
*
* [--post_types=<post-types>]
* : Post types to register for use with the taxonomy.
*
* [--label=<label>]
* : The text used to translate the update messages.
*
* [--textdomain=<textdomain>]
* : The textdomain to use for the labels.
*
* [--theme]
* : Create a file in the active theme directory, instead of sending to
* STDOUT. Specify a theme with `--theme=<theme>` to have the file placed in that theme.
*
* [--plugin=<plugin>]
* : Create a file in the given plugin's directory, instead of sending to STDOUT.
*
* [--raw]
* : Just generate the `register_taxonomy()` call and nothing else.
*
* [--force]
* : Overwrite files that already exist.
*
* ## EXAMPLES
*
* # Generate PHP code for registering a custom taxonomy and save in a file
* $ wp scaffold taxonomy venue --post_types=event,presentation > taxonomy.php
*
* @subcommand taxonomy
*
* @alias tax
*/
public function taxonomy( $args, $assoc_args ) {
$defaults = [
'textdomain' => '',
'post_types' => "'post'",
];
if ( isset( $assoc_args['post_types'] ) ) {
$assoc_args['post_types'] = $this->quote_comma_list_elements( $assoc_args['post_types'] );
}
$templates = [
'taxonomy.mustache',
'taxonomy_extended.mustache',
];
$this->scaffold( $args[0], $assoc_args, $defaults, '/taxonomies/', $templates );
}
private function scaffold( $slug, $assoc_args, $defaults, $subdir, $templates ) {
$wp_filesystem = $this->init_wp_filesystem();
$control_defaults = [
'label' => preg_replace( '/_|-/', ' ', strtolower( $slug ) ),
'theme' => false,
'plugin' => false,
'raw' => false,
];
$control_args = $this->extract_args( $assoc_args, $control_defaults );
$vars = $this->extract_args( $assoc_args, $defaults );
$dashicon = $this->extract_dashicon( $assoc_args );
if ( $dashicon ) {
$vars['dashicon'] = $dashicon;
}
$vars['slug'] = $slug;
$vars['textdomain'] = $this->get_textdomain( $vars['textdomain'], $control_args );
$vars['label'] = $control_args['label'];
$vars['label_ucfirst'] = ucfirst( $vars['label'] );
$vars['label_plural'] = $this->pluralize( $vars['label'] );
$vars['label_plural_ucfirst'] = ucfirst( $vars['label_plural'] );
$machine_name = $this->generate_machine_name( $slug );
$machine_name_plural = $this->pluralize( $slug );
list( $raw_template, $extended_template ) = $templates;
$raw_output = self::mustache_render( $raw_template, $vars );
if ( ! $control_args['raw'] ) {
$vars['machine_name'] = $machine_name;
$vars['output'] = rtrim( $raw_output );
$target_slug = '';
if ( false !== $control_args['theme'] ) {
$target_slug = $control_args['theme'];
} elseif ( false !== $control_args['plugin'] ) {
$target_slug = $control_args['plugin'];
}
$target_name = ( $target_slug ) ? $this->generate_machine_name( $target_slug ) : '';
if ( empty( $target_name ) ) {
$target_name = $machine_name;
}
$vars['prefix'] = $target_name;
$final_output = self::mustache_render( $extended_template, $vars );
} else {
$final_output = $raw_output;
}
$path = $this->get_output_path( $control_args, $subdir );
if ( is_string( $path ) && ! empty( $path ) ) {
$filename = "{$path}{$slug}.php";
$force = Utils\get_flag_value( $assoc_args, 'force' );
$files_written = $this->create_files( [ $filename => $final_output ], $force );
$skip_message = "Skipped creating '{$filename}'.";
$success_message = "Created '{$filename}'.";
$this->log_whether_files_written( $files_written, $skip_message, $success_message );
} else {
// STDOUT
echo $final_output;
}
}
/**
* Generates PHP, JS and CSS code for registering a Gutenberg block for a plugin or theme.
*
* **Warning: `wp scaffold block` is deprecated.**
*
* The official script to generate a block is the [@wordpress/create-block](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package.
*
* See the [Create a Block tutorial](https://developer.wordpress.org/block-editor/getting-started/tutorial/) for a complete walk-through.
*
* ## OPTIONS
*
* <slug>
* : The internal name of the block.
*
* [--title=<title>]
* : The display title for your block.
*
* [--dashicon=<dashicon>]
* : The dashicon to make it easier to identify your block.
*
* [--category=<category>]
* : The category name to help users browse and discover your block.
* ---
* default: widgets
* options:
* - common
* - embed
* - formatting
* - layout
* - widgets
* ---
*
* [--theme]
* : Create files in the active theme directory. Specify a theme with `--theme=<theme>` to have the file placed in that theme.
*
* [--plugin=<plugin>]
* : Create files in the given plugin's directory.
*
* [--force]
* : Overwrite files that already exist.
*
* @subcommand block
*/
public function block( $args, $assoc_args ) {
$slug = $args[0];
if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
WP_CLI::error( 'Invalid block slug specified. Block slugs can contain only lowercase alphanumeric characters or dashes, and start with a letter.' );
}
$defaults = [
'title' => str_replace( '-', ' ', $slug ),
'category' => 'widgets',
];
$data = $this->extract_args( $assoc_args, $defaults );
$data['slug'] = $slug;
$data['title_ucfirst'] = ucfirst( $data['title'] );
$data['title_ucfirst_js'] = esc_js( $data['title_ucfirst'] );
$dashicon = $this->extract_dashicon( $assoc_args );
if ( $dashicon ) {
$data['dashicon'] = $dashicon;
}
$control_defaults = [
'force' => false,
'plugin' => false,
'theme' => false,
];
$control_args = $this->extract_args( $assoc_args, $control_defaults );
if ( isset( $control_args['plugin'] ) ) {
if ( ! preg_match( '/^[A-Za-z0-9\-]*$/', $control_args['plugin'] ) ) {
WP_CLI::error( 'Invalid plugin name specified. The block editor can only register blocks for plugins that have nothing but lowercase alphanumeric characters or dashes in their slug.' );
}
}
$data['namespace'] = $control_args['plugin'] ? $control_args['plugin'] : $this->get_theme_name( $control_args['theme'] );
$data['machine_name'] = $this->generate_machine_name( $slug );
$data['plugin'] = $control_args['plugin'] ? true : false;
$data['theme'] = ! $data['plugin'];
$block_dir = $this->get_output_path( $control_args, '/blocks' );
if ( ! $block_dir ) {
WP_CLI::error( 'No plugin or theme selected.' );
}
$files_to_create = [
"{$block_dir}/{$slug}.php" => self::mustache_render( 'block-php.mustache', $data ),
"{$block_dir}/{$slug}/index.js" => self::mustache_render( 'block-index-js.mustache', $data ),
"{$block_dir}/{$slug}/editor.css" => self::mustache_render( 'block-editor-css.mustache', $data ),
"{$block_dir}/{$slug}/style.css" => self::mustache_render( 'block-style-css.mustache', $data ),
];
$files_written = $this->create_files( $files_to_create, $control_args['force'] );
$skip_message = 'All block files were skipped.';
$success_message = "Created block '{$data['title_ucfirst']}'.";
$this->log_whether_files_written( $files_written, $skip_message, $success_message );
}
/**
* Generates starter code for a theme based on _s.
*
* See the [Underscores website](https://underscores.me/) for more details.
*
* ## OPTIONS
*
* <slug>
* : The slug for the new theme, used for prefixing functions.
*
* [--activate]
* : Activate the newly downloaded theme.
*
* [--enable-network]
* : Enable the newly downloaded theme for the entire network.
*
* [--theme_name=<title>]
* : What to put in the 'Theme Name:' header in 'style.css'.
*
* [--author=<full-name>]
* : What to put in the 'Author:' header in 'style.css'.
*
* [--author_uri=<uri>]
* : What to put in the 'Author URI:' header in 'style.css'.
*
* [--sassify]
* : Include stylesheets as SASS.
*
* [--woocommerce]
* : Include WooCommerce boilerplate files.
*
* [--force]
* : Overwrite files that already exist.
*
* ## EXAMPLES
*
* # Generate a theme with name "Sample Theme" and author "John Doe"
* $ wp scaffold _s sample-theme --theme_name="Sample Theme" --author="John Doe"
* Success: Created theme 'Sample Theme'.
*
* @alias _s
*/
public function underscores( $args, $assoc_args ) {
$theme_slug = $args[0];
$theme_path = WP_CONTENT_DIR . '/themes';
$url = 'https://underscores.me';
$timeout = 30;
if ( ! preg_match( '/^[a-z_]\w+$/i', str_replace( '-', '_', $theme_slug ) ) ) {
WP_CLI::error( 'Invalid theme slug specified. Theme slugs can only contain letters, numbers, underscores and hyphens, and can only start with a letter or underscore.' );
}
$defaults = [
'theme_name' => ucfirst( $theme_slug ),
'author' => 'Me',
'author_uri' => '',
];
$data = wp_parse_args( $assoc_args, $defaults );
$_s_theme_path = "$theme_path/$data[theme_name]";
$error_msg = $this->check_target_directory( 'theme', $_s_theme_path );
if ( ! empty( $error_msg ) ) {
WP_CLI::error( "Invalid theme slug specified. {$error_msg}" );
}
$force = Utils\get_flag_value( $assoc_args, 'force' );
$should_write_file = $this->prompt_if_files_will_be_overwritten( $_s_theme_path, $force );
if ( ! $should_write_file ) {
WP_CLI::log( 'No files created' );
die;
}
$theme_description = "Custom theme: {$data['theme_name']}, developed by {$data['author']}";
$body = [];
$body['underscoresme_name'] = $data['theme_name'];
$body['underscoresme_slug'] = $theme_slug;
$body['underscoresme_author'] = $data['author'];
$body['underscoresme_author_uri'] = $data['author_uri'];
$body['underscoresme_description'] = $theme_description;
$body['underscoresme_generate_submit'] = 'Generate';
$body['underscoresme_generate'] = '1';
if ( Utils\get_flag_value( $assoc_args, 'sassify' ) ) {
$body['underscoresme_sass'] = 1;
}
if ( Utils\get_flag_value( $assoc_args, 'woocommerce' ) ) {
$body['underscoresme_woocommerce'] = 1;
}
$tmpfname = wp_tempnam( $url );
$post_args = [
'timeout' => $timeout,
'body' => $body,
'stream' => true,
'filename' => $tmpfname,
];
$response = wp_remote_post( $url, $post_args );
if ( is_wp_error( $response ) ) {
WP_CLI::error( $response );
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== (int) $response_code ) {
WP_CLI::error( "Couldn't create theme (received {$response_code} response)." );
}
$this->maybe_create_themes_dir();
$this->init_wp_filesystem();
$unzip_result = unzip_file( $tmpfname, $theme_path );
unlink( $tmpfname );
if ( true === $unzip_result ) {
$files_to_create = [
"{$theme_path}/{$theme_slug}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ),
];
$this->create_files( $files_to_create, false );
WP_CLI::success( "Created theme '{$data['theme_name']}'." );
} else {
WP_CLI::error( "Could not decompress your theme files ('{$tmpfname}') at '{$theme_path}': {$unzip_result->get_error_message()}" );
}
if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) {
WP_CLI::run_command( [ 'theme', 'activate', $theme_slug ] );
} elseif ( Utils\get_flag_value( $assoc_args, 'enable-network' ) ) {
WP_CLI::run_command( [ 'theme', 'enable', $theme_slug ], [ 'network' => true ] );
}
}
/**
* Generates child theme based on an existing theme.
*
* Creates a child theme folder with `functions.php` and `style.css` files.
*
* ## OPTIONS
*
* <slug>
* : The slug for the new child theme.
*
* --parent_theme=<slug>
* : What to put in the 'Template:' header in 'style.css'.
*
* [--theme_name=<title>]
* : What to put in the 'Theme Name:' header in 'style.css'.
*
* [--author=<full-name>]
* : What to put in the 'Author:' header in 'style.css'.
*
* [--author_uri=<uri>]
* : What to put in the 'Author URI:' header in 'style.css'.
*
* [--theme_uri=<uri>]
* : What to put in the 'Theme URI:' header in 'style.css'.
*
* [--activate]
* : Activate the newly created child theme.
*
* [--enable-network]
* : Enable the newly created child theme for the entire network.
*
* [--force]
* : Overwrite files that already exist.
*
* ## EXAMPLES
*
* # Generate a 'sample-theme' child theme based on TwentySixteen
* $ wp scaffold child-theme sample-theme --parent_theme=twentysixteen
* Success: Created '/var/www/example.com/public_html/wp-content/themes/sample-theme'.
*
* @subcommand child-theme
*/
public function child_theme( $args, $assoc_args ) {
$theme_slug = $args[0];
if ( in_array( $theme_slug, [ '.', '..' ], true ) ) {
WP_CLI::error( "Invalid theme slug specified. The slug cannot be '.' or '..'." );
}
$defaults = [
'theme_name' => ucfirst( $theme_slug ),
'author' => 'Me',
'author_uri' => '',
'theme_uri' => '',
];
$data = wp_parse_args( $assoc_args, $defaults );
$data['slug'] = $theme_slug;
$data['prefix_safe'] = str_replace( [ ' ', '-' ], '_', $theme_slug );
$data['description'] = ucfirst( $data['parent_theme'] ) . ' child theme.';
$theme_dir = WP_CONTENT_DIR . "/themes/{$theme_slug}";
$error_msg = $this->check_target_directory( 'theme', $theme_dir );
if ( ! empty( $error_msg ) ) {
WP_CLI::error( "Invalid theme slug specified. {$error_msg}" );
}
$theme_style_path = "{$theme_dir}/style.css";
$theme_functions_path = "{$theme_dir}/functions.php";
$this->maybe_create_themes_dir();
$files_to_create = [
$theme_style_path => self::mustache_render( 'child_theme.mustache', $data ),
$theme_functions_path => self::mustache_render( 'child_theme_functions.mustache', $data ),
"{$theme_dir}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ),
];
$force = Utils\get_flag_value( $assoc_args, 'force' );
$files_written = $this->create_files( $files_to_create, $force );
$skip_message = 'All theme files were skipped.';
$success_message = "Created '{$theme_dir}'.";
$this->log_whether_files_written( $files_written, $skip_message, $success_message );
if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) {
WP_CLI::run_command( [ 'theme', 'activate', $theme_slug ] );
} elseif ( Utils\get_flag_value( $assoc_args, 'enable-network' ) ) {
WP_CLI::run_command( [ 'theme', 'enable', $theme_slug ], [ 'network' => true ] );
}
}
private function get_output_path( $assoc_args, $subdir ) {
if ( $assoc_args['theme'] ) {
$theme = $assoc_args['theme'];
if ( is_string( $theme ) ) {
$path = get_theme_root( $theme ) . "/{$theme}";
} else {
$path = get_stylesheet_directory();
}
if ( ! is_dir( $path ) ) {
WP_CLI::error( "Can't find '{$theme}' theme." );
}
} elseif ( $assoc_args['plugin'] ) {
$plugin = $assoc_args['plugin'];
$path = WP_PLUGIN_DIR . "/{$plugin}";
if ( ! is_dir( $path ) ) {
WP_CLI::error( "Can't find '{$plugin}' plugin." );
}
} else {
return false;
}
$path .= $subdir;
return $path;
}
/**
* Generates starter code for a plugin.
*
* The following files are always generated:
*
* * `plugin-slug.php` is the main PHP plugin file.
* * `readme.txt` is the readme file for the plugin.
* * `package.json` needed by NPM holds various metadata relevant to the project. Packages: `grunt`, `grunt-wp-i18n` and `grunt-wp-readme-to-markdown`. Scripts: `start`, `readme`, `i18n`.
* * `Gruntfile.js` is the JS file containing Grunt tasks. Tasks: `i18n` containing `addtextdomain` and `makepot`, `readme` containing `wp_readme_to_markdown`.
* * `.editorconfig` is the configuration file for Editor.
* * `.gitignore` tells which files (or patterns) git should ignore.
* * `.distignore` tells which files and folders should be ignored in distribution.
*
* The following files are also included unless the `--skip-tests` is used:
*
* * `phpunit.xml.dist` is the configuration file for PHPUnit.
* * `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service.
* * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database.
* * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite.
* * `tests/test-sample.php` is a sample file containing test cases.
* * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules.
*
* ## OPTIONS
*
* <slug>
* : The internal name of the plugin.
*
* [--dir=<dirname>]
* : Put the new plugin in some arbitrary directory path. Plugin directory will be path plus supplied slug.
*
* [--plugin_name=<title>]
* : What to put in the 'Plugin Name:' header.
*
* [--plugin_description=<description>]
* : What to put in the 'Description:' header.
*
* [--plugin_author=<author>]
* : What to put in the 'Author:' header.
*
* [--plugin_author_uri=<url>]
* : What to put in the 'Author URI:' header.
*
* [--plugin_uri=<url>]
* : What to put in the 'Plugin URI:' header.
*
* [--skip-tests]
* : Don't generate files for unit testing.
*
* [--ci=<provider>]
* : Choose a configuration file for a continuous integration provider.
* ---
* default: circle
* options:
* - circle
* - gitlab
* - bitbucket
* - github
* ---
*
* [--activate]
* : Activate the newly generated plugin.
*
* [--activate-network]
* : Network activate the newly generated plugin.
*
* [--force]
* : Overwrite files that already exist.
*
* ## EXAMPLES
*
* $ wp scaffold plugin sample-plugin
* Success: Created plugin files.
* Success: Created test files.
*/
public function plugin( $args, $assoc_args ) {
$plugin_slug = $args[0];
$plugin_name = ucwords( str_replace( '-', ' ', $plugin_slug ) );
$plugin_package = str_replace( ' ', '_', $plugin_name );
if ( in_array( $plugin_slug, [ '.', '..' ], true ) ) {
WP_CLI::error( "Invalid plugin slug specified. The slug cannot be '.' or '..'." );
}
$defaults = [
'plugin_slug' => $plugin_slug,
'plugin_name' => $plugin_name,
'plugin_package' => $plugin_package,
'plugin_description' => 'PLUGIN DESCRIPTION HERE',
'plugin_author' => 'YOUR NAME HERE',
'plugin_author_uri' => 'YOUR SITE HERE',
'plugin_uri' => 'PLUGIN SITE HERE',
'plugin_tested_up_to' => get_bloginfo( 'version' ),
];
$data = wp_parse_args( $assoc_args, $defaults );
$data['textdomain'] = $plugin_slug;
if ( ! empty( $assoc_args['dir'] ) ) {
if ( ! is_dir( $assoc_args['dir'] ) ) {
WP_CLI::error( "Cannot create plugin in directory that doesn't exist." );
}
$plugin_dir = "{$assoc_args['dir']}/{$plugin_slug}";
} else {
$plugin_dir = WP_PLUGIN_DIR . "/{$plugin_slug}";
$this->maybe_create_plugins_dir();
$error_msg = $this->check_target_directory( 'plugin', $plugin_dir );
if ( ! empty( $error_msg ) ) {
WP_CLI::error( "Invalid plugin slug specified. {$error_msg}" );
}
}
$plugin_path = "{$plugin_dir}/{$plugin_slug}.php";
$plugin_readme_path = "{$plugin_dir}/readme.txt";
$files_to_create = [
$plugin_path => self::mustache_render( 'plugin.mustache', $data ),
$plugin_readme_path => self::mustache_render( 'plugin-readme.mustache', $data ),
"{$plugin_dir}/composer.json" => self::mustache_render( 'plugin-composer.mustache', $data ),
"{$plugin_dir}/.gitignore" => self::mustache_render( 'plugin-gitignore.mustache', $data ),
"{$plugin_dir}/.distignore" => self::mustache_render( 'plugin-distignore.mustache', $data ),
"{$plugin_dir}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ),
];
$force = Utils\get_flag_value( $assoc_args, 'force' );
$files_written = $this->create_files( $files_to_create, $force );
$skip_message = 'All plugin files were skipped.';
$success_message = 'Created plugin files.';
$this->log_whether_files_written( $files_written, $skip_message, $success_message );
if ( ! Utils\get_flag_value( $assoc_args, 'skip-tests' ) ) {
$command_args = [
'dir' => $plugin_dir,
'ci' => empty( $assoc_args['ci'] ) ? '' : $assoc_args['ci'],
'force' => $force,
];
WP_CLI::run_command( [ 'scaffold', 'plugin-tests', $plugin_slug ], $command_args );
}
if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) {
WP_CLI::run_command( [ 'plugin', 'activate', $plugin_slug ] );
} elseif ( Utils\get_flag_value( $assoc_args, 'activate-network' ) ) {
WP_CLI::run_command( [ 'plugin', 'activate', $plugin_slug ], [ 'network' => true ] );
}
}
/**
* Generates files needed for running PHPUnit tests in a plugin.
*
* The following files are generated by default:
*
* * `phpunit.xml.dist` is the configuration file for PHPUnit.
* * `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service.
* * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database.
* * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite.
* * `tests/test-sample.php` is a sample file containing the actual tests.
* * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules.
*
* Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/).
*
* ## ENVIRONMENT
*
* The `tests/bootstrap.php` file looks for the WP_TESTS_DIR environment
* variable.
*
* ## OPTIONS
*
* [<plugin>]
* : The name of the plugin to generate test files for.
*
* [--dir=<dirname>]
* : Generate test files for a non-standard plugin path. If no plugin slug is specified, the directory name is used.
*
* [--ci=<provider>]
* : Choose a configuration file for a continuous integration provider.
* ---
* default: circle
* options:
* - circle
* - gitlab
* - bitbucket
* - github
* ---
*
* [--force]
* : Overwrite files that already exist.
*
* ## EXAMPLES
*
* # Generate unit test files for plugin 'sample-plugin'.
* $ wp scaffold plugin-tests sample-plugin
* Success: Created test files.
*
* @subcommand plugin-tests
*/
public function plugin_tests( $args, $assoc_args ) {
$this->scaffold_plugin_theme_tests( $args, $assoc_args, 'plugin' );
}
/**
* Generates files needed for running PHPUnit tests in a theme.
*
* The following files are generated by default:
*
* * `phpunit.xml.dist` is the configuration file for PHPUnit.
* * `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service.
* * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database.
* * `tests/bootstrap.php` is the file that makes the current theme active when running the test suite.
* * `tests/test-sample.php` is a sample file containing the actual tests.
* * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules.
*
* Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/).
*
* ## ENVIRONMENT
*
* The `tests/bootstrap.php` file looks for the WP_TESTS_DIR environment
* variable.
*
* ## OPTIONS
*
* [<theme>]
* : The name of the theme to generate test files for.
*
* [--dir=<dirname>]
* : Generate test files for a non-standard theme path. If no theme slug is specified, the directory name is used.
*
* [--ci=<provider>]
* : Choose a configuration file for a continuous integration provider.
* ---
* default: circle
* options:
* - circle
* - gitlab
* - bitbucket
* - github
* ---
*
* [--force]
* : Overwrite files that already exist.
*
* ## EXAMPLES
*
* # Generate unit test files for theme 'twentysixteenchild'.
* $ wp scaffold theme-tests twentysixteenchild
* Success: Created test files.
*
* @subcommand theme-tests
*/
public function theme_tests( $args, $assoc_args ) {
$this->scaffold_plugin_theme_tests( $args, $assoc_args, 'theme' );
}
private function scaffold_plugin_theme_tests( $args, $assoc_args, $type ) {
$wp_filesystem = $this->init_wp_filesystem();
if ( ! empty( $args[0] ) ) {
$slug = $args[0];
if ( in_array( $slug, [ '.', '..' ], true ) ) {
WP_CLI::error( "Invalid {$type} slug specified. The slug cannot be '.' or '..'." );
}
if ( 'theme' === $type ) {
$theme = wp_get_theme( $slug );
if ( $theme->exists() ) {
$target_dir = $theme->get_stylesheet_directory();
} else {
WP_CLI::error( "Invalid {$type} slug specified. The theme '{$slug}' does not exist." );
}
} else {
$target_dir = WP_PLUGIN_DIR . "/{$slug}";
}
if ( empty( $assoc_args['dir'] ) && ! is_dir( $target_dir ) ) {
WP_CLI::error( "Invalid {$type} slug specified. No such target directory '{$target_dir}'." );
}
$error_msg = $this->check_target_directory( $type, $target_dir );
if ( ! empty( $error_msg ) ) {
WP_CLI::error( "Invalid {$type} slug specified. {$error_msg}" );
}
}
if ( ! empty( $assoc_args['dir'] ) ) {
$target_dir = $assoc_args['dir'];
if ( ! is_dir( $target_dir ) ) {
WP_CLI::error( "Invalid {$type} directory specified. No such directory '{$target_dir}'." );
}
if ( empty( $slug ) ) {
$slug = Utils\basename( $target_dir );
}
}
if ( empty( $slug ) || empty( $target_dir ) ) {
WP_CLI::error( "Invalid {$type} specified." );
}
$name = ucwords( str_replace( '-', ' ', $slug ) );
$package = str_replace( ' ', '_', $name );
$tests_dir = "{$target_dir}/tests";
$bin_dir = "{$target_dir}/bin";
$wp_filesystem->mkdir( $tests_dir );
$wp_filesystem->mkdir( $bin_dir );
$wp_versions_to_test = [];
// Parse plugin readme.txt
if ( file_exists( "{$target_dir}/readme.txt" ) ) {
$readme_content = (string) file_get_contents( "{$target_dir}/readme.txt" );
preg_match( '/Requires at least\:(.*)\n/m', $readme_content, $matches );
if ( isset( $matches[1] ) && $matches[1] ) {
$wp_versions_to_test[] = trim( $matches[1] );
}
}
$wp_versions_to_test[] = 'latest';
$wp_versions_to_test[] = 'trunk';
$main_file = "{$slug}.php";
if ( 'plugin' === $type ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
if ( ! empty( $all_plugins ) ) {
$filtered = array_filter(
array_keys( $all_plugins ),
static function ( $item ) use ( $slug ) {
return ( false !== strpos( $item, "{$slug}/" ) );
}
);
if ( ! empty( $filtered ) ) {
$main_file = basename( reset( $filtered ) );
}
}
}
$template_data = [
"{$type}_slug" => $slug,
"{$type}_package" => $package,
"{$type}_main_file" => $main_file,
];
$force = Utils\get_flag_value( $assoc_args, 'force' );
$files_to_create = [
"{$tests_dir}/bootstrap.php" => self::mustache_render( "{$type}-bootstrap.mustache", $template_data ),
"{$tests_dir}/test-sample.php" => self::mustache_render( "{$type}-test-sample.mustache", $template_data ),
];
if ( 'circle' === $assoc_args['ci'] ) {
$files_to_create[ "{$target_dir}/.circleci/config.yml" ] = self::mustache_render( 'plugin-circle.mustache', compact( 'wp_versions_to_test' ) );
} elseif ( 'gitlab' === $assoc_args['ci'] ) {
$files_to_create[ "{$target_dir}/.gitlab-ci.yml" ] = self::mustache_render( 'plugin-gitlab.mustache' );
} elseif ( 'bitbucket' === $assoc_args['ci'] ) {
$files_to_create[ "{$target_dir}/bitbucket-pipelines.yml" ] = self::mustache_render( 'plugin-bitbucket.mustache' );
} elseif ( 'github' === $assoc_args['ci'] ) {
$files_to_create[ "{$target_dir}/.github/workflows/testing.yml" ] = self::mustache_render( 'plugin-github.mustache' );
}
$files_written = $this->create_files( $files_to_create, $force );
$to_copy = [
'install-wp-tests.sh' => $bin_dir,
'phpunit.xml.dist' => $target_dir,
'.phpcs.xml.dist' => $target_dir,
];
foreach ( $to_copy as $file => $dir ) {
$file_name = "{$dir}/{$file}";
$force = Utils\get_flag_value( $assoc_args, 'force' );
$should_write_file = $this->prompt_if_files_will_be_overwritten( $file_name, $force );
if ( ! $should_write_file ) {
continue;
}
$files_written[] = $file_name;
$wp_filesystem->copy( self::get_template_path( $file ), $file_name, true );
if ( 'install-wp-tests.sh' === $file ) {
if ( ! $wp_filesystem->chmod( "{$dir}/{$file}", 0755 ) ) {
WP_CLI::warning( "Couldn't mark 'install-wp-tests.sh' as executable." );
}
}
}
$skip_message = 'All test files were skipped.';
$success_message = 'Created test files.';
$this->log_whether_files_written( $files_written, $skip_message, $success_message );
}
/**
* Checks that the `$target_dir` is a child directory of the WP themes or plugins directory, depending on `$type`.
*
* @param string $type "theme" or "plugin"
* @param string $target_dir The theme/plugin directory to check.
*
* @return null|string Returns null on success, error message on error.
*/
private function check_target_directory( $type, $target_dir ) {
$parent_dir = dirname( self::canonicalize_path( str_replace( '\\', '/', $target_dir ) ) );
if ( 'theme' === $type && str_replace( '\\', '/', WP_CONTENT_DIR . '/themes' ) !== $parent_dir ) {
return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, WP_CONTENT_DIR . '/themes' );
}
if ( 'plugin' === $type && str_replace( '\\', '/', WP_PLUGIN_DIR ) !== $parent_dir ) {
return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, WP_PLUGIN_DIR );
}
// Success.
return null;
}
protected function create_files( $files_and_contents, $force ) {
$wp_filesystem = $this->init_wp_filesystem();
$wrote_files = [];
foreach ( $files_and_contents as $filename => $contents ) {
$should_write_file = $this->prompt_if_files_will_be_overwritten( $filename, $force );
if ( ! $should_write_file ) {
continue;
}
$wp_filesystem->mkdir( dirname( $filename ) );
// Create multi-level folders.
if ( false === $wp_filesystem->exists( dirname( $filename ) ) ) {
$wp_filesystem->mkdir( dirname( dirname( $filename ) ) );
$wp_filesystem->mkdir( dirname( $filename ) );
}
if ( ! $wp_filesystem->put_contents( $filename, $contents ) ) {
WP_CLI::error( "Error creating file: {$filename}" );
} elseif ( $should_write_file ) {
$wrote_files[] = $filename;
}
}
return $wrote_files;
}
protected function prompt_if_files_will_be_overwritten( $filename, $force ) {
$should_write_file = true;
if ( ! file_exists( $filename ) ) {
return true;
}
WP_CLI::warning( 'File already exists.' );
WP_CLI::log( $filename );
if ( ! $force ) {
do {
$answer = cli\prompt(
'Skip this file, or replace it with scaffolding?',
$default = false,
$marker = '[s/r]: '
);
} while ( ! in_array( $answer, [ 's', 'r' ], true ) );
$should_write_file = 'r' === $answer;
}
$outcome = $should_write_file ? 'Replacing' : 'Skipping';
WP_CLI::log( $outcome . PHP_EOL );
return $should_write_file;
}
protected function log_whether_files_written( $files_written, $skip_message, $success_message ) {
if ( empty( $files_written ) ) {
WP_CLI::log( $skip_message );
} else {
WP_CLI::success( $success_message );
}
}
/**
* Extracts dashicon name when provided or return null otherwise.
*
* @param array{dashicon?: string} $assoc_args
* @return string|null
*/
private function extract_dashicon( $assoc_args ) {
$dashicon = Utils\get_flag_value( $assoc_args, 'dashicon' );
if ( ! $dashicon ) {
return null;
}
return preg_replace( '/dashicon(-|s-)/', '', $dashicon );
}
/**
* If you're writing your files to your theme directory your textdomain also needs to be the same as your theme.
* Same goes for when plugin is being used.
*/
private function get_textdomain( $textdomain, $args ) {
if ( strlen( $textdomain ) ) {
return $textdomain;
}
if ( $args['theme'] ) {
return $this->get_theme_name( $args['theme'] );
}
if ( $args['plugin'] && true !== $args['plugin'] ) {
return $args['plugin'];
}
return 'YOUR-TEXTDOMAIN';
}
/**
* Generates the machine name for function declarations.
*
* @param string $slug Slug name to convert.
* @return string
*/
private function generate_machine_name( $slug ) {
return str_replace( '-', '_', $slug );
}
/**
* Pluralizes a noun.
*
* @see Inflector::pluralize()
* @param string $word Word to be pluralized.
* @return string
*/
private function pluralize( $word ) {
return Inflector::pluralize( $word );
}
protected function extract_args( $assoc_args, $defaults ) {
$out = [];
foreach ( $defaults as $key => $value ) {
$out[ $key ] = Utils\get_flag_value( $assoc_args, $key, $value );
}
return $out;
}
protected function quote_comma_list_elements( $comma_list ) {
return "'" . implode( "', '", explode( ',', $comma_list ) ) . "'";
}
/**
* Creates the themes directory if it doesn't already exist.
*/
protected function maybe_create_themes_dir() {
$themes_dir = WP_CONTENT_DIR . '/themes';
if ( ! is_dir( $themes_dir ) ) {
wp_mkdir_p( $themes_dir );
}
}
/**
* Creates the plugins directory if it doesn't already exist.
*/
protected function maybe_create_plugins_dir() {
if ( ! is_dir( WP_PLUGIN_DIR ) ) {
wp_mkdir_p( WP_PLUGIN_DIR );
}
}
/**
* Initializes WP_Filesystem.
*/
protected function init_wp_filesystem() {
global $wp_filesystem;
WP_Filesystem();
return $wp_filesystem;
}
/**
* Localizes the template path.
*/
private static function mustache_render( $template, $data = [] ) {
return Utils\mustache_render( dirname( __DIR__ ) . "/templates/{$template}", $data );
}
/**
* Gets the template path based on installation type.
*/
private static function get_template_path( $template ) {
$command_root = Utils\phar_safe_path( dirname( __DIR__ ) );
$template_path = "{$command_root}/templates/{$template}";
if ( ! file_exists( $template_path ) ) {
WP_CLI::error( "Couldn't find {$template}" );
}
return $template_path;
}
/*
* Returns the canonicalized path, with dot and double dot segments resolved.
*
* Copied from Symfony\Component\DomCrawler\AbstractUriElement::canonicalizePath().
* Implements RFC 3986, section 5.2.4.
*
* @param string $path The path to make canonical.
*
* @return string The canonicalized path.
*/
private static function canonicalize_path( $path ) {
if ( '' === $path || '/' === $path ) {
return $path;
}
if ( '.' === substr( $path, -1 ) ) {
$path .= '/';
}
$output = [];
foreach ( explode( '/', $path ) as $segment ) {
if ( '..' === $segment ) {
array_pop( $output );
} elseif ( '.' !== $segment ) {
$output[] = $segment;
}
}
return implode( '/', $output );
}
/**
* Gets an active theme's name when true provided or the same name otherwise.
*
* @param string|bool $theme Theme name or true.
* @return string
*/
private function get_theme_name( $theme ) {
if ( true === $theme ) {
$theme = wp_get_theme()->template;
}
return strtolower( $theme );
}
}