PHPackages                             joby/smol-cast - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. joby/smol-cast

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

joby/smol-cast
==============

Predictable type coercion from mixed inputs with explicit error handling.

v1.3.0(3mo ago)0286↓25%4MITPHPPHP &gt;=8.1CI passing

Since Jan 25Pushed 3mo agoCompare

[ Source](https://github.com/joby-lol/smol-cast)[ Packagist](https://packagist.org/packages/joby/smol-cast)[ RSS](/packages/joby-smol-cast/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (7)Dependencies (2)Versions (8)Used By (4)

smolCast
========

[](#smolcast)

Predictable type coercion from mixed input with explicit error handling.

Installation
------------

[](#installation)

```
composer require joby-lol/smol-cast
```

About
-----

[](#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
-----------

[](#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
---------------------

[](#type-conversion-rules)

### int()

[](#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()

[](#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()

[](#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()

[](#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
--------------

[](#usage-patterns)

### Query String Parsing

[](#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

[](#decoded-jsonapi-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

[](#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

[](#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
----------

[](#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
-------------------

[](#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

[](#basic-usage-1)

```
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

[](#available-methods)

The trait provides two variants for each type: **get*() methods*\* - Return typed value or null:

- `getInt(string $name, ?int $default = null): ?int`
- `getFloat(string $name, ?float $default = null): ?float`
- `getBool(string $name, ?bool $default = null): ?bool`
- `getString(string $name, ?string $default = null): ?string`**require*() methods*\* - Return typed value or throw:
- `requireInt(string $name): int`
- `requireFloat(string $name): float`
- `requireBool(string $name): bool`
- `requireString(string $name): string`

### get\*() vs require\*()

[](#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

[](#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

[](#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

[](#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

[](#usage-patterns-1)

#### Request/Form Handling

[](#requestform-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

[](#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

[](#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

[](#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

[](#implementation-requirements-1)

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 `null` for 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
------------

[](#requirements)

PHP 8.1+

License
-------

[](#license)

MIT License - See [LICENSE](LICENSE) file for details.

###  Health Score

40

—

FairBetter than 88% of packages

Maintenance78

Regular maintenance activity

Popularity14

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

7

Last Release

113d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/f4e606aa41ebfd7d82c365c6dc4ec158fb1b0bfff5f8dcbfad4ab53e3092e6a6?d=identicon)[jobyone](/maintainers/jobyone)

---

Top Contributors

[![joby-lol](https://avatars.githubusercontent.com/u/856610?v=4)](https://github.com/joby-lol "joby-lol (10 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/joby-smol-cast/health.svg)

```
[![Health](https://phpackages.com/badges/joby-smol-cast/health.svg)](https://phpackages.com/packages/joby-smol-cast)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
