dranzd / common-utils
Small, dependency-free PHP utilities: array key case conversion and merging, string case conversion, safe nested access, reflection helpers, date validation, money minor-unit conversion and SQL LIKE pattern building.
Requires
- php: ^8.3
- ext-mbstring: *
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.5
- squizlabs/php_codesniffer: ^3.13
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-12 11:14:08 UTC
README
A small set of dependency-free PHP utilities for the chores that keep turning up at the edges of
an application: converting key casing between an API and a database, reading values out of nested
data without isset() chains, validating dates strictly, and building safe SQL LIKE patterns.
Seven final classes of stateless static helpers. No framework, no runtime dependencies beyond
ext-mbstring, checked by PHPStan at its maximum level.
| Class | Purpose |
|---|---|
StringUtil |
Convert between snake_case, camelCase, PascalCase and kebab-case (Unicode-aware) |
ArrayUtil |
Convert array key casing, recursively; merge nested arrays |
Idx |
Read a value from nested arrays and objects, with a default |
ObjectUtil |
Read and write properties of any visibility; instantiate without a constructor |
DateUtil |
Strict, format-based date validation |
MoneyUtil |
Convert between decimal amounts and integer minor units |
SearchUtil |
Build escaped SQL LIKE patterns from user input |
Installation
composer require dranzd/common-utils
Requires PHP 8.3 or newer and ext-mbstring.
Usage
All classes live in the Dranzd\Common\Utils namespace. Invalid arguments throw
\InvalidArgumentException.
StringUtil
Every conversion accepts any input style, because all of them share one word tokenizer.
StringUtil::toSnakeCase('HTTPServerError'); // 'http_server_error' StringUtil::toCamelCase('LAST_NAME'); // 'lastName' StringUtil::toPascalCase('user-id'); // 'UserId' StringUtil::toKebabCase('Élan Vital'); // 'élan-vital'
ArrayUtil
ArrayUtil::keyToSnakeCase([ 'userId' => 7, 'contactDetails' => ['emailAddress' => 'a@example.com'], 'tags' => ['a', 'b'], ], recursive: true); // ['user_id' => 7, 'contact_details' => ['email_address' => 'a@example.com'], 'tags' => ['a', 'b']]
keyToCamelCase(), keyToPascalCase() and keyToKebabCase() work the same way. Integer keys are
left alone, and two keys that would convert to the same key throw instead of silently dropping data:
ArrayUtil::keyToSnakeCase(['firstName' => 1, 'first_name' => 2]); // throws InvalidArgumentException
merge() merges nested arrays. String keys are merged recursively; lists are concatenated unless
you ask for integer keys to be preserved:
$defaults = ['debug' => false, 'db' => ['host' => 'localhost', 'replicas' => ['a']]]; $overrides = ['debug' => true, 'db' => ['port' => 5432, 'replicas' => ['b']]]; ArrayUtil::merge($defaults, $overrides); // ['debug' => true, 'db' => ['host' => 'localhost', 'replicas' => ['a', 'b'], 'port' => 5432]] ArrayUtil::merge([1, 2], [3], preserveNumericKeys: true); // [3, 2]
Idx
Idx::get() walks a key or a path through arrays, ArrayAccess objects and public properties. For a
non-public property it uses the matching accessor (getX(), isX(), hasX(), x()) when there is
one, and reads the property directly otherwise. It never calls a method that has no property behind it.
$order = [ 'id' => 42, 'customer' => (object) ['name' => 'Ada', 'email' => null], 'total' => new Money(1999, 'USD'), // private $amount; public function currency() 'lines' => new ArrayObject([['sku' => 'A-1']]), ]; Idx::get($order, ['customer', 'name']); // 'Ada' Idx::get($order, ['customer', 'phone'], 'n/a'); // 'n/a' (missing) Idx::get($order, ['customer', 'email'], 'n/a'); // null (present, and null) Idx::get($order, ['customer', 'email'], 'n/a', true); // 'n/a' (default on empty) Idx::get($order, ['total', 'currency']); // 'USD' (accessor method) Idx::get($order, ['total', 'amount']); // 1999 (private property) Idx::get($order, ['lines', 0, 'sku']); // 'A-1' Idx::get($order, ['lines', 3, 'sku'], '-'); // '-'
The exact lookup order, and what counts as "empty", is in the Idx reference.
ObjectUtil
Reflection helpers for infrastructure code such as hydrators and tests. Private properties declared on parent classes are found too.
$money = ObjectUtil::newInstanceWithoutConstructor(Money::class); ObjectUtil::setPropertyValue($money, 'amount', 500); ObjectUtil::getPropertyValue($money, 'amount'); // 500 ObjectUtil::hasProperty($money, 'nope'); // false
DateUtil
A string is valid only if it is a real calendar date written exactly in the given format.
DateUtil::isDateValid('2024-02-29'); // true DateUtil::isDateValid('2024-02-30'); // false (no such day) DateUtil::isDateValid('tomorrow'); // false (not in the format) DateUtil::isDateValid('31/12/2024', 'd/m/Y'); // true
MoneyUtil
For the boundary between decimal input and integer storage. Store and calculate money as integer minor units; floats are only safe to convert once, on the way in or out.
MoneyUtil::fromFloatToLowestDenomination(19.99); // 1999 MoneyUtil::fromFloatToLowestDenomination(0.1 + 0.2); // 30 MoneyUtil::fromFloatToLowestDenomination(1500.0, 1); // 1500 (currency without minor units) MoneyUtil::fromLowestDenominationToFloat(1999); // 19.99
SearchUtil
Turns what a user typed into a LIKE pattern, escaping the wildcards they did not mean.
SearchUtil::toLooseIncludeEnds(' john smith '); // '%john%smith%' SearchUtil::toLooseIncludeRight('50% off'); // '50\%%off%' SearchUtil::toLooseIncludeEnds(''); // '%'
The result is a value to bind, never SQL to concatenate. Backslash is the default escape character,
which MySQL, MariaDB and PostgreSQL understand without an ESCAPE clause. For a query that reads the
same on every engine, pass your own escape character and declare it:
$statement = $pdo->prepare("SELECT * FROM users WHERE name LIKE :pattern ESCAPE '!'"); $statement->execute(['pattern' => SearchUtil::toLooseIncludeEnds($input, '!')]);
SQL Server is not supported, because it also treats [ as a wildcard.
Design notes
- Stateless. No I/O and no global state. Only
ObjectUtil::setPropertyValue()modifies what you pass in. - Fail loudly. Bad input is an
\InvalidArgumentExceptionwith a message naming the problem. - Unicode-aware. String handling uses
mb_*functions and/uregular expressions. - No runtime dependencies. Development tooling is the only thing Composer installs.
Development
composer install
composer check # coding standards, static analysis and tests
| Command | Tool |
|---|---|
composer test |
PHPUnit |
composer test:coverage |
PHPUnit with coverage (needs pcov or Xdebug) |
composer phpstan |
PHPStan, max level with strict rules |
composer cs-check / composer cs-fix |
PHP_CodeSniffer, PSR-12 |
A Docker setup is included for running the same commands without a local PHP: make install, then
make check.
CI runs the suite on PHP 8.3, 8.4 and 8.5 and requires at least 95% line coverage.
Contributing
See CONTRIBUTING.md. Security issues: see SECURITY.md.