44
55namespace ComplexHeart \Domain \Model \Traits ;
66
7+ use ComplexHeart \Domain \Model \Contracts \Aggregatable ;
78use ComplexHeart \Domain \Model \Exceptions \InvariantViolation ;
89use Throwable ;
910
@@ -110,6 +111,15 @@ private function computeInvariantViolations(string $exception): array
110111 return $ violations ;
111112 }
112113
114+ /**
115+ * Compute the invariant handler function.
116+ *
117+ * The handler is responsible for throwing exceptions (single or aggregated).
118+ *
119+ * @param string|callable $handlerFn
120+ * @param string $exception
121+ * @return callable
122+ */
113123 private function computeInvariantHandler (string |callable $ handlerFn , string $ exception ): callable
114124 {
115125 if (!is_string ($ handlerFn )) {
@@ -121,17 +131,45 @@ private function computeInvariantHandler(string|callable $handlerFn, string $exc
121131 $ this ->{$ handlerFn }($ violations , $ exception );
122132 }
123133 : function (array $ violations ) use ($ exception ): void {
124- if (count ($ violations ) === 1 ) {
125- throw array_shift ($ violations );
134+ $ this ->throwInvariantViolations ($ violations , $ exception );
135+ };
136+ }
137+
138+ /**
139+ * Throw invariant violations (single or aggregated).
140+ *
141+ * Responsible for all exception throwing logic:
142+ * - Non-aggregatable exceptions: throw the first one immediately
143+ * - Aggregatable exceptions: aggregate and throw as InvariantViolation
144+ *
145+ * @param array<string, Throwable> $violations
146+ * @param string $exception
147+ * @return void
148+ * @throws Throwable
149+ */
150+ private function throwInvariantViolations (array $ violations , string $ exception ): void
151+ {
152+ // Separate aggregatable from non-aggregatable violations
153+ $ aggregatable = [];
154+ $ nonAggregatable = [];
155+
156+ foreach ($ violations as $ key => $ violation ) {
157+ if ($ violation instanceof Aggregatable) {
158+ $ aggregatable [$ key ] = $ violation ;
159+ } else {
160+ $ nonAggregatable [$ key ] = $ violation ;
126161 }
162+ }
127163
128- throw new $ exception ( // @phpstan-ignore-line
129- sprintf (
130- "Unable to create %s due: %s " ,
131- basename (str_replace ('\\' , '/ ' , static ::class)),
132- implode (", " , map (fn (Throwable $ e ): string => $ e ->getMessage (), $ violations )),
133- )
134- );
135- };
164+ // If there are non-aggregatable exceptions, throw the first one immediately
165+ if (!empty ($ nonAggregatable )) {
166+ throw array_shift ($ nonAggregatable );
167+ }
168+
169+ // All violations are aggregatable - aggregate them
170+ if (!empty ($ aggregatable )) {
171+ $ messages = map (fn (Throwable $ e ): string => $ e ->getMessage (), $ aggregatable );
172+ throw InvariantViolation::fromViolations (array_values ($ messages ));
173+ }
136174 }
137175}
0 commit comments