Skip to content

WordPress Plugin Boilerplate + Composer, namespaces, autoloading, Codeception tests, and GitHub Actions CI/CD to WordPress.org.

License

Notifications You must be signed in to change notification settings

BrianHenryIE/WordPress-Plugin-Boilerplate

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WordPress Plugin Boilerplate – BrianHenryIE Fork

The popular WordPress Plugin Boilerplate with added Composer, namespaces, autoloading, Codeception testing, and GitHub Actions WordPress.org deployment.

Overview

The WordPress Plugin Boilerplate is a well-documented starting point for WordPress plugins which encourages consistent conventions in plugin development. This fork adds:

  • PHP namespacing
  • PHP autoloading
  • Acceptance, integration & unit tests
  • WordPress Coding Standards (PHPCBF/PHPCS)
  • Composer namespace prefixing
  • Local copy of WordPress in development environment
  • Symlinks to make a sensible directory layout
  • Configuration of PhpStorm
  • Base README.md with Contributing section for new repos
  • GitHub Actions for WordPress.org deployment

The documentation is typically a little behind the code, but what's going on should be understandable to most developers.

The ultimate dream would be to have all WordPress plugins understandable by convention, unit tested, and in a repo thay can be contributed to with CI/CD to WordPress.org.

Environment

This was written for a local environment with:

  • MacOS Catalina (Bash)
  • Built-in Apache serving to localhost:8080 from your projects directory (probably ~/Sites/)
  • PHP 7.4 setup guide
  • Xdebug
  • MySQL 8
  • NPM

There are some differences between sed on MacOS and Linux. See PR6.

Setup a New Plugin

Open Terminal and set the variables, your local MySQL credentials, and your WordPress.org credentials for deployment:

your_name="BrianHenryIE"
your_email="[email protected]"

plugin_name="Example Plugin"
plugin_package_name="BrianHenryIE\\\\\\Plugin_Name"

mysql_username="root"
mysql_password="secret"

WP_ORG_SVN_USERNAME="[email protected]"
WP_ORG_SVN_PASSWORD="wordpressorgpassword"

Run these commands to generate replacements (or define them yourself):

plugin_slug=$(echo $plugin_name | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g'); echo $plugin_slug; # example-plugin
plugin_snake=$(echo $plugin_name | sed 's/ /_/g'); echo $plugin_snake; # Example_Plugin
plugin_snake_lower=$(echo $plugin_snake | tr '[:upper:]' '[:lower:]'); echo $plugin_snake_lower; # example_plugin
plugin_capitalized=$(echo $plugin_name | tr '[:lower:]' '[:upper:]' | sed 's/ /_/g'); echo $plugin_capitalized; # EXAMPLE_PLUGIN
php_package_name=$(echo "${plugin_package_name%%\\*}"/$plugin_slug | tr '[:upper:]' '[:lower:]'); echo $php_package_name # brianhenryie/example-plugin
test_site_db_name=${plugin_snake_lower:0:57}"_tests"; # example_plugin_tests
test_db_name=${plugin_snake_lower:0:51}"_integration"; # example_plugin_integration
plugin_db_username=${plugin_slug:0:31}; # 32 character max for username 
plugin_db_password=$plugin_slug;

And display them so you can change them if you need to:

echo plugin_slug=$plugin_slug \# Should only contain lowercase letters,numbers and hyphens;
echo plugin_snake=$plugin_snake;
echo plugin_snake_lower=$plugin_snake_lower;
echo plugin_package_name=$plugin_package_name;
echo plugin_capitalized=$plugin_capitalized;
echo test_site_db_name=$test_site_db_name;
echo test_db_name=$test_db_name;
echo plugin_db_username=$plugin_db_username;
echo plugin_db_password=$plugin_db_password;

Then this block of commands will take care of most of the downloading:

git clone https://github.com/BrianHenryIE/WordPress-Plugin-Boilerplate.git;
mv WordPress-Plugin-Boilerplate $plugin_slug;
cd $plugin_slug;


# Branches can be merged here.
# git merge origin/no-loader

open -a PhpStorm .;

Then the renaming/replacing:

find . -depth -name '*plugin-slug*' -execdir bash -c 'git mv "$1" "${1//plugin-slug/'$plugin_slug'}"' bash {} \;
find . -depth \( -name '*.php' -o -name '*.txt' -o -name '.env.testing' -o -name '*.md' \) -exec sed -i '' "s/plugin_title/$plugin_name/g" {} +
find . -type f \( -name '*.php' -o -name '*.txt' -o -name '*.json' -o -name '*.xml' -o -name '.env.testing'  -o -name '*.yml' -o -name '.gitignore' -o -name '.htaccess'  -o -name '.sh' -o -name '*.md' \) -exec sed -i '' 's/plugin-slug/'$plugin_slug'/g' {} +
find . -depth \( -name '*.php' -o -name '*.testing' \) -exec sed -i '' 's/plugin_snake_lower/'$plugin_snake_lower'/g' {} +
find . -type f \( -name '*.php' -o -name '*.txt' -o -name '*.json' -o -name '*.xml' \) -exec sed -i '' 's/Plugin_Snake/'$plugin_snake'/g' {} \;
find . -type f \( -name '*.php' -o -name '*.txt' -o -name '*.xml' \) -exec sed -i '' 's/Plugin_Package_Name/'$plugin_package_name'/g' {} \;
find . -type f \( -name '*.json' \) -exec sed -i '' 's/Plugin_Package_Name/'$(echo $plugin_package_name | sed 's/\\/\\\\/g')'/g' {} \;
find . -type f \( -name '*.php' -o -name '*.txt' -o -name '*.json' -o -name '*.xml' \) -exec sed -i '' 's#PHP_Package_Name#'$php_package_name'#g' {} \;
find . -depth -name '*.php' -exec sed -i '' 's/PLUGIN_NAME/'$plugin_capitalized'/g' {} +
find . -type f \( -name '*.php' -o -name '*.txt' -o -name '*.json' \) -exec sed -i '' "s/Your Name/$your_name/g" {} +
find . -type f \( -name '*.php' -o -name '*.txt' -o -name '*.json' -o -name '.env.testing' \) -exec sed -i '' "s/[email protected]/$your_email/g" {} +
find . -type f \( -name '.env.testing' \) -exec sed -i '' 's/plugin-db-username/'$plugin_db_username'/g' {} +

Create two local databases for tests:

# export PATH=${PATH}:/usr/local/mysql/bin

# mysql_username="root"
# mysql_password="secret"

# Make .env available to zsh or bash
[[ $0 == "-zsh" ]] && source .env.testing || export $(grep -v '^#' .env.testing | xargs);

# Create the database user: (different syntax for MariaDB and MySQL).
[[ $(mysqld --version) =~ .*MariaDB.* ]] && mysql -u $mysql_username -p$mysql_password -e "CREATE USER '"$TEST_DB_USER"'@'%' IDENTIFIED BY '"$TEST_DB_PASSWORD"';" || mysql -u $mysql_username -p$mysql_password -e "CREATE USER '"$TEST_DB_USER"'@'%' IDENTIFIED WITH mysql_native_password BY '"$TEST_DB_PASSWORD"';";

# Create the databases.
mysql -u $mysql_username -p$mysql_password -e "CREATE DATABASE "$TEST_SITE_DB_NAME"; USE "$TEST_SITE_DB_NAME"; GRANT ALL PRIVILEGES ON "$TEST_SITE_DB_NAME".* TO '"$TEST_DB_USER"'@'%';";
mysql -u $mysql_username -p$mysql_password -e "CREATE DATABASE "$TEST_DB_NAME"; USE "$TEST_DB_NAME"; GRANT ALL PRIVILEGES ON "$TEST_DB_NAME".* TO '"$TEST_DB_USER"'@'%';";

Install everything, setup WordPress, save a copy of the database:

composer update

# Make .env available to bash or zsh
[[ $0 == "-zsh" ]] && source .env.testing || export $(grep -v '^#' .env.testing | xargs);

vendor/bin/wp core install --url="localhost:8080/$PLUGIN_SLUG" --title="$PLUGIN_NAME" --admin_user=admin --admin_password=password [email protected];


vendor/bin/wp plugin activate $PLUGIN_SLUG;
vendor/bin/wp user create bob [email protected] --user_pass=password

mysqldump -u $TEST_SITE_DB_USER -p$TEST_SITE_DB_PASSWORD  $TEST_SITE_DB_NAME > tests/_data/dump.sql;

Run the tests to confirm it's working (this also does code generation necessary for autocomplete):

cp .env.secret.dist .env.secret

vendor/bin/codecept clean
vendor/bin/codecept run acceptance;

If this is a WooCommerce plugin:

# Uses :"*" because WooCommerce does not follow semver.
composer require wpackagist-plugin/woocommerce:"*" --dev --no-scripts;
composer require wpackagist-theme/storefront:"*" --dev --no-scripts;
composer require php-stubs/woocommerce-stubs:"*" --dev --no-scripts;

vendor/bin/wp plugin activate woocommerce;
vendor/bin/wp theme activate storefront;

vendor/bin/wp wc tool run install_pages --user=admin;

# Import sample products
vendor/bin/wp plugin install wordpress-importer --activate
vendor/bin/wp import wp-content/plugins/woocommerce/sample-data/sample_products.xml --authors=create

# Create a customer
vendor/bin/wp wc customer create --email='[email protected]' --billing='{"first_name":"Bob","last_name":"Tester","company":"Woo", "address_1": "123 Main St.", "city":"New York", "state:": "NY", "country":"USA"}' --shipping='{"first_name":"Bob","last_name":"Tester","company":"Woo", "address_1": "123 Main St.", "city":"New York", "state:": "NY", "country":"USA"}' --password='hunter2' --username='mrbob' --first_name='Bob' --last_name='Tester' --user=admin;

# Create dump after changing site.
[[ $0 == "-zsh" ]] && source .env.testing || export $(grep -v '^#' .env.testing | xargs);

mysqldump -u $TEST_SITE_DB_USER -p$TEST_SITE_DB_PASSWORD  $TEST_SITE_DB_NAME > tests/_data/dump.sql;

Discard this boilerplate repo's .git and README and start fresh:

rm -rf .git;
rm README.md;
mv README-rename.md README.md;

git init;
git add README.md;
git commit -m "Initial commit";

Set up GitHub using GitHub CLI (brew install gh, gh auth login). You might want to set the repo private here too, by appending --private instead.

gh repo create $PLUGIN_SLUG --push --source . --public;

Setup gh-pages branch (for code coverage html report)).

git checkout --orphan gh-pages;

touch index.html;
git add index.html;
git commit -m 'Set up gh-pages branch' index.html;

git push origin gh-pages;
git checkout master;

Start a dev branch.

git checkout -b dev
git add .
git commit -am "Dev"

Add WordPress.org username and password to GitHub Secrets for use in deployments:

gh secret set SVN_USERNAME -b"${WP_ORG_SVN_USERNAME}"
gh secret set SVN_PASSWORD -b"${WP_ORG_SVN_PASSWORD}"

The default admin login is admin/password:

open "http://localhost:8080/$PLUGIN_SLUG/wp-admin"

Usage

WordPress Coding Standards

To see WordPress Coding Standards errors using PHP_CodeSniffer run:

vendor/bin/phpcs

Use PHP Code Beautifier and Fixer to automatically correct them where possible:

vendor/bin/phpcbf

https://make.wordpress.org/core/2020/03/20/updating-the-coding-standards-for-modern-php/ https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/php/

Testing

Tests are handled with WP-Browser,

vendor/bin/codecept run unit
vendor/bin/codecept run wpunit
vendor/bin/codecept run integration
vendor/bin/codecept run acceptance

When making changes to the local WordPress installation to prep for acceptance tests, it needs to be saved because it is restored each time:

# export PATH=${PATH}:/usr/local/mysql/bin

[[ $0 == "-zsh" ]] && source .env.testing || export $(grep -v '^#' .env.testing | xargs);

mysql -u $TEST_SITE_DB_USER -p$TEST_SITE_DB_PASSWORD $TEST_SITE_DB_NAME < tests/_data/dump.sql

If you need to manually restore it:

[[ $0 == "-zsh" ]] && source .env.testing || export $(grep -v '^#' .env.testing | xargs);

mysql -u $mysql_username -p$mysql_password $TEST_SITE_DB_NAME < tests/_data/dump.sql

Deployment

To create a .zip archive for uploading to WordPress:

mv includes $(basename "`pwd`"); zip -r $(basename "`pwd`").zip $(basename "`pwd`"); mv $(basename "`pwd`") includes;

To configure automatic WordPress.org plugin repository deployment, add your WordPress.org username and password as Secrets SVN_USERNAME and SVN_PASSWORD in the GitHub repository's settings, then when a Release is created, the plugin will be updated on WordPress.org (from zerowp.com's Use Github Actions to publish WordPress plugins on WP.org repository).

Composer Notes

By convention, WordPress plugins and themes installed by composer get installed into the project's /wp-content/plugins and /wp-content/themes directory. In a typical PHP project, libraries required by the project during runtime are installed in the vendor directory. In the case of this project, libraries are downloaded to the project's vendor folder, then their files copied to vendor-prefixed and their namespace changed.

https://stackoverflow.com/a/32407670/336146

Mozart

Mozart is included in composer.json to prefix libraries' namespaces to avoid clashes with other WordPress plugins. e.g. in this case, wp-namespace-autoloader appears in vendor-prefixed/ with the namespace Plugin_Name\Pablo_Pacheco\WP_Namespace_Autoloader.

To use e.g. a .ics parser in your project:

composer require johngrogg/ics-parser --dev --no-scripts
vendor/bin/mozart compose

The Mozart configuration in composer.json:

 "extra": {
  "mozart": {
   "dep_namespace": "Plugin_Name\\",
   "dep_directory": "/src/vendor/",
   "classmap_directory": "/src/dependencies/",
   "classmap_prefix": "Plugin_Name_"
  }
 }

Composer-Patches

composer-patches is used to apply PRs to composer dependencies (e.g. while waiting for the repository owners to accept the required changes). Currently nothing is being patched, but for hte majoriity of this project's life some thing or another has needed patching.

When you make a PR against a library, or see an existing unmerged PR that you need, add .patch to the end of the PR's URL and add it to your project's composer.json like:

 "extra": {
  "patches": {
   "coenjacobs/mozart": {
    "Allow default packages": "https://github.com/coenjacobs/mozart/pull/49.patch"
   }
  }
 }

cweagans/composer-patches#286

WordPress Packagist

Plugins published on WordPress.org are made available through composer via wpackagist.org. Add to composer.json using:

 "repositories": [
  {
   "type":"composer",
   "url":"https://wpackagist.org"
  }
 ]

Then add the plugin or theme

composer require wpackagist-theme/twentytwenty --dev --no-scripts
 "require-dev": {
  "wpackagist-plugin/bh-wp-autologin-urls":">=1.1",
  "wpackagist-theme/twentytwenty":"*"
 }

Local Projects

To add a project in another local directory, ensure it has its own composer.json with "name": "brianhenryie/local-lib"

 "repositories": [
  {
   "type": "path",
   "url": "../phpunit-github-actions-printer"
  }
 ]
 "require": {
  "brianhenryie/local-lib":"*",
 }

https://getcomposer.org/doc/articles/repository-priorities.md

GitHub repository containing composer.json

By including plugins direct from GitHub, you may get additional files such as unit tests and JavaScript sources.

 "repositories": [
 {
  "url": "https://github.com/johnbillion/user-switching",
  "type": "git"
 }
 "require-dev": {
  "johnbillion/user-switching": "dev-master"
 }

GitHub Branch/Fork

When including a fork or branch, the repository may need to be changed, and the branch name should be prefixed with dev-.

 "repositories": [
 {
  "url": "https://github.com/BrianHenryIE/wp-namespace-autoloader",
  "type": "git"
 }
 "require": {
  "pablo-sg-pacheco/wp-namespace-autoloader": "dev-brianhenryie"
 }

GitHub repository without composer.json

For GitHub repositories that are not set up for composer:

 "repositories": [
 {
  "type": "package",
  "package": {
   "name": "enhancedathlete/ea-wp-aws-sns-client-rest-endpoint",
   "version": "1.0",
   "source": {
    "url": "https://github.com/EnhancedAthlete/EA-WP-AWS-SNS-Client-REST-Endpoint",
    "type": "git",
    "reference": "master"
   }
  }
 }
 "require-dev": {
  "enhancedathlete/ea-wp-aws-sns-client-rest-endpoint":"*"
 }

SatisPress

SatisPress is a WordPress plugin that allows you to expose the plugins and themes installed on your WordPress site via a private Composer repository.

Once installed, plugins need to be whitelisted via checkboxes in the admin UI's plugins.php page, and credentials need to be defined in Settings/SatisPress.

 "repositories": [
 {
  "type": "composer",
  "url": "https://brianhenry.ie/satispress/"
 }
 "require-dev": {
  "satispress/my-plugin": "*
 }  

When running composer update you will be prompted (once) for the credentials you created on the site.

Symlinks

If an included WordPress plugin or theme does not install to the project's wp-content folder, it can be symlinked with Composer Symlink Handler.

 "extra": {
  "symlinks": {
   "./vendor/enhancedathlete/ea-wp-aws-sns-client-rest-endpoint/trunk": "./wp-content/plugins/ea-wp-aws-sns-client-rest-endpoint"
  }

More

https://github.com/wikimedia/composer-merge-plugin

https://github.com/wikimedia/composer-merge-plugin

Xdebug with WP ClI. Run once in your Terminal session:

export XDEBUG_CONFIG="idekey=PHPSTORM remote_connect_back=1"

Xdebug with Postman. Append to the URL:

&XDEBUG_SESSION_START=PHPSTORM

Notes

WPPB-Lib

The WordPress Plugin Boilerplate as-is passes the plugin name and version to every class. This code and the loader code are better suited to a library than every plugin's own code, so a branch exists wppb-lib that can be merged. I haven't see where the reason to pass the name and version to every class is. Maybe it can be handled better in the root plugin file in namespaced methods?

Minimum WordPress Version

The minimum WordPress version can be determined using wpseek.com's Plugin Doctor.

TODO

Plugins with WP CLI tests:

Open API

Tools to investigate:

REST

I have made some progress on a lot of these open an issue with changes you propose working on and I'll tidy up what I have and hopefully save you some time

Alternatives

Acknowledgements

The contributors to WordPress Plugin Boilerplate and more.

WP-Browser, Mozart and SatisPress are three tools that really take this project to another level.

About

WordPress Plugin Boilerplate + Composer, namespaces, autoloading, Codeception tests, and GitHub Actions CI/CD to WordPress.org.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages

  • PHP 88.6%
  • TypeScript 4.3%
  • JavaScript 3.4%
  • Shell 3.3%
  • CSS 0.4%