26

I'm searching for a way to use named arguments for sprintf or printf.

Example:

sprintf(
  'Last time logged in was %hours hours, 
   %minutes minutes, %seconds seconds ago'
  ,$hours,$minutes, $seconds
);

or via vsprintf and an associative array.

I have found some coding examples here

function sprintfn ($format, array $args = array())

http://php.net/manual/de/function.sprintf.php

and here

function vnsprintf( $format, array $data)

http://php.net/manual/de/function.vsprintf.php

where people wrote their own solutions.

But my question is, is there maybe an standard PHP solution out there to achieve this or is there another way, maybe with a simple PHP templating provided by PEAR, that I can achieve this by sticking to standard PHP?

Thanks for any help.

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Jeremy S.
  • 6,423
  • 13
  • 48
  • 67

10 Answers10

39

Late to the party, but you can simply use strtr to "translate characters or replace substrings"

<?php

$hours = 2;
$minutes = 24;
$seconds = 35;

// Option 1: Replacing %variable
echo strtr(
    'Last time logged in was %hours hours, %minutes minutes, %seconds seconds ago',
    [
        '%hours' => $hours,
        '%minutes' => $minutes,
        '%seconds' => $seconds
    ]
);

// Option 2: Alternative replacing {variable}
echo strtr(
    'Last time logged in was  {hours} hours, {minutes} minutes, {seconds} seconds ago',
    [
        '{hours}' => $hours,
        '{minutes}' => $minutes,
        '{seconds}' => $seconds
    ]
);

// Option 3: Using an array with variables:
$data = [
    '{hours}' => 2,
    '{minutes}' => 24,
    '{seconds}' => 35,
];

echo strtr('Last time logged in was {hours} hours, {minutes} minutes, {seconds} seconds ago', $data);

// More options: Of course you can replace any string....

outputs the following:

Last time logged in was 2 hours, 24 minutes, 35 seconds ago

Alexander Taubenkorb
  • 3,031
  • 2
  • 28
  • 30
  • 3
    Big vote for strtr! I hate sprintf, translators rarely understand the placeholders, especially when there's more than one. "Shipped %1$d on %2$s" conveys nothing and is ripe for errors in typing, whereas "Shipped [QUANTITY] on [DATE]" (or similar) is clear for everyone. – JoLoCo Aug 19 '20 at 17:38
  • 2
    This should be the accepted answer. Dynamite! Although I prefer to use `{replace_me}` as apposed to `%replace_me`. – Carl Brubaker Oct 22 '21 at 14:04
  • @CarlBrubaker good idea. I added this as option. Thx. – Alexander Taubenkorb Oct 29 '21 at 09:45
22

I've written a small component exactly for this need. It's called StringTemplate. With it you can get what you want with a code like this:

$engine = new StringTemplate\Engine;

$engine->render(
   'Last time logged in was {hours} hours, {minutes} minutes, {seconds} seconds ago',
   [
      'hours' => '08',
      'minutes' => 23,
      'seconds' => 12,
   ]
);
//Prints "Last time logged in was 08 hours, 23 minutes, 12 seconds ago"

Hope that can help.

Nicolò Martini
  • 5,182
  • 4
  • 32
  • 38
  • This implementation is excellent. It even has advantages over Python (which was probably the inspiration for this package in the first place). – dreftymac Jul 30 '19 at 03:11
  • **See also:** https://stackoverflow.com/questions/5701985/vsprintf-or-sprintf-with-named-arguments-or-simple-template-parsing-in-php – dreftymac Jul 30 '19 at 03:12
  • 3
    Isn't this just the same as ```strtr()```? – JoLoCo Aug 19 '20 at 17:37
  • 2
    @JoLoCo For flat arrays, yes, it's comparable with `strtr`. But StringTemplate also supports _nested_ arrays, as is illustrated in [this test case](https://github.com/nicmart/StringTemplate/blob/master/tests/StringTemplate/EngineTest.php). – Franz Zieris Apr 22 '21 at 18:40
16

As far as I know printf/sprintf does not accept assoc arrays.

However it is possible to do printf('%1$d %1$d', 1);

Better than nothing ;)

Kamil Tomšík
  • 2,415
  • 2
  • 28
  • 32
  • thanks for your comment, I have seen this of course. But I ask if there are other ways to achieve this – Jeremy S. Apr 18 '11 at 12:01
  • 2
    str_replace/preg_replace/strtr - but I doubt it's what you're looking for. You can also leverage HEREDOC syntax (http://php.net/heredoc) – Kamil Tomšík Apr 18 '11 at 13:01
  • 1
    I went with the solutions form php.net, neverthelesss thanks for answering – Jeremy S. Apr 24 '11 at 10:49
  • No problem, which solution? (just curious) – Kamil Tomšík Apr 24 '11 at 11:03
  • you can see it in the comments to the php.net function documentation http://php.net/manual/de/function.vsprintf.php – Jeremy S. Apr 24 '11 at 16:06
  • This is an old question, but it's still an issue with PHP. HEREDOC is the best solution when formatting a large string with many different variables needed to build it. Keeping track of %s's and %d's is tedious. – Halfstop Apr 24 '19 at 17:39
7

This is from php.net

function vnsprintf( $format, array $data)
{
    preg_match_all( '/ (?<!%) % ( (?: [[:alpha:]_-][[:alnum:]_-]* | ([-+])? [0-9]+ (?(2) (?:\.[0-9]+)? | \.[0-9]+ ) ) ) \$ [-+]? \'? .? -? [0-9]* (\.[0-9]+)? \w/x', $format, $match, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
    $offset = 0;
    $keys = array_keys($data);
    foreach( $match as &$value )
    {
        if ( ( $key = array_search( $value[1][0], $keys, TRUE) ) !== FALSE || ( is_numeric( $value[1][0] ) && ( $key = array_search( (int)$value[1][0], $keys, TRUE) ) !== FALSE) )
        {
            $len = strlen( $value[1][0]);
            $format = substr_replace( $format, 1 + $key, $offset + $value[1][1], $len);
            $offset -= $len - strlen( 1 + $key);
        }
    }
    return vsprintf( $format, $data);
}

Example:

$example = array(
    0 => 'first',
    'second' => 'second',
    'third',
    4.2 => 'fourth',
    'fifth',
    -6.7 => 'sixth',
    'seventh',
    'eighth',
    '9' => 'ninth',
    'tenth' => 'tenth',
    '-11.3' => 'eleventh',
    'twelfth'
);

echo vnsprintf( '%1$s %2$s %3$s %4$s %5$s %6$s %7$s %8$s %9$s %10$s %11$s %12$s<br />', $example); // acts like vsprintf
echo vnsprintf( '%+0$s %second$s %+1$s %+4$s %+5$s %-6.5$s %+6$s %+7$s %+9$s %tenth$s %-11.3$s %+10$s<br />', $example);

Example 2:

$examples = array(
    2.8=>'positiveFloat',    // key = 2 , 1st value
    -3=>'negativeInteger',    // key = -3 , 2nd value
    'my_name'=>'someString'    // key = my_name , 3rd value
);

echo vsprintf( "%%my_name\$s = '%my_name\$s'\n", $examples);    // [unsupported]
echo vnsprintf( "%%my_name\$s = '%my_name\$s'\n", $examples);    // output : "someString"

echo vsprintf( "%%2.5\$s = '%2.5\$s'\n", $examples);        // [unsupported]
echo vnsprintf( "%%2.5\$s = '%2.5\$s'\n", $examples);        // output : "positiveFloat"

echo vsprintf( "%%+2.5\$s = '%+2.5\$s'\n", $examples);        // [unsupported]
echo vnsprintf( "%%+2.5\$s = '%+2.5\$s'\n", $examples);        // output : "positiveFloat"

echo vsprintf( "%%-3.2\$s = '%-3.2\$s'\n", $examples);        // [unsupported]
echo vnsprintf( "%%-3.2\$s = '%-3.2\$s'\n", $examples);        // output : "negativeInteger"

echo vsprintf( "%%2\$s = '%2\$s'\n", $examples);            // output : "negativeInteger"
echo vnsprintf( "%%2\$s = '%2\$s'\n", $examples);            // output : [= vsprintf]

echo vsprintf( "%%+2\$s = '%+2\$s'\n", $examples);        // [unsupported]
echo vnsprintf( "%%+2\$s = '%+2\$s'\n", $examples);        // output : "positiveFloat"

echo vsprintf( "%%-3\$s = '%-3\$s'\n", $examples);        // [unsupported]
echo vnsprintf( "%%-3\$s = '%-3\$s'\n", $examples);        // output : "negativeInteger"
RoboTamer
  • 3,474
  • 2
  • 39
  • 43
5

I know this has been resolved for too long now, but maybe my solution is simple enough, yet useful for somebody else.

With this little function you can mimic a simple templating system:

function parse_html($html, $args) {

  foreach($args as $key => $val) $html = str_replace("#[$key]", $val, $html);

  return $html;
}

Use it like this:

$html = '<h1>Hello, #[name]</h1>';
$args = array('name' => 'John Appleseed';

echo parse_html($html,$args);

This would output:

<h1>Hello, John Appleseed</h1>

Maybe not for everyone and every case, but it saved me.

Armin Cifuentes
  • 517
  • 1
  • 6
  • 17
  • Thats some good approach ! ... Works like charm :D only it uses alot of resources when you look at your logs ( I passed many arguments ) , if only there could be a way for printf or sprintf to accept associative array ... it would 've been aweeesome , Nice Job BTW – Fahad Jul 30 '15 at 19:52
  • @Fahad The loop requires `str_replace`-ing for *every* key, which will always start scanning from the beginning of the string every time, which is a lot of wasted time. A better variation would build up an entirely new string based on the input, iterating over the source string only once. – Deji Nov 06 '15 at 11:34
  • A large amount of data could slow your app down with this approach because the str_replace is inefficient. I like the approach though, it's a useful bit of code. Thanks for sharing. – Halfstop Apr 24 '19 at 17:38
4

See drupal's implementation

https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/format_string/7

It's simple and doesn't use regexp

function format_string($string, array $args = array()) {
  // Transform arguments before inserting them.
  foreach ($args as $key => $value) {
    switch ($key[0]) {
      case '@':
        // Escaped only.
        $args[$key] = check_plain($value);
        break;

      case '%':
      default:
        // Escaped and placeholder.
        $args[$key] = drupal_placeholder($value);
        break;

      case '!':
        // Pass-through.
    }
  }
  return strtr($string, $args);
}

function drupal_placeholder($text) {
  return '<em class="placeholder">' . check_plain($text) . '</em>';
}

Example:

$unformatted = 'Hello, @name';
$formatted = format_string($unformatted, array('@name' => 'John'));
ymakux
  • 3,415
  • 1
  • 34
  • 43
4

Since 5.3 because of the use keyword:

This function supports formatting {{var}} or {{dict.key}}, you can change the {{}} to {} etc to match you favor.

function formatString($str, $data) {
    return preg_replace_callback('#{{(\w+?)(\.(\w+?))?}}#', function($m) use ($data){
        return count($m) === 2 ? $data[$m[1]] : $data[$m[1]][$m[3]];
    }, $str);
}

Example:

$str = "This is {{name}}, I am {{age}} years old, I have a cat called {{pets.cat}}.";
$dict = [
    'name' => 'Jim',
    'age' => 20,
    'pets' => ['cat' => 'huang', 'dog' => 'bai']
];
echo formatString($str, $dict);

Output:

This is Jim, I am 20 years old, I have a cat called huang.

Keaser
  • 351
  • 3
  • 6
4

This is what I'm using:

$arr = ['a' => 'happy','b' => 'funny'];

$templ = "I m a [a] and [b] person";

$r = array_walk($arr,function($i,$k) use(&$templ){
    $templ = str_replace("[$k]",$i,$templ);
} );

var_dump($templ);
zhangv
  • 950
  • 1
  • 9
  • 25
2

This is really the best way to go imho. No cryptic characters, just use the key names!

As taken from the php site: http://www.php.net/manual/en/function.vsprintf.php

function dsprintf() {
  $data = func_get_args(); // get all the arguments
  $string = array_shift($data); // the string is the first one
  if (is_array(func_get_arg(1))) { // if the second one is an array, use that
    $data = func_get_arg(1);
  }
  $used_keys = array();
  // get the matches, and feed them to our function
  $string = preg_replace('/\%\((.*?)\)(.)/e',
    'dsprintfMatch(\'$1\',\'$2\',\$data,$used_keys)',$string);
  $data = array_diff_key($data,$used_keys); // diff the data with the used_keys
  return vsprintf($string,$data); // yeah!
}

function dsprintfMatch($m1,$m2,&$data,&$used_keys) {
  if (isset($data[$m1])) { // if the key is there
    $str = $data[$m1];
    $used_keys[$m1] = $m1; // dont unset it, it can be used multiple times
    return sprintf("%".$m2,$str); // sprintf the string, so %s, or %d works like it should
  } else {
    return "%".$m2; // else, return a regular %s, or %d or whatever is used
  }
}

$str = <<<HITHERE
Hello, %(firstName)s, I know your favorite PDA is the %(pda)s. You must have bought %(amount)s
HITHERE;

$dataArray = array(
  'pda'         => 'Newton 2100',
  'firstName'   => 'Steve',
  'amount'      => '200'
);
echo dsprintf($str, $dataArray);
// Hello, Steve, I know your favorite PDA is the Newton 2100. You must have bought 200
roberthuttinger
  • 1,172
  • 1
  • 17
  • 31
  • 4
    Certainly simple, but using the `/e` PCRE modifier is now [deprecated](http://php.net/manual/en/reference.pcre.pattern.modifiers.php) and a bad idea in the first place. – Synchro Aug 05 '14 at 12:56
0

You'll want to avoid using % in you custom functions as it can interfere with other implementations, for example, date formatting in SQL, so...

function replace(string $string, iterable $replacements): string
{
    return str_replace(
        array_map(
            function($k) {
                return sprintf("{%s}", $k);
            },
            array_keys($replacements)
        ),
        array_values($replacements),
        $string
    );      
}

$string1 = 'Mary had a little {0}. Its {1} was white as {2}.';

echo replace($string1, ['lamb', 'fleece', 'snow']);

$string2 = 'Mary had a little {animal}. Its {coat} was white as {color}.';

echo replace($string2, ['animal' => 'lamb', 'coat' => 'fleece', 'color' => 'snow']);

$string1: Mary had a little lamb. Its fleece was white as snow.
$string2: Mary had a little lamb. Its fleece was white as snow.

Racer X
  • 11
  • 1