Location: PHPKode > scripts > String Formatter > StringFormatter.php
<?php

/**
 * StringFormatter - simple, but powerful string formatting
 *
 * @author Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @version 0.2
 * @copyright Copyright (c) 2010, Marcin Sztolcman
 * @license http://opensource.org/licenses/lgpl-3.0.html The GNU Lesser General Public License, version 3.0 (LGPLv3)
 */

/**
 * StringFormatter - simple, but powerful string formatting
 *
 * Recognized patterns (indexed version):
 *  * simple replacement:
 *      $f = new StringFormatter ('{} {}!');
 *      echo $f->parse ('Hello', 'world'); # Hello world!
 *
 *  * simple replacement with strict sequence:
 *      $f = new StringFormatter ('{1} {0}!');
 *      echo $f->parse ('world', 'Hello'); # Hello world!
 *
 *  * text alignment:
 *      * left:
 *          $f = new StringFormatter ('{} "{1:<20}"');
 *          echo $f->parse ('Hello', 'world'); # Hello "world               "
 *      * right:
 *          $f = new StringFormatter ('{} "{1:>20}"');
 *          echo $f->parse ('Hello', 'world'); # Hello "               world"
 *      * center:
 *          $f = new StringFormatter ('{} "{1:^20}"');
 *          echo $f->parse ('Hello', 'world'); # Hello "       world        "
 *  * text alignment with specified character:
 *      $f = new StringFormatter ('{} "{1:*^20}"');
 *      echo $f->parse ('Hello', 'world'); # Hello "*******world********"
 *  * sprintf-like formatting:
 *      $f = new StringFormatter ('Test: {:%.3f}');
 *      echo $f->parse (2.1234567); # Test: 2.123
 *      $f = new StringFormatter ('Test 2: {:%c}');
 *      echo $f->parse (97); # Test2: a
 *  * call object method or get object property:
 *      $f = new StringFormatter ('Test: {0->method} {->property}');
 *      class TestStringFormatter {
 *          public $property = 'test property';
 *          public function method () {
 *              return 'test method';
 *          }
 *      }
 *      echo $f->parse (new TestStringFormatter (), new TestStringFormatter ()); # Test: test method test property
 *  * convert int to other base:
 *      $f = new StringFormatter ('Test: 10: {#d}, 16: {0#x}, 2: {0#b}');
 *      echo $f->parse (11); # Test: 10: 11, 16: b, 2: 1011
 *      $f = new StringFormatter ('Test: 10: {#10}, 16: {0#16}, 2: {0#2}, 7: {0#7}');
 *      echo $f->parse (11); # Test: 10: 11, 16: b, 2: 1011, 7: 14
 *
 *      Available bases:
 *          * b - binary
 *          * o - octal
 *          * d - decimal
 *          * x - hex (small letters)
 *          * X - hex (big letters)
 *  * array indexes:
 *      $f = new StringFormatter ('Test: test1: {[test1]}, test2: {0[test2]}');
 *      echo $f->parse (array ('test1' => 'Hello', 'test2' => 'world')); # Test: test1: Hello, test2: world
 *
 *
 *
 * There is also named version of templates. You can use in template, instead of numeric indexes, named arguments, like:
 *      $f = new StringFormatter ('{hello} {name}!');
 * In this case, you must use StringFormatter::parseNamed method to parse this template. This works with one argument only, an array.
 * Keys in that array are related to named tokens in your tenplate, in above example there is: 'hello' and 'name'. Example of use:
 *      echo $f->parseNamed (array ('name' => 'world', 'hello' => 'Hello'); # Hello world!
 *
 * The same mechanism works with every previous example, but there is no automatic ({}) tokens!
 *
 * @author Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @version 0.1
 * @copyright Copyright (c) 2010, Marcin Sztolcman
 * @license http://opensource.org/licenses/lgpl-3.0.html The GNU Lesser General Public License, version 3.0 (LGPLv3)
 */
class StringFormatter {
    const MODE_NORMAL   = 1;
    const MODE_NAMED    = 2;

    /**
     * Matrix for mapping string suffixes to values provided for base_convert function
     *
     * @var array
     */
    protected static $matrix__base_convert = array (
        'b' =>  2,
        'o' =>  8,
        'd' => 10,
        'x' => 16,
        'X' => 16
    );

    /**
     * Matrix for mapping string suffixes to values provided for str_pad function
     *
     * @var array
     */
    protected static $matrix__str_pad = array (
        '<' => STR_PAD_RIGHT,
        '>' => STR_PAD_LEFT,
        '^' => STR_PAD_BOTH,
    );

    /**
     * Regular expressions for key used in template.
     *
     * Key must be one of StringFormatter::MODE_* constant, and value is regular expression used to find key in tokens
     *
     * @var array
     */
    protected static $rxp_keys = array (
        self::MODE_NORMAL   => '\d*',
        self::MODE_NAMED    => '\w+',
    );

    /**
     * Regular expression for finding tokens in format
     *
     * @var string
     */
    protected static $rxp_token = '
        /
        \{              # opening brace
            (
                [^}]*   # all but closing brace
            )
        \}              # closing brace
    /x';

    /**
     * Store provided by user format string
     *
     * @var string
     */
    protected $format = null;

    /**
     * Mode we are run
     *
     * @var int one of: StringFormatter::MODE_NORMAL, StringFormatter::MODE_NAMED
     */
    protected $mode = self::MODE_NORMAL;

    /**
     * Given for StringFormatter::parse parameters
     *
     * @var array
     */
    protected $params = array ();

    /**
     * Pointer for accessing given elements when no placeholder in format is given
     *
     * @var int
     */
    protected $pointer = 0;

    /**
     * Constructor
     *
     * @param string $format format to parse
     */
    public function __construct ($format) {
        $this->format = $format;
    }

    /**
     * Helper function - test for existence of key in given parameters
     *
     * @param string|int key
     * @return bool
     */
    protected function has_key ($key) {
        return ($this->mode == self::MODE_NORMAL && $key == '') || isset ($this->params[$key]);
    }

    /**
     * Helper function for find current param
     *
     * @param int parameter index (optional)
     * @return mixed
     */
    protected function get_param ($key = '') {
        if ($key == '') {
            $key = $this->pointer++;
        }

        return $this->params[$key];
    }

    /**
     * Callback for preg_replace_callback - here is doing all magic with replacing format token with
     * proper values from given arguments in StringFormatter::parse method.
     *
     * @param string matched token data
     * @return string
     */
    protected function format_callback ($data) {
        if (count ($data) < 2) {
            return $data[0];
        }

        ## simple auto or explicit placeholder
        if ($this->mode == self::MODE_NORMAL && $this->has_key ($data[1])) {
            return $this->get_param ($data[1]);
        }

        ## simple named, explicit placeholder
        else if ($this->mode == self::MODE_NAMED && strlen ($data[1]) > 0 && $this->has_key ($data[1])) {
            return $this->get_param ($data[1]);
        }

        ## text alignment
        else if (preg_match ('
            /
            ^
                ('. self::$rxp_keys[$this->mode] .')    # placeholder
                :                                       # explicit colon
                (.)?                                    # pad character
                ([<>^])                                 # alignment
                (\d+)                                   # pad length
            $
            /x', $data[1], $match) &&
            $this->has_key ($match[1])
        ) {
            return str_pad (
                $this->get_param ($match[1]),
                $match[4],
                (strlen ($match[2]) > 0 ? $match[2] : ' '),
                self::$matrix__str_pad[$match[3]]
            );
        }

        ## sprintf pattern
        else if (preg_match ('
            /
            ^
                ('. self::$rxp_keys[$this->mode] .')    # placeholder
                %                                       # explicit percent
                (.*)                                    # sprintf pattern
            $
            /x', $data[1], $match) &&
            $this->has_key ($match[1])
        ) {
            return vsprintf ($match[2], $this->get_param ($match[1]));
        }

        ## call object method or get object property
        else if (preg_match ('
            /
            ^
                ('. self::$rxp_keys[$this->mode] .')    # placeholder
                ->                                      # explicit arrow
                (\w+)                                   # keyword (field or method name)
            $
            /x', $data[1], $match) &&
            $this->has_key ($match[1])
        ) {
            $param = $this->get_param ($match[1]);
            if (method_exists ($param, $match[2])) {
                return call_user_func (array ($param, $match[2]));
            }
            else if (property_exists ($param, $match[2])) {
                return $param->$match[2];
            }
            else if (in_array ('__call', get_class_methods ($param))) {
                return call_user_func (array ($param, $match[2]));
            }
            else if (in_array ('__get', get_class_methods ($param))) {
                return $param->$match[2];
            }
            else {
                return $data[0];
            }
        }

        ## converting int to other base
        else if (preg_match ('
            /
            ^
            ('. self::$rxp_keys[$this->mode] .')    # placeholder
            [#]                                     # explicit hash
            (?:
                (\d+)                               # source base
                [#]                                 # explicit hash
            )?
            ([dxXob]|\d\d?)                         # destination base
            $
            /x', $data[1], $match) &&
            $this->has_key ($match[1])
        ) {
            $ret = base_convert (
                $this->get_param ($match[1]),                       # value to convert
                ($match[2] ? $match[2] : 10),                       # source base (defaults to 10)
                (
                    is_numeric ($match[3])                          # destination base is:
                        ? $match[3]                                 # - numeric
                        : self::$matrix__base_convert[$match[3]]    # - or named
                )
            );
            if ($match[3] == 'X') {
                $ret = strtoupper ($ret);
            }
            return $ret;
        }

        ## array index
        else if (preg_match ('
            /
            ^
                ('. self::$rxp_keys[$this->mode] .')    # placeholder
                \[                                      # opening square bracket
                    (\w+)                               # key
                \]                                      # closing square bracket
            $
            /x', $data[1], $match) &&
            $this->has_key ($match[1]) &&
            is_array ($ret = $this->get_param ($match[1])) &&
            isset ($ret[$match[2]])
        ) {
            return $ret[$match[2]];
        }

        ## unknown token type
        else {
            return $data[0];
        }
    }

    /**
     * Main StringFormatter method - call it with series of argument to create formatted string.
     *
     * @param mixed arg,... parameters used to format given string
     * @return string
     */
    public function parse () {
        $this->mode = self::MODE_NORMAL;

        $this->params = func_get_args ();
        return preg_replace_callback (self::$rxp_token, array ($this, 'format_callback'), $this->format);
    }

    /**
     * Main StringFormatter method - call it with one array argument, when want to use named parameters in your template.
     *
     * Keys in given array must correspond with parameters in template.
     *
     * @param array arg parameters used to format given string
     * @return string
     */
    public function parseNamed (array $args = null) {
        $this->mode = self::MODE_NAMED;

        $this->params = $args;
        return preg_replace_callback (self::$rxp_token, array ($this, 'format_callback'), $this->format);
    }

}
Return current item: String Formatter