Skip to content

Instantly share code, notes, and snippets.

@DrewAPicture
Created March 22, 2021 20:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DrewAPicture/a855a49e2fdbe2dff552c8bfc81700c6 to your computer and use it in GitHub Desktop.
Save DrewAPicture/a855a49e2fdbe2dff552c8bfc81700c6 to your computer and use it in GitHub Desktop.
<?php
/**
* Plugin With Checks Bootstrap
*
* @package Plugin With Checks
* @subpackage Core
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @since 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Plugin_With_Checks' ) ) {
/**
* Setup class.
*
* @since 1.0.0
*/
final class Plugin_With_Checks {
/**
* Holds the instance.
*
* Ensures that only one instance of the main plugin class exists in memory at any one
* time and it also prevents needing to define globals all over the place.
*
* TL;DR This is a static property property that holds the singleton instance.
*
* @access private
* @var \Plugin_With_Checks
* @static
*
* @since 1.0.0
*/
private static $instance;
/**
* The version number.
*
* @access private
* @since 1.0.0
* @var string
*/
private $version = '1.0.0';
/**
* Main plugin file.
*
* @since 1.0.0
* @var string
*/
private $file = '';
/**
* Generates the main plugin instance.
*
* Insures that only one instance of the plugin exists in memory at any one
* time. Also prevents needing to define globals all over the place.
*
* @since 1.0.0
* @static
*
* @param string $file Main plugin file.
* @return \Plugin_With_Checks The one true plugin class instance.
*/
public static function instance( $file = null ) {
if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Plugin_With_Checks ) ) {
self::$instance = new \Plugin_With_Checks;
self::$instance->file = $file;
self::$instance->setup_constants();
self::$instance->includes();
self::$instance->init();
self::$instance->hooks();
self::$instance->setup_objects();
}
return self::$instance;
}
/**
* Throws an error on object clone.
*
* The whole idea of the singleton design pattern is that there is a single
* object therefore, we don't want the object to be cloned.
*
* @access protected
* @since 1.0.0
*
* @return void
*/
protected function __clone() {
// Cloning instances of the class is forbidden
_doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh? This object cannot be cloned.', 'plugin-with-checks' ), '1.0.0' );
}
/**
* Disables unserializing of the class.
*
* @access protected
* @since 1.0.0
*
* @return void
*/
protected function __wakeup() {
// Unserializing instances of the class is forbidden
_doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh? This class cannot be unserialized.', 'plugin-with-checks' ), '1.0.0' );
}
/**
* Sets up the class.
*
* @access private
* @since 1.0.0
*/
private function __construct() {
self::$instance = $this;
}
/**
* Resets the instance of the class.
*
* @access public
* @since 1.0.0
* @static
*/
public static function reset() {
self::$instance = null;
}
/**
* Setup plugin constants
*
* @access private
* @since 1.0.0
*
* @return void
*/
private function setup_constants() {
// Plugin version
if ( ! defined( 'PLUGIN_W_CHECKS_VERSION' ) ) {
define( 'PLUGIN_W_CHECKS_VERSION', $this->version );
}
// Plugin Folder Path
if ( ! defined( 'PLUGIN_W_CHECKS_PLUGIN_DIR' ) ) {
define( 'PLUGIN_W_CHECKS_PLUGIN_DIR', plugin_dir_path( $this->file ) );
}
// Plugin Folder URL
if ( ! defined( 'PLUGIN_W_CHECKS_PLUGIN_URL' ) ) {
define( 'PLUGIN_W_CHECKS_PLUGIN_URL', plugin_dir_url( $this->file ) );
}
// Plugin Root File
if ( ! defined( 'PLUGIN_W_CHECKS_PLUGIN_FILE' ) ) {
define( 'PLUGIN_W_CHECKS_PLUGIN_FILE', $this->file );
}
}
/**
* Include necessary files.
*
* @access private
* @since 1.0.0
*
* @return void
*/
private function includes() {}
/**
* Initializes the plugin.
*
* @since 1.0.0
*/
private function init() {}
/**
* Setup all objects
*
* @access public
* @since 1.0.0
* @return void
*/
public function setup_objects() {}
/**
* Sets up the default hooks and actions.
*
* @access private
* @since 1.0.0
*
* @return void
*/
private function hooks() {}
}
/**
* The main function responsible for returning the one true plugin class instance to functions everywhere.
*
* Use this function like you would a global variable, except without needing
* to declare the global.
*
* Example: <?php $plugin_with_checks = plugin_with_checks(); ?>
*
* @since 1.0.0
*
* @return \Plugin_With_Checks The one true plugin class instance.
*/
function plugin_with_checks() {
return Plugin_With_Checks::instance();
}
}
<?php
/**
* Sandhills Development Minimum Requirements API
*
* For use by Sandhills products and their extensions.
*
* @package SandhillsDev
* @subpackage Tools
* @copyright Copyright (c) 2021, Sandhills Development, LLC
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* Class used by AffiliateWP to enforce minimum requirements for itself and its add-ons.
*
* @since 1.0.0
* @abstract
*/
abstract class Sandhills_Requirements_Check {
/**
* Plugin base file.
*
* @since 1.0.0
* @var string
*/
private $file = '';
/**
* Plugin basename.
*
* @since 1.0.0
* @var string
*/
private $base = '';
/**
* Plugin slug.
*
* @since 1.0.0
* @var string
*/
protected $slug = 'sandhills-dev';
/**
* Requirements array.
*
* @since 1.0.0
* @var array[]
*/
protected $requirements = array(
// PHP.
'php' => array(
'minimum' => '7.4',
'name' => 'PHP',
'exists' => true,
'current' => false,
'checked' => false,
'met' => false
),
// WordPress.
'wp' => array(
'minimum' => '5.0.0',
'name' => 'WordPress',
'exists' => true,
'current' => false,
'checked' => false,
'met' => false
),
);
/**
* Add-on requirements array.
*
* @since 1.0.0
* @var array
*/
protected $addon_requirements = array();
/**
* Sets up the plugin requirements class.
*
* @since 1.0.0
*
* @param string $file Main plugin file.
*/
public function __construct( $file ) {
// Setup file & base.
$this->file = $file;
$this->base = plugin_basename( $this->get_file() );
// Merge add-on requirements (if any).
$this->requirements = array_merge( $this->requirements, $this->addon_requirements );
// Always load translations.
add_action( 'plugins_loaded', array( $this, 'load_textdomain' ) );
}
/**
* Retrieves the main plugin file.
*
* @since 1.0.0
*
* @return string Main plugin file.
*/
public function get_file() {
return $this->file;
}
/**
* (Maybe) loads the plugin.
*
* @since 1.0.0
*/
public function maybe_load() {
// Load or quit.
$this->met() ? $this->load() : $this->quit();
}
/**
* Getter for requirements.
*
* The requirements class automatically supports checking wp- and php-keyed requirements.
*
* Add-ons can register custom requirements (or override defaults outlined in `$requirements`) by
* defining the `$addon_requirements` property in its own sub-class. If overriding default requirements,
* the same keys and metadata - save for the new version numbers - should be used.
*
* Custom requirement example:
*
* protected $addon_requirements = array(
* // AffiliateWP.
* 'affwp' => array(
* 'minimum' => '2.7',
* 'name' => 'AffiliateWP',
* 'exists' => true,
* 'current' => false,
* 'checked' => false,
* 'met' => false
* ),
* );
*
* To hook up the version check, a corresponding method for each custom requirement MUST be added.
* The simplify this, the requirements class will automatically look for a method named
* "check_{requirement_name}".
*
* For example, for the 'affwp' requirement:
*
* public function check_affwp() {
* return get_option( 'affwp_version' );
* }
*
*
* @since 1.0.0
*
* @return array Plugin requirements.
*/
protected function get_requirements() {
return $this->requirements;
}
/**
* Quits without loading the plugin.
*
* @since 1.0.0
*/
protected function quit() {
add_action( 'admin_head', array( $this, 'admin_head' ) );
add_filter( "plugin_action_links_{$this->base}", array( $this, 'plugin_row_links' ) );
add_action( "after_plugin_row_{$this->base}", array( $this, 'plugin_row_notice' ) );
}
//
// Specific Methods
//
/**
* Handles actually loading the plugin.
*
* @since 1.0.0
*/
abstract protected function load();
/**
* Install, usually on an activation hook.
*
* Note: A sub-class extension of this method is typically a good place to call a relevant
* install function or set the add-on version option directly.
*
* @since 1.0.0
*/
public function install() {
// Bootstrap to include all of the necessary files
$this->bootstrap();
}
/**
* Bootstraps everything.
*
* @since 1.0.0
*/
public function bootstrap() {
\Sandhills_Dev::instance( $this->get_file() );
}
/**
* Sets the plugin-specific URL for an external requirements page.
*
* @since 1.0.0
*
* @return string Unmet requirements URL.
*/
protected function unmet_requirements_url() {
return '';
}
/**
* Sets the plugin-specific text to quickly explain what's wrong.
*
* @since 1.0.0
*
* @return string Unmet requirements text.
*/
private function unmet_requirements_text() {
esc_html_e( 'This plugin is not fully active.', 'sandhills-dev' );
}
/**
* Sets the plugin-specific text to describe a single unmet requirement.
*
* @since 1.0.0
*
* @return string Unment requirements description text.
*/
private function unmet_requirements_description_text() {
return esc_html__( 'Requires %s (%s), but (%s) is installed.', 'sandhills-dev' );
}
/**
* Sets the plugin-specific text to describe a single missing requirement.
*
* @since 1.0.0
*
* @return string Unmet missing requirements text.
*/
private function unmet_requirements_missing_text() {
return esc_html__( 'Requires %s (%s), but it appears to be missing.', 'sandhills-dev' );
}
/**
* Sets the plugin-specific text used to link to an external requirements page.
*
* @since 1.0.0
*
* @return string Unmet requirements link text.
*/
private function unmet_requirements_link() {
return esc_html__( 'Requirements', 'sandhills-dev' );
}
/**
* Sets the plugin-specific aria label text to describe the requirements link.
*
* @since 1.0.0
*
* @return string Aria label text.
*/
protected function unmet_requirements_label() {
return esc_html__( 'AffiliateWP Requirements', 'sandhills-dev' );
}
/**
* Sets the plugin-specific text used in CSS to identify attribute IDs and classes.
*
* @since 1.0.0
*
* @return string CSS selector.
*/
protected function unmet_requirements_name() {
return 'affwp-requirements';
}
//
// Agnostic Methods
//
/**
* Sets up the plugin-agnostic method to output the additional plugin row.
*
* @since 1.0.0
*/
public function plugin_row_notice() {
?><tr class="active <?php echo esc_attr( $this->unmet_requirements_name() ); ?>-row">
<th class="check-column">
<span class="dashicons dashicons-warning"></span>
</th>
<td class="column-primary">
<?php $this->unmet_requirements_text(); ?>
</td>
<td class="column-description">
<?php $this->unmet_requirements_description(); ?>
</td>
</tr><?php
}
/**
* Sets up the plugin-agnostic method used to output all unmet requirement information.
*
* @since 1.0.0
*/
private function unmet_requirements_description() {
foreach ( $this->requirements as $properties ) {
if ( empty( $properties['met'] ) ) {
$this->unmet_requirement_description( $properties );
}
}
}
/**
* Sets up the plugin-agnostic method to output specific unmet requirement information
*
* @since 1.0.0
*
* @param array $requirement Requirements array.
*/
private function unmet_requirement_description( $requirement = array() ) {
// Requirement exists, but is out of date
if ( ! empty( $requirement['exists'] ) ) {
$text = sprintf(
$this->unmet_requirements_description_text(),
'<strong>' . esc_html( $requirement['name'] ) . '</strong>',
'<strong>' . esc_html( $requirement['minimum'] ) . '</strong>',
'<strong>' . esc_html( $requirement['current'] ) . '</strong>'
);
// Requirement could not be found
} else {
$text = sprintf(
$this->unmet_requirements_missing_text(),
'<strong>' . esc_html( $requirement['name'] ) . '</strong>',
'<strong>' . esc_html( $requirement['minimum'] ) . '</strong>'
);
}
// Output the description
echo '<p>' . $text . '</p>';
}
/**
* Sets up the plugin-agnostic method to output unmet requirements styling
*
* @since 1.0.0
*/
public function admin_head() {
// Get the requirements row name
$name = $this->unmet_requirements_name(); ?>
<style id="<?php echo esc_attr( $name ); ?>">
.plugins tr[data-plugin="<?php echo esc_html( $this->base ); ?>"] th,
.plugins tr[data-plugin="<?php echo esc_html( $this->base ); ?>"] td,
.plugins .<?php echo esc_html( $name ); ?>-row th,
.plugins .<?php echo esc_html( $name ); ?>-row td {
background: #fff5f5;
}
.plugins tr[data-plugin="<?php echo esc_html( $this->base ); ?>"] th {
box-shadow: none;
}
.plugins .<?php echo esc_html( $name ); ?>-row th span {
margin-left: 6px;
color: #dc3232;
}
.plugins tr[data-plugin="<?php echo esc_html( $this->base ); ?>"] th,
.plugins .<?php echo esc_html( $name ); ?>-row th.check-column {
border-left: 4px solid #dc3232 !important;
}
.plugins .<?php echo esc_html( $name ); ?>-row .column-description p {
margin: 0;
padding: 0;
}
.plugins .<?php echo esc_html( $name ); ?>-row .column-description p:not(:last-of-type) {
margin-bottom: 8px;
}
</style>
<?php
}
/**
* Sets up the plugin-agnostic method to add the "Requirements" link to row actions
*
* @since 1.0.0
*
* @param array $links Requirement links.
* @return array Requirement links with markup.
*/
public function plugin_row_links( $links = array() ) {
// Add the Requirements link
$links['requirements'] =
'<a href="' . esc_url( $this->unmet_requirements_url() ) . '" aria-label="' . esc_attr( $this->unmet_requirements_label() ) . '">'
. esc_html( $this->unmet_requirements_link() )
. '</a>';
// Return links with Requirements link
return $links;
}
//
// Checkers
//
/**
* Sets up the plugin-specific requirements checker.
*
* @since 1.0.0
*/
private function check() {
// Loop through requirements
foreach ( $this->requirements as $dependency => $properties ) {
if ( method_exists( $this, 'check_' . $dependency ) ) {
$version = call_user_func( array( $this, 'check_' . $dependency ) );
} else {
$version = false;
}
// Merge to original array
if ( ! empty( $version ) ) {
$this->requirements[ $dependency ] = array_merge( $this->requirements[ $dependency ], array(
'current' => $version,
'checked' => true,
'met' => version_compare( $version, $properties['minimum'], '>=' )
) );
}
}
}
/**
* Checks the PHP version.
*
* @since 1.0.0
*
* @return string PHP version.
*/
protected function check_php() {
return phpversion();
}
/**
* Checks the WordPress version.
*
* @since 1.0.0
*
* @return string WordPress version.
*/
protected function check_wp() {
return get_bloginfo( 'version' );
}
/**
* Determines if all requirements been met.
*
* @since 1.0.0
*
* @return bool True if met, otherwise false.
*/
public function met() {
// Run the check
$this->check();
// Default to true (any false below wins)
$retval = true;
$to_meet = wp_list_pluck( $this->requirements, 'met' );
// Look for unmet dependencies, and exit if so
foreach ( $to_meet as $met ) {
if ( empty( $met ) ) {
$retval = false;
continue;
}
}
// Return
return $retval;
}
//
// Translations
//
/**
* Handles loading the plugin-specific text-domain.
*
* @since 1.0.0
*
* @return void
*/
public function load_textdomain() {
// Set filter for plugin's languages directory.
$lang_dir = dirname( plugin_basename( $this->get_file() ) ) . '/languages/';
/**
* Filters the languages directory for AffiliateWP - Affiliate Portal plugin.
*
* @since 1.0
*
* @param string $lang_dir Language directory.
*/
$lang_dir = apply_filters( $this->base . '_languages_directory', $lang_dir );
// Traditional WordPress plugin locale filter.
$locale = apply_filters( 'plugin_locale', get_locale(), $this->slug );
$mofile = sprintf( '%1$s-%2$s.mo', $this->slug, $locale );
// Setup paths to current locale file.
$mofile_local = $lang_dir . $mofile;
$mofile_global = WP_LANG_DIR . '/' . $lang_dir . '/' . $mofile;
if ( file_exists( $mofile_global ) ) {
// Look in global /wp-content/languages/{plugin_dir}/ folder.
load_textdomain( $this->slug, $mofile_global );
} elseif ( file_exists( $mofile_local ) ) {
// Look in local /wp-content/plugins/{plugin_dir}/languages/ folder.
load_textdomain( $this->slug, $mofile_local );
} else {
// Load the default language files.
load_plugin_textdomain( $this->slug, false, $lang_dir );
}
}
}
<?php
/**
* Plugin Name: Plugin With Checks
* Plugin URI: https://sandhillsdev.com
* Description: Example plugin demonstrating stable partial activation and requirements checking.
* Author: Sandhills Development, LLC
* Author URI: https://sandhillsdev.com
* Version: 1.0.0
* Text Domain: plugin-with-checks
* Domain Path: languages
*/
if ( ! class_exists( 'Sandhills_Requirements_Check' ) ) {
require_once dirname( __FILE__ ) . '/class-sandhills-requirements-check.php';
}
/**
* Class used to check requirements for and bootstrap the plugin.
*
* @since 1.0.0
*
* @see Sandhills_Requirements_Check
*/
class Plugin_With_Checks_Requirements extends Sandhills_Requirements_Check {
/**
* Plugin slug.
*
* @since 1.0.0
* @var string
*/
protected $slug = 'plugin-with-checks';
/**
* Add-on requirements.
*
* @since 1.0.0
* @var array[]
*/
protected $addon_requirements = array(
// AffiliateWP.
'affwp' => array(
'minimum' => '2.7',
'name' => 'AffiliateWP',
'exists' => true,
'current' => false,
'checked' => false,
'met' => false
),
// WordPress.
'wp' => array(
'minimum' => '5.7.0',
'name' => 'WordPress',
'exists' => true,
'current' => false,
'checked' => false,
'met' => false
),
);
/**
* Checks the AffiliateWP version.
*
* @since 1.0.0
*
* @return string AffiliateWP version.
*/
protected function check_affwp() {
return get_option( 'affwp_version' );
}
/**
* Bootstrap everything.
*
* @since 1.0.0
*/
public function bootstrap() {
\Plugin_With_Checks::instance( __FILE__ );
}
/**
* Loads the add-on.
*
* @since 1.0.0
*/
protected function load() {
// Maybe include the bundled bootstrapper.
if ( ! class_exists( 'Plugin_With_Checks' ) ) {
require_once dirname( __FILE__ ) . '/class-plugin-with-checks.php';
}
// Maybe hook-in the bootstrapper.
if ( class_exists( 'Plugin_With_Checks' ) ) {
add_action( 'plugins_loaded', array( $this, 'bootstrap' ), 100 );
// Register the activation hook.
register_activation_hook( __FILE__, array( $this, 'install' ) );
}
}
/**
* Install, usually on an activation hook.
*
* @since 1.0.0
*/
public function install() {
// Bootstrap to include all of the necessary files
$this->bootstrap();
if ( defined( 'PLUGIN_W_CHECKS_VERSION' ) ) {
update_option( 'plugin_w_checks_version', PLUGIN_W_CHECKS_VERSION );
}
}
/**
* Plugin-specific aria label text to describe the requirements link.
*
* @since 1.0.0
*
* @return string Aria label text.
*/
protected function unmet_requirements_label() {
return esc_html__( 'Plugin With Checks Requirements', 'plugin-with-checks' );
}
/**
* Plugin-specific text used in CSS to identify attribute IDs and classes.
*
* @since 1.0.0
*
* @return string CSS selector.
*/
protected function unmet_requirements_name() {
return 'plugin-with-checks-requirements';
}
/**
* Plugin specific URL for an external requirements page.
*
* @since 1.0.0
*
* @return string Unmet requirements URL.
*/
protected function unmet_requirements_url() {
return 'https://wiki-or-site.com/with/minimum/requirements';
}
}
$requirements = new Plugin_With_Checks_Requirements( __FILE__ );
$requirements->maybe_load();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment