{"id":17125,"date":"2023-12-04T17:15:21","date_gmt":"2023-12-04T16:15:21","guid":{"rendered":"https:\/\/plugins.seindal.dk\/?page_id=17125"},"modified":"2024-04-16T19:58:20","modified_gmt":"2024-04-16T17:58:20","slug":"rs-base-plugin","status":"publish","type":"rsplugin","link":"https:\/\/plugins.seindal.dk\/plugins\/rs-base-plugin\/","title":{"rendered":"RS Base Plugin"},"content":{"rendered":"\n<p>RS Base Plugin &#8211; a really simple class based plugin framework<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Description<\/h2>\n\n\n\n<p>On its own this plugin does nothing.<\/p>\n\n\n\n<p>The class <code>\\ReneSeindal\\PluginBase<\/code> 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.<\/p>\n\n\n\n<p>Filters, actions and shortcodes are defined in derived classes simply by making appropriately named class methods. The <code>PluginBase<\/code> class then automatically adds all the filters, actions and shortcodes.<\/p>\n\n\n\n<p>Additional functionality is defined as PHP traits, which can be used in any combination with the base class.<\/p>\n\n\n\n<p>This class is for making KISS plugins, really simple to program and use, but not flashy. Simplicity is the goal, not looks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Setup<\/h3>\n\n\n\n<p>A minimal plugin looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>add_action( 'plugins_loaded', function() {\n    class PluginTemplate extends PluginBase {\n        protected $plugin_file = __FILE__;\n\n        \/\/ use PluginBaseSettings;\n\n        \/\/ Methods galore\n    }\n\n    new PluginTemplate();\n} );<\/code><\/pre>\n\n\n\n<p>The class is defined within the <code>plugins_loaded<\/code> hook to make sure the <code>PluginBase<\/code> class is loaded.<\/p>\n\n\n\n<p>All derived plugin classes must set the property <code>plugin_file<\/code> to <code>__FILE__<\/code>, which is needed for several parts of the base plugin.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Singleton interface<\/h4>\n\n\n\n<p>The <code>PluginBase<\/code> class also implements a static method <code>instance()<\/code> which creates a singleton interface. This is useful if the plugin object is needed elsewhere, e.g., for a WP-CLI interface.<\/p>\n\n\n\n<p>The last line in the example above could therefore also be written as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PluginTemplate::instance();<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Actions and filters<\/h3>\n\n\n\n<p>Most plugins interact with the WordPress core through action and filter hooks.<\/p>\n\n\n\n<p>Actions and filters are defined in derived classes simply by making appropriately named class methods.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Hooks<\/h4>\n\n\n\n<p>Hooks for actions and filters are simply correctly named class methods:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8216;<code>do_<em>ACTION<\/em>_action()<\/code>&#8216; defines a hook for <em>ACTION<\/em>;<\/li>\n\n\n\n<li>&#8216;<code>do_<em>ACTION<\/em>_action_<em>EXTRA<\/em>()<\/code>&#8216; defines the same hook for <em>ACTION<\/em> as above. The <em>EXTRA<\/em> part can be descriptive or allow several callbacks for the same <em>ACTION<\/em>, which can make sense to keep related code together and readable;<\/li>\n\n\n\n<li>&#8216;<code>do_<em>FILTER<\/em>_filter()<\/code> defines a hook for <em><em>FILTER<\/em><\/em>;<\/li>\n\n\n\n<li> or &#8216;<code>do_<em>FILTER<\/em>_filter_<em>EXTRA<\/em>()<\/code>&#8216; is the same hook for <em>FILTER<\/em>, just as for actions above.<\/li>\n<\/ul>\n\n\n\n<p>This cannot work in all cases, as WordPress in some cases uses hook names which do not translate to legal PHP identifiers.<\/p>\n\n\n\n<p>In such cases the correct name of the hook can be defined by a protected class property named <code>$<em>ACTION<\/em>_callback_hook<\/code> or <code>$<em>FILTER<\/em>_callback_hook<\/code>.<\/p>\n\n\n\n<p>The common case of having the plugin basename in name of a hook can be handled by putting the string <code>__PLUGIN_BASENAME__<\/code> 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.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Priority<\/h4>\n\n\n\n<p>The priority of an action or filter hook is set by a protected class property named <code>$<em>ACTION_EXTRA<\/em>_callback_priority<\/code> or <code>$<em>FILTER_EXTRA<\/em>_callback_priority<\/code>. If not defined the WordPress default of 10 is used.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Shortcodes<\/h3>\n\n\n\n<p>Shortcodes are likewise defined in derived classes simply by making appropriately named class methods.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8216;<code>do_<em>SHORTCODE<\/em>_shortcode()<\/code>&#8216; defines a shortcode named <em>SHORTCODE<\/em>.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Ajax end-points<\/h3>\n\n\n\n<p>To be done: AJAX call backs<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Options<\/h3>\n\n\n\n<p>Each plugin object has at least these three options\/settings related properties:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>options_page<\/code> &#8212; if not set, it is set to the base-name of <code>$plugin_file<\/code> with the extension <code>.php<\/code> removed. This is the default option page URL. See section Settings below.<\/li>\n\n\n\n<li><code>options_base<\/code> &#8212; if not set, it is set to $options_page with all hyphens changed to underscores.<\/li>\n\n\n\n<li><code>options_name<\/code> &#8212; if not set, it is set to <code>$options_base<\/code> with <code>_options<\/code> added.<\/li>\n<\/ul>\n\n\n\n<p>The method <code>get_options()<\/code> will retrieve option <code>$option_name<\/code> which is usually an array of all the plugin&#8217;s options.<\/p>\n\n\n\n<p>The method <code>get_option( $name, $default = NULL )<\/code> will retrieve the named option from the array of plugin options.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Settings<\/h3>\n\n\n\n<p>The trait <code>PluginBaseSettings<\/code> defines a set of methods for making simple option pages.<\/p>\n\n\n\n<p>A link to the Settings page is added automatically to the plugin&#8217;s entry on the Plugin admin page.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Plugin requirements<\/h4>\n\n\n\n<p>The derived class muse use the trait above to activate the settings functionality of base plugin.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>use PluginBaseSettings<\/code><\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Adding the settings page to the menu<\/h4>\n\n\n\n<p>The derived class must define a hook for the <code>admin_menu<\/code> action to generate a menu link to the settings page. In most cases the method <code>settings_add_menu()<\/code> will be sufficient:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function do_admin_menu_action() {\n    $this->settings_add_menu(\n        __( 'Settings page title', 'text-domain' ),\n        __( 'Menu heading', 'text-domain' )\n    );\n}<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Generating the settings page<\/h4>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>If the derived class has a <code>settings_define_sections_and_fields()<\/code> method, that is called automatically to define, wait for it, sections and fields.<\/p>\n\n\n\n<p>A section is defined with <code>PluginBase::settions_add_section()<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$section = $this-&gt;settings_add_section(\n        'section_id',\n        __( 'Display name', 'text-domain' ),\n        __( 'Explanatory text', 'text-domain' )\n);\n<\/code><\/pre>\n\n\n\n<p>The return value is needed for adding fields to the section.<\/p>\n\n\n\n<p>A field is created and added to a section by <code>PluginBase::settings_add_field()<\/code>, in this way:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$this->settings_add_field(\n    'field_id', $section,\n    __( 'Field name', 'text-domain' ),\n    'settings_field_XXX_html',\n    $args\n);<\/code><\/pre>\n\n\n\n<p>The <code>field_id<\/code> is a unique identifier for the field &#8212; which can be used with <code>PluginBase::get_option()<\/code> to retrieve the value &#8212; and the translated string is for display.<\/p>\n\n\n\n<p>The field is rendered by plugin method, in the example above indicated as <code>settings_field_XXX_html<\/code> with additional arguments <code>$args<\/code>, an associative array.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Field types<\/h4>\n\n\n\n<p>The <code>PluginBase<\/code> class defines field renderer methods for the most common input types.<\/p>\n\n\n\n<p><strong>Single line text input<\/strong> fields can use  <code>settings_field_input_html<\/code> which takes these arguments:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>type<\/strong> &#8212; input type (optional, defaults to &#8216;text&#8217;)<\/li>\n\n\n\n<li><strong>default<\/strong> &#8212; default value if no value has ever been saved<\/li>\n<\/ul>\n\n\n\n<p><strong>Multiline text input<\/strong>, or textareas, can use <code>settings_field_textarea_html<\/code>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>default<\/strong> &#8212; default value if no value has ever been saved<\/li>\n<\/ul>\n\n\n\n<p><strong>Checkboxes<\/strong> can use <code>settings_field_checkbox_html<\/code>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>default<\/strong> &#8212; default value if no value has ever been saved<\/li>\n<\/ul>\n\n\n\n<p><strong>Menus<\/strong> with single or multiple selection can use <code>settings_field_select_html<\/code>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>multiple<\/strong> &#8212; whether to allow multiple sections (optional, boolean, default false)<\/li>\n\n\n\n<li><strong>size<\/strong> &#8212; how many rows to show for multiple selections (default as many as are needed)<\/li>\n\n\n\n<li><strong>values<\/strong> &#8212; an associative array of the allowed selection; the array key is the value of the option, the array value is the text to display<\/li>\n\n\n\n<li><strong>default<\/strong> &#8212; default value if no value has ever been saved<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Post types and taxonomy menus<\/h4>\n\n\n\n<p>The cases of creating multiple selection menus for post types or taxonomies have some extra helpers.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$this->settings_build_post_types_menu(\n    $field, $section, $label,\n    $query, $default, $hooks\n);<\/code><\/pre>\n\n\n\n<p>Here <code>$query<\/code>\u00a0is the argument to <code>get_post_types()<\/code> \ud83d\udd17, while <code>$default<\/code> is the defaults selection. The last <code>$hooks<\/code> (string|array) are filters through which the list of post types to display is run before rendering the menu.<\/p>\n\n\n\n<p>There&#8217;s a similar <code>settings_build_taxonomies_menu()<\/code>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Minimal settings page example<\/h4>\n\n\n\n<p>An minimal example of a settings page with just a post type selection menu (from <a href=\"https:\/\/plugins.seindal.dk\/plugins\/rs-word-count\/\">RS Word Count<\/a>):<\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>function do_admin_menu_action() {\n    $this->settings_add_menu(\n        __( 'Word count', 'rs-word-count' ),\n        __( 'RS Word count', 'rs-word-count' )\n    );\n}\n\nfunction settings_define_sections_and_fields() {\n    $section = $this->settings_add_section(\n        'section_post_types',\n        __( 'Post-types', 'rs-word-count' ),\n        __( 'Select the post-types you want to count words in', 'rs-word-count' )\n    );\n\n    $this->settings_build_post_types_menu(\n        'post_types', $section,\n        __( 'Post-types', 'rs-word-count' ),\n        &#091; 'public' => true ],\n        $this->default_post_types,\n        &#091;\n            'rs_word_count_post_types',\n            'rs_custom_post_types',\n        ]\n    );\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Other traits<\/h3>\n\n\n\n<p>The trait <code>PluginBaseCustomLoginPage<\/code> puts the site logo on the login page instead of the default WordPress logo.<\/p>\n\n\n\n<p>The trait <code>PluginBaseYoastSeo<\/code> removes the Yoast SEO filter menus from the admin interface.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Installation<\/h2>\n\n\n\n<p>The plugin can be installed as any other plugin.<\/p>\n\n\n\n<p>Other plugins using the framework should be defined in the <code>plugins_loaded<\/code> hook, to ensure the <code>PluginBase<\/code> class is defined first. If derived classes are loaded earlier, WordPress might report errors.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">As a must-use plugin<\/h3>\n\n\n\n<p>This can also be resolved by copying the main plugin file to the wp-content\/mu-plugins\/ folder, where must-use plugins reside. They&#8217;re always loaded before the normal plugins.<\/p>\n\n\n\n<p>This should never be necessary if the plugins with derived classes behave as explained above.<\/p>\n\n\n\n<p>A WP-CLI command &#8216;rsbp&#8217; can install or remove this must-use plugin. Install the must-use plugin with <code>wp rsbp on<\/code>, remove it with <code>wp rsbp off<\/code> and check the status with <code>wp rsbp status<\/code>.<\/p>\n\n\n\n<p>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&#8217;t have to be activated in that case.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Resolving errors<\/h3>\n\n\n\n<p>If WP-CLI reports errors, use the &#8211;skip-plugins to avoid loading the offending plugins until the situation is resolved.<\/p>\n\n\n\n<p>Alternatively, on the URL of the WordPress login screen, add the query string <code>?action=entered_recovery_mode<\/code> at the end, and WordPress will not load plugins.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Changelog<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1.15<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Now uses use <code>ReflectionMethod<\/code> to find argument count for hooks.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">1.0<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>First version.<\/li>\n<\/ul>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Download<\/h2>\n\n\n\n<div class=\"wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\"><a href=\"https:\/\/plugins.seindal.dk\/download\/rs-base-plugin-1.18.1.zip\">Download stable version 1.18.1<\/a>.\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>A really simple class based plugin framework<\/p>\n","protected":false},"author":2,"featured_media":0,"menu_order":0,"comment_status":"open","ping_status":"closed","template":"","meta":{"activitypub_content_warning":"","activitypub_content_visibility":"","activitypub_max_image_attachments":3,"activitypub_interaction_policy_quote":"anyone","activitypub_status":"","footnotes":""},"categories":[13,5],"tags":[21],"plugin-requirements":[],"class_list":["post-17125","rsplugin","type-rsplugin","status-publish","hentry","category-base-plugins","category-self-contained","tag-plugin-framework"],"_links":{"self":[{"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/rsplugin\/17125","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/rsplugin"}],"about":[{"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/types\/rsplugin"}],"author":[{"embeddable":true,"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/comments?post=17125"}],"version-history":[{"count":20,"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/rsplugin\/17125\/revisions"}],"predecessor-version":[{"id":17441,"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/rsplugin\/17125\/revisions\/17441"}],"wp:attachment":[{"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/media?parent=17125"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/categories?post=17125"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/tags?post=17125"},{"taxonomy":"plugin-requirements","embeddable":true,"href":"https:\/\/plugins.seindal.dk\/wp-json\/wp\/v2\/plugin-requirements?post=17125"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}