PHPackages                             gladehq/php-coerce - 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. gladehq/php-coerce

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

gladehq/php-coerce
==================

Safe, predictable type coercion for PHP. If it can't convert meaningfully, return null — never guess.

v1.1.0(1mo ago)00MITPHPPHP ^8.1

Since Feb 26Pushed 1mo agoCompare

[ Source](https://github.com/gladehq/php-coerce)[ Packagist](https://packagist.org/packages/gladehq/php-coerce)[ RSS](/packages/gladehq-php-coerce/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (2)Dependencies (6)Versions (3)Used By (0)

php-coerce
==========

[](#php-coerce)

> Safe, predictable type coercion for PHP. If it cannot convert meaningfully, it returns `null`. Never guess.

[![PHP Version](https://camo.githubusercontent.com/cc9cdea9aa96b40a822425e981b0a030e3371202973c7d57b74e8e99834f81dc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e312d626c7565)](https://www.php.net/)[![Latest Version on Packagist](https://camo.githubusercontent.com/5aad7fad1193d942fdc87edbc0e49719c999e10cc33d8fa00533b4c59785ff99/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f676c61646568712f7068702d636f657263652e737667)](https://packagist.org/packages/gladehq/php-coerce)[![Total Downloads](https://camo.githubusercontent.com/a7019e114f2edb9022e2e56c20d87125ad3ef25ca330915ead855a887644010c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f676c61646568712f7068702d636f657263652e737667)](https://packagist.org/packages/gladehq/php-coerce)[![Tests](https://camo.githubusercontent.com/9e8fa8013388149dcfb272c36f3750ae8a46d96091a9b65e2ca27a4f7d12733f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74657374732d3238322532307061737365642d627269676874677265656e)](https://github.com/gladehq/php-coerce/actions)[![PHPStan Level](https://camo.githubusercontent.com/b72adb1f27170ecf486459c4b07e920bb3db2b464444bce8277e018270665646/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230392d627269676874677265656e)](https://phpstan.org/)[![License: MIT](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)[![PSR-12](https://camo.githubusercontent.com/a89fd09f9220e515095dbff11c3f24ad8bf6fe4183045d4529bd50f1633972ed/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652532307374796c652d5053522d2d31322d6f72616e6765)](https://www.php-fig.org/psr/psr-12/)

---

PHP's native type juggling is unpredictable. `(int)"abc"` returns `0`. `empty(0)` returns `true`. `BackedEnum::from()` throws on invalid values. Data arrives in many formats: HTML forms, JSON APIs, CSV files, `.env` files, and database rows. Each source has its own quirks.

**php-coerce** provides a single, consistent interface to handle all of it. The rule is simple: if a conversion is ambiguous, lossy, or meaningless, it returns `null`. Never guess.

```
use GladeHq\PhpCoerce\Coerce;

Coerce::toInteger('42');               // 42
Coerce::toInteger('abc');              // null (not 0)
Coerce::toInteger(42.9);              // null (no silent truncation)
Coerce::toBoolean('yes');             // true
Coerce::toFloat('1,234.56');          // 1234.56 (auto-detected US format)
Coerce::toDateTime('2024-01-15');     // DateTimeImmutable
Coerce::toEnum('active', Status::class); // Status::Active
Coerce::equals(0.1 + 0.2, 0.3);      // true (IEEE-754 safe comparison)
Coerce::toBcDecimal(3.14, 4);         // '3.1400' (financial-safe decimal string)
Coerce::toPercent('50%');             // 0.5
Coerce::isEmail('user@example.com'); // true
```

---

Table of Contents
-----------------

[](#table-of-contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
    - [Static API](#static-api)
    - [Fluent API](#fluent-api)
- [API Reference](#api-reference)
    - [Boolean Coercion](#boolean-coercion)
    - [Integer Coercion](#integer-coercion)
    - [Positive &amp; Unsigned Integer](#positive--unsigned-integer)
    - [Float Coercion](#float-coercion)
    - [Rounded Float](#rounded-float)
    - [Decimal String (BC Math safe)](#decimal-string-bc-math-safe)
    - [Percent](#percent)
    - [String Coercion](#string-coercion)
    - [Array Coercion](#array-coercion)
    - [Array Transform](#array-transform)
    - [DateTime Coercion](#datetime-coercion)
    - [Enum Coercion](#enum-coercion)
    - [Comparison](#comparison)
    - [Blank Detection](#blank-detection)
    - [Format Validation](#format-validation)
- [Configuration](#configuration)
    - [Number Format](#number-format)
    - [Date Format](#date-format)
    - [Boolean Values](#boolean-values)
    - [Float Epsilon](#float-epsilon)
    - [Bulk Configuration](#bulk-configuration)
    - [Reset](#reset)
- [Real-World Examples](#real-world-examples)
- [Architecture](#architecture)
- [Design Principles](#design-principles)
- [Edge Cases &amp; Gotchas](#edge-cases--gotchas)
- [Testing](#testing)
- [Contributing](#contributing)
- [Security](#security)
- [Changelog](#changelog)
- [License](#license)

---

Requirements
------------

[](#requirements)

RequirementVersionPHP`^8.1`Runtime dependencies**None**Framework-agnostic. Works with Laravel, Symfony, Slim, or plain PHP.

---

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

[](#installation)

```
composer require gladehq/php-coerce
```

No service providers, no configuration files, no bootstrapping required. Import the class and use it.

---

Quick Start
-----------

[](#quick-start)

### Static API

[](#static-api)

The simplest way to use the package. All methods are static, zero setup required.

```
use GladeHq\PhpCoerce\Coerce;

$age     = Coerce::toInteger($request->input('age'));      // ?int
$price   = Coerce::toFloat($row['price']);                 // ?float
$active  = Coerce::toBoolean($env['FEATURE_FLAG']);        // ?bool
$status  = Coerce::toEnum($data['status'], Status::class); // ?Status
```

### Fluent API

[](#fluent-api)

Chain from a single value with `Coerce::from()`. Use `*Or($default)` methods for guaranteed non-null returns.

```
use GladeHq\PhpCoerce\Coerce;

$age      = Coerce::from($request->input('age'))->toPositiveIntOr(1);
$name     = Coerce::from($input)->toStringOrEmpty();
$status   = Coerce::from($value)->toEnum(Status::class);
$tags     = Coerce::from($input)->toArrayOr([], ',');
$email    = Coerce::from($input)->toNullIfBlank();
$discount = Coerce::from($input)->toPercent();   // '20%' → 0.2
```

---

API Reference
-------------

[](#api-reference)

### Boolean Coercion

[](#boolean-coercion)

```
Coerce::toBoolean(mixed $value): ?bool
```

InputOutput`true` / `false``true` / `false``1` / `0``true` / `false``'true'`, `'1'`, `'yes'`, `'on'``true``'false'`, `'0'`, `'no'`, `'off'``false``2`, `'abc'`, `null`, `[]``null`- String matching is **case-insensitive**: `"TRUE"`, `"True"`, and `"true"` all resolve to `true`.
- Only integers `0` and `1` map to booleans. `2`, `-1`, and other integers return `null`.
- Truthy and falsy string sets are [configurable](#boolean-values).

```
Coerce::isTruthy(mixed $value): bool   // coerce($value) === true
Coerce::isFalsy(mixed $value): bool    // coerce($value) === false
```

**Fluent:**

```
Coerce::from($value)->toBoolean();         // ?bool
Coerce::from($value)->toBooleanOr(false);  // bool
Coerce::from($value)->isTruthy();          // bool
Coerce::from($value)->isFalsy();           // bool
```

---

### Integer Coercion

[](#integer-coercion)

```
Coerce::toInteger(mixed $value): ?int
```

InputOutput`42``42``'42'``42``' 42 '``42` (trimmed)`'-5'``-5``'+42'``42``'1,234,567'``1234567` (auto-detected US format)`'1.234.567'``1234567` (auto-detected EU format)`42.9``null`. No silent truncation.`'42.9'``null``true` / `false``null`. Booleans are not numbers.`'abc'``null``'1e5'``null`. Scientific notation is not integer domain.`'007'``null`. Leading zeros rejected.`'9999999999999999999'``null`. Overflow protection.**Fluent:**

```
Coerce::from($value)->toInteger();      // ?int
Coerce::from($value)->toIntegerOr(0);   // int
```

---

### Positive &amp; Unsigned Integer

[](#positive--unsigned-integer)

```
Coerce::toPositiveInt(mixed $value): ?int   // > 0
Coerce::toUnsignedInt(mixed $value): ?int   // >= 0
```

Thin guards built on top of `toInteger()`.

Input`toPositiveInt``toUnsignedInt``1``1``1``0``null``0``-1``null``null``'42'``42``42``'abc'``null``null`**Fluent:**

```
Coerce::from($value)->toPositiveInt();          // ?int
Coerce::from($value)->toPositiveIntOr(1);       // int
Coerce::from($value)->toUnsignedInt();          // ?int
Coerce::from($value)->toUnsignedIntOr(0);       // int
```

---

### Float Coercion

[](#float-coercion)

```
Coerce::toFloat(mixed $value): ?float
```

InputOutput`3.14``3.14``42``42.0` (int promoted to float)`'3.14'``3.14``'1,234.56'``1234.56` (auto-detected US)`'1.234,56'``1234.56` (auto-detected EU)`'1e5'``100000.0` (scientific notation)`'1.5E-3'``0.0015``INF``INF` (native float passthrough)`NAN``null`. Not a meaningful number.`'INF'` / `'NaN'``null`. String representations rejected.`true` / `false``null``'abc'``null`**Fluent:**

```
Coerce::from($value)->toFloat();         // ?float
Coerce::from($value)->toFloatOr(0.0);    // float
```

---

### Rounded Float

[](#rounded-float)

```
Coerce::toRoundedFloat(mixed $value, int $precision): ?float
```

Coerces to float then rounds to the requested number of decimal places. Returns `null` if the value cannot be coerced.

```
Coerce::toRoundedFloat(3.14159, 2);     // 3.14
Coerce::toRoundedFloat('3.14159', 3);   // 3.142
Coerce::toRoundedFloat('abc', 2);       // null
```

**Fluent:**

```
Coerce::from($value)->toRoundedFloat(2);            // ?float
Coerce::from($value)->toRoundedFloatOr(2, 0.0);     // float
```

---

### Decimal String (BC Math safe)

[](#decimal-string-bc-math-safe)

```
Coerce::toBcDecimal(mixed $value, int $scale = 10): ?string
```

Returns a fixed-point decimal string suitable for `bcmath` functions or financial display. The result has exactly `$scale` digits after the decimal point. Returns `null` for values that cannot be coerced, `INF`, or `NAN`.

```
Coerce::toBcDecimal(3.14);              // '3.1400000000'
Coerce::toBcDecimal(3.14, 2);          // '3.14'
Coerce::toBcDecimal('1,234.56', 4);    // '1234.5600'
Coerce::toBcDecimal(1e5, 2);           // '100000.00'
Coerce::toBcDecimal(INF);             // null
Coerce::toBcDecimal('abc');            // null
```

Throws `\InvalidArgumentException` if `$scale < 0`.

**Fluent:**

```
Coerce::from($value)->toBcDecimal();        // ?string (scale 10)
Coerce::from($value)->toBcDecimal(4);       // ?string (scale 4)
```

---

### Percent

[](#percent)

```
Coerce::toPercent(mixed $value): ?float
```

Normalises a percentage value to a decimal ratio (0–1 range). Two input conventions are supported:

- **Percent string** (`'50%'`): strips the `%` suffix and divides by 100
- **Numeric value**: if the value is within `[-1, 1]` it is returned as-is (already a ratio); otherwise it is divided by 100

```
Coerce::toPercent('50%');     // 0.5
Coerce::toPercent('100%');    // 1.0
Coerce::toPercent('-50%');    // -0.5
Coerce::toPercent(50);        // 0.5   (50 / 100)
Coerce::toPercent(0.5);       // 0.5   (already a ratio)
Coerce::toPercent(1.5);       // 0.015 (1.5 / 100)
Coerce::toPercent('abc%');    // null
```

> **Note:** An integer `1` is treated as within `[-1, 1]` and returned as `1.0` (100%). If you mean 1%, pass `'1%'`.

**Fluent:**

```
Coerce::from($value)->toPercent();            // ?float
Coerce::from($value)->toPercentOr(0.0);       // float
```

---

### String Coercion

[](#string-coercion)

```
Coerce::toString(mixed $value): ?string
Coerce::toStringOrEmpty(mixed $value): string
```

InputOutput`'hello'``'hello'``42``'42'``3.14``'3.14'``true` / `false``'true'` / `'false'`Stringable objectResult of `__toString()``null`, `[]`, non-Stringable object`null``toStringOrEmpty()` returns `''` instead of `null` for unconvertible values.

**Fluent:**

```
Coerce::from($value)->toString();           // ?string
Coerce::from($value)->toStringOr('N/A');    // string
Coerce::from($value)->toStringOrEmpty();    // string
```

---

### Array Coercion

[](#array-coercion)

```
Coerce::toArray(mixed $value, ?string $separator = null): ?array
```

Conversion is attempted in this priority order:

1. **Already an array**: returned as-is
2. **Traversable** (Iterator, Generator): converted via `iterator_to_array()`
3. **JSON string** (starts with `[` or `{`): decoded with `json_decode()`
4. **String with separator**: split by separator, each part trimmed
5. **Scalar string** (no JSON, no separator): wrapped in a single-element array
6. **int, float, bool**: wrapped in a single-element array
7. **null, non-traversable objects**: `null`

```
Coerce::toArray('["a","b"]');           // ['a', 'b']
Coerce::toArray('{"key":"val"}');       // ['key' => 'val']
Coerce::toArray('a, b, c', ',');        // ['a', 'b', 'c']
Coerce::toArray('one|two', '|');        // ['one', 'two']
Coerce::toArray('[]');                  // []
Coerce::toArray('{}');                  // []
Coerce::toArray(42);                    // [42]
Coerce::toArray(null);                  // null
```

JSON takes priority over separator splitting:

```
Coerce::toArray('["a","b"]', ',');      // ['a', 'b'] (JSON wins)
```

**Fluent:**

```
Coerce::from($value)->toArray(',');          // ?array
Coerce::from($value)->toArrayOr([], ',');    // array
```

---

### Array Transform

[](#array-transform)

```
/**
 * @param callable(mixed): mixed $fn
 * @return array|null
 */
Coerce::coerceEach(mixed $value, callable $fn): ?array
```

Coerces the value to an array (using the same logic as `toArray()`) then applies `$fn` to every element. Returns `null` if the value cannot be converted to an array. Keys are preserved.

```
Coerce::coerceEach('[1,2,3]', fn($v) => Coerce::toInteger($v));
// [1, 2, 3]

Coerce::coerceEach('a,b,c', fn($v) => strtoupper($v));
// null — no separator provided, 'a,b,c' wraps to ['a,b,c']
// Pass separator via toArray first, or use the fluent API:

Coerce::from('a,b,c')->toArray(',');
// then map manually, or:

Coerce::coerceEach(['a', 'b', 'c'], fn($v) => strtoupper($v));
// ['A', 'B', 'C']

Coerce::coerceEach(null, fn($v) => $v);
// null
```

**Fluent:**

```
Coerce::from($value)->coerceEach(fn($v) => Coerce::toInteger($v));  // ?array
```

---

### DateTime Coercion

[](#datetime-coercion)

```
Coerce::toDateTime(mixed $value): ?DateTimeImmutable
```

Always returns `DateTimeImmutable` or `null`. Never `DateTime`.

InputOutput`DateTimeImmutable` instanceReturned as-is`DateTime` instanceConverted to `DateTimeImmutable``1705276800` (int)Unix timestamp, returns `DateTimeImmutable``0` (int)Unix epoch (`1970-01-01`)`-1` (int)`1969-12-31 23:59:59``'2024-01-15'`Parsed by PHP`'2024-01-15T10:30:00+00:00'`ISO 8601 parsed`'0'` (string)`null`. Numeric strings rejected.`'1705276800'` (string)`null`. Pass as `int` for timestamps.`'not a date'``null``''`, `'   '``null``3.14`, `true`, `[]``null`> **Important:** Numeric strings are intentionally rejected in auto mode. To handle a timestamp stored as a string, cast it first:

```
Coerce::toDateTime(1705276800);              // DateTimeImmutable (2024-01-15)
Coerce::toDateTime('1705276800');            // null
Coerce::toDateTime((int) '1705276800');      // DateTimeImmutable (2024-01-15)
```

Custom date format enforces strict matching. Trailing garbage is rejected:

```
Coerce::setDateFormat('d/m/Y');
Coerce::toDateTime('15/01/2024');            // DateTimeImmutable
Coerce::toDateTime('15/01/2024 garbage');    // null (strict matching)
Coerce::toDateTime('2024-01-15');            // null (format mismatch)
```

**Auto mode accepts PHP's relative date strings.**

In auto mode, `toDateTime()` delegates to PHP's native `\DateTimeImmutable` constructor, which accepts the full range of formats that `strtotime()` understands. This includes relative expressions:

```
Coerce::toDateTime('next monday');    // DateTimeImmutable (next Monday at 00:00:00)
Coerce::toDateTime('last friday');    // DateTimeImmutable (last Friday at 00:00:00)
Coerce::toDateTime('tomorrow');       // DateTimeImmutable (tomorrow at 00:00:00)
Coerce::toDateTime('yesterday');      // DateTimeImmutable (yesterday at 00:00:00)
Coerce::toDateTime('+2 weeks');       // DateTimeImmutable (14 days from now)
Coerce::toDateTime('now');            // DateTimeImmutable (current date and time)
```

This is not a bug. It is PHP's documented behavior, and it is intentionally preserved so that `toDateTime()` in auto mode is as permissive as PHP itself.

However, if your input comes from untrusted sources (user form fields, API payloads, CSV rows) and you expect a concrete date, relative strings passing through silently may be surprising. For example, an API field that should contain a birth date would silently accept `'next monday'` and produce a future date with no error.

The fix is to set an explicit format. When a format is configured, the native fallback is bypassed entirely and only strings that match the format exactly are accepted:

```
Coerce::setDateFormat('Y-m-d');

Coerce::toDateTime('2024-01-15');     // DateTimeImmutable
Coerce::toDateTime('next monday');    // null (does not match Y-m-d)
Coerce::toDateTime('tomorrow');       // null (does not match Y-m-d)
Coerce::toDateTime('now');            // null (does not match Y-m-d)
```

Use `setDateFormat()` whenever you are processing external input that must represent a real, absolute date.

**Fluent:**

```
Coerce::from($value)->toDateTime();                            // ?DateTimeImmutable
Coerce::from($value)->toDateTimeOr(new DateTimeImmutable());   // DateTimeImmutable
```

---

### Enum Coercion

[](#enum-coercion)

```
Coerce::toEnum(mixed $value, string $enumClass): ?BackedEnum
```

Works with PHP 8.1+ backed enums (string-backed and int-backed). Returns `null` instead of throwing.

```
enum Status: string {
    case Active   = 'active';
    case Inactive = 'inactive';
}

enum Priority: int {
    case Low    = 1;
    case Medium = 2;
    case High   = 3;
}
```

InputEnum ClassOutput`'active'``Status::class``Status::Active``Status::Active``Status::class``Status::Active` (returned as-is)`'invalid'``Status::class``null``1``Priority::class``Priority::Low``'2'``Priority::class``Priority::Medium` (cross-type coercion)`99``Priority::class``null``null`any`null`any valueUnit enum class`null`. Unit enums not supported.Cross-type coercion: a string `"2"` will match an int-backed enum with value `2`. An int `1` will try string `"1"` against string-backed enums. Unit enums (enums without a backing type) safely return `null`. No crash.

**Fluent:**

```
Coerce::from($value)->toEnum(Status::class);                      // ?Status
Coerce::from($value)->toEnumOr(Status::class, Status::Active);    // Status
```

---

### Comparison

[](#comparison)

```
Coerce::equals(mixed $a, mixed $b): bool
Coerce::isOneOf(mixed $value, array $values): bool
```

Semantic equality that respects types. Comparison layers are applied in this priority order:

1. **Same type**: strict `===` for non-floats; epsilon comparison for floats
2. **One side is native `bool`**: coerce both to boolean and compare
3. **Both coercible to float**: epsilon comparison
4. **Both coercible to string**: compare as strings
5. **None match**: `false`

```
Coerce::equals(42, '42');            // true  (numeric layer)
Coerce::equals(3.14, '3.14');        // true  (numeric layer)
Coerce::equals(true, 'yes');         // true  (boolean layer, one side is bool)
Coerce::equals(true, 1);             // true  (boolean layer, one side is bool)
Coerce::equals('1', 'true');         // false (neither side is bool)
Coerce::equals('0', 'false');        // false (neither side is bool)
Coerce::equals(null, null);          // true  (same type)

// IEEE-754 safe — these all return true
Coerce::equals(0.1 + 0.2, 0.3);     // true
Coerce::equals(1/3 * 3, 1.0);       // true

Coerce::isOneOf(42, [1, 42, 100]);   // true
Coerce::isOneOf('42', [1, 42]);      // true  (cross-type)
Coerce::isOneOf(99, [1, 42]);        // false
```

> The boolean layer **only activates when at least one operand is a native `bool`**. Two strings like `"1"` and `"true"` are never compared as booleans. They fall through to string comparison.

> Float comparisons use a relative epsilon (`|a - b| ≤ ε × max(|a|, |b|, 1)`). The default epsilon is `PHP_FLOAT_EPSILON`. See [Float Epsilon](#float-epsilon) to customise it.

---

### Blank Detection

[](#blank-detection)

```
Coerce::isBlank(mixed $value): bool
Coerce::isPresent(mixed $value): bool
```

Input`isBlank``isPresent``null``true``false``''``true``false``'   '` (whitespace only)`true``false``[]``true``false``0``false``true``false``false``true``'hello'``false``true``[1, 2]``false``true`Unlike PHP's `empty()`, the values `0` and `false` are **not** blank. They are valid, meaningful values.

**Fluent:**

```
Coerce::from($value)->isBlank();         // bool
Coerce::from($value)->isPresent();       // bool
Coerce::from($value)->toNullIfBlank();   // mixed — returns null if blank, original value otherwise
```

`toNullIfBlank()` is useful for normalising optional form fields before persistence:

```
$bio = Coerce::from($request->input('bio'))->toNullIfBlank(); // null or string
```

---

### Format Validation

[](#format-validation)

```
Coerce::isEmail(mixed $value): bool
Coerce::isUrl(mixed $value): bool
```

Thin wrappers over PHP's `filter_var`. Non-string values always return `false`.

```
Coerce::isEmail('user@example.com');    // true
Coerce::isEmail('invalid');             // false
Coerce::isEmail(null);                  // false
Coerce::isEmail(42);                    // false

Coerce::isUrl('https://example.com');   // true
Coerce::isUrl('not-a-url');             // false
Coerce::isUrl(null);                    // false
```

**Fluent:**

```
Coerce::from($value)->isEmail();    // bool
Coerce::from($value)->isUrl();      // bool
```

---

Configuration
-------------

[](#configuration)

All configuration is optional. The package works out of the box with sensible defaults.

### Number Format

[](#number-format)

Controls how numeric strings with separators are interpreted.

```
// Auto (default): intelligently detects format
Coerce::toFloat('1,234.56');    // 1234.56 (detected US)
Coerce::toFloat('1.234,56');    // 1234.56 (detected EU)

// Force US format: comma = thousands separator, dot = decimal
Coerce::setNumberFormat('us');
Coerce::toFloat('1,234.56');    // 1234.56

// Force EU format: dot = thousands separator, comma = decimal
Coerce::setNumberFormat('eu');
Coerce::toFloat('1.234,56');    // 1234.56
```

**Auto-detection rules:**

Input patternInterpretationSingle dot or comma only (`1.5`, `1,5`)Decimal separatorMultiple dots (`1.234.567`)Thousands separatorMultiple commas (`1,234,567`)Thousands separatorBoth dot and comma, dot last (`1,234.56`)US formatBoth dot and comma, comma last (`1.234,56`)EU format### Date Format

[](#date-format)

```
// Auto (default): PHP native parsing
Coerce::toDateTime('2024-01-15');          // works
Coerce::toDateTime('Jan 15, 2024');        // works

// Custom format: strict matching, no trailing garbage
Coerce::setDateFormat('d/m/Y');
Coerce::toDateTime('15/01/2024');          // DateTimeImmutable
Coerce::toDateTime('2024-01-15');          // null (wrong format)
Coerce::toDateTime('15/01/2024 extra');    // null (trailing data rejected)
```

### Boolean Values

[](#boolean-values)

Customize which strings are considered truthy or falsy:

```
Coerce::configure([
    'truthy_values' => ['true', '1', 'yes', 'on', 'enabled', 'active'],
    'falsy_values'  => ['false', '0', 'no', 'off', 'disabled', 'inactive'],
]);

Coerce::toBoolean('enabled');     // true
Coerce::toBoolean('inactive');    // false
```

### Float Epsilon

[](#float-epsilon)

Controls the tolerance used by `equals()` when comparing float values.

```
// Default: PHP_FLOAT_EPSILON (~2.2e-16)
Coerce::equals(0.1 + 0.2, 0.3);     // true

// Widen epsilon for less-precise comparisons
Configuration::setFloatEpsilon(0.01);
Coerce::equals(1.0, 1.005);          // true
Coerce::equals(1.0, 1.02);           // false
```

The comparison formula is relative, not absolute: `|a - b| ≤ ε × max(|a|, |b|, 1)`. This prevents epsilon from becoming meaninglessly small for large numbers or inappropriately large for numbers near zero.

Throws `\InvalidArgumentException` if epsilon is ` 'eu',
    'date_format'   => 'd/m/Y',
    'truthy_values' => ['true', '1', 'yes', 'on'],
    'falsy_values'  => ['false', '0', 'no', 'off'],
    'float_epsilon' => 0.0001,
]);
```

OptionAccepted valuesDefault`number_format``'auto'`, `'us'`, `'eu'``'auto'``date_format`Any PHP date format string, or `'auto'``'auto'``truthy_values``list``['true', '1', 'yes', 'on']``falsy_values``list``['false', '0', 'no', 'off']``float_epsilon``float > 0``PHP_FLOAT_EPSILON`### Reset

[](#reset)

```
Coerce::resetConfiguration(); // Restores all defaults
```

---

Real-World Examples
-------------------

[](#real-world-examples)

### Form Input Processing

[](#form-input-processing)

```
$age       = Coerce::from($request->input('age'))->toIntegerOr(0);
$subscribe = Coerce::from($request->input('newsletter'))->toBooleanOr(false);
$tags      = Coerce::from($request->input('tags'))->toArrayOr([], ',');
$joinedAt  = Coerce::toDateTime($request->input('joined_at'));
```

### Environment Variables

[](#environment-variables)

```
$debug   = Coerce::toBoolean(env('APP_DEBUG'));               // ?bool
$port    = Coerce::from(env('PORT'))->toIntegerOr(8080);      // int
$timeout = Coerce::from(env('TIMEOUT'))->toFloatOr(30.0);     // float
$dsn     = Coerce::from(env('DATABASE_URL'))->toStringOr(''); // string
```

### CSV / Spreadsheet Data

[](#csv--spreadsheet-data)

```
Coerce::setNumberFormat('eu');

foreach ($rows as $row) {
    $price = Coerce::toFloat($row['price']);    // "1.234,56" -> 1234.56
    $qty   = Coerce::toInteger($row['qty']);    // "42" -> 42, "abc" -> null
    $date  = Coerce::toDateTime($row['date']);  // "15/01/2024" -> DateTimeImmutable
}
```

### API Response Normalization

[](#api-response-normalization)

```
$status    = Coerce::toEnum($response['status'], OrderStatus::class);
$createdAt = Coerce::toDateTime($response['created_at']);
$amount    = Coerce::toFloat($response['amount']);

if ($status === null) {
    throw new InvalidResponseException('Unknown order status: ' . $response['status']);
}
```

### Safe Defaults with Fluent API

[](#safe-defaults-with-fluent-api)

```
$config = [
    'retries' => Coerce::from($options['retries'] ?? null)->toIntegerOr(3),
    'timeout' => Coerce::from($options['timeout'] ?? null)->toFloatOr(30.0),
    'verbose' => Coerce::from($options['verbose'] ?? null)->toBooleanOr(false),
    'tags'    => Coerce::from($options['tags'] ?? null)->toArrayOr([]),
];
```

### Blank vs Empty Checks

[](#blank-vs-empty-checks)

```
// Unlike empty(), zero and false are NOT blank
Coerce::isBlank(0);        // false
Coerce::isBlank(false);    // false
Coerce::isBlank('');       // true
Coerce::isBlank('   ');    // true
Coerce::isBlank(null);     // true
Coerce::isBlank([]);       // true

if (Coerce::isPresent($input)) {
    // We have a real, meaningful value
}
```

### Cross-Type Enum Matching

[](#cross-type-enum-matching)

```
// String value matching int-backed enum
$priority = Coerce::toEnum('2', Priority::class); // Priority::Medium

// Enum instance passed through unchanged
$same = Coerce::toEnum(Status::Active, Status::class); // Status::Active

// Unknown value, returns null instead of throwing
$unknown = Coerce::toEnum('deleted', Status::class); // null
```

### Financial / Decimal Arithmetic

[](#financial--decimal-arithmetic)

```
// Safe decimal strings for bcmath — no floating-point drift
$price    = Coerce::toBcDecimal($row['price'], 2);    // '1234.56'
$tax_rate = Coerce::toPercent($row['tax_rate']);       // 0.2 from '20%' or 20

if ($price !== null && $tax_rate !== null) {
    $tax = bcmul($price, (string) $tax_rate, 2);       // '246.91'
}
```

### Validated User Input

[](#validated-user-input)

```
$email = Coerce::from($request->input('email'))->toNullIfBlank();

if ($email !== null && ! Coerce::isEmail($email)) {
    throw new ValidationException('Invalid email address.');
}

$website = Coerce::from($request->input('website'))->toNullIfBlank();

if ($website !== null && ! Coerce::isUrl($website)) {
    throw new ValidationException('Invalid URL.');
}
```

### Batch Array Coercion

[](#batch-array-coercion)

```
// JSON payload with mixed types — coerce all IDs to integers
$ids = Coerce::coerceEach($request->input('ids'), fn($v) => Coerce::toInteger($v));
// null if 'ids' is not array-like, or [1, 2, 3] after coercion

// Filter out nulls after coercion
$validIds = array_filter($ids ?? [], fn($v) => $v !== null);
```

### Positive / Unsigned Guards

[](#positive--unsigned-guards)

```
$page     = Coerce::from($request->input('page'))->toPositiveIntOr(1);   // min page 1
$offset   = Coerce::from($request->input('offset'))->toUnsignedIntOr(0); // no negatives
$perPage  = Coerce::from($request->input('per_page'))->toPositiveIntOr(25);
```

---

Architecture
------------

[](#architecture)

```
Coerce (static facade + configuration API)
  │
  ├── CoerceValue (fluent wrapper for Coerce::from())
  │
  ├── Configuration (number_format, date_format, truthy/falsy values, float_epsilon)
  │
  ├── Coercers/
  │   ├── BooleanCoercer      toBoolean, isTruthy, isFalsy
  │   ├── NumericCoercer      toInteger, toFloat, toPositiveInt, toUnsignedInt,
  │   │                       toPercent, toRoundedFloat, toBcDecimal (US / EU / auto)
  │   ├── StringCoercer       toString, toStringOrEmpty
  │   ├── ArrayCoercer        JSON, separator, Traversable
  │   ├── DateTimeCoercer     timestamps, string parsing, strict format
  │   ├── EnumCoercer         BackedEnum with cross-type coercion
  │   └── ComparisonCoercer   equals (IEEE-754 safe), isOneOf
  │
  └── Detectors/
      └── BlankDetector       isBlank, isPresent, toNullIfBlank, isEmail, isUrl

```

All coercers and detectors are `@internal`. The public API surface consists of `Coerce` and `CoerceValue` only. Internal classes may change between minor versions without a semver break.

---

Design Principles
-----------------

[](#design-principles)

PrincipleDescription**Null over guessing**If a conversion is ambiguous or lossy, return `null`. Never invent a value.**No exceptions**Coercion methods never throw; invalid input returns `null`**No silent truncation**`toInteger(42.9)` returns `null`, not `42`**Booleans are not numbers**`toInteger(true)` returns `null`, not `1`**Type safety**Generics on enum coercion, strict return types throughout**Zero dependencies**Only PHP 8.1+ standard library. No framework coupling.**PHPStan level 9**Maximum static analysis strictness enforced in CI---

Edge Cases &amp; Gotchas
------------------------

[](#edge-cases--gotchas)

ScenarioBehaviorRationale`toInteger(42.0)``null`Float is never silently truncated to int`toInteger(true)``null`Booleans must not silently become numbers`toBoolean(2)``null`Only `0` and `1` are boolean integers`toFloat(NAN)``null`NaN is not a meaningful number`toFloat(INF)``INF`Valid IEEE 754 float, passed through`toFloat("INF")``null`String `"INF"` is not a valid numeric string`toFloat("1e5")``100000.0`Scientific notation is valid float domain`toInteger("1e5")``null`Scientific notation is not integer domain`toInteger("007")``null`Leading zeros rejected (ambiguous octal)`toDateTime("0")``null`Numeric strings rejected. Use `toDateTime(0)`.`toDateTime(0)``1970-01-01`Integer zero is valid Unix epoch`toEnum('x', UnitEnum::class)``null`Unit enums gracefully return `null`, not crash`equals("1", "true")``false`Boolean layer requires a native `bool` operand`equals(true, "yes")``true`Native `bool` present. Boolean layer activates.`equals(0.1 + 0.2, 0.3)``true`IEEE-754 epsilon comparison, not `===``toArray("a,b,", ",")``["a","b",""]`Trailing separator produces empty string element`isBlank(0)``false`Unlike `empty()`, zero is a meaningful value`isBlank(false)``false`Unlike `empty()`, false is a meaningful value`toPositiveInt(0)``null`Zero is not positive`toUnsignedInt(-1)``null`Negative integers are not unsigned`toPercent(1)``1.0`Integer 1 is within `[-1, 1]`, returned as-is (100%) — use `'1%'` for 1%`toBcDecimal(INF)``null`Infinity has no decimal representation`isEmail(42)``false`Non-string values always return `false``isUrl(null)``false`Non-string values always return `false``toNullIfBlank('')``null`Blank values become null`toNullIfBlank(0)``0`Non-blank values returned unchanged---

Testing
-------

[](#testing)

```
# Run the full test suite
vendor/bin/phpunit

# Static analysis, PHPStan level 9
vendor/bin/phpstan analyse

# Code style check, PSR-12 (dry run)
vendor/bin/php-cs-fixer fix --dry-run --diff

# Code style fix, apply changes
vendor/bin/php-cs-fixer fix
```

The test suite covers **282 tests** and **410 assertions**, including:

- All coercion methods with valid, invalid, and boundary inputs
- Auto-detection logic for US/EU number formats
- DateTime parsing: timestamps, strings, custom formats, edge cases
- Enum coercion: backed enums, cross-type, unit enum safety
- IEEE-754 float comparison regression (`equals(0.1 + 0.2, 0.3) === true`)
- Comparison semantics: boolean layer activation rules, custom epsilon
- `toBcDecimal`, `toPercent`, `toRoundedFloat`, `toPositiveInt`, `toUnsignedInt`
- `isEmail`, `isUrl`, `toNullIfBlank`, `coerceEach`
- Configuration mutations and reset (including `float_epsilon`)
- Full fluent API coverage with default fallbacks

---

Contributing
------------

[](#contributing)

Contributions are welcome. Please follow these guidelines:

1. Fork the repository and create a feature branch from `main`
2. Write tests for all new behaviour. The test suite must pass.
3. Ensure PHPStan level 9 reports no errors: `vendor/bin/phpstan analyse`
4. Follow PSR-12 coding style: `vendor/bin/php-cs-fixer fix --dry-run --diff`
5. Open a pull request with a clear description of the change and its rationale

For significant changes or new coercion domains, open an issue first to discuss the approach.

---

Security
--------

[](#security)

If you discover a security vulnerability, please do **not** open a public GitHub issue. Report it privately via the repository's [Security Advisories](https://github.com/gladehq/php-coerce/security/advisories/new) tab on GitHub.

All security reports will be acknowledged within 48 hours.

---

Changelog
---------

[](#changelog)

All notable changes are documented in [CHANGELOG.md](CHANGELOG.md). This project follows [Semantic Versioning](https://semver.org/).

---

License
-------

[](#license)

The MIT License (MIT). See [LICENSE](LICENSE) for full details.

---

Made with care by [Dulitha Rajapaksha](https://github.com/dulithamahishka94)

###  Health Score

36

—

LowBetter than 82% of packages

Maintenance89

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity43

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 ~16 days

Total

2

Last Release

56d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/130d3ca03c3035d1d05d614d5555b3956c254b774bd550bbb80ed35bb99b4ad4?d=identicon)[dulithamahishka94](/maintainers/dulithamahishka94)

---

Top Contributors

[![dulithamahishka94](https://avatars.githubusercontent.com/u/64469682?v=4)](https://github.com/dulithamahishka94 "dulithamahishka94 (28 commits)")

---

Tags

composercomposer-packagedatetimeenumsnull-safepackagistphpphp8type-castingtype-coerciontype-safetyphptypesafeconversioncastingcoercion

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/gladehq-php-coerce/health.svg)

```
[![Health](https://phpackages.com/badges/gladehq-php-coerce/health.svg)](https://phpackages.com/packages/gladehq-php-coerce)
```

###  Alternatives

[phpoption/phpoption

Option Type for PHP

2.7k541.2M158](/packages/phpoption-phpoption)[pinkary-project/type-guard

Type Guard module is part of the Pinkary Project, and allows you to \*\*narrow down the type\*\* of a variable to a more specific type.

198102.4k14](/packages/pinkary-project-type-guard)[zakirullin/mess

Convenient array-related routine &amp; better type casting

21228.9k2](/packages/zakirullin-mess)[strictus/strictus

Strict Typing for local variables in PHP

1606.9k](/packages/strictus-strictus)[garoevans/php-enum

Convenient way to always have an Enum object available and utilise Spl Types if available.

19158.8k5](/packages/garoevans-php-enum)[webparking/laravel-type-safe-collection

This package provides type-safe extension of the laravel collection, forcing a single type of object.

378.2k](/packages/webparking-laravel-type-safe-collection)

PHPackages © 2026

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