Pile Gallery is a WordPress plugin which allows users to quickly build stacked galleries which expand with a great animation. Today I thought I’d walk you through the process of how the free version was created. I also have a great deal for you on the premium licences. Get the personal licence for $3.99 (that’s 33% off) and the developer licence for $17.50 (that’s 50% off).
Enough marketing, let’s build something! Since even a simple WordPress plugin like Pile Gallery has a lot of components, let’s take a look at the backend side of things, I will not be talking about the Javascript implementation here.
The two major decision I made before I wrote a single line of code was that I will be using the excellent WordPress Plugin Boilerplate and my favourite plugin of all, Advanced Custom Fields for registering options.
The use of Advanced Custom Fields (ACF) poses a unique challenge however. Many components of ACF are free, however the repeater and gallery field – which I needed – are not. The problem is that the ACF licence does not allow these to be built into free products. This is one of the reasons I decided to make a free and a premium version of Pile Gallery.
The other issue with ACF is that it is a fully qualified plugin and the WordPress repository does not allow you to simply include a class which is also a plugin in its own right. What I ended up doing is I simply included ACF 5 in the premium version and I used the TGM Activation Class in the free version to require ACF. This will display a notice which allows the user to install ACF on the spot, from the plugins repository. With that in mind, it really is time to code something!
WordPress Plugin Boilerplate First Steps
The first step was to clone the WordPress Plugin Boilerplate repository. If you don’t know how to use Git you can use the “Download ZIP” link on the right hand side of the page to grab it. This will give you the framework of your plugin.
Note that this framework mimics a WordPress plugin package from the repository. The root directory is for Github, it contains the readme, changelog and .gitignore files. The plugin-name
directory contains a structure which is identical to the one you would maintain in the SVN repo.
This means that if you plop the main folder into your plugins folder it won’t work because this isn’t an installable WordPress plugin. The working plugin would be in plugin-name/trunk
.
You have two choices here. You can copy-paste the trunk folder into your plugins directory and rename it ‘pile-gallery’ and copy-paste the contents back whenever you want to update your Git repository, or you can create a symbolic link. I like the later because it is simpler and it also allows me to include plugins into WordPress from anywhere.
If your main project folder is placed in wp-content/plugins
and is named Pile-Gallery-Repo
you can create a symlink like this:
ln -s target destination // on Mac and Linux mklink /j target destination // on Windows
So in our case this would be:
ln -s /path/to/wordpress/wp-content/plugins/Pile-Gallery-Repo/pile-gallery/trunk /path/to/wordpress/wp-content/plugins/pile-gallery
Once I got the plugin to show up in the WordPress admin I first renamed everything from the default plugin-name
to pile-gallery
. I started with all the files, and then I mass replaced everything inside the files. If you use an editor like Sublime, Coda, Atom.io, Notepad++ you will be able to search and replace within a folder. Here’s what you need to replace:
- All instances of
plugin-name
withpile-gallery
- All instances of
plugin_name
withpile_gallery
- All instances of
Plugin_Name
withPile_Gallery
WordPress Plugin Boilerplate Structure
Inside the trunk
folder you have your run-of-the-mill licence, readme, empty index files and the main plugin file, plus an uninstall file which we won’t need for Pile Gallery at the moment.
The admin
folder contains all the backend functionality of your plugin. This would include custom post types, custom options pages, scripts and styles which modify the backend and so on.
The public
folder is the public facing counterpart. It contains all the functionality which affects the front-end of the website. This would include shortcodes, list displays, scripts and styles which modify the front-end.
The includes
folder contains functionality which is shared between the admin and public facing sections of the plugin. This includes things like third party additions, your plugins main class and other similar things.
Finally, the languages
folder is for adding languages to, it should contain translations or at least a pot file which will allow others to translate the plugin.
Adding The Pile Gallery Post Type
One thing I didn’t want is for Pile Gallery to add its own top level menu. Way too many plugins do this unnecessarily, it crowds the WordPress UI. Installing 4-5 plugins which do this will lead to clutter so I thought I’d spare our users. I decided to add Pile Gallery to the media section.
All hooks in the plugin boilerplate must be registered in includes/class-pile-gallery.php
, what I call the “main plugin class”. I added the appropriate hook to the define_admin_hooks()
function in there:
$this->loader->add_action( 'init', $plugin_admin, 'post_type' );
Once done, I visited the admin class (admin/class-pile-gallery-admin.php
) and created the code for the custom post type.
public function post_type() { $labels = array( 'name' => _x( 'Pile Galleries', 'post type general name', $this->name ), 'singular_name' => _x( 'Pile Gallery', 'post type singular name', $this->name ), 'menu_name' => _x( 'Pile Galleries', 'admin menu', $this->name ), 'name_admin_bar' => _x( 'Pile Gallery', 'add new on admin bar', $this->name ), 'add_new' => _x( 'Add New', 'pile_gallery', $this->name ), 'add_new_item' => __( 'New Pile Gallery', $this->name ), 'new_item' => __( 'New Pile Gallery', $this->name ), 'edit_item' => __( 'Edit Pile Gallery', $this->name ), 'view_item' => __( 'View Pile Gallery', $this->name ), 'all_items' => __( 'Pile Galleries', $this->name ), 'search_items' => __( 'Search Pile Galleries', $this->name ), 'parent_item_colon' => __( 'Parent Pile Galleries:', $this->name ), 'not_found' => __( 'No pile galleries found.', $this->name ), 'not_found_in_trash' => __( 'No pile galleries found in Trash.', $this->name ) ); $args = array( 'labels' => $labels, 'public' => true, 'exclude_from_search' => true, 'show_in_nav_menus' => false, 'show_in_menu' => 'upload.php', 'query_var' => true, 'rewrite' => array( 'slug' => 'pile_gallery' ), 'capability_type' => 'post', 'hierarchical' => false, 'menu_position' => null, 'supports' => array( 'title' ) ); register_post_type( 'pile_gallery', $args ); }
Note value for show_in_menu
, this is what placed the menu entry for this custom post type inside the media section.
The next step was to modify the post list in the admin to show the shortcode. We need to add two hooks to make this happen. The first one will define any custom columns, the second one will define the contents of these columns. So, back to the main plugin class to add the hooks.
$this->loader->add_filter( 'manage_pile_gallery_posts_columns', $plugin_admin, 'admin_list_columns' ); $this->loader->add_action( 'manage_pile_gallery_posts_custom_column', $plugin_admin, 'admin_list_content', 10, 2 );
The code for the function is pretty simple. We need to add an element to the array of existing columns, then simply echo out the ID of the post for the shortcode, here’s how (remember to add this to the admin class).
public function admin_list_columns( $defaults ) { $defaults['shortcode'] = 'Shortcode'; return $defaults; } public function admin_list_content( $column_name, $post_id ) { if( 'shortcode' == $column_name ) { echo "[pile_gallery id='" . $post_id . "']"; } }
Requiring Advanced Custom Fields
To make sure our users have ACF installed (without which the plugin doesn’t work properly) we’ll use the TGM activation class. This class will display a notice to users and will let them install the required plugins with a few clicks.
Head over to the TGM Activation Github Page and grab the contents. We only need class-tgm-plugin-activation.php
so you can just copy that file.
We’ll put it in our includes
folder, on my end I created a vendor
folder within to hold all the third party things we’ll use. So the final path of my TGM Activation file will be: includes/vendor/TGM-Plugin-Activation/class-tgm-plugin-activation.php
.
Since this constitutes a dependency for our plugin we need to include it using the load_dependencies()
method in our main plugin class.
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/vendor/TGM-Plugin-Activation/class-tgm-plugin-activation.php';
Now that we’ve included it, let’s use it! Taking a look at the TGM Plugin Activator Example we’ll need to add an action to define our options, and which plugins to require. Let’s add this to our usual admin hooks section:
$this->loader->add_action( 'tgmpa_register', $plugin_admin, 'required_plugins' );
We can then go to the admin class and create the required_plugins
class to hold the general options for TGM and define the required plugins.
public function required_plugins() { $plugins = array( array( 'name' => 'Advanced Custom Fields', 'slug' => 'advanced-custom-fields', 'required' => true, ), array( 'name' => 'Media Categories', 'slug' => 'media-categories-2', 'required' => false, ), ); $config = array( 'default_path' => '', 'menu' => 'tgmpa-install-plugins', 'has_notices' => true, 'dismissable' => false, 'is_automatic' => false, 'message' => '', 'strings' => array( 'page_title' => __( 'Install Required Plugins', $this->name ), 'menu_title' => __( 'Install Plugins', $this->name ), 'installing' => __( 'Installing Plugin: %s', $this->name ), 'oops' => __( 'Something went wrong with the plugin API.', $this->name ), 'notice_can_install_required' => _n_noop( 'Pile Galleries requires the following plugin to function properly: %1$s.', 'Pile Galleries requires the following plugins to function properly: %1$s.' ), 'notice_can_install_recommended' => _n_noop( 'Pile Galleries recommends the following plugin: %1$s.', 'Pile Galleries recommends the following plugins: %1$s.' ), 'notice_cannot_install' => _n_noop( 'Sorry, but you do not have the correct permissions to install the %s plugin. Contact the administrator of this site for help on getting the plugin installed.', 'Sorry, but you do not have the correct permissions to install the %s plugins. Contact the administrator of this site for help on getting the plugins installed.' ), 'notice_can_activate_required' => _n_noop( 'The following required plugin is currently inactive: %1$s.', 'The following required plugins are currently inactive: %1$s.' ), 'notice_can_activate_recommended' => _n_noop( 'The following recommended plugin is currently inactive: %1$s.', 'The following recommended plugins are currently inactive: %1$s.' ), 'notice_cannot_activate' => _n_noop( 'Sorry, but you do not have the correct permissions to activate the %s plugin. Contact the administrator of this site for help on getting the plugin activated.', 'Sorry, but you do not have the correct permissions to activate the %s plugins. Contact the administrator of this site for help on getting the plugins activated.' ), 'notice_ask_to_update' => _n_noop( 'The following plugin needs to be updated to its latest version to ensure maximum compatibility with Pile Galleries: %1$s.', 'The following plugins need to be updated to their latest version to ensure maximum compatibility with Pile Galleries: %1$s.' ), 'notice_cannot_update' => _n_noop( 'Sorry, but you do not have the correct permissions to update the %s plugin. Contact the administrator of this site for help on getting the plugin updated.', 'Sorry, but you do not have the correct permissions to update the %s plugins. Contact the administrator of this site for help on getting the plugins updated.' ), 'install_link' => _n_noop( 'Install plugin now', 'Install plugins now' ), 'activate_link' => _n_noop( 'Activate plugin now', 'Activate plugins now' ), 'return' => __( 'Return to Required Plugins Installer', $this->name ), 'plugin_activated' => __( 'Plugin activated successfully.', $this->name ), 'complete' => __( 'All plugins installed and activated successfully. %s', $this->name ), 'nag_type' => 'updated' ) ); tgmpa( $plugins, $config ); }
Note that I’ve added two plugins. The first one, ACF, is required while the second one, Media Categories is not. Pile Gallery can use categorized media items to build galleries as well, however media categorization is not available in WordPress. This is why we’ve suggested that users install this plugin.
In addition to all that, I’ve also included an add-on to ACF, the Advanced Taxonomy Selector. This is needed so users can select a taxonomy to base their galleries on.
if( function_exists( 'get_field' ) ) { require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/vendor/acf-advanced-taxonomy-selector/acf-advanced_taxonomy_selector.php'; }
By checking that the get_field()
function exists I make sure that the file is only included if ACF itself is active.
Adding Options
Now that we have everything we need, we can generate some options for our Pile Galleries. I did this by going into the Custom Fields menu and creating them in the UI. I then exported them to PHP using ACFs built in tool and then copy-pasted it into an included options file.
First I created the pile-gallery-options.php
file in the includes
folder, then I included it in the main plugin class’ load_dependencies()
method.
if( function_exists( 'get_field' ) ) { require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/pile-gallery-options.php'; }
Inside the file I copy-pasted the exported fields. Since this is a bit long perhaps it is easier if I just link out to it: behold, the options file.
If you visit the Pile Gallery menu now and create a new gallery, you should be able to see these options kick in. Nothing else needs to be done, all the options are saved and loaded for us by ACF, all we need to do is use them wherever we wish.
The Pile Gallery Shortcode
The last thing on our plate is writing the shortcode. Since shortcodes are public facing things I’ve added a class-pile-gallery-shortcodes.php
file to the public
directory which houses the Pile_Gallery_shortcodes
class. This class is not a part of the boilerplate so we’ll need to add it as a dependency:
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'public/class-pile-gallery-shortcodes.php';
This class is also instantiated inside the dependency method:
$this->shortcodes = new Pile_Gallery_Shortcodes();
The shortcode actually uses four separate functions to display the gallery. The main pile_gallery()
method is where it all starts. Depending on the type of gallery, a different function is used to build the pile array. The free version has the post
and gallery
type, the premium version also allows you to build custom galleries so there is a third, gallery
type in the premium version.
This is also a pretty long bit of code so linking out to the shortcodes file seems easier. I’d like to point out a couple of things in the code:
- The final display of each gallery type is handled by the same
public/partials/shortcodes/pile-display.php
file. The only difference in these galleries is the source of the images, not how they are shown. - I’ve added actions and filters to allow other developers to modify the gallery’s behaviour. This includes adding an action before and after a gallery is shown and adding a filter to the queries which retrieve the posts/images from the database.
Final Words
As I mentioned, we tried to make Pile Gallery as extendible as possible. You can learn more about this on the Developers section. We’re actually adding a lot to this plugin as we speak so it will become easier and easier to work with.
I hope this article provided some insight into how a plugin is created, and how it is built in a standardized way, helping it play nice with other plugins and themes. While some things we did could have been done faster, making sure that convention trumps cleverness means that others will be able to understand your code better (and you’ll understand it 1 year from now as well).
Don’t forget that that we have excellent offers for WDL readers. You can get the developer version for just $17.5 (that’s 50% off) or the personal version for $3.99 (that’s 33% off). The offer lasts until the 15th of October, so grab a copy now!
Leave a Reply