=== RS Base Plugin ===
Contributors: seindal
Tags: plugin framework
Requires at least: 5.0
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 1.18.1
License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Donate link: https://mypos.com/@historywalks

RS Base Plugin - a really simple class based plugin framework

== Description ==

On its own this plugin does nothing.

The class `\ReneSeindal\PluginBase` is meant as a base class for plugin classes. It is a kind of very simple plugin framework, in reality little more than a class constructor and a handful of helper methods to avoid repetitive coding.

Filters, actions and shortcodes are defined in derived classes simply by making appropriately named class methods. The `PluginBase` class then automatically adds all the filters, actions and shortcodes.

Additional functionality is defined as PHP traits, which can be used in any combination with the base class.

This class is for making KISS plugins, really simple to program and use, but not flashy. Simplicity is the goal, not looks.

= Setup =

A minimal plugin looks like this:

	add_action( 'plugins_loaded', function() {
	    class PluginTemplate extends PluginBase {
	        protected $plugin_file = __FILE__;
	
	        // use PluginBaseSettings;
	
	        // Methods galore
	    }
	
	    new PluginTemplate();
	} );

The class is defined within the `plugins_loaded` hook to make sure the `PluginBase` class is loaded.

All derived plugin classes must set the property `plugin_file` to `__FILE__`, which is needed for several parts of the base plugin.

The `PluginBase` class also implements a static method `instance()` which creates a singleton interface. This is useful if the plugin object is needed elsewhere, e.g., for a WP-CLI interface.

The last line in the example above could therefore also be written as:

	PluginTemplate::instance();

= Actions and filters =

Most plugins interact with the WordPress core through action and filter hooks.

Actions and filters are defined in derived classes simply by making appropriately named class methods.

Hooks for actions and filters are simply correctly named class methods:

* 'do_ACTION_action()' defines a hook for ACTION;
* 'do_ACTION_action_EXTRA()' defines the same hook for ACTION as above. The EXTRA part can be descriptive or allow several callbacks for the same ACTION, which can make sense to keep related code together and readable;
* 'do_FILTER_filter() defines a hook for FILTER;
*  or 'do_FILTER_filter_EXTRA()' is the same hook for FILTER, just as for actions above.

This cannot work in all cases, as WordPress in some cases uses hook names which do not translate to legal PHP identifiers.

In such cases the correct name of the hook can be defined by a protected class property named `$ACTION_callback_hook` or `$FILTER_callback_hook`.

The common case of having the plugin basename in name of a hook can be handled by putting the string `__PLUGIN_BASENAME__` in its place, which the base plugin will substitute automatically. Please note that this often leads to three underscores in a row in a method name.

The priority of an action or filter hook is set by a protected class property named `$ACTION_EXTRA_callback_priority` or `$FILTER_EXTRA_callback_priority`. If not defined the WordPress default of 10 is used.

= Shortcodes =

Shortcodes are likewise defined in derived classes simply by making appropriately named class methods.

* 'do_SHORTCODE_shortcode()' defines a shortcode named SHORTCODE.

= Ajax end-points =

To be done: AJAX call backs

= Options =

Each plugin object has at least these three options/settings related properties:

* options_page -- if not set, it is set to the base-name of $plugin_file with the extension .php removed. This is the default option page URL. See section Settings below.
* options_base -- if not set, it is set to $options_page with all hyphens changed to underscores.
* options_name -- if not set, it is set to $options_base with _options added.

The method `get_options()` will retrieve option `$option_name` which is usually an array of all the plugin's options.

The method `get_option( $name, $default = NULL )` will retrieve the named option from the array of plugin options.

= Settings =

The trait `PluginBaseSettings` defines a set of methods for making simple option pages.

A link to the Settings page is added automatically to the plugin's entry on the Plugin admin page.

The derived class muse use the trait above to activate the settings functionality of base plugin.

	use PluginBaseSettings

The derived class must define a hook for the `admin_menu` action to generate a menu link to the settings page. In most cases the method `settings_add_menu()` will be sufficient:

	function do_admin_menu_action() {
	    $this->settings_add_menu(
	        __( 'Settings page title', 'text-domain' ),
	        __( 'Menu heading', 'text-domain' )
	    );
	}

A settings page is usually made of one or more sections, each containing one or more related fields. Each section and field has some explanatory text.

If the derived class has a `settings_define_sections_and_fields()` method, that is called automatically to define, wait for it, sections and fields.

A section is defined with `PluginBase::settions_add_section()`:

	$section = $this->settings_add_section(
	        'section_id',
	        __( 'Display name', 'text-domain' ),
	        __( 'Explanatory text', 'text-domain' )
	);
	

The return value is needed for adding fields to the section.

A field is created and added to a section by `PluginBase::settings_add_field()`, in this way:

	$this->settings_add_field(
	    'field_id', $section,
	    __( 'Field name', 'text-domain' ),
	    'settings_field_XXX_html',
	    $args
	);

The `field_id` is a unique identifier for the field -- which can be used with `PluginBase::get_option()` to retrieve the value -- and the translated string is for display.

The field is rendered by plugin method, in the example above indicated as `settings_field_XXX_html` with additional arguments `$args`, an associative array.

The `PluginBase` class defines field renderer methods for the most common input types.

**Single line text input** fields can use  `settings_field_input_html` which takes these arguments:

* type -- input type (optional, defaults to 'text')
* default -- default value if no value has ever been saved

**Multiline text input**, or textareas, can use `settings_field_textarea_html`:

* default -- default value if no value has ever been saved

**Checkboxes** can use `settings_field_checkbox_html`:

* default -- default value if no value has ever been saved

**Menus** with single or multiple selection can use `settings_field_select_html`:

* multiple -- whether to allow multiple sections (optional, boolean, default false)
* size -- how many rows to show for multiple selections (default as many as are needed)
* values -- an associative array of the allowed selection; the array key is the value of the option, the array value is the text to display
* default -- default value if no value has ever been saved

The cases of creating multiple selection menus for post types or taxonomies have some extra helpers.

	$this->settings_build_post_types_menu(
	    $field, $section, $label,
	    $query, $default, $hooks
	);

Here `$query`Â is the argument to `get_post_types()` ð, while `$default` is the defaults selection. The last `$hooks` (string|array) are filters through which the list of post types to display is run before rendering the menu.

There's a similar `settings_build_taxonomies_menu()`.

An minimal example of a settings page with just a post type selection menu (from [RS Word Count](https://plugins.seindal.dk/plugins/rs-word-count/)):

	function do_admin_menu_action() {
	    $this->settings_add_menu(
	        __( 'Word count', 'rs-word-count' ),
	        __( 'RS Word count', 'rs-word-count' )
	    );
	}
	
	function settings_define_sections_and_fields() {
	    $section = $this->settings_add_section(
	        'section_post_types',
	        __( 'Post-types', 'rs-word-count' ),
	        __( 'Select the post-types you want to count words in', 'rs-word-count' )
	    );
	
	    $this->settings_build_post_types_menu(
	        'post_types', $section,
	        __( 'Post-types', 'rs-word-count' ),
	        [ 'public' => true ],
	        $this->default_post_types,
	        [
	            'rs_word_count_post_types',
	            'rs_custom_post_types',
	        ]
	    );
	}

= Other traits =

The trait `PluginBaseCustomLoginPage` puts the site logo on the login page instead of the default WordPress logo.

The trait `PluginBaseYoastSeo` removes the Yoast SEO filter menus from the admin interface.

== Installation ==

The plugin can be installed as any other plugin.

Other plugins using the framework should be defined in the `plugins_loaded` hook, to ensure the `PluginBase` class is defined first. If derived classes are loaded earlier, WordPress might report errors.

= As a must-use plugin =

This can also be resolved by copying the main plugin file to the wp-content/mu-plugins/ folder, where must-use plugins reside. They're always loaded before the normal plugins.

This should never be necessary if the plugins with derived classes behave as explained above.

A WP-CLI command 'rsbp' can install or remove this must-use plugin. Install the must-use plugin with `wp rsbp on`, remove it with `wp rsbp off` and check the status with `wp rsbp status`.

Currently the must-used plugin is created as a symlink to the normal plugin, so updates take effect automatically when the normal plugin is updated. It doesn't have to be activated in that case.

= Resolving errors =

If WP-CLI reports errors, use the --skip-plugins to avoid loading the offending plugins until the situation is resolved.

Alternatively, on the URL of the WordPress login screen, add the query string `?action=entered_recovery_mode` at the end, and WordPress will not load plugins.

== Changelog ==

= 1.15 =

* Now uses use ReflectionMethod to find argument count for hooks.

= 1.0 =

* First version.
