timefrontiers / php-core
Core utilities, enums, and helpers for TimeFrontiers packages
Requires
- php: ^8.5
- ext-curl: *
- ext-json: *
- ext-libxml: *
- ext-simplexml: *
- giggsey/libphonenumber-for-php: ^8.13
- timefrontiers/php-has-errors: ^1.0
- timefrontiers/php-validator: ^1.1.1
Requires (Dev)
- php-parallel-lint/php-parallel-lint: ^1.4
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.5
README
Core utilities, enums, and helpers for TimeFrontiers packages.
Installation
composer require timefrontiers/php-core
Requirements
- PHP 8.5+
- ext-curl, ext-json, ext-libxml, and ext-simplexml
timefrontiers/php-has-errors^1.0timefrontiers/php-validator^1.1.1giggsey/libphonenumber-for-php^8.13 (forPhoneutilities — installed automatically)
Package Contents
| Class | Description |
|---|---|
AccessRank |
Enum for user access levels (int-backed) |
AccessGroup |
Enum for user access groups (string-backed) |
HttpStatus |
Enum for HTTP status codes |
Http |
HTTP utilities (redirect, JSON responses, headers) |
Header |
HTTP header utilities (status, redirects, caching) |
Client |
HTTP client using cURL |
Request |
Request parameter handling with validation |
Url |
URL manipulation utilities |
Str |
String utilities |
Time |
Date/time utilities |
Phone |
Phone number parsing, formatting, country/continent lookup |
AccessRank Enum
User access levels for permission control and error visibility filtering.
use TimeFrontiers\AccessRank; // Get rank $rank = AccessRank::USER; echo $rank->value; // 1 echo $rank->label(); // "User" // Compare ranks if ($user_rank->atLeast(AccessRank::MODERATOR)) { // User is staff or higher } // Check capabilities $rank->isStaff(); // true for MODERATOR+ $rank->isTechnical(); // true for DEVELOPER+ $rank->isAdmin(); // true for ADMIN+ // Error visibility if ($rank->canSee($error_min_rank)) { // User can see this error } // Get all options (for dropdowns) $options = AccessRank::options(); // [0 => 'Guest', 1 => 'User', ...]
Rank Values
| Rank | Value | Description |
|---|---|---|
| GUEST | 0 | Public users |
| USER | 1 | Logged in users |
| ANALYST | 2 | Data analysts |
| ADVERTISER | 3 | Advertisers |
| MODERATOR | 4 | Staff (internal errors) |
| EDITOR | 5 | Content editors |
| ADMIN | 6 | Administrators |
| DEVELOPER | 7 | Developers (system errors) |
| SUPERADMIN | 8 | Super admins (debug errors) |
| OWNER | 14 | Full access |
AccessGroup Enum
String-backed version of AccessRank for database storage.
use TimeFrontiers\AccessGroup; use TimeFrontiers\AccessRank; // Get group $group = AccessGroup::USER; echo $group->value; // "USER" echo $group->label(); // "User" echo $group->rankValue(); // 1 // Convert between rank and group $rank = $group->toRank(); // AccessRank::USER $group = AccessGroup::fromRank($rank); // AccessGroup::USER // Check capabilities (same as AccessRank) $group->isStaff(); // true for MODERATOR+ $group->isTechnical(); // true for DEVELOPER+ $group->isAdmin(); // true for ADMIN+ // Compare groups if ($group->atLeast(AccessGroup::ADMIN)) { // User is admin or higher } // Get all options (for dropdowns) $options = AccessGroup::options(); // ['GUEST' => 'Guest', 'USER' => 'User', ...]
HttpStatus Enum
HTTP status codes with helper methods.
use TimeFrontiers\Http\HttpStatus; // Use status code $status = HttpStatus::NOT_FOUND; echo $status->value; // 404 echo $status->phrase(); // "Not Found" echo $status->line(); // "404 Not Found" // Send as header $status->send(); // Check status type $status->isSuccess(); // 2xx $status->isRedirect(); // 3xx $status->isClientError(); // 4xx $status->isServerError(); // 5xx $status->isError(); // 4xx or 5xx // Get from code $status = HttpStatus::fromCode(500);
Http Utilities
use TimeFrontiers\Http\Http; use TimeFrontiers\Http\HttpStatus; use TimeFrontiers\Http\OriginPolicy; use TimeFrontiers\Http\TrustedProxyConfig; // Redirect (exits) Http::redirect('/dashboard'); Http::redirect('/login', HttpStatus::SEE_OTHER); // JSON responses (exit) Http::json(['user' => $user]); Http::success($data, 'User created'); Http::error('Validation failed', HttpStatus::BAD_REQUEST, $errors); Http::jsonp($data, 'myCallback'); // Deprecated compatibility API // Build a response body without sending it (middleware / controller returns) $body = Http::buildResponse( success: true, message: 'User created', data: $user, meta: ['request_id' => $id] ); // Direct request info (forwarding headers are ignored) $ip = Http::clientIp(); $method = Http::method(); $url = Http::currentUrl(); // Host-owned proxy and origin trust $proxies = new TrustedProxyConfig(['10.0.0.0/8']); $origin = new OriginPolicy('https://app.example.com', ['tenant.example.com']); $ip = Http::clientIp($proxies); $url = Http::currentUrl($origin, $proxies); // Check request type Http::isMethod('POST'); Http::isAjax(); Http::isSecure(); Http::accepts('application/json'); // Headers $auth = Http::header('Authorization'); $headers = Http::headers(); Http::setHeader('X-Custom', 'value'); // CORS Http::cors('https://example.com', ['GET', 'POST']); // Disable caching Http::noCache();
Request Handling
use TimeFrontiers\Http\Request; use TimeFrontiers\Http\SessionCsrfAdapter; // Create from different sources $request = Request::fromPost(); $request = Request::fromGet(); $request = Request::fromJson(); $request = Request::fromArray($data); // Get parameters $email = $request->get('email'); $page = $request->get('page', 1); $request->has('email'); // true/false // Filter parameters $allowed = $request->only(['name', 'email', 'phone']); $filtered = $request->except(['password', 'token']); // Validate parameters. Validation happens before normalization/coercion. $columns = [ 'email' => ['Email', 'email'], 'name' => ['Name', 'text', 2, 100], 'age' => ['Age', 'int', 18, 120], ]; $required = ['email', 'name']; $result = $request->validateResult($columns, $required); if ($result->fails()) { $errors = $result->errors(); } $params = $result->validated(); // CSRF belongs to php-session. Core receives an explicit adapter. $csrf = new SessionCsrfAdapter($session); $request = Request::fromPost($csrf); $token = $csrf->issue('contact_form'); // In form handler: $token = $request->get('csrf_token'); if (!is_string($token) || !$request->verifyCSRF('contact_form', $token)) { $errors = $request->getErrors(); }
verifyCSRF() never infers login state and never reads globals. Verification
fails when no adapter is present. The generateCSRF() and csrfField() static
helpers remain deprecated compatibility adapters and also require an explicitly
supplied/configured manager.
Http Client
cURL-based HTTP client for making API requests.
use TimeFrontiers\Http\Client; // Create client $client = new Client('https://api.example.com'); // Or configure manually $client = Client::create() ->setBaseUrl('https://api.example.com') ->setHeaders(['Authorization' => 'Bearer token']) ->setConnectTimeout(5) ->setTimeout(30) ->setResponseLimits(64 * 1024, 2 * 1024 * 1024) ->allowCrossOriginHeaders(['X-Trace-Id']) // explicit nonstandard opt-in ->verifySsl(true); // GET request $response = $client->get('/users', ['page' => 1]); // POST with form data $response = $client->post('/users', ['name' => 'John', 'email' => 'john@example.com']); // POST with JSON body $response = $client->postJson('/users', ['name' => 'John']); // PUT/PATCH/DELETE $response = $client->putJson('/users/1', ['name' => 'Jane']); $response = $client->patchJson('/users/1', ['status' => 'active']); $response = $client->delete('/users/1'); // Handle response if ($response->isSuccess()) { $data = $response->json(); $name = $response->get('data.user.name'); } else { $error = $response->get('error'); $code = $response->statusCode(); } // Response methods $response->isOk(); // true if 200 $response->isSuccess(); // true if 2xx $response->isError(); // true if 4xx or 5xx $response->isJson(); // true if JSON content type $response->body(); // raw body string $response->json(); // decoded JSON; throws JsonException if malformed $response->header('Content-Type'); $response->throwIfError(); // throws on error
The default client accepts only HTTP(S), resolves every hop, rejects multicast and every IANA special-purpose/non-global address class, and pins approved DNS answers for the transport hop. Redirects are handled manually so every destination is revalidated. A single monotonic total deadline covers DNS and all redirect hops; synchronous platform DNS cannot be interrupted mid-call, but its elapsed time is charged and transport is skipped when it exhausts the budget.
Cross-origin redirects retain only a small standard content-negotiation
allowlist. Authorization, cookies, AWS session tokens, API keys, client
secrets, and caller-defined headers are removed by default. A caller-owned
nonstandard field requires explicit allowCrossOriginHeaders() opt-in.
Non-idempotent bodies are not replayed unless
allowNonIdempotentRedirects() is explicitly enabled.
Trusted server-side callers that deliberately need an internal destination can
inject UrlPolicy::trusted(). This is the low-level opt-out; the default policy
must be used for request-derived URLs. ClientResponse::toArray() is a safe
metadata projection. Use toDebugArray() only in trusted diagnostics because
it includes the body, headers, and transport context. ClientResponse::xml()
accepts UTF-8 only (with an optional UTF-8 BOM) and rejects DTD/entity-bearing,
malformed, unsupported-encoding, and oversized documents.
Http Header
HTTP header utilities for responses.
use TimeFrontiers\Http\Header; use TimeFrontiers\Http\HttpStatus; // Send status and exit Header::send(HttpStatus::OK); Header::send(404); // Redirects Header::redirect('/dashboard'); Header::redirect('/login', 'Please log in first'); Header::refresh('/dashboard', 5, 'Redirecting in 5 seconds...'); // Error pages (exit) Header::notFound(); Header::badRequest(); Header::unauthorized(); Header::forbidden(); Header::internalError(); // Error pages redirect to the relative /app/{code} path by default Header::notFound(redirect: true, message: 'Page not found'); // Configure a canonical origin if absolute error redirects are required Header::configureErrorOrigin(new OriginPolicy('https://app.example.com')); // Authentication Header::authDialog('Admin Area', 'Access denied'); // Caching Header::noCache(); Header::cache(3600); // 1 hour private cache Header::cache(86400, true); // 1 day public cache // Content headers Header::contentType('application/pdf'); Header::contentLength(1024); Header::download('report.pdf'); Header::language('en'); // Security headers Header::security(); // Common security headers Header::hsts(); // Strict-Transport-Security Header::csp([ 'default-src' => "'self'", 'script-src' => "'self' 'unsafe-inline'", ]); // Custom headers Header::set('X-Custom', 'value'); Header::setCustom('Request-Id', '123'); // X-TFr-Request-Id Header::poweredBy('TimeFrontiers'); // Get custom X-TFr headers from response $custom = Header::getCustom(); // All X-TFr headers $value = Header::getCustom('Request-Id');
Time (Date/Time Utilities)
Date and time formatting and manipulation.
use TimeFrontiers\Time; // Current time Time::now(); // "2024-01-15 14:30:00" (MySQL format) Time::today(); // "2024-01-15" Time::timestamp(); // Unix timestamp // Formatting Time::format('Y-m-d', '2024-01-15 14:30:00'); Time::day('2024-01-15'); // "15th" Time::month('2024-01-15'); // "January" Time::month('2024-01-15', true); // "Jan" Time::year('2024-01-15'); // "2024" Time::weekday('2024-01-15'); // "Monday" Time::week('2024-01-15'); // 3 (week of year) // Combined formats Time::monthDay('2024-01-15'); // "January 15th" Time::mdy('2024-01-15'); // "January 15th, 2024" Time::hms('2024-01-15 14:30:00'); // "14:30:00" Time::hm('2024-01-15 14:30:00'); // "14:30" Time::hm('...', true); // "2:30 PM" (12-hour) Time::datetime('2024-01-15 14:30:00'); // "January 15th, 2024 at 14:30:00" Time::weekDatetime('2024-01-15'); // "Monday, 15th January 2024" // Relative time Time::relative('2024-01-14'); // "1 day ago" Time::relative('2024-01-20'); // "in 5 days" // Calculations Time::add('P1D'); // Add 1 day Time::add('PT2H', '2024-01-15'); // Add 2 hours Time::sub('P1M'); // Subtract 1 month Time::diff('2024-01-01', '2024-01-15'); // DateInterval Time::diffSeconds('2024-01-01', '2024-01-15'); // Seconds // Comparisons Time::isPast('2024-01-01'); // true Time::isFuture('2025-01-01'); // true Time::isToday('2024-01-15'); // true/false Time::isSameDay('...', '...'); // true/false // Conversion Time::toTimestamp('2024-01-15'); // Unix timestamp Time::toDateTime('2024-01-15'); // DateTime object Time::toMysql(1705320600); // MySQL datetime Time::toIso('2024-01-15'); // ISO 8601 format
URL Utilities
use TimeFrontiers\Url; use TimeFrontiers\Http\UrlPolicy; // Add/update query parameters $url = Url::withParams('/search', ['q' => 'test', 'page' => 2]); // "/search?q=test&page=2" // Remove parameters $url = Url::withoutParams($url, ['page']); // "/search?q=test" // Get parameters $query = Url::getParam($url, 'q'); // "test" $params = Url::getParams($url); // ['q' => 'test'] // Check URL status Url::exists('https://example.com'); // true/false (200 only, safe network policy) Url::isAccessible('https://example.com'); // true/false (2xx/3xx) $code = Url::getStatusCode('https://example.com'); // Explicit internal-network opt-out for a trusted server-owned URL $code = Url::getStatusCode('http://service.internal/health', 10, UrlPolicy::trusted()); // Parse and build $parts = Url::parse('https://user:pass@example.com:8080/path?q=1#hash'); $url = Url::build($parts); // Validation Url::isValid('https://example.com'); // true Url::getDomain('https://example.com/path'); // "example.com" Url::normalize('example.com'); // "https://example.com"
String Utilities
use TimeFrontiers\Str; // Parse email with name $parsed = Str::parseEmailName('John Doe <john@example.com>'); // ['name' => 'John', 'surname' => 'Doe', 'email' => 'john@example.com'] // File extension Str::fileExtension('document.pdf'); // "pdf" Str::fileExtension('image.jpg?v=123'); // "jpg" // Base64 Str::isBase64('SGVsbG8gV29ybGQ='); // true Str::base64Decode('SGVsbG8gV29ybGQ='); // "Hello World" // Slugify Str::slug('Hello World!'); // "hello-world" Str::slug('Hello World!', '_'); // "hello_world" // Truncate Str::truncate('Long text here', 10); // "Long te..." Str::truncateWords('Long text here', 10); // "Long..." Str::limitWords('One two three four', 2); // "One two..." // Case conversion Str::toCamelCase('hello_world'); // "helloWorld" Str::toPascalCase('hello_world'); // "HelloWorld" Str::toSnakeCase('helloWorld'); // "hello_world" Str::toKebabCase('helloWorld'); // "hello-world" // Masking Str::mask('4111111111111111', 4, 4); // "4111********1111" // String checks Str::contains('Hello World', 'World'); // true Str::startsWith('Hello World', 'Hello'); // true Str::endsWith('Hello World', 'World'); // true // Word count Str::wordCount('Hello World'); // 2 // Excerpt (strips HTML, normalizes whitespace, truncates at word boundary) Str::excerpt('<p>Long <b>article</b> text goes here...</p>', 30); // "Long article text goes here..." // Excerpt window around a phrase Str::excerptAround('Long text with keyword here', 'keyword', 10); // "...with keyword her..." // Chunk a string (e.g., split a code into groups) Str::chunk('1234567890', 3, '-'); // "123-456-789-0" // Pattern replacement Str::patternReplace( ['{{name}}' => '{{name}}', '{{date}}' => '{{date}}'], ['{{name}}' => 'John', '{{date}}' => '2024-01-15'], 'Hello {{name}}, today is {{date}}' ); // "Hello John, today is 2024-01-15"
Need random strings? Use
TimeFrontiers\Data\Randomfromtimefrontiers/php-data— it's CSPRNG-backed withalphanumeric,hex,base64,uuid,numeric, and more.Str::randomno longer exists in php-core.
Phone Utilities
Static wrapper around giggsey/libphonenumber-for-php for parsing, validating,
formatting, and geolocating phone numbers. Every method returns null (or
false for is* checks) on failure — no exceptions thrown.
use TimeFrontiers\Phone;
Formatting
Phone::toE164('08031234567', 'NG'); // "+2348031234567" Phone::toIntl('+2348031234567'); // "+234 803 123 4567" Phone::toNational('+2348031234567'); // "0803 123 4567" Phone::toLocal('+2348031234567'); // alias of toNational() Phone::toRfc3966('+2348031234567'); // "tel:+234-803-123-4567" // Generic formatter Phone::format('08031234567', Phone::FORMAT_E164, 'NG');
Country & continent lookup
Phone::country('+2348031234567'); // "NG" Phone::countryName('+2348031234567'); // "Nigeria" Phone::dialCode('+2348031234567'); // 234 Phone::continent('+2348031234567'); // "Africa" // Skip the phone step if you already have the ISO code: Phone::continentFromCountry('JP'); // "Asia" Phone::continentFromCountry('BR'); // "South America"
Supported continents: Africa, Asia, Europe, North America,
South America, Oceania, Antarctica. The internal map covers all 249
ISO 3166-1 alpha-2 codes.
Line type & carrier
Phone::type('+2348031234567'); // "mobile" Phone::type('+442071838750'); // "landline" Phone::type('+18005551234'); // "toll_free" Phone::carrier('+2348031234567'); // "MTN" (mobile only) Phone::location('+2348031234567'); // "Nigeria" / "Lagos" (locale-dependent) Phone::timeZones('+2348031234567'); // ["Africa/Lagos"]
Possible type() values: mobile, landline, fixed_or_mobile, toll_free,
premium_rate, shared_cost, voip, personal_number, pager, uan,
voicemail, unknown.
Validation
Phone::isValid('+2348031234567'); // true Phone::isValid('08031234567', 'NG'); // true Phone::isValid('12345'); // false Phone::isPossible('+2348031234567'); // true — length-only check Phone::isRegion('+2348031234567', 'NG'); // true
Structured parse
One call returning everything at once — handy for logging or API responses:
Phone::parse('08031234567', 'NG'); // [ // 'valid' => true, // 'possible' => true, // 'country' => 'NG', // 'country_name' => 'Nigeria', // 'continent' => 'Africa', // 'dial_code' => 234, // 'national' => '0803 123 4567', // 'e164' => '+2348031234567', // 'intl' => '+234 803 123 4567', // 'rfc3966' => 'tel:+234-803-123-4567', // 'type' => 'mobile', // 'carrier' => 'MTN', // 'location' => 'Nigeria', // 'time_zones' => ['Africa/Lagos'], // ]
Normalization
Cheap, no libphonenumber needed — useful as a pre-store cleanup:
Phone::normalize(' +234 (803) 123-4567 '); // "+2348031234567" Phone::normalize('0803.123.4567'); // "08031234567"
Development gate
Because Core now declares its existing HasErrors use directly while released
php-has-errors 1.0.0 still requires Core, this package checkout must advertise
the candidate version when Composer solves development dependencies:
COMPOSER_ROOT_VERSION=1.1.0 composer update composer check composer validate --strict composer audit --locked composer dump-autoload --optimize --strict-psr
The CI workflow sets COMPOSER_ROOT_VERSION itself. A normal consuming project
does not need this environment variable; Composer resolves the two released
packages together.
License
MIT License