theodorejb/polycast
theodorejb/polycast brings type-safe casting to PHP: a simple Cast class converts values to int, float, bool, string, array, or DateTime, throwing clear exceptions when a value can't be cast instead of silently coercing it.
composer require theodorejb/polycast
Polycast is a tiny, framework-agnostic utility: no service provider, no config file, no facade. It ships a small set of cast helpers (namespaced functions or static methods — check src/ and the README for the exact entry points) that convert mixed values to int, float, or string and throw when the conversion would be lossy or ambiguous, instead of silently coercing the way PHP's native casts do.
Where to look first, in order:
src/ — the actual casting rules. This is where the contract lives.tests/ — the real specification. Read the edge-case cases ("12abc", " 42 ", "1e3", 1.0, true, null) before you rely on it anywhere.First use case: sanitize untrusted input once, at the boundary, and let failures throw rather than becoming 0 or "".
use function TheodoreBrown\Polycast\to_int;
// Native: (int) "abc" === 0, silently. Polycast throws instead.
$page = to_int($request->query('page') ?? 1);
Cast at the edge, not in the model. The package's value is that failures are loud. Call it in controllers, request/DTO factories, or console command handlers — places where you can convert an exception into a 422 or a clear CLI error. Once a value is a typed int/float/string, keep it typed; don't re-cast deeper in the stack.
DTO / value-object constructors are the strongest fit:
final class Pagination
{
public function __construct(public readonly int $page, public readonly int $perPage)
{
// page/perPage already validated by the caller
}
public static function fromArray(array $input): self
{
return new self(
to_int($input['page'] ?? 1),
to_int($input['per_page'] ?? 25),
);
}
}
Database and CSV rows. PDO returns strings for INT/DECIMAL columns depending on driver and ATTR_STRINGIFY_FETCHES; CSV/JSON imports are all strings. Use Polycast at the mapping layer so the rest of the app sees real types.
Config and env parsing. env('FEATURE_LIMIT') / getenv() are strings or false. Wrap them in a small typed config object rather than sprinkling (int) around.
Map the exception to your validation layer. Catch the package's cast exception and rethrow as your domain/validation exception so HTTP and API layers stay framework-shaped:
try {
$amount = to_float($input['amount']);
} catch (CastException $e) {
throw ValidationException::withMessages(['amount' => 'Must be a number.']);
}
Compose, don't replace, validation. filter_var(..., FILTER_VALIDATE_INT) returns false and loses the distinction between "invalid" and "actually false". Polycast's throwing contract is easier to reason about; use it as the single conversion step and keep business rules (ranges, min/max) separate.
Static analysis. The strict return types make PHPStan/Psalm happy and eliminate a whole class of mixed-propagation warnings; annotate your DTO properties accordingly and let the analyzer enforce that casts happen before use.
Testing. Assert on behavior, not implementation: valid inputs return the expected native type, invalid inputs throw. The package's own tests are a good template.
(int) is which strings are accepted. Confirm in tests/ how it handles "12abc", "0x1A", " 42 ", "042", "1e3", "+5", and "". Behavior you assume instead of verify is the #1 source of surprises in a 50-star utility.(int) 5.9 === 5 truncates. Expect Polycast to reject non-integral floats; if your code legitimately needs rounding, do it explicitly (to_int(round($x))) so the intent is visible.PHP_INT_MAX become floats in PHP. Decide up front whether huge numeric strings should throw or be rejected by a range check, and don't rely on the cast to catch it for you.. decimals regardless of setlocale. Bad news: your users may send "1,50"; handle that normalization before calling Polycast, not inside a catch.int/float/string only. Booleans, dates, and enums are your responsibility — don't force a boolean through to_string() and expect "true".null handling. Decide whether null throws or is defaulted at the call site (to_int($x ?? 0)). Prefer the explicit default; it documents intent and keeps the exception for genuinely bad data.files autoload, not classmap. After adding the dependency run composer dump-autoload; in long-running workers (Octane, Swoole) restart the worker so the new files entry is picked up.string and back to int re-introduces exactly the ambiguity the package exists to remove.gettype() — " " vs "" vs null are easy to confuse in a stack trace.CastsInput helper (or a Cast value object) if you need project-wide defaults, custom messages, or metrics; the library is too small to warrant forking.How can I help you explore Laravel packages today?