Skip to content

Commit 3ce814f

Browse files
committed
feat: Add ObjectValidator and refactor for API consistency
This major feature release introduces the `ObjectValidator` and refactors the existing `SchemaValidator` to `AssociativeValidator` for a more consistent and symmetrical API. - **feat(ObjectValidator):** Add new `ObjectValidator` for validating `stdClass` objects, created via `Validator::isObject()`. - **feat(Coercion):** Implement cross-coercion. `ObjectValidator` can now coerce arrays to objects, and `AssociativeValidator` can coerce objects to arrays. - **refactor(SchemaValidator):** Rename `SchemaValidator` to `AssociativeValidator` to align with the factory method `isAssociative()` and improve API clarity. - **fix(Validator):** Loosen the type hint on the internal `FieldValidator::tryValidate()` method to allow both array and object payloads, fixing a TypeError. - **docs:** Update README, CHANGELOG, and project roadmap. Add Packagist badge and installation instructions. - **chore:** Add IDEAS.md to version control. - **chore:** Improve PHPDoc blocks to fix all static analysis errors.
1 parent c4479ff commit 3ce814f

12 files changed

Lines changed: 285 additions & 39 deletions

CHANGELOG.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,27 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [0.1.0] - 2025-09-05
5+
## [Unreleased]
6+
7+
## [0.2.0] - 2025-09-07
68

79
### Added
810

9-
- Initial release of the `lemmon/validator` package.
11+
- `ObjectValidator` for validating `stdClass` objects, created with `Validator::isObject()`.
12+
- Coercion support for `ObjectValidator` to convert associative arrays into `stdClass` objects.
13+
- Coercion support for `AssociativeValidator` to convert `stdClass` objects into associative arrays.
1014

11-
## [Unreleased]
15+
### Changed
16+
17+
- **BREAKING**: Renamed `SchemaValidator` to `AssociativeValidator` for better API consistency.
18+
- The factory method `Validator::isAssociative()` now returns an `AssociativeValidator` instance.
19+
- Improved error message for associative array type validation to be more specific.
20+
21+
### Fixed
22+
23+
- Changed the type hint on `FieldValidator::tryValidate()` to `mixed` to correctly accept both array and object payloads as context.
24+
25+
## [0.1.0] - 2025-09-05
1226

1327
### Added
1428

GEMINI.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
# Lemmon Validator Project Summary
22

3-
This project is a lightweight, fluent validation library for PHP, inspired by Valibot and Zod. It provides a structured way to validate various data types, including associative arrays, plain arrays, strings, integers, and booleans, with support for coercion, required fields, default values, and `oneOf` constraints.
3+
This project is a lightweight, fluent validation library for PHP, inspired by Valibot and Zod. It provides a structured way to validate various data types, including associative arrays, `stdClass` objects, plain arrays, strings, integers, and booleans. It features a symmetrical and predictable API for handling different data structures.
44

55
## Key Components:
66

7-
* **`Validator.php`**: Acts as a factory for creating different validator instances (e.g., `isAssociative()`, `isArray()`, `isString()`). The `isAssociative()` method can now be called without arguments to create an empty schema.
8-
* **`SchemaValidator.php`**: Handles validation for associative arrays, allowing definition of a schema with `FieldValidator` instances. **Crucially, it now extends `FieldValidator.php`**, inheriting its `validate()` and `tryValidate()` methods. Its core schema validation logic resides in its `validateType()` method.
9-
* **`FieldValidator.php`**: An abstract base class for individual field validators. It defines common validation methods like `required()`, `default()`, `coerce()`, and `oneOf()`. It implements a unified `validate()` (throws exception) and `tryValidate()` (returns status, data, errors) pattern. Error messages are now generic, with field context provided by the error object's structure.
10-
* **Specific Validators (e.g., `StringValidator.php`, `IntValidator.php`, `BoolValidator.php`, `ArrayValidator.php`)**: Implement the abstract methods from `FieldValidator.php` to provide type-specific validation and coercion logic. The `ArrayValidator.php` now coerces empty strings to empty arrays.
11-
* **`ValidationException.php`**: Custom exception class used to report validation errors.
7+
* **`Validator.php`**: Acts as a factory for creating different validator instances (e.g., `isAssociative()`, `isObject()`, `isArray()`).
8+
* **`AssociativeValidator.php`**: Handles validation for associative arrays. It can be configured with `->coerce()` to automatically convert a `stdClass` object into an associative array before validation.
9+
* **`ObjectValidator.php`**: Handles validation for `stdClass` objects. It can be configured with `->coerce()` to automatically convert an associative array into a `stdClass` object before validation.
10+
* **`FieldValidator.php`**: An abstract base class for all individual validators. It defines common validation methods like `required()`, `default()`, `coerce()`, and `oneOf()`. It implements a unified `validate()` (throws exception) and `tryValidate()` (returns result tuple) pattern.
11+
* **Specific Validators (e.g., `StringValidator.php`, `IntValidator.php`, `ArrayValidator.php`)**: Implement type-specific validation and coercion logic.
12+
* **`ValidationException.php`**: Custom exception class used to report validation errors, capable of holding a nested structure of error messages.
1213

1314
## Development Setup:
1415

1516
* **Dependencies**: Managed by Composer (`composer.json`).
16-
* **Testing**: Uses Pest PHP (`tests/ValidatorTest.php`).
17-
* **Code Style**: Enforced by PHP-CS-Fixer.
18-
* **Static Analysis**: Performed by PHPStan.
19-
* **Local Testing**: A `temp/` directory is used for ad-hoc CLI tests, with a `_bootstrap.php` file for common setup (Composer autoloader, Symfony ErrorHandler). The `temp/` directory is excluded from Git via `.gitignore`.
20-
* **Debugging**: `symfony/var-dumper` is included as a dev dependency for easy debugging.
21-
* **Error Handling**: `symfony/error-handler` is included as a dev dependency and registered in the bootstrap for improved error reporting.
17+
* **Testing**: Uses Pest PHP (`tests/ValidatorTest.php`). The test suite is run with `composer test`.
18+
* **Code Style**: Enforced by PHP-CS-Fixer. Can be checked with `composer lint` and fixed with `composer fix`.
19+
* **Static Analysis**: Performed by PHPStan. Can be run with `composer analyse`.
20+
* **Debugging**: `symfony/var-dumper` is included as a dev dependency.
21+
* **Error Handling**: `symfony/error-handler` is included as a dev dependency.

IDEAS.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Lemmon Validator - Future Ideas and Suggestions
2+
3+
This document captures ideas and suggestions for potential future enhancements to the Lemmon Validator library, beyond the current roadmap. These are not prioritized but serve as a backlog of possibilities for further development.
4+
5+
## 1. Data Transformation / Piping
6+
7+
* **Concept**: Introduce a more general `transform(callable $callback)` method (or a `pipe()` concept) that allows users to apply arbitrary transformations to the validated data *after* validation but *before* it's returned. This is distinct from `coerce()` which is typically about type conversion.
8+
* **Benefit**: Very powerful for data normalization, sanitization, or shaping. For example, you could validate a string, then transform it into a `DateTime` object, or encrypt it, all within the validation chain.
9+
* **Example**: `Validator::isString()->datetime()->transform(fn($value) => new DateTime($value))`
10+
11+
## 2. Enhanced Error Pathing for Nested Schemas
12+
13+
* **Concept**: For errors in nested schemas, enhance the error reporting to include the full "path" to the invalid field.
14+
* **Benefit**: When dealing with deeply nested data structures, knowing the exact path (e.g., `user.address.street` instead of just `street`) to an error is invaluable for debugging and user feedback.
15+
* **Example**: Instead of `['street' => ['Value is required.']]`, the error object could contain a `path` property (e.g., `['path' => 'user.address.street', 'message' => 'Value is required.']`) or the error key could be the full path.
16+
17+
## 3. Schema Manipulation Methods
18+
19+
* **Concept**: Introduce methods on `SchemaValidator` (or `FieldValidator` for `partial()`) that allow for easy manipulation and reuse of existing schemas.
20+
* **`partial()`**: Makes all fields in a schema (or a specific validator) optional. Useful for PATCH requests where only a subset of fields might be sent.
21+
* **`pick(array $keys)`**: Creates a new schema containing only the specified keys from an existing schema.
22+
* **`omit(array $keys)`**: Creates a new schema excluding the specified keys from an existing schema.
23+
* **`merge(SchemaValidator $otherSchema)`**: Combines two schemas into one.
24+
* **Benefit**: Promotes schema reusability and reduces boilerplate when you need slightly different versions of a base schema.
25+
26+
## 4. Error Codes
27+
28+
* **Concept**: Assign unique, programmatic error codes to common validation failures (e.g., `STRING_TOO_SHORT`, `INVALID_EMAIL_FORMAT`).
29+
* **Benefit**: Allows for easier programmatic handling of specific error types in the application layer, beyond just parsing the human-readable message.

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
# Lemmon Validator
22

33
[![CI](https://github.com/lemmon/validator-php/actions/workflows/ci.yml/badge.svg)](https://github.com/lemmon/validator-php/actions/workflows/ci.yml)
4+
[![Latest Stable Version](https://img.shields.io/packagist/v/lemmon/validator.svg)](https://packagist.org/packages/lemmon/validator)
45
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
56

67
A lightweight, fluent validation library for PHP, inspired by Valibot and Zod.
78

9+
## Installation
10+
11+
Install the package with Composer:
12+
13+
```bash
14+
composer require lemmon/validator
15+
```
16+
817
## Usage
918

1019
### Associative array validation
@@ -41,6 +50,28 @@ $emptyData = $emptySchema->validate([]); // Validates an empty array
4150
// $validNull will be true, $dataNull will be null, $errorsNull will be null
4251
```
4352

53+
### Object validation
54+
55+
Similar to associative arrays, you can validate `stdClass` objects.
56+
57+
```php
58+
use Lemmon\Validator;
59+
60+
// Define a schema for an object
61+
$schema = Validator::isObject([
62+
'name' => Validator::isString()->required(),
63+
'age' => Validator::isInt()->coerce(),
64+
]);
65+
66+
// Validate a stdClass object
67+
$object = (object)['name' => 'John Doe', 'age' => '42'];
68+
$validatedObject = $schema->validate($object); // ✓ $validatedObject->age is now (int) 42
69+
70+
// Coerce an associative array into a stdClass object
71+
$array = ['name' => 'Jane Doe', 'age' => 30];
72+
$coercedObject = $schema->coerce()->validate($array); // ✓
73+
```
74+
4475
### Array validation
4576

4677
```php

TODO.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
This roadmap outlines the planned features for future releases, prioritizing commonly needed functionalities for application development.
44

55
**Released:**
6+
- `0.2.0` - Added `ObjectValidator`, refactored `SchemaValidator` to `AssociativeValidator`, and enabled cross-coercion between them.
67
- `0.1.0` - Initial release.
78

8-
## Release 0.2: Common Formats & Utility Constraints
9+
## Common Formats & Utility Constraints
910

1011
This release focuses on introducing highly practical format validators for strings and other frequently used utility constraints.
1112

@@ -26,11 +27,11 @@ This release focuses on introducing highly practical format validators for strin
2627
* Implement `enum(array $allowedValues)`: Validates that a value is one of a predefined set of values (for all `FieldValidator` types).
2728
* Implement `const(mixed $constantValue)`: Validates that a value is exactly equal to a specific constant.
2829

29-
## Release 0.3: Advanced Structure & Relationships
30+
## Advanced Structure & Relationships
3031

3132
This release introduces more sophisticated validation rules for complex data structures and their interdependencies.
3233

33-
* **SchemaValidator (Object Properties)**:
34+
* **AssociativeValidator & ObjectValidator**:
3435
* Implement `additionalProperties(FieldValidator|bool $validatorOrBoolean)`: Controls whether properties not explicitly defined in the schema are allowed, and optionally validates them against a subschema.
3536
* Implement `patternProperties(array $regexToValidatorMap)`: Validates properties whose names match a given regular expression against a subschema.
3637
* Implement `propertyNames(StringValidator $validator)`: Validates the names of all properties in an object against a string schema.
@@ -39,7 +40,7 @@ This release introduces more sophisticated validation rules for complex data str
3940
* Implement `contains(FieldValidator $validator)`: Validates that an array contains at least one item that matches a given subschema.
4041
* Implement `additionalItems(FieldValidator|bool $validatorOrBoolean)`: Controls whether items beyond those defined in `items` (when `items` is an array of schemas) are allowed, and optionally validates them.
4142

42-
## Release 0.4: Logical Combinators
43+
## Logical Combinators
4344

4445
This release introduces the powerful logical combinators for expressing complex validation rules.
4546

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"description": "A lightweight, fluent validation library for PHP.",
44
"type": "library",
55
"license": "MIT",
6+
"homepage": "https://github.com/lemmon/validator-php",
7+
"keywords": ["validation", "validator", "schema", "zod", "valibot"],
68
"authors": [
79
{
810
"name": "Jakub Pelák",
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Lemmon;
44

5-
class SchemaValidator extends FieldValidator
5+
class AssociativeValidator extends FieldValidator
66
{
77
private bool $coerceAll = false;
88

@@ -25,11 +25,12 @@ public function coerceAll(): self
2525
*/
2626
protected function coerceValue(mixed $value): mixed
2727
{
28-
// If it's already an array, return as-is
29-
if (is_array($value)) {
30-
return $value;
28+
if (is_object($value)) {
29+
return (array) $value;
3130
}
32-
// For other types, return as-is; validateType will handle the error
31+
32+
// For other types (including arrays), return as-is.
33+
// The subsequent validateType method will handle non-array errors.
3334
return $value;
3435
}
3536

@@ -39,7 +40,7 @@ protected function coerceValue(mixed $value): mixed
3940
protected function validateType(mixed $value, string $key): mixed
4041
{
4142
if (!is_array($value)) {
42-
throw new ValidationException(['Value must be an array.']);
43+
throw new ValidationException(['Input must be an associative array.']);
4344
}
4445

4546
$data = [];
@@ -63,4 +64,4 @@ protected function validateType(mixed $value, string $key): mixed
6364

6465
return $data;
6566
}
66-
}
67+
}

src/Lemmon/FieldValidator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function validate(mixed $value, string $key = '', array $input = []): mix
8585
{
8686
[$valid, $data, $errors] = $this->tryValidate($value, $key, $input);
8787
if (!$valid) {
88-
throw new ValidationException($errors);
88+
throw new ValidationException($errors ?? ['Validation failed.']);
8989
}
9090
return $data;
9191
}
@@ -95,13 +95,13 @@ public function validate(mixed $value, string $key = '', array $input = []): mix
9595
*
9696
* @param mixed $value The value to validate.
9797
* @param string $key The key of the field being validated.
98-
* @param array<string, mixed> $input The entire input array.
98+
* @param mixed|null $input The entire input payload (array or object).
9999
* @return array{bool, mixed, array<string>|null} A tuple containing:
100100
* - bool: true if validation is successful, false otherwise.
101101
* - mixed: The validated and potentially coerced value, or the original value on failure.
102102
* - array|null: An array of error messages on failure, or null on success.
103103
*/
104-
public function tryValidate(mixed $value, string $key = '', array $input = []): array
104+
public function tryValidate(mixed $value, string $key = '', mixed $input = null): array
105105
{
106106
if ($this->nullifyEmpty && (($value === '') || (is_array($value) && empty($value)))) {
107107
$value = null;
@@ -142,4 +142,4 @@ abstract protected function coerceValue(mixed $value): mixed;
142142
* @throws ValidationException If the type validation fails.
143143
*/
144144
abstract protected function validateType(mixed $value, string $key): mixed;
145-
}
145+
}

src/Lemmon/ObjectValidator.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Lemmon;
4+
5+
class ObjectValidator extends FieldValidator
6+
{
7+
private bool $coerceAll = false;
8+
9+
/**
10+
* @param array<string, FieldValidator> $schema
11+
*/
12+
public function __construct(
13+
private array $schema
14+
) {
15+
}
16+
17+
public function coerceAll(): self
18+
{
19+
$this->coerceAll = true;
20+
return $this;
21+
}
22+
23+
/**
24+
* @inheritDoc
25+
*/
26+
protected function coerceValue(mixed $value): mixed
27+
{
28+
if (is_array($value)) {
29+
return (object) $value;
30+
}
31+
32+
// For other types (including objects), return as-is.
33+
// The subsequent validateType method will handle non-object errors.
34+
return $value;
35+
}
36+
37+
/**
38+
* @inheritDoc
39+
*/
40+
protected function validateType(mixed $value, string $key): mixed
41+
{
42+
if (!is_object($value)) {
43+
throw new ValidationException(['Input must be an object.']);
44+
}
45+
46+
$data = new \stdClass();
47+
$errors = [];
48+
49+
foreach ($this->schema as $fieldKey => $validator) {
50+
if ($this->coerceAll) {
51+
$validator->coerce();
52+
}
53+
54+
$fieldValue = $value->{$fieldKey} ?? null;
55+
56+
[$valid, $validatedFieldValue, $fieldErrors] = $validator->tryValidate($fieldValue, $fieldKey, $value);
57+
58+
if (!$valid) {
59+
$errors[$fieldKey] = $fieldErrors;
60+
} elseif (isset($validatedFieldValue)) {
61+
$data->{$fieldKey} = $validatedFieldValue;
62+
}
63+
}
64+
65+
if (!empty($errors)) {
66+
throw new ValidationException($errors);
67+
}
68+
69+
return $data;
70+
}
71+
}

src/Lemmon/ValidationException.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
class ValidationException extends \Exception
66
{
77
/**
8-
* @param array<string> $errors
8+
* @param array<array-key, mixed> $errors
99
*/
1010
public function __construct(
1111
private array $errors
@@ -15,7 +15,7 @@ public function __construct(
1515
}
1616

1717
/**
18-
* @return array<string>
18+
* @return array<array-key, mixed>
1919
*/
2020
public function getErrors(): array
2121
{

0 commit comments

Comments
 (0)