Wanted: PHP core function for dynamically performing double-quoted string variable interpolation

For a long time I’ve wished PHP had a built-in function for dynamically evaluating a string variable as a double-quoted string or HEREDOC, to perform variable interpolation (aka expansion) on-the-fly. It’s frequently useful to store a template in a string and then interpolate data into it later, pass the template to a function, etc. Example:


// Template for a URI

$template = '/{$section}/post/{$id}/{$slug}';


function get_uri( $template_id, $data ) {

  // Get appropriate template using $template_id

  $template = get_template( $template_id );


  // Interpolate $data into $template

  $uri = ...;


  return $uri;

}
// get_uri


get_uri( 'post', $data );


Current Techniques

Currently available ways of accomplishing this kind of thing are clunky, annoying, and have other problems. One solution, of sorts, is sprintf(), but there’s a huge, glaring problem with that: you can’t name the variables. You’d have to use a template like this and pass the variables in a specific order:


$template = '/%s/post/%s/%s';


Another solution, of sorts, would be to put the template in a file and include it when necessary:


/* File 1 (uri-template.php) */

<?php return "/{$section}/post/{$id}/{$slug}";


/* File 2 */

// Assume that $section, $id, and $slug are set at this point.

$uri = include "uri-template.php";


It ought to be obvious why that sucks.

There are other solutions using things like regular expressions, loops, str_replace(), etc., but having to define a custom function to be able to do this is a huge pain. It’s really fundamental functionality that should be built in, for reasons such as convenience, performance, robustness, security, consistency, etc.

The closest to a real solution is using eval(), but that has a number of drawbacks that make it far from ideal. At best it’s cumbersome, ugly, and verbose:


// Assume that $section, $id, and $slug are set at this point.

$uri = eval( 'return "' . $template . '";' );

// Or

$uri = eval( "return \"{$template}\";" );


Sure, you could wrap that in a function that makes it less ugly to invoke, but as mentioned above, that’s a huge inconvenience compared to a built-in function.

At best it’s ugly when you can use eval() for this. It wouldn’t even be safe to use it on templates that you haven’t defined yourself (e.g. templates provided as user input), since eval() executes PHP code in general and doesn’t just interpolate variables. I think that in most cases where I’ve needed this functionality, I’ve created the templates myself, but it would be nice to have a general purpose solution that can safely be used with arbitrary templates.

Aside from the ugliness and other issues, I wonder how the performance of using eval() like this would compare with a built-in function.

Solution

What we need is something that would work like this (function name chosen off-the-cuff):


// Assume that $section, $id, and $slug are set at this point.

$uri = interp_vars( $template );

// Or, assume that $vars is an array with 'section', 'about', 'slug' keys

$uri = interp_vars( $template, $vars );


I assume that a core function would have access to the local vars from the calling context, and that the first version of the function is therefore possible, because compact() and extract() do.

I have to consider it more, but what I’m leaning toward at the moment is a single function like this (assuming that interp_vars() could access the local vars from the calling context):


string interp_vars ( string $template [, array|NULL $vars [, bool $allow_local_vars = false ] ] )


Where $allow_local_vars indicates whether variables defined in the local scope should be used if not defined in $vars. E.g.:


$vars = array( 'section' => 'about', 'id' => 123, 'slug' => 'xyz' );

$uri = interp_vars( $template, $vars );

// $uri == '/about/post/123/xyz'


$id = 555;

$uri = interp_vars( $template, $vars, true );

// $uri == '/about/post/123/xyz'


unset( $vars[ 'id' ] );

$uri = interp_vars( $template, $vars, true );

// $uri == '/about/post/555/xyz'


$uri = interp_vars( $template, $vars );

// $uri == '/about/post//xyz'


$uri = interp_vars( $template, NULL, true );

// $uri == '//post/555/'


$uri = interp_vars( $template, array(), true );

// $uri == '//post/555/'


Perhaps the function should convert the third argument to int, so true / false could be used to indicate whether or not to fall back on local variables if they’re not set in $vars, and a constant (e.g. IV_PREF_LOCAL) could be used to indicate that local variables should take precedence over those set in $vars. E.g.:


$vars = array( 'section' => 'about', 'id' => 123, 'slug' => 'xyz' );

$id = 555;


$uri = interp_vars( $template, $vars, IV_PREF_LOCAL );

// $uri == '/about/post/555/xyz'


Maybe it would be good for the third argument to default to true when $vars == NULL. E.g.:

$id = 555;

$uri = interp_vars( $template );

// $uri == '//post/555/'


I assume it’s possible to implement that, and that the bigger issue would be explaining that behavior in the documentation.

What are use cases where that would be undesirable? On balance, would it be more convenient to default $allow_local_vars = true when $vars == NULL (or even when it doesn’t?) and require explicitly calling the function with 3 args to avoid that behavior, or to always default $allow_local_vars = false and require calling with 3 args to use local vars even when $vars == NULL? If the latter, then $vars should be a required argument.

Conclusion

I’ve been meaning to look into this for quite some time to make sure I hadn’t missed a built-in function or something that provides a real solution for this. As far as I can tell, there isn’t a real solution now. Am I still missing something? I did find this PHP bug report, and the author says there are similar ones. I might try harder to find those other bug reports, except that I get the impression from that reporter’s comments that it’s too likely that I’d get aggravated by the responses from the PHP team.

I agree with the main sentiments of the bug report, but I think it would be even better if it also pointed out how ugly eval() solutions are, and that other solutions such as “use of the str_replace function in a loop” are hugely inconvenient compared to a built-in function.

The internal workings of PHP are over my head, so I don’t know how easy / hard this would be to implement, but as the author of the bug report says, it seems like the functionality is basically already there. If it could just be exposed in a core function, that would be great. I can’t believe so much time has passed without this functionality becoming available in the PHP core. Please vote / comment on the bug report, or similar ones, to encourage action. Or better yet, if you have the ability to contribute an implementation for this, that would be awesome. I can only hope it wouldn’t go to waste.

Leave a Reply

Your email address will not be published. Required fields are marked *

Note: Comments are moderated. Spam comments will never be published.

Is this comment spam?