197 lines
5.6 KiB
PHP
197 lines
5.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Shared\Helpers;
|
||
|
|
|
||
|
|
final readonly class StringHelper
|
||
|
|
{
|
||
|
|
private function __construct()
|
||
|
|
{
|
||
|
|
// Static class
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Transliterates UTF-8 text to ASCII using ICU Transliterator with iconv fallback.
|
||
|
|
*/
|
||
|
|
public static function transliterate(string $value): string
|
||
|
|
{
|
||
|
|
if (\function_exists('transliterator_transliterate')) {
|
||
|
|
$transliterated = \transliterator_transliterate('Any-Latin; Latin-ASCII', $value);
|
||
|
|
if ($transliterated !== false) {
|
||
|
|
return $transliterated;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (string) iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function mbStrtok(string $delimiters, ?string $str = null): string | false
|
||
|
|
{
|
||
|
|
/** @var int $pos */
|
||
|
|
static $pos = 0;
|
||
|
|
/** @var string $string */
|
||
|
|
static $string = '';
|
||
|
|
|
||
|
|
if ($str !== null) {
|
||
|
|
$pos = 0;
|
||
|
|
$string = $str;
|
||
|
|
}
|
||
|
|
|
||
|
|
$token = '';
|
||
|
|
$len = mb_strlen($string, 'UTF-8');
|
||
|
|
|
||
|
|
while ($pos < $len) {
|
||
|
|
$char = mb_substr($string, $pos, 1, 'UTF-8');
|
||
|
|
$pos++;
|
||
|
|
|
||
|
|
if (mb_strpos($delimiters, $char, 0, 'UTF-8') === false) {
|
||
|
|
$token .= $char;
|
||
|
|
} else {
|
||
|
|
if ($token !== '') {
|
||
|
|
return $token;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($token !== '') {
|
||
|
|
return $token;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Transliterates and formats a string into a URL-friendly slug.
|
||
|
|
*/
|
||
|
|
public static function slugify(string $value, string $separator = '-'): string
|
||
|
|
{
|
||
|
|
$transliterated = self::transliterate($value);
|
||
|
|
$cleaned = preg_replace('/[^a-zA-Z0-9]+/u', $separator, $transliterated) ?? '';
|
||
|
|
|
||
|
|
return strtolower(trim($cleaned, $separator));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert a string to snake_case.
|
||
|
|
*/
|
||
|
|
public static function snakeCase(string $value): string
|
||
|
|
{
|
||
|
|
return mb_strtolower(implode('_', self::splitWords($value)));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert a string to kebab-case.
|
||
|
|
*/
|
||
|
|
public static function kebabCase(string $value): string
|
||
|
|
{
|
||
|
|
return mb_strtolower(implode('-', self::splitWords($value)));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert a string to Title Case.
|
||
|
|
*/
|
||
|
|
public static function titleCase(string $value): string
|
||
|
|
{
|
||
|
|
$trimmed = trim($value);
|
||
|
|
if ($trimmed === '') {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
return mb_convert_case(
|
||
|
|
mb_strtolower($trimmed, 'UTF-8'),
|
||
|
|
\MB_CASE_TITLE,
|
||
|
|
'UTF-8'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert a string to PascalCase (StudlyCase).
|
||
|
|
*/
|
||
|
|
public static function pascalCase(string $value): string
|
||
|
|
{
|
||
|
|
$words = array_map(
|
||
|
|
static fn (string $word): string => mb_ucfirst(mb_strtolower($word)),
|
||
|
|
self::splitWords($value)
|
||
|
|
);
|
||
|
|
|
||
|
|
return implode('', $words);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert a string to camelCase.
|
||
|
|
*/
|
||
|
|
public static function camelCase(string $value): string
|
||
|
|
{
|
||
|
|
return mb_lcfirst(self::pascalCase($value));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Split a string into words, respecting Unicode word boundaries.
|
||
|
|
*/
|
||
|
|
public static function words(string $value, int $words = 100, string $end = ' ...'): string
|
||
|
|
{
|
||
|
|
preg_match('/^\s*+(?:\S+\s*+){1,' . $words . '}/u', $value, $matches);
|
||
|
|
|
||
|
|
if (! isset($matches[0]) || mb_strlen($value) === mb_strlen($matches[0])) {
|
||
|
|
return $value;
|
||
|
|
}
|
||
|
|
|
||
|
|
return rtrim($matches[0]) . $end;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sanitizes and normalizes a text string (strips HTML, removes non-printable control characters, ensures valid UTF-8).
|
||
|
|
*/
|
||
|
|
public static function sanitize(?string $value): ?string
|
||
|
|
{
|
||
|
|
if ($value === null || $value === '') {
|
||
|
|
return $value;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decode HTML entities then strip tags
|
||
|
|
$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||
|
|
$value = strip_tags($value);
|
||
|
|
|
||
|
|
// Normalize newlines, temporarily convert to <br /> to preserve paragraph breaks
|
||
|
|
$value = str_replace(["\r\n", "\r"], "\n", $value);
|
||
|
|
$value = str_replace("\n", '<br />', $value);
|
||
|
|
|
||
|
|
// Ensure valid UTF-8 (handling Windows-1252 / ISO-8859-1 legacy strings)
|
||
|
|
$value = StringEncoder::detectAndConvertToUtf8($value);
|
||
|
|
|
||
|
|
// Remove astral symbols (4-byte UTF-8) that may break storage/processing
|
||
|
|
$value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $value) ?? '';
|
||
|
|
|
||
|
|
// Remove control/non-printable characters but keep TAB(0x09), LF(0x0A) and CR(0x0D)
|
||
|
|
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\xA0\xAD]/u', '', $value) ?? '';
|
||
|
|
|
||
|
|
// Revert <br /> back to newlines and final strip_tags as safety
|
||
|
|
$value = str_replace('<br />', "\n", $value);
|
||
|
|
|
||
|
|
return strip_tags($value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Split an arbitrary string into individual word tokens.
|
||
|
|
*
|
||
|
|
* @return list<string>
|
||
|
|
*/
|
||
|
|
private static function splitWords(string $value): array
|
||
|
|
{
|
||
|
|
// 1. Separate camelCase transitions (e.g., "fooBar" -> "foo Bar")
|
||
|
|
$value = preg_replace('/(?<=[a-z0-9])([A-Z])/u', ' $1', $value) ?? $value;
|
||
|
|
|
||
|
|
// 2. Separate acronyms followed by regular words (e.g., "HTTPResponse" -> "HTTP Response")
|
||
|
|
$value = preg_replace('/(?<=[A-Z])([A-Z][a-z])/u', ' $1', $value) ?? $value;
|
||
|
|
|
||
|
|
// 3. Replace non-alphanumeric characters with whitespace
|
||
|
|
$value = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $value) ?? $value;
|
||
|
|
|
||
|
|
return array_values(array_filter(
|
||
|
|
explode(' ', trim($value)),
|
||
|
|
static fn (string $word): bool => $word !== ''
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|