diff --git a/.gitignore b/.gitignore index 8bbc05b..e196396 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ composer.phar # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file # composer.lock /composer.lock +/nbproject/private/ diff --git a/README.md b/README.md index f2189ec..a40d982 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This is a collection of useful classes and functions for every day PHP life. It * Slugifying strings for usage in URLs * Generating random strings * Formatting prices and units +* Simple text templating These classes are no rocket science, just simple helpers that prevent from wiriting the same code in various flavours over and over again. @@ -44,7 +45,7 @@ It is possible to create an object yourself, but it is recommend to use the sing $request = \TgUtils\Request::getRequest(); ``` -Inspect the [source code](https://github.com/technicalguru/php-utils/blob/src/TgUtils/Request.php) to find out about the various methods available. +Inspect the [source code](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Request.php) to find out about the various methods available. ## Date class @@ -74,7 +75,7 @@ $iso8601 = $date->toISO8601(); $someString = $date->format('d.m.Y H:i:s'); ``` -Inspect the [source code](https://github.com/technicalguru/php-utils/blob/src/TgUtils/Date.php) to find out about the various methods available. +Inspect the [source code](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Date.php) to find out about the various methods available. ## Logging @@ -174,8 +175,8 @@ Log::clean(); ## Authentication Helper A simple authentication helper interface along with a default implementation is provided: -* [TgUtils\Auth\CredentialsProvider](https://github.com/technicalguru/php-utils/blob/src/TgUtils/Auth/CredentialsProvider.php) - Interface to provide username and password to other objects -* [TgUtils\Auth\DefaultCredentialsProvider](https://github.com/technicalguru/php-utils/blob/src/TgUtils/Auth/DefaultCredentialsProvider.php) - Simple default implementation of the interface +* [TgUtils\Auth\CredentialsProvider](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Auth/CredentialsProvider.php) - Interface to provide username and password to other objects +* [TgUtils\Auth\DefaultCredentialsProvider](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Auth/DefaultCredentialsProvider.php) - Simple default implementation of the interface ## Sensitive Data Obfuscation @@ -220,7 +221,45 @@ $emailJavascript = Obfuscation::obfuscateEmail('john.doe@example.com', $id); Please notice that not all characters are supported in the default character map. It covers mainly e-mail addresses and phone numbers. However, you can pass your own character set to the obfuscate methods -as third argument. Please consult the [source code](https://github.com/technicalguru/php-utils/blob/src/TgUtils/Obfuscation.php) for more details. +as third argument. Please consult the [source code](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Obfuscation.php) for more details. + +## Text Templating + +To ease the generation of dynamic texts, a template processor is provided. This processor can work on texts that contain variables in +curly brackets `{{variable-definition}}`. The processor knows objects, snippets and formatters. + +**Objects** are application objects that hold attributes that you want to be replaced. An object's attribute will be referenced in a template +with `{{objectKey.attributeName}}`, e.g. `{{user.name}}`. + +**Snippets** are more complex replacements that will be inserted in your template. This is useful when you need the same complex +text structure in multiple template generations, e.g. for a footer or a header text. Snippets are referenced in a template by +their keys only: `{{snippetKey}}`. A snippet is implemented by the interface [`Snippet`](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Templating/Snippet.php). + +**Formatters** can be used to format an object's attribute. Formatters can take parameters to further customize the formatting. A good example +is the [`DateFormatter`](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Templating/DateFormatter.php). The formatter +is referenced with the object's attribute by `{{objectKey.attribute:formatterKey:param1:param2...}}`, e.g. `{{user.created_on:date:rfc822}}`. + +All three elements - objects, snippets and formatters - are given to the [`Processor`](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Templating/Processor.php) in its constructor: + +``` +$objects = array('user' => $myUser); +$snippets = array('header' => new HeaderSnippet(), 'footer' => $new FooterSnippet()); +$formatters = array('date' => new DateFormatter()); +$language = 'en'; +$processor = new Processor($objects, $snippets, $formatters, $language); +``` + +The language is for information and can be used in snippets or formatters to select the right text. + +Finally you can process a template: + +``` +$template = '{{header}} Hello {{user.name}}! Your account was created on {{user.created_on:date:d/m/Y}}.{{footer}}'; +echo $processor->process($template); + +// Output is: +// IMPORTANT MESSAGE! Hello John Doe! Your account was created on 02/03/2017. Best regards! +``` ## Other Utils There are some daily tasks that need to be done in applications. The `Utils` class addresses a few of them: @@ -264,7 +303,7 @@ $priceString = FormatUtils::formatPrice(3000.643, 'EUR'); $fileSize = FormatUtils::formatUnit(3000643, 'B'); ``` -Inspect the [source code](https://github.com/technicalguru/php-utils/blob/src/TgUtils/Utils.php) to find more about the methods available. +Inspect the [source code](https://github.com/technicalguru/php-utils/blob/main/src/TgUtils/Utils.php) to find more about the methods available. # Contribution Report a bug, request an enhancement or pull request at the [GitHub Issue Tracker](https://github.com/technicalguru/php-utils/issues). diff --git a/composer.json b/composer.json index ed7c5be..c6585bf 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,11 @@ "TgLog\\" : "src/TgLog/" } }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, "require-dev" : { "phpunit/phpunit" : "^9" } diff --git a/src/TgUtils/Templating/CurrencyFormatter.php b/src/TgUtils/Templating/CurrencyFormatter.php new file mode 100644 index 0000000..90f3236 --- /dev/null +++ b/src/TgUtils/Templating/CurrencyFormatter.php @@ -0,0 +1,18 @@ + 0 ? $params[0] : ''; + return trim(\TgUtils\FormatUtils::formatPrice($value, $currency, $processor->language, ' ')); + } +} diff --git a/src/TgUtils/Templating/DateFormatter.php b/src/TgUtils/Templating/DateFormatter.php new file mode 100644 index 0000000..6b32e11 --- /dev/null +++ b/src/TgUtils/Templating/DateFormatter.php @@ -0,0 +1,30 @@ +timezone = $timezone; + } + + public function format($value, $params, Processor $processor) { + if ($value == NULL) return ''; + if (!is_object($value) && !is_a($value, 'TgUtils\\Date')) { + $value = new \TgUtils\Date($value, $this->timezone); + } + if (count($params) > 0) { + switch ($params[0]) { + case 'unix': return $value->toUnix(); + case 'iso8601': return $value->toISO8601(TRUE); + case 'rfc822': return $value->toRFC822(TRUE); + } + return $value->format(\TgI18n\I18N::_($params[0]), TRUE, TRUE, $processor->language); + } + return $value->__toString(); + } +} diff --git a/src/TgUtils/Templating/Formatter.php b/src/TgUtils/Templating/Formatter.php new file mode 100644 index 0000000..a06fdac --- /dev/null +++ b/src/TgUtils/Templating/Formatter.php @@ -0,0 +1,16 @@ +language); + } +} diff --git a/src/TgUtils/Templating/Processor.php b/src/TgUtils/Templating/Processor.php new file mode 100644 index 0000000..548551f --- /dev/null +++ b/src/TgUtils/Templating/Processor.php @@ -0,0 +1,148 @@ + object items + * @param array $snippets - snippets with key => snippet object items + * @param array $formatters - formatters with key => formatter object items + */ + public function __construct($objects = NULL, $snippets = NULL, $formatters = NULL, $language = NULL) { + $this->objects = $objects != NULL ? $objects : array(); + $this->snippets = $snippets != NULL ? $snippets : array(); + $this->formatters = $formatters != NULL ? $formatters : array(); + $this->language = $language != NULL ? $language : \TgI18n\I18N::$defaultLangCode; + } + + /** + * Sets the language. + */ + public function setLanguage($language) { + $this->language = $language; + } + + /** + * Replace any variables in content with values. + * a variable is a text with {{object.attribute}} + * if attribute is missing, a snippet with this name will be searched + * @param string $s - the content to be processed + */ + public function process($s) { + $rc = ''; + $matches = array(); + preg_match_all('/{{(.*?)}}/', $s, $matches, PREG_OFFSET_CAPTURE); + $fullMatches = $matches[0]; + $slimMatches = $matches[1]; + $lastEnd = 0; + for ($i=0; $i $lastEnd) $rc .= substr($s, $lastEnd, $newStart-$lastEnd); + // Take over the replacement + $rc .= $this->getVar($sMatch[0]); + // Prepare next iteration + $lastEnd = $newStart + $length; + } + // Finally take the rest + if ($lastEnd < strlen($s)) $rc .= substr($s, $lastEnd); + return $rc; + } + + /** Returns the variable with given content definition. + * A variable is a text with object.attribute + * If attribute is missing, a snippet with this name will be searched + */ + protected function getVar($s) { + $parts = explode('.', $s); + $object = $parts[0]; + if (count($parts) > 1) return $this->getAttribute($object, $parts[1]); + else { + // Is there a string object? + $object = $this->getObject($object); + if (is_string($object)) return $object; + + // Try a snippet + $snippet = $this->getSnippet($object); + if ($snippet != NULL) { + if (is_string($snippet)) return $snippet; + if (is_array($snippet)) return I18N::_($snippet, $this->language); + return $snippet->getOutput($this); + } + } + return '[Not defined: '.$s.']'; + } + + /** + * Returns the object with the given key or NULL. + */ + public function getObject($name) { + return isset($this->objects[$name]) ? $this->objects[$name] : NULL; + } + + /** + * Returns the formatter with the given key or NULL. + */ + public function getFormatter($name) { + return isset($this->formatters[$name]) ? $this->formatters[$name] : NULL; + } + + /** + * Returns the snippet with the given key or NULL. + */ + public function getSnippet($name) { + return isset($this->snippets[$name]) ? $this->snippets[$name] : NULL; + } + + /** + * Returns the value of the attribute in the object. + * An attribute can have a formatter definition attached e.g. created_on:datetime + * The formatter "datetime" will be used then. + * More arguments for the formatter can follow, separated with : again + */ + public function getAttribute($objName, $attr) { + $object = $this->getObject($objName); + $rc = ''; + if ($object != null) { + // Split attributeName from format instructions + $attrDef = explode(':', $attr); + $attrName = array_shift($attrDef); + $attrFormat = count($attrDef) > 0 ? array_shift($attrDef) : 'plain'; + if (isset($object->$attrName)) { + $value = $object->$attrName; + $rc = ''; + // check formatting + if ($attrFormat != 'plain') { + $formatter = $this->getFormatter($attrFormat); + if ($formatter != NULL) { + $rc = $formatter->format($value, $attrDef, $this); + } + } else if (is_object($value)) { + if (is_a($value, 'TgUtils\\Date')) { + $formatter = $this->getFormatter('date'); + if ($formatter == NULL) $formatter = new DateFormatter(); + $rc = $formatter->format($value, $attrDef, $this); + } else { + $rc = $value->__toString(); + } + } else { + $rc = $value; + } + } + } + return $rc; + } + +} diff --git a/src/TgUtils/Templating/Snippet.php b/src/TgUtils/Templating/Snippet.php new file mode 100644 index 0000000..093f83e --- /dev/null +++ b/src/TgUtils/Templating/Snippet.php @@ -0,0 +1,16 @@ +process($template); + $this->assertEquals('This is the output: This is text snippet.', $result); + } + + public function testSnippetWithSnippet(): void { + $processor = self::createProcessor(); + $template = 'This is the output: {{mySnippet}}'; + $result = $processor->process($template); + $this->assertEquals('This is the output: This is my-snippet.', $result); + } + + public function testSimpleAttribute(): void { + $processor = self::createProcessor(); + $template = 'This is the output: {{testObject.name}}'; + $result = $processor->process($template); + $this->assertEquals('This is the output: testObjectName', $result); + } + + public function testFormatter(): void { + $processor = self::createProcessor(); + $template = 'This is the output: {{testObject.aDate:date:rfc822}}'; + $result = $processor->process($template); + $this->assertEquals('This is the output: Fri, 01 Jan 2021 00:00:00 +0000', $result); + } + + protected static function createProcessor(): Processor { + $testObject = new \stdClass; + $testObject->name = 'testObjectName'; + $testObject->aDate = new \TgUtils\Date(1609459200, 'UTC'); + + $objects = array( + 'testObject' => $testObject, + ); + $snippets = array( + 'textSnippet' => 'This is text snippet.', + 'mySnippet' => new TestSnippet(), + ); + $formatters = array( + 'date' => new DateFormatter(), + ); + return new Processor($objects, $snippets, $formatters, 'en'); + } +} + +class TestSnippet implements Snippet { + + public function getOutput($processor) { + return 'This is my-snippet.'; + } +}