silassare / php-utils
PHP utils: a set of php utilities used across projects.
Requires
- php: >=8.0
- ext-json: *
- psr/http-message: ^2.0
Requires (Dev)
- oliup/oliup-cs-php: dev-main
- phpunit/phpunit: ^9.5.26
- roave/security-advisories: dev-latest
- vimeo/psalm: ^5.26.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A PHP 8.1+ utility library providing reusable components across projects, with strict typing throughout.
Requirements
- PHP ≥ 8.1
ext-jsonpsr/http-message ^2.0
Installation
composer require silassare/php-utils
Components
- String utilities (
Str) - Class introspection (
ClassUtils) - Stack trace utilities (
FuncUtils) - Dot/bracket path value object (
DotPath) - HTML node builder (
DOM) .envparser & editor (Env)- Event system (
Events) - Rich exceptions (
Exceptions) - Filesystem utilities (
FS) - Interfaces
- Lock system (
Lock) - Traits
- Key-value store (
Store)
Str
PHPUtils\Str — Static string manipulation utilities.
use PHPUtils\Str; // Placeholder interpolation Str::interpolate('Hello, {name}!', ['name' => 'World']); // "Hello, World!" // URL slug Str::stringToURLSlug('Héllo Wörld'); // "hello-world" // Naming conventions Str::toMethodName('my_property'); // "myProperty" Str::toClassName('my_component'); // "MyComponent" Str::toGetterName('name'); // "getName" Str::toSetterName('name'); // "setName" // Prefix / suffix helpers Str::hasPrefix('foobar', 'foo'); // true Str::removePrefix('foobar', 'foo'); // "bar" Str::hasSuffix('foobar', 'bar'); // true Str::removeSuffix('foobar', 'bar'); // "foo" // Closest match suggestion Str::getSuggestion(['get', 'set', 'has'], 'haz'); // "has" // Indentation Str::indent("line1\nline2", ' '); // 4-space indent Str::unIndent(" line1\n line2"); // removes leading tabs // Encoding Str::toUtf8($string); Str::encodeFix($mixedValue); // Hex color Str::hex2rgb('#ff6600'); // [255, 102, 0] Str::hex2rgb('#ff6600', true); // "255,102,0" // Callable name (uses reflection) Str::callableName('array_map'); // "array_map" Str::callableName([MyClass::class, 'method']); // "MyClass::method"
ClassUtils
PHPUtils\ClassUtils — Deep trait introspection with result caching.
use PHPUtils\ClassUtils; // Check if a class (or any parent/trait) uses a trait ClassUtils::hasTrait(MyClass::class, SomeTrait::class); // bool // Get all traits used transitively across the full class hierarchy $traits = ClassUtils::getUsedTraitsDeep(MyClass::class); // ['TraitFQCN' => 'TraitFQCN', ...]
FuncUtils
PHPUtils\FuncUtils — Stack-trace caller location.
use PHPUtils\FuncUtils; function myHelper(): array { return FuncUtils::getCallerLocation(); // ['file' => '...', 'line' => N] } // Returns the file and line of the code that called myHelper() $location = myHelper();
DotPath
PHPUtils\DotPath — Value object for parsed JS-like dot/bracket path strings.
Supported syntax:
| Syntax | Example | Description |
|---|---|---|
| Plain | foo.bar |
Segments matching [a-zA-Z0-9_]+ |
| Bracket integer | items[0] |
Array index access |
| Bracket quoted | map['my.key'] |
Keys containing dots or special chars |
use PHPUtils\DotPath; $path = DotPath::parse('users[0].address.city'); $path->getSegments(); // ['users', '0', 'address', 'city'] (string) $path; // "users.0.address.city" // Keys with special characters $path = DotPath::parse("config['db.host']"); $path->getSegments(); // ['config', 'db.host'] (string) $path; // "config['db.host']"
DOM
PHPUtils\DOM\ — Lightweight HTML node builder.
use PHPUtils\DOM\Tag; $div = new Tag('div'); $div->setAttribute('class', 'container') ->addTextNode('Hello, World!') ->addCommentNode('Generated content'); echo $div; // <div class="container"> // Hello, World! // <!-- Generated content --> // </div> // Self-closing tag $img = (new Tag('img', true)) ->setAttribute('src', '/logo.png') ->setAttribute('alt', 'Logo'); echo $img; // <img src="/logo.png" alt="Logo"/>
Env
PHPUtils\Env\ — Tokenizing .env file parser and token-level editor.
Parsing
use PHPUtils\Env\EnvParser; // From a file $parser = EnvParser::fromFile('/path/to/.env'); // From a string $parser = EnvParser::fromString("APP_ENV=production\nDEBUG=false"); // Get all values $envs = $parser->getEnvs(); // ['APP_ENV' => 'production', 'DEBUG' => false] // Get a single value $env = $parser->getEnv('APP_ENV', 'local'); // 'production' // Merge additional .env content $parser->mergeFromFile('/path/to/.env.local');
Casting rules:
- Unquoted
true/false→bool(whencast_bool=true) - Unquoted numbers →
int/float(whencast_numeric=true) - Quoted values are always
string
Editing
$editor = $parser->edit(); // Update or append a key $editor->upset('APP_ENV', 'staging'); $editor->upset('NEW_KEY', 'value', quote: true); // wraps in double-quotes // Serialize back to string (preserves comments and whitespace) $updated = (string) $editor; file_put_contents('/path/to/.env', $updated);
Events
PHPUtils\Events\ — Priority-based event system with channels.
Priority levels
| Constant | Value | Description |
|---|---|---|
EventInterface::RUN_FIRST |
1 |
Runs before default listeners |
EventInterface::RUN_DEFAULT |
2 |
Default priority |
EventInterface::RUN_LAST |
3 |
Runs after all others (reverse registration order) |
Usage
use PHPUtils\Events\Event; use PHPUtils\Events\EventManager; use PHPUtils\Events\Interfaces\EventInterface; // Define a custom event class UserCreated extends Event { public function __construct(public readonly int $userId) {} } // Register a listener (returns a detach closure) $detach = UserCreated::listen(function (UserCreated $event) { echo "User created: {$event->userId}"; }); // Dispatch the event (new UserCreated(42))->dispatch(); // Detach the listener $detach(); // Stop propagation UserCreated::listen(function (UserCreated $event) { $event->stopPropagation(); }, EventInterface::RUN_FIRST); // Scoped channels UserCreated::listen($handler, channel: 'admin'); (new UserCreated(42))->dispatch(channel: 'admin');
Exceptions
PHPUtils\Exceptions\RuntimeException — Rich exception with structured data and suspect tracking.
use PHPUtils\Exceptions\RuntimeException; throw (new RuntimeException('Invalid configuration', ['key' => 'db.host'])) ->suspect(['value' => $value, 'expected' => 'string']) ->suspectLocation(['file' => __FILE__, 'line' => __LINE__]); // Retrieve exception data $e->getData(); // hides keys starting with '_' $e->getData(true); // shows all keys, including sensitive '_suspect'
Suspect helpers:
| Method | Description |
|---|---|
suspect(array) |
Set raw suspect data |
suspectLocation(['file', 'line?', 'start?', 'end?']) |
Point to a source location |
suspectCallable(callable) |
Record a callable's file/line range |
suspectArray(array, ?string $path) |
Record an array (optionally with a DotPath) |
suspectObject(object, ?string $path) |
Record an object |
FS
PHPUtils\FS\ — Filesystem utilities.
PathUtils
PHPUtils\FS\PathUtils — Static path resolution and normalisation.
use PHPUtils\FS\PathUtils; PathUtils::resolve('/var/www', 'html/index.php'); // "/var/www/html/index.php" PathUtils::resolve('/var/www', '/etc/nginx.conf'); // "/etc/nginx.conf" PathUtils::isRelative('./config'); // true PathUtils::isRelative('/etc'); // false PathUtils::getProtocol('https://example.com'); // "https" PathUtils::getProtocol('/etc/hosts'); // "" // Register a custom protocol resolver PathUtils::registerResolver('storage', fn(string $path) => '/mnt/data/' . ltrim($path, '/')); PathUtils::resolve('', 'storage://uploads/file.txt'); // "/mnt/data/uploads/file.txt"
FSUtils
PHPUtils\FS\FSUtils — Filesystem operations rooted at a base path.
use PHPUtils\FS\FSUtils; $fs = new FSUtils('/var/www/project'); // Navigate $fs->cd('public'); // change root $fs->cd('cache', true); // change root, auto-create if missing // Write / read $fs->wf('config.json', json_encode($data)); $fs->append('log.txt', "New entry\n"); $fs->prepend('log.txt', "Header\n"); // Copy, move, delete $fs->cp('src/', 'dist/'); $fs->rm('temp.txt'); $fs->rmdir('cache/'); // Create directories and symlinks $fs->mkdir('uploads/avatars'); $fs->ln('../shared/assets', 'assets'); // Download a remote file $fs->download('https://example.com/file.zip', 'downloads/file.zip'); // Walk a directory tree $fs->walk('src/', function (string $file, string $path, bool $isDir) { echo $path . PHP_EOL; }); // File metadata $info = $fs->info('uploads/'); $full = $fs->fullPathInfo('config.json');
FilesFilter
PHPUtils\FS\FilesFilter — Chainable file finder / filter.
use PHPUtils\FS\FSUtils; $filter = (new FSUtils('/var/www/project'))->filter() ->isFile() ->name('/\.php$/') ->notPath('/vendor/') ->isReadable(); foreach ($filter->find() as $path => $fileInfo) { echo $path . PHP_EOL; } // Assert conditions (throws RuntimeException on failure) $filter->assert('/var/www/project/index.php'); // Check conditions (returns bool) if (!$filter->check('/var/www/project/index.php')) { echo $filter->getError(); }
Interfaces
| Interface | Description |
|---|---|
PHPUtils\Interfaces\ArrayCapableInterface |
Contracts toArray(): array|ArrayAccess and jsonSerialize(). |
PHPUtils\Interfaces\MetaCapableInterface |
Contracts getMeta(): Map, setMetaKey() and mergeMeta(). |
PHPUtils\Interfaces\RichExceptionInterface |
Rich exception contract with getData(bool $show_sensitive). |
PHPUtils\Lock\Interfaces\LockInterface |
Lock token contract: acquire(), isAcquired(). |
PHPUtils\Lock\Interfaces\ReleasableLockInterface |
Extends LockInterface with release() for reversible locks. |
PHPUtils\Lock\Interfaces\LockableInterface |
Lockable entity contract: getLock(), lock(), unlock(), isLocked(), assertNotLocked(). |
Lock
PHPUtils\Lock\ — Extensible lock system with support for both releasable and permanent locks.
Design
The lock token (LockInterface) is decoupled from the lockable entity (LockableInterface), so you can inject or share a custom lock implementation without changing the entity class.
| Class / Interface | Role |
|---|---|
Interfaces\LockInterface |
Token contract: acquire() + isAcquired() |
Interfaces\ReleasableLockInterface |
Extends LockInterface with release() for reversible locks |
Interfaces\LockableInterface |
Entity contract: getLock(), lock(), unlock(), isLocked(), assertNotLocked() |
Lock |
Default in-memory, releasable ReleasableLockInterface implementation |
PermanentLock |
Irreversible LockInterface implementation — no release() |
Traits\LockableTrait |
Default implementation of LockableInterface; override createLock() to inject a custom token |
Traits\PermanentlyLockableTrait |
Variant of LockableTrait using PermanentLock — unlock() always throws |
Basic usage
use PHPUtils\Lock\Interfaces\LockableInterface; use PHPUtils\Lock\Traits\LockableTrait; class Config implements LockableInterface { use LockableTrait; private array $data = []; public function set(string $key, mixed $value): static { $this->assertNotLocked(); $this->data[$key] = $value; return $this; } } $config = new Config(); $config->set('debug', true); $config->lock(); $config->isLocked(); // true $config->set('debug', false); // throws RuntimeException $config->unlock(); $config->isLocked(); // false $config->set('debug', false); // OK
Permanent (irreversible) lock
use PHPUtils\Lock\Interfaces\LockableInterface; use PHPUtils\Lock\Traits\PermanentlyLockableTrait; class FrozenConfig implements LockableInterface { use PermanentlyLockableTrait; } $config = new FrozenConfig(); $config->lock(); $config->unlock(); // throws RuntimeException — lock is permanent
Shared lock across instances
use PHPUtils\Lock\Lock; use PHPUtils\Lock\Interfaces\LockableInterface; use PHPUtils\Lock\Interfaces\LockInterface; use PHPUtils\Lock\Traits\LockableTrait; $sharedLock = new Lock(); class TenantConfig implements LockableInterface { use LockableTrait; public function __construct(private readonly LockInterface $token) {} protected function createLock(): LockInterface { return $this->token; } } $a = new TenantConfig($sharedLock); $b = new TenantConfig($sharedLock); $a->lock(); $b->isLocked(); // true — same token $a->unlock(); $b->isLocked(); // false — released via shared token
Traits
| Trait | Description |
|---|---|
PHPUtils\Traits\ArrayCapableTrait |
Implements jsonSerialize() by delegating to toArray(). Set $json_empty_array_is_object = true to serialize an empty result as {}. |
PHPUtils\Lock\Traits\LockableTrait |
Implements LockableInterface. Override createLock() to inject a custom LockInterface token. |
PHPUtils\Lock\Traits\PermanentlyLockableTrait |
Variant of LockableTrait using PermanentLock by default — unlock() always throws. |
PHPUtils\Traits\MetaCapableTrait |
Implements MetaCapableInterface. Provides getMeta(): Map (lazy), setMetaKey() and mergeMeta(). Guards mutation with assertNotLocked() when the host implements LockableInterface. |
PHPUtils\Traits\RichExceptionTrait |
Full implementation of RichExceptionInterface with suspect tracking. |
PHPUtils\Traits\RecordableTrait |
Records dynamic method calls via __call() and replays them on another object via play($target). |
MetaCapableTrait
use PHPUtils\Interfaces\MetaCapableInterface; use PHPUtils\Traits\MetaCapableTrait; use PHPUtils\Store\Map; class Product implements MetaCapableInterface { use MetaCapableTrait; } $product = new Product(); // Set individual keys $product->setMetaKey('color', 'red'); $product->setMetaKey('weight', 1.5); // Merge multiple keys at once (array or Map) $product->mergeMeta(['origin' => 'France', 'certified' => true]); // Read back $product->getMeta()->get('color'); // 'red' $product->getMeta()->toArray(); // ['color' => 'red', ...]
When the host class implements LockableInterface, both setMetaKey() and mergeMeta() automatically call assertNotLocked().
RecordableTrait
use PHPUtils\Traits\RecordableTrait; class QueryBuilder { use RecordableTrait; } $builder = new QueryBuilder(); $builder->where('id', 1)->orderBy('name'); // calls recorded // Replay all calls on a real target $realBuilder = new RealQueryBuilder(); $builder->play($realBuilder);
Store
PHPUtils\Store\ — Generic data containers with dot/bracket-notation path access.
Map
PHPUtils\Store\Map — A Store specialised for array<string, mixed> data. Serialises an empty result as {} (JSON object) instead of [].
use PHPUtils\Store\Map; $map = new Map(); $map->set('host', 'localhost'); $map->set('port', 5432); $map->get('host'); // 'localhost' $map->has('port'); // true $map->toArray(); // ['host' => 'localhost', 'port' => 5432] json_encode($map); // '{"host":"localhost","port":5432}' json_encode(new Map()); // '{}' // Accepts a reference to an existing array $data = ['x' => 1]; $map = new Map($data); $map->set('y', 2); $data; // ['x' => 1, 'y' => 2] — reference is mutated
Store (editable)
use PHPUtils\Store\Store; $store = new Store(['user' => ['name' => 'Alice', 'roles' => ['admin']]]); $store->get('user.name'); // "Alice" $store->get('user.roles[0]'); // "admin" $store->has('user.age'); // false $store->set('user.age', 30); $store->set("config['db.host']", 'localhost'); // bracket notation for keys with dots $store->remove('user.roles[0]'); // ArrayAccess $store['user.name'] = 'Bob'; echo $store['user.name']; // "Bob" // Magic properties $store->user; // same as $store->get('user') // Deep merge — nested arrays/objects are merged recursively; type mismatch or leaf values are replaced $store->merge(['user' => ['email' => 'bob@example.com']]); // Dump to array $store->toArray();
StoreNotEditable (read-only)
use PHPUtils\Store\StoreNotEditable; $store = new StoreNotEditable($data); $store->get('key'); // OK $store->set('key', 'value'); // throws RuntimeException
License
MIT — see LICENSE for details.