Skip to content

Commit a1c905e

Browse files
committed
Add jsonSerialize support across all typed value classes
- Implemented `jsonSerialize` method in `BoolStandard`, `TrueStandard`, `FalseStandard`, `FloatNonNegative`, `FloatPositive`, `FloatStandard`, `IntegerPositive`, `IntegerNonNegative`, `IntegerStandard`, `IntegerWeekDay`, `StringStandard`, `StringNonEmpty`, `StringCountryCode`, `StringEmail`, `StringJson`, `StringUrl`, `StringUuidV4`, `StringUuidV7`, `StringDecimal`, `StringText`, `StringVarChar255`, and respective MariaDB String classes to return native values. - Updated unit tests to validate `jsonSerialize` behavior for all modified classes. - Updated PHPUnit configuration to exclude legacy `src/Usage` directories.
1 parent baa137f commit a1c905e

File tree

98 files changed

+1356
-213
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+1356
-213
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ composer require georgii-web/php-typed-values:^1.0
2727
#### 1. Use existing typed values with validation built in:
2828

2929
```php
30-
$id = PositiveInt::fromString('123');
30+
$id = IntegerPositive::fromString('123');
3131
```
3232

3333
instead of duplicating this logic across your application like:
@@ -42,15 +42,15 @@ if ($id <= 0) {
4242
#### 2. Create aliases:
4343

4444
```php
45-
readonly class Id extends PositiveInt {}
45+
readonly class Id extends IntegerPositive {}
4646

4747
Id::fromInt(123);
4848
```
4949

5050
#### 3. Create a composite value object:
5151

5252
```php
53-
final class Profile
53+
final readonly class Profile
5454
{
5555
public function __construct(
5656
public readonly IntegerPositive $id,
@@ -65,21 +65,21 @@ final class Profile
6565
): static {
6666
return new static(
6767
IntegerPositive::fromInt($id),
68-
StringNonEmpty::fromString($firstName),
68+
StringNonEmpty::fromString($firstName), // Early fail
6969
$height !== null ? FloatNonNegative::fromString((string) $height) : null,
7070
);
7171
}
7272

7373
public function getHeight(): FloatNonNegative|Undefined { // avoid using NULL, which could mean anything
74-
return $this->height ?? Undefined::create();
74+
return $this->height ?? Undefined::create(); // Late fail
7575
}
7676
}
7777

7878
// Usage
79-
Profile::fromScalars(id: 101, firstName: 'Alice', height: '172.5');
80-
Profile::fromScalars(id: 157, firstName: 'Tom', height: null);
79+
\PhpTypedValues\Usage\Composite::fromScalars(id: 101, firstName: 'Alice', height: '172.5');
80+
\PhpTypedValues\Usage\Composite::fromScalars(id: 157, firstName: 'Tom', height: null);
8181
// From array
82-
$profile = Profile::fromScalars(...[157, 'Tom', null]);
82+
$profile = \PhpTypedValues\Usage\Composite::fromScalars(...[157, 'Tom', null]);
8383
// Accessing values
8484
$profile->getHeight(); // "172.5" OR "Undefined" type class (will throw an exception on trying to get value)
8585
```

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@
5959
"./vendor/bin/pest"
6060
],
6161
"performance": [
62-
"php ./src/Usage/Performance.php 1000 && php ./src/Usage/Performance.php 10000 && php ./src/Usage/Performance.php 100000 && php ./src/Usage/Performance.php 1000000"
62+
"php ./src/Usage/Util/Performance.php 1000 && php ./src/Usage/Util/Performance.php 10000 && php ./src/Usage/Util/Performance.php 100000 && php ./src/Usage/Util/Performance.php 1000000"
6363
],
6464
"usage": [
65-
"php ./src/Usage/AllTypes.php"
65+
"php ./src/Usage/Composite/Composite.php && php ./src/Usage/Primitive/Boolean.php && php ./src/Usage/Primitive/DateTime.php && php ./src/Usage/Primitive/Float.php && php ./src/Usage/Primitive/Integer.php && php ./src/Usage/Primitive/String.php && php ./src/Usage/Primitive/Undefined.php"
6666
],
6767
"type": [
6868
"./vendor/bin/pest --type-coverage --min=100 --do-not-cache-result"

phpunit.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
<directory>src</directory>
2121
</include>
2222
<exclude>
23-
<directory>src/Usage</directory>
23+
<directory>src/Usage/Composite</directory>
24+
<directory>src/Usage/Primitive</directory>
25+
<directory>src/Usage/Util</directory>
2426
</exclude>
2527
</source>
2628
</phpunit>

src/Abstract/AbstractType.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\Abstract;
6+
7+
use JsonSerializable;
8+
use PhpTypedValues\Exception\TypeException;
9+
use Stringable;
10+
11+
use function is_scalar;
12+
13+
/**
14+
* Base class for immutable typed values.
15+
*
16+
* Responsibilities
17+
* - Define a common root for value objects in this library.
18+
* - Enforce immutability and a unified interface via {@see AbstractTypeInterface}.
19+
* - Mark all descendants as implementing {@see JsonSerializable} — concrete
20+
* classes SHOULD implement `jsonSerialize()` consistently with their
21+
* `toString()`/`value()` representation.
22+
*
23+
* Notes
24+
* - This class does not provide storage or behavior by itself; concrete
25+
* subclasses define validation, factories (e.g. `fromString()`), and accessors
26+
* (e.g. `value()` and formatting helpers).
27+
*
28+
* @internal
29+
*
30+
* @psalm-internal PhpTypedValues
31+
*
32+
* @psalm-immutable
33+
*/
34+
abstract readonly class AbstractType implements AbstractTypeInterface, JsonSerializable
35+
{
36+
/**
37+
* Safely attempts to convert a mixed value to a string.
38+
* Returns null if conversion is impossible (array, resource, non-stringable object).
39+
*
40+
* @throws TypeException
41+
*/
42+
public static function convertMixedToString(mixed $value): string
43+
{
44+
if (is_scalar($value) || $value === null) {
45+
return (string) $value;
46+
}
47+
48+
if ($value instanceof Stringable) {
49+
return (string) $value;
50+
}
51+
52+
throw new TypeException('Value cannot be cast to string');
53+
}
54+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\Abstract;
6+
7+
use PhpTypedValues\Exception\TypeException;
8+
use PhpTypedValues\Undefined\Alias\Undefined;
9+
10+
/**
11+
* Base contract for all immutable typed values in this library.
12+
*
13+
* Responsibilities
14+
* - Provide strict construction from a validated string via {@see fromString}.
15+
* - Provide a lossless string representation via {@see toString} and {@see __toString}.
16+
* - Concrete implementations may also provide tolerant factories like
17+
* `tryFromMixed(mixed): static|Undefined` that return {@see Undefined} on failure.
18+
*
19+
* Notes
20+
* - All implementations MUST be immutable.
21+
*
22+
* @psalm-immutable
23+
*/
24+
interface AbstractTypeInterface
25+
{
26+
/**
27+
* Create an instance from a validated string representation.
28+
*
29+
* Implementations should perform strict validation and may throw a
30+
* domain-specific subtype of {@see TypeException}
31+
* when the provided value is invalid.
32+
*
33+
* @throws TypeException
34+
*/
35+
public static function fromString(string $value): static;
36+
37+
// public static function tryFromMixed(mixed $value): static|Undefined;
38+
39+
/**
40+
* Returns a normalized string representation of the underlying value.
41+
*/
42+
public function toString(): string;
43+
44+
/**
45+
* Alias of {@see toString} for convenient casting.
46+
*/
47+
public function __toString(): string;
48+
}

src/Abstract/Bool/BoolType.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace PhpTypedValues\Abstract\Bool;
66

7-
use PhpTypedValues\Abstract\TypeInterface;
7+
use PhpTypedValues\Abstract\AbstractType;
88

99
/**
1010
* Base implementation for boolean typed values.
@@ -17,9 +17,13 @@
1717
* - $v = MyBoolean::fromBool(true);
1818
* - $v->toString(); // "true"
1919
*
20+
* @internal
21+
*
22+
* @psalm-internal PhpTypedValues
23+
*
2024
* @psalm-immutable
2125
*/
22-
abstract readonly class BoolType implements TypeInterface, BoolTypeInterface
26+
abstract readonly class BoolType extends AbstractType implements BoolTypeInterface
2327
{
2428
abstract protected function __construct(bool $value);
2529

src/Abstract/Bool/BoolTypeInterface.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,5 @@ public static function tryFromString(string $value): static|Undefined;
2727

2828
public static function tryFromInt(int $value): static|Undefined;
2929

30-
public static function fromString(string $value): static;
31-
3230
public static function fromBool(bool $value): static;
33-
34-
public function toString(): string;
35-
36-
public function __toString(): string;
3731
}

src/Abstract/DateTime/DateTimeType.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
use DateTimeImmutable;
1010
use DateTimeZone;
11-
use PhpTypedValues\Abstract\TypeInterface;
11+
use PhpTypedValues\Abstract\AbstractType;
1212
use PhpTypedValues\Exception\DateTimeTypeException;
1313
use PhpTypedValues\Exception\ReasonableRangeDateTimeTypeException;
1414

@@ -19,16 +19,20 @@
1919
* Base implementation for DateTime typed values.
2020
*
2121
* Provides strict parsing with detailed error aggregation, round-trip
22-
* validation against the format, timezone normalization and reasonable
22+
* validation against the format, timezone normalization, and reasonable
2323
* timestamp range checks.
2424
*
2525
* Example
2626
* - $v = MyDateTime::fromString('2025-01-02T03:04:05+00:00');
2727
* - $v->toString(); // '2025-01-02T03:04:05+00:00'
2828
*
29+
* @internal
30+
*
31+
* @psalm-internal PhpTypedValues
32+
*
2933
* @psalm-immutable
3034
*/
31-
abstract readonly class DateTimeType implements TypeInterface, DateTimeTypeInterface
35+
abstract readonly class DateTimeType extends AbstractType implements DateTimeTypeInterface
3236
{
3337
protected const FORMAT = '';
3438
protected const ZONE = 'UTC';

src/Abstract/DateTime/DateTimeTypeInterface.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,4 @@ public function value(): DateTimeImmutable;
2727
public static function fromDateTime(DateTimeImmutable $value): static;
2828

2929
public static function tryFromString(string $value): static|Undefined;
30-
31-
public function toString(): string;
32-
33-
public static function fromString(string $value): static;
34-
35-
public function __toString(): string;
3630
}

src/Abstract/Float/FloatType.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace PhpTypedValues\Abstract\Float;
66

7-
use PhpTypedValues\Abstract\TypeInterface;
7+
use PhpTypedValues\Abstract\AbstractType;
88
use PhpTypedValues\Exception\FloatTypeException;
99

1010
use function sprintf;
@@ -20,9 +20,13 @@
2020
* - $v->value(); // 3.14 (float)
2121
* - (string) $v; // "3.14"
2222
*
23+
* @internal
24+
*
25+
* @psalm-internal PhpTypedValues
26+
*
2327
* @psalm-immutable
2428
*/
25-
abstract readonly class FloatType implements TypeInterface, FloatTypeInterface
29+
abstract readonly class FloatType extends AbstractType implements FloatTypeInterface
2630
{
2731
/**
2832
* @throws FloatTypeException

0 commit comments

Comments
 (0)