ahmedzidan / format
Library for string formatting.
Requires
- php: ^7.0 || ^8.0
- ext-pcre: *
- ahmedzidan/php-core: ^1.0
- ahmedzidan/php-value: ^1.0
Requires (Dev)
- phpunit/phpunit: ^6.0
This package is auto-updated.
Last update: 2024-10-21 02:57:04 UTC
README
Format
The format library provides the functionality to format special string
patterns. The implementation is similar to sprintf
and msgfmt_format_message
but with various unique and very useful features.
Installation
Open a terminal, enter your project directory and execute the following command to add this library to your dependencies:
composer require fleshgrinder/format
This command requires you to have Composer installed globally, as explained in the installation chapter of the Composer documentation.
Usage
This library features a single static method to format string patterns. Some examples of the format method are:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('Hello, {}!', ['World']) === 'Hello, World!'); assert(Formatter::format('The number is {}', [1000]) === 'The number is 1,000'); assert(Formatter::format('{}', [[1, 2, 3]]) === '1, 2, 3'); assert(Formatter::format('{value}', ['value' => 42]) === '42'); assert(Formatter::format('{} {}', [1, 2]) === '1 2'); assert(Formatter::format('{.3}', [0.123456789]) === '0.123'); assert(Formatter::format('{.2:and}', [[1, 2, 3]]) === '1.00, 2.00, and 3.00'); assert(Formatter::format('{#b}', [2]) === '0b10'); assert(Formatter::format('{:?}', [tmpfile()]) === 'stream resource');
Placeholders
Each placeholder can specify which argument value it references, and if
omitted it is assumed to be “the next argument”. For example, the pattern
{} {} {}
would take three arguments, and they would be formatted in the
same order as they are given. The pattern {2} {1} {0}
, however, would format
arguments in reverse order.
Things can get a little tricky once you start intermingling the two types of positional placeholders. The “next argument” specifier can be thought of as an iterator over the arguments. Each time a “next argument” specifier is seen, the iterator advances. This leads to behavior like this:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{1} {} {0} {}', [1, 2]) === '2 1 1 2');
The internal iterator over the arguments has not been advanced by the time the
first {}
is seen, so it prints the first argument. Then upon reaching the
second {}
, the iterator has advanced forward to the second argument.
Essentially, placeholders which explicitly specify their argument do not affect
placeholders which do not specify an argument in terms of positional
placeholders.
Placeholders are not limited to numbers, it is also possible to access them
by names. Names are limited to A-Za-z0-9_-
characters, this ensures highest
compatibility and should match 99 % of all associative array keys in the
PHP world:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{placeholder}', ['placeholder' => 'test']) === 'test'); assert(Formatter::format('{placeholder} {}', ['placeholder' => 2, 1]) === '2 1'); assert(Formatter::format('{a} {c} {b}', ['a' => 'a', 'b' => 'b', 'c' => 3]) === 'a 3 b');
A pattern is required to use all placeholders, but not all arguments. A
MissingPlaceholderException
is thrown if a placeholder is missing from the arguments:
<?php use Fleshgrinder\Core\Formatter; use Fleshgrinder\Core\Formatter\MissingPlaceholderException; try { Formatter::format('{}', []); } catch (MissingPlaceholderException $e) { assert($e->getMessage() === 'Placeholder `0` missing from arguments, got empty array'); }
You may refer to the same argument more than once in the pattern, including different modifiers (which are explained in the following sections).
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{a} {a} {a}', ['a' => 'foo']) === 'foo foo foo');
Type Modifier
The type modifier can be used to print the type of an argument value instead of attempting to format the value itself:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{:?}', []) === 'void'); assert(Formatter::format('{:?}', [[]]) === 'array'); assert(Formatter::format('{:?}', [true]) === 'boolean'); assert(Formatter::format('{:?}', [false]) === 'boolean'); assert(Formatter::format('{:?}', [1.2]) === 'float'); assert(Formatter::format('{:?}', [1]) === 'integer'); assert(Formatter::format('{:?}', [null]) === 'null'); assert(Formatter::format('{:?}', [new stdClass]) === 'stdClass'); assert(Formatter::format('{:?}', [new DateTime]) === 'DateTime'); assert(Formatter::format('{:?}', [tmpfile()]) === 'stream resource'); assert(Formatter::format('{:?}', ['']) === 'string');
Combination with positional and named placeholders is of course possible:
<?php use Fleshgrinder\Core\{Formatter, Value}; assert( Formatter::format( 'Expected argument of type {expected}, got {actual:?}', ['expected' => Value::TYPE_ARRAY, 'actual' => ''] ) === 'Expected argument of type array, got string' );
The fleshgrinder/value
library
is used to get the type of the value, this means in effect that type names are
consistent in naming and case. However, the output for resources is slightly
extended by including the type of the resource:
<?php use Fleshgrinder\Core\{Formatter, Value}; $r = tmpfile(); assert(Value::getType($r) === 'resource'); assert(Formatter::format('{:?}', [$r]) === 'stream resource');
IMPORTANT
The type modifier cannot be combined with any other modifier and always overwrites anything else!
String Formatting and Modifiers
The method is binary safe and strings are printed as is.
Since version 1.1.0 empty strings and objects that can be converted to strings
are formatted as empty {:?}
instead of resulting in no output at all:
<?php namespace Fleshgrinder\Examples; use Fleshgrinder\Core\Formatter; class Stringable { function __toString() { return ''; } } assert(Formatter::format('{}', ['']) === 'empty string'); assert(Formatter::format('{}', [new Stringable]) === 'empty Fleshgrinder\Examples\Stringable');
Also available since 1.1.0 are the
caret notation modifier c
and the printable Unicode replacement modifier p
for ASCII control
characters:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{#c}', ["\0\n"]) === '^@^J'); assert(Formatter::format('{#p}', ["\0\n"]) === '␀␊');
This is useful while constructing error messages where a string that violated some constraint should be included in the error message, but might lead to undesired side-effects due to the nature of control characters. Both string modifiers help to mitigate this problem. Note that recreation of the original string might be impossible with the caret notation, the printable Unicode replacements should be used in such cases. However, those characters are UTF-8 characters and not ASCII characters, which might not be acceptable.
Number Formatting and Modifiers
Numbers are formatted with number_format
and
its default values for decimal and thousand separators, this ensures best
readability and consistent formatting. The amount of decimals defaults to zero,
but may be configured by inserting a natural number separated by a dot (.
)
within the braces after the placeholder:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{.3}', [1.23456]) === '1.235'); assert(Formatter::format('$ {.2}', [9999.99]) === '$ 9,999.99'); assert(Formatter::format('{0.3}', [1.2345]) === '1.235'); assert(Formatter::format('{a.2}', ['a' => 1]) === '1.00');
The output format of numbers can be changed with format modifiers. A format
modifier is added by inserting the desired modifier after a hash symbol (#
)
within the braces after the placeholder. Available format modifiers are:
#b
for binary numbers,#e
for exponent notation,#o
for octal, and#x
for hexadecimal.
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{#b}', [2]) === '0b10'); assert(Formatter::format('{#e}', [0.123456789]) === '1.234568e-1'); assert(Formatter::format('{#o}', [493]) === '0o755'); assert(Formatter::format('{#x}', [42]) === '0x2A');
Note that there are no #E
or #X
modifiers, this is on purpose to force
consistent output.
Iterable Listings and Modifiers
Iterable data structures are formatted as comma-separated lists. An optional
conjunction may be configured by adding the desired word after a colon (:
)
inside the braces after the placeholder:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{}', [[]]) === 'empty array'); assert(Formatter::format('{}', [new ArrayIterator]) === 'empty ArrayIterator'); assert(Formatter::format('{}', [['a']]) === 'a'); assert(Formatter::format('{}', [['a', 'b']]) === 'a, b'); assert(Formatter::format('{}', [['a', 'b', 'c']]) === 'a, b, c'); assert(Formatter::format('{:and}', [['a', 'b']]) === 'a and b'); assert(Formatter::format('{:or}', [['a', 'b', 'c']]) === 'a, b, or c');
This is perfect for the construction of error messages:
<?php use Fleshgrinder\Core\Formatter; try { $expected = ['a', 'b', 'c']; $actual = 'x'; if (in_array($actual, $expected, true) === false) { throw new InvalidArgumentException(Formatter::format( 'Value must be one of {expected:or}, got {actual}', ['expected' => $expected, 'actual' => $actual] )); } } catch (InvalidArgumentException $e) { assert($e->getMessage() === 'Value must be one of a, b, or c, got x'); }
Other modifiers, except for type, are of course combinable with iterable listings:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{.1:and}', [[0, 1, 2]]) === '0.0, 1.0, and 2.0'); assert(Formatter::format('{#b:and}', [[0, 1, 2]]) === '0b0, 0b1, and 0b10'); assert(Formatter::format('{#o:and}', [[7, 8, 9]]) === '0o7, 0o10, and 0o11'); assert(Formatter::format('{#x:and}', [[9, 10, 11]]) === '0x9, 0xA, and 0xB');
Note well that expansion of iterable data structures is recursive, this might lead to unexpected output, but it might also result in infinite loops in case the data structure contains circular references. Ensure that your iterable argument values are sane, you have been warned:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('{}', [[0, [1, 2], 3]]) === '0, 1, 2, 3');
Optional Parts
Another unique feature are optional parts, which are included only if a marked argument is present and contains a non-empty value (according to PHP’s empty rules). Optional parts are specified by enclosing some text in brackets:
<?php use Fleshgrinder\Core\Formatter; $pattern = 'This is always printed[, and this is printed only if {this_argument}? is non-empty]'; assert( Formatter::format($pattern, ['this_argument' => '']) === 'This is always printed' ); assert( Formatter::format($pattern, ['this_argument' => 'this argument']) === 'This is always printed, and this is printed only if this argument is non-empty' );
A placeholder is marked by appending a question mark (?
) after the closing
brace of the placeholder. All marked placeholders within an optional part must
be non-empty in order to be included. Note that no exception is thrown if a
marked placeholder within an optional part is missing from the arguments, as
this counts as being empty. Note further that nesting of optional parts is not
possible, however, if you think that this would be greater feature feel free to
open an issue.
IMPORTANT
Only fixed positional and named placeholders can be marked, the internal argument iterator is not taken into account. This means in effect that a pattern like the following never includes the optional part because there is no fixed positional or named placeholder present:
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('[{}?]', ['foobar']) === '');The reason for this limitation is simple: optional parts are evaluated separately before the full pattern is being evaluated. Hence, any internal iterator would start counting inside the optional part, which would lead to hard to understand and weird behavior; if supported.
Escaping
The special characters [
, ]
, {
, and }
can be escaped by doubling them,
[[
, ]]
, {{
, or }}
respectively.
<?php use Fleshgrinder\Core\Formatter; assert(Formatter::format('[[{.1}, {.1}]]', [0, 1]) === '[0.0, 1.0]'); assert(Formatter::format('{:con}}junction}', [[0, 1]]) === '0 con}junction 1');
Errors and Exceptions
Any error or exception that might be thrown by an argument that is being
formatted is not caught and bubbles up. The method itself throws the already
explained MissingPlaceholderException
.
It might throw an InvalidArgumentException
as well, if the type modifier is not present and an argument is:
- a resource—as there is no meaningful way to format them—or
- an object that is not
Traversable
, and has none of the following methods:__toString
toFloat
toInt
toString
Testing
Open a terminal, enter the project directory and execute the following commands
to run the PHPUnit tests with your locally installed
PHP executable. This requires that you have at least make
4.0 installed:
make
You can also execute the following two commands, in case make
is not
available on your system, or too old:
composer install
composer test