Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: plugins

  • Review: FacetWP

    Review: FacetWP

    I’ve been using FacetWP since April 2017 and I can unequivocally say that it was one of the best purchases I’ve made.

    Search Is Hard

    There’s no two ways around this. Search is difficult. You have to guess what peoples’ intents are, and you have to order the results in a way that is meaningful. While it would be great if people searched for keywords, they prefer to look for things in whole phrases like “jackets made of feathers.” Those are terms and presentations that make sense to the human mind.

    On top of that, there are different kinds of search.

    Most people are familiar with document search, which is more or less what WordPress and Google do. Since webpages are just text documentation at the end, Google searches all the text, figures out how many people link to the page, use some secret dipping sauce, five spices, and determine relevancy. WordPress’ own search is much simpler and consequently less effective. Not that Google gets it right all the time either, though…

    Another common type of search, also used by Google, is graph search. This is popular on Facebook and Twitter, but it uses connections between your friends to prioritize and determine depth of search.

    Finally there’s the concept of faceted search. This is useful when a site knows you’re looking for a product, like a shoe, and you just need help narrowing down the size, the color, the fit, etc. And that’s where FacetWP comes in.

    Facets vs Filters

    You might have heard about search ‘filters.’ If you’ve ever used Google’s image search, or news search, and you tried to narrow down results based on dates or colors or formats, you’ve used filters. They help you filter the results by changing the parameters. A faceted search is similar, in that it uses the same concepts as filters to toggle multiple aspects of the search item, giving you even more flexibility in your results.

    The term ‘filter’ and ‘facet’ are oft used interchangeably, and since they’re so similar and related, this does not help a single person at all. They both help reduce large data sets into something manageable, but filters are relatively easier than facets. In fact, your WordPress site already does basic filters. Ever gone to a category or tag page? That’s a very basic example of a filter.

    Faceted Search Is Hard

    If regular relational searches are hard, it shouldn’t surprise you to hear that faceted search is too. A faceted search has the job of analyzing a large data set and excluding anything that doesn’t fit your specific criteria. This means it uses multiple filters, once for each aspect of the data set.

    Okay, let’s make this a little easier to understand with a practical example!

    Let’s say you have a database of 750 TV shows. You’ve identified what you feel to be the key components of the shows, such air dates, countries, ratings, specific genres, and if the reviewer liked it. Now, if someone comes to your site and wants a list of crime dramas that aired between 2000 and 2017, in the US, that the reviewer hated, you don’t need filters, you need a faceted search.

    By building in options to sort each of those things, you reduce the dimensions of content and offer a structure to help your users understand the contextual construct of the data. You are giving them ideas about what data is available, and how they can search through it without having to guess at keywords.

    FacetWP Does All That

    Simply put, FacetWP does that.

    It does all of that. It even lets me add in a sort-by so once a user has narrowed down the shows, they can reorder them based on name, date added, number of characters, and show ratings. If I wanted to extend that to order based on airdate, I could do that too. People can toggle criteria on and off and the content updates dynamically

    If you have a large amount of data (like 750 TV shows or 2250 TV characters), and you want to organize them sanely, swiftly, and not crash your server, use FacetWP. It even works with WooCommerce and EDD, so if you want to be the next Amazon, you need this.

  • Make WordPress Gay

    Make WordPress Gay

    In September, WordPress.com changed their admin bar from the normal black or blue to a rainbow color. Why?

    Australia will be holding a national survey on marriage equality over the next two months. To show our support as a company for marriage equality, we’re showing the rainbow bar in the WordPress.com admin bar to all logged-in Australian visitors. You can read more about the marriage equality campaign here: http://www.equalitycampaign.org.au/

    And this was super cool. They’d done it before when Gay Marriage was legalized in the US and it’s much appreciated as a show of solidarity. But… The menu bar only shows if you’re visiting WordPress.com from Australia. This causes two problems for me:

    1) I’m in the US
    2) I self host

    Don’t worry, there’s a solution.

    The ‘Official’ Solution

    The code for what WordPress.com does can be found on the Github repository for Calypso. But that’s all mixed in with a lot of extra ‘stuff’ that has to be there for their services, like geo-location and so on.

    The rest of us don’t need that, and since Gary (the fellow who wrote the ticket) is a friend of mine, I asked him if the code was available. It is.

    Rainbow Bar Code

    <?php
    
    /*
     * Plugin Name: Rainbow Bar!
     */
    
    function rainbow_bar() {
    ?>
    	<style type="text/css">
    		#wpadminbar {
    			background: linear-gradient(
    				to bottom,
    				#e24c3e 0%,
    				#e24c3e 16.66667%,
    				#f47d3b 16.66667%,
    				#f47d3b 33.33333%,
    				#fdb813 33.33333%,
    				#fdb813 50%,
    				#74bb5d 50%,
    				#74bb5d 66.66667%,
    				#38a6d7 66.66667%,
    				#38a6d7 83.33333%,
    				#8c7ab8 83.33333%,
    				#8c7ab8 100% );
    		}
    
    		#wpadminbar,
    		#wpadminbar .quicklinks > ul > li {
    			-webkit-box-shadow: unset;
    			-moz-box-shadow: unset;
    			box-shadow: unset;
    		}
    
    		#wpadminbar .ab-top-menu > li > a {
    			background-color: rgba( 50, 55, 60, .85 );
    		}
    	</style>
    <?php
    }
    add_action( 'wp_before_admin_bar_render', 'rainbow_bar' );
    

    Install that to get this:

    Rainbow Pride WP Admin Bar

    Small Changes

    In order to make it look ‘right’ for me, I changed two things

    I removed this:

    	#wpadminbar .ab-top-menu > li > a {
    		background-color: rgba( 50, 55, 60, .85 );
    	}
    

    And I added this:

    	#wpadminbar .ab-item, #wpadminbar a.ab-item, #wpadminbar > #wp-toolbar span.ab-label, #wpadminbar > #wp-toolbar span.noticon,
    	#wpadminbar .ab-icon, #wpadminbar .ab-icon:before, #wpadminbar .ab-item:before, #wpadminbar .ab-item:after {
    		color: #000;
    	}
    

    Caveats

    Even with my changes, the color for my Jetpack stats looks wrong. It’s too washed out. And that’s because it’s apparently hardcoded into the plugin in a way I can’t overwrite. I can live with that problem.

    The other issue is that this will only show for logged in users (unless you’re using some code to always show the admin bar). That begs the question, of course, of why would you do this if only logged in users?

    If you’re running a queer themed website that happens to use WordPress … like, say, Autostraddle, then this makes perfect sense. For me, it just makes me feel happy to see that pride rainbow all the time.

  • Loading Common Libraries Smartly

    Loading Common Libraries Smartly

    When you’re writing code, there’s a desire to not reinvent the wheel. This is a good desire. If someone else has made the code you need, using it is a better use of your time. But there’s a right way and a wrong way to include those common libraries, especially in WordPress themes and plugins.

    PHP Is Dumb

    Okay, PHP isn’t dumb, but it’s a very top-down/left-right kind of language. The whole reason we tell people to name functions and classes uniquely in WordPress plugins and themes is that if two people name a function the same thing, PHP gets dumb and errors, telling you it can’t define the same function twice.

    This means that commonly used PHP libraries should also be uniquely named. To their credit, most do. But still, what happens if two WordPress plugins or themes include the same library? That same error comes back about not defining the same function twice!

    What this means is that when using a common library, it’s important that you enqueue it safely and smartly.

    Check If The Library Exists

    The magic is that you need to detect if the code is already included and not re-include it. PHP gives you two straightforward ways to do this: class_exists and function_exists.

    It’s much easier for me when the code is wrapped nicely in a class. Assuming you have a library file that includes the class but doesn’t call it, you can check like this:

    <?php
    if ( !class_exists( 'MyLibraryClass' ) ) {
        include( 'path/to/library.php' );
        $mylibraryclass = new MyLibraryClass();
    }
    

    If it does include the class (with a new MyLibraryClass() in the library) then you can leave that bit out.

    When you have a library that perhaps predates classes, you’ll want to pick the primary function and call it like this:

    <?php
    if ( !function_exists( 'my_library_function' ) ) {
        include( 'path/to/library.php' );
    }
    

    What About The Weird Stuff?

    There are two things I left out. What about namespaces and javascript libraries?

    First of all, it’s impossible to check for the existence of a namespace. You see, a namespace doesn’t really exist. What does exists are the structures within the namespace. That means if you wanted to check for the class ClassName in the namespace of MyName you have to do this:

    class_exists( '\MyName\ClassName') );
    

    Second, if you’re looking for javascript libraries, and you’re using WordPress, you don’t have to. Just enqueue it.

    Schnieder from One Day at a Time (2017) going 'Mind blown'

    The WordPress function wp_enqueue_script() registers the script but does not overwrite and enqueues it.

    Now there is a trick to all this. If you use the enqueue command properly, then this always works. By this I mean if you do it this way, it’ll work:

    wp_enqueue_script ( 'chartjs', 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.bundle.min.js' );
    

    But… A lot of people do this:

    wp_enqueue_script ( 'my-plugin-chartjs', 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.bundle.min.js' );
    

    And that means WordPress will load both chartjs and my-plugin-chartjs which sucks.

    Also you don’t need to append -js (or -css) to your enqueues, as WordPress will do that for you.

  • Hello Trello

    Hello Trello

    Organizing my life is usually done on the Apple Reminders app. I shove my to-dos in there and it alerts me when I forget about scheduled, repeated, events. Like mailing in a rent check.

    But when you have a group of people, it can be trickier, especially if you all use different devices and have different workflows.

    Enter Trello

    There are a lot of options out there, but Trello is free for the small scale. It’s easy to set up and my friend Tracy uses it a lot. When I realized we needed something better than my list to keep track of everything, I popped over to Trello to make it work.

    I’ve used it before, and I use Jira — which is owned by Trello’s Atlassian — so I’m familiar with the basic style of Kanban boards for tracking work. Every project has a ‘board’ and on the board you have ‘lists.’ In each list you have your ‘tasks’ which you drag from list to list to list. The general concept is you have a list of ‘backlog’ where you store everything. Tasks move from there to ‘in progress’ and then to ‘done’ (or they get archived).

    There are some things about the flow I find annoying. Like there isn’t a way to mark something as ‘done’ unless it’s scheduled, it just gets archived, but it can work. And you can integrate it with Slack so that’s nifty.

    What About WordPress?

    I’m going to shoot your hopes and dreams. There isn’t a WordPress plugin for Trello.

    Oh fine, there’s WP Trello but that actually is meant to display your boards as widgets or shortcodes. For the life of me, I can’t figure out why anyone would want to do that. No, what I mean is there’s no back-end WordPress plugin where I can go to, say, the dashboard and check out what boards and tasks are being worked on.

    I suppose that’s much the point, though. You shouldn’t be managing those things in WordPress. In fact, if you look into it, embedding Trello anywhere is kind of a mess. They clearly don’t want you to miss out on their API and their apps. Their experience matters.

    On the other hand they’re very hip on integration, letting people stick in one tool and access everything. I suspect the brunt of the lack of a WordPress tool is due to the fact that there’s no one recommended PHP API. If you go to their community page, you’ll see a lot of python and Ruby, but no PHP.

    What Can You Do With WordPress?

    Thankfully, you can integrate WordPress and Trello. There are extensions for Ninja Forms and Gravity Forms which let you integrate your form submissions and make them into Trello Tasks. If you use those plugins to allow visitors to post submissions, it’s a near perfect idea. They submit a post, you make a to-do, and your life remains organized instead of wondering who handled which submission.

    But if you’re looking for more, you need to consider a 3rd party integration via Zapier or IFTTT. There isn’t even a Jetpack contact form to Trello plugin.

    Yet.

  • Taxonomy Icons

    Taxonomy Icons

    Last year I talked about how I made icons for my taxonomy terms. When you have a limited number of terms, that makes sense. When you have a taxonomy with the potential for a high volume of terms, like nations (192), or worse an unlimited number of terms, this approach looses its value.

    Instead, I realized what I needed for a particular project was a custom icon for each taxonomy. Not the term.

    I split this up into two files because I used a slightly different setup for my settings API, but the tl;dr of all this is I made a settings page under themes called “Taxonomy Icons” which loads all the public, non-default taxonomies and associates them with an icon.

    For this to work for you, you will need to have your images in a folder and define that as your IMAGE_PATH in the code below. Also mine is using .svg files, so change that if you’re not.

    File 1: taxonomy-icons.php

    The one gotcha with this is I usually set my default values with $this->plugin_vars = array(); in the __construct function. You can’t do that with custom taxonomies, as they don’t exist yet.

    class TaxonomyIcons {
    
    	private $settings;
    	const SETTINGS_KEY = 'taxicons_settings';
    	const IMAGE_PATH   = '/path/to/your/images/';
    
    	/*
    	 * Construct
    	 *
    	 * Actions to happen immediately
    	 */
        public function __construct() {
    
    		add_action( 'admin_menu', array( $this, 'admin_menu' ) );
    		add_action( 'admin_init', array( $this, 'admin_init' ) );
    		add_action( 'init', array( $this, 'init' ) );
    
    		// Normally this array is all the default values
    		// Since we can't set it here, it's a placeholder
    		$this->plugin_vars = array();
    
    		// Create the list of imagess
    		$this->images_array = array();
    		foreach ( glob( static::IMAGE_PATH . '*.svg' ) as $file) {
    			$this->images_array[ basename($file, '.svg') ] = basename($file);
    		}
    
    		// Permissions needed to use this plugin
    		$this->plugin_permission = 'edit_posts';
    
    		// Menus and their titles
    		$this->plugin_menus = array(
    			'taxicons'    => array(
    				'slug'         => 'taxicons',
    				'submenu'      => 'themes.php',
    				'display_name' => __ ( 'Taxonomy Icons', 'taxonomy-icons' ),
    			),
    		);
        }
    
    	/**
    	 * admin_init function.
    	 *
    	 * @access public
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function admin_init() {
    		// Since we couldn't set it in _construct, we do it here
    		// Create a default (false) for all current taxonomies
    		$taxonomies = get_taxonomies( array( 'public' => true, '_builtin' => false ), 'names', 'and' );
    		if ( $taxonomies && empty( $this->plugin_vars ) ) {
    			foreach ( $taxonomies as $taxonomy ) {
    				$this->plugin_vars[$taxonomy] = false;
    			}
    		}
    	}
    
    	/**
    	 * init function.
    	 *
    	 * @access public
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function init() {
    		add_shortcode( 'taxonomy-icon', array( $this, 'shortcode' ) );
    	}
    
    	/**
    	 * Get Settings
    	 *
    	 * @access public
    	 * @param bool $force (default: false)
    	 * @return settings array
    	 * @since 0.1.0
    	 */
    	function get_settings( $force = false) {
    		if ( is_null( $this->settings ) || $force ) {
    			$this->settings = get_option( static::SETTINGS_KEY, $this->plugin_vars );
    		}
    		return $this->settings;
    	}
    
    	/**
    	 * Get individual setting
    	 *
    	 * @access public
    	 * @param mixed $key
    	 * @return key value (if available)
    	 * @since 0.1.0
    	 */
    	function get_setting( $key ) {
    		$this->get_settings();
    		if ( isset( $this->settings[$key] ) ) {
    			return $this->settings[$key];
    		} else {
    			return false;
    		}
    	}
    
    	/**
    	 * Set setting from array
    	 *
    	 * @access public
    	 * @param mixed $key
    	 * @param mixed $value
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function set_setting( $key, $value ) {
    		$this->settings[$key] = $value;
    	}
    
    	/**
    	 * Save individual setting
    	 *
    	 * @access public
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function save_settings() {
    		update_option( static::SETTINGS_KEY, $this->settings );
    	}
    
    	/**
    	 * admin_menu function.
    	 *
    	 * @access public
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function admin_menu() {
    
    		foreach ( $this->plugin_menus as $menu ) {
    			$hook_suffixes[ $menu['slug'] ] = add_submenu_page(
    				$menu['submenu'],
    				$menu['display_name'],
    				$menu['display_name'],
    				$this->plugin_permission,
    				$menu['slug'],
    				array( $this, 'render_page' )
    			);
    		}
    
    		foreach ( $hook_suffixes as $hook_suffix ) {
    			add_action( 'load-' . $hook_suffix , array( $this, 'plugin_load' ) );
    		}
    	}
    
    	/**
    	 * Plugin Load
    	 * Tells plugin to handle post requests
    	 *
    	 * @access public
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function plugin_load() {
    		$this->handle_post_request();
    	}
    
    	/**
    	 * Handle Post Requests
    	 *
    	 * This saves our settings
    	 *
    	 * @access public
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function handle_post_request() {
    		if ( empty( $_POST['action'] ) || 'save' != $_POST['action'] || !current_user_can( 'edit_posts' ) ) return;
    
    		if ( !wp_verify_nonce( $_POST['_wpnonce'], 'taxicons-save-settings' ) ) die( 'Cheating, eh?' );
    
    		$this->get_settings();
    
    		$post_vars = $this->plugin_vars;
    		foreach ( $post_vars as $var => $default ) {
    			if ( !isset( $_POST[$var] ) ) continue;
    			$this->set_setting( $var, sanitize_text_field( $_POST[$var] ) );
    		}
    
    		$this->save_settings();
    	}
    
    	/**
    	 * Render admin settings page
    	 * If setup is not complete, display setup instead.
    	 *
    	 * @access public
    	 * @return void
    	 * @since 0.1.0
    	 */
    	function render_page() {
    		// Not sure why we'd ever end up here, but just in case
    		if ( empty( $_GET['page'] ) ) wp_die( 'Error, page cannot render.' );
    
    		$screen = get_current_screen();
    		$view  = $screen->id;
    
    		$this->render_view( $view );
    	}
    
    	/**
    	 * Render page view
    	 *
    	 * @access public
    	 * @param mixed $view
    	 * @param array $args ( default: array() )
    	 * @return content based on the $view param
    	 * @since 0.1.0
    	 */
    	function render_view( $view, $args = array() ) {
    		extract( $args );
    		include 'view-' . $view . '.php';
    	}
    
    	/**
    	 * Render Taxicon
    	 *
    	 * This outputs the taxonomy icon associated with a specific taxonomy
    	 *
    	 * @access public
    	 * @param mixed $taxonomy
    	 * @return void
    	 */
    	public function render_taxicon ( $taxonomy ) {
    
    		// BAIL: If it's empty, or the taxonomy doesn't exist
    		if ( !$taxonomy || taxonomy_exists( $taxonomy ) == false ) return;
    
    		$filename = $this->get_setting( $taxonomy );
    
    		// BAIL: If the setting is false or otherwise empty
    		if ( $filename == false || !$filename || empty( $filename ) ) return;
    
    		$icon     = file_get_contents( static::IMAGE_PATH . $filename  . '.svg' );
    		$taxicon  = '<span role="img" class="taxonomy-icon ' . $filename . '">' . $icon . '</span>';
    
    		echo $taxicon;
    	}
    
    	/*
    	 * Shortcode
    	 *
    	 * Generate the Taxicon via shortcode
    	 *
    	 * @param array $atts Attributes for the shortcode
    	 *        - tax: The taxonomy
    	 * @return SVG icon of awesomeness
    	 */
    	function shortcode( $atts ) {
    		return $this->render_taxicon( $atts[ 'tax' ] );
    	}
    
    }
    
    new TaxonomyIcons();
    

    File 2: view-appearance_page_taxicons.php

    Why that name? If you look at my render_view function, I pass the ID from get_current_screen() to it, and that means the ID is appearance_page_taxicons and that’s the page name.

    <div class="wrap">
    
    	<h1><?php _e( 'Taxonomy Icons', 'taxonomy-icons' ); ?></h1>
    
    	<div>
    
    		<p><?php __( 'Taxonomy Icons allows you to assign an icon to a non-default taxonomy in order to make it look damn awesome.', 'taxonomy-icons' ); ?></p>
    
    		<p><?php __( 'By default, Taxonomy Icons don\'t display in your theme. In order to use them, you can use a shortcode or a function:' , 'taxonomy-icons' ); ?></p>
    
    		<ul>
    			<li>Shortcode: <code>[taxonomy-icon tax=TAXONOMY]</code></li>
    		</ul>
    
    		<form method="post">
    
    		<?php
    		if ( isset( $_GET['updated'] ) ) {
    			?>
    			<div class="notice notice-success is-dismissible"><p><?php _e( 'Settings saved.', 'taxonomy-icons' ); ?></p></div>
    			<?php
    		}
    		?>
    
    		<input type="hidden" name="action" value="save" />
    		<?php wp_nonce_field( 'taxicons-save-settings' ) ?>
    
    		<table class="form-table">
    
    			<tr>
    				<th scope="row"><?php _e( 'Category', 'taxonomy-icons' ); ?></th>
    				<th scope="row"><?php _e( 'Current Icon', 'taxonomy-icons' ); ?></th>
    				<th scope="row"><?php _e( 'Select Icon', 'taxonomy-icons' ); ?></th>
    			</tr>
    
    			<?php
    
    			foreach ( $this->plugin_vars as $taxonomy => $value ) {
    				?>
    				<tr>
    					<td>
    						<strong><?php echo get_taxonomy( $taxonomy )->label; ?></strong>
    						<br /><em><?php echo get_taxonomy( $taxonomy )->name; ?></em>
    					</td>
    
    					<td>
    						<?php
    						if ( $this->get_setting( $taxonomy ) && $this->get_setting( $taxonomy ) !== false ) {
    							echo $this->render_taxicon( $taxonomy );
    						}
    						?>
    
    					</td>
    
    					<td>
    						<select name="<?php echo $taxonomy; ?>" class="taxonomy-icon">
    							<option value="">-- <?php _e( 'Select an Icon', 'taxonomy-icons' ); ?> --</option>
    							<?php
    							foreach ( $this->images_array as $file => $name ) {
    								?><option value="<?php echo esc_attr( $file ); ?>" <?php echo $file == $this->get_setting( $taxonomy ) ? 'selected="selected"' : ''; ?>><?php echo esc_html( $name ); ?></option><?php
    								};
    							?>
    						</select>
    					</td>
    
    				</tr><?php
    			}
    
    			?>
    
    			<tr valign="top">
    				<td colspan="3">
    					<button type="submit" class="button button-primary"><?php _e( 'Save', 'taxonomy-icons' ); ?></button>
    				</td>
    			</tr>
    		</table>
    		</form>
    	</div>
    </div>
    

    End Result

    And in the end?

    An example of Taxonomy Icons

    By the way, the image has some wrong text, but that is what it looks like.

  • Grandchildren Templates

    Grandchildren Templates

    When I posted about my cleverness with grandchildren themes (which as Cosper pointed out was a plugin), reader Damien mentioned templates. He said:

    I have considered using template_redirect() to override template files in the child theme. I’ve experimented with providing selectable page templates where the template file is not in the child theme directory (not so easy).

    Well Damien, I think you’ll be pleased to know there is a solution.

    WPExplorer has made a WordPress page templates plugin. My caution is not to use the GitHub repo, which is not up to date, but copy the one in the post.

    However … I forked it. And my fork is only going to work if you have both WordPress 4.7 and PHP 7. That’s because in PHP 7, PHP finally decided to allow defines to have arrays.

    The Pre-Code

    I use the template in an MU plugin. It sits in a folder called “cpts” and is summoned by my master index.php file that has this:

    define( 'PAGE_TEMPLATER_ARRAY', [
    	'videos-template.php' => 'Videos Archive',
    ] );
    
    include_once( dirname( __FILE__ ) . '/cpts/NAME-OF-cpt.php' );
    include_once( dirname( __FILE__ ) . '/cpts/page-templater.php' );
    

    As you can see, I define my array and then I call the CPT file and the templater itself.

    The Template File

    This is 90% the same as the original. The two changes are I removed the check and failsafe for pre-WP 4.7, and I changed the section to add templates to call my define.

    <?php
    /*
    Plugin Name: Page Template Plugin
    Plugin URI: http://www.wpexplorer.com/wordpress-page-templates-plugin/
    Version: 2.0
    Author: WPExplorer
    */
    
    class PageTemplater {
    
    	/**
    	 * A reference to an instance of this class.
    	 */
    	private static $instance;
    
    	/**
    	 * The array of templates that this plugin tracks.
    	 */
    	protected $templates;
    
    	/**
    	 * Returns an instance of this class.
    	 */
    	public static function get_instance() {
    
    		if ( null == self::$instance ) {
    			self::$instance = new PageTemplater();
    		}
    
    		return self::$instance;
    
    	}
    
    	/**
    	 * Initializes the plugin by setting filters and administration functions.
    	 */
    	private function __construct() {
    
    		$this->templates = array();
    
    		// Add a filter to the wp 4.7 version attributes metabox
    		add_filter(
    			'theme_page_templates', array( $this, 'add_new_template' )
    		);
    
    		// Add a filter to the save post to inject out template into the page cache
    		add_filter(
    			'wp_insert_post_data',
    			array( $this, 'register_project_templates' )
    		);
    
    
    		// Add a filter to the template include to determine if the page has our
    		// template assigned and return it's path
    		add_filter(
    			'template_include',
    			array( $this, 'view_project_template')
    		);
    
    		$this->templates = PAGE_TEMPLATER_ARRAY;
    
    	}
    
    	/**
    	 * Adds our template to the page dropdown for v4.7+
    	 *
    	 */
    	public function add_new_template( $posts_templates ) {
    		$posts_templates = array_merge( $posts_templates, $this->templates );
    		return $posts_templates;
    	}
    
    	/**
    	 * Adds our template to the pages cache in order to trick WordPress
    	 * into thinking the template file exists where it doens't really exist.
    	 */
    	public function register_project_templates( $atts ) {
    
    		// Create the key used for the themes cache
    		$cache_key = 'page_templates-' . md5( get_theme_root() . '/' . get_stylesheet() );
    
    		// Retrieve the cache list.
    		// If it doesn't exist, or it's empty prepare an array
    		$templates = wp_get_theme()->get_page_templates();
    		if ( empty( $templates ) ) {
    			$templates = array();
    		}
    
    		// New cache, therefore remove the old one
    		wp_cache_delete( $cache_key , 'themes');
    
    		// Now add our template to the list of templates by merging our templates
    		// with the existing templates array from the cache.
    		$templates = array_merge( $templates, $this->templates );
    
    		// Add the modified cache to allow WordPress to pick it up for listing
    		// available templates
    		wp_cache_add( $cache_key, $templates, 'themes', 1800 );
    
    		return $atts;
    
    	}
    
    	/**
    	 * Checks if the template is assigned to the page
    	 */
    	public function view_project_template( $template ) {
    
    		// Get global post
    		global $post;
    
    		// Return template if post is empty
    		if ( ! $post ) {
    			return $template;
    		}
    
    		// Return default template if we don't have a custom one defined
    		if ( ! isset( $this->templates[get_post_meta(
    			$post->ID, '_wp_page_template', true
    		)] ) ) {
    			return $template;
    		}
    
    		$file = plugin_dir_path( __FILE__ ). get_post_meta(
    			$post->ID, '_wp_page_template', true
    		);
    
    		// Just to be safe, we check if the file exist first
    		if ( file_exists( $file ) ) {
    			return $file;
    		} else {
    			echo $file;
    		}
    
    		// Return template
    		return $template;
    
    	}
    
    }
    add_action( 'plugins_loaded', array( 'PageTemplater', 'get_instance' ) );
    

    So long as you have PHP 7+ and WP 4.7+, it all works great.