{"id":20314959,"url":"https://github.com/phphd/exceptional-validation","last_synced_at":"2026-02-24T01:34:11.362Z","repository":{"id":219427934,"uuid":"737356491","full_name":"phphd/exceptional-validation","owner":"phphd","description":null,"archived":false,"fork":false,"pushed_at":"2025-12-19T11:57:44.000Z","size":435,"stargazers_count":3,"open_issues_count":3,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-22T04:16:08.164Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/phphd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-12-30T18:27:26.000Z","updated_at":"2025-12-19T09:25:19.000Z","dependencies_parsed_at":"2024-06-29T08:44:19.188Z","dependency_job_id":"cec919d2-06da-4ed5-8e7d-61873e1866cb","html_url":"https://github.com/phphd/exceptional-validation","commit_stats":null,"previous_names":["phphd/exceptional-validation-bundle","phphd/exceptional-validation"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/phphd/exceptional-validation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-validation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-validation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-validation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-validation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/phphd","download_url":"https://codeload.github.com/phphd/exceptional-validation/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-validation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29766674,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-24T01:28:30.166Z","status":"ssl_error","status_checked_at":"2026-02-24T01:28:27.518Z","response_time":90,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T18:17:25.467Z","updated_at":"2026-02-24T01:34:11.354Z","avatar_url":"https://github.com/phphd.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Exceptional Validation 🏹\n\n🧰 Correlate Domain Exceptions with Object Properties\n\n[![Build Status](https://img.shields.io/github/actions/workflow/status/phphd/exceptional-validation/ci.yaml?branch=main\u0026logo=github\u0026logoColor=024C1A\u0026cacheSeconds=3600)](https://github.com/phphd/exceptional-validation/actions?query=branch%3Amain)\n[![Codecov](https://codecov.io/gh/phphd/exceptional-validation/graph/badge.svg?token=GZRXWYT55Z)](https://codecov.io/gh/phphd/exceptional-validation)\n[![PHPStan level](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fphphd%2Fexceptional-validation%2Frefs%2Fheads%2Fmain%2Fphpstan.dist.neon\u0026query=%24.parameters.level\u0026label=PHPStan%20level\u0026color=%23516CB3\u0026cacheSeconds=300\u0026logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAlCAMAAAAOVfv7AAABxVBMVEUAAABRa7JQa7NQa7NQa7NQbLNQa7JQa7JQa7NQa7JQa7NQa7NQa7NQa7NQbLJQa7NRbLNQa7NQa7NQa7NQa7JQa7JQa7NQa7NQa7NRa7NQa7MICQsjHx9Qa7NQa7NQa7JQa7NRa7JRbLJQa7NRbLNQa7JQa7NRbLNRa7NQbLPq6urn5ubf399ubGxqaGlHRkZQa7NQa7NRbLNQa7P6+vqoqKilpKSXlpc1PVokMFAsLCy1tLRKYqJycnI1MzQyMDGMi4t5eHgjHyBQa7NQbLJRa7NQa7JQa7IDBAdRbLNQa7JQa7NRa7P7+/vR0dHJyMi+vr6CgIArOV8pN1xbW1tOTk5NTU1AQEDz8/OtrKwuMkQTExNVVVUJDRUNER0AAABJYaIAAAAAAABRbLI1RnUjHh8AAAAAAABKYZ4jHh9RbLIAAAAAAAArLTsjHyAjHiAiHh8iHiAsPGMAAABRbLMAAAD///9Qa7IjHyD+/v4CAwRParAEBgpOaKxMZqkwQWwWHjIPFCJEW5c7T4UgK0gTGiwkJCXHxsZHXppCWJFBVIotPWYmMlQcJT4ZITgcHBxCWJORkZE9S3U6RmomM1UxNk0oJzC4QKvnAAAAdHRSTlMA/QUz+hwXDvDXysYT7JxVOyH06Ofj1KumkQz+/fbz3861sJ99dXFKNyf+/v79/f25hEEI/v7+/v7+/v39/f39/Pzswr6Yi4FzZ2FFLP7+/v7+/v7+/v7+/f39/fzy7+bgzr+8vLyri31xaV9ZVSopIxILC4tc+BMAAALhSURBVDjLhZJld+JQEIYnCdYiRYvsIoVSSt3d3dbd3X33JiFQobr19f29O9zcBM7poft8mtx5GCZvLmik/F7OE6indchkMtNiMW7nogn1sIixhlAsAgjB6gjHRSqmnSBY6KEhYSt1631EJZVu4wiDmxSc7KHaVXSdFk2oxKpI2CqzypfW3LSHHeU2D4/zRwcbTCmlgm3SGWPq2hlRJb93Uq/hqdxOKKvHok527aS9AIjLTeuV7YL17drVG3T+SdtbSDNIS/mwoHZ3SZLSMJzF2XuaZNCKOgCznVa7KOw0SRSlBx/yMnONUe0deahSB//GYV8lpbH30uA5SWnGv9lgafOVWu4CTKvyuii2KlLTSCaTaVakxmXcmsi5FRxvmyruYVVfD7s/cGIGOXtBUkZFcUve/7vTlyPGNk0OgEfNDbfslpTWTIEeLETxiPz6uftnlXPVaLIVDPrkXklqoZNx9xGcTGSZyKRErgZO3/k67jyKbosiNWDWB0zBNXTZrhb9orh8XpIax8ZvdUnKOKbxnWUQsuq3DFgwG7j0QAPLeRCX2pYLbT+m69VkPzhYtYXDdi4qqN4cwyWy+wSpSsqvU0QjyT4K5rEuojLQMnz3johcLgy2m60Bc0yXBTBjdpwl7ogNbZfcuisyDdZYG/LL+vvxhRsqT5mMQqjjYVZz1zepwaXAZSU68wBg83lNswbOWzWH2vPH9/P9qFKiPAic7sZ4QDpm8Yhz5+6J4iPgfUQnic249mBZAgqfxE9pfIqD38Bi0bXYsGeqIBSPExi1uF/w84vsgy+QKMpB2rM53ESOOIyaq8b3xPR2DkJ23Y1ofZuz3gxFTGGC+Jz0ZwxuBsoQoH33PC6kYTeVk5cM6jSHuVatiFwHZQmy/650OdgN48vL74aIiludHBagPM9u50gpdae4H89m+mRSJAGn8GkiM/HSoKfm74TT+PDqPXSwLxtu5+H/dC7EK6KTM+mywj+gq7vpNKT05QAAAABJRU5ErkJggg==)](https://github.com/phphd/exceptional-validation/blob/main/phpstan.dist.neon)\n[![Psalm level](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fphphd%2Fexceptional-validation%2Frefs%2Fheads%2Fmain%2Fpsalm.xml\u0026query=%2F%2F%40errorLevel\u0026label=Psalm%20level\u0026logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAyVBMVEXAwMCurq7ExMSysrLGxsbv7%2B8xMTH7%2B%2Fv9%2Ff3z8%2FPzmIv6%2Bvr39%2Ffo6Ojx8fHp6en19fXt7e3r6%2Butra2%2Fv7%2FwrqX7Ujr7SjGzs7O4uLi2tra8vLywsLDX19c0NDTh4eH0mozr5%2BfOzs66urqvr6%2Fj4%2BO%2Bvr5jY2Pk5OTKysrIyMjDw8OUlJSNjY1SUlJQUFA7Ozvd3d3S0tKkpKSgoKCAgIBra2tZWVlJSUlCQkLZ2dnFxcWqqqqnp6ebm5t7e3tcXFxbW1s%2FPz9ZaoUcAAAABXRSTlPu7qCgCW2MECkAAAGBSURBVDjLfY4HcoMwEEXlgmwFIRBxEjeMa3DvNT25%2F6HyFzQM8Zg8idXf1WOAlYuslvCMnZxpz4plVtzKai5yW2SM7vMNxlqVaiWmaurfvgXhCtmO1utoQtEIdgUbJdn%2BFyc6s7iHMLAzvG9s3eExq2QyyAriwAPbjs771hLG2AgNVCnpGX1zCHKibDmHMLJpCkEaphcOIY7%2BmfNlMm2kwo6%2BfXI8RBXRT6aCJz3Psz7pz%2BfCA%2FKAWMeUIAGMT7i%2F9JNZgLzAaYQmqhALDo6BQPTeEPuCxlhN1hQxjTV9Y6ERx6HzYgmDEUCw4vGbRIgnIyilBJZzNMJoyVehUAAFgkqZQ8Cxg7hXhjqrK2VZFFFaPxPkKYQZ9UawMgQbqtOP13RCQj5G0BY2ltYmZnsImni8gSbarK21r%2FXTDXq4IMHXvu93u3cpXVN6PiCBQH9FKgxR3RwBF0M2dImHG%2FRcMGT37n9AKIwc1yFcU9xsHxZYqRY6uYS1EiuXCve5FErlX%2BGRPmznPMokAAAAAElFTkSuQmCC\u0026cacheSeconds=300\u0026color=%23f75c31)](https://shepherd.dev/github/phphd/exceptional-validation)\n[![Type coverage](https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fshepherd.dev%2Fgithub%2Fphphd%2Fexceptional-validation\u0026search=%3Cth%3E[\\w\\s]*coverage.*%3F%3C%2Fth%3E.*%3F%3Ctd%3E(\\d%2B)(\\.\\d{1})%3F\\d*\\%25%3C%2Ftd%3E\u0026replace=%241%25\u0026flags=is\u0026style=flat\u0026logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAABblBMVEUAAAAAAAAAAAAAAAABAQEBAQELCwsAAAAAAAAAAAAGBgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD%2F%2F%2F8AAAD9%2Ff0FBQX39%2Ff8%2FPwQEBAUFBT09PQgICAYGBjq6uo6Ojrj4%2BPd3d2%2Fv7%2B0tLSmpqZvb289PT3x8fHi4uKxsbGSkpJ0dHQwMDAtLS3KysrExMS8vLy4uLigoKCNjY2IiIiFhYV7e3tkZGRfX19cXFxMTExBQUEnJycdHR0JCQn29vbs7Oze3t7a2trX19fOzs7Hx8fBwcGtra2oqKiZmZl3d3dWVlZJSUkyMjIqKirn5%2BfR0dG7u7ujo6OBgYFUVFRFRUX5%2Bfn19fXl5eXT09O2tradnZ1%2Bfn5ra2tOTk64D2w2AAAALnRSTlMA%2Bjz99%2FX%2B6soc%2B%2FFuUBYG2qtlV0H37sSzhXxgSCoRDQoE4dPOeTAuI7yjl48yeQ609QAAAylJREFUOMuNlWdX4lAQhm8IHVRU7Lr27uYmQCD0XpUiHaUjxd5199%2FvBQIEjLLvl5z7znPmTiZzJoBXsrVN8J%2BalQqxrZWpMdTkxMysYHURS3k8Kbl0VoCOa9%2BxapWQlotEgaqGIDRnJkyEjpIZfnZ%2Fyxk0misVF9GRvlIxGwuN%2BQk%2BdmM6UyC%2B6Ma08%2BsrO6WcCxM88swtjdJ7S9K5uoYP1pwLt4%2FVnNZsLksMSQ%2FLksarpCmd8L5pu%2BeLaiKjOBpknxA5LGyI0FYDsCMm%2BsJapLsuX%2B7D0y0juu%2B1aLkktXYdZEXFXMVgKFjUo840t%2FuwKk4a7alrJuuPRxDbE21icjBHpyMuIja%2F0Wua5IF4oHr5TI%2F%2BzqPR6lnpG%2BJDIet9DYWdcLdY9vO1ZktA6LurWeJd2l8kiDORoMv%2Bms66CSJk6EQyZh%2FU2cW6Ux30Bem2EyihdzRmpJ3Uk4d0jUTnoJNqpzE2IeXVMWEKpm4NFMU4zJ2GWLM7kwg%2Bos8Qi%2BQKPzqdAdupMxmhcrVE%2Bu7Dee%2Bx9NpXyk4DsIbFENvXpyFie0AdMXjfY7SPEyDDuAysYiGCo1PIkYcbyeOr4EB4xc1sZgasLs%2FNfIbvAbCT4g6QPj2AnS5OQJtUAQCUYjc3QR3CaweEjyYI69wrXeJdBAuE1pGic96QtT0iQ74bm0DwjNzCNY3tj%2BOIUOiCS65%2FwbQHT%2Fp0wTW1qIYn%2B3tDDM%2FJIT%2BhAmw3OHoTU8lAJk7TeWJIVfw32BeGhk1ttDtRXnLYt%2BCzQCY61w67F74c6nFUM%2ByS9zgavBOdbSTHcznqvdWOsGXD4iaaugUGTdIYkVaGndFFQ34cfOlfkLFLQ2EfB1tFatDVuiQ6Dr5STAJWWw5yTMnnkqn%2B3jBd%2Fgzrn6RDG0nzQ96bmFw52KDLiuzfsJ4f1Zz6AorjdTCQYEkqir%2Fwb1H59pJgdD%2Fvzt3zlWATn6zzbP7Fa%2FdX2Nxa4GFRKZLmrb5gLevZ%2B0ulgt7cmFcDXq3M42Icwwy2ZzSod35MiIkpyQr4RgfK3RmB%2BhBv2sp%2FqAXB2qpSKQNjtKKCUKLk%2BSH%2FA%2B%2BQHmpvMd%2BAAAAAAElFTkSuQmCC\u0026label=Type%20coverage\u0026color=f8f8f8\u0026cacheSeconds=3600)](https://shepherd.dev/github/phphd/exceptional-validation)\n[![Packagist downloads](https://img.shields.io/packagist/dt/phphd/exceptional-validation?logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAC%2FVBMVEUAAAAFBQVVWF8DBAQAAAABAQEMDAy1u8slJioDBAQCAQEAAAAAAAAAAACPk6CDg4RJS1I6PEFjZ28uLzMmEQIlDwAJCAgKCAcDBAQAAAABAQEAAAAAAAAAAAAAAABxdX4oDgAjDgAfHyEcHSAXFxgWFhYEBAUZGRwdHiAJBAEHAgAODg8EBAUEBQUAAABMTEyYnatxdYCJjpqFipV9gYwhIiUdHR5scHlmanN8gIsfDABucnthZG0bHB5DRUsvMTUTBgBWWWBDRktMT1YRERMkDQAkJSkVFhgrEAA3OT5ERkwrLDBSVVwuMDMqEQEGAQAfHyIRBwAGBgYpFAYsEAAKCgsNDQ8xMzgmJysODxATBwAICAkZGh0fHyI5O0ArLTEXFxoHBwdOTk7Z4PPZ4fTb4vXQ1%2BnW3e%2FU2%2B3CyNnc4%2FbY3%2FLX3vDN1OXM0uSUmaZfYmra4fXIz%2BHGzN2lq7mBOADT2uzEy9y%2Fxtaorr2Wm6h%2Bgo1bXmZQU1o2OD0CAQDHzt%2B9w9S5vs%2Bxt8ado7Can6yMkJ1scHhnanNhZG5dYGhGSU8%2FQkhfKAD%2F%2F%2F%2FS2erK0OK2vMuvtcW8vLyiqLeepLKIjJeEiJSBhZF1eYRwdH5kaHFSVVxJS1FDRUs%2BQEU9PkIwMTYlJipSIwBEHQDd5Pfw8PDU2%2B7P1efDytm%2BxNK7wdG5wNCus8OsssGTl6OJjZl7f4p6foh5fIZ2e4V%2BfoBgYmVhYWJWWWFOUFhLTlUfHyEUFBYODg4DBghtLgBXJAD29fXs7Oy0ucmhp7SoqKiQkJF2eoJ4eXpqbXZycnIPQl1KTFNOTk8FNU9KS046PEE0NDchIiUYGx8CEBlwMABkKgBZJgD6%2Bvrd5fff39%2FU0tHIycisscAAeLeysrOsrKwGcqmmpKMEaaCQk52YmJkAYZWHiYtxdYAGT3YmU2sVTmsfTWZdXV0AOVgySldVVVYDK0IbMD0AJTssLjIqKiopIyWGOwB8NgB4NABLIABBGQA8GQA3FgAsEgCoWa7%2BAAAAY3RSTlMACf5REQ4H%2FpiQgkQtIf7%2B7ejm1aqainFjXTkzJRsW%2Fv38sKainZyQfn5zbmtNNQr9%2B%2Fr4%2BPf39vbz8%2B7u7uzr6%2Bbj4N%2Fd29XV0s7My8vKyr29ubi4s6ypo5ycmJOLeHNsXg8rMEp9AAAD%2FElEQVQ4y6WUZXDbUBCElaRtmqTMzMzMzMzM7T2Z2a4ZQmY7nIY5ZWZuU2ZmZmbmTiXbsV1wnJnujzen0aedu5V02L%2FUel5rrJAqNndM%2BZEzSxaK9WnQLkQiuDHIrzBw8TA%2BEArtGFi0AKpkk0YlsBIN4wCRMKJoyzVu5QEtMaOcP7d3vV5asAsBUxhTtqVf8SmVqhT7w3VqD%2FlaZQxFSE0ysmwwX5tmoES192dHRWdW%2FY31K1uKp088%2FV4MQNXxKCQsN7GJk5ayVbEvdIB790UGL9sWiUO0iRGi3S%2BV0BEhKgvEdHbw9nWqs6LEQDe4Zl%2BZHAE%2FWZSwKTZ24wLc1jNTt%2FHgYcXxxbjQuKRMdTe4u4SGYGH6shOL9lxbtFlog7kH9%2B65XHobUa5aI2rhgmt0xsnp01eXvv783tPDOhJemKo%2FduzRVR75XFRYU7fYapMwx7z%2F6KubJ5%2Bd44ZzZRJQvzt558ndUxwyxRWVXHC12jhCEJ1Jy3jzctfOXdv0hrTTmdmanW8ffsjaggAhemMXHHSEdFZuwP3P7dJoNlDJzMKyNJrz57dSLGsIH0Fxt5j1OPBXEylEblHz4pD9dQefMabIcFieLUawZLILnh5O3E2iAUpW4Ay6SCalko2u5B1iI2CkcBCw%2BtfKZ2v1ZANQlTSIVFNiTMnhIWwWCQMzTk0BhoFNlG1a5sNVVQwAvjGeY96hsLBtnP24khUNFPViopQ4m26gIydeoVZtPJNtSwrZBMylO1RSE5esWeXzv7yG8aQTg3ciYvNqAJdzcMbRA9mJthKV9XHAlRXkJVr3IOKFv50L1Yn4AEtOLYo10QVkOjSncytfMT0GGMbYiNfr7bCYrreEsaibDmSaN6TKCJhe3%2Fk7%2B81qPq7UUtWtiM2PkaMNxoLtlvU7zCtxJNzCZSFuZcxNNefMbl567yblQsdwXEPY%2Bp1rWETJFNwvM6JJEewP1bt9SLmUiJeMOARwldyWjLRDmRY%2B2F%2Bq1ilh31b6WoojjSjipIb3q1LDhbqrWQKeeFYG4MyPciSjqsfNUQ44aTQXHGlY0bWGx801jImS5M6XQvOViod6Xkv12SCxCB2BCFLkaPkkzKOCfFmwjBeC275n5XKAUlUwzwrgUUHg6xsvoGuPhxIDDi%2BKFaBGqcSAoYrkpLXBCPAuQQVv54C25Gw4vli0gBEX4G2XT4znI1iVnprAkX6s5gUuMmQ7B0CYTkWcrG%2FNvMBN69ZJC1%2B1zrxSlKH5OcGLcUBdqzXna05e3iXrhTpjixZMB1789Hm3NSf3e651d26QF%2BfRA%2Fv8yM3Z%2FeXCxbxulap76bkWFnhpvl2jfDCvqj6tYsWKFSpUGF8Z%2Bx%2F9Aipg0qzWNVY9AAAAAElFTkSuQmCC\u0026color=%23F28D1A\u0026cacheSeconds=3600)](https://packagist.org/packages/phphd/exceptional-validation)\n[![Licence](https://img.shields.io/github/license/phphd/exceptional-validation.svg?color=3DA639)](https://github.com/phphd/exceptional-validation/blob/main/LICENSE)\n\nA library that captures validation exceptions and maps them to validated object properties.\n\nNo longer do you need custom validators in your object \\\nnor any validation in application/ui layers.\n\nInstead, **declaratively _relate_ domain exceptions** with their relevant form fields \\\nand yield validation failed response as you do normally.\n\n## A Validation Library? 🤔\n\nIt's not a validation library. Not ever intended to be. \\\nIt doesn't provide validation rules, constraints, or validators.\n\nInstead, it provides **exception handling** functionality, specific to a validation task.\n\nYou can validate business logic with any third-party library (or even plain PHP), \\\nwhile the library will be **_correlating_** these **validation exceptions** to the specific properties \\\nwhose invalid values caused them.\n\nIt's not a strict requirement to use Symfony Validator as a validation component, \\\nthough this library integrates it well.\n\n## Why Exceptional Validation? ✨\n\nOrdinarily, validation flows through two different layers:\n\n- HTTP/form level;\n- domain layer.\n\nIt leads to duplication and potential inconsistencies of validation rules.\n\n### Traditional Validation 🕯️\n\nThe traditional validation uses an attribute-based approach, \\\nwhich strips the domain layer from most business logic.\n\nBesides that, any custom validation you'd normally implement in a service \\\nmust be wrapped in a custom validator attribute and moved away from the service.\n\nIt's all for the sake of being able to display a nice validation message on the form.\n\nThus, the domain services and model end up naked, \\\nall business rules having been leaked elsewhere.\n\n### Exceptional Validation 💡\n\nOn the other hand, it's a common practice in DDD for domain objects to be responsible for their own validation rules.\n\n- `Email` value object validates its own format and naturally throws an exception that represents validation failure.\n- `RegisterUserService` normally verifies email is not yet taken and naturally throws an exception.\n\nThat is the kind of code that utterly expresses the model of the business, \\\nwhich should not be stripped down.\n\nYet, with a domain-driven approach, it's not possible to use standard validation tools, \\\nas these drain domain from all logic.\n\nHow then do we show contextual validation errors to the users? \\\nIt's a task of relating thrown exception with the property which value caused this exception.\n\nTo return a neat json-response with `email` as a property path and validation error description, \\\nit's necessary to match `EmailAlreadyTakenException` with a `$email` property of the original `RegisterUserCommand`.\n\nThis is what Exceptional Validation was designed for.\n\nCapturing exceptions like `EmailValidationFailedException` and mapping them to the particular form fields as `$email`, \\\nyou maintain a single source of truth for the domain validation logic.\n\nDomain enforces its invariants through value objects and services, \\\nwhile this library ensures that validation failures will properly appear in your forms and API responses.\n\n### Key takeways\n\nExceptional Validation:\n\n- Eliminates duplicate validation across HTTP/application and domain layers;\n- Keeps business rules where they belong — in the domain;\n- Makes validation logic easily unit-testable;\n- Reduces complexity of nested validation scenarios;\n- Eliminates the need for validation groups and custom validators.\n\n## Installation 📥\n\n1. Install via composer:\n\n    ```sh\n    composer require phphd/exceptional-validation\n    ```\n\n2. Enable bundles in the `bundles.php`:\n\n    ```php\n    PhPhD\\ExceptionalValidation\\Bundle\\PhdExceptionalValidationBundle::class =\u003e ['all' =\u003e true],\n    PhPhD\\ExceptionToolkit\\Bundle\\PhdExceptionToolkitBundle::class =\u003e ['all' =\u003e true],\n    ```\n\n   \u003e Note: `PhdExceptionToolkitBundle` is a required dependency\\\n   \u003e that provides exception unwrapping needful for this library.\n\n## Get Started 🎯\n\nMark a message with `#[ExceptionalValidation]` attribute. \\\nIt's used by mapper to include this object for processing.\n\nThen, define `#[Capture]` exception mappings on your properties. \\\nThese declaratively describe what exceptions correlate to what properties:\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\n\n#[ExceptionalValidation]\nclass RegisterUserCommand\n{\n    #[Capture(LoginAlreadyTakenException::class, 'auth.login.already_taken')]\n    public string $login;\n\n    #[Capture(WeakPasswordException::class, 'auth.password.weak')]\n    public string $password;\n}\n```\n\nHere, we say that `LoginAlreadyTakenException` is related to `login` property, \\\nwhile `WeakPasswordException` is related to `password` property.\n\nThe actual mapping takes place when the mapper is used:\n\n```php\nuse PhPhD\\ExceptionalValidation\\Mapper\\ExceptionMapper;\n\n/** @var ExceptionMapper\u003cConstraintViolationListInterface\u003e $mapper */\n\ntry {\n    $command = new RegisterUserCommand($login, $password);\n\n    $this-\u003eservice-\u003eregister($command);\n} catch (DomainException $exception) {\n    $violationList = $mapper-\u003emap($command, $exception);\n\n    return new JsonResponse($violationList, 422);\n}\n```\n\nEach exception, when mapped, results in a `ConstraintViolation` object, \\\nwhich contains a property path, and an exception message translation.\n\nYou can use it to render form with validation errors or serialize these into a json-response.\n\n\u003e Note that the default messages translation domain is `validators`, \\\n\u003e being inherited from `validator.translation_domain` parameter.\n\u003e\n\u003e You can change it by setting `phd_exceptional_validation.translation_domain` parameter.\n\n## How is this different from a standard validation? ⚖️\n\nYou might be wondering why we wouldn't just use simple validation asserts right in the command?\n\nThis is a logical question. A simple answer is that this's not always convenient / best.\n\nFor example, let's take the same `RegisterUserCommand` as used before.\n\nA comparison of the approaches would look something like this:\n\n```diff\n+#[ExceptionalValidation]\n class RegisterUserCommand\n {\n-    #[App\\Assert\\UniqueLogin]\n+    #[Capture(LoginAlreadyTakenException::class, 'auth.login.already_taken')]\n     public string $login;\n\n-    #[Assert\\PasswordStrength(minScore: 2)]\n+    #[Capture(WeakPasswordException::class, 'auth.password.weak')]\n     public string $password;\n }\n```\n\nThe main difference between the two is that standard validation runs before your actual business logic. \\\nThis alone means that for every domain-specific rule like \"login must be unique\" it's necessary to create \\\na custom validation constraint and a validator that implements this business logic.\n\nThereby, the main problem with the standard approach is that domain leaks into validators. \\\nThat code, which you would've normally implemented in the service, you are obliged to wrap into the validator.\n\nOne more point is that oftentimes there are multiple actions that use the same validations.\n\nFor example, login uniqueness is validated both during registration and during profile update. \\\nEven though a \"login is unique\" rule is conceptually obvious, \\\na validator approach is fraught with problems to check that a user's own login isn't taken into account when validating.\n\nExceptional validation doesn't force you to write business logic in any validators. \\\nInstead, you can throw an instance of exception in whatever scenario you would like to, \\\nand then the library will retroactively analyse it.\n\nAnother example is a password validation, which's used both during registration and during password reset. \\\nUsing the validation attributes results in duplicated asserts between the two, \\\nwhile this business conceptually belongs to `Password`, \\\nwhich most properly would be represented as a value object, used in both actions.\n\nWith exceptional validation you just write business logic in your domain and then retroactively map violations. \\\nRetroactively — after your business logic has worked out. \\\nRepresentation of the errors to the user is separate from the business logic concern which's managed by this library.\n\nFinally, this approach gives a lot of flexibility, \\\nremoving the need for custom validators, validation groups, duplicate validation rules, \\\nallowing you to keep the domain code in the domain objects, \\\nresulting in a better design of the system.\n\nFocus on the domain and let the library take care of the exception representation:\n\n```php\n// RegisterUserService\n\nif ($this-\u003euserRepository-\u003eloginExists($command-\u003elogin)) {\n    throw new LoginAlreadyTakenException($command-\u003elogin);\n}\n```\n\n## Usage with Command Bus 📇\n\nIf you are using Symfony Messenger as a Command Bus, \\\nit's recommended to use this package\nas [Symfony Messenger Middleware](https://symfony.com/doc/current/messenger.html#middleware).\n\n\u003e If you are not using `Messenger` component, you can still leverage features of this library, \\\n\u003e as it provides a rigorously structured set of tools w/o depending on any particular implementation. \\\n\u003e Installation of third-party dependencies is optional — they won't be installed unless you need it.\n\nAdd `phd_exceptional_validation` middleware to the list:\n\n```diff\n framework:\n     messenger:\n         buses:\n             command.bus:\n                 middleware:\n                     - validation\n+                    - phd_exceptional_validation\n                     - doctrine_transaction\n```\n\nOnce you have done this, the middleware will take care of capturing exceptions and re-throwing\n`ExceptionalValidationFailedException`.\n\nYou can use it to catch and process it:\n\n```php\n$command = new RegisterUserCommand($login, $password);\n\ntry {\n    $this-\u003ecommandBus-\u003edispatch($command);\n} catch (ExceptionalValidationFailedException $exception) {\n    $violationList = $exception-\u003egetViolationList();\n\n    return $this-\u003erender('registrationForm.html.twig', ['errors' =\u003e $violationList]);\n} \n```\n\nThis exception just wraps respectively mapped `ConstraintViolationList` with all your messages and property paths.\n\n### How it works ⚙️\n\nPrimarily, it works as\na [Command Bus](https://symfony.com/doc/current/messenger.html#multiple-buses-command-event-buses)\nmiddleware that intercepts exceptions and uses exception mapper to perform their mapping to the relevant form\nproperties, eventually formatting captured exceptions as\nstandard [SF Validator](https://symfony.com/doc/current/validation.html) violations.\n\n\u003e Besides that, `ExceptionMapper` is also available for direct use w/o any middleware. You can\n\u003e reference it as `ExceptionMapper\u003cConstraintViolationListInterface\u003e` service.\n\nThis diagram represents the concept:\n\n![Exceptional Validation.svg](https://raw.githubusercontent.com/phphd/exceptional-validation/refs/heads/main/assets/Exceptional%20Validation.svg)\n\n## Custom Usage 🔌\n\nIt's possible to use features of this bundle without necessarily depending on Command Bus middleware, nor on the\nMessenger component.\n\nIf you're using Symfony, you can check what exception mappers are available using this command:\n\n```shell\nbin/console debug:container ExceptionMapper\n```\n\nThis should provide you with a list, similar to this:\n\n```text\n[0] PhPhD\\ExceptionalValidation\\Mapper\\ExceptionMapper\u003cPhPhD\\ExceptionalValidation\\Rule\\Exception\\MatchedExceptionList\u003e\n[1] PhPhD\\ExceptionalValidation\\Mapper\\ExceptionMapper\u003cSymfony\\Component\\Validator\\ConstraintViolationListInterface\u003e\n```\n\nThese mappers allow you to map the Exception to any available format, specified as a generic parameter.\nIt could be `ConstraintViolationList`, or a list of `MatchedException`, or anything else.\n\nTherefore, you can inject the needed service into your own code:\n\n```php\nuse PhPhD\\ExceptionalValidation\\Mapper\\ExceptionMapper;\nuse Symfony\\Component\\Validator\\ConstraintViolationListInterface;\n\nclass SignDocumentActivity\n{\n    public function __construct(\n        /** @var ExceptionMapper\u003cConstraintViolationListInterface\u003e */\n        #[Autowire(service: ExceptionMapper::class.'\u003c'.ConstraintViolationListInterface::class.'\u003e')]\n        private ExceptionMapper $exceptionMapper,\n    ) {\n    }\n\n    public function sign(SignCommand $command): string\n    {\n        try {\n            return $command-\u003eprocess();\n        } catch (DomainException $e) {\n            /** @var ConstraintViolationListInterface $violationList */\n            $violationList = $this-\u003eexceptionMapper-\u003emap($message, $e);\n\n            throw new ApplicationFailure(\n                'Validation Failed',\n                $this-\u003eencode($violationList),\n                previous: $e,\n            );\n        }\n    }\n}\n```\n\nIn this example, we use `ExceptionMapper` to relate the caught exception to some property of the `$message`, \\\nproducing `ConstraintViolationListInterface` that can be used however you want to.\n\n## Standalone Usage 🔧\n\nIf you are not using Symfony framework, you can still take advantage of this library.\n\nCreate a Service Container (`symfony/dependency-injection` is required) with a DI Extension \\\nand then use it to create necessary services:\n\n```php\nuse PhPhD\\ExceptionalValidation\\Bundle\\DependencyInjection\\PhdExceptionalValidationExtension;\n\n$container = (new PhdExceptionalValidationExtension())-\u003egetContainer([\n    'kernel.environment' =\u003e 'prod',\n    'kernel.build_dir' =\u003e __DIR__.'/var/cache',\n]);\n\n$container-\u003ecompile();\n\n/** @var ExceptionMapper\u003cConstraintViolationListInterface\u003e $mapper */\n$mapper = $container-\u003eget(ExceptionMapper::class.'\u003c'.ConstraintViolationListInterface::class.'\u003e');\n```\n\nHerein, you create a Container, compile it, and use it to retrieve `ExceptionMapper`.\n\n## Features 📙\n\n`#[ExceptionalValidation]` and `#[Capture]` attributes allow you to implement very flexible mappings. \\\nHere are the examples of how you can use them.\n\n### Capture Conditions\n\n#### Exception Class Condition\n\nA minimum required condition. \\\nMatches the exception by its class name using `instanceof` check, \\\nmaking it similar to `catch` operation.\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\n\n#[ExceptionalValidation]\nclass PublishMessageCommand\n{\n    #[Capture(MessageNotFoundException::class)]\n    public string $messageId;\n}\n```\n\n#### Origin Source Condition\n\nBesides filtering by exception class, \\\nit's possible to filter by the origin class and method name \\\nwhence the exception raised from.\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\nuse Symfony\\Component\\Uid\\Uuid;\n\n#[ExceptionalValidation]\nclass ConfirmPackageCommand\n{\n    #[Capture(\\InvalidArgumentException::class, from: [Uuid::class, 'fromString'])]\n    public string $uid;\n}\n```\n\nIn this example `InvalidArgumentException` is a generic one, which can originate from multiple places. \\\nTo catch only those exceptions that belong to `Uuid` class, `from:` clause specifies class and method names.\n\nThus, Exception Mapper will analyse the exception trace \\\nand check whether it was originated from the `from:` place.\n\n#### When-Closure Condition\n\n`#[Capture]` attribute allows to specify `when:` argument with a callback function to be used to determine \\\nwhether particular instance of the exception should be captured for a given property or not. \\\nThis is particularly useful when the same exception could be originated from multiple places:\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\n\n#[ExceptionalValidation]\nclass TransferMoneyCommand\n{\n    #[Capture(BlockedCardException::class, when: [self::class, 'isWithdrawalCardBlocked'])]\n    public int $withdrawalCardId;\n\n    #[Capture(BlockedCardException::class, when: [self::class, 'isDepositCardBlocked'])]\n    public int $depositCardId;\n\n    public function isWithdrawalCardBlocked(BlockedCardException $exception): bool\n    {\n        return $exception-\u003egetCardId() === $this-\u003ewithdrawalCardId;\n    }\n\n    public function isDepositCardBlocked(BlockedCardException $exception): bool\n    {\n        return $exception-\u003egetCardId() === $this-\u003edepositCardId;\n    }\n}\n```\n\nIn this example, once we've matched `BlockedCardException` by class, custom closure is called.\n\nIf `isWithdrawalCardBlocked()` callback returns `true`, then exception is captured for `withdrawalCardId` property.\n\nOtherwise, we analyse `depositCardId`, and if `isDepositCardBlocked()` callback returns `true`, \\\nthen the exception is captured on this property.\n\nIf neither of them returned `true`, then exception is re-thrown upper in the stack.\n\n#### ValueException Condition\n\nSince in most cases capture conditions come down to the simple value comparison, it's easier to make the exception\nimplement `ValueException` interface and specify `condition: ExceptionValueMatchCondition::class` instead of\nimplementing `when:` closure every time.\n\nThis way it's possible to avoid much of the boilerplate code, keeping it clean:\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\nuse PhPhD\\ExceptionalValidation\\Rule\\Object\\Property\\Capture\\Condition\\Value\\ExceptionValueMatchCondition;\n\n#[ExceptionalValidation]\nclass TransferMoneyCommand\n{\n    #[Capture(BlockedCardException::class, condition: ExceptionValueMatchCondition::class)]\n    public int $withdrawalCardId;\n\n    #[Capture(BlockedCardException::class, condition: ExceptionValueMatchCondition::class)]\n    public int $depositCardId;\n}\n```\n\nIn this example `BlockedCardException` could be captured either to `withdrawalCardId` or `depositCardId`, \\\ndepending on the `cardId` value from the exception.\n\nAnd `BlockedCardException` itself must implement `ValueException` interface:\n\n```php\nuse PhPhD\\ExceptionalValidation\\Rule\\Object\\Property\\Capture\\Condition\\Value\\ValueException;\n\nclass BlockedCardException extends DomainException implements ValueException\n{\n    public function __construct(private Card $card) \n    {\n        parent::__construct('card.blocked');\n    }\n\n    public function getValue(): int\n    {\n        return $this-\u003ecard-\u003egetId();    \n    }\n}\n```\n\n#### ValidationFailedException Condition\n\nThis one is very similar to `ValueException` condition \\\nwith the difference that it integrates Symfony's native `ValidationFailedException`.\n\nSpecify `ValidationFailedExceptionMatchCondition` to correlate validation exception's value with a property value:\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\nuse PhPhD\\ExceptionalValidation\\Rule\\Object\\Property\\Capture\\Condition\\Validator\\ValidationFailedExceptionMatchCondition;\nuse Symfony\\Component\\Validator\\Exception\\ValidationFailedException;\n\n#[ExceptionalValidation]\nclass RegisterUserCommand\n{\n    #[Capture(\n        exception: ValidationFailedException::class,\n        from: Password::class,\n        condition: ValidationFailedExceptionMatchCondition::class,\n    )]\n    public string $password;\n}\n```\n\n### Violation Formatters 🎨\n\nThere are two main built-in violation formatters you can use: `DefaultExceptionViolationFormatter` and\n`ViolationListExceptionFormatter`.\n\nIf needed, create a custom violation formatter as described below.\n\n#### Main\n\n`MainExceptionViolationFormatter` is used by default if another formatter is not specified.\n\nIt provides a basic way of creating a `ConstraintViolation` with these parameters: \\\n`$root`, `$message`, `$propertyPath`, `$value`.\n\n#### Constraint Violation List Formatter\n\n`ViolationListExceptionFormatter` allows formatting the exceptions \\\nthat contain a `ConstraintViolationList` from the validator.\n\nSuch exceptions should implement `ViolationListException` interface:\n\n```php\nuse PhPhD\\ExceptionalValidation\\Mapper\\Validator\\Formatter\\ViolationList\\ViolationListException;\nuse Symfony\\Component\\Validator\\ConstraintViolationListInterface;\n\nfinal class CardNumberValidationFailedException extends \\RuntimeException implements ViolationListException\n{\n    public function __construct(\n        private readonly string $cardNumber,\n        private readonly ConstraintViolationListInterface $violationList,\n    ) {\n        parent::__construct('Card Number Validation Failed');\n    }\n\n    public function getViolationList(): ConstraintViolationListInterface\n    {\n        return $this-\u003eviolationList;\n    }\n}\n```\n\nThen, specify `ViolationListExceptionFormatter` as a `formatter:` for the `#[Capture]` attribute:\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\nuse PhPhD\\ExceptionalValidation\\Mapper\\Validator\\Formatter\\ViolationList\\ViolationListExceptionFormatter;\n\n#[ExceptionalValidation]\nclass IssueCreditCardCommand\n{\n    #[Capture(\n        exception: CardNumberValidationFailedException::class, \n        formatter: ViolationListExceptionFormatter::class,\n    )]\n    private string $cardNumber;\n}\n```\n\nThus, `CardNumberValidationFailedException` is captured on a `cardNumber` property, \\\nand formatter makes sure all its constraint violations are mapped for this property.\n\n\u003e If `#[Capture]` attribute specified a message, \\\n\u003e it would've been ignored in favour of `ConstraintViolationList` messages.\n\n\n\u003e Besides that, it's also possible to use `ValidationFailedExceptionFormatter`, \\\n\u003e which can format Symfony's native `ValidationFailedException`.\n\n#### Custom Violation Formatters\n\nIn some cases, you might want to customize the created violations. \\\nFor example, pass additional parameters to the message translation.\n\nYou can create custom violation formatter by implementing `ExceptionViolationFormatter` interface:\n\n```php\nuse PhPhD\\ExceptionalValidation\\Mapper\\Validator\\Formatter\\ExceptionViolationFormatter;\nuse PhPhD\\ExceptionalValidation\\Rule\\Exception\\MatchedException;\nuse Symfony\\Component\\Validator\\ConstraintViolationInterface;\n\n/** @implements ExceptionViolationFormatter\u003cLoginAlreadyTakenException|WeakPasswordException\u003e */\nfinal class RegistrationViolationsFormatter implements ExceptionViolationFormatter\n{\n    public function __construct(\n        #[Autowire(service: ExceptionViolationFormatter::class.'\u003cThrowable\u003e')]\n        private ExceptionViolationFormatter $formatter,\n    ) {\n    }\n\n    /** @return array{ConstraintViolationInterface} */\n    public function format(MatchedException $matchedException): ConstraintViolationInterface\n    {\n        // format violation with the default formatter\n        // and then adjust only the necessary parts\n        [$violation] = $this-\u003eformatter-\u003eformat($matchedException);\n\n        $exception = $matchedException-\u003egetException();\n\n        if ($exception instanceof LoginAlreadyTakenException) {\n            $violation = new ConstraintViolation(\n                $violation-\u003egetMessage(),\n                $violation-\u003egetMessageTemplate(),\n                ['loginHolder' =\u003e $exception-\u003egetLoginHolder()],\n                // ...\n            );\n        }\n\n        if ($exception instanceof WeakPasswordException) {\n            // ...\n        }\n\n        return [$violation];\n    }\n}\n```\n\nThen, register it as a service:\n\n```yaml\nservices:\n    App\\Auth\\User\\Features\\Registration\\Validation\\RegistrationViolationsFormatter:\n        autoconfigure: true\n```\n\n\u003e In order for violation formatter to be recognized by the bundle, \\\n\u003e its service must be tagged with `MatchedExceptionFormatter` class-name tag.\n\u003e\n\u003e If you are using [autoconfiguration](https://symfony.com/doc/current/service_container.html#the-autoconfigure-option),\n\u003e this will be done automatically by the service container, \\\n\u003e owing to the fact that `MatchedExceptionFormatter` interface is implemented.\n\nFinally, specify formatter in the `#[Capture]` attribute:\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\n\n#[ExceptionalValidation]\nfinal class RegisterUserCommand\n{\n    #[Capture(LoginAlreadyTakenException::class, formatter: LoginAlreadyTakenViolationFormatter::class)]\n    private string $login;\n\n    #[Capture(WeakPasswordException::class, formatter: WeakPasswordViolationFormatter::class)]\n    private string $password;\n}\n```\n\nIn this example, `LoginAlreadyTakenViolationFormatter` is used to format constraint violation for\n`LoginAlreadyTakenException`, \\\nand `WeakPasswordViolationFormatter` formats `WeakPasswordException`.\n\nThough not recommended, you might use a single formatter for the two.\n\n### In-depth analysis\n\n`#[ExceptionalValidation]` attribute works side-by-side with Symfony Validator's `#[Valid]` attribute.\n\nOnce you define `#[Valid]` on an object/iterable property, \\\nthe mapper will pick it up for the nested exception mapping analysis, \\\nproviding a respective property path for the created violations.\n\n```php\nuse PhPhD\\ExceptionalValidation;\nuse PhPhD\\ExceptionalValidation\\Capture;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n#[ExceptionalValidation]\nclass CreateOrderCommand\n{\n    /** @var OrderItemDto[] */\n    #[Assert\\Valid]\n    public array $items;\n}\n\n#[ExceptionalValidation]\nclass OrderItemDto\n{\n    public int $productId;\n\n    #[Capture(InsufficientStockException::class, when: [self::class, 'isStockExceptionForThisItem'])]\n    public string $quantity;\n\n    public function isStockExceptionForThisItem(InsufficientStockException $exception): bool\n    {\n        return $exception-\u003egetProductId() === $this-\u003eproductId;\n    }\n}\n```\n\nIn this example, every time exception is processed, it will also be matched with inner objects from `items` property,\nuntil it finally arrives at `items[*].quantity` (`*` stands for the particular array item index) property, being matched\nby `InsufficientStockException` class name, and custom closure condition that makes sure that it was this particular\n`OrderItemDto` that caused the exception.\n\nThe resulting property path of the caught violation includes all intermediary items, starting from the root of the tree,\nproceeding down to the leaf item, where the exception was actually caught.\n\n### Capturing multiple exceptions\n\nTypically, validation is expected to return all present violations at once (not just the first one) so they can be shown\nto the user.\n\nThough due to the limitations of the sequential computation model, only one instruction can be executed at a time, and\ntherefore, only one exception can be thrown at a time. This leads to a situation where validation ends up in only the\nfirst exception being thrown, while the rest are not even reached.\n\nFor example, if we consider user registration with `RegisterUserCommand` from the code above, we'd like to capture both\n`LoginAlreadyTakenException` and `WeakPasswordException` at once, so that the user can fix all the form errors at once,\nrather than sorting them out one by one.\n\nThis limitation can be overcome by implementing some concepts from an Interaction Calculus model in a sequential PHP\nenvironment. The key idea is to use a semi-parallel execution flow instead of a purely sequential.\n\nIn practice, if validation is split into multiple functions, each of which may throw an exception, the concept can be\nimplemented by calling them one by one and collecting any exceptions as they raise. If there were any, they are wrapped\ninto a composite exception that is eventually thrown.\n\nFortunately, you don't need to implement this manually, since `amphp/amp` library already provides a more efficient\nsolution than one you'd likely write yourself, using async Futures:\n\n```php\n/**\n * @var Login $login \n * @var Password $password \n */\n[$login, $password] = await([\n    // validate and create an instance of Login\n    async($this-\u003ecreateLogin(...), $service),\n    // validate and create an instance of Password\n    async($this-\u003ecreatePassword(...), $service),\n]);\n```\n\nIn this example, `createLogin()` method could throw `LoginAlreadyTakenException` and `createPassword()` method could\nthrow `WeakPasswordException`.\n\nBy using `async` and `awaitAnyN` functions, we are leveraging semi-parallel execution flow instead of sequential, so\nthat both `createLogin()` and `createPassword()` methods are executed regardless of thrown exceptions.\n\nIf no exceptions were thrown, then `$login` and `$password` variables are populated with the respective return\nvalues. But if there were indeed some exceptions then `Amp\\CompositeException` will be thrown with all the wrapped\nexceptions inside.\n\n\u003e If you would like to use a custom composite exception, make sure to read\n\u003e about [ExceptionUnwrapper](https://github.com/phphd/exception-toolkit?tab=readme-ov-file#exception-unwrapper)\n\nSince the library is capable of processing composite exceptions (with unwrappers for Amp and Messenger exceptions), all\nof our thrown exceptions will be processed, and the user will get the complete stack of validation errors at hand.\n\n## Upgrading 👻\n\nThe basic upgrade can be performed by [Rector](https://getrector.com/documentation) using\n`ExceptionalValidationSetList` \\\nthat comes with the library and contains automatic upgrade rules.\n\nTo upgrade a project to the latest version of `exceptional-validation`, \\\nadd the following configuration to your `rector.php` file:\n\n```php\nreturn RectorConfig::configure()\n    -\u003ewithPaths([ __DIR__ . '/src'])\n    -\u003ewithImportNames(removeUnusedImports: true)\n    // Upgrading from your version (e.g. 1.4) to the latest version\n    -\u003ewithSets(ExceptionalValidationSetList::fromVersion('1.4')-\u003egetSetList());\n```\n\nMake sure to specify your current version of the library so that upgrade sets will be matched correctly.\n\nYou should also check [UPGRADE.md](UPGRADE.md) for additional instructions and breaking changes.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphphd%2Fexceptional-validation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphphd%2Fexceptional-validation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphphd%2Fexceptional-validation/lists"}