simp / php-toolkit
Fluent helpers for Strings, Numbers, Arrays/Iterables, Phone numbers, Emails and Countries, backed by real ISO country / dialing-code data.
Requires
- php: >=8.1
Requires (Dev)
- phpunit/phpunit: ^10.0
This package is not auto-updated.
Last update: 2026-07-22 11:09:54 UTC
README
Fluent, dependency-free PHP helpers for strings, numbers, arrays/iterables, phone numbers, emails and countries — with Phone and Country backed by real ISO-3166 + international dialing-code data (bundled in resources/countries.json, merged from your two uploaded datasets, 249 countries / 237 with dial codes).
$name = new Str("Chance Nyasulu"); $price = new Num(1234.5); $list = new Arr([3, 1, 2]); $phone = new Phone("0987678987", "MW"); $email = new Email("chance@example.com"); $country = new Country("MW");
⚠️ One naming note
You asked for new String("..."). PHP doesn't allow that — string (like int, float, bool, array, null, void, object, mixed, never) is a reserved word and PHP raises a fatal error if you try to declare a class called String or Array. So this library calls them Str and Arr instead, but everything else works exactly as you described:
$name = new Str("Chance Nyasulu"); $name = Str::create("Chance Nyasulu"); $name = Str::of("Chance Nyasulu"); // shorthand alias $list = new Arr([1, 2, 3]); $list = Arr::create([1, 2, 3]); $list = Arr::of(1, 2, 3); // variadic shorthand
Install
No Composer registry access needed — drop the folder in and require the autoloader:
require __DIR__ . '/php-toolkit/autoload.php';
Or, if you do have Composer/Packagist access, composer.json is already set up with PSR-4 (Toolkit\ → src/), so composer dump-autoload + require 'vendor/autoload.php' works too.
Requires PHP ≥ 8.1 with the mbstring extension (intl optional, used for better transliteration in Str::ascii() if present, with an iconv fallback otherwise).
Run the tests / demo
php tests/smoke_test.php # Str, Num, Country, Phone, Email - ~60 assertions php tests/arr_lazy_test.php # Arr, LazyCollection - ~60 assertions php examples/demo.php # runnable usage walkthrough
Str — string manipulation
Every method returns a new Str (immutable, chainable). Cast to string with (string), echo, or ->value().
Str::of(" chance nyasulu ")->trim()->title(); // "Chance Nyasulu" Str::of("Chance Nyasulu")->camel(); // "chanceNyasulu" Str::of("Chance Nyasulu")->snake(); // "chance_nyasulu" Str::of("Chance Nyasulu")->kebab(); // "chance-nyasulu" Str::of("Hello, World! 2026")->slug(); // "hello-world-2026" Str::of("The quick brown fox")->limit(9); // "The quick..." Str::of("[abc]")->between('[', ']'); // "abc" Str::of("0987678987")->mask('*', 3); // "098*******" Str::of("Chance Nyasulu")->contains('nyasulu', true); // true (case-insensitive) Str::random(12); // random 12-char hex string Str::uuid4(); // random UUID v4
Grouped by category:
| Category | Methods |
|---|---|
| Case | upper lower ucfirst lcfirst title pascal/studly camel snake kebab slug swapCase |
| Trim/pad | trim ltrim rtrim squish pad padLeft padRight |
| Search/replace | substr limit words reverse repeat replace replaceFirst replaceLast remove append prepend between before beforeLast after afterLast mask wrap unwrap |
| Checks | isEmpty isNotEmpty contains containsAll startsWith endsWith equals is (wildcard) isAscii isAlpha isAlphaNumeric isNumeric isJson isUuid isUrl isEmail |
| Convert | toInt toFloat toBool split chunk toArray jsonDecode stripTags escape unescape ascii |
| Info | length wordCount countOccurrences |
| Static | Str::random(len) Str::uuid4() |
Arr — array manipulation
A fluent, chainable wrapper around PHP arrays (think a dependency-free Laravel Collection). Implements ArrayAccess, Countable, IteratorAggregate and JsonSerializable, so an Arr works with foreach, count(), $arr[0], and json_encode() directly. Transformation methods are immutable — they return a new Arr rather than mutating in place.
$people = new Arr([ ['name' => 'Chance', 'age' => 30, 'city' => 'Lilongwe'], ['name' => 'Grace', 'age' => 24, 'city' => 'Blantyre'], ['name' => 'John', 'age' => 41, 'city' => 'Lilongwe'], ]); $people->sortByDesc('age')->pluck('name')->implode(', '); // "John, Chance, Grace" $people->groupBy('city')['Lilongwe']->pluck('name'); // Arr(["Chance", "John"]) $people->avg('age'); // 31.67 $people->filter(fn ($p) => $p['age'] > 25)->values();
Dot-notation for nested data:
$data = new Arr(['address' => ['city' => 'Lilongwe']]); $data->get('address.city'); // "Lilongwe" $data->set('address.zip', '10101'); // new Arr with the value added $data->has('address.city'); // true
Full method list, grouped:
| Category | Methods |
|---|---|
| Access | get set has forget (dot-notation aware) · first last only except pluck |
| Transform | map flatMap mapWithKeys filter reject reduce each keys values flip flatten collapse chunk split combine |
| Combine | merge mergeRecursive union diff diffKeys intersect intersectKeys unique |
| Order | sort sortBy sortByDesc sortKeys sortKeysDesc reverse shuffle random |
| Mutate-ish (return new Arr) | pad prepend push pop shift |
| Group | groupBy countBy partition |
| Aggregate | count sum avg min max isEmpty isNotEmpty contains containsKey search implode |
| Convert | toArray toJson lazy() (→ LazyCollection) |
| Static | Arr::create Arr::of Arr::wrap Arr::range Arr::materialize |
Arr also accepts a Traversable/generator in its constructor and eagerly materializes it — great for arrays and small-to-medium query results. For huge or infinite sequences, use LazyCollection instead.
LazyCollection — memory-safe iterables
A generator-backed pipeline for large, expensive, or literally infinite iterables (file lines, DB cursors, ranges). Nothing runs until you call a terminal method (each, first, reduce, count, toArray, sum) — and operations like take()/takeWhile() let you short-circuit a source that never ends on its own.
LazyCollection::range(1, PHP_INT_MAX) // infinite ->filter(fn ($n) => $n % 7 === 0) ->map(fn ($n) => $n * $n) ->take(5) ->toArray(); // [49, 196, 441, 784, 1225] - only 35 numbers ever touched
Reading a large file lazily, line by line:
$errors = LazyCollection::make(function () { $handle = fopen('huge.log', 'r'); while (($line = fgets($handle)) !== false) { yield rtrim($line); } fclose($handle); }) ->filter(fn ($line) => str_contains($line, 'ERROR')) ->take(10) ->toArray(); // stops reading the file after the 10th match
| Kind | Methods |
|---|---|
| Lazy (no work until consumed) | map filter reject take takeWhile skip chunk tap keys values |
| Terminal (consumes the pipeline) | each first reduce sum contains count toArray toArr() (→ Arr) |
| Static | LazyCollection::make($arrayOrGeneratorFn) LazyCollection::range($start, $end = null, $step = 1) LazyCollection::repeat($value, $times = null) |
Passing a fn(): iterable factory closure (rather than an already-created Generator) to make() is what lets a LazyCollection be safely iterated more than once — plain arrays work either way.
Num — number manipulation
Num::of(1234.5)->currency('MK'); // "MK 1,234.50" Num::of(1234.5)->format(2); // "1,234.50" Num::of(3)->ordinal(); // "3rd" Num::of(11)->ordinal(); // "11th" Num::of(1994)->roman(); // "MCMXCIV" Num::of(2026)->toWords(); // "two thousand twenty-six" Num::of(1048576)->humanFileSize(); // "1 MB" Num::of(2)->add(3)->multiply(4); // Num(20) — chainable math Num::of(150)->clamp(0, 100); // Num(100) Num::of(17)->isPrime(); // true Num::average([1, 2, 3, 4]); // 2.5 (static) Num::random(1, 100); // random int (static)
Full list: format currency percentage ordinal roman toWords humanFileSize add subtract multiply divide abs round ceil floor clamp percentageOf isEven isOdd isPositive isNegative isZero isBetween isPrime, and statics random sum average min max range.
Country — ISO 3166 + dialing data
Resolves by alpha-2, alpha-3, numeric ISO code, or name — whichever you pass in.
$mw = new Country("MW"); // or Country::create("Malawi") / Country::create("MWI") $mw->getName(); // "Malawi" $mw->getAlpha2(); // "MW" $mw->getAlpha3(); // "MWI" $mw->getDialCode(); // "265" $mw->getDialCodeFormatted(); // "+265" $mw->getTld(); // ".mw" $mw->getFlagUrl(); // flag image URL $mw->getTimezone(); // "+02:00" Country::all(); // array of all 249 Country objects Country::search('mala'); // array of Country objects matching "mala" Country::random(); // random Country Country::fromDialCode('+44'); // Country("GB") Country::tryFind('Narnia'); // null instead of throwing
Unknown identifiers throw Toolkit\Exceptions\UnknownCountryException; use Country::tryFind() if you'd rather get null.
Phone — phone numbers (powered by Country)
$phone = new Phone("0987678987", "MW"); // national number + ISO country $phone = Phone::create("+265987678987"); // or a full international number — country auto-detected $phone->toE164(); // "+265987678987" $phone->toInternational(); // "+265 987 678 987" $phone->toNational(); // "0987 678 987" $phone->getCountry(); // Country("MW") $phone->getDialCode(); // "265" $phone->getNationalNumber(); // "987678987" $phone->isValid(); // true (plausible digit count) $phone->equals($otherPhone); Phone::isValidNumber("0987678987", "MW"); // true, no exception Phone::tryParse("garbage"); // null instead of throwing
North American numbers format NANP-style automatically:
Phone::create("2025551234", "US")->toNational(); // "(202) 555-1234"
Honest limitation on isMobile(): the source dataset only has country-level dialing codes, not the detailed number-range tables carriers publish, so true mobile-vs-landline detection isn't reliably derivable from it. isMobile() returns null (unknown) unless you register prefixes yourself:
Phone::registerMobilePrefixes('MW', ['88', '89', '99', '31']); $phone->isMobile(); // true/false once a rule exists for that country, otherwise null
Invalid/unparseable numbers throw Toolkit\Exceptions\InvalidPhoneNumberException.
Email — validation & formatting
$email = new Email("Chance.Nyasulu+news@Gmail.com"); $email->isValid(); // true (RFC-ish, via filter_var) $email->getLocalPart(); // "Chance.Nyasulu+news" $email->getDomain(); // "Gmail.com" $email->getTld(); // "com" $email->isFree(); // true (Gmail, Yahoo, Outlook, iCloud, etc.) $email->isDisposable(); // false (checked against a small built-in list) $email->normalize(); // Email("chancenyasulu@gmail.com") — dots/+tag stripped for Gmail $email->mask(); // "Ch*****************@Gmail.com" $email->gravatarUrl(); // Gravatar image URL $email->equals($other); // compares normalized addresses by default $email->hasMxRecord(); // live DNS check (needs network at runtime)
Register more free/disposable domains at runtime:
Email::registerFreeProvider('mycompany-webmail.com'); Email::registerDisposableProvider('some-temp-mail-service.com');
Email::fromValidated($addr) throws Toolkit\Exceptions\InvalidEmailException if the address is malformed; the plain constructor never throws (call isValid() yourself).
Data & attribution
resources/countries.json was built by merging your two uploaded files:
- ISO 3166 country list (names, alpha-2/3, numeric codes, TLDs, flags)
- Wikipedia's "List of telephone country codes" (dial codes, area-code hints, UTC offsets)
236 of 272 Wikipedia entries matched a real ISO country (the rest are things like satellite services, disputed territories, or sub-national exchanges that have no ISO code, e.g. Kosovo, Taiwan, Transnistria — they're simply not in your countries.json source). 237 of 249 ISO countries ended up with a dial code attached.
Project layout
php-toolkit/
├── composer.json
├── autoload.php # zero-dependency PSR-4 autoloader (works without Composer)
├── src/
│ ├── Str.php
│ ├── Num.php
│ ├── Arr.php
│ ├── LazyCollection.php
│ ├── Phone.php
│ ├── Email.php
│ ├── Country.php
│ ├── Support/CountryRepository.php
│ └── Exceptions/{InvalidPhoneNumberException,InvalidEmailException,UnknownCountryException}.php
├── resources/countries.json
├── examples/demo.php
└── tests/
├── smoke_test.php # Str, Num, Country, Phone, Email
└── arr_lazy_test.php # Arr, LazyCollection