gosuperscript / schema
A PHP library for data transformation, type validation, and expression evaluation.
Requires
- php: ^8.4
- ext-intl: *
- azjezz/psl: ^3.2 || ^4.0
- brick/math: ^0.12.0 || ^0.13.0
- gosuperscript/monads: ^1.0.0
- illuminate/container: ^11.0 || ^12.0
- illuminate/support: ^11.0 || ^12.0
- psr/container: ^2.0
- sebastian/exporter: ^6.0 || ^7.0
Requires (Dev)
- infection/infection: ^0.29.14
- laravel/pint: ^1.22
- phpstan/phpstan: 2.1.45
- phpunit/phpunit: 12.5.11
- robiningelbrecht/phpunit-coverage-tools: ^1.9
- 0.x-dev
- v0.4.0
- v0.3.5
- v0.3.4
- v0.3.3
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.4
- v0.2.3
- v0.2.2
- v0.2.1
- v0.2.0
- v0.1.2
- v0.1.1
- v0.1.0
- dev-research
- dev-replace-azjezz-psl-package
- dev-claude/add-type-safety-LuT5r
- dev-next
- dev-dsl-match-conditionals
- dev-claude/add-match-expression-xMcAj
- dev-claude/lock-phpstan-version-W55i2
- dev-claude/add-describable-interface-Jpo2I
- dev-claude/annotate-operand-values-kDFY9
- dev-claude/update-infixresolver-constructor-zgYxV
- dev-claude/review-value-result-readme-8kE8e
- dev-claude/handle-missing-overloader-2rrxK
- dev-null-overloader
- dev-renovate/configure
- dev-push-qmqysoowukwn
- dev-copilot/remove-supports-from-resolver
- dev-copilot/add-copilot-instructions-file
- dev-copilot/create-readme-file
- dev-push-kpvlyprnlozp
- dev-push-olqkxuqulqvw
- dev-push-kptmvpystouw
This package is auto-updated.
Last update: 2026-04-15 13:38:08 UTC
README
A powerful PHP library for data transformation, type validation, and expression evaluation. This library provides a flexible framework for defining data schemas, transforming values, and evaluating complex expressions with type safety.
Features
- Type System: Robust type validation and transformation for numbers, strings, booleans, lists, and dictionaries
- Expression Evaluation: Support for infix expressions with custom operators
- Match Expressions: Unified conditional logic — if/then/else, dispatch tables, and cond-style matching
- Compiled Expressions: Turn a source tree into a callable you invoke with inputs
- Resolver Pattern: Pluggable resolver system for different data sources
- Operator Overloading: Extensible operator system for custom evaluation logic
- Monadic Error Handling: Built on functional programming principles using Result and Option types
Requirements
- PHP 8.4 or higher
- ext-intl extension
Installation
composer require gosuperscript/axiom
Quick Start
Expressions as callables
The top-level API is Expression: wrap a Source tree with the resolver stack you want, then invoke it with inputs like a function:
<?php use Superscript\Axiom\Definitions; use Superscript\Axiom\Expression; use Superscript\Axiom\Operators\DefaultOverloader; use Superscript\Axiom\Operators\OperatorOverloader; use Superscript\Axiom\Resolvers\DelegatingResolver; use Superscript\Axiom\Resolvers\InfixResolver; use Superscript\Axiom\Resolvers\StaticResolver; use Superscript\Axiom\Resolvers\SymbolResolver; use Superscript\Axiom\Sources\InfixExpression; use Superscript\Axiom\Sources\StaticSource; use Superscript\Axiom\Sources\SymbolSource; $resolver = new DelegatingResolver([ StaticSource::class => StaticResolver::class, SymbolSource::class => SymbolResolver::class, InfixExpression::class => InfixResolver::class, ]); $resolver->instance(OperatorOverloader::class, new DefaultOverloader()); // area = PI * radius * radius $source = new InfixExpression( left: new SymbolSource('PI'), operator: '*', right: new InfixExpression( left: new SymbolSource('radius'), operator: '*', right: new SymbolSource('radius'), ), ); $area = new Expression( source: $source, resolver: $resolver, definitions: new Definitions(['PI' => new StaticSource(3.14159)]), ); $area->parameters(); // ['radius'] $area(['radius' => 5])->unwrap()->unwrap(); // ~78.54 $area(['radius' => 10])->unwrap()->unwrap(); // ~314.16
The key idea: the expression's inputs are its parameters, passed at the call site.
Basic Type Transformation
<?php use Superscript\Axiom\Expression; use Superscript\Axiom\Resolvers\DelegatingResolver; use Superscript\Axiom\Resolvers\StaticResolver; use Superscript\Axiom\Resolvers\ValueResolver; use Superscript\Axiom\Sources\StaticSource; use Superscript\Axiom\Sources\TypeDefinition; use Superscript\Axiom\Types\NumberType; $resolver = new DelegatingResolver([ StaticSource::class => StaticResolver::class, TypeDefinition::class => ValueResolver::class, ]); $source = new TypeDefinition( type: new NumberType(), source: new StaticSource('42'), ); $expression = new Expression($source, $resolver); $expression()->unwrap()->unwrap(); // 42 (as integer)
Inputs, Definitions, and Namespaces
Inputs are bindings — passed at the call site. Stable named expressions (constants, named sub-expressions) are definitions — bound once when the Expression is constructed. Both support flat names and dotted namespaces.
use Superscript\Axiom\Definitions; use Superscript\Axiom\Expression; use Superscript\Axiom\Sources\StaticSource; use Superscript\Axiom\Sources\SymbolSource; $expression = new Expression( source: /* ... */, resolver: $resolver, definitions: new Definitions([ // Global scope 'version' => new StaticSource('1.0.0'), // Namespaced scope 'math' => [ 'pi' => new StaticSource(3.14159), 'e' => new StaticSource(2.71828), ], ]), ); // Flat and namespaced inputs $expression([ 'tier' => 'small', 'quote' => [ 'claims' => 3, 'turnover' => 600000, ], ]);
SymbolSource looks up by name + optional namespace:
new SymbolSource('pi', 'math'); // -> math.pi new SymbolSource('claims', 'quote'); // -> quote.claims new SymbolSource('version'); // -> version (global)
Bindings shadow definitions. A binding with a null value is still a real binding — it intentionally shadows any definition of the same name.
Match Expressions
MatchExpression provides a unified way to express conditionals, dispatch tables, and cond-style matching. A match expression has a subject and an ordered list of arms. Each arm pairs a pattern with a result expression. The first matching arm wins.
Patterns:
- LiteralPattern: Matches via strict equality (
===) - WildcardPattern: Always matches (the default/catch-all arm)
- ExpressionPattern: Wraps a
Source— resolves it and compares to the subject
If/then/else:
// if quote.claims > 2 then 100 * 0.25 else 0 new MatchExpression( subject: new StaticSource(true), arms: [ new MatchArm( new ExpressionPattern( new InfixExpression(new SymbolSource('claims', 'quote'), '>', new StaticSource(2)), ), new InfixExpression(new StaticSource(100), '*', new StaticSource(0.25)), ), new MatchArm(new WildcardPattern(), new StaticSource(0)), ], );
Dispatch table:
// match tier { "micro" => 1.3, "small" => 1.1, _ => 1.0 } new MatchExpression( subject: new SymbolSource('tier'), arms: [ new MatchArm(new LiteralPattern('micro'), new StaticSource(1.3)), new MatchArm(new LiteralPattern('small'), new StaticSource(1.1)), new MatchArm(new WildcardPattern(), new StaticSource(1.0)), ], );
Extensible pattern matching: The MatchResolver delegates pattern evaluation to a registry of PatternMatcher implementations. Extension packages can register their own pattern types (e.g. IntervalPattern from axiom-interval) without modifying core axiom:
$matchers = [ new WildcardMatcher(), new LiteralMatcher(), new ExpressionMatcher($resolver), // Add custom matchers from extension packages here ]; $resolver->instance(MatchResolver::class, new MatchResolver($resolver, $matchers));
Core Concepts
Types
The library provides several built-in types for data validation and coercion:
NumberType
Validates and coerces values to numeric types (int/float):
- Numeric strings:
"42"→42 - Percentage strings:
"50%"→0.5 - Numbers:
42.5→42.5
StringType
Validates and coerces values to strings:
- Numbers:
42→"42" - Stringable objects: converted to string representation
- Special handling for null and empty values
BooleanType
Validates and coerces values to boolean:
- Truthy/falsy evaluation
- String representations:
"true","false"
ListType and DictType
For collections and associative arrays with nested type validation.
Type API: Assert vs Coerce
The Type interface provides two methods for value processing, following the @azjezz/psl pattern:
assert(T $value): Result<Option<T>>- Validates that a value is already of the correct typecoerce(mixed $value): Result<Option<T>>- Attempts to convert a value from any type to the target type
When to use:
- Use
assert()when you expect a value to already be the correct type and want strict validation - Use
coerce()when you want to transform values from various input types (permissive conversion)
Example:
$numberType = new NumberType(); $numberType->assert(42); // Ok(Some(42)) $numberType->assert('42'); // Err(TransformValueException) $numberType->coerce(42); // Ok(Some(42)) $numberType->coerce('42'); // Ok(Some(42)) $numberType->coerce('45%'); // Ok(Some(0.45))
Both methods return Result<Option<T>, Throwable> where:
Ok(Some(value))- successful validation/coercion with a valueOk(None())- successful but no value (e.g., empty strings)Err(exception)- failed validation/coercion
Sources
Sources represent different ways to provide data:
- StaticSource: Direct values
- SymbolSource: Named references resolved from the context's bindings or definitions
- TypeDefinition: Combines a type with a source for validation and coercion
- InfixExpression: Mathematical/logical expressions
- UnaryExpression: Single-operand expressions
- MatchExpression: Conditional matching with ordered arms
- MemberAccessSource: Chained property/array-key access
Resolvers
Resolvers handle the evaluation of sources. They are stateless — all per-call state (bindings, definitions, the inspector, and the symbol memo) lives on a Context threaded through resolve(Source, Context):
- StaticResolver: Resolves static values
- ValueResolver: Applies type coercion using the
coerce()method - InfixResolver: Evaluates binary expressions
- UnaryResolver: Evaluates unary expressions
- SymbolResolver: Looks up symbols from bindings (first) then definitions (with per-context memoization)
- MemberAccessResolver: Evaluates property/array-key access
- MatchResolver: Evaluates match expressions with extensible pattern matching
- DelegatingResolver: Chains multiple resolvers together
Context
Context carries everything a single call needs:
use Superscript\Axiom\Bindings; use Superscript\Axiom\Context; use Superscript\Axiom\Definitions; $context = new Context( bindings: new Bindings(['radius' => 5]), definitions: new Definitions(['PI' => new StaticSource(3.14159)]), inspector: $inspector, // optional ); $resolver->resolve($source, $context);
Expression::call() / Expression::__invoke() build the Context for you from the bindings you pass.
Operators
The library supports various operators through the overloader system:
- Binary:
+,-,*,/,%,** - Comparison:
==,!=,<,<=,>,>= - Logical:
&&,|| - Special:
has,in,intersects
Resolution Inspector
The ResolutionInspector interface provides a zero-overhead observability primitive for resolution. Resolvers accept the inspector via the Context and annotate metadata about their work. When no inspector is present on the context, resolvers skip annotation entirely via null-safe calls.
Interface:
interface ResolutionInspector { public function annotate(string $key, mixed $value): void; }
Built-in annotations from first-party resolvers:
| Resolver | Annotations |
|---|---|
StaticResolver |
label: "static(int)", "static(string)", etc. |
ValueResolver |
label: type class name (e.g. "NumberType"); coercion: type change (e.g. "string -> int") |
InfixResolver |
label: operator (e.g. "+", "&&"); left, right, result |
UnaryResolver |
label: operator (e.g. "!", "-"); result |
SymbolResolver |
label: symbol name (e.g. "A", "math.pi"); memo: "hit"/"miss"; result |
MatchResolver |
label: "match"; subject: resolved subject value; matched_arm: index of matched arm; result: final value |
Usage:
use Superscript\Axiom\ResolutionInspector; final class ResolutionContext implements ResolutionInspector { private array $annotations = []; public function annotate(string $key, mixed $value): void { $this->annotations[$key] = $value; } public function get(string $key): mixed { return $this->annotations[$key] ?? null; } } $inspector = new ResolutionContext(); $expression->withInspector($inspector)(['radius' => 5]); // Annotations are available via $inspector->get('label'), etc.
Advanced Usage
Custom Types
Implement the Type interface to create custom data validations and coercions:
<?php use Superscript\Axiom\Types\Type; use Superscript\Monads\Result\Result; use Superscript\Monads\Result\Err; use Superscript\Axiom\Exceptions\TransformValueException; use function Superscript\Monads\Result\Ok; use function Superscript\Monads\Option\Some; class EmailType implements Type { public function assert(mixed $value): Result { if (is_string($value) && filter_var($value, FILTER_VALIDATE_EMAIL)) { return Ok(Some($value)); } return new Err(new TransformValueException(type: 'email', value: $value)); } public function coerce(mixed $value): Result { $stringValue = is_string($value) ? $value : strval($value); $trimmed = trim($stringValue); if (filter_var($trimmed, FILTER_VALIDATE_EMAIL)) { return Ok(Some($trimmed)); } return new Err(new TransformValueException(type: 'email', value: $value)); } public function compare(mixed $a, mixed $b): bool { return $a === $b; } public function format(mixed $value): string { return (string) $value; } }
Custom Resolvers
Create specialized resolvers for specific data sources. Resolvers must be stateless and read everything they need from the Context:
<?php use Superscript\Axiom\Context; use Superscript\Axiom\Resolvers\Resolver; use Superscript\Axiom\Source; use Superscript\Monads\Result\Result; class DatabaseResolver implements Resolver { public function resolve(Source $source, Context $context): Result { // Custom resolution logic — connect to database, fetch data, etc. } }
Development
Setup
- Clone the repository
- Install dependencies:
composer install - Run tests:
composer test
Testing
# Run all tests composer test # Individual test suites composer test:unit # Unit tests composer test:types # Static analysis (PHPStan) composer test:infection # Mutation testing
Code Quality
- PHPStan: Level max static analysis
- Infection: Mutation testing for test quality
- Laravel Pint: Code formatting
- 100% Code Coverage: Required for all new code
Architecture
The library follows several design patterns:
- Strategy Pattern: Different resolvers for different source types
- Chain of Responsibility: DelegatingResolver chains multiple resolvers
- Factory Pattern: Type system for creating appropriate transformations
- Functional Programming: Extensive use of Result and Option monads
- Explicit Per-Call State: Resolvers are stateless;
Contextcarries inputs, definitions, inspector, and memo
Error Handling
All type validation and coercion operations return Result<Option<T>, Throwable> types:
Result::Ok(Some(value)): Successful validation/coercion with valueResult::Ok(None()): Successful validation/coercion with no value (null/empty)Result::Err(exception): Validation/coercion failed with error
This approach ensures:
- No exceptions for normal control flow
- Explicit handling of success/failure cases
- Type-safe null handling
License
This library is open-sourced software licensed under the MIT license.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details on how to contribute to this project.
Security
If you discover any security-related issues, please review our Security Policy for information on how to responsibly report vulnerabilities.