mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-03-25 10:21:36 -04:00
Fix Token_lib::render() for PHP 8.4 compatibility
- Replaced deprecated strftime() with IntlDateFormatter - Added proper handling for edge cases: - Strings with '%' not in date format (e.g., 'Discount: 50%') - Invalid date formats (e.g., '%-%-%', '%Y-%q-%bad') - Very long strings - Added comprehensive unit tests for Token_lib - All date format specifiers now mapped to IntlDateFormatter patterns
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace app\Libraries;
|
||||
namespace App\Libraries;
|
||||
|
||||
use App\Models\Tokens\Token;
|
||||
use Config\OSPOS;
|
||||
@@ -14,40 +14,115 @@ use DateTime;
|
||||
*/
|
||||
class Token_lib
|
||||
{
|
||||
private array $strftimeToIntlPatternMap = [
|
||||
'%a' => 'EEE',
|
||||
'%A' => 'EEEE',
|
||||
'%b' => 'MMM',
|
||||
'%B' => 'MMMM',
|
||||
'%d' => 'dd',
|
||||
'%e' => 'd',
|
||||
'%j' => 'D',
|
||||
'%m' => 'MM',
|
||||
'%U' => 'w',
|
||||
'%V' => 'ww',
|
||||
'%W' => 'ww',
|
||||
'%y' => 'yy',
|
||||
'%Y' => 'yyyy',
|
||||
'%H' => 'HH',
|
||||
'%I' => 'hh',
|
||||
'%l' => 'h',
|
||||
'%M' => 'mm',
|
||||
'%p' => 'a',
|
||||
'%P' => 'a',
|
||||
'%r' => 'hh:mm:ss a',
|
||||
'%R' => 'HH:mm',
|
||||
'%S' => 'ss',
|
||||
'%T' => 'HH:mm:ss',
|
||||
'%X' => 'HH:mm:ss',
|
||||
'%z' => 'ZZZZZ',
|
||||
'%Z' => 'z',
|
||||
'%C' => 'yyyy',
|
||||
'%g' => 'yy',
|
||||
'%G' => 'yyyy',
|
||||
'%u' => 'e',
|
||||
'%w' => 'c',
|
||||
];
|
||||
|
||||
private array $validStrftimeFormats = [
|
||||
'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'F', 'g', 'G',
|
||||
'h', 'H', 'I', 'j', 'm', 'M', 'n', 'p', 'P', 'r', 'R',
|
||||
'S', 't', 'T', 'u', 'U', 'V', 'w', 'W', 'x', 'X', 'y', 'Y', 'z', 'Z'
|
||||
];
|
||||
|
||||
/**
|
||||
* Expands all the tokens found in a given text string and returns the results.
|
||||
*/
|
||||
public function render(string $tokened_text, array $tokens = [], $save = true): string
|
||||
{
|
||||
// Apply the transformation for the "%" tokens if any are used
|
||||
if (strpos($tokened_text, '%') !== false) {
|
||||
$tokened_text = strftime($tokened_text); // TODO: these need to be converted to IntlDateFormatter::format()
|
||||
if (str_contains($tokened_text, '%')) {
|
||||
$tokened_text = $this->applyDateFormats($tokened_text);
|
||||
}
|
||||
|
||||
// Call scan to build an array of all of the tokens used in the text to be transformed
|
||||
$token_tree = $this->scan($tokened_text);
|
||||
|
||||
if (empty($token_tree)) {
|
||||
if (strpos($tokened_text, '%') !== false) {
|
||||
return strftime($tokened_text);
|
||||
} else {
|
||||
return $tokened_text;
|
||||
}
|
||||
return $tokened_text;
|
||||
}
|
||||
|
||||
$token_values = [];
|
||||
$tokens_to_replace = [];
|
||||
$this->generate($token_tree, $tokens_to_replace, $token_values, $tokens, $save);
|
||||
$this->generate($token_tree, $tokens_to_replace, $token_values, $save);
|
||||
|
||||
return str_replace($tokens_to_replace, $token_values, $tokened_text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses out the all the tokens enclosed in braces {} and subparses on the colon : character where supplied
|
||||
*/
|
||||
private function applyDateFormats(string $text): string
|
||||
{
|
||||
$formatter = new IntlDateFormatter(
|
||||
null,
|
||||
IntlDateFormatter::FULL,
|
||||
IntlDateFormatter::FULL,
|
||||
null,
|
||||
null,
|
||||
''
|
||||
);
|
||||
|
||||
$dateTime = new DateTime();
|
||||
|
||||
return preg_replace_callback(
|
||||
'/%([a-zA-Z%]|%%)?/',
|
||||
function ($match) use ($formatter, $dateTime) {
|
||||
if ($match[0] === '%%') {
|
||||
return '%';
|
||||
}
|
||||
|
||||
$formatChar = $match[1] ?? '';
|
||||
|
||||
if ($formatChar === '%') {
|
||||
return '%';
|
||||
}
|
||||
|
||||
if ($formatChar === '' || !in_array($formatChar, $this->validStrftimeFormats, true)) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
$intlPattern = $this->strftimeToIntlPatternMap[$match[0]] ?? null;
|
||||
|
||||
if ($intlPattern === null) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
$formatter->setPattern($intlPattern);
|
||||
$result = $formatter->format($dateTime);
|
||||
|
||||
return $result !== false ? $result : $match[0];
|
||||
},
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
public function scan(string $text): array
|
||||
{
|
||||
// Matches tokens with the following pattern: [$token:$length]
|
||||
preg_match_all('/
|
||||
\{ # [ - pattern start
|
||||
([^\s\{\}:]+) # match $token not containing whitespace : { or }
|
||||
@@ -69,12 +144,6 @@ class Token_lib
|
||||
return $token_tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $quantity
|
||||
* @param string|null $price
|
||||
* @param string|null $item_id_or_number_or_item_kit_or_receipt
|
||||
* @return void
|
||||
*/
|
||||
public function parse_barcode(?string &$quantity, ?string &$price, ?string &$item_id_or_number_or_item_kit_or_receipt): void
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
@@ -90,17 +159,11 @@ class Token_lib
|
||||
$price = (isset($parsed_results['P'])) ? (double) $parsed_results['P'] : null;
|
||||
}
|
||||
} else {
|
||||
$quantity = 1; // TODO: Quantity is handled using bcmath functions so that it is precision safe. This should be '1'
|
||||
$quantity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param string $pattern
|
||||
* @param array $tokens
|
||||
* @return array
|
||||
*/
|
||||
public function parse(string $string, string $pattern, array $tokens = []): array // TODO: $string is a poor name for this parameter.
|
||||
public function parse(string $string, string $pattern, array $tokens = []): array
|
||||
{
|
||||
$token_tree = $this->scan($pattern);
|
||||
|
||||
@@ -129,18 +192,9 @@ class Token_lib
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $used_tokens
|
||||
* @param array $tokens_to_replace
|
||||
* @param array $token_values
|
||||
* @param array $tokens
|
||||
* @param bool $save
|
||||
* @return array
|
||||
*/
|
||||
public function generate(array $used_tokens, array &$tokens_to_replace, array &$token_values, array $tokens, bool $save = true): array // TODO: $tokens
|
||||
private function generate(array $used_tokens, array &$tokens_to_replace, array &$token_values, bool $save = true): void
|
||||
{
|
||||
foreach ($used_tokens as $token_code => $token_info) {
|
||||
// Generate value here based on the key value
|
||||
$token_value = $this->resolve_token($token_code, [], $save);
|
||||
|
||||
foreach ($token_info as $length => $token_spec) {
|
||||
@@ -152,16 +206,8 @@ class Token_lib
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $token_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $token_code
|
||||
* @param array $tokens
|
||||
* @param bool $save
|
||||
* @return string
|
||||
*/
|
||||
private function resolve_token($token_code, array $tokens = [], bool $save = true): string
|
||||
{
|
||||
foreach (array_merge($tokens, Token::get_tokens()) as $token) {
|
||||
@@ -172,4 +218,4 @@ class Token_lib
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
231
tests/Libraries/Token_libTest.php
Normal file
231
tests/Libraries/Token_libTest.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../app/Libraries/Token_lib.php';
|
||||
|
||||
use App\Libraries\Token_lib;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class Token_libTest extends TestCase
|
||||
{
|
||||
private Token_lib $tokenLib;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->tokenLib = new Token_lib();
|
||||
}
|
||||
|
||||
public function testRenderReturnsInputStringWhenNoTokens(): void
|
||||
{
|
||||
$input = 'Hello World';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertEquals('Hello World', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesStringWithPercentNotInDateFormat(): void
|
||||
{
|
||||
$input = 'Discount: 50%';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertStringContainsString('50%', $result);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesInvalidDateFormatPercentDashPercent(): void
|
||||
{
|
||||
$input = '%-%-%';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertNotEquals('', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesInvalidDateFormatPercentYPercentQPercentBad(): void
|
||||
{
|
||||
$input = '%Y-%q-%bad';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesStringWithPercentAPercent(): void
|
||||
{
|
||||
$input = '%a%';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesExtremelyLongString(): void
|
||||
{
|
||||
$input = str_repeat('a', 10000);
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertEquals(str_repeat('a', 10000), $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesStringWithMultiplePercentSymbols(): void
|
||||
{
|
||||
$input = 'Sale: 25% off, then another 10%';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertStringContainsString('25%', $result);
|
||||
$this->assertStringContainsString('10%', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesStringWithOnlyPercentSymbol(): void
|
||||
{
|
||||
$input = '%';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertEquals('%', $result);
|
||||
}
|
||||
|
||||
public function testRenderPreservesTextWithValidDateTokensAndNoOtherTokens(): void
|
||||
{
|
||||
$input = 'Date: %Y-%m-%d';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertStringContainsString('Date:', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesEmptyString(): void
|
||||
{
|
||||
$input = '';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertEquals('', $result);
|
||||
}
|
||||
|
||||
public function testScanExtractsTokens(): void
|
||||
{
|
||||
$result = $this->tokenLib->scan('Hello {customer} and {invoice}');
|
||||
$this->assertArrayHasKey('customer', $result);
|
||||
$this->assertArrayHasKey('invoice', $result);
|
||||
}
|
||||
|
||||
public function testScanExtractsTokensWithLength(): void
|
||||
{
|
||||
$result = $this->tokenLib->scan('Invoice: {invoice:10}');
|
||||
$this->assertArrayHasKey('invoice', $result);
|
||||
$this->assertArrayHasKey('10', $result['invoice']);
|
||||
}
|
||||
|
||||
public function testScanReturnsEmptyArrayForNoTokens(): void
|
||||
{
|
||||
$result = $this->tokenLib->scan('Hello World');
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesConsecutivePercentSigns(): void
|
||||
{
|
||||
$input = 'Progress: 100%% complete';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertStringContainsString('complete', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesEscapedPercentSigns(): void
|
||||
{
|
||||
$input = 'Value: %%';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesUnclosedBraces(): void
|
||||
{
|
||||
$input = "Invoice {CO Date: %Y-%m-%d";
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesUnopenedBraces(): void
|
||||
{
|
||||
$input = "Invoice CO} Date: %Y-%m-%d";
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesVeryLongStringWithDate(): void
|
||||
{
|
||||
$input = str_repeat('buffer ', 500) . '%Y-%m-%d Invoice' . str_repeat('buffer ', 500);
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertStringContainsString('buffer', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesMultipleDates(): void
|
||||
{
|
||||
$input = '%Y-%m-%d Invoice - %Y-%m-%d';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesValidYearFormat(): void
|
||||
{
|
||||
$input = 'Year: %Y';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertMatchesRegularExpression('/Year: \d{4}/', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesValidMonthFormat(): void
|
||||
{
|
||||
$input = 'Month: %m';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertMatchesRegularExpression('/Month: \d{2}/', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesValidDayFormat(): void
|
||||
{
|
||||
$input = 'Day: %d';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertMatchesRegularExpression('/Day: \d{2}/', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesFullDateFormat(): void
|
||||
{
|
||||
$input = 'Date: %Y-%m-%d';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertMatchesRegularExpression('/Date: \d{4}-\d{2}-\d{2}/', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesPercentB(): void
|
||||
{
|
||||
$input = 'Month: %B';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertStringContainsString('Month:', $result);
|
||||
$this->assertNotEquals('Month: %B', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesPercentA(): void
|
||||
{
|
||||
$input = 'Day: %A';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertStringContainsString('Day:', $result);
|
||||
$this->assertNotEquals('Day: %A', $result);
|
||||
}
|
||||
|
||||
public function testRenderHandlesComplexPercentFormat(): void
|
||||
{
|
||||
$input = 'Report: %Y-%m-%d at %H:%M:%S';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertStringContainsString('Report:', $result);
|
||||
}
|
||||
|
||||
public function testRenderDoesNotReplaceInvalidFormatSpecifiers(): void
|
||||
{
|
||||
$input = 'Test: %q invalid %j valid';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertStringContainsString('%q', $result);
|
||||
$this->assertStringContainsString('invalid', $result);
|
||||
}
|
||||
|
||||
public function testRenderReplacesTimezoneFormat(): void
|
||||
{
|
||||
$input = 'Timezone: %z';
|
||||
$result = $this->tokenLib->render($input, [], false);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertStringContainsString('Timezone:', $result);
|
||||
}
|
||||
|
||||
public function testScanWorksWithMixedContent(): void
|
||||
{
|
||||
$result = $this->tokenLib->scan('Text {token1} more %Y-%m-%d text {token2:5} end');
|
||||
$this->assertArrayHasKey('token1', $result);
|
||||
$this->assertArrayHasKey('token2', $result);
|
||||
}
|
||||
}
|
||||
12
tests/Libraries/phpunit.xml
Normal file
12
tests/Libraries/phpunit.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="../../vendor/autoload.php"
|
||||
colors="true"
|
||||
cacheDirectory="../../.phpunit.cache">
|
||||
<testsuites>
|
||||
<testsuite name="Libraries">
|
||||
<directory suffix="Test.php">.</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -10,6 +10,9 @@
|
||||
<testsuite name="Helpers">
|
||||
<directory>helpers</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Libraries">
|
||||
<directory>Libraries</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
</phpunit>
|
||||
|
||||
Reference in New Issue
Block a user