simp/php-toolkit

Fluent helpers for Strings, Numbers, Arrays/Iterables, Phone numbers, Emails and Countries, backed by real ISO country / dialing-code data.

Maintainers

Package info

github.com/CHANCENY/php-toolkit

pkg:composer/simp/php-toolkit

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-21 12:54 UTC

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 thatstring (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