44
55namespace ComplexHeart \Domain \Model ;
66
7- use ComplexHeart \Domain \Model \Exceptions \InstantiationException ;
87use ComplexHeart \Domain \Model \Traits \HasAttributes ;
98use ComplexHeart \Domain \Model \Traits \HasInvariants ;
10- use Doctrine \Instantiator \Exception \ExceptionInterface ;
11- use Doctrine \Instantiator \Instantiator ;
12- use RuntimeException ;
139
1410/**
1511 * Trait IsModel
1612 *
13+ * Provides type-safe object instantiation with automatic invariant checking.
14+ *
15+ * Key improvements in this version:
16+ * - Type-safe make() method with validation
17+ * - Automatic invariant checking after construction
18+ * - Constructor as single source of truth
19+ * - Better error messages
20+ *
1721 * @author Unay Santisteban <usantisteban@othercode.io>
18- * @package ComplexHeart\Domain\Model\Traits
1922 */
2023trait IsModel
2124{
2225 use HasAttributes;
2326 use HasInvariants;
2427
2528 /**
26- * Initialize the Model. Just as the constructor will do.
29+ * Create instance with type-safe validation.
30+ *
31+ * This method:
32+ * 1. Validates parameter types against constructor signature
33+ * 2. Creates instance through constructor (type-safe)
34+ * 3. Invariants are checked automatically after construction
35+ *
36+ * @param mixed ...$params Constructor parameters
37+ * @return static
38+ * @throws \InvalidArgumentException When required parameters are missing
39+ * @throws \TypeError When parameter types don't match
40+ */
41+ final public static function make (mixed ...$ params ): static
42+ {
43+ $ reflection = new \ReflectionClass (static ::class);
44+ $ constructor = $ reflection ->getConstructor ();
45+
46+ if (!$ constructor ) {
47+ throw new \RuntimeException (
48+ sprintf ('%s must have a constructor to use make() ' , static ::class)
49+ );
50+ }
51+
52+ // Validate parameters against constructor signature
53+ // array_values ensures we have a proper indexed array
54+ self ::validateConstructorParameters ($ constructor , array_values ($ params ));
55+
56+ // Create instance through constructor (PHP handles type enforcement)
57+ // @phpstan-ignore-next-line - new static() is safe here as we validated the constructor
58+ $ instance = new static (...$ params );
59+
60+ // Auto-check invariants if enabled
61+ $ instance ->autoCheckInvariants ();
62+
63+ return $ instance ;
64+ }
65+
66+ /**
67+ * Validate parameters match constructor signature.
68+ *
69+ * @param \ReflectionMethod $constructor
70+ * @param array<int, mixed> $params
71+ * @return void
72+ * @throws \InvalidArgumentException
73+ * @throws \TypeError
74+ */
75+ private static function validateConstructorParameters (
76+ \ReflectionMethod $ constructor ,
77+ array $ params
78+ ): void {
79+ $ constructorParams = $ constructor ->getParameters ();
80+ $ required = $ constructor ->getNumberOfRequiredParameters ();
81+
82+ // Check parameter count
83+ if (count ($ params ) < $ required ) {
84+ $ missing = array_slice ($ constructorParams , count ($ params ), $ required - count ($ params ));
85+ $ names = array_map (fn ($ p ) => $ p ->getName (), $ missing );
86+ throw new \InvalidArgumentException (
87+ sprintf (
88+ '%s::make() missing required parameters: %s ' ,
89+ basename (str_replace ('\\' , '/ ' , static ::class)),
90+ implode (', ' , $ names )
91+ )
92+ );
93+ }
94+
95+ // Validate types for each parameter
96+ foreach ($ constructorParams as $ index => $ param ) {
97+ if (!isset ($ params [$ index ])) {
98+ continue ; // Optional parameter not provided
99+ }
100+
101+ $ value = $ params [$ index ];
102+ $ type = $ param ->getType ();
103+
104+ if (!$ type instanceof \ReflectionNamedType) {
105+ continue ; // No type hint or union type
106+ }
107+
108+ $ typeName = $ type ->getName ();
109+ $ isValid = self ::validateType ($ value , $ typeName , $ type ->allowsNull ());
110+
111+ if (!$ isValid ) {
112+ throw new \TypeError (
113+ sprintf (
114+ '%s::make() parameter "%s" must be of type %s, %s given ' ,
115+ basename (str_replace ('\\' , '/ ' , static ::class)),
116+ $ param ->getName (),
117+ $ typeName ,
118+ get_debug_type ($ value )
119+ )
120+ );
121+ }
122+ }
123+ }
124+
125+ /**
126+ * Validate a value matches expected type.
127+ *
128+ * @param mixed $value
129+ * @param string $typeName
130+ * @param bool $allowsNull
131+ * @return bool
132+ */
133+ private static function validateType (mixed $ value , string $ typeName , bool $ allowsNull ): bool
134+ {
135+ if ($ value === null ) {
136+ return $ allowsNull ;
137+ }
138+
139+ return match ($ typeName ) {
140+ 'int ' => is_int ($ value ),
141+ 'float ' => is_float ($ value ) || is_int ($ value ), // Allow int for float
142+ 'string ' => is_string ($ value ),
143+ 'bool ' => is_bool ($ value ),
144+ 'array ' => is_array ($ value ),
145+ 'object ' => is_object ($ value ),
146+ 'callable ' => is_callable ($ value ),
147+ 'iterable ' => is_iterable ($ value ),
148+ 'mixed ' => true ,
149+ default => $ value instanceof $ typeName
150+ };
151+ }
152+
153+ /**
154+ * Determine if invariants should be checked automatically after construction.
155+ *
156+ * Override this method in your class to disable auto-check:
157+ *
158+ * protected function shouldAutoCheckInvariants(): bool
159+ * {
160+ * return false;
161+ * }
162+ *
163+ * @return bool
164+ */
165+ protected function shouldAutoCheckInvariants (): bool
166+ {
167+ return false ; // Disabled by default for backward compatibility
168+ }
169+
170+ /**
171+ * Called after construction to auto-check invariants.
172+ *
173+ * This method is automatically called after the constructor completes
174+ * if shouldAutoCheckInvariants() returns true.
175+ *
176+ * @return void
177+ */
178+ private function autoCheckInvariants (): void
179+ {
180+ if ($ this ->shouldAutoCheckInvariants ()) {
181+ $ this ->check ();
182+ }
183+ }
184+
185+ /**
186+ * Initialize the Model (legacy method - DEPRECATED).
187+ *
188+ * @deprecated Use constructor with make() factory method instead.
189+ * This method will be removed in v1.0.0
27190 *
28191 * @param array<int|string, mixed> $source
29192 * @param string|callable $onFail
30- *
31193 * @return static
32194 */
33195 protected function initialize (array $ source , string |callable $ onFail = 'invariantHandler ' ): static
@@ -39,17 +201,16 @@ protected function initialize(array $source, string|callable $onFail = 'invarian
39201 }
40202
41203 /**
42- * Transform an indexed array into assoc array by combining the
43- * given values with the list of attributes of the object.
204+ * Transform an indexed array into assoc array (legacy method - DEPRECATED).
44205 *
206+ * @deprecated This method will be removed in v1.0.0
45207 * @param array<int|string, mixed> $source
46- *
47208 * @return array<string, mixed>
48209 */
49210 private function prepareAttributes (array $ source ): array
50211 {
51212 // check if the array is indexed or associative.
52- $ isIndexed = fn ($ source ): bool => ([] !== $ source ) && array_keys ($ source ) === range (0 , count ($ source ) - 1 );
213+ $ isIndexed = fn ($ source ): bool => ([] !== $ source ) && array_keys ($ source ) === range (0 , count ($ source ) - 1 );
53214
54215 /** @var array<string, mixed> $source */
55216 return $ isIndexed ($ source )
@@ -58,22 +219,4 @@ private function prepareAttributes(array $source): array
58219 // return the already mapped array source.
59220 : $ source ;
60221 }
61-
62- /**
63- * Restore the instance without calling __constructor of the model.
64- *
65- * @return static
66- *
67- * @throws RuntimeException
68- */
69- final public static function make (): static
70- {
71- try {
72- return (new Instantiator ())
73- ->instantiate (static ::class)
74- ->initialize (func_get_args ());
75- } catch (ExceptionInterface $ e ) {
76- throw new InstantiationException ($ e ->getMessage (), $ e ->getCode (), $ e );
77- }
78- }
79222}
0 commit comments