Skip to content

Commit ac2986b

Browse files
committed
Add per-directory schema domains, cache the Opis loader, and expand examples
- Add schemas (path/domain pairs) config option, deprecating the single schemas_path/schema_domain pair; SchemaValidator::registerSchemaDir() lets the bundle register its own Examples/Schemas without colliding with the app's schemas_path on the shared resolver - Reuse one SchemaLoader/SchemaResolver per SchemaValidator instance instead of rebuilding it on every validate() call, so long-lived processes (Swoole/RoadRunner) don't re-parse the same schema on every request; filters are still re-registered fresh per call - Split ValidationException (domain) from HttpValidationException (HTTP transport); extract SchemaFilterResolver and Arrays::toObjectGraph() - Add a multi-level nested example (order-create -> customer -> name/ email/shippingAddress, with both relative and absolute $ref) and a custom $error message on the promo code filter - Add Mermaid diagrams (request flow, schema $ref graph) and a GitHub Sponsors button to the docs - Fix numerous documentation inaccuracies against the real API
1 parent 043e617 commit ac2986b

45 files changed

Lines changed: 2519 additions & 561 deletions

Some content is hidden

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

‎.github/FUNDING.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: [outcomer]

‎CHANGELOG.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [4.0.0] - 2026-09-02
9+
10+
### Added
11+
- `schemas` configuration option - an array of `{path, domain}` pairs, replacing the single `schemas_path`/`schema_domain` pair so multiple schema directories (each under their own domain) can be registered at once
12+
- `SchemaValidator::registerSchemaDir(domain, path)` - public API to register an additional schema directory on the shared resolver after construction; used internally to register the bundle's own `Examples/Schemas/` (and its `common/` subdirectory) without colliding with the app's own `schemas_path`
13+
- `auto_cast_query` and `auto_cast_path` configuration options (default `true`) to control automatic numeric/boolean casting for query and path parameters independently
14+
- `HttpValidationException` - HTTP-transport wrapper around the domain `ValidationException`, thrown by `MapRequestResolver` when `#[MapRequest]`'s `triggerResponse` is `true`
15+
- `SchemaFilterResolver` - extracted service that recursively discovers `$func` filter names declared in a schema's `$filters`
16+
- `Helpers\Arrays::toObjectGraph()` - the array-to-stdClass-graph conversion previously inlined in `SchemaValidator`
17+
- Opis `$filters` example (`PromoCodeFilter`) wired into the bundled `/_examples/validation/user` route, including a custom `$error` message
18+
- Reusable schema `$ref` examples: `name.json`, `email.json`, `age.json` extracted from `user-create.json`
19+
- New `/_examples/validation/order` example route and schema set (`order-create.json`, `customer.json`, `order-item.json`, `common/address.json`, `common/country.json`) demonstrating multi-level nesting with both relative and absolute `$ref`
20+
- Application-level end-to-end tests exercising the bundle's example routes through a real host application kernel and router (replacing the previous bundle-only synthetic kernel tests)
21+
- Mermaid diagrams in the documentation: a sequence diagram of the `#[MapRequest]` request/validation/DTO flow, and a dependency graph of the example schemas' `$ref` relationships
22+
- `.github/FUNDING.yml` and a GitHub Sponsors button in the documentation nav bar
23+
24+
### Changed
25+
- **`ValidationException` is now a plain domain exception** (`extends RuntimeException`, no longer `extends HttpException`) - HTTP concerns are handled exclusively by the new `HttpValidationException`
26+
- `SchemaValidator` now builds one `SchemaLoader`/`SchemaResolver` per instance and reuses it across calls, instead of a fresh `Validator` (and its underlying schema cache) on every single validation - Opis parses each schema file only once per loader, so long-lived processes (e.g. Swoole/RoadRunner workers) no longer pay the full parse cost on every request. Filters are still re-registered fresh on every call, since a filter's resolved service may not be shared
27+
- Headers are never auto-cast (previously could be affected by global type casting)
28+
29+
### Deprecated
30+
- `schemas_path` and `schema_domain` configuration options - use `schemas` instead. They still work but emit a deprecation notice
31+
32+
### Removed
33+
- Dead code in `Helpers\Arrays`: `insertInArray`, `toObject`, `groupBy`, `arrayReplaceKeys` (only `sortArrayByKeys` remains, in use)
34+
- Synthetic bundle-only `TestKernel` and its E2E test suite - replaced by application-level E2E tests (see Added)
35+
36+
### Fixed
37+
- Multiple documentation inaccuracies found to not match the actual implementation: fabricated `ValidatedRequest`/`ValidatedPayload` API in api.md (real `ValidatedRequest` only exposes `getPayload(): Payload`, plus `getViolations()`/`hasViolations()`/`isValid()`/`getStatus()`; `ValidatedPayload` does not exist - the real class is `Payload`), `ValidatedDtoInterface` incorrectly shown as an empty marker interface, fabricated `TrimFilter`/`LowercaseFilter` classes, wrong PHP/Symfony version requirements (was showing PHP 8.4+/Symfony 8.0+; real requirement is PHP >=8.2, Symfony ^7.4|^8.0), wrong `schema_domain` default (was documented as `null`, actually `https://outcomer.dev`), a reproducible bug in a dto-injection.md example (`$query['query']`/`$headers['authorization']` array access on what are actually `object` return types from `Payload::getQuery()`/`getHeaders()`), incorrect error response `message` text (`"Validation failed"` vs actual `"Request data is invalid"`), incorrect `Examples/Model/` path (actual: `Examples/Dto/`), incomplete example routes list (missing `/api-user`, `/order` and `/info`), and incorrect namespace in README.md's quick usage snippet
38+
- A schema file loaded outside `schemas_path` (e.g. one of the bundle's own examples) with the same filename as a schema inside `schemas_path` would overwrite that directory's registration on the shared resolver, breaking any concurrently-used schema in `schemas_path` for the rest of the process's lifetime - fixed by giving each registered directory its own domain instead of reusing `schema_domain` for both
39+
- Clarified that the bundle's `/_examples/*` routes ship with their own exception listener and don't require the manual listener setup described in quick-start.md
40+
- Clarified that importing the bundle's `config/routes.yaml` is only needed when the host app doesn't already auto-discover attribute-routed controllers via `routing.controllers`
41+
842
## [3.0.0] - 2026-03-02
943

1044
### Changed

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ composer require outcomer/symfony-json-schema-validation
6969
### Basic Usage
7070

7171
```php
72-
use Outcomer\Bundle\SymfonyJsonSchemaValidation\Attribute\MapRequest;
72+
use Outcomer\ValidationBundle\Attribute\MapRequest;
7373

7474
class UserController
7575
{

‎composer.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"opis/json-schema": "^2.6",
3434
"phpunit/phpunit": "^11.0|^12.0",
3535
"squizlabs/php_codesniffer": "^3.13",
36+
"symfony/browser-kit": "^8.1",
3637
"symfony/phpunit-bridge": "^7.4|^8.0",
3738
"symfony/var-dumper": "^7.4|^8.0"
3839
},

‎composer.lock‎

Lines changed: 144 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎config/services.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ services:
2020
Outcomer\ValidationBundle\ArgumentResolver\MapRequestResolver:
2121
arguments:
2222
$schemasPath: '%outcomer_validation.schemas_path%'
23+
$autoCastQuery: '%outcomer_validation.auto_cast_query%'
24+
$autoCastPath: '%outcomer_validation.auto_cast_path%'
2325
tags:
2426
- { name: controller.argument_value_resolver, priority: 50 }

‎docs/.vitepress/config.js‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { defineConfig } from 'vitepress'
1+
import { withMermaid } from 'vitepress-plugin-mermaid'
22

33
const REPO_NAME = 'symfony-json-schema-validation'
44

5-
export default defineConfig({
5+
export default withMermaid({
66
title: 'JSON Schema Validation',
77
description: 'Single Source of Truth for Symfony API contracts with JSON Schema validation and automatic OpenAPI documentation',
88
base: `/${REPO_NAME}/`,
@@ -14,14 +14,19 @@ export default defineConfig({
1414
ignoreDeadLinks: false,
1515
lastUpdated: true,
1616

17+
vite: {
18+
optimizeDeps: {
19+
include: ['mermaid']
20+
}
21+
},
22+
1723
themeConfig: {
1824
logo: { src: '/logo.svg', alt: 'Symfony JSON Schema Validation' },
1925

2026
nav: [
2127
{ text: 'Guide', link: '/guide/how-it-works' },
2228
{ text: 'Examples', link: '/guide/examples' },
23-
{ text: 'API', link: '/guide/api' },
24-
{ text: 'GitHub', link: `https://github.com/outcomer/${REPO_NAME}` }
29+
{ text: 'API', link: '/guide/api' }
2530
],
2631

2732
sidebar: [

‎docs/.vitepress/theme/custom.css‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,25 @@
1717
.main img:hover {
1818
transform: scale(1.02);
1919
}
20+
21+
/* Mermaid diagram zoom */
22+
.mermaid-zoom-dialog {
23+
width: 90vw;
24+
height: 90vh;
25+
max-width: none;
26+
max-height: none;
27+
border: none;
28+
border-radius: 8px;
29+
padding: 24px;
30+
background: var(--vp-c-bg);
31+
}
32+
33+
.mermaid-zoom-dialog::backdrop {
34+
background: rgba(0, 0, 0, 0.6);
35+
}
36+
37+
.mermaid-zoom-dialog svg {
38+
width: 100% !important;
39+
height: 100% !important;
40+
cursor: zoom-out;
41+
}

‎docs/.vitepress/theme/index.js‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,60 @@
11
import DefaultTheme from 'vitepress/theme'
22
import mediumZoom from 'medium-zoom'
3-
import { onMounted, watch, nextTick } from 'vue'
3+
import { h, onMounted, watch, nextTick } from 'vue'
44
import { useRoute } from 'vitepress'
55
import './custom.css'
66

7+
function initImageZoom() {
8+
mediumZoom('.main img', {
9+
background: 'var(--vp-c-bg)',
10+
margin: 24
11+
})
12+
}
13+
14+
function initMermaidZoom() {
15+
document.querySelectorAll('.main .mermaid svg:not([data-zoom-bound])').forEach((svg) => {
16+
svg.setAttribute('data-zoom-bound', '')
17+
svg.style.cursor = 'zoom-in'
18+
svg.addEventListener('click', () => openMermaidDialog(svg))
19+
})
20+
}
21+
22+
function openMermaidDialog(svg) {
23+
const dialog = document.createElement('dialog')
24+
dialog.className = 'mermaid-zoom-dialog'
25+
dialog.innerHTML = svg.outerHTML
26+
dialog.addEventListener('click', () => dialog.close())
27+
dialog.addEventListener('close', () => dialog.remove())
28+
document.body.appendChild(dialog)
29+
dialog.showModal()
30+
}
31+
732
export default {
833
extends: DefaultTheme,
34+
Layout() {
35+
return h(DefaultTheme.Layout, null, {
36+
'nav-bar-content-after': () =>
37+
h('iframe', {
38+
src: 'https://github.com/sponsors/outcomer/button',
39+
title: 'Sponsor outcomer',
40+
height: '32',
41+
width: '114',
42+
style: 'border: 0; border-radius: 6px; margin-left: 12px;'
43+
})
44+
})
45+
},
946
setup() {
1047
const route = useRoute()
1148
const initZoom = () => {
12-
mediumZoom('.main img', {
13-
background: 'var(--vp-c-bg)',
14-
margin: 24
15-
})
49+
initImageZoom()
50+
initMermaidZoom()
1651
}
1752
onMounted(() => {
1853
initZoom()
54+
new MutationObserver(() => initMermaidZoom()).observe(document.querySelector('.main') ?? document.body, {
55+
childList: true,
56+
subtree: true
57+
})
1958
})
2059
watch(
2160
() => route.path,

0 commit comments

Comments
 (0)