Wordpress - Prevent comments_template() to load comments.php

Not sure the following solution is better than the solution in OP, let's just say is an alternative, probably more hackish, solution.

I think you can use a PHP exception to stop WordPress execution when 'comments_template' filter is applied.

You can use a custom exception class as a DTO to carry the template.

This is a draft for the excepion:

class CommentsTemplateException extends \Exception {

   protected $template;

   public static function forTemplate($template) {
     $instance = new static();
     $instance->template = $template;

     return $instance;
   }

   public function template() {
      return $this->template;
   }
}

With this exception class available, your function becomes:

function engineCommentsTemplate($myEngine) {

    $filter = function($template) {
       throw CommentsTemplateException::forTemplate($template);
    };  

    try {
       add_filter('comments_template', $filter, PHP_INT_MAX); 
       // this will throw the excption that makes `catch` block run
       comments_template();
    } catch(CommentsTemplateException $e) {
       return $myEngine->render($e->template());
    } finally {
       remove_filter('comments_template', $filter, PHP_INT_MAX);
    }
}

The finally block requires PHP 5.5+.

Works the same way, and doesn't require an empty template.


I have wrestled with this before and my solution was — it can knock itself out requiring file, as long as it doesn't do anything.

Here is relevant code from my Meadow templating project:

public function comments_template( \Twig_Environment $env, $context, $file = 'comments.twig', $separate_comments = false ) {

    try {
        $env->loadTemplate( $file );
    } catch ( \Twig_Error_Loader $e ) {
        ob_start();
        comments_template( '/comments.php', $separate_comments );
        return ob_get_clean();
    }

    add_filter( 'comments_template', array( $this, 'return_blank_template' ) );
    comments_template( '/comments.php', $separate_comments );
    remove_filter( 'comments_template', array( $this, 'return_blank_template' ) );

    return twig_include( $env, $context, $file );
}

public function return_blank_template() {

    return __DIR__ . '/blank.php';
}

I let comments_template() go through the motions to set up globals and such, but feed it empty PHP file to require and move on to my actual Twig template for output.

Note that this requires to be able to intercept initial comments_template() call, which I can do since my Twig template is calling intermediary abstraction rather than actual PHP function.

While I still have to ship empty file for it, I do so in library and implementing theme doesn't have to care about it at all.


Solution: Use a temporary file – with a unique file name

After a lot of jumps and crawling into the dirtiest corners of PHP, I rephrased the question to just:

How can one trick PHP into returning TRUE for file_exists( $file )?

as the code in core just is

file_exists( apply_filters( 'comments_template', $template ) )

Then the question was solved quicker:

$template = tempnam( __DIR__, '' );

and that's it. Maybe it would be better to use wp_upload_dir() instead:

$uploads = wp_upload_dir();
$template = tempname( $uploads['basedir'], '' );

Another option might be to use get_temp_dir() which wraps WP_TEMP_DIR. Hint: It strangely falls back to /tmp/ so files will not get preserved between reboots, which /var/tmp/ would. One can do a simple string comparison at the end and check the return value and then fix this in case it's needed – which is not in this case:

$template = tempname( get_temp_dir(), '' )

Now to quickly test if there are errors thrown for a temporary file without contents:

<?php
error_reporting( E_ALL );
$template = tempnam( __DIR__, '' );
var_dump( $template );
require $template;

And: No Errors → working.

EDIT: As @toscho pointed out in the comments, there's still a better way to do it:

$template = tempnam( trailingslashit( untrailingslashit( sys_get_temp_dir() ) ), 'comments.php' );

Note: According to a users note on php.net docs, the sys_get_temp_dir() behavior differs between systems. Therefore the result gets the trailing slash removed, then added again. As the core bug #22267 is fixed, this should work on Win/ IIS servers now as well.

Your refactored function (not tested):

function engineCommentsTemplate( $engine )
{
    $template = null;

    $tmplGetter = function( $original ) use( &$template ) {
        $template = $original;
        return tempnam( 
            trailingslashit( untrailingslashit( sys_get_temp_dir() ) ),
            'comments.php'
        );
    };

    add_filter( 'comments_template', $tmplGetter, PHP_INT_MAX );

    comments_template();

    remove_filter( 'comments_template', $tmplGetter, PHP_INT_MAX );

    if ( is_file( $template ) && is_readable( $template ) ) {
        return $engine->render( $template );
    }

    return '';
}

Bonus Nr.1: tmpfile() will return NULL. Yeah, really.

Bonus Nr.2: file_exists( __DIR__ ) will return TRUE. Yeah, really … in case you forgot.

^ This leads to an actual bug in WP core.


To help others going in explorer mode and finding those (badly to undocumented pieces), I will quickly sum up what I tried:

Attempt 1: Temporary file in memory

The first attempt I made was to create a stream to a temporary file, using php://temp. From the PHP docs:

The only difference between the two is that php://memory will always store its data in memory, whereas php://temp will use a temporary file once the amount of data stored hits a predefined limit (the default is 2 MB). The location of this temporary file is determined in the same way as the sys_get_temp_dir() function.

The code:

$handle = fopen( 'php://temp', 'r+' );
fwrite( $handle, 'foo' );
rewind( $handle );
var_dump( file_exist( stream_get_contents( $handle, 5 ) );

Finding: Nope, does not work.

Attempt 2: Use a temporary file

There's tmpfile(), so why not use that?!

var_dump( file_exists( tmpfile() ) );
// boolean FALSE

Yeah, that much about this shortcut.

Attempt 3: Use a custom stream wrapper

Next I thought I could build a custom stream wrapper and register it using stream_wrapper_register(). Then I could use a virtual template from that stream to trick core into believing that we have a file. Example code below (I already deleted the full class and history does not have enough steps…)

class TemplateStreamWrapper
{
    public $context;

    public function stream_open( $path, $mode, $options, &$opened )
    {
        // return boolean
    }
}

stream_wrapper_register( 'vt://comments', 'TemplateStreamWrapper' );
// … etc. …

Again, this returned NULL on file_exists().


Tested with PHP 5.6.20

Tags:

Templates