This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Nette Schema is a validation and normalization library for data structures. It provides a fluent API for defining schemas and validating configuration files, API inputs, and other structured data.
- Package: nette/schema
- PHP Support: 8.1 - 8.5
- Dependencies: nette/utils ^4.0
- Documentation: https://doc.nette.org/schema
# Run all tests
composer run tester
# Run specific test file
vendor/bin/tester tests/Schema/Expect.structure.phpt -s
# Run tests in specific directory
vendor/bin/tester tests/Schema/ -sThe -s flag shows test output (useful for debugging).
# Run PHPStan (level 8)
composer run phpstan- All PHP files must include
declare(strict_types=1) - PHPStan level 8 static analysis is enforced
- Follow Nette Coding Standard (based on PSR-12)
- Use tabs for indentation
- Use single quotes for strings
The library is built around the Schema interface with four key operations:
interface Schema {
normalize(mixed $value, Context $context); // Input normalization
merge(mixed $value, mixed $base); // Merging configs
complete(mixed $value, Context $context); // Validation & finalization
completeDefault(Context $context); // Default value handling
}- Type (
Elements\Type) - Scalar types, arrays, lists - Structure (
Elements\Structure) - Object-like structures with defined properties (returnsstdClass) - AnyOf (
Elements\AnyOf) - Union types / enumerations
Note: Since v1.3.2, Expect::array() is available for array schemas with defined keys (similar to Structure but returns arrays). Useful for tuples with indexed positions.
The Expect class provides the main API using magic methods (__callStatic):
Expect::string() // Created via __callStatic
Expect::int()
Expect::structure([...])
Expect::anyOf('a', 'b')
Expect::arrayOf('string')All schema elements support method chaining via the Base trait:
required()- Make field mandatorydefault($value)- Set default valuenullable()- Allow null valuesbefore($fn)- Pre-normalization hooktransform($fn)- Post-validation transformation (v1.2.5+)assert($fn, $description)- Custom validation with optional descriptioncastTo($type)- Type castingdeprecated($msg)- Deprecation warningsmin($val)/max($val)- Range constraintspattern($regex)- Regex validation
Structure-specific methods:
skipDefaults()- Omit properties with default values from outputotherItems($schema)- Allow extra items validated by schemagetShape()- Get array of all structure propertiesextend($items)- Create new structure by extending existing one
AnyOf-specific methods:
firstIsDefault()- Make first variant the default (instead of null)
ArrayOf/ListOf-specific methods:
mergeDefaults(false)- Disable merging defaults with input (v1.1+)
The Processor class executes validation in three phases:
- Normalize - Transform input data (via
before()hooks) - Validate - Check against schema (collect errors in Context)
- Complete - Apply defaults, finalize values (via
transform())
The Context class accumulates errors during validation instead of throwing immediately:
- Tracks path for nested structures
- Collects all errors before throwing
- Separate warnings collection for deprecations
createChecker()method allows early termination on first error
Tests use Nette Tester with .phpt extension and the test() helper function:
<?php
declare(strict_types=1);
use Tester\Assert;
use Nette\Schema\Expect;
use Nette\Schema\Processor;
require __DIR__ . '/../bootstrap.php';
test('descriptive test name', function () {
$schema = Expect::string();
$processor = new Processor;
Assert::same('hello', $processor->process($schema, 'hello'));
});
testException('throws exception for invalid input', function () {
$schema = Expect::int();
(new Processor)->process($schema, 'invalid');
}, ValidationException::class);checkValidationErrors() - Validates exception messages:
checkValidationErrors(
fn() => (new Processor)->process($schema, $data),
['Expected error message']
);Test naming:
- Use
test()function with clear description as first parameter - Do NOT add comments before
test()calls - Group related tests in the same file
The Expect class uses __callStatic to create Type schemas:
public static function __callStatic(string $name, array $args): Type
{
return (new Type($name))->default($args[0] ?? null);
}This allows Expect::string(), Expect::email(), Expect::unicode(), etc.
Structure (Expect::structure([...])) - Returns stdClass objects:
- Accepts arrays and objects as input
- All properties optional by default (default: null)
- Use
required()for mandatory properties - Use
otherItems($schema)to allow extra items - Use
skipDefaults()to omit default values from output - Use
extend($items)to create new structures from existing ones - Use
getShape()to retrieve all properties
Array (Expect::array([...])) - Returns arrays (v1.3.2+):
- Same behavior as Structure but output is array
- Can define tuples with indexed positions:
Expect::array([Expect::int(), Expect::string(), Expect::bool()])
arrayOf()- Associative or indexed arrayslistOf()- Strictly indexed arrays (0, 1, 2, ...)
Both validate element types and optionally key types (since v1.2).
The merge() operation combines multiple configurations:
- Arrays are merged recursively
- Structures merge property-by-property
- Special
PreventMergingkey disables merging for a value - Used by
Processor::processMultiple()
$schema = Expect::structure([
'database' => Expect::structure([
'host' => Expect::string()->default('localhost'),
'port' => Expect::int()->min(1)->max(65535)->default(3306),
'credentials' => Expect::structure([
'username' => Expect::string()->required(),
'password' => Expect::string()->required(),
]),
]),
'features' => Expect::arrayOf('bool'),
'mode' => Expect::anyOf('development', 'production')->default('development'),
]);Expect::string()
->assert(fn($s) => strlen($s) > 0, 'String cannot be empty')
->transform(fn($s) => trim($s));transform() can both validate and modify values using Context:
Expect::string()->transform(function (string $s, Nette\Schema\Context $context) {
if (!ctype_lower($s)) {
$context->addError('All characters must be lowercased', 'my.case.error');
return null;
}
return strtoupper($s);
});$base = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$extended = $base->extend([
'email' => Expect::string(),
]);Class without constructor - Properties are assigned:
class Info {
public bool $processRefund;
public int $refundAmount;
}
Expect::structure([...])->castTo(Info::class);
// Creates: $obj = new Info; $obj->processRefund = ...; $obj->refundAmount = ...;Class with constructor - Named parameters passed:
class Info {
public function __construct(
public bool $processRefund,
public int $refundAmount,
) {}
}
Expect::structure([...])->castTo(Info::class);
// Creates: new Info(processRefund: ..., refundAmount: ...)Scalar to class - Value passed to constructor:
Expect::string()->castTo(DateTime::class);
// Creates: new DateTime($value)Generate schema from class definition:
class Config {
public string $name;
public ?string $password;
public bool $admin = false;
}
$schema = Expect::from(new Config);
// Optionally override specific fields:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w+'),
]);- Expect.php - Main API entry point (fluent builder)
- Processor.php - Validation engine
- Schema.php - Core interface
- Context.php - Error collection and path tracking
- Message.php - Error/warning messages with templating
- ValidationException.php - Exception with message collection
- Helpers.php - Internal utilities (not public API)
- DynamicParameter.php - Marker interface for runtime parameters
- Elements/ - Schema implementations (Type, Structure, AnyOf, Base trait)
Test files follow the pattern Expect.<feature>.phpt:
Expect.structure.phpt- Structure validationExpect.anyOf.phpt- Union typesExpect.scalars.phpt- Primitive typesExpect.assert.phpt- Custom assertionsExpect.transform.phpt- TransformationsHelpers.*.phpt- Internal helper tests
- Minimal phpDoc - only when adding value beyond types
- Use
@internalfor implementation details - Use
@methodfor magic methods in Expect class - No redundant documentation of obvious signatures
- Focus on "why" not "what" for complex logic
- Default value vs nullable: Default
nulldoesn't mean input can benull- usenullable()explicitly - Array merging: Default values in
arrayOf()are merged with input unlessmergeDefaults(false)is used (v1.1+) - Pattern matching:
pattern()matches the entire string (automatically wrapped in^and$) - AnyOf variants: Pass variants as separate arguments, not array (use unpacking if needed:
anyOf(...$array)) - AnyOf default: Default is
nullunless you usefirstIsDefault()to make first variant the default - Transform vs assert:
transform()can both validate and modify using Context;assert()only validates - Structure vs Array:
structure()returnsstdClass,array()returns array (v1.3.2+) - Structure output: Use
skipDefaults()to omit properties with default values - Operation order:
assert(),transform(), andcastTo()execute in declaration order