Objective Best Practices for Plugin Development?

Starting a community wiki to collect up objective best practices for plugin development. This question was inspired by @EAMann’s comments on wp-hackers.

The idea is to collaborate on what objective best practices might be so that we can potentially eventually use them in some community collaboration review process.

Read More

UPDATE: After seeing the first few responses it becomes clear that we need to have only one idea/suggestion/best-practice per answer and people should review the list to ensure there are no duplicates before posting.

Related posts

Leave a Reply

26 comments

  1. Use Actions and Filters

    If you think people would like to add or alter some data: provide apply_filters() before returning.

    P.S. One thing I find a bit disappointing and that your question addresses is the percentage of plugins that are designed only for end-users, i.e. that have no hooks of their own. Imagine if WordPress were designed like most plugins? It would be inflexible and a very niche solution.

    Maybe things would be different if WordPress were to have the ability to auto-install plugins on which other plugins depended? As it is I typically have to write a lot of the functionality I need from scratch because clients want things a certain way and the available plugins, while 90% there, don’t allow me the flexibility to update the remaining 10%.

    I really do wish those leading the WordPress community would identify a way to ensure that plugins are rewarded for following best practices (such as adding in hooks for other developers) much like good answers are rewarded on a StackExchange site.

    Let’s take an example from another question:

    Example: I want to do something in my plugin when someone retweets an article. If there was a custom hook in whatever the popular retweet plugin is that I could hook in to and fire off of, that would be great. There isn’t, so I can modify their plugin to include it, but that only works for my copy, and I don’t want to try to redistribute that.

    Related

  2. Load Scripts/CSS with wp_enqueue_script and wp_enqueue_style

    Plugins should not load / attempt to load duplicate versions of JS / CSS files, especially jQuery and other JS files included in WP Core.

    Plugins should always use wp_enqueue_script and wp_enqueue_style when linking JS and CSS files and never directly via <script> tags.

    Related

  3. I18n support

    All output strings should be linked to an appropriate text domain to allow for internationalization by interested parties, even if the developer has no interest in translating their own plug-in.

    Note that it is very important to load the language files during the init action so the user can hook into the action.

    See the Codex: I18n for WordPress Developers

    And also this article: Loading WP language files the correctly.

    Since WordPress 4.6+

    WP 4.6 changed the load order and the locations checked, it has made it a lot easier for developers and users.

    Considering a plugin with a textdomain ‘my-plugin’, WordPress will now FIRST look for a translation file in:
    /wp-content/languages/plugins/my-plugin-en_US.mo

    If it fails to find one there it will then look for one where the plugin tells it to look (usualy in the pluigns ‘language’ folder if following the codex):
    /wp-content/plugins/my-plugin/languages/my-plugin-en_US.mo

    Lastly if no language file is found it will check the default location of:
    /wp-content/languages/my-plugin-en_US.mo

    The first check was added in 4.6 and gives users a defined place to add a language file, as before they would need to know where the developer added the language file, now the user just needs to know the plugin’s textdomain:
    /wp-content/languages/plugins/TEXTDOMAIN-LOCAL.mo


    Below is the old way (Not relevant since WP 4.6+)

    […]
    Finally, I would like to point out that is important to load custom user language files from WP_LANG_DIR before you load the language files that ship with the plugin. When multiple mo-files are loaded for the same domain, the first found translation will be used. This way the language files provided by the plugin will serve as a fallback for strings not translated by the user.

    public function load_plugin_textdomain()
    {
        $domain = 'my-plugin';
        // The "plugin_locale" filter is also used in load_plugin_textdomain()
        $locale = apply_filters( 'plugin_locale', get_locale(), $domain );
    
        load_textdomain( 
                $domain, 
                WP_LANG_DIR . '/my-plugin/' . $domain . '-' . $locale . '.mo' 
        );
        load_plugin_textdomain( 
                $domain, 
                FALSE, 
                dirname( plugin_basename(__FILE__) ) . '/languages/' 
        );
    }
    
  4. Ensure Plugins Generate No Errors with WP_DEBUG

    Always test your plugins with WP_DEBUG turned on and ideally have it turned on throughout your development process. A plugin should not throw ANY errors with WP_DEBUG on. This includes deprecated notices and unchecked indexes.

    To turn debugging on, edit your wp-config.php file so that the WP_DEBUG constant is set to true. See the Codex on Debug for more details.

  5. First Use Existing Functions in WordPress Core

    If you can: use existing functions included in WordPress core instead of writing your own. Only develop custom PHP functions when there is not an appropriate pre-existing function in WordPress core.

    One benefit is you can use “log deprecated notices” to easily monitor functions that should be replaced. Another benefit is users can view the function documentation in the Codex and better understand what the plugin does even if they are not an experienced PHP developer.

    Related

  6. Uninstalling should remove all of a plugin’s data

    Upon being removed from a WordPress installation, a plugin should delete all files, folders, database entries, and tables which it created as well as the option values it created.

    Plugins may offer an option to export/import settings, so that settings can be saved outside of WordPress prior to deletion.

    Related

  7. Prefix All Global Namespace Items

    A plugin should properly prefix ALL global namespace items (constants, functions, classes, variables, even things like custom taxonomies, post types, widgets, etc.). For example, do not create a function called init(); instead, name it something like jpb_init().

    Its common should use a three or four letter prefix in front of names or to make use of the PHP Namespace Feature. Compare: Single-letter prefix for PHP class constants?

    Related

  8. Use a class and object-oriented PHP code

    There’s no reason not to write clean, object-oriented PHP code. PHP4 is not supported since 2008. Of course, you can prefix all your function names to end up with endlessly_long_function_names_with_lots_of_underscores, but it’s much easier to just write a simple class and bundle everything in that. Also, put your class in a separate file and name it accordingly so you can easily extend and maintain it:

    // in functions.php
    require 'inc/class-my-cool-plugin.php';
    new MyCoolPlugin();
    
    // in inc/class-my-cool-plugin.php
    class MyCoolPlugin {
        function __construct() {
            // add filter hooks, wp_enqueue_script, etc.
            
            // To assign a method from your class to a WP 
            // function do something like this
            add_action('admin_menu', [$this, "admin"]);
        }
        
        public function admin() {
            // public methods, for use outside of the class
            // Note that methods used in other WP functions 
            // (such as add_action) should be public
        }
        
        private function somethingelse() {
            // methods you only use inside this class
        }
    }
    
  9. Minimize Names Added to the Global Namespace

    A plugin should reduce it’s impact as much as possible by minimizing the number of names it adds to the global namespace.

    This can be done by encapsulating the plugin’s functions into a class or by using the PHP namespaces feature. Prefixing everything can help as well but is not that flexible.

    Next to functions and classes, a plugin should not introduce global variables. Using classes normally obsoletes them and it simplifies plugin maintenance.

    Related

  10. Let plugin’s folder name be changed

    /plugins/pluginname/{various}

    The “pluginname” used for the folder should always be changeable.

    This is normally handled by defining constants and consistantly using them throughout the plugin.

    Needless to say many popular plugins are sinners.

    Related:

    • plugins_url() for easy linking to resources, included with plugin.
  11. Use WordPress (built in) Error handling

    Don’t just return; if some user input was wrong. Deliver them some information about was was done wrong.

    function some_example_fn( $args = array() ) 
    {
        // If value was not set, build an error message
        if ( ! isset( $args['some_value'] ) )
            $error = new WP_Error( 'some_value', sprintf( __( 'You have forgotten to specify the %1$s for your function. %2$s Error triggered inside %3$s on line %4$s.', TEXTDOMAIN ), '$args['some_value']', "n", __FILE__, __LINE__ ) );
    
        // die & print error message & code - for admins only!
        if ( isset( $error ) && is_wp_error( $error ) && current_user_can( 'manage_options' ) ) 
            wp_die( $error->get_error_code(), 'Theme Error: Missing Argument' );
    
        // Elseif no error was triggered continue...
    }
    

    One error (object) for all

    You can set up a global error object for your theme or plugin during the bootstrap:

    function bootstrap_the_theme()
    {
        global $prefix_error, $prefix_theme_name;
        // Take the theme name as error ID:
        $theme_data = wp_get_theme();
        $prefix_theme_name = $theme_data->Name;
        $prefix_error = new WP_Error( $theme_data->Name );
    
        include // whatever, etc...
    }
    add_action( 'after_setup_theme', 'bootstrap_the_theme' );
    

    Later you can add unlimited Errors on demand:

    function some_theme_fn( $args )
    {
        global $prefix_error, $prefix_theme_name;
        $theme_data = wp_get_theme();
        if ( ! $args['whatever'] && current_user_can( 'manage_options' ) ) // some required value not set
            $prefix_error->add( $prefix_theme_name, sprintf( 'The function %1$s needs the argument %2$s set.', __FUNCTION__, '$args['whatever']' ) );
    
        // continue function...
    }
    

    Then you can fetch them all at the end of your theme. This way you don’t interrupt rendering the page and can still output all your errors for developing

    function dump_theme_errors()
    {
        global $prefix_error, $prefix_theme_name;
    
        // Not an admin? OR: No error(s)?
        if ( ! current_user_can( 'manage_options' ) ! is_wp_error( $prefix_error ) )
            return;
    
        $theme_errors = $prefix_error->get_error_messages( $prefix_theme_name );
        echo '<h3>Theme Errors</h3>';
        foreach ( $theme_errors as $error )
            echo "{$error}n";
    }
    add_action( 'shutdown', 'dump_theme_errors' );
    

    You can find further information at this Q. A related ticket to fix the “working together” of WP_Error and wp_die() is linked from there and another ticket will follow. Comments, critics & such is appreciated.

  12. Comment using PhpDoc

    Best practice is close to the PhpDoc style.
    If you don’t use an IDE like “Eclipse”, you can just take a look at the PhpDoc Manual.

    You don’t have to know exactly how this works. Professional Developers can read the code anyway and just need this as a summary. Hobby coders and users might appreciate the way you explain it on the same knowledge level.

  13. Use the Settings API before add_option

    Instead of adding options to the DB via the add_option function, you should store them as an array with using the Settings API that takes care of everything for you.

    Use the Theme Modifications API before add_option

    The Modifications API is a pretty simple construct and a safe way that allows adding and retrieving options. Everything gets saved as serialized value in your database. Easy, safe & simple.

  14. Protect Plugin Users Privacy

    (Previously: Anonymous API Communication)

    If a plug-in communicates with an external system or API (e.g. some Webservice), it should do so anonymously or provide the user with an anonymous option that ensures that no data related to the user of the plugin leaks to a second party uncontrolled.

  15. Provide Access Control by Using Permissions

    In many instances, users may not want everyone to have access to areas created by your plugin especially with plugins that do multiple complex operations, a single hardcoded capability check may not be enough.

    At the very least, have appropriate capability checks for all of the different kind of procedures your plugin can be used for.

  16. Import / Export Plugin Settings

    It’s not that common across plugins, but if your plugin has (some) settings, it should provide Import / Export of data like configuration and user input.

    Import/Export improves the usability of a plugin.

    An example-plugin that has such an import and export functionality (and as well an undo mechanism) is Breadcrumb NavXT (WordPress Plugin) (full disclosure: some little code by me in there, most has been done by mtekk).

    Related

  17. Organize your code

    It’s alway hard to read code that’s not written in the order it get’s executed. First include/require, define, wp_enqueue_style & _script, etc., then the functions that the plugin/theme needs and at last the builder (ex. admin screen, stuff that integrates in the theme, etc.).

    Try to separate things like css and js in their own folders. Also try to do this with functions that are only helpers, like array flatteners and similar. Keeping the “main” file as clean and easy to read as possible is a way that helps users, developers and you, when you try to update in a year and haven’t seen the code for a longer time.

    It’s also good to have a structure you repeat often, so you always find your way through. Developing in a known structure on different projects will give you time to make it better and even if your client switches to another developer, you will never hear “he left a chaos”. This builds your reputation and should be a long term goal.

  18. Die with style

    die in a decent manner
    All of a plugins (and even themes) functions should use wp_die() in critical places to offer the user a little information on what had happened. Php errors are annoying and wp_die can give the user a nice styled message on what the plugin (or they) did wrong. Plus, if the user has debugging deactivated the plugin will just break.

    Using wp_die() also helps that your plugins / themes are compatible with the wordpress testsuite.

    Related:

  19. Provide Help Screens for users

    It is nicer to say RTFM (click help) as an answer than having to answer the question time and time again.

    /**
      * Add contextual help for this screen
      * 
      * @param $rtfm
      * @uses get_current_screen
      */ 
      function ContextualHelp( /*string*/ $rtfm) 
      { 
         $current_screen = get_current_screen();
         if ($current_screen->id == $this->_pageid) 
         {
            $rtfm .= '<h3>The WordPress Plugin - Screen A</h3>';
            $rtfm .= '<p>Here are some tips: donate to me ' .
         }
         return $rtfm; 
      }
    add_action('contextual_help', array($this,'ContextualHelp'),1,1);
    

    update / note: (see comments from kaiser): the above example is to be used in a class

  20. include function always via Hook, not directly.

    Example:

    • Dont use for include the class of the plugin via new without hook

    • Use the Hook plugins_loaded

      // add the class to WP                                   
      function my_plugin_start() {                                                               
          new my_plugin();   
      }                                                        
      add_action( 'plugins_loaded', 'my_plugin_start' );
      

    Update:
    a small live example: Plugin-svn-trunk-page
    and a pseudo example

    //avoid direct calls to this file where wp core files not present
    if (!function_exists ('add_action')) {
            header('Status: 403 Forbidden');
            header('HTTP/1.1 403 Forbidden');
            exit();
    }
    
    if ( !class_exists( 'plugin_class' ) ) {
        class plugin_class {
    
            function __construct() {
            }
    
        } // end class
    
        function plugin_start() {
    
            new plugin_class();
        }
    
        add_action( 'plugins_loaded', 'plugin_start' );
    } // end class_exists
    

    You can also load via mu_plugins_loaded on multisite-install, see the codex for action reference: http://codex.wordpress.org/Plugin_API/Action_Reference
    Also here do you see, how inlcude wP with this hook: http://adambrown.info/p/wp_hooks/hook/plugins_loaded?version=2.1&file=wp-settings.php
    I uses this very often and its not so hard and early, better as an hard new class();