DEV Community

Cover image for Introducing Underpin - A Modern WordPress Framework
Alex Standiford
Alex Standiford

Posted on • Updated on • Originally published at wpdev.academy

Introducing Underpin - A Modern WordPress Framework

Over the years, I have built dozens and dozens of WordPress plugins. Some have been GPL plugins shared publicly, but most have been custom plugins for clients. I started out much like most other WordPress developers, building ever-improving homemade boilerplates. It started simple enough - nothing more than a collection of handy functions that I always wanted in my plugins. Over time, it expanded into a bootstrap file that sets up the plugin, set up constants, and all of the other things that you will find in pretty much any major WordPress plugin today.

Over time, I tinkered, built, re-built, and re-imagined my little plugin boilerplate into a full-blown WordPress framework, that I use on all of my client sites, and all of my distributed plugins.

WordPress Isn't a Framework

WordPress 'aint perfect. It does a lot of things "old skool", and wasn't really designed to make building web applications easy. Instead, it has always been focused on making its core as extendable as possible, and simply stays out of your way so you can go on to build what you want.

In my opinion, I think this is great. I think WordPress's staying power has come from the fact that it just does not give a damn about how you build your stuff. If you hook into the right action, it'll do whatever you want.

But this does not make plugin development particularly efficient in WordPress. If you're starting from scratch, you're literally starting with a blank PHP file in a plugin directory. There's a lot of extra setup that comes with doing pretty much everything in WordPress that you don't find in actual frameworks.

Over time, you will also find that WordPress is missing some key things that most plugins will need as they mature, such as plugin upgrade routines.

In fact, WordPress has a lot of shortcomings that many plugins have to eventually overcome:

  1. Troubleshooting their plugin on a live customer site
  2. Building robust, extendable admin pages
  3. Making your plugin extendable without taking on tons of technical debt in the process
  4. Using consistent code practices across add-ons
  5. Handling upgrade routines, and database upgrades
  6. Keeping technical debt managed, and under-control

These are a few of the challenges that I have had to overcome in my own plugins, and Underpin tackles each one of them.

Using Underpin

Underpin takes everything that is done in WordPress, and converts it into a consistent syntax. In other words, the process to create a widget is pretty much identical to how you would register a custom post type. Things as seemingly different as creating scripts, and creating admin pages, even use the same exact steps.

  1. Install the appropriate loader via Composer
  2. Register the item using the loader's add function.

Registering a custom post type could look like this:

// Register custom Post Type
underpin()->custom_post_types()->add( 'example_type', [
    'type' => 'example-type', // see register_post_type
    'args' => [ /*...*/ ] // see register_post_type
] );
Enter fullscreen mode Exit fullscreen mode

Registering a widget could look something like this:

// Register widget
underpin()->widgets()->add( 'hello-world-widget', [
    'name'                => underpin()->__( 'Hello World Widget' ),                               // Required. The name of the widget.
    'id_base'             => 'widget_name',                                                        // Required. The ID.
    'description'         => underpin()->__( 'Displays hello to a specified name on your site.' ), // Widget description.
    'widget_options'      => [                                                                     // Options to pass to widget. See wp_register_sidebar_widget
        'classname' => 'test_widget',
    ],
    'get_fields_callback' => function ( $fields, \WP_Widget $widget ) {                            // Fetch, and set settings fields.
        $name = isset( $fields['name'] ) ? esc_html( $fields['name'] ) : 'world';

        return [
            new \Underpin\Factories\Settings_Fields\Text( $name, [
                'name'        => $widget->get_field_name( 'name' ), // See WP_Widget get_field_name
                'id'          => $widget->get_field_id( 'name' ),   // See WP_Widget get_field_id
                'setting_key' => 'name',                            // Must match field name and field ID
                'description' => underpin()->__( 'Optional. Specify the person to say hello to. Default "world".' ),
                'label'       => underpin()->__( 'Name' ),
            ] ),
        ];
    },
    'render_callback'    => function ( $instance, $fields ) {                                      // Render output
        $name = ! empty( $fields['name'] ) ? esc_html( $fields['name'] ) : 'world';

        echo underpin()->__( sprintf( 'Hello, %s!', $name ) );
    },
] );
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, each of these items do all of the things necessary to register their respective items. It uses all of the information you provide to build when the time is right.

Debugging With Underpin

Underpin comes with a PSR3 compatible logging utility. This utility will write error logs to a file, or you can extend it to do something else if your plugin needs something else.

This logger is used throughout Underpin, and is something that you could use as you build your own plugin, as well.

plugin_name_replace_me()->logger()->log(
    'error',             // Error type.
    'unique_error_code', // Error code
    'Human readable error message',
    ['arbitrary' => 'data', 'that' => 'is relevant', 'ref' => 1]
);
Enter fullscreen mode Exit fullscreen mode

Not only does this optionally save certain events to the log, it also https://github.com/Underpin-WP/debug-bar-extension with Query Monitor and Debug Bar debug plugins. All Underpin events automatically get added to these logs. This allows you to troubleshoot problems on a live site, and begin to make sense of what's going on.

Settings Fields

Full-Site editing is coming, but for now we still need a way to create admin pages using WordPress's HTML markup. If you've ever worked with this system, you know - there be dragons.

To help make this easier, Underpin has a set of HTML input classes that make it possible to sanitize, save, and render HTML on admin pages. These fields are capable of natively using all of the markup that WordPress expects, or a bare-minimum set of markup for places that do not work inside of a table.

A basic text field would look like this:

$text_field = new \Underpin\Factories\Settings_Fields\Text( $name, [
    'name'        => $widget->get_field_name( 'name' ), // See WP_Widget get_field_name
    'id'          => $widget->get_field_id( 'name' ),   // See WP_Widget get_field_id
    'setting_key' => 'name',                            // Must match field name and field ID
    'description' => underpin()->__( 'Human Readable Description' ),
    'label'       => underpin()->__( 'Field Name' ),
] );

// Render the field
echo $text_field->place();
Enter fullscreen mode Exit fullscreen mode

With these fields, you can create entire settings pages using the exact same add syntax discussed above. The example below would render an admin page in WordPress, complete with support for saving the field to an option.

// Register the option to use on the settings page. See Underpin_Options\Abstracts\Option
underpin()->options()->add( 'example_admin_options', [
    'key'           => 'example_option', // required
    'default_value' => [
        'test_setting' => 'Hello world',
    ],
    'name'          => 'Example Admin Page',
    'description'   => 'Settings manged by Example Admin Page',
] );

// Register the admin page
underpin()->admin_pages()->add( 'example-admin-page', [
    'page_title' => underpin()->__( 'Example Admin Page' ),
    'menu_title' => underpin()->__( 'Example' ),
    'capability' => 'administrator',
    'menu_slug'  => 'example-admin-page',
    'icon'       => 'dashicons-admin-site-alt',
    'position'   => 5,
    'sections'   => [
        [
            'id'          => 'primary-section',
            'name'        => underpin()->__( 'Primary Section' ),
            'options_key' => 'example_admin_options',
            'fields'      => [
                'test_setting' => [
                    'class' => 'Underpin\Factories\Settings_Fields\Text',
                    'args'  => [ underpin()->options()->pluck( 'example_admin_options', 'test_setting' ), [
                        'name'        => 'test_setting',
                        'description' => underpin()->__( 'Optional. Specify the person to say hello to. Default "world".' ),
                        'label'       => underpin()->__( 'Name' ),
                    ] ],
                ],
            ],
        ],
    ],
] );
Enter fullscreen mode Exit fullscreen mode

Plugin Extend-ability

The loader pattern provided by Underpin makes it possible to extend WordPress core, and you can use the exact same technology to make your own plugin extendable. This can be done by registering your own custom loader registries to your WordPress plugin.

In fact, the process to add a loader uses the exact same syntax as adding literally anything else. The only difference here is that you have to also create the Loader_Registry class to reference.

I'm not going to get into this too deep in this post, but you can see how it all works in the custom post type loader source code. This particular bit of functionality is really the heart and soul of Underpin, and an entire post could be easily written on it.

Here's an example of how a loader is registered in an upcoming course I'm working on:


        /**
         * Setup Colors
         */
        underpin()->loaders()->add( 'colors', [
            'registry' => 'Beer_List\Loaders\Colors', 
        ] );

Enter fullscreen mode Exit fullscreen mode

Now, if I wanted to add a new color to the registry, I would do this:

underpin()->colors()->add( 'color_id', [/** Args to register the color **/] );
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, this would create a new instance of your Color class, and store it for later use.

That color class could be accessed at any time with:

$color = underpin()->colors()->get( 'color_id' );
Enter fullscreen mode Exit fullscreen mode

This could also be done by anyone extending the plugin.

Upgrade Routines & Batch Tasks

Like anything else, Underpin has a custom loader that handles the admin interface, script, and batch actions needed to make an upgrade routine work. Like everything else, this is a pre-built loader that can be installed and used like this:

\Underpin\underpin()->batch_tasks()->add( 'example-batch', [
    'description'             => 'A batch task that does nothing 20 times',
    'name'                    => 'Batch Task Example',
    'tasks_per_request'       => 50,
    'stop_on_error'           => true,
    'total_items'             => 1000,
    'notice_message'          => 'Run the most pointless batch task ever made.',
    'button_text'             => 'LETS GO.',
    'capability'              => 'administrator',
    'batch_id'                => 'example-batch',
    'task_callback'           => '__return_null', // The callback that iterates on every task
    'finish_process_callback' => '__return_null', // The callback that runs after everything is finished
    'prepare_task_callback'   => '__return_null', // The callback that runs before each task
    'finish_task_callback'    => '__return_null', // The callback that runs after each task
  ] );
Enter fullscreen mode Exit fullscreen mode

Distributing Underpin

One big thing that stops most developers from using dependencies in their WordPress plugin is that there's usually no easy way to prevent plugin conflicts with other plugins that also use the system. Underpin resolves that problem with a rename compiler.

This compiler is a basic find/replace script that replaces all mention of the word underpin throughout the plugin with a word of your choice. Everything in Underpin that can cause a conflict has been intentionally prefixed with underpin to make it possible to do this rename.

This means that you can use Underpin in its entirety in your WordPress plugin, and run the script to compile Underpin so it is safe to be distributed with other plugins that also use Underpin.

If you don't need to distribute your plugin, and are using Underpin on a single website, it usually makes sense to install it as a mu-plugin. This removes the need to do any compilation, and also ties all of your plugins to a single installation of underpin.

Conclusion

There is so much more about Underpin that hasn't been discussed here, and I expect I'll be publishing more content over time. In the meantime, if you think you'd like to learn more about Underpin, check out the GitHub organization containing all of the current loaders, as well as Underpin's core.

PR's, discussions, and questions are always welcome.

Looking for more WordPress Resources?

Join WP Dev Academy’s Discord server, and become a part of a growing community of WordPress developers.

Top comments (0)