Add custom templates to the backend dropdown menu in your plugin

If you want to load a custom template for certain post types or such things, it’s easy to do it with the template_redirect action.
But if you are writing a plugin and that you want that plugin to inject page templates in the Template dropdown menu of Page Attributes metabox, there is a way to do it.
Well, not so easy, tough; since there is no hooks to filter the page templates; but we can achieve it by tricking the WordPress cache.

Add a ‘templates’ directory in your plugin, add put your custom page templates in it. Then add this code to your plugin:

new WP_Custom_Plugin_Templates();

class WP_Custom_Plugin_Templates{
    
    /**
     * author : G.Breant
     * version : 0.1
     * 
     */
    
    var $plugin_dir;
    var $templates_dir;
    var $plugin_name;
    
    /**
     * @var $theme_override 
     * if a matching template is found in the current template directory, use it
     * eg. if the plugin is called 'my-plugin' 
     * and the template requested is called 'my-plugin-template.php',
     * It will check for the template 
     * CURRENT_CHILD_THEME/my-plugin/my-plugin-template.php, then
     * CURRENT_PARENT_THEME/my-plugin/my-plugin-template.php, then
     * my-plugin/templates/my-plugin/my-plugin-template.php
     */
    
    var $theme_override; 
    
    function __construct($plugin_dir=false,$templates_dir='templates',$theme_override=true){
        global $wp_custom_plugin_templates;
        
        if(!$plugin_dir)
            $plugin_dir = plugin_dir_path(__FILE__);
        
        $this->plugin_dir = $plugin_dir;
        $this->templates_dir = trailingslashit($this->plugin_dir).$templates_dir;
        $this->theme_override = $theme_override;
        
        $plugin_name_chunks = explode(trailingslashit(WP_PLUGIN_DIR),$plugin_dir);
        $this->plugin_name = rtrim($plugin_name_chunks[1],'/');

        $this->setup_hooks();
        
        $wp_custom_plugin_templates = $this;
        
    }
    
    function setup_hooks(){
        //register templates
        add_filter('init',array(&$this,'inject_page_templates'));

        //load template
        add_filter('page_template',array(&$this,'load_plugin_template'));
    }
    
    function inject_page_templates(){
        global $wp_theme_directories;

        /* there is no hooks to filter the theme pages templates.
         * BUT the function get_page_templates in WP_Theme first checks 'page_templates' in the cache before it runs.
         * So, to register our pages, we will fake that cache value.
         */

        //current themes page templates
        $current_theme = wp_get_theme();
        $theme_page_templates =  (array)$current_theme->get_page_templates();

        //our page templates
        $plugin_page_templates = (array)$this->get_page_templates();

        //merging
        $page_templates = array_merge($theme_page_templates,$plugin_page_templates);

        //replace theme cache value
        $this->replace_theme_cached_data('page_templates',$page_templates);
    }

    function get_page_templates() {

            $page_templates = array();

            $files = (array) glob(trailingslashit($this->templates_dir)."*.php",GLOB_BRACE);

            foreach ( $files as $full_path ) {
                    if ( ! preg_match( '|Template Name:(.*)$|mi', file_get_contents( $full_path ), $header ) )
                            continue;

                    $file_chunks = explode(trailingslashit($this->templates_dir),$full_path);
                    $array_k = trailingslashit($this->plugin_name).$file_chunks[1];

                    $page_templates[ $array_k ] = _cleanup_header_comment( $header[1] );
            }

            return apply_filters('wp_custom_'.$this->plugin_name.'_templates',$page_templates);

    }

    //emulates the cache_add function from class WP_Theme
    function replace_theme_cached_data($key,$data){
        global $wp_theme_directories;

        //get the same hash than the one used to store the 
        $theme_root = $wp_theme_directories[0];
        $chunks = explode(trailingslashit($theme_root),STYLESHEETPATH);
        $theme_stylesheet = $chunks[1];
        $cache_hash = md5( trailingslashit($theme_root) . $theme_stylesheet );
        $cache_expiration = 1800;

        return wp_cache_replace( $key . '-' . $cache_hash, $data, 'themes', $cache_expiration );
    }

    function load_plugin_template($template){
        global $post;

        $template_slug = get_page_template_slug();
        $chunks = explode(trailingslashit($this->plugin_name),$template_slug); //plugin plugin_name found
        if(!isset($chunks[1])) return $template;

        //plugin template set
        $template_filename = $chunks[1];

        $plugin_template = trailingslashit($this->templates_dir).$template_filename;
        if (!file_exists( $plugin_template ) ) return $template;
        
        if($this->theme_override){
            $plugin_template = $this->locate_template($template_filename);
        }

        return apply_filters('wp_custom_'.$this->plugin_name.'_template',$plugin_template,$template_filename,$template);

    }
    
    function locate_template( $template_names, $load = false, $require_once = false ) {

            $located_template = '';
            foreach ( (array) $template_names as $template_name ) {
                    if ( ! $template_name )
                            continue;

                    

                    if ( file_exists( trailingslashit(STYLESHEETPATH) . $this->plugin_name .'/'. $template_name ) ) {
                            $located_template = trailingslashit(STYLESHEETPATH) . $this->plugin_name .'/'. $template_name;

                            break;
                    } else if ( file_exists( trailingslashit(TEMPLATEPATH) . $this->plugin_name .'/'. $template_name ) ) {
                            $located_template = trailingslashit(TEMPLATEPATH) . $this->plugin_name .'/'. $template_name;
                            break;
                    } else if ( file_exists( trailingslashit($this->templates_dir). $template_name ) ) {
                            $located_template = trailingslashit($this->templates_dir). $template_name;
                            break;
                    }
            }

            if ( $load && ( !empty( $located_template ))){

                load_template( $located_template, $require_once );

            }

            return $located_template;
    }
    
    function is_page_template( $template = '' ) {
            if ( ! is_page() )
                    return false;

            $page_template = get_page_template_slug( get_queried_object_id() );
            $chunks =explode($this->plugin_name .'/',$page_template);
            
            if(!isset($chunks[1])) return false;
            
            $page_template = $chunks[1];

            if ( empty( $template ) )
                    return (bool) $page_template;

            if ( $template == $page_template )
                    return true;

            return false;
    }
    
}

Then, you may also need this conditionnal function :

function is_wp_custom_plugin_template($template=''){
    global $wp_custom_plugin_templates;
    if(!isset($wp_custom_plugin_templates)) return false;
    return $wp_custom_plugin_templates->is_page_template($template);
}