Inital commit after splitting vom main project
PHP Composer / build (push) Successful in 35s

This commit is contained in:
2026-07-26 17:18:28 +02:00
commit 0b6dcce287
23 changed files with 6065 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Tez\Utils\Tests\Str;
use PHPUnit\Framework\TestCase;
use Tez\Utils\Str\CaseConverter;
final class CaseConverterTest extends TestCase
{
// -------------------------------------------------------------------------
// toSnake
// -------------------------------------------------------------------------
public function testCamelToSnake(): void
{
self::assertSame('user_profile_name', CaseConverter::toSnake('userProfileName'));
}
public function testPascalToSnake(): void
{
self::assertSame('user_profile_name', CaseConverter::toSnake('UserProfileName'));
}
public function testKebabToSnake(): void
{
self::assertSame('user_profile_name', CaseConverter::toSnake('user-profile-name'));
}
public function testScreamingSnakeToSnake(): void
{
self::assertSame('user_profile_name', CaseConverter::toSnake('USER_PROFILE_NAME'));
}
public function testSnakeToSnakeIsIdempotent(): void
{
self::assertSame('user_profile_name', CaseConverter::toSnake('user_profile_name'));
}
// -------------------------------------------------------------------------
// toCamel
// -------------------------------------------------------------------------
public function testSnakeToCamel(): void
{
self::assertSame('userProfileName', CaseConverter::toCamel('user_profile_name'));
}
public function testKebabToCamel(): void
{
self::assertSame('userProfileName', CaseConverter::toCamel('user-profile-name'));
}
public function testPascalToCamel(): void
{
self::assertSame('userProfileName', CaseConverter::toCamel('UserProfileName'));
}
public function testCamelToCamelIsIdempotent(): void
{
self::assertSame('userProfileName', CaseConverter::toCamel('userProfileName'));
}
// -------------------------------------------------------------------------
// toPascal
// -------------------------------------------------------------------------
public function testSnakeToPascal(): void
{
self::assertSame('UserProfileName', CaseConverter::toPascal('user_profile_name'));
}
public function testKebabToPascal(): void
{
self::assertSame('UserProfileName', CaseConverter::toPascal('user-profile-name'));
}
public function testCamelToPascal(): void
{
self::assertSame('UserProfileName', CaseConverter::toPascal('userProfileName'));
}
// -------------------------------------------------------------------------
// toKebab
// -------------------------------------------------------------------------
public function testPascalToKebab(): void
{
self::assertSame('user-profile-name', CaseConverter::toKebab('UserProfileName'));
}
public function testSnakeToKebab(): void
{
self::assertSame('user-profile-name', CaseConverter::toKebab('user_profile_name'));
}
public function testCamelToKebab(): void
{
self::assertSame('user-profile-name', CaseConverter::toKebab('userProfileName'));
}
// -------------------------------------------------------------------------
// toScreamingSnake
// -------------------------------------------------------------------------
public function testCamelToScreamingSnake(): void
{
self::assertSame('USER_ID', CaseConverter::toScreamingSnake('userId'));
}
public function testKebabToScreamingSnake(): void
{
self::assertSame('USER_PROFILE_NAME', CaseConverter::toScreamingSnake('user-profile-name'));
}
public function testSnakeToScreamingSnake(): void
{
self::assertSame('USER_PROFILE_NAME', CaseConverter::toScreamingSnake('user_profile_name'));
}
// -------------------------------------------------------------------------
// Special cases
// -------------------------------------------------------------------------
public function testSingleWord(): void
{
self::assertSame('name', CaseConverter::toSnake('name'));
self::assertSame('name', CaseConverter::toCamel('name'));
self::assertSame('Name', CaseConverter::toPascal('name'));
self::assertSame('NAME', CaseConverter::toScreamingSnake('name'));
}
public function testAcronymInPascal(): void
{
self::assertSame('html_parser', CaseConverter::toSnake('HTMLParser'));
}
public function testEmptyString(): void
{
self::assertSame('', CaseConverter::toCamel(''));
self::assertSame('', CaseConverter::toSnake(''));
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Tez\Utils\Tests\Str;
use PHPUnit\Framework\TestCase;
use Tez\Utils\Str\Interpolate;
final class InterpolateTest extends TestCase
{
public function testBasicReplacement(): void
{
$result = Interpolate::render(
'Hello {name}, you have {count} messages.',
['name' => 'Anna', 'count' => 5],
);
self::assertSame('Hello Anna, you have 5 messages.', $result);
}
public function testNoPlaceholdersReturnsTemplateUnchanged(): void
{
self::assertSame('Hello World', Interpolate::render('Hello World', []));
}
public function testCustomDelimiters(): void
{
$result = Interpolate::render(
'Hello :name!',
['name' => 'Bob'],
prefix: ':',
suffix: '',
);
self::assertSame('Hello Bob!', $result);
}
public function testNestedKeyWithDotNotation(): void
{
$result = Interpolate::render(
'Welcome, {user.name}!',
['user' => ['name' => 'Clara']],
);
self::assertSame('Welcome, Clara!', $result);
}
public function testDeeplyNestedKey(): void
{
$result = Interpolate::render(
'{a.b.c}',
['a' => ['b' => ['c' => 'deep']]],
);
self::assertSame('deep', $result);
}
public function testMissingKeyThrowsByDefault(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Missing key "missing"');
Interpolate::render('Hello {missing}!', []);
}
public function testMissingKeyUsesFallback(): void
{
$result = Interpolate::render('Hello {missing}!', [], fallback: 'N/A');
self::assertSame('Hello N/A!', $result);
}
public function testEmptyStringFallback(): void
{
$result = Interpolate::render('{greeting} World', [], fallback: '');
self::assertSame(' World', $result);
}
public function testIntegerValueIsStringified(): void
{
$result = Interpolate::render('Count: {n}', ['n' => 42]);
self::assertSame('Count: 42', $result);
}
public function testEmptyTemplateReturnsEmpty(): void
{
self::assertSame('', Interpolate::render('', []));
}
public function testMissingNestedKeyThrows(): void
{
$this->expectException(\InvalidArgumentException::class);
Interpolate::render('{user.email}', ['user' => ['name' => 'Anna']]);
}
public function testMultiplePlaceholdersReplacedInOrder(): void
{
$result = Interpolate::render(
'{a} {b} {a}',
['a' => 'foo', 'b' => 'bar'],
);
self::assertSame('foo bar foo', $result);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Tez\Utils\Tests\Str;
use PHPUnit\Framework\TestCase;
use Tez\Utils\Str\Mask;
final class MaskTest extends TestCase
{
// --- email ---
public function testEmailMasksLocalPart(): void
{
self::assertSame('t***@example.com', Mask::email('test@example.com'));
}
public function testEmailKeepsOnlyFirstChar(): void
{
self::assertSame('a***@domain.org', Mask::email('administrator@domain.org'));
}
public function testEmailEmptyStringReturnsEmpty(): void
{
self::assertSame('', Mask::email(''));
}
public function testEmailWithoutAtSignReturnsOriginal(): void
{
self::assertSame('notanemail', Mask::email('notanemail'));
}
public function testEmailAtSignAtStartReturnsOriginal(): void
{
self::assertSame('@example.com', Mask::email('@example.com'));
}
// --- creditCard ---
public function testCreditCardShowsLastFourDigits(): void
{
self::assertSame('**** **** **** 1111', Mask::creditCard('4111111111111111'));
}
public function testCreditCardStripsSpaces(): void
{
self::assertSame('**** **** **** 1234', Mask::creditCard('4000 0000 0000 1234'));
}
public function testCreditCardStripsHyphens(): void
{
self::assertSame('**** **** **** 9999', Mask::creditCard('4000-0000-0000-9999'));
}
public function testCreditCardTooShortReturnsOriginal(): void
{
self::assertSame('123', Mask::creditCard('123'));
}
// --- iban ---
public function testIbanMasksMiddleSection(): void
{
self::assertSame('DE89 **** **** **** **** 00', Mask::iban('DE89370400440532013000'));
}
public function testIbanStripsExistingSpaces(): void
{
self::assertSame('DE89 **** **** **** **** 00', Mask::iban('DE89 3704 0044 0532 0130 00'));
}
public function testIbanTooShortReturnsOriginal(): void
{
self::assertSame('DE89', Mask::iban('DE89'));
}
// --- custom ---
public function testCustomMasksMiddle(): void
{
self::assertSame('My*********et', Mask::custom('MySuperSecret', start: 2, end: 2));
}
public function testCustomStartOnly(): void
{
self::assertSame('He********', Mask::custom('HelloWorld', start: 2, end: 0));
}
public function testCustomEndOnly(): void
{
self::assertSame('********ld', Mask::custom('HelloWorld', start: 0, end: 2));
}
public function testCustomDifferentMaskChar(): void
{
self::assertSame('He######ld', Mask::custom('HelloWorld', start: 2, end: 2, char: '#'));
}
public function testCustomStartPlusEndEqualsLengthReturnsOriginal(): void
{
self::assertSame('Hello', Mask::custom('Hello', start: 3, end: 2));
}
public function testCustomEmptyStringReturnsEmpty(): void
{
self::assertSame('', Mask::custom('', start: 1, end: 1));
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Tez\Utils\Tests\Str;
use PHPUnit\Framework\TestCase;
use Tez\Utils\Str\Random;
final class RandomTest extends TestCase
{
// -------------------------------------------------------------------------
// token()
// -------------------------------------------------------------------------
public function testTokenReturnsCorrectLength(): void
{
self::assertSame(32, strlen(Random::token(32)));
self::assertSame(64, strlen(Random::token(64)));
}
public function testTokenDefaultLengthIs32(): void
{
self::assertSame(32, strlen(Random::token()));
}
public function testTokenReturnsOnlyHexChars(): void
{
self::assertMatchesRegularExpression('/^[0-9a-f]+$/', Random::token(64));
}
public function testTokenOddLengthWorks(): void
{
self::assertSame(7, strlen(Random::token(7)));
}
public function testTokenThrowsOnZeroLength(): void
{
$this->expectException(\InvalidArgumentException::class);
Random::token(0);
}
public function testTokenThrowsOnNegativeLength(): void
{
$this->expectException(\InvalidArgumentException::class);
Random::token(-1);
}
// -------------------------------------------------------------------------
// alphanumeric()
// -------------------------------------------------------------------------
public function testAlphanumericReturnsCorrectLength(): void
{
self::assertSame(16, strlen(Random::alphanumeric(16)));
}
public function testAlphanumericDefaultLengthIs32(): void
{
self::assertSame(32, strlen(Random::alphanumeric()));
}
public function testAlphanumericReturnsOnlyAlphanumericChars(): void
{
self::assertMatchesRegularExpression('/^[a-zA-Z0-9]+$/', Random::alphanumeric(128));
}
public function testAlphanumericThrowsOnZeroLength(): void
{
$this->expectException(\InvalidArgumentException::class);
Random::alphanumeric(0);
}
// -------------------------------------------------------------------------
// charset()
// -------------------------------------------------------------------------
public function testCharsetReturnsCorrectLength(): void
{
self::assertSame(12, strlen(Random::charset(12, '0123456789')));
}
public function testCharsetReturnsOnlyCharsFromAlphabet(): void
{
$alphabet = 'ABCDE';
$result = Random::charset(100, $alphabet);
self::assertMatchesRegularExpression('/^[ABCDE]+$/', $result);
}
public function testCharsetWithSingleCharReturnsRepeatedChar(): void
{
self::assertSame('aaaa', Random::charset(4, 'a'));
}
public function testCharsetThrowsOnZeroLength(): void
{
$this->expectException(\InvalidArgumentException::class);
Random::charset(0, 'abc');
}
public function testCharsetThrowsOnEmptyAlphabet(): void
{
$this->expectException(\InvalidArgumentException::class);
Random::charset(8, '');
}
// -------------------------------------------------------------------------
// Randomness sanity check
// -------------------------------------------------------------------------
public function testTwoTokensAreDifferent(): void
{
self::assertNotSame(Random::token(64), Random::token(64));
}
public function testTwoAlphanumericStringsAreDifferent(): void
{
self::assertNotSame(Random::alphanumeric(64), Random::alphanumeric(64));
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Tez\Utils\Tests\Str;
use PHPUnit\Framework\TestCase;
use Tez\Utils\Str\Slugify;
final class SlugifyTest extends TestCase
{
public function testBasicSlug(): void
{
self::assertSame('hello-world', Slugify::slug('Hello World'));
}
public function testGermanUmlauts(): void
{
self::assertSame('aerger-mit-umlauten', Slugify::slug('Ärger mit Umlauten!'));
}
public function testGermanSharpS(): void
{
self::assertSame('strasse', Slugify::slug('Straße'));
}
public function testFrenchAccents(): void
{
self::assertSame('creme-brulee', Slugify::slug('Crème brûlée'));
}
public function testMultipleSpacesCollapsed(): void
{
self::assertSame('multiple-spaces', Slugify::slug(' Multiple Spaces '));
}
public function testCustomSeparator(): void
{
self::assertSame('hello_world', Slugify::slug('Hello World', '_'));
}
public function testSpecialCharactersReplaced(): void
{
self::assertSame('foo-bar-baz', Slugify::slug('foo/bar::baz'));
}
public function testLeadingAndTrailingSeparatorsRemoved(): void
{
self::assertSame('hello', Slugify::slug('---hello---'));
}
public function testEmptyStringReturnsEmpty(): void
{
self::assertSame('', Slugify::slug(''));
}
public function testBlankStringReturnsEmpty(): void
{
self::assertSame('', Slugify::slug(' '));
}
public function testNordicCharacters(): void
{
self::assertSame('ae-o', Slugify::slug('Æ Ø'));
}
public function testSpanishN(): void
{
self::assertSame('canon', Slugify::slug('Cañon'));
}
public function testAlreadyValidSlugUnchanged(): void
{
self::assertSame('hello-world-123', Slugify::slug('hello-world-123'));
}
public function testNumbersPreserved(): void
{
self::assertSame('php-8-3', Slugify::slug('PHP 8.3'));
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Tez\Utils\Tests\Str;
use PHPUnit\Framework\TestCase;
use Tez\Utils\Str\Truncate;
final class TruncateTest extends TestCase
{
// -------------------------------------------------------------------------
// Short strings — returned unchanged
// -------------------------------------------------------------------------
public function testShortStringReturnedUnchanged(): void
{
self::assertSame('Short', Truncate::chars('Short', 20));
}
public function testStringExactlyAtLimitReturnedUnchanged(): void
{
self::assertSame('12345', Truncate::chars('12345', 5));
}
// -------------------------------------------------------------------------
// Word boundary (default)
// -------------------------------------------------------------------------
public function testTruncatesAtWordBoundary(): void
{
// cut position falls on a space → keep up to the space
self::assertSame(
'The quick brown fox…',
Truncate::chars('The quick brown fox jumps over the lazy dog', 20),
);
}
public function testTruncatesBackToLastWordWhenCutIsInsideWord(): void
{
// limit=15, suffix=1 → cut at 14 chars → 'The quick brow' → trims to 'The quick'
self::assertSame('The quick…', Truncate::chars('The quick brown fox', 15));
}
public function testWordBoundaryWithNoSpaceInCutKeepsRawCut(): void
{
self::assertSame('Hello…', Truncate::chars('Hellothere', 6));
}
// -------------------------------------------------------------------------
// Exact cut (wordBoundary = false)
// -------------------------------------------------------------------------
public function testExactCutIgnoresWordBoundary(): void
{
self::assertSame(
'The quick brown fox…',
Truncate::chars('The quick brown fox jumps', 20, wordBoundary: false),
);
}
public function testExactCutSplitsWord(): void
{
self::assertSame('The quick bro…', Truncate::chars('The quick brown fox', 14, wordBoundary: false));
}
// -------------------------------------------------------------------------
// Custom suffix
// -------------------------------------------------------------------------
public function testCustomSuffix(): void
{
// suffix=' [...]' (6 chars), limit=22 → cut at 16 → 'The quick brown ' → trims to 'The quick brown'
self::assertSame(
'The quick brown [...]',
Truncate::chars('The quick brown fox jumps', 22, suffix: ' [...]'),
);
}
public function testEmptySuffix(): void
{
self::assertSame('The quick', Truncate::chars('The quick brown fox', 9, suffix: ''));
}
// -------------------------------------------------------------------------
// Multibyte
// -------------------------------------------------------------------------
public function testMultibyteCharactersCountedCorrectly(): void
{
// 'Ärger mit Umlauten' — each char counts as 1 even though ä is multibyte
self::assertSame('Ärger…', Truncate::chars('Ärger mit Umlauten', 6));
}
// -------------------------------------------------------------------------
// Edge cases / exceptions
// -------------------------------------------------------------------------
public function testThrowsWhenLimitShorterThanSuffix(): void
{
$this->expectException(\InvalidArgumentException::class);
Truncate::chars('Hello', 2, suffix: '...');
}
public function testLimitEqualToSuffixLengthReturnsEmptySuffix(): void
{
// limit=3, suffix='...' (3 chars) → cutLength=0 → '' + '...'
self::assertSame('...', Truncate::chars('Hello world', 3, suffix: '...'));
}
}