efureev / support
PHP Support Package is a collection of useful functions and snippets
Requires
- php: ^8.5
- ext-ctype: *
- ext-json: *
- ext-mbstring: *
Requires (Dev)
- ergebnis/composer-normalize: ^2.52
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.0
- squizlabs/php_codesniffer: ^4.0
Suggests
- ext-intl: For full Unicode transliteration in Str::slugify()/removeAccents(); a bundled character map is used otherwise
- phpunit/phpunit: To use the Php\Support\Testing traits in your own test suite
This package is auto-updated.
Last update: 2026-08-24 00:05:04 UTC
README
The small things you write in every project, written once.
Array and string helpers, collections, a dot-notation store, typed exceptions and a pile of traits — for PHP 8.5, with no runtime dependencies.
composer require efureev/support
use Php\Support\Helpers\Arr; use Php\Support\Helpers\Str; use Php\Support\Structures\Collections\ArrayCollection; Arr::get($config, 'database.connections.pgsql.host', '127.0.0.1'); Str::slugify('Привет, мир!'); // 'privet-mir' (new ArrayCollection($orders)) ->filter(static fn(array $o) => $o['paid']) ->groupBy('customer_id') ->map(static fn(ArrayCollection $c) => $c->sum('total'));
Why
Most projects grow their own helpers.php: a dot-notation getter, a slugifier, a bit-mask check.
This package is that file, extracted and hardened — 98% test coverage, PHPStan level 7, and a
strict test run that fails on warnings and deprecations.
- No runtime dependencies. Only
ext-ctype,ext-jsonandext-mbstring, all standard.ext-intlis optional and improves transliteration. - Multibyte-aware throughout. Case conversion, padding, truncation and masking count characters, not bytes, and work in any script.
- One exception hierarchy. Everything thrown implements a single interface, so one
catchcovers the whole package. - Nothing you cannot address. Global helper functions are convenient but shadowable; every one of them is also a static method on a class nothing can intercept.
Install
composer require efureev/support
Installing on an older PHP
| PHP | Version |
|---|---|
| >= 8.5 | composer require efureev/support "^6.1" |
| 8.4 | composer require efureev/support "^5.3" |
| 8.1 – 8.3 | composer require efureev/support "^4.19" |
| 7.4 – 8.0 | composer require efureev/support "^3.0" |
| 7.2 – 7.4 | composer require efureev/support "^2.0" |
Upgrading a major? See UPGRADE.md.
Arrays
Read and write nested data without a pile of isset().
use Php\Support\Helpers\Arr; $config = ['db' => ['host' => 'localhost', 'ports' => [5432, 5433]]]; Arr::get($config, 'db.host'); // 'localhost' Arr::get($config, 'db.ports.0'); // 5432 Arr::get($config, 'db.user', 'postgres') // 'postgres' — the default Arr::has($config, 'db.host'); // true Arr::set($config, 'db.user', 'admin'); // returns the whole array Arr::remove($config, 'db.ports');
Any separator you like, on every method:
Arr::get($config, 'db/host', null, '/');
Reshape a result set:
$users = [ ['id' => 1, 'name' => 'Ada', 'team' => 'core'], ['id' => 2, 'name' => 'Linus', 'team' => 'core'], ['id' => 3, 'name' => 'Grace', 'team' => 'ops'], ]; Arr::pluck($users, 'name'); // ['Ada', 'Linus', 'Grace'] Arr::pluck($users, 'name', 'id'); // [1 => 'Ada', 2 => 'Linus', 3 => 'Grace'] Arr::keyBy($users, 'id'); // [1 => [...], 2 => [...], 3 => [...]] Arr::only($users[0], ['id', 'name']); // ['id' => 1, 'name' => 'Ada'] Arr::first($users, static fn(array $u) => $u['team'] === 'ops');
Flatten and unflatten:
Arr::dot(['db' => ['host' => 'localhost']]); // ['db.host' => 'localhost'] Arr::undot(['db.host' => 'localhost']); // ['db' => ['host' => 'localhost']] Arr::flatten([1, [2, [3, [4]]]]); // [1, 2, 3, 4]
Strings
Case conversion that understands more than ASCII:
use Php\Support\Helpers\Str; Str::toSnake('getHTTPResponse'); // 'get_http_response' Str::toCamel('user_first_name'); // 'UserFirstName' Str::toLowerCamel('user_id'); // 'userId' Str::toKebab('BackgroundColor'); // 'background-color' Str::toSnake('ПриветМир'); // 'привет_мир' — not only Latin Str::toCamel('über_straße'); // 'ÜberStraße'
URL-safe slugs, transliterating through ICU when ext-intl is available:
Str::slugify('Привет, мир!'); // 'privet-mir' Str::slugify('Il était une fois'); // 'il-etait-une-fois' Str::slugify('Hello World', '_'); // 'hello_world'
Trimming, masking, padding — all counting characters, not bytes:
Str::truncate('The quick brown fox', 9); // 'The...' — at a word boundary Str::limit('The quick brown fox', 9); // 'The quick...' — at an exact length Str::mask('4111111111111111', '*', 4, 8); // '4111********1111' Str::padLeft('7', 3, '0'); // '007' Str::squish(" too much \n space "); // 'too much space'
Identifiers:
Str::uuid(); // '9f8c1f1e-...' — RFC 4122 v4 Str::ulid(); // '01J...' — sortable by creation time Str::random(); // 16 random alphanumerics from the CSPRNG
Collections
ArrayCollection is an ordered map: countable, iterable, ArrayAccess, JSON-serialisable.
use Php\Support\Structures\Collections\ArrayCollection; $orders = new ArrayCollection([ ['id' => 1, 'customer' => 'ada', 'total' => 120, 'paid' => true], ['id' => 2, 'customer' => 'ada', 'total' => 80, 'paid' => true], ['id' => 3, 'customer' => 'linus', 'total' => 200, 'paid' => false], ]); $orders->filter(static fn(array $o) => $o['paid'])->sum('total'); // 200 $orders->pluck('total')->median(); // 120 $orders->countBy('customer'); // ['ada' => 2, 'linus' => 1] $orders->every(static fn(array $o) => $o['total'] > 0); // true $byCustomer = $orders->groupBy('customer'); $byCustomer->get('ada')->count(); // 2 json_encode($orders->pluck('id')); // '[1,2,3]'
Chain and branch without breaking the pipeline:
$orders ->when($onlyPaid, static fn($c) => $c->filter(static fn($o) => $o['paid'])) ->sortBy('total', descending: true) ->take(10) ->values();
HashCollection is the string-keyed sibling, which derives a key from the element's class:
use Php\Support\Structures\Collections\HashCollection; $handlers = new HashCollection(); $handlers[] = new SendEmail(); // keyed by SendEmail::class $handlers->get(SendEmail::class);
Storage
A dot-notation bag, reachable as an object, an array or a loop.
use Php\Support\Storage; $storage = new Storage(); $storage->set('user.profile.name', 'Ada'); $storage->get('user.profile.name'); // 'Ada' $storage->exist('user.profile'); // true $storage['user.profile.name']; // same value, ArrayAccess $storage->user; // ['profile' => ['name' => 'Ada']] foreach ($storage as $key => $value) { /* ... */ } $storage->merge(['app' => ['debug' => true]])->only('app')->toArray();
JSON, numbers, bits, base64
use Php\Support\Helpers\{Json, Number, Bit, B64}; Json::decodeOrThrow('{"a":1}'); // ['a' => 1] — throws on invalid input, unlike decode() Json::prettyPrint(['a' => 1]); Json::isValid('{oops'); // false Number::humanize(1536); // '1.5 KB' Number::ordinal(22); // '22nd' Number::clamp(15, 0, 10); // 10 Number::percentage(1, 3); // 33.33 Number::safeInt('9007199254740993'); // stays a string: too big for JavaScript const READ = 1, WRITE = 2, ADMIN = 4; $perm = Bit::grant([READ, WRITE]); Bit::checkFlag($perm, WRITE); // true Bit::hasAll($perm, [READ, WRITE]);// true Bit::flags($perm); // [1, 2] B64::encodeSafe($binary); // RFC 4648 URL-safe, readable by atob() and Python
Enums
Two traits add the lookups PHP's enums leave out.
use Php\Support\Enums\WithEnhancesForStrings; enum Status: string { use WithEnhancesForStrings; case Draft = 'draft'; case Published = 'published'; } Status::values(); // ['draft', 'published'] Status::names(); // ['Draft', 'Published'] Status::labels(); // ['draft' => 'Draft', 'published' => 'Published'] Status::hasValue('draft'); // true Status::tryFromName('Draft'); // Status::Draft — from() only looks at values Status::casesToEscapeString(); // "'draft', 'published'" — ready for an SQL IN list
Exceptions
Every exception implements one interface, so a single catch covers the package even though the
concrete classes extend different SPL bases.
use Php\Support\Exceptions\ExceptionInterface; try { Json::decodeOrThrow($payload); Str::truncate($title, $length); } catch (ExceptionInterface $e) { // InvalidValueException, InvalidParamException, MissingPropertyException, ... }
Traits
use Php\Support\Traits\{Maker, UseErrorsBox, Metable}; final class Report { use Maker; // Report::make(...$args) use UseErrorsBox; // addError(), errors(), firstError(), hasErrors() use Metable; // withMeta(), metaAttribute() } Report::make() ->withMeta(['source' => 'cron']) ->addError('row 12 is malformed');
HasPrePostActions gives an object named callback groups to run around an operation. A callback
returning exactly false stops the group and makes runActions() return false, which is how a
listener vetoes the work:
use Php\Support\Traits\HasPrePostActions; final class Importer { use HasPrePostActions; public function import(array $rows): bool { if (!$this->runActions('before', $rows)) { return false; // a listener said no } // ... do the work $this->runActions('after', count($rows)); return true; } } $importer = (new Importer()) ->addCallbackAction('before', static fn(array $rows) => $rows !== []) ->addCallbackAction('after', static fn(int $count) => $logger->info("imported {$count}")); $importer->import([]); // false — the guard vetoed it, nothing ran $importer->import([['id' => 1]]); // true
All traits
| Trait | What it gives you |
|---|---|
Maker |
::make(...) static constructor |
Singleton |
::getInstance(), protected constructor, unserialize guard |
Thrower |
::throw(), ::throwIf() on an exception class |
UseErrorsBox |
collect errors on an object: addError, firstError, errorsCount |
Metable |
attach metadata: withMeta, metaAttribute, setMetaAttribute |
UseStorage |
back an object with a Storage, reachable as properties |
UseConfigurableStorage |
configurable() plus a storage fallback for unknown keys |
ConfigurableTrait |
configure an object from an array through setters or properties |
ReadOnlyProperties |
expose chosen non-public properties for reading |
HasPrePostActions |
register and run callback groups around an operation |
Whener |
when($condition, $callback) for fluent chains |
ConsolePrint |
print() / printError(), working outside the CLI SAPI too |
TraitBooter |
Eloquent-style bootXxx() hooks for traits |
TraitInitializer |
initializeXxx() hooks, run per instance |
Conditional handlers
ConditionalHandler pairs a piece of work with the condition that guards it, so the two travel
together and the work is only built when it is actually needed. Both closures receive the same
arguments.
use Php\Support\ConditionalHandler; $auditLink = ConditionalHandler::make( static fn(User $user) => ['label' => 'Audit log', 'href' => '/audit'], )->handleIf(static fn(User $user) => $user->isAdmin()); $auditLink($admin); // ['label' => 'Audit log', 'href' => '/audit'] $auditLink($guest); // null — the handler was never called
Useful for assembling a menu, a set of API fields or a list of tabs, where each entry knows for itself whether it belongs:
$fields = array_filter(array_map( static fn(ConditionalHandler $field) => $field($user), [$auditLink, $billingLink, $profileLink], ));
The condition can also be a plain boolean, and the handler is immutable — handleIf() returns a
new instance rather than mutating the old one:
ConditionalHandler::make(static fn() => buildReport(), $featureEnabled);
PostgreSQL types
use Php\Support\Helpers\Arr; use Php\Support\Types\GeoPoint; Arr::toPostgresArray(['a,b', 'plain', null]); // '{"a,b",plain,NULL}' Arr::fromPostgresArray('{"a,b",plain}'); // ['a,b', 'plain'] $point = GeoPoint::castFromDatabase('(37.6,55.7)'); $point->toJson(); // '{"longitude":37.6,"latitude":55.7}'
Elements are quoted only when the array literal requires it, and escaping is handled — a value containing a comma stays one element.
Testing helpers
Two traits for your own test suite. AdditionalAssertionsTrait needs phpunit/phpunit, declared
in suggest.
use Php\Support\Testing\{AdditionalAssertionsTrait, TestingHelper}; final class ReportTest extends TestCase { use AdditionalAssertionsTrait; use TestingHelper; public function testItIsMakeable(): void { static::assertClassUsesTraits(Report::class, [Maker::class]); self::assertSame('hidden', static::runProtectedMethod(new Report(), 'internal')); self::assertSame(1, static::getProperty(new Report(), 'privateCounter')); } }
Global functions
The helpers are also available as global functions, and as static methods on Php\Support\Func:
dataGet($order, 'customer.address.city'); Func::dataGet($order, 'customer.address.city'); // identical
Prefer the class in library code. Every global is declared under function_exists(), so a name
another package claimed first — Laravel defines value(), class_basename() and
class_uses_recursive() — wins silently, which makes behaviour depend on autoload order. Func
cannot be shadowed.
Full API reference
Arr — accessible, collapse, crossJoin, dataToArray, divide, dot, duplicates, except, exists,
fillKeysByValues, first, flatten, fromPostgresArray, fromPostgresArrayWithBraces, fromPostgresPoint,
get, has, isAssoc, isList, keyBy, last, map, merge, only, pluck, prepend, random, remove,
removeByValue, replaceByTemplate, set, shuffle, sortRecursive, toArray, toIndexedArray,
toPostgresArray, toPostgresPoint, undot, where, whereNotNull, wrap
Str — after, before, between, clearCache, contains, endsWith, isRegExp, lcFirst, limit, mask,
padBoth, padLeft, padRight, random, removeAccents, removeMultiSpace, replaceByTemplate,
replaceStrTo, slugify, slugifyWithFormat, squish, startsWith, title, toCamel, toCamelInitCase,
toDelimited, toKebab, toLowerCamel, toScreamingDelimited, toScreamingSnake, toSnake, trimPrefix,
trimSuffix, truncate, ucFirst, ulid, uuid, wrap
Json — decode, decodeOrThrow, encode, encodeOrThrow, htmlEncode, isValid, prettyPrint
Number — clamp, format, humanize, isInteger, ordinal, percentage, safeInt, MAX_SAFE_INTEGER
Bit — addFlag, checkFlag, decBinPad, flags, grant, hasAll, hasAny, hasFlagIn, removeFlag,
toggleFlag
B64 — decode, decodeOrThrow, decodeSafe, encode, encodeSafe
URLify — downcode, seemsUTF8
ArrayCollection — add, all, avg, chunk, clear, clone, collapse, concat, contains, containsKey,
count, countBy, current, diff, each, every, exists, filter, findFirst, first, flatten, flip, get,
getIterator, getKeys, getValues, groupBy, implode, indexOf, intersect, isEmpty, key, keyBy, last,
lazy, map, mapByKey, mapInto, max, median, merge, min, next, partition, pipe, pluck, prepend, push,
random, reduce, reject, remove, removeElement, reverse, set, skip, slice, some, sort, sortBy,
sortDesc, sortKeys, sum, take, tap, testForAll, toArray, toJson, transform, unique, unless, values,
when, whereInstanceOf, zip
HashCollection — add, all, clear, contains, count, each, filter, find, get, getIterator,
groupBy, hasKey, implode, isEmpty, keys, map, partition, reduce, remove, set, sortBy, toArray, values
Both collections also implement ArrayAccess and JsonSerializable; those methods are left out of
the lists above.
Storage — all, clear, count, countRecursive, except, exist, get, isEmpty, merge, only, remove,
set, toArray, plus property access, ArrayAccess and foreach
Func — attributeToGetterMethod, attributeToSetterMethod, classBasename, classNamespace,
classUsesRecursive, dataGet, doesTraitUse, eachValue, findGetterMethod, findSetterMethod,
getPropertyValue, instance, isTrue, mapValue, publicPropertyExists, remoteCall, remoteStaticCall,
remoteStaticCallOrThrow, traitUsesRecursive, value, when
Exceptions — ExceptionInterface, plus ConfigException, Exception, InvalidArgumentException, InvalidCallException, InvalidConfigException, InvalidParamException, InvalidValueException, MethodNotAllowedException, MissingClassException, MissingConfigException, MissingMethodException, MissingPropertyException, NotSupportedException, UnknownMethodException, UnknownPropertyException
Enums — casesToEscapeString, casesToString, fromName, hasName, hasValue, labels, names, random, toKeyValueArray, toValueKeyArray, tryFromName, values
ConditionalHandler — make, handleIf, resolve, __invoke
Types — Point, GeoPoint · Interfaces — Arrayable, Jsonable
Good to know
Install ext-intl if you slugify non-Latin text. Without it the bundled character maps cover
Latin, Cyrillic and Greek, but not the Œ/œ ligature or CJK.
Arr::fromPostgresArray cannot restore a null. The writer emits the NULL keyword correctly,
but the reader returns it as the string 'NULL', so a round trip is not value-preserving for null
elements.
Str caches its case conversions in a bounded, process-wide cache. Call Str::clearCache() in
a long-running worker if you convert unbounded user input.
Contributing
Bug reports and pull requests are welcome — see CONTRIBUTING.md.
composer test # PHPStan + PHPUnit composer test-cover # with coverage composer phpcs # coding standard composer cs-fix # fix what can be fixed automatically
License
MIT — see LICENSE.