Skip to content

Commit a992e39

Browse files
committed
Expand documentation with composite value objects and extendable typed values
- Added examples showcasing composite value objects with nullable fields for richer domain modeling. - Enhanced documentation for creating custom aliases and extending core typed values. - Improved consistency by refining usage instructions and naming conventions. - Updated validation logic examples to demonstrate stricter exception handling.
1 parent c6d0a45 commit a992e39

File tree

2 files changed

+129
-27
lines changed

2 files changed

+129
-27
lines changed

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ PHP 8.2 typed value objects for common PHP data types.
1414
- Use tiny immutable objects as building blocks for larger value objects.
1515
- Fit naturally into DDD (Domain-Driven Design) shared domain models.
1616
- Work well with CQRS by expressing clear intent in commands and queries.
17+
- Extendable with custom-typed values.
1718

1819
Install
1920
-------
@@ -27,7 +28,7 @@ composer require georgii-web/php-typed-values
2728
Usage
2829
-----
2930

30-
Create and use typed values with validation built in:
31+
1. Use existing typed values with validation built in:
3132

3233
```php
3334
$id = PositiveInt::fromString('123');
@@ -42,13 +43,52 @@ if ($id <= 0) {
4243
}
4344
```
4445

46+
2. Create aliases:
47+
48+
```php
49+
readonly class Id extends PositiveInt {}
50+
51+
Id::fromString('123');
52+
```
53+
54+
3. Create a composite value object from other typed values (nullable values example):
55+
56+
```php
57+
final class Profile
58+
{
59+
public function __construct(
60+
public readonly PositiveInt $id,
61+
public readonly NonEmptyStr $firstName,
62+
public readonly ?NonEmptyStr $lastName,
63+
) {}
64+
65+
public static function fromScalars(
66+
int $id,
67+
string $firstName,
68+
string $lastName,
69+
): self {
70+
return new self(
71+
PositiveInt::fromInt($id),
72+
NonEmptyStr::fromString($firstName),
73+
$lastName !== null ? NonEmptyStr::fromString($lastName) : null,
74+
);
75+
}
76+
}
77+
78+
// Usage
79+
Profile::fromScalars(id: 101, firstName: 'Alice', lastName: 'Smith');
80+
Profile::fromScalars(id: 157, firstName: 'Tom', lastName: null);
81+
```
82+
83+
4584
## Key Features
4685

4786
- **Static analysis** – Designed for tools like Psalm and PHPStan with precise type annotations.
4887
- **Strict types** – Uses `declare(strict_types=1);` and strict type hints throughout.
4988
- **Validation** – Validates input on construction so objects can’t be created in an invalid state.
5089
- **Immutable** – Value objects are read‑only and never change after creation.
5190
- **No external dependencies** – Pure PHP implementation without requiring third‑party packages.
91+
- **Extendable** – Extendable with custom-typed values and composite value objects.
5292

5393
More information
5494
-------

docs/USAGE.md

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Floats (PhpTypedValues\Float):
3535
DateTime (PhpTypedValues\DateTime):
3636

3737
- DateTimeAtom — immutable DateTime in RFC3339 (ATOM) format
38-
- DateTimeTimestamp — immutable DateTime represented as Unix timestamp (seconds since epoch, UTC)
38+
- DateTimeTimestamp — immutable DateTime represented as a Unix timestamp (seconds since epoch, UTC)
3939

4040
Static usage examples
4141
---------------------
@@ -111,7 +111,7 @@ DateTimeAtom::fromString('not-a-date'); // throws: String has no valid datetime
111111
Create your own type: alias of PositiveInt
112112
------------------------------------------
113113

114-
If you want a domain-specific alias (e.g., UserId) that behaves like PositiveInt, extend PositiveInt and override static factories so they return the subclass instance (because the base uses `new self(...)`).
114+
If you want a domain-specific alias (e.g., UserId) that behaves like PositiveInt, extend PositiveInt.
115115

116116
```php
117117
<?php
@@ -121,22 +121,7 @@ namespace App\Domain;
121121

122122
use PhpTypedValues\Integer\PositiveInt;
123123

124-
final class UserId extends PositiveInt
125-
{
126-
public static function fromInt(int $value): self
127-
{
128-
// PositiveInt's constructor validates (> 0)
129-
return new self($value);
130-
}
131-
132-
public static function fromString(string $value): self
133-
{
134-
// Reuse the core numeric-string assertion
135-
parent::assertNumericString($value);
136-
// PositiveInt's constructor (inherited) validates (> 0)
137-
return new self((int) $value);
138-
}
139-
}
124+
final class UserId extends PositiveInt {}
140125

141126
// Usage
142127
$userId = UserId::fromInt(42);
@@ -154,7 +139,7 @@ declare(strict_types=1);
154139
namespace App\Domain;
155140

156141
use PhpTypedValues\Code\Assert\Assert;
157-
use PhpTypedValues\Code\Exception\NumericTypeException;
142+
use PhpTypedValues\Code\Exception\FloatTypeException;
158143
use PhpTypedValues\Code\Integer\IntType;
159144

160145
final class EvenPositiveInt extends IntType
@@ -163,14 +148,18 @@ final class EvenPositiveInt extends IntType
163148
protected int $value;
164149

165150
/**
166-
* @throws NumericTypeException
151+
* @throws FloatTypeException
167152
*/
168153
public function __construct(int $value)
169154
{
170-
Assert::greaterThanEq($value, 1);
171-
Assert::true($value % 2 === 0, 'Value must be even');
155+
if ($value <= 0) {
156+
throw new IntegerTypeException(sprintf('Expected positive integer, got "%d"', $value));
157+
}
158+
159+
if ($value % 2 !== 0) {
160+
throw new IntegerTypeException(sprintf('Expected even integer, got "%d"', $value));
161+
}
172162

173-
/** @var positive-int $value */
174163
$this->value = $value;
175164
}
176165

@@ -180,16 +169,17 @@ final class EvenPositiveInt extends IntType
180169
return $this->value;
181170
}
182171

183-
/** @throws NumericTypeException */
172+
/** @throws IntegerTypeException */
184173
public static function fromInt(int $value): self
185174
{
186175
return new self($value);
187176
}
188177

189-
/** @throws NumericTypeException */
178+
/** @throws IntegerTypeException */
190179
public static function fromString(string $value): self
191180
{
192-
parent::assertNumericString($value);
181+
parent::assertIntegerString($value);
182+
193183
return new self((int) $value);
194184
}
195185
}
@@ -198,6 +188,78 @@ final class EvenPositiveInt extends IntType
198188
$v = EvenPositiveInt::fromInt(6);
199189
```
200190

191+
Composite value object (with nullable fields)
192+
--------------------------------------------
193+
194+
You can compose several primitive value objects into a richer domain object. The example below shows a simple Profile value object that uses multiple primitives and also supports nullable fields.
195+
196+
```php
197+
<?php
198+
declare(strict_types=1);
199+
200+
namespace App\Domain;
201+
202+
use PhpTypedValues\Integer\PositiveInt;
203+
use PhpTypedValues\String\NonEmptyStr;
204+
use PhpTypedValues\Float\NonNegativeFloat;
205+
use PhpTypedValues\DateTime\DateTimeAtom;
206+
207+
final class Profile
208+
{
209+
public function __construct(
210+
public readonly PositiveInt $id,
211+
public readonly NonEmptyStr $firstName,
212+
public readonly NonEmptyStr $lastName,
213+
public readonly ?NonEmptyStr $middleName, // nullable field
214+
public readonly ?DateTimeAtom $birthDate, // nullable field
215+
public readonly ?NonNegativeFloat $heightM // nullable field
216+
) {}
217+
218+
// Convenience named constructor that accepts raw scalars and builds primitives internally
219+
public static function fromScalars(
220+
int $id,
221+
string $firstName,
222+
string $lastName,
223+
?string $middleName,
224+
?string $birthDateAtom, // e.g. "2025-01-02T03:04:05+00:00"
225+
int|float|string|null $heightM
226+
): self {
227+
return new self(
228+
PositiveInt::fromInt($id),
229+
NonEmptyStr::fromString($firstName),
230+
NonEmptyStr::fromString($lastName),
231+
$middleName !== null ? NonEmptyStr::fromString($middleName) : null,
232+
$birthDateAtom !== null ? DateTimeAtom::fromString($birthDateAtom) : null,
233+
$heightM !== null ? NonNegativeFloat::fromString((string)$heightM) : null,
234+
);
235+
}
236+
}
237+
238+
// Usage
239+
$p1 = Profile::fromScalars(
240+
id: 101,
241+
firstName: 'Alice',
242+
lastName: 'Smith',
243+
middleName: null, // nullable
244+
birthDateAtom: '1990-05-10T00:00:00+00:00', // optional, can be null
245+
heightM: '1.70', // string, int or float supported
246+
);
247+
248+
$p2 = new Profile(
249+
id: PositiveInt::fromInt(202),
250+
firstName: NonEmptyStr::fromString('Bob'),
251+
lastName: NonEmptyStr::fromString('Johnson'),
252+
middleName: NonEmptyStr::fromString('A.'),
253+
birthDate: null,
254+
heightM: null,
255+
);
256+
257+
// Accessing wrapped values
258+
$first = $p1->firstName->toString(); // "Alice"
259+
$height = $p1->heightM?->toString(); // "1.70" or null
260+
$birthIso = $p1->birthDate?->toString(); // RFC3339 string or null
261+
```
262+
201263
Notes
202264
-----
203265

0 commit comments

Comments
 (0)