PHPackages                             phphd/exceptional-validation - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Validation &amp; Sanitization](/categories/validation)
4. /
5. phphd/exceptional-validation

ActiveSymfony-bundle[Validation &amp; Sanitization](/categories/validation)

phphd/exceptional-validation
============================

Match exceptions with the properties that caused them

1.5.0(1y ago)37.7k—3.6%2[2 issues](https://github.com/phphd/exceptional-validation/issues)MITPHPPHP &gt;=8.1CI passing

Since Feb 7Pushed 1mo agoCompare

[ Source](https://github.com/phphd/exceptional-validation)[ Packagist](https://packagist.org/packages/phphd/exceptional-validation)[ RSS](/packages/phphd-exceptional-validation/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (8)Dependencies (17)Versions (28)Used By (0)

Exceptional Matcher 🏹
=====================

[](#exceptional-matcher-)

💼 Match the Exceptions to the Object's Properties

[![Build Status](https://camo.githubusercontent.com/59b50254132c9427cbe02be1f103662b26057af9e0c1ec6aba6ea16c5f35d445/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f70687068642f657863657074696f6e616c2d76616c69646174696f6e2f63692e79616d6c3f6272616e63683d6d61696e266c6f676f3d676974687562266c6f676f436f6c6f723d3032344331412663616368655365636f6e64733d33363030)](https://github.com/phphd/exceptional-validation/actions?query=branch%3Amain)[![Codecov](https://camo.githubusercontent.com/7e7230fca643ac043558b6babc9b0209fcd4e392c5e85ab46060b6dc9b4ed0f4/68747470733a2f2f636f6465636f762e696f2f67682f70687068642f657863657074696f6e616c2d76616c69646174696f6e2f67726170682f62616467652e7376673f746f6b656e3d475a525857595435355a)](https://codecov.io/gh/phphd/exceptional-validation)[![PHPStan level](https://camo.githubusercontent.com/2d155bc537d2460bac2cc7e5f27873be97f31716ee144065530bd1385f7e5d97/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64796e616d69632f79616d6c3f75726c3d68747470732533412532462532467261772e67697468756275736572636f6e74656e742e636f6d2532467068706864253246657863657074696f6e616c2d76616c69646174696f6e2532467265667325324668656164732532466d61696e2532467068707374616e2e646973742e6e656f6e2671756572793d2532342e706172616d65746572732e6c6576656c266c6162656c3d5048505374616e2532306c6576656c26636f6c6f723d2532333531364342332663616368655365636f6e64733d333030266c6f676f3d646174613a696d6167652f706e673b6261736536342c6956424f5277304b47676f414141414e5355684555674141414377414141416c43414d414141414f56667637414141427856424d564555414141425261374a5161374e5161374e5161374e51624c4e5161374a5161374a5161374e5161374a5161374e5161374e5161374e5161374e51624c4a5161374e52624c4e5161374e5161374e5161374e5161374a5161374a5161374e5161374e5161374e5261374e5161374d494351736a4878395161374e5161374e5161374a5161374e5261374a52624c4a5161374e52624c4e5161374a5161374e52624c4e5261374e51624c50713675726e35756266333939756247787161476c48526b5a5161374e5161374e52624c4e51613750362b76716f714b696c704b53586c70633150566f6b4d4641734c437931744c524b59714a79636e49314d7a51794d44474d693474356548676a4879425161374e51624c4a5261374e5161374a516137494442416452624c4e5161374a5161374e52613750372b2f76523064484a794d692b76723643674941724f5638704e3178625731744f546b354e545531415145447a382f4f74724b77754d6b515445784e565656554a4452554e455230414141424a596149414141414141414252624c4931526e556a48683841414141414141424b595a346a48683952624c494141414141414141724c54736a4879416a48694169486838694869417350474d4141414252624c4d414141442f2f2f39516137496a4879442b2f76344341775250617241454267704f614b784d5a716b7751577757486a495046434a4557356337543455674b3067544769776b4a43584878735a4858707043574a464256496f745057596d4d6c51634a54345a4954676348427843574a4f526b5a453953335536526d6f6d4d3155784e6b306f4a7a4334514b766e4141414164485253546c4d412f51557a2b6877584476445879735954374a78564f794830364f666a314b756d6b517a2b2f66627a33383631734a39396458464b4e79662b2f7637392f663235684545492f76372b2f76372b2f7633392f6633392f507a73777236596934467a5a3246464c50372b2f76372b2f76372b2f76372b2f6633392f667a79372b62677a722b38764c7972693331786156395a56536f704978494c433474632b424d4141414c685355524256446a4c685a4a6c642b4a514549596e4364596952597673496f56535374336433646264335833334a6946516f627231396632394f397a63424d37706f6674386d74783547435a764c6d696b2f46374f4536696e6463686b4d744e694d57376e6f676e3173496978686c417341676a4236676a485253716d6e534259364b456859537431363331454a5a5675347769446d785363374b486156585364466b326f784b70493243717a79706657334c53484865553244342f7a5277636254436d6c676d33534757507132686c524a62393355712f687164784f4b4b76486f6b35323761533941496a4c5465755637594c31376472564733542b5364746253444e49532f6d776f485a33535a4c534d4a7a46325875615a4e434b4f67437a6e5661374b4f77305352536c42782f794d6e4f4e5565306465616853422f2f475956386c706248333075413553576e4776396c6761664f5657753443544b7679756969324b6c4c54534361546156616b786d58636d736935465278766d797275595656664437732f634749474f587442556b5a46635576652f3776546c7950474e6b304f6745664e446266736c705457544945654c45547869507a367566746e6c585056614c49564450726b586b6c716f5a4e78397847635447535a794b524572675a4f332f6b36376a794b626f73694e57445742307a424e58545a726862396f7268385870496178385a7664556e4b4f4b62786e575551737571334446677747376a305141504c655243583270594c62542b6d3639566b507a68597459584464693471714e3463777957792b775370537371765530516a7954344b357245756f6a4c514d6e7a336a6f68634c6779326d363042633079584254426a6470776c376f674e625a66637569737944645a59472f4c4c2b7676786852737154356d4d51716a6a59565a7a317a6570776158415a5355363877426738336c4e7377624f577a574832765048392f503971464b695041696337735a345144706d3859687a352b364a346950676655516e69633234396d425a416771667845397066497144333842693062585973476571494253504578693175462f773834767367792b514b4d704232724d353345534f4f4979617138623378505232446b4a323359316f665a757a33677846544747432b4a7a305a77787542736f516f4833335043366b595465566b35634d366a534875566174694677485a516d792f3635304f64674e3438764c37346149696c756448426167504d397535306770646165344838396d2b6d52534a41476e38476b694d2f48536f4b666d373454542b504471505853774c787475352b482f644337454b364b544d2b6d79776a2b67713776704e4b5430355141414141424a52553545726b4a6767673d3d)](https://github.com/phphd/exceptional-validation/blob/main/phpstan.dist.neon)[![Psalm level](https://camo.githubusercontent.com/f989de5e72b9d62deb9b2cbb6bd531f35fd8cc2831d01bfca90e23a2d40f075d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64796e616d69632f786d6c3f75726c3d68747470732533412532462532467261772e67697468756275736572636f6e74656e742e636f6d2532467068706864253246657863657074696f6e616c2d76616c69646174696f6e2532467265667325324668656164732532466d61696e2532467073616c6d2e786d6c2671756572793d2532462532462534306572726f724c6576656c266c6162656c3d5073616c6d2532306c6576656c266c6f676f3d64617461253341696d616765253246706e672533426261736536342532436956424f5277304b47676f414141414e5355684555674141414341414141416743414d414141424570497247414141417956424d56455841774d437572713745784d537973724c47787362763725324238784d544837253242253246763925324666337a38253246507a6d49763625324276723339253246666f364f6a783866487036656e31396658743765337236253242757472613225324676372532467772715837556a7237536a477a73374f34754c693274726138764c7977734c4458313963304e445468346548306d6f7a7235253242664f7a7336367572717672362532466a342532424f2532427672356a5932506b354f544b79737249794d6a4477384f556c4a534e6a593153556c4a51554641374f7a76643364335330744b6b704b53676f4b4341674942726132745a57566c4a53556c43516b4c5a32646e46786357717171716e703665626d3574376533746358467862573173253246507a395a616f55634141414142585253546c5075377143674357324d45436b41414147425355524256446a4c66593448636f4d774545586c676d774649524278456a654d613344764e543235253246364879467a514d385a67386964586631574f416c5975736c76434d6e5a78707a34706c56747a4b616935795732534d37764d4e786c71566169576d617572667667586843746d4f3175746f51744549646755624a646e25324246796336733769484d4c417a76473973336545787132517979417269774150626a73373731684c473241674e56436e704758317a43484b6962446d484d4c4a70436b45617068634f4959372532426d664e6c4d6d326b776f362532426658493852425852543661434a7a3350737a37707a2532426643412532464b41574d65554941474d543769253246394a4e5a674c7a416159516d7168414c446f36425150546545507543786c684e316851786a545639593645527836487a59676d4445554377347647625249676e4979696c424a5a7a4e4d4a6f7956656855414146676b715a5138437867376858686a71724b32565a464646615078506b4b59515a395561774d67516271744f5031335243516a3547304259326c74596d5a6e73496d6e693867536261724b323172253246585444587134494d4858767539337533637058564e3650694342514839464b6778523352774246304d3264496d484725324652634d475433376e39414b497763317946635539787348785a59715259367559533145697558437665354645726c582532424752506d7a6e504d6f6b4141414141456c46546b5375516d43432663616368655365636f6e64733d33303026636f6c6f723d253233663735633331)](https://shepherd.dev/github/phphd/exceptional-validation)[![Type coverage](https://camo.githubusercontent.com/3fff651504cabe26b3be44c64f623fb7c733cbf3fec0af525df1f70f5d956d49/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64796e616d69632f72656765783f75726c3d687474707325334125324625324673686570686572642e6465762532466769746875622532467068706864253246657863657074696f6e616c2d76616c69646174696f6e267365617263683d253343746825334525354225354377253543732535442a636f7665726167652e2a25334625334325324674682533452e2a2533462533437464253345282535436425324229282e253543642537423125374429253346253543642a2532352533432532467464253345267265706c6163653d2532343125323526666c6167733d6973267374796c653d666c6174266c6f676f3d64617461253341696d616765253246706e672533426261736536342532436956424f5277304b47676f414141414e5355684555674141414377414141417343414d414141417057716f7a41414142626c424d5645554141414141414141414141414141414142415145424151454c437773414141414141414141414141474267594141414141414141414141414141414141414141414141414141414141414141414141414141414149434167414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414144253246253246253246384141414439253246663046425158333925324666382532465077514542415546425430395051674943415947426a7136756f364f6a726a342532425064336432253246763725324230744c536d70715a7662323839505433783866486934754b78736247536b704a30644851774d4441744c53334b79737245784d5338764c7934754c69676f4b434e6a59324969496946685956376533746b5a475266583139635846784d544578425155456e4a7963644852304a43516e3239766273374f7a6533743761327472583139664f7a73374878386642776347747261326f714b695a6d5a6c3364336457566c5a4a53556b794d6a49714b69726e352532426652306447377537756a6f364f42675946555646524652555835253242666e313966586c3565585430394f32747261646e5a31253242666e35726132744f546b363444327732414141414c6e5253546c4d412532426a7a39392532465825324236736f632532422532464675554259473271746c563048333773537a6858786753436f5244516f453464504f655441754937796a6c343879655136303951414141796c4a524546554f4d754e6c576458346c4151686d384948565255374c72323775596d51434430587055694861556a78643531393925324676425149456a4c4c766c357a377a6e506d54695a7a4a6f42587372564e384a253242616c517178725a57704d64546b784d79735948555253336b384b626c30566f434f613925324278617057516c6f7445676171474944526e4a6b79456a70495a666e5a25324679786b306d69735646394752766c49784777754e253242516b253242646d4d36557943253242364d61303825324273724f36576343784d38387377746a644a3753394b35756f59503170774c7434253246566e4e5a734c6b734d53512532464c6b73617270436d64384c357075253242654c61694b6a4f42706b6e7841354c4779493046594473434d6d253242734a61704c73755825324237443079306a757525324231614c6b6b745859645a455846584d56674b466a556f3834307425324675774b6b346137616c724a7575505278446245323169636a424870794d75496a6125324630577561354946346f48723554492532462532427a715052366c6e70472532424a444965743944595764634c645939764f315a6b7441364c757257654a64326c386b69444f526f4d762532426d73363643534a6b364551795a6825324655326357365578333042656d3245796968647a526d704a33556b3464306a55546e6f4a4e71707a4532496558564d57454b706d344e464d55347a4a3247574c4d376b77672532426f73385169253242514b507a7164416475704d786d6863725645253242753744656525324278394e7058796b344473496246454e765870794669653041644d586a66593753504579444475417973596947436f3150496b59636279654f7234454234786331735a6761734c732532464e664962764162435434673651506a32416e53354f514a745541514355596a633351523343617765456a7959493639777258654a6442417545317047696339365174543069513734626d3044776a4e7a434e5933746a2532424f49554f694353363525324677625148542532467030775457317149596e253242337444444d2532464a495425324268416d77334f486f5455386c414a6b375465574a495666773332426547686b317474447452586e4c5974253242437a514359363177363746373463366e46554d2532427953397a676176424f6462535448637a6e717664574f734758443469616175675547546449596b5661476e64464651333463664f6c666b4c464c513245664231744661744456756951364472355354414a5757773579544d6e6e6b716e253242336a4264253246677a726e36524447306e7a513936626d467735324b444c69757a66734a3466315a7a36416f726a6454435159456b716972253246776231483539704a676444253246767a74337a6c5741546e367a7a62503746612532466458324e7861344746524b5a4c6d726235674c65765a25324230756c677437636d4663445871334d34324963777779325a7a536f6433354d69496b70795172345267664b33526d4225324268427632737025324671415842327170534b514e6a744b4b43554b4c6b25324253482532464125324225324251486d70764d64253242414141414141456c46546b5375516d4343266c6162656c3d54797065253230636f76657261676526636f6c6f723d6638663866382663616368655365636f6e64733d33363030)](https://shepherd.dev/github/phphd/exceptional-validation)[![Packagist downloads](https://camo.githubusercontent.com/6a8eeb30c2a44fea6816880f09ac89ee0fec6ad7cb763e3d8dd297ed32b3a8dd/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f70687068642f657863657074696f6e616c2d76616c69646174696f6e3f6c6f676f3d64617461253341696d616765253246706e672533426261736536342532436956424f5277304b47676f414141414e5355684555674141414377414141417343414d414141417057716f7a4141414325324656424d5645554141414146425156565746384442415141414141424151454d444179317538736c4a696f4442415143415145414141414141414141414143506b3643446734524a533149365045466a5a3238754c7a4d6d4551496c4477414a4341674b43416344424151414141414241514541414141414141414141414141414142786458346f4467416a44674166487945634853415846786757466859454241555a475277644869414a424145484167414f4467384542415545425155414141424d544579596e6174786459434a6a7071466970563967597768496955644852357363486c6d616e4e386749736644414275636e74685a47306248423544525573764d5455544267425757574244526b744d5431595245524d6b4451416b4a536b5646686772454141334f543545526b77724c444253565677754d444d714551454741514166487949524277414742675970464159734541414b4367734e445138784d7a676d4a79734f447841544277414943416b5a47683066487949354f3041724c54455846786f484277644f546b375a3450505a3466546234765851312532426e57336525324655322532423343794e6e63342532466259332532464c583376444e314f584d307553556d615a66596d7261346658497a25324248477a4e326c71376d424f41445432757a457939792532467874616f727232576d3668253242676f3162586d5a5155316f324f443043415144487a742532423977395335767325324278743861646f3743616e36794d6b4a31736348686e616e4e685a47356459476847535538253246516b68664b4144253246253246253246253246533265724b304f4b32764d757674635738764c7969714c6565704c4b496a4a6545694a5342685a4631655952776448356b614846535656784a5331464452557325324251455539506b49774d54596c4a697053497742454851446435506677385044553225324237503165664479746d253242784e4b3777644735774e437573384f73737347546c364f4a6a5a6c3766347036666f683566495a32653456253242666f4267596d566859574a575757464f5546684c546c5566487945554642594f44673444426768744c6742584a41443239665873374f793075636d687037536f714b69516b4a4632656f4a346558707162585a79636e4950516c314b54464e4f546b38464e55394b53303436504545304e446368496955594778384345426c774d41426b4b67425a4a67443625324276726435666666333925324655307448497963697373634141654c657973724f73724b774763716d6d704b4d45616143516b3532596d4a6b41595a574869597478645941475433596d55327356546d736654575a64585630414f566779536c6456565659444b3049624d4430414a5473734c6a49714b696f70497957474f7742384e6742344e41424c494142424751413847514133466741734567436f5761372532424141414159335253546c4d414366355245513448253246706951676b517449663725324237656a6d31617161696e466a58546b7a4a527357253246763338734b61696e5a7951666e357a626d744e4e51723925324225324672342532425066333976627a38253242377537757a7236253242626a344e25324664323958563073374d7938764b7972323975626934733679706f3579636d4a4f4c65484e73586738724d45703941414144253246456c4551565134793657555a584462554243456c6152746d71544d7a4d7a4d7a4d7a4d3754325a3261345a516d59376e4959355a575a7555325a6d5a6d626d54695862735631776e4a6e756a7a656e3061656475355630324c25324655656c3572724a41714e6e644d2532425a457a5378614b39576e514c6b516975444849727a42773854412532424541727447466930414b706b6b30596c7342494e347743524d4b4a6f797a5675355145744d614f63503764337656356173417342557868547471566638536d567168543777335671442532466c615a51784653453079736d77775835746d6f45533139326448525764572532465933314b31754b7030383825324656344d514e58784b4351734e37474a6b3561795662457664494237393055474c39735769554f3069524769335325324256304245684b67764564486277396e577173364c45514465345a6c2532425a484145253246575a53774b545a3234774c63316a4e547425324648675963587878626a51754b524d64546534753453475947483673684f4c396c786274466c6f67376b4839253242363558486f625561356149327268676d743078736e703031655876763738337450444f684a656d4b6f25324664757a5256523735584652595537665961704d7778377a253246364b75624a352532426434345a7a5a524a51767a743535386e64557877797852575658484331326a6843454a314a79336a7a6374664f58647630687254546d646d616e57386666736a6167674168656d4d584848534564465a757750335037644a6f4e6c444a7a4d4b794e4a727a353764534c4773494830467874356a314f50425845796c45626c487a34704439645165664d616249634669654c5561775a4c494c6e68354f3345326941557057344179365343616c6b6f3275354231694932436b6342437725324274664b5a3276315a414e516c54534956464e69544d6e684957775743514d7a546b3042686f464e6c47316135734e5656517741766a476559393668734c42746e5032346b68554e465056696f7051346d323667497964656f565a74504a4e745377725a424d796c4f315253453565735765587a76377947386151546733636959764e71414a647a634d625241396d4a74684b56395848416c52586b4a567233494f4b467635304c31596e344145744f4c596f313051566b4f6a536e637974664d543047474d6259694e6672376243597272654573616962446d53614e36544b434a6865332532466b372532423831715071375555745774694d32506b614d4e786f4c746c7655377a4374784a4e7a435a5346755a63784e4e65664d626c35363779626c51736477584550592532427031725745544a464e77764d364a4a4565775031627439534c6d55694a654d4f4152776c6479576a4c52446d525925324232462532427131696c6833316236576f6f6a6a536a697049623371314c446862717257514b6565465947344d79506369536a7173664e555134346154515848476c5930625747783830316a496d53354d365851764f56696f6436586b7631325343784342324243464c6b61506b6b7a4b4f4366466d776a4265433237356e35584b41556c55777a77726755554867367873766f4775506878494444692532424b46614247716353416f59726b704c584243504175515156763534433235477734766c6930674245583447325854347a6e493169566e7072416b583673356755754d6d513742304359546b576372472532464e764d424e36395a4a4331253242317a7278536c4b48354f63474c63554264717a586e61303565336958726854706a69785a4d4231373839486d334e5366336536353164323651462532426652412532467638794d335a253246655843786278756c61703736626b57466e6870766c326a66444376716a36745973574b465370554746385a25324278253246394169706730717a574e5659394141414141456c46546b5375516d434326636f6c6f723d2532334632384431412663616368655365636f6e64733d33363030)](https://packagist.org/packages/phphd/exceptional-validation)[![Licence](https://camo.githubusercontent.com/ba43eabb9e81fa3246d6cb25f53cf71dd79df04d11436ef64e6b2af08753dabc/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f70687068642f657863657074696f6e616c2d76616c69646174696f6e2e7376673f636f6c6f723d334441363339)](https://github.com/phphd/exceptional-validation/blob/main/LICENSE)

A library that matches exceptions against object properties.

**No** longer do you need any **peripheral validation** for your objects
that doesn't really dig down into the domain.

Instead, build full-fledged declarative feature-rich domain **validation** with full **use of exceptions**
and let this library to ***relate*** these **exceptions *to*** their **originator fields**.

Eventually, you can return a normal validation failed response.

A Validation Library? 🤔
-----------------------

[](#a-validation-library-)

It's not a validation library. Not ever intended to be.
It doesn't provide validation rules, constraints, or validators.

It is **exception handling** library, specifically featured with usefulties for validation.

You can validate business logic with any third-party library (or even plain PHP),
while the library will be ***correlating*** these **validation exceptions** to the specific properties
whose invalid values caused them.

It's not a strict requirement to use Symfony Validator as a validation component,
though this library integrates it well.

Why Exceptional Validation? ✨
-----------------------------

[](#why-exceptional-validation-)

Ordinarily, validation flows through two different layers:

- HTTP/form level;
- domain layer.

It leads to duplication and potential inconsistencies of validation rules.

### Traditional Validation 🕯️

[](#traditional-validation-️)

The traditional validation uses an attribute-based approach,
which strips the domain layer from most business logic.

Besides that, any custom validation you'd normally implement in a service
must be wrapped in a custom validator attribute and moved away from the service.

It's all for the sake of being able to display a nice validation message on the form.

Thus, the domain services and model end up naked,
all business rules having been leaked elsewhere.

### Exceptional Validation 💡

[](#exceptional-validation-)

On the other hand, it's a common practice in DDD for domain objects to be responsible for their own validation rules.

- `Email` value object validates its own format and naturally throws an exception that represents validation failure.
- `RegisterUserService` normally verifies email is not yet taken and naturally throws an exception.

That is the kind of code that utterly expresses the model of the business,
which should not be stripped down.

Yet, with a domain-driven approach, it's not possible to use standard validation tools,
as these drain domain from all logic.

How then do we show contextual validation errors to the users?
It's a task of relating a thrown exception with the property which value caused this exception.

To return a neat json-response with `email` as a property path and validation error description,
it's necessary to match `EmailAlreadyTakenException` with a `$email` property of the original `RegisterUserCommand`.

This is what Exceptional Validation was designed for.

Throwing exceptions like `EmailValidationFailedException` and matching them with the particular form fields as `$email`,
you maintain a **single source of truth** for the domain validation logic.

Domain enforces its invariants via exceptions in value objects and services,
while this library ensures that these validation failures will properly match form fields and appear correct in your API responses or forms.

### Key takeways

[](#key-takeways)

Exceptional Validation:

- Eliminates duplicate validation across HTTP/application and domain layers;
- Keeps business rules where they belong — in the domain;
- Makes validation logic easily unit-testable;
- Reduces complexity of nested validation scenarios;
- Eliminates the need for validation groups and custom validators.

Installation 📥
--------------

[](#installation-)

1. Install via composer:

    ```
    composer require phphd/exceptional-validation
    ```
2. Enable bundles in the `bundles.php`:

    ```
    PhPhD\ExceptionalMatcher\Bundle\PhdExceptionalMatcherBundle::class => ['all' => true],
    PhPhD\ExceptionToolkit\Bundle\PhdExceptionToolkitBundle::class => ['all' => true],
    ```

    > Note: `PhdExceptionToolkitBundle` is a required dependency
    > that provides exception unwrapping needful for this library.

Get Started 🎯
-------------

[](#get-started-)

Mark a message with `#[Try_]` attribute.
It's used by matcher to include this object for processing.

Define `#[Catch_]` matching rules for your properties.
These declaratively describe what properties what exceptions correlate with:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;

#[Try_]
class RegisterUserCommand
{
    #[Catch_(LoginAlreadyTakenException::class, message: 'auth.login.already_taken')]
    public string $login;

    #[Catch_(WeakPasswordException::class, message: 'auth.password.weak')]
    public string $password;
}
```

For example, we say that `login` property is related to `LoginAlreadyTakenException`,
while `password` - to `WeakPasswordException`.

Matching takes place when the matcher is used:

```
use PhPhD\ExceptionalMatcher\ExceptionMatcher;

/** @var ExceptionMatcher $matcher */
$matcher = $container->get(ExceptionMatcher::class.'');
$command = new RegisterUserCommand($login, $password);

try {
    $this->service->register($command);
} catch (DomainException $exception) {
    $violationList = $matcher->match($exception, $command);

    return new JsonResponse($violationList, 422);
}
```

Each exception, when matched, results in a `ConstraintViolation` object,
which contains a property path, and a message translation.

You can serialize this violation list into a json-response or render a form with it.

> Note that the default messages translation domain is `validators`,
> inherited from `validator.translation_domain` parameter.
>
> You can change it by setting `phd_exceptional_matcher.translation_domain` parameter.

How is this different from a standard validation? ⚖️
----------------------------------------------------

[](#how-is-this-different-from-a-standard-validation-️)

Conceptually.

If you're wondering why we wouldn't use "normal" validation asserts right in the command,
I'll say to you that this is not always best / convenient.

For example, let's take the same `RegisterUserCommand` as used before.

A comparison of the approaches would look something like this:

```
+#[Try_]
 class RegisterUserCommand
 {
-    #[AppAssert\UniqueLogin]
+    #[Catch_(LoginAlreadyTakenException::class, message: 'auth.login.already_taken')]
     public string $login;

-    #[Assert\PasswordStrength(minScore: 2)]
+    #[Catch_(WeakPasswordException::class, message: 'auth.password.weak')]
     public string $password;
 }
```

The main difference between the two is that standard validation runs before your actual business logic.
This alone means that for every domain-specific rule like "login must be unique" it's necessary to create
a custom validation constraint and a validator that implements this business logic.

Thereby, the main problem with the standard approach is that domain leaks into validators.
That code, which you would've normally implemented in the service, you are obliged to wrap into the validator.

One more point is that oftentimes there are multiple actions that use the same validations.

For example, login uniqueness is validated both during registration and during profile update.
Even though a "login is unique" rule is conceptually obvious,
a validator approach is fraught with problems to check that a user's own login isn't taken into account when validating.

Exceptional validation doesn't force you to write business logic in any validators.
Instead, you can throw an instance of exception in whatever scenario you would like to,
and then the library will retroactively analyse it.

Another example is a password validation, which's used both during registration and during password reset.
Using the validation attributes results in duplicated asserts between the two,
while this business conceptually belongs to `Password`,
which most properly would be represented as a value object, used in both actions.

With exceptional validation you just write business logic in your domain and then retroactively relate violations.
Retroactively — after your business logic has worked out.
Representation of the errors to the user is separate from the business logic concern which's managed by this library.

Finally, this approach gives a lot of flexibility,
removing the need for custom validators, validation groups, duplicate validation rules,
allowing you to keep the domain code in the domain objects,
resulting in a better design of the system.

Focus on the domain and let the library take care of the exception representation:

```
// RegisterUserService

if ($this->userRepository->loginExists($command->login)) {
    throw new LoginAlreadyTakenException($command->login);
}
```

Direct Usage 🔌
--------------

[](#direct-usage-)

> It's possible to use features of this library without necessarily depending on the frameworks. See [Standalone Usage](#standalone-usage-) section.

If you're using Symfony, you can check what exception matchers are available using this command:

```
bin/console debug:container ExceptionMatcher
```

This should provide you with a list, similar to this:

```
[0] PhPhD\ExceptionalMatcher\ExceptionMatcher
[1] PhPhD\ExceptionalMatcher\ExceptionMatcher

```

These matchers format the Exception to their respective format, specified as a generic parameter.
Format could be `ConstraintViolationList`, or `MatchedExceptionList`, or anything else dumped by the command.

Therefore, you can inject the wanted service into your own code:

```
use PhPhD\ExceptionalMatcher\ExceptionMatcher;
use Symfony\Component\Validator\ConstraintViolationListInterface;

class SignDocumentActivity
{
    public function __construct(
        /** @var ExceptionMatcher */
        #[Autowire(service: ExceptionMatcher::class.'')]
        private ExceptionMatcher $exceptionMatcher,
    ) {
    }

    public function sign(SignCommand $command): string
    {
        try {
            return $command->businessLogic($this);
        } catch (DomainException $e) {
            throw $this->failure($e, $command);
        }
    }

    private function failure(Throwable $e, SignCommand $command): Throwable
    {
        /** @var ?ConstraintViolationListInterface $violationList */
        $violationList = $this->exceptionMatcher->match($e, $command);

        if (null === $violationList) {
            return $e;
        }

        return new ApplicationFailure('Validation Failed', $this->encode($violationList), previous: $e);
    }
}
```

In this example, we use `ExceptionMatcher` to relate the exception to some property of the `$command`,
which produces `ConstraintViolationListInterface` that can be used however you want to.

Usage with Command Bus 📇
------------------------

[](#usage-with-command-bus-)

If you are using Symfony Messenger as a Command Bus,
it's recommended to use this package as [Symfony Messenger Middleware](https://symfony.com/doc/current/messenger.html#middleware).

> If you are not using `Messenger` component, you can still leverage features of this library,
> as it provides a rigorously structured set of tools w/o depending on any particular implementation.
> Installation of third-party dependencies is optional — they won't be installed unless you need it.

Add `phd_exceptional_validation` middleware to the list:

```
 framework:
     messenger:
         buses:
             command.bus:
                 middleware:
                     - validation
+                    - phd_exceptional_validation
                     - doctrine_transaction
```

Once you have done this, the middleware will take care of exception capturing, matching, and re-throwing `ExceptionalValidationFailedException`.

You can use it to catch and process it:

```
$command = new RegisterUserCommand($login, $password);

try {
    $this->commandBus->dispatch($command);
} catch (ExceptionalValidationFailedException $exception) {
    $violationList = $exception->getViolationList();

    return $this->render('registrationForm.html.twig', ['errors' => $violationList]);
}
```

This exception just wraps respectively created `ConstraintViolationList` with all your messages and property paths.

### How it works ⚙️

[](#how-it-works-️)

Primarily, it works as a [Command Bus](https://symfony.com/doc/current/messenger.html#multiple-buses-command-event-buses)middleware that intercepts exceptions and performs their matching to object's properties by an exception matcher, eventually formatting matched exceptions as standard [SF Validator](https://symfony.com/doc/current/validation.html)violations.

> Besides that, `ExceptionMatcher` is also available for direct use w/o any middleware.
> You can reference it as `ExceptionMatcher` service.

This diagram represents the concept:

[![Exceptional Validation.svg](https://raw.githubusercontent.com/phphd/exceptional-validation/refs/heads/main/assets/Exceptional%20Validation.svg)](https://raw.githubusercontent.com/phphd/exceptional-validation/refs/heads/main/assets/Exceptional%20Validation.svg)

Standalone Usage 🔧
------------------

[](#standalone-usage-)

If you are not using a Symfony framework, you still have a great opportunity of taking advantage of this library.

You can create a Service Container (`symfony/dependency-injection` is required)
and use it to get necessary services:

```
use PhPhD\ExceptionalMatcher\Bundle\DependencyInjection\PhdExceptionalMatcherExtension;

$container = (new PhdExceptionalMatcherExtension())->getContainer([
    'kernel.environment' => 'prod',
    'kernel.build_dir' => __DIR__.'/var/cache',
]);

$container->compile();

/** @var ExceptionMatcher $matcher */
$matcher = $container->get(ExceptionMatcher::class.'');
```

Herein, you create a Container, compile it, and use to get `ExceptionMatcher`.

Features 💎
----------

[](#features-)

`#[Try_]` and `#[Catch_]` attributes allow implementing very flexible matching rules.
It's highly recommended to see the examples below to know the power of these solutions.

### Match Conditions 🖇️

[](#match-conditions-️)

#### Exception Class Condition

[](#exception-class-condition)

A bare minimum condition.

Matches the exception by its class name using `instanceof` check,
acting similarly to `catch` operation.

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;

#[Try_]
class SubmitOrderCommand
{
    #[Catch_(OrderSubmissionPeriodClosedException::class)]
    public string $id;
}
```

#### Origin Source Condition

[](#origin-source-condition)

Filters the exception by its origin place,
specifying whence it was to be raised from (class name and method name).

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;
use Symfony\Component\Uid\Uuid;

#[Try_]
class ConfirmParcelDeliveryCommand
{
    #[Catch_(\InvalidArgumentException::class, from: [Uuid::class, 'fromString'])]
    public string $uid;
}
```

In this example `InvalidArgumentException` is a generic one, possibly originating from multiple places.
If you want to catch only those that belong to `Uuid` class, specify `from:` clause with class and method name.

Therefore, Exception Matcher will analyse the exception trace
and check whether the exception was originated from that origin `from:` place.

#### When-Closure Condition

[](#when-closure-condition)

`#[Catch_]` attribute allows to specify `if:` argument with a callback function to be used to determine
whether particular instance of the exception should be matched with a given property or not.
This is particularly useful when the same exception could be originated from multiple places:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;

#[Try_]
class TransferMoneyCommand
{
    #[Catch_(CardBlockedException::class, if: [self::class, 'isWithdrawalCard'])]
    public int $withdrawFromCardId;

    #[Catch_(CardBlockedException::class, if: [self::class, 'isDepositCard'])]
    public int $depositToCardId;

    public function isWithdrawalCard(CardBlockedException $exception): bool
    {
        return $this->withdrawFromCardId === $exception->getCardId();
    }

    public function isDepositCard(CardBlockedException $exception): bool
    {
        return $this->depositToCardId === $exception->getCardId();
    }
}
```

In this example, once we've matched `CardBlockedException` by class, custom closure is called.

If `isWithdrawalCardBlocked()` callback returns `true`, the exception is matched for `withdrawalCardId` property.

Otherwise, we analyse `depositCardId`, and if `isDepositCardBlocked()` callback returns `true`,
then the exception is matched for this property.

If neither of them returned `true`, then exception is re-thrown upper in the stack.

#### Uid Condition

[](#uid-condition)

You can match Symfony's `InvalidArgumentException` from the `Uid` component using `InvalidUidExceptionMatchCondition`:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;
use Symfony\Component\Uid\Exception\InvalidArgumentException as InvalidUidException;

use const PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Uid\uid_value;

#[Try_]
class ApproveVerificationCommand
{
    #[Catch_(InvalidUidException::class, match: uid_value)]
    public string $id;
}
```

This condition compares exception's `invalidValue` with the property value.
If they are equal, the exception is matched for this property, otherwise other properties are analysed (if any).

Only string property values are allowed for this condition.

> This condition is registered only when `symfony/uid` is installed and exposes `Symfony\Component\Uid\Exception\InvalidArgumentException::$invalidValue`.

#### ValueException Condition

[](#valueexception-condition)

Since in most cases matching conditions come down to the simple value comparison, it's easier to make the exception implement `ValueException` interface and specify `match: ExceptionValueMatchCondition::class` instead of implementing `if:` closure every time.

This way it's possible to avoid much of the boilerplate code, keeping it clean:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;

use const PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Value\exception_value;

#[Try_]
class TransferMoneyCommand
{
    #[Catch_(CardBlockedException::class, match: exception_value)]
    public int $withdrawalCardId;

    #[Catch_(CardBlockedException::class, match: exception_value)]
    public int $depositCardId;
}
```

In this example `CardBlockedException` could be matched either with `withdrawalCardId` or with `depositCardId`,
depending on the `cardId` value from the exception.

And `CardBlockedException` itself must implement `ValueException` interface:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Value\ValueException;

class CardBlockedException extends DomainException implements ValueException
{
    public function __construct(private Card $card)
    {
        parent::__construct('card.blocked');
    }

    public function getValue(): int
    {
        return $this->card->getId();
    }
}
```

#### ValidationFailedException Condition

[](#validationfailedexception-condition)

This one is very similar to `ValueException` condition
with the difference that it integrates Symfony's native `ValidationFailedException`.

Specify `validated_value` match condition to compare property's value against exception's validated value:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;
use Symfony\Component\Validator\Exception\ValidationFailedException;

use const PhPhD\ExceptionalMatcher\Rule\Object\Property\Match\Condition\Validator\validated_value;
use const PhPhD\ExceptionalMatcher\Validator\Formatter\Validator\validator_violations;

#[Try_]
class RegisterUserCommand
{
    #[Catch_(ValidationFailedException::class, from: Password::class, match: validated_value, format: validator_violations)]
    public string $password;
}
```

### Violation Formatters 🎨

[](#violation-formatters-)

There are two main built-in violation formatters you can use: `DefaultExceptionViolationFormatter` and `ViolationListExceptionFormatter`.

If needed, create a custom violation formatter as described below.

#### Main

[](#main)

`MainExceptionViolationFormatter` is used by default if another formatter is not specified.

It provides a basic way of creating a `ConstraintViolation` with these parameters:
`$root`, `$message`, `$propertyPath`, `$value`.

#### Constraint Violation List Formatter

[](#constraint-violation-list-formatter)

`ViolationListExceptionFormatter` allows formatting the exceptions
that contain a `ConstraintViolationList` from the validator.

Such exceptions should implement `ViolationListException` interface:

```
use PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\ViolationListException;
use Symfony\Component\Validator\ConstraintViolationListInterface;

final class CardNumberValidationFailedException extends \RuntimeException implements ViolationListException
{
    public function __construct(
        private readonly string $cardNumber,
        private readonly ConstraintViolationListInterface $violationList,
    ) {
        parent::__construct('Card Number Validation Failed');
    }

    public function getViolationList(): ConstraintViolationListInterface
    {
        return $this->violationList;
    }
}
```

Then, specify `ViolationListExceptionFormatter` as a `format:` for the `#[Catch_]` attribute:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;

use const PhPhD\ExceptionalMatcher\Validator\Formatter\ViolationList\included_violations;

#[Try_]
class IssueCreditCardCommand
{
    #[Catch_(CardNumberValidationFailedException::class, format: included_violations)]
    private string $cardNumber;
}
```

Thus, once `cardNumber` property gets a hold of `CardNumberValidationFailedException`,
formatter makes sure that a proper representation of this exception in a `ConstraintViolation` form is created for this property.

> If `#[Catch_]` attribute specified a message,
> it would've been ignored in favour of `ConstraintViolationList` messages.

> Besides that, it's also possible to use `validator_violations` formatter,
> which can format Symfony's native `ValidationFailedException`.

#### Custom Violation Formatters 🎨🖌️

[](#custom-violation-formatters-️)

In some cases, you might want to customize the created violations.
For example, pass additional parameters to the message translation.

You can create custom violation formatter by implementing `ExceptionViolationFormatter` interface:

```
use PhPhD\ExceptionalMatcher\Exception\MatchedException;
use PhPhD\ExceptionalMatcher\Validator\Formatter\ExceptionViolationFormatter;
use Symfony\Component\Validator\ConstraintViolationInterface;

/** @implements ExceptionViolationFormatter */
final class LoginAlreadyTakenViolationFormatter implements ExceptionViolationFormatter
{
    public function __construct(
        #[Autowire(service: ExceptionViolationFormatter::class.'')]
        private ExceptionViolationFormatter $formatter,
    ) {
    }

    /** @return array{ConstraintViolationInterface} */
    public function format(MatchedException $matchedException): ConstraintViolationInterface
    {
        // format violation with the default formatter
        // and then adjust only the necessary parts
        [$violation] = $this->formatter->format($matchedException);

        /** @var LoginAlreadyTakenException $exception */
        $exception = $matchedException->getException();

        $violation = new ConstraintViolation(
            $violation->getMessage(),
            $violation->getMessageTemplate(),
            ['loginHolder' => $exception->getLoginHolder()],
            // ...
        );

        return [$violation];
    }
}
```

Then, register it as a service:

```
services:
    App\Auth\User\Support\Validation\LoginAlreadyTakenViolationFormatter:
        autoconfigure: true
```

> In order for violation formatter to be recognized by the bundle,
> its service must be tagged with `MatchedExceptionFormatter` class-name tag.
>
> If you are using [autoconfiguration](https://symfony.com/doc/current/service_container.html#the-autoconfigure-option), this will be done automatically by the service container,
> owing to the fact that `MatchedExceptionFormatter` interface is implemented.

Finally, specify formatter in the `#[Catch_]` attribute:

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;

#[Try_]
final class RegisterUserCommand
{
    #[Catch_(LoginAlreadyTakenException::class, format: LoginAlreadyTakenViolationFormatter::class)]
    private string $login;

    #[Catch_(WeakPasswordException::class, format: WeakPasswordViolationFormatter::class)]
    private string $password;
}
```

In this example, `LoginAlreadyTakenViolationFormatter` formats constraint violation for `LoginAlreadyTakenException`,
while `WeakPasswordViolationFormatter` formats `WeakPasswordException`.

### In-depth analysis

[](#in-depth-analysis)

> The approach described is done away with.

`#[Try_]` attribute works side-by-side with Symfony Validator's `#[Valid]` attribute.

Once you define `#[Valid]` on an object/iterable property,
the matcher will pick it up for a nested analysis,
providing a respective property path for the created violations.

```
use PhPhD\ExceptionalMatcher\Rule\Object\Try_;
use PhPhD\ExceptionalMatcher\Rule\Object\Property\Catch_;
use Symfony\Component\Validator\Constraints as Assert;

#[Try_]
class CreateOrderCommand
{
    /** @var OrderItemDto[] */
    #[Assert\Valid]
    public array $items;
}

#[Try_]
class OrderItemDto
{
    public int $productId;

    #[Catch_(InsufficientStockException::class, if: [self::class, 'isStockExceptionForThisItem'])]
    public string $quantity;

    public function isStockExceptionForThisItem(InsufficientStockException $exception): bool
    {
        return $exception->getProductId() === $this->productId;
    }
}
```

In this example, every time exception is processed, it will also be matched with inner objects from `items` property, until it finally arrives at `items[*].quantity` (`*` stands for the particular array item index) property, being matched by `InsufficientStockException` class name, and custom closure condition that makes sure that it was this particular `OrderItemDto` that caused the exception.

The resulting property path of the caught violation includes all intermediary items, starting from the root of the tree, proceeding down to the leaf item, where the exception was actually caught.

### Matching multiple exceptions

[](#matching-multiple-exceptions)

Typically, validation is expected to return all present violations at once (not just the first one) so they can be shown to the user.

Though due to the limitations of the sequential computation model, only one instruction can be executed at a time, and therefore, only one exception can be thrown at a time. This leads to a situation where validation ends up in only the first exception being thrown, while the rest are not even reached.

For example, if we consider user registration with `RegisterUserCommand` from the code above, we'd like to capture both `LoginAlreadyTakenException` and `WeakPasswordException` at once, so that the user can fix all the form errors at once, rather than sorting them out one by one.

This limitation can be overcome by implementing some concepts from an Interaction Calculus model in a sequential PHP environment. The key idea is to use a semi-parallel execution flow instead of a purely sequential.

In practice, if validation is split into multiple functions, each of which may throw an exception, the concept can be implemented by calling them one by one and collecting any exceptions as they raise. If there were any, they are wrapped into a composite exception that is eventually thrown.

Fortunately, you don't need to implement this manually, since `amphp/amp` library already provides a more efficient solution than one you'd likely write yourself, using async Futures:

```
/**
 * @var Login $login
 * @var Password $password
 */
[$login, $password] = await([
    // validate and create an instance of Login
    async($this->createLogin(...), $service),
    // validate and create an instance of Password
    async($this->createPassword(...), $service),
]);
```

In this example, `createLogin()` method could throw `LoginAlreadyTakenException` and `createPassword()` method could throw `WeakPasswordException`.

By using `async` and `awaitAnyN` functions, we are leveraging semi-parallel execution flow instead of sequential, so that both `createLogin()` and `createPassword()` methods are executed regardless of thrown exceptions.

If no exceptions were thrown, then `$login` and `$password` variables are populated with the respective return values. But if there were indeed some exceptions then `Amp\CompositeException` will be thrown with all the wrapped exceptions inside.

> If you would like to use a custom composite exception, make sure to read about [ExceptionUnwrapper](https://github.com/phphd/exception-toolkit?tab=readme-ov-file#exception-unwrapper)

Since the library is capable of processing composite exceptions (with unwrappers for Amp and Messenger exceptions), all of our thrown exceptions will be processed, and the user will get the complete stack of validation errors at hand.

Upgrading 👻
-----------

[](#upgrading-)

The basic upgrade can be performed by [Rector](https://getrector.com/documentation) using `ExceptionalMatcherSetList`
which comes with the library and contains automatic upgrade rules.

To upgrade a project to the latest version of `exceptional-validation`,
add the following configuration to your `rector.php` file:

```
use PhPhD\ExceptionalMatcher\Upgrade\ExceptionalMatcherSetList;

return RectorConfig::configure()
    ->withPaths([ __DIR__ . '/src'])
    ->withImportNames(removeUnusedImports: true)
    // Upgrading from your version (e.g. 1.4) to the latest version
    ->withSets(ExceptionalMatcherSetList::fromVersion('1.4')->getSetList());
```

Make sure to specify your current version of the library so that upgrade sets will be matched correctly.

You should also check [UPGRADE.md](UPGRADE.md) for breaking changes and additional instructions.

###  Health Score

41

—

FairBetter than 89% of packages

Maintenance53

Moderate activity, may be stable

Popularity30

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~76 days

Recently: every ~89 days

Total

8

Last Release

298d ago

Major Versions

1.5.0 → 2.0.0-rc12025-07-05

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/41589422?v=4)[Yevhen Sidelnyk](/maintainers/rela589n)[@rela589n](https://github.com/rela589n)

---

Top Contributors

[![rela589n](https://avatars.githubusercontent.com/u/41589422?v=4)](https://github.com/rela589n "rela589n (229 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Psalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/phphd-exceptional-validation/health.svg)

```
[![Health](https://phpackages.com/badges/phphd-exceptional-validation/health.svg)](https://phpackages.com/packages/phphd-exceptional-validation)
```

###  Alternatives

[sllh/iso-codes-validator

Symfony validator wrapper of ronanguilloux/isocodes

37299.8k2](/packages/sllh-iso-codes-validator)[digitalrevolution/symfony-validation-shorthand

Validation shorthand for symfony

12108.5k2](/packages/digitalrevolution-symfony-validation-shorthand)[digitalrevolution/symfony-request-validation

Automatic request validation for symfony

1296.0k](/packages/digitalrevolution-symfony-request-validation)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
