Plugin Development

Common scripts and file structures for WordPress plugin development.

Main Plugin File

The foundation for a new WordPress plugin:

<?php
/**
 * WordPress Plugin
 *
 * @author            Michelle Blanchette
 * @copyright         2022 Michelle Blanchette
 * @license           GPL-3.0-or-later
 *
 * @wordpress-plugin
 * Plugin Name:       WordPress Plugin
 * Description:       
 * Version:           1.0.0
 * Requires PHP:      
 * Requires at least: 
 * Tested up to:      
 * Author:            Purple Turtle Creative
 * Author URI:        https://purpleturtlecreative.com/
 * License:           GPL v3 or later
 * License URI:       https://www.gnu.org/licenses/gpl-3.0.txt
 */

/*
This program is open-source software: you can redistribute it and/or modify
it UNDER THE TERMS of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.txt.
*/

namespace PTC_Plugin;// @TODO: Change namespace.

defined( 'ABSPATH' ) || die();

/**
 * The absolute path to this plugin's main file.
 *
 * @since 1.0.0
 *
 * @var string PLUGIN_FILE
 */
define( __NAMESPACE__ . '\PLUGIN_FILE', __FILE__ );

/**
 * The absolute path to this plugin's directory, ending with a slash.
 *
 * @since 1.0.0
 *
 * @var string PLUGIN_PATH
 */
define( __NAMESPACE__ . '\PLUGIN_PATH', plugin_dir_path( __FILE__ ) );

/**
 * The full url to this plugin's directory, ending with a slash.
 *
 * @since 1.0.0
 *
 * @var string PLUGIN_URL
 */
define( __NAMESPACE__ . '\PLUGIN_URL', plugins_url( '/', __FILE__ ) );

/**
 * This plugin's current version, as defined in the plugin header.
 *
 * @since 1.0.0
 *
 * @var string PLUGIN_VERSION
 */
define( __NAMESPACE__ . '\PLUGIN_VERSION', get_file_data( __FILE__, [ 'Version' => 'Version' ], 'plugin')['Version'] ?? '0.0.0' );

/**
 * This plugin's basename, like "my-plugin/my-plugin.php".
 *
 * @since 1.0.0
 *
 * @var string PLUGIN_BASENAME
 */
define( __NAMESPACE__ . '\PLUGIN_BASENAME', plugin_basename( __FILE__ ) );

/**
 * This plugin's directory basename, like "my-plugin".
 *
 * @since 1.0.0
 *
 * @var string PLUGIN_SLUG
 */
define( __NAMESPACE__ . '\PLUGIN_SLUG', dirname( PLUGIN_BASENAME ) );

/**
 * The namespace for all v1 REST API routes registered by this plugin.
 *
 * @since 1.0.0
 *
 * @var string REST_API_NAMESPACE_V1
 */
define( __NAMESPACE__ . '\REST_API_NAMESPACE_V1', 'ptc-resources/v1' );

/* CODE REGISTRATION */

/**
 * Requires a class file and calls its static "register" method.
 *
 * Class file names and class names must follow WordPress naming conventions.
 * For example, /path/to/class-my-class.php should contain the declaration of
 * class My_Class.
 *
 * Intentionally does not check if a "register" method exists in the class
 * or if the class has been properly included. Doing so would hide ACTUAL errors
 * due to formatting mistakes that should, indeed, be noticed and fixed!
 *
 * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions
 *
 * @param string $file The full class filename.
 */
function register_class_from_file( string $file ) {

	$class_name = str_replace(
		[ 'class-', '-' ],
		[ '', '_' ],
		basename( $file, '.php' )
	);
	$class_name = __NAMESPACE__ . '\\' . ucwords( $class_name, '_' );

	if ( ! class_exists( $class_name ) ) {
		require_once $file;
		$class_name::register();
	}
}

/* Register Public Functionality */
foreach ( glob( PLUGIN_PATH . '/src/public/class-*.php' ) as $file ) {
	register_class_from_file( $file );
}

if ( is_admin() ) {
	/* Register Admin-Only Functionality */
	foreach ( glob( PLUGIN_PATH . '/src/admin/class-*.php' ) as $file ) {
		register_class_from_file( $file );
	}
} else {
	/* Register Frontend-Only Functionality */
	foreach ( glob( PLUGIN_PATH . '/src/public/frontend/class-*.php' ) as $file ) {
		register_class_from_file( $file );
	}
}

bundle.sh

Place this bash script in the root folder of your plugin. I typically name it bundle.sh.

It zips your necessary plugin files into an installable plugin package with the current version appended.

#!/bin/bash

PLUGIN_SLUG=$( basename `pwd` )

VERSION=$( grep -Eio 'Version:\s*[0-9\.]+' "${PLUGIN_SLUG}.php" | grep -Eo '[0-9\.]+' )

cd ..
zip -rT9X "${PLUGIN_SLUG}-${VERSION}.zip" "${PLUGIN_SLUG}" --exclude '*/.git*' '*/.DS_Store' '*.zip' '*.log' '*.sh'

Separate exclude.lst

Instead of listing the exact exclude globs in the bundle.sh script, you can instead specify a separate file. This is helpful for long lists, generic implementations, or shared excludes such as would be used by pipeline logic.

The zip command becomes this:

zip -rT9X "${PLUGIN_SLUG}-${VERSION}.zip" "${PLUGIN_SLUG}" --exclude @"${PLUGIN_SLUG}"/exclude.lst

And the exclude.lst file contains something like this:

completionist/*.zip
completionist/*.log
completionist/*.map
completionist/*.sh
completionist/*.DS_Store
completionist/.git*
completionist/action.yml
completionist/exclude.lst
completionist/composer.json
completionist/composer.lock
completionist/node_modules/*
completionist/package-lock.json
completionist/package.json
completionist/assets/styles/scss/*
completionist/src/components/*
completionist/src/*.js
completionist/src/*.jsx

Notice that the paths are the absolute zip package paths, which begin with the plugin directory’s name (ie. plugin slug).