joby / smol-cast
Predictable type coercion from mixed inputs with explicit error handling.
Installs: 53
Dependents: 4
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/joby/smol-cast
Requires
- php: >=8.1
Requires (Dev)
- php: >=8.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.1
README
Predictable type coercion from mixed input with explicit error handling.
Installation
composer require joby-lol/smol-cast
About
smolCast provides type-safe conversion methods for scalar values with permissive coercion rules and clear error handling. Designed for parsing user input, query strings, and decoded data where types may not match expectations.
Key features:
- Permissive coercion: Accepts multiple input types for each target type
- Explicit errors: Throws exceptions instead of silent failures
- Null-safe: Returns null for null input, throws for unconvertible values
- No dependencies: Pure PHP with no external requirements
Basic Usage
use Joby\Smol\Cast\Cast; // Returns null for null input Cast::int(null); // null Cast::float(null); // null Cast::bool(null); // null Cast::string(null); // null // Converts compatible types Cast::int("42"); // 42 Cast::float("3.14"); // 3.14 Cast::bool("yes"); // true Cast::string(123); // "123" // Throws TypeCastException for incompatible types Cast::int("not a number"); // throws Cast::bool("maybe"); // throws
Type Conversion Rules
int()
Converts to integer with the following rules:
- int: Returned as-is
- bool:
true→ 1,false→ 0 - float: Converted if whole number within int range (123.0 → 123, 123.5 throws)
- string/Stringable: Parsed if numeric (whitespace trimmed, "-42" and "3.0" work)
Cast::int(42); // 42 Cast::int(true); // 1 Cast::int(3.0); // 3 Cast::int(" -42 "); // -42 Cast::int(3.14); // throws (fractional part) Cast::int("abc"); // throws (not numeric)
float()
Converts to float with the following rules:
- float: Returned as-is
- int: Converted to float
- bool:
true→ 1.0,false→ 0.0 - string/Stringable: Parsed if numeric (whitespace trimmed)
Cast::float(3.14); // 3.14 Cast::float(42); // 42.0 Cast::float(true); // 1.0 Cast::float(" 2.5 "); // 2.5 Cast::float("abc"); // throws (not numeric)
bool()
Converts to boolean with the following rules:
- bool: Returned as-is
- int: 1 →
true, 0 →false(other values throw) - float: 1.0 →
true, 0.0 →false(other values throw) - string/Stringable: Parsed case-insensitively (whitespace trimmed):
- True: "1", "true", "yes", "on"
- False: "0", "false", "no", "off", "" (empty string)
Cast::bool(true); // true Cast::bool(1); // true Cast::bool("yes"); // true Cast::bool("YES"); // true (case-insensitive) Cast::bool(0); // false Cast::bool(""); // false (empty string) Cast::bool(" "); // false (whitespace-only) Cast::bool("maybe"); // throws (invalid string) Cast::bool(2); // throws (invalid int)
string()
Converts to string with the following rules:
- string: Returned as-is
- bool:
true→ "1",false→ "" (empty string) - int/float: Standard string conversion
- Stringable: Calls
__toString()
Cast::string("hello"); // "hello" Cast::string(true); // "1" Cast::string(false); // "" Cast::string(42); // "42" Cast::string(3.14); // "3.14" Cast::string([1,2,3]); // throws (not scalar)
Usage Patterns
Query String Parsing
// $_GET['page'] might be string "5" or missing $page = Cast::int($_GET['page'] ?? null) ?? 1; // $_GET['debug'] might be "1", "true", "yes", etc. $debug = Cast::bool($_GET['debug'] ?? null) ?? false;
Decoded JSON/API Data
$data = json_decode($response, true); // Data types might not match expectations $userId = Cast::int($data['user_id'] ?? null); $isActive = Cast::bool($data['active'] ?? null); $price = Cast::float($data['price'] ?? null);
Database Result Handling
// Some DB drivers return all values as strings $row = $pdo->fetch(PDO::FETCH_ASSOC); $id = Cast::int($row['id']); // "42" → 42 $amount = Cast::float($row['amount']); // "19.99" → 19.99 $enabled = Cast::bool($row['enabled']); // "1" → true
Form Input Validation
try { $age = Cast::int($_POST['age']); $email = Cast::string($_POST['email']); $subscribe = Cast::bool($_POST['subscribe'] ?? null) ?? false; // Process validated data } catch (TypeCastException $e) { // Handle invalid input }
Exceptions
All methods throw TypeCastException (extends Exception) when a value cannot be converted to the target type. Null input always returns null and never throws.
use Joby\Smol\Cast\TypeCastException; try { $value = Cast::int($_GET['count']); } catch (TypeCastException $e) { // Handle conversion error $value = 1; // fallback }
CastingGettersTrait
The CastingGettersTrait provides a convenient way to add type-safe getters to any class that stores keyed values. Perfect for configuration objects, request wrappers, or any class that needs to convert raw data to specific types. There is also a static version that works the same but with static methods: StaticCastingGettersTrait.
Basic Usage
use Joby\Smol\Cast\CastingGettersTrait; use Joby\Smol\Cast\TypeCastException; class UserInput { use CastingGettersTrait; public function __construct(private array $data) {} protected function getCastableValue(string $name): mixed { return $this->data[$name] ?? null; } protected function createRequiredException(string $type, string $name): \Throwable { return new TypeCastException("Required $type property '$name' is null"); } protected function createCastException(string $type, string $name, \Throwable $previous): \Throwable { return new TypeCastException("Cannot cast property '$name' to $type", 0, $previous); } } $input = new UserInput([ 'user_id' => '123', 'age' => '25', 'is_active' => 'yes', 'email' => 'user@example.com', ]); $userId = $input->getInt('user_id'); // 123 $age = $input->getInt('age'); // 25 $isActive = $input->getBool('is_active'); // true $email = $input->getString('email'); // "user@example.com"
Available Methods
The trait provides two variants for each type: get() methods* - Return typed value or null:
getInt(string $name, ?int $default = null): ?intgetFloat(string $name, ?float $default = null): ?floatgetBool(string $name, ?bool $default = null): ?boolgetString(string $name, ?string $default = null): ?stringrequire() methods* - Return typed value or throw:requireInt(string $name): intrequireFloat(string $name): floatrequireBool(string $name): boolrequireString(string $name): string
get*() vs require*()
The key difference is null handling:
// get*() returns null for missing/null values $optional = $input->getInt('optional_field'); // null // require*() throws exception for missing/null values try { $required = $input->requireInt('missing_field'); } catch (TypeCastException $e) { // "Required int property 'missing_field' is null" }
Both variants throw exceptions if the value exists but cannot be cast to the target type.
Default Values
All get*() methods accept an optional default value:
// Use default when value is null or missing $limit = $input->getInt('limit', 10); // 10 if 'limit' not set $timeout = $input->getFloat('timeout', 30.0); // 30.0 if 'timeout' not set $debug = $input->getBool('debug', false); // false if 'debug' not set $theme = $input->getString('theme', 'default'); // 'default' if 'theme' not set // Default is ignored when value exists (even if zero/false/empty) $count = $input->getInt('count', 99); // 0 if 'count' is 0, not 99 $flag = $input->getBool('flag', true); // false if 'flag' is false, not true
Important: Defaults are NOT cast - they must already be the correct type. If a value exists but cannot be cast, an exception is thrown even when a default is provided.
Implementation Requirements
Classes using the trait must implement three abstract methods:
abstract protected function getCastableValue(string $name): mixed; abstract protected function createRequiredException(string $type, string $name): \Throwable; abstract protected function createCastException(string $type, string $name, \Throwable $previous): \Throwable;
getCastableValue() - Returns the raw value for the given property name, or null if missing.
createRequiredException() - Creates the exception thrown by require*() methods when a value is null/missing. Receives the requested type name and property name.
createCastException() - Creates the exception thrown when type conversion fails. Receives the requested type name, property name, and the original TypeCastException as previous exception for context.
Custom Exceptions
Implement the exception factory methods to customize error handling:
class ApiResponse { use CastingGettersTrait; public function __construct(private array $data) {} protected function getCastableValue(string $name): mixed { return $this->data[$name] ?? null; } protected function createRequiredException(string $type, string $name): \Throwable { return new ApiException("Missing required $type field: $name"); } protected function createCastException(string $type, string $name, \Throwable $previous): \Throwable { return new ApiException("Invalid $type value for field '$name'", 0, $previous); } }
This allows each implementing class to:
- Use domain-specific exception types
- Customize error messages for better context
- Include additional debugging information
- Chain exceptions to preserve the original error details
Usage Patterns
Request/Form Handling
class Request { use CastingGettersTrait; public function __construct(private array $query, private array $post) {} protected function getCastableValue(string $key): mixed { return $this->post[$key] ?? $this->query[$key] ?? null; } } $request = new Request($_GET, $_POST); // Required fields throw on missing $userId = $request->requireInt('user_id'); // Optional fields with defaults $page = $request->getInt('page', 1); $perPage = $request->getInt('per_page', 20); $sortDesc = $request->getBool('sort_desc', false);
Configuration Object
class Config { use CastingGettersTrait; public function __construct(private array $config) {} protected function getCastableValue(string $key): mixed { // Support dot notation for nested config $keys = explode('.', $key); $value = $this->config; foreach ($keys as $key) { if (!isset($value[$key])) return null; $value = $value[$key]; } return $value; } } $config = new Config([ 'database' => [ 'host' => 'localhost', 'port' => '5432', ], 'cache' => [ 'enabled' => 'yes', 'ttl' => '3600', ], ]); $dbHost = $config->requireString('database.host'); $dbPort = $config->getInt('database.port', 5432); $cacheEnabled = $config->getBool('cache.enabled', false); $cacheTtl = $config->getInt('cache.ttl', 300);
API Response Wrapper
class ApiResponse { use CastingGettersTrait; public function __construct(private array $data) {} protected function getCastableValue(string $key): mixed { return $this->data[$key] ?? null; } public function isSuccess(): bool { return $this->requireBool('success'); } public function getErrorMessage(): ?string { return $this->getString('error'); } } $response = new ApiResponse(json_decode($json, true)); if ($response->isSuccess()) { $userId = $response->requireInt('user_id'); $username = $response->requireString('username'); } else { $error = $response->getErrorMessage() ?? 'Unknown error'; }
Database Row Object
class UserRow { use CastingGettersTrait; public function __construct(private array $row) {} protected function getCastableValue(string $key): mixed { return $this->row[$key] ?? null; } } // Many DB drivers return all values as strings $row = $pdo->fetch(PDO::FETCH_ASSOC); $user = new UserRow($row); $id = $user->requireInt('id'); // "123" → 123 $balance = $user->getFloat('balance'); // "19.99" → 19.99 $isActive = $user->getBool('is_active'); // "1" → true $email = $user->getString('email'); // string
Implementation Requirements
Classes using the trait must implement:
abstract protected function getCastableValue(string $key): mixed;
This method should:
- Return the raw value for the given property name
- Return
nullfor missing properties - Return the actual stored value for properties that exist (even if that value is
null)
The trait handles all type conversion and error handling.
Requirements
PHP 8.1+
License
MIT License - See LICENSE file for details.