<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Henrique Moody</title>
    <description>The latest articles on DEV Community by Henrique Moody (@henriquemoody).</description>
    <link>https://dev.to/henriquemoody</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3693314%2Fd7744259-8f99-476f-9c58-fb7c24f7ce2c.jpeg</url>
      <title>DEV Community: Henrique Moody</title>
      <link>https://dev.to/henriquemoody</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/henriquemoody"/>
    <language>en</language>
    <item>
      <title>Keeping boundaries clean: Upstream vs. Downstream</title>
      <dc:creator>Henrique Moody</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:25:52 +0000</pubDate>
      <link>https://dev.to/henriquemoody/keeping-bounded-contexts-clean-upstream-vs-downstream-14d9</link>
      <guid>https://dev.to/henriquemoody/keeping-bounded-contexts-clean-upstream-vs-downstream-14d9</guid>
      <description>&lt;p&gt;Boundaries are hard to get right. Not only between the big parts of a system, but anywhere we draw a line: between classes, objects, even methods.&lt;/p&gt;

&lt;p&gt;I've spent a lot of time describing systems, both new and existing ones. I try not to throw terms like "Domain-Driven Design" at people, because they are open to a lot of interpretation and there are many nuances to them. But then, how do you help someone understand boundaries, whether they have a technical background or not?&lt;/p&gt;

&lt;p&gt;I start with the parts of the business they already know: Acquisition, Distribution, Billing. Then we explore which of those parts depends on which, and in what way. Those dependencies play an important role in how we build products and design systems, and making them visible is where I've had the most success. The words that help us understand them are &lt;strong&gt;upstream&lt;/strong&gt; and &lt;strong&gt;downstream&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article goes a bit deeper than those conversations, so the parts need a name. Domain-Driven Design calls them &lt;strong&gt;bounded contexts&lt;/strong&gt;: boundaries within which one model and its language stay consistent. A bounded context can be a single service, or a module inside one. Bounded contexts rarely stand alone: each one depends on others, and others depend on it.&lt;/p&gt;

&lt;p&gt;What follows explains which way those dependencies should point, how to tell the sides apart, why that matters, how to spot a boundary that has started to leak, and why dependencies must never go in a circle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upstream vs. Downstream
&lt;/h2&gt;

&lt;p&gt;Think of a river: the downstream part only gets the water that comes from upstream.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqi0ely3czz9ebi454go2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqi0ely3czz9ebi454go2.png" alt="A river flowing from upstream to downstream" width="360" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrqzqpi5fvk4e2y9opv5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrqzqpi5fvk4e2y9opv5.png" alt="Upstream above Downstream, with an arrow pointing down" width="148" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Upstream
&lt;/h3&gt;

&lt;p&gt;Upstreams provide capabilities that Downstreams can use. &lt;strong&gt;Upstreams should not depend on Downstreams.&lt;/strong&gt; Ideally they don't know their Downstreams exist; if they do, they still contain none of their concepts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Downstream
&lt;/h3&gt;

&lt;p&gt;Downstreams consume what Upstreams provide. &lt;strong&gt;Downstreams depend on Upstreams.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Request Paradox (Data vs. Control Flow)
&lt;/h2&gt;

&lt;p&gt;Data, business value and events flow downstream, like water. Network requests flow the other way: the Downstream calls up to the Upstream to fetch data or to ask it to do something. Two things cross every boundary, in opposite directions. &lt;strong&gt;Calls go up, data comes down.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the paradox, and it is why context maps often end up drawn upside down. Follow the request and you draw the arrow from caller to callee. But the arrow on a context map shows the dependency, and the dependency follows the water, not the request. So if the call doesn't settle it, what does?&lt;/p&gt;

&lt;h2&gt;
  
  
  Telling the sides apart
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The contract decides the direction, not the call.&lt;/strong&gt; The Upstream defines the contract, and the Downstream adapts to it. The call is still a good hint, because the caller is usually downstream. When the two disagree, the contract wins.&lt;/p&gt;

&lt;p&gt;Webhooks look like an exception, but they aren't. A payment provider calls your endpoint, so the call travels from Upstream to Downstream. Yet the provider defines the payload and you adapt to it. You registered your URL with it: the Downstream subscribes, and the Upstream delivers to whatever address it was given. It knows &lt;em&gt;where&lt;/em&gt; to send, not &lt;em&gt;who&lt;/em&gt; you are or what your model looks like.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb.&lt;/strong&gt; Start with the network calls, but don't stop there. Ask: &lt;em&gt;whose model does the other one have to know about?&lt;/em&gt; The side that can be described without the other is the Upstream; the side that can't is the Downstream. If neither can be described without the other, the boundary is either leaking or in the wrong place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Keeping an Upstream unaware of its Downstreams pays off on both sides of the boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reuse.&lt;/strong&gt; The Upstream can serve any Downstream, not only the one it was first built for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replaceability.&lt;/strong&gt; The Upstream can be replaced (by a vendor, a rewrite, a new platform), and Downstreams only change an adapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent testing.&lt;/strong&gt; The Upstream can be tested without fixtures from any Downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stable foundation.&lt;/strong&gt; Downstreams build on a language that doesn't change just because one Downstream changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contained blast radius.&lt;/strong&gt; A change in one Downstream can't break the Upstream or its other Downstreams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team autonomy.&lt;/strong&gt; A Downstream can change without the Upstream team in its planning meeting, and the Upstream can change its internals freely. Only changes to the Upstream's contract need coordinating, because every Downstream adapts to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Signs of a leaking boundary
&lt;/h2&gt;

&lt;p&gt;The boundary leaks the moment downstream concepts start showing up inside the Upstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Downstream identifiers in upstream data or APIs.&lt;/strong&gt; The Upstream stores or accepts IDs that only mean something to one Downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream-specific APIs.&lt;/strong&gt; The Upstream exposes endpoints or accepts payloads shaped around one Downstream's model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream state in upstream events.&lt;/strong&gt; The Upstream's events carry statuses or fields that describe a Downstream's state, not its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream rules in upstream logic.&lt;/strong&gt; The Upstream checks conditions that belong to a Downstream's business rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases driven by a Downstream.&lt;/strong&gt; The Upstream has to change just because one Downstream changed its model or request format. This is the Request Paradox going wrong: the call travels upstream, and the Downstream's concepts travel with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you spot one, the fix is the same: move the downstream-specific knowledge back into the Downstream, and let the Downstream hold the reference to the Upstream, never the other way round.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid dependency cycles
&lt;/h2&gt;

&lt;p&gt;Getting the direction right between two bounded contexts is not enough: &lt;strong&gt;a bounded context must never end up downstream of itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbd4a2c2inxb0xambsfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbd4a2c2inxb0xambsfn.png" alt="Cycle: Spring → River → Delta → Spring. Spring ends up downstream of itself." width="219" height="301"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cycle: Spring → River → Delta → Spring. Spring ends up downstream of itself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe6hgmbxscac0kefieh0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe6hgmbxscac0kefieh0w.png" alt="No cycle: every arrow points the same way." width="148" height="256"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;No cycle: every arrow points the same way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A cycle cancels the benefits above: changes travel around the loop and come back, and concepts leak in through a third bounded context. Two bounded contexts that depend on each other directly are the smallest version of the same problem.&lt;/p&gt;

&lt;p&gt;To break one, find the arrow that points the wrong way. Usually one bounded context should coordinate the others; if two bounded contexts can't be described without each other, they may be one bounded context.&lt;/p&gt;
&lt;h2&gt;
  
  
  Example: Shipping and Ordering
&lt;/h2&gt;

&lt;p&gt;Shipping and Ordering show up in almost any business that sells things and delivers them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shipping is the Upstream&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ordering is the Downstream&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrip85hr1qqsqi0pj3xu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrip85hr1qqsqi0pj3xu.png" alt="Shipping above Ordering: model and events flow down, requests go up" width="199" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That might surprise people at first, because Ordering is the one that "kicks things off". The sections below show why.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Request Paradox in action
&lt;/h3&gt;

&lt;p&gt;Ordering starts the process and makes the call, and Shipping is the Upstream. &lt;strong&gt;Calls go up, data comes down.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Ordering calls Shipping
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Request travels upstream.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1tzs6w8rd3xkbsxjjlvw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1tzs6w8rd3xkbsxjjlvw.png" alt="Ordering sends CreateShipment to Shipping and gets a shipment_id back" width="620" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When an Order is paid, Ordering asks Shipping to create a shipment: "ship &lt;em&gt;these parcels&lt;/em&gt; to &lt;em&gt;this address&lt;/em&gt;". It speaks Shipping's language: parcels, weights, an address. It does &lt;em&gt;not&lt;/em&gt; send an Order object and expect Shipping to figure out what to do with it. Ordering saves the ID of the shipment to later get information about that shipment.&lt;/p&gt;
&lt;h4&gt;
  
  
  Shipping publishes what happened
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Data flows downstream.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzj9oy9sd3a5rur01pz55.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzj9oy9sd3a5rur01pz55.png" alt="Shipping publishes ShipmentCreated, ShipmentDispatched and ShipmentDelivered" width="654" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ShipmentCreated&lt;/code&gt;, &lt;code&gt;ShipmentDispatched&lt;/code&gt;, &lt;code&gt;ShipmentDelivered&lt;/code&gt;. These events are about shipments. They carry a shipment id, a carrier, timestamps. They do not carry an order id, an order status, or a customer's purchase history.&lt;/p&gt;
&lt;h4&gt;
  
  
  Ordering consumes those events
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;The Downstream adapts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8lqrctu39hv9ra3hcsa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8lqrctu39hv9ra3hcsa.png" alt="Ordering translates " width="628" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ordering translates events into its own concepts: &lt;code&gt;ShipmentDelivered&lt;/code&gt; becomes "Order fulfilled". Ordering keeps the mapping &lt;em&gt;order ↔ shipment&lt;/em&gt; on its side, because Ordering is the one that cares about it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Telling the sides apart, in practice
&lt;/h3&gt;

&lt;p&gt;Shipping defines the contract (the &lt;code&gt;CreateShipment&lt;/code&gt; request and the &lt;code&gt;Shipment…&lt;/code&gt; events), and Ordering adapts to it. Here the hint and the contract agree: the caller, Ordering, is the Downstream.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Shipping (Upstream)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Ordering (Downstream)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Move a parcel from an origin to a destination address, through a carrier, and report where it is.&lt;/td&gt;
&lt;td&gt;Turn what a customer bought into a fulfilled purchase: items, prices, payment, and getting the goods to the customer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concepts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shipment, Parcel, Weight, Dimensions, Address, Carrier, Label, Tracking number, Delivered.&lt;/td&gt;
&lt;td&gt;Order, Line item, Customer, Total, Payment, Fulfilment status, &lt;em&gt;and&lt;/em&gt; Shipment (borrowed from Shipping).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it knows about the other&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nothing. Shipping has no idea what an Order is. It ships parcels for whoever asks: a sales order, a warranty replacement, a sample sent to a partner, a return.&lt;/td&gt;
&lt;td&gt;Quite a lot. Ordering knows that a Shipment exists, how to request one, and what "Delivered" means for an Order's fulfilment status.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Shipping can be fully described &lt;em&gt;without&lt;/em&gt; the word "Order" ever appearing. Ordering &lt;em&gt;cannot&lt;/em&gt; be described without Shipping. That is the rule of thumb at work: &lt;strong&gt;the side that can be described without the other is the Upstream.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why it matters, in practice
&lt;/h3&gt;

&lt;p&gt;Applied to this example, the benefits of a clean Upstream mean Shipping can ship returns, warranty replacements and partner samples, not just orders; it can be replaced by a carrier platform behind an adapter; it can be tested without an Order fixture; and only changes to Shipping's contract need coordinating with Ordering.&lt;/p&gt;
&lt;h3&gt;
  
  
  Signs of a leaking boundary, in practice
&lt;/h3&gt;

&lt;p&gt;Here is what each of the signs of a leaking boundary looks like between Shipping and Ordering:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Sign&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;In Shipping and Ordering&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Downstream identifiers in upstream data or APIs&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;order_id&lt;/code&gt; column on the &lt;code&gt;Shipment&lt;/code&gt; table, or an &lt;code&gt;order_id&lt;/code&gt; parameter in Shipping's REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream-specific APIs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /shipments/from-order&lt;/code&gt; taking an Order payload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream state in upstream events&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ShipmentDelivered&lt;/code&gt; carrying &lt;code&gt;orderStatus: FULFILLED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downstream rules in upstream logic&lt;/td&gt;
&lt;td&gt;Shipping checking "is this order paid?" before printing a label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Releases driven by a Downstream&lt;/td&gt;
&lt;td&gt;Shipping redeploying because the Order's shape changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Avoiding dependency cycles, in practice
&lt;/h3&gt;

&lt;p&gt;To see why dependency cycles are a problem, add a third bounded context, Billing, and a reasonable rule: &lt;em&gt;don't dispatch a parcel until it has been paid for&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mistake&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq495kazsrf9de9m5mndd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq495kazsrf9de9m5mndd.png" alt="Shipping → Ordering → Billing → Shipping: a cycle" width="320" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Billing listens to &lt;code&gt;OrderPlaced&lt;/code&gt;, and Shipping waits for &lt;code&gt;InvoicePaid&lt;/code&gt;. Shipping is now downstream of itself: a change to the Order travels through Billing to Shipping and back to Ordering, and an &lt;code&gt;order_id&lt;/code&gt; reaches Shipping through Billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hj0p5kok2wdtjq0hz4y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hj0p5kok2wdtjq0hz4y.png" alt="Shipping and Billing are both upstream of Ordering" width="320" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ordering coordinates. It asks Billing for an invoice in Billing's language and, once &lt;code&gt;InvoicePaid&lt;/code&gt; arrives, asks Shipping to ship. Neither Upstream knows the other exists.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  See the fixed flow step by step
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3hb67exa6q7qabzluz4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp3hb67exa6q7qabzluz4.png" alt="Ordering asks Billing for an invoice, waits for InvoicePaid, then asks Shipping to ship" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Eric Evans, &lt;em&gt;Domain-Driven Design: Tackling Complexity in the Heart of Software&lt;/em&gt; (2003), Part IV, "Strategic Design".&lt;/li&gt;
&lt;li&gt;Eric Evans, &lt;a href="https://www.domainlanguage.com/ddd/reference/" rel="noopener noreferrer"&gt;&lt;em&gt;Domain-Driven Design Reference&lt;/em&gt;&lt;/a&gt; (2015): the context-mapping patterns in summary form.&lt;/li&gt;
&lt;li&gt;Vaughn Vernon, &lt;em&gt;Implementing Domain-Driven Design&lt;/em&gt; (2013), chapter 3, "Context Maps".&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>ddd</category>
      <category>boundaries</category>
    </item>
    <item>
      <title>What is new on Respect\Validation 3.0</title>
      <dc:creator>Henrique Moody</dc:creator>
      <pubDate>Mon, 05 Jan 2026 08:28:29 +0000</pubDate>
      <link>https://dev.to/henriquemoody/what-is-new-on-respectvalidation-30-37no</link>
      <guid>https://dev.to/henriquemoody/what-is-new-on-respectvalidation-30-37no</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/Respect/Validation" rel="noopener noreferrer"&gt;Respect\Validation&lt;/a&gt; is a PHP validation library that makes complex validations simple, with many validators out of the box.&lt;/p&gt;

&lt;p&gt;It's built for engineers who validate real data and might need to display meaningful messages to end-users without hassle. It was first released over a decade ago, and has since grown to over 35 million installs on Packagist, nearly 6k stars on GitHub, and hundreds of public packages depending on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation 3.0 lands in February, 2026&lt;/strong&gt; — and it's a big one.&lt;/p&gt;

&lt;p&gt;I've been the main maintainer of Validation for a long time, and after a lot of thought and several conversations with users and dear engineer friends, I've refactored the whole library and rebuilt the engine from the ground up.&lt;/p&gt;

&lt;p&gt;Validation still has the simplicity of making complex validations easier to end users, but now features consistent naming, better separation of concerns, higher customization capabilities, modern PHP features, and a developer experience that just &lt;em&gt;feels right&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this post:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accurate errors for nested validators&lt;/li&gt;
&lt;li&gt;Result composition&lt;/li&gt;
&lt;li&gt;Prefixed shortcuts&lt;/li&gt;
&lt;li&gt;PHP attributes&lt;/li&gt;
&lt;li&gt;No need for exceptions&lt;/li&gt;
&lt;li&gt;Paths in error messages&lt;/li&gt;
&lt;li&gt;Helpful stack traces&lt;/li&gt;
&lt;li&gt;Use your own exception&lt;/li&gt;
&lt;li&gt;Use your own templates&lt;/li&gt;
&lt;li&gt;Attaching templates to the chain&lt;/li&gt;
&lt;li&gt;Placeholder pipes&lt;/li&gt;
&lt;li&gt;Naming validators&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Accurate errors for nested validators
&lt;/h2&gt;

&lt;p&gt;Previous versions of Validation had a fundamental limitation: when a validator passed, nothing happened — and when it failed, it threw an exception. That sounds reasonable until you consider validators that &lt;em&gt;wrap&lt;/em&gt; other validators, like &lt;code&gt;Not&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's the problem. In previous versions, when you wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before v3 🤔&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;intType&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'not an integer'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → These rules must not pass for "not an integer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cryptic. Which rules? Why? The library couldn't produce a coherent message because the inner &lt;code&gt;not(intType())&lt;/code&gt; passed (since &lt;code&gt;'not an integer'&lt;/code&gt; isn't an integer), but the outer &lt;code&gt;Not&lt;/code&gt; had no idea &lt;em&gt;why&lt;/em&gt; it passed — only that it did.&lt;/p&gt;

&lt;p&gt;Years ago I read &lt;a href="https://martinfowler.com/eaaDev/Notification.html" rel="noopener noreferrer"&gt;Replacing Throwing Exceptions with Notification in Validations&lt;/a&gt; by Martin Fowler, and that sparked some ideas. What if we could do something similar in Validation? What if every validator (pass or fail) would return a result object that captures what happened and why?&lt;/p&gt;

&lt;p&gt;After playing around with different prototypes and with PHP's newest features, I came up with a result object that has everything we need, without a lot of verbosity. Now, when validators compose, they can inspect each other's results and produce messages that actually make sense:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v3 👍&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;intType&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'not an integer'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → "not an integer" must be an integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No matter how deep the nesting, Validation now knows exactly what happened and tells you clearly.&lt;/p&gt;

&lt;p&gt;This change also unlocked capabilities that simply weren't possible before — like result composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result composition
&lt;/h2&gt;

&lt;p&gt;I always found it a pity that we couldn't compose validators and their messages. I wrote lots of customizations on top of Validation in &lt;a href="https://github.com/Respect/Assertion" rel="noopener noreferrer"&gt;Assertion&lt;/a&gt;, but there were quite some limitations.&lt;/p&gt;

&lt;p&gt;In v3, validators can finally wrap others and combine their results into a single, coherent message. The outer validator provides context (what was extracted), and the inner validator provides the validation (what was expected).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;intType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'3'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// → Every item in `["1", "2", "3"]` must be an integer&lt;/span&gt;

&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;greaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abc'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → The length of "abc" must be greater than 3&lt;/span&gt;

&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// → The minimum of `[3, 1, 0, -5]` must be a positive number&lt;/span&gt;

&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// → The maximum of `[-1, -2, -3]` must be a positive number&lt;/span&gt;

&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MB'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;greaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'path/to/file.zip'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → The size in megabytes of "path/to/file.zip" must not be greater than 5&lt;/span&gt;

&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dateTimeDiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'years'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;greaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → The number of years between now and "2025" must be greater than 18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prefixed shortcuts
&lt;/h2&gt;

&lt;p&gt;In a distant past, Validation had a &lt;code&gt;not&lt;/code&gt; prefix as part of the &lt;code&gt;Validator&lt;/code&gt; class, but we removed it. Many years later, when I wrote &lt;a href="https://github.com/Respect/Assertion" rel="noopener noreferrer"&gt;Assertion&lt;/a&gt;, I added several prefixes to make it easier to use validators that wrap other validators. I decided to bring them to Validation too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;allEmoji&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// all items must be emojis&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;keyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// key 'email' must be valid email&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;propertyPositive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// property 'age' must be positive&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;lengthBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// length between 5 and 10&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;maxLessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// max value less than 100&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;minGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// min value greater than 0&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;nullOrEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// null or valid email&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;undefOrPositive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// undefined or positive number&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PHP attributes
&lt;/h2&gt;

&lt;p&gt;PHP attributes have been available for years, and I always found it a shame we didn't support validating object properties with them. Symfony Validator has been supporting annotations/attributes for a very long time, but it doesn't have as many validators out-of-the-box as we have. Now, we can use any validator as an attribute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Respect\Validation\Rules\&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Between&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;GreaterThan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;#[Email]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;#[Between(18, 120)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;#[Length(new GreaterThan(1))]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Validate everything at once&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  No need for exceptions
&lt;/h2&gt;

&lt;p&gt;Sometimes you just want to validate data and handle the result manually. In previous versions, the only way to do that was wrapping the validation in a &lt;code&gt;try&lt;/code&gt; block. In v3, you can skip the exception entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;numericVal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;between&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;validate()&lt;/code&gt; method returns a &lt;code&gt;ResultQuery&lt;/code&gt; object that has the methods &lt;code&gt;getMessage()&lt;/code&gt;, &lt;code&gt;getFullMessage()&lt;/code&gt;, and &lt;code&gt;getMessages()&lt;/code&gt; to output messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$mysqlUserResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findByPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mysql.user'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mysqlUserResult&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$mysqlUserResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;findByPath()&lt;/code&gt; returns either a &lt;code&gt;ResultQuery&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt;, and you can also use &lt;code&gt;findByName()&lt;/code&gt; and &lt;code&gt;findById()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paths in error messages
&lt;/h2&gt;

&lt;p&gt;Because previous versions of Validation didn't have result objects, it was impossible to trace paths of nested structures. Consider the following chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$validator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'mysql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'schema'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'postgresql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'schema'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In previous versions of Validation, this is what would happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before v3 🤔&lt;/span&gt;
&lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → host must be a string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Was it &lt;code&gt;host&lt;/code&gt; from &lt;code&gt;mysql&lt;/code&gt; or &lt;code&gt;postgresql&lt;/code&gt;? You had no idea which one failed.&lt;/p&gt;

&lt;p&gt;In v3, because every validator returns a result object, we can trace the path of nested structures and display much more helpful messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v3 👍&lt;/span&gt;
&lt;span class="nv"&gt;$validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → `.mysql.host` must be a string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not only do you have the full path of the nested structure, but it's also clear that &lt;code&gt;.mysql.host&lt;/code&gt; is a path, not a name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Helpful stack traces
&lt;/h2&gt;

&lt;p&gt;When an exception is thrown, PHP reports where it was &lt;em&gt;created&lt;/em&gt;, not where it was &lt;em&gt;caused&lt;/em&gt;. In most validation libraries (including older versions of Validation) that means stack traces point deep inside library internals. You end up hunting through the trace to find your actual code.&lt;/p&gt;

&lt;p&gt;v3 fixes this. If &lt;code&gt;v::intType()-&amp;gt;assert($input)&lt;/code&gt; fails in &lt;code&gt;example.php&lt;/code&gt; line &lt;code&gt;11&lt;/code&gt;, your stack trace looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stack trace:
#0 /opt/example.php(11): Respect\Validation\Validator-&amp;gt;assert(1.0)
#1 {main}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your file. Your line. Your problem to fix — not ours to hide.&lt;/p&gt;

&lt;p&gt;I picked up this technique while digging into &lt;a href="https://pestphp.com/" rel="noopener noreferrer"&gt;Pest&lt;/a&gt;'s source code. Kudos to Pest's engineers!&lt;/p&gt;

&lt;h2&gt;
  
  
  Use your own exception
&lt;/h2&gt;

&lt;p&gt;Even with a helpful stack trace, at times we just want to use our own exceptions. The trouble with older versions of Validation (and some other libraries) is that when a validation fails, you have to wrap it in a &lt;code&gt;try/catch&lt;/code&gt; or manually check the result so you can throw your own exception.&lt;/p&gt;

&lt;p&gt;I implemented this in &lt;a href="https://github.com/Respect/Assertion" rel="noopener noreferrer"&gt;Assertion&lt;/a&gt; years ago. It was an awesome idea that &lt;a href="https://github.com/malukenho" rel="noopener noreferrer"&gt;Jefersson Nathan&lt;/a&gt; suggested while I was working on it. In v3, you can do that right when you're using the &lt;code&gt;assert()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DomainException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid email'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But sometimes, you might want to reuse the message from Validation. For cases like that, you can just pass a callable to &lt;code&gt;assert()&lt;/code&gt;. This idea came from seeing how &lt;a href="https://github.com/thephpleague/openapi-psr7-validator" rel="noopener noreferrer"&gt;openapi-psr7-validator&lt;/a&gt; uses Validation. It made a lot of sense that people might want their own exceptions without having to rebuild the message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ValidationException&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DomainException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callable receives the exception that Validation would have thrown. You can do whatever you want in there (log it, wrap it, enrich it), but it needs to return an exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use your own templates
&lt;/h2&gt;

&lt;p&gt;Sometimes, you don't need your own exception, but you need a specific message. You can pass your template to &lt;code&gt;assert()&lt;/code&gt; just as a string, and Validation will throw &lt;code&gt;ValidationException&lt;/code&gt; with it as the message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'This is my template'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → This is my template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you have multiple validators in a chain, you can define one template for each validator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;intVal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'intVal'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Must be an integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'positive'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Must be positive'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'lessThan'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Must be under 100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using validators that handle structures (like &lt;code&gt;Key&lt;/code&gt; and &lt;code&gt;Property&lt;/code&gt;), you can define the template by the path of the validator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Target nested structures by path&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;intVal&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'__root__'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Please check your user data'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Please provide a valid name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'age'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Age must be a number'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;__root__&lt;/code&gt; key targets the root validator. In this case, that's an &lt;code&gt;AllOf&lt;/code&gt; that wraps the chain.&lt;/p&gt;

&lt;p&gt;Passing templates to &lt;code&gt;assert()&lt;/code&gt; is best for when you don't want to reuse the templates. When you do, you'll want to keep the templates in the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attaching templates to the chain
&lt;/h2&gt;

&lt;p&gt;In previous versions of Validation, you could use the &lt;code&gt;setTemplate&lt;/code&gt; method in the chain to define your template.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'This is my template'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → This is my template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a couple of issues with &lt;code&gt;setTemplate&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It can be confusing when you have nested validators in the chain&lt;/li&gt;
&lt;li&gt;It's not as explicit as I would have liked it to be&lt;/li&gt;
&lt;li&gt;It added some overhead to the &lt;code&gt;Validator&lt;/code&gt; class that was already quite complex&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because of the new engine (result composition), I created a validator called &lt;code&gt;Templated&lt;/code&gt; that overwrites the template of the result of a wrapped validator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;templated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'This is my template'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → This is my template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the validation more explicit and less confusing. The &lt;code&gt;Templated&lt;/code&gt; validator also allows you to pass parameters to your template, so you can inject your own placeholders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;templated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Every {{user_type}} user needs a valid email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userType&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'not an email'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → Every "admin" user needs a valid email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Placeholder pipes
&lt;/h2&gt;

&lt;p&gt;Validation uses &lt;a href="https://github.com/Respect/Stringifier" rel="noopener noreferrer"&gt;Stringifier&lt;/a&gt; to convert values into strings for templates. By default, strings get double quotes around them. With placeholder pipes, you can customise how values are rendered. Just add a pipe (&lt;code&gt;|&lt;/code&gt;) followed by the modifier name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;raw&lt;/strong&gt;: removes quotes, useful for field names or labels that shouldn't look like string values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;templated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'The {{field|raw}} field is required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;notEmpty&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'field'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → The email field is required&lt;/span&gt;
&lt;span class="c1"&gt;// (instead of: The "email" field is required)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;quote&lt;/strong&gt;: uses backticks, great for patterns or code-like values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;templated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Product SKU must follow the {{pattern|quote}} format'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^[A-Z]{3}-\d{4}$/'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'pattern'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'XXX-0000'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invalid-sku'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → Product SKU must follow the `XXX-0000` format&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;trans&lt;/strong&gt;: translates the value when using a translator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;templated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Le champ {{field|trans}} est invalide'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'field'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'email_address'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// key in your translation file&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'not-an-email'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → Le champ "adresse e-mail" est invalide&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;list:or&lt;/strong&gt; and &lt;strong&gt;list:and&lt;/strong&gt;: formats arrays as readable lists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;templated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Status must be {{haystack|list:or}}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;in&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'archived'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'deleted'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → Status must be "active", "pending", or "archived"&lt;/span&gt;

&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;templated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'User must have {{roles|list:and}} roles to perform this action'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasRoles&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'roles'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → User must have "admin" and "editor" roles to perform this action&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Naming validators
&lt;/h2&gt;

&lt;p&gt;The same reasoning applies to naming. In previous versions, you could use &lt;code&gt;setName&lt;/code&gt; to give a validator a custom name in messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Your email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → Your email must be a valid email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This had the same issues as &lt;code&gt;setTemplate&lt;/code&gt;: confusing with nested validators, not explicit, and added overhead to the &lt;code&gt;Validator&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;In v3, there's a &lt;code&gt;Named&lt;/code&gt; validator that wraps another validator and gives it a name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;named&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Your email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → Your email must be a valid email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern as &lt;code&gt;Templated&lt;/code&gt;, same benefits. And because they're just validators, they work as PHP attributes too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;#[Named('Your email', new Email())]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;#[Templated('You must be at least {{age}} years old', new GreaterThanOrEqual(18))]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;This is the biggest release in Validation's history! The new engine, the result objects, the composition capabilities, the customization options... it's all been a long time coming.&lt;/p&gt;

&lt;p&gt;This post focused on the most exciting user-facing changes, but there's a lot more to this version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ability to customize a lot of standard behavior&lt;/li&gt;
&lt;li&gt;Improved validation messages&lt;/li&gt;
&lt;li&gt;Renamed validators for better semantic meaning&lt;/li&gt;
&lt;li&gt;More validators that are only possible due to the new engine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm incredibly grateful to everyone who contributed ideas, feedback, and code over the years! I can't wait to get feedback on the new version!&lt;/p&gt;

</description>
      <category>backend</category>
      <category>news</category>
      <category>opensource</category>
      <category>php</category>
    </item>
  </channel>
</rss>
