timefrontiers/php-core

Core utilities, enums, and helpers for TimeFrontiers packages

Maintainers

Package info

github.com/timefrontiers/php-core

pkg:composer/timefrontiers/php-core

Transparency log

Statistics

Installs: 104

Dependents: 6

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-08-20 09:54 UTC

This package is auto-updated.

Last update: 2026-08-20 09:55:50 UTC


README

Core utilities, enums, and helpers for TimeFrontiers packages.

PHP Version License

Installation

composer require timefrontiers/php-core

Requirements

  • PHP 8.5+
  • ext-curl, ext-json, ext-libxml, and ext-simplexml
  • timefrontiers/php-has-errors ^1.0
  • timefrontiers/php-validator ^1.1.1
  • giggsey/libphonenumber-for-php ^8.13 (for Phone utilities — 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\Random from timefrontiers/php-data — it's CSPRNG-backed with alphanumeric, hex, base64, uuid, numeric, and more. Str::random no 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