{"id":50115210,"url":"https://github.com/phphd/exceptional-matcher","last_synced_at":"2026-05-23T14:05:20.128Z","repository":{"id":219427934,"uuid":"737356491","full_name":"phphd/exceptional-matcher","owner":"phphd","description":"Match exceptions to the properties that originated them","archived":false,"fork":false,"pushed_at":"2026-05-08T12:03:24.000Z","size":1440,"stargazers_count":3,"open_issues_count":1,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-08T12:30:03.306Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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":"2026-05-08T11:48:35.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-matcher","commit_stats":null,"previous_names":["phphd/exceptional-validation-bundle","phphd/exceptional-validation","phphd/exceptional-matcher"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/phphd/exceptional-matcher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-matcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-matcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-matcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-matcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/phphd","download_url":"https://codeload.github.com/phphd/exceptional-matcher/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phphd%2Fexceptional-matcher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33398432,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T04:15:53.637Z","status":"ssl_error","status_checked_at":"2026-05-23T04:15:53.242Z","response_time":53,"last_error":"SSL_read: 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":"2026-05-23T14:05:17.672Z","updated_at":"2026-05-23T14:05:20.118Z","avatar_url":"https://github.com/phphd.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Exceptional Matcher 🏹\n\n💼 Match the Exceptions to the Object's Properties\n\n[![Build Status](https://img.shields.io/github/actions/workflow/status/phphd/exceptional-matcher/ci.yaml?branch=main\u0026logo=github\u0026logoColor=024C1A\u0026cacheSeconds=3600)](https://github.com/phphd/exceptional-matcher/actions?query=branch%3Amain)\n[![Codecov](https://codecov.io/gh/phphd/exceptional-matcher/graph/badge.svg?token=GZRXWYT55Z)](https://codecov.io/gh/phphd/exceptional-matcher)\n[![PHPStan level](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fphphd%2Fexceptional-matcher%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-matcher/blob/main/phpstan.dist.neon)\n[![Psalm level](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fphphd%2Fexceptional-matcher%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-matcher)\n[![Type coverage](https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fshepherd.dev%2Fgithub%2Fphphd%2Fexceptional-matcher\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-matcher)\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-matcher.svg?color=3DA639)](https://github.com/phphd/exceptional-matcher/blob/main/LICENSE)\n\nA lightweight bridge from domain exceptions to validation violations.\n\n![Exceptional Matcher.svg](https://raw.githubusercontent.com/phphd/exceptional-matcher/refs/heads/main/assets/Exceptional%20Matcher.svg)\n\nYour domain code that processes Dto (i.e. services / entities) can run [validation by throwing](./docs/exceptional-validation/exceptional-validation.md) business exceptions. \\\nUsing Matcher, you can correlate it to the property that originated it –\nthereby allowing to return precise field-specific validation errors inferred from the exceptions.\n\nThence it makes up for what was lacking in tools for relating validation exceptions to their originator fields.\n\n## Quick Start ⚡\n\n### Install 📥\n\n1. Require via composer:\n\n    ```sh\n    composer require phphd/exceptional-matcher\n    ```\n\n2. \\[Symfony\\] enable the bundles in the `bundles.php`:\n\n   ```php\n   PhPhD\\ExceptionalMatcher\\Bundle\\PhdExceptionalMatcherBundle::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\n3. \\[Non-Symfony\\] configure the container:\n\n   You can use features of this library outside frameworks. \\\n   See [Standalone Usage](#standalone-usage-).\n\n### Map and Throw 🎯\n\nMark a command or dto with `#[Try_]` attribute, and properties with `#[Catch_]`.\n\n`#[Try_]` lets the matcher know it's included for processing. \\\n`#[Catch_]` defines rules about \"what\" and \"how\" to catch.\n\nFinally, throw those exceptions from your use-case handler:\n\n```php\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Try_;\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Catch_;\n\n#[Try_]\nclass UserRegistration\n{\n    #[Catch_(LoginAlreadyTakenException::class)]\n    public string $login;\n\n    #[Catch_(PasswordCompromisedException::class)]\n    public string $password;\n\n    public function process(UserRegistrationServices $services): void\n    {\n        $userWithTheSameLogin = $services-\u003euserRepository-\u003ewhereLogin($this-\u003elogin)-\u003efirstOrNull();\n\n        if (null !== $userWithTheSameLogin) {\n            throw new LoginAlreadyTakenException($this-\u003elogin);        \n        }\n\n        if ($services-\u003epasswordSecurity-\u003eisCompromised($this-\u003epassword)) {\n            throw new PasswordCompromisedException($this-\u003epassword);\n        }\n\n        $services-\u003eentityManager-\u003epersist(new User($this-\u003elogin, $this-\u003epassword));\n        $services-\u003eentityManager-\u003eflush();\n    }\n}\n```\n\n\u003e Note: `UserRegistration` embodies both data (DTO) and behavior (Service) over this data, \\\n\u003e effectively attaining well-comprehensible Object-Oriented Design.\n\nThese mappings describe what exceptions what properties correlate with.\n\nHere, `LoginAlreadyTakenException` is bound to the `login` property, \\\nwhile `PasswordCompromisedException` is bound to the `password` property.\n\n\u003e You can have additional matching conditions beyond just the exception class name. \\\n\u003e See [Match Conditions 🖇️](docs/config/match-conditions.md).\n\nThe equivalent (very rough) simplified manual logic if not using this library:\n\n```php\n$registration = new UserRegistration('jzs', 'jn3.16');\n\n$errors = [];\ntry {\n    return $registration-\u003eprocess($services);\n} catch (LoginAlreadyTakenException $e) {\n    $errors['login'] = $e-\u003egetMessage();\n} catch (PasswordCompromisedException $e) {\n    $errors['password'] = $e-\u003egetMessage();\n}\n```\n\n### Catch and Match 🎣\n\n[Exceptional Validation](./docs/exceptional-validation/exceptional-validation.md) is like a parable of fisherman.\n\n\u003e One man went out to **fish carps**. \\\n\u003e Having cast a fishing rod, he waits... \\\n\u003e And there it is! It's biting! \\\n\u003e In anticipation of a catch, he starts reeling up... \\\n\u003e Yet, to his disappointment, it's a fry and not a fish! \\\n\u003e He throws it back.\n\u003e \n\u003e The second time he casts the line... \\\n\u003e In faith of a good catch, he waits... \\\n\u003e And now at last, he pulls a fine 5-pound carp. \\\n\u003e He takes it home and roasts it to eat.\n\nThe code:\n\n```php\nuse PhPhD\\ExceptionalMatcher\\ExceptionMatcher;\nuse Symfony\\Component\\DependencyInjection\\Attribute\\Autowire;\nuse Symfony\\Component\\HttpKernel\\Attribute\\AsController;\nuse Symfony\\Component\\Routing\\Attribute\\Route;\nuse Symfony\\Component\\Validator\\ConstraintViolationListInterface;\n\n#[AsController]\nclass RegisterUserApiPoint\n{\n    public function __construct(\n        /** @var ExceptionMatcher\u003cConstraintViolationListInterface\u003e */\n        #[Autowire(service: ExceptionMatcher::class.'\u003c'.ConstraintViolationListInterface::class.'\u003e')]\n        private ExceptionMatcher $matcher,\n        private UserRegistrationServices $services,\n    ) { }\n\n    #[Route(\n        path: '/register',\n        name: 'user_register',\n        methods: ['POST'],\n    )]\n    public function __invoke(\n        #[MapRequestPayload] UserRegistration $registration,\n    ): Response {\n        try {\n            $registration-\u003eprocess($this-\u003eservices);\n        } catch (Throwable $exception) {\n            return $this-\u003ehandleError($exception, $registration);\n        }\n\n        return new Response(status: HTTP_CREATED);\n    }\n\n    private function handleError(Throwable $exception, UserRegistration $registration): Response\n    {\n        /** @var ?ConstraintViolationListInterface $violationList */\n        $violationList = $this-\u003ematcher-\u003ematch($exception, $registration);\n\n        if (null === $violationList) {\n            throw $exception;\n        }\n\n        return new JsonResponse($violationList, HTTP_UNPROCESSABLE_ENTITY);   \n    }\n}\n```\n\nThe cue to a parable:\n- Exception is the fish;\n- Code that catches is the fisherman;\n- Data object's mappings – man's expectation of a carp;\n- ConstraintViolation – the fish after roasting.\n\nFisherman evaluates the fish against his expectation - \\\n`ExceptionMatcher` matches the exception against `UserRegistration` object.\n\nFisherman roasts the fish - \\\n`ExceptionMatcher` returns the `ConstraintViolation` list (or custom format) created of the exception.\n\nFisherman releases the fry - \\\n`ExceptionMatcher` didn't match, and the main code `throws $exception;` back.\n\n\nFinally, the created `ConstraintViolationList` contains violation-objects with matched property path,\nmessage translation, and invalid value.\n\nYou can serialize it into a json-response or render on a form.\n\n```json\n{\n    \"propertyPath\": \"login\",\n    \"invalidValue\": \"jzs\",\n    \"message\": \"Login is already taken. Try another one.\"\n}\n```\n\n## Why Exceptional Matcher ✨\n\nExceptional Matcher opens the way for [Exceptional Validation](./docs/exceptional-validation/exceptional-validation.md),\nwherewith you can **omit** any **peripheral validation** off of your dto objects, \\\nand rely solely on validation in real code (services, value objects) – that belongs to and resides in the domain.\n\nThus, you have a full-fledged domain-embedded **validation** that naturally expresses itself with the **exceptions**.\n\n### Where is the Power 🚀\n\nConsider another use-case: after registration, the user should be able to _update_ his _profile_.\n\nUpdating the _login_ must ensure its _uniqueness_ in spite of the current user.\n\nHere's what we'd have to do with an upfront attribute-driven validation:\n\n```php\n#[UniqueEntity(\n    fields: ['login'],\n    entityClass: User::class,\n    identifierFieldNames: ['user' =\u003e 'id'],\n)]\nclass UserProfileUpdate\n{ \n    public string $login; \n@@\n```\n\nCompare this to `#[Catch_]` approach and recognize which communicates the intent better:\n\n```php\n#[Try_]\nclass UserProfileUpdate\n{ \n    #[Catch_(LoginAlreadyTakenException::class)]\n    public string $login;\n@@\n```\n\nThe first approach is very imperative, verbose. \\\nThe second declaratively states the fact.\n\nMoreover, now you don't restrain yourself with the limitations of the framework. \\\nYou don't have limitations at all. \\\n[Exceptional Validation](./docs/exceptional-validation/exceptional-validation.md) enables you to implement everything at the highest quality possible.\n\nThe mapping for profile update `Dto` is just as high-level as it was for [registration `Dto`](#map-and-throw-):\n\n```php\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Try_;\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Catch_;\n\n#[Try_]\nclass UserProfileUpdate\n{\n    #[Catch_(LoginAlreadyTakenException::class)]\n    public string $login;\n\n    #[Catch_(PasswordCompromisedException::class)]\n    #[Catch_(PasswordCannotBeReusedException::class)]\n    public string $password;\n    \n    public function process(User $user, UserProfileUpdateServices $services): void\n    {\n        $userWithTheSameLogin = $services-\u003euserRepository-\u003ewhereLogin($this-\u003elogin)-\u003efirstOrNull();\n\n        if ($userWithTheSameLogin?-\u003eis($user) === false) {\n            throw new LoginAlreadyTakenException($this-\u003elogin);\n        }\n\n        if ($services-\u003epasswordSecurity-\u003eisCompromised($this-\u003epassword)) {\n            throw new PasswordCompromisedException($this-\u003epassword);\n        }\n\n        // Could throw PasswordCannotBeReusedException\n        $user-\u003eupdateProfile($this-\u003elogin, $this-\u003epassword);\n\n        $services-\u003eentityManager-\u003eflush();\n    }\n}\n```\n\nNo custom validators, no attribute-driven-rules - just pure business description.\n\nThe main code is just as simple as it could be, throwing the same elucidated exceptions:\n- the same `LoginAlreadyTakenException` as used in registration, just under another condition.\n- the same check of `PasswordCompromisedException` as with registration.\n- additional `PasswordCannotBeReusedException`, specific to profile update.\n\nThis communicates the design much better than what we've seen thus far.\n\nThis is where the power comes from. You don't cram the validation into the framework. \\\nYou broaden the framework so that it embraces your validation in a way that it naturally fits in.\n\nRead more in: [Exceptional Validation](docs/exceptional-validation/exceptional-validation.md).\n\n## Interaction approaches 🔁\n\nThe library provides a few interaction points:\n\n- [Matcher Service](docs/interaction/direct-matcher-service-usage.md) – manual handling\n  (just [as shown](#catch-and-match-));\n- [Bus Middleware](docs/interaction/command-bus-middleware.md) – automated handling.\n\n## Features 💎\n\n`#[Try_]` and `#[Catch_]` attributes allow implementation of very flexible matching rules. \\\nIt's highly recommended to get acquainted with the examples to apprehend the full power of these solutions.\n\nThere are two configuration features:\n\n- [Match Conditions 🖇️](docs/config/match-conditions.md) – determine whether a given exception should match the given\n  property;\n- [Violation Formatters 🎨](docs/config/violation-formatters.md) – represent the exception in a desired format.\n\nThat's really all this library does – matches the exception and formats it (i.e. roasts the fish).\n\n#### Cheat Sheet 📝\n\nFor a cheat-sheet example of configuration, check the following:\n\n```php\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Catch_;\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Try_;\nuse Symfony\\Component\\Uid\\Exception\\InvalidArgumentException as InvalidUidException;\nuse Symfony\\Component\\Validator\\Exception\\ValidationFailedException;\n\nuse const PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Match\\Condition\\Enum\\enum_value;\nuse const PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Match\\Condition\\Uid\\uid_value;\nuse const PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Match\\Condition\\Value\\exception_value;\nuse const PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Match\\Condition\\Validator\\validated_value;\nuse const PhPhD\\ExceptionalMatcher\\Validator\\Formatter\\Validator\\validator_violations;\n\n#[Try_]\nclass ImportProductDto\n{\n    #[Catch_(InvalidUidException::class, match: uid_value, message: 'This is not a valid UUID.')]\n    public string $id;\n\n    #[Catch_(CategoryNotFoundException::class, match: exception_value)] // Message is derived from Exception\n    public string $categoryId;\n\n    #[Catch_(\\ValueError::class, from: ProductStatus::class, match: enum_value, message: 'The value you selected is not a valid choice.')]\n    public string $status;\n\n    #[Catch_(ValidationFailedException::class, from: ProductDescription::class, match: validated_value, format: validator_violations)]\n    public string $description;\n\n    #[Catch_(BackorderDisabledForCategoryException::class, if: [self::class, 'thisProductViolatesBackorder'])]\n    public ?int $backorderLimit;\n\n    /**\n     * Needed in case of deep analysis.\n     * \n     * If this method returns TRUE, the exception is linked to $backorderLimit of *this object*;\n     * otherwise this exception has nothing to do with this object. \n     */\n    public function thisProductViolatesBackorder(BackorderDisabledForCategoryException $exception): bool\n    {\n        if ($exception-\u003ecategoryId !== $this-\u003ecategoryId) {\n            return false; // Backorder configuration of the given category has nothing to do with this category.\n        }\n\n        if (null === $this-\u003ebackorderLimit) {\n            return false; // The product didn't even enable backorder, much less violated it.\n        }\n\n        return true;\n    }\n}\n```\n\n### Deep analysis 🌊\n\nThe matcher automatically picks all nested objects for analysis, provided that they define `#[Try_]` attribute.\n\n```php\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Try_;\nuse PhPhD\\ExceptionalMatcher\\Rule\\Object\\Property\\Catch_;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n#[Try_]\nclass ImportProductBatchDto\n{\n    /** @var ImportProductDto[] */\n    public array $items;\n}\n```\n\nWith nested matching with array properties, property paths are formatted differently.\n\nIn the example above, when the exception is matched, the path would be `items[\u003cindex\u003e].\u003cfiled\u003e`:\n\n- `\u003cindex\u003e` - a particular array index;\n- `\u003cfield\u003e` - a particualr property name of that object.\n\nWhen nesting is really deep, the resulting property path of the formatted violation\nwould include all intermediary properties in its path,\nstarting from the root, down to the leaf item where the exception was actually matched.\n\n#### Need for conditions\n\nFinding a match for the exception in `array` field is like finding your luggage in the _baggage claim_ \\\nwhen everyone else took just the _same alike red backpack_ as you did.\n\n\u003cimg src=\"https://raw.githubusercontent.com/phphd/exceptional-matcher/refs/heads/main/assets/Red Backpack.jpeg\" alt=\"Red Backpack\" width=\"75px\"\u003e\n\nWhen many backpacks are as yours, you must know which one is yours.\n\nSimilarly, finding a match for `BackorderDisabledForCategoryException` across `ImportProductDto[]` must know which\none to relate to, lest it would choose the first object by exception's `class:` condition (i.e. \"grab the first red one\nand go\").\n\nTo find your backpack, you would look at some other characteristics that discern it from the rest, \\\nyea, up to the point of opening it and discovering (or not discovering) your stuff in there.\n\n```php\nif ($exception-\u003ecategoryId !== $this-\u003ecategoryId) {\n    // not my backpack\n}\n```\n\nThat's what `if:` condition is there for – to relate an exception to `$this` particular object.\n\nIn our example, we check that the object's category (e.g. stuff in a backpack) is the same as the one we seek for of the\nexception.\n\nIf the category is different, the object is skipped and another is taken for consideration.\n\nThe same applies to the products that don't enable backorder (`backorderLimit` is not filled):\n\n```php\nif (null === $this-\u003ebackorderLimit) {\n    // It's not my BackorderDisabledForCategoryException! I didn't enable backorder! \n}\n```\n\nThus, we prevent false attribution of the exception to an object that had nothing to do with it.\n\n## Advanced 🛠️\n\n- [Matching multiple exceptions 🕎](docs/multi-match/matching-multiple-exceptions.md) – beyond just one\n  thrown exception.\n\n## Standalone Usage 🔧\n\nIf you are not using a Symfony framework, you can still have a great advantage of this library.\n\nIn your vanilla project, create a Service Container (`symfony/dependency-injection` is required) \\\nand use it to get necessary services:\n\n```php\nuse PhPhD\\ExceptionalMatcher\\Bundle\\DependencyInjection\\PhdExceptionalMatcherExtension;\nuse PhPhD\\ExceptionalMatcher\\ExceptionMatcher;\nuse PhPhD\\ExceptionalMatcher\\Exception\\MatchedExceptionList;\n\n$container = (new PhdExceptionalMatcherExtension())-\u003egetContainer([\n    // These are not used but still required by Symfony DI\n    'kernel.environment' =\u003e 'prod',\n    'kernel.build_dir' =\u003e __DIR__.'/var/cache',\n]);\n\n$container-\u003ecompile();\n\n/** @var ExceptionMatcher\u003cMatchedExceptionList\u003e $matcher */\n$matcher = $container-\u003eget(ExceptionMatcher::class.'\u003c'.MatchedExceptionList::class.'\u003e');\n```\n\nHerein, you create a Container, compile it, and use to get `ExceptionMatcher`.\n\n## Upgrading 👻\n\nThe basic upgrade should be performed by [Rector](https://getrector.com/documentation) using\n`ExceptionalMatcherSetList` \\\nthat comes with the library and contains automatic upgrade rules.\n\nTo upgrade a project to the latest version of `exceptional-matcher`, \\\nmake the following configuration to your `rector.php` file:\n\n```php\nuse PhPhD\\ExceptionalMatcher\\Upgrade\\ExceptionalMatcherSetList;\n\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(ExceptionalMatcherSetList::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 the list of breaking changes and additional instructions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphphd%2Fexceptional-matcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphphd%2Fexceptional-matcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphphd%2Fexceptional-matcher/lists"}