@@ -151,33 +151,49 @@ public function prepend(ContainerBuilder $container): void
151151 * Merge config-supplied anonymization rules into the attribute-discovered set.
152152 * For the same property name, the config wins (it's the explicit override).
153153 *
154+ * $configRules comes from a variableNode in the config tree, so its shape is
155+ * not enforced upstream — every layer is validated here. Class keys are not
156+ * required to exist locally (the bundle supports overrides for third-party
157+ * entities that may live in unloaded namespaces).
158+ *
154159 * @param array<class-string, list<array{property: string, strategy: string, replacement: ?string}>> $discovered
155- * @param array<string, array<string, array{strategy: string, replacement?: ?string}>> $configRules
160+ * @param array<string, mixed> $configRules
156161 *
157- * @return array<class- string, list<array{property: string, strategy: string, replacement: ?string}>>
162+ * @return array<string, list<array{property: string, strategy: string, replacement: ?string}>>
158163 */
159164 private function mergeAnonymizationRules (array $ discovered , array $ configRules ): array
160165 {
166+ /** @var array<string, list<array{property: string, strategy: string, replacement: ?string}>> $merged */
167+ $ merged = $ discovered ;
168+
161169 foreach ($ configRules as $ class => $ propRules ) {
170+ if (!\is_array ($ propRules )) {
171+ throw new InvalidConfigurationException (sprintf ('itk_dev_entity.anonymization.rules[%s] must be a map of property => { strategy: ..., replacement?: ... }. ' , $ class ));
172+ }
173+
162174 /** @var array<string, array{property: string, strategy: string, replacement: ?string}> $byProp */
163175 $ byProp = [];
164- foreach ($ discovered [$ class ] ?? [] as $ rule ) {
176+ foreach ($ merged [$ class ] ?? [] as $ rule ) {
165177 $ byProp [$ rule ['property ' ]] = $ rule ;
166178 }
167179 foreach ($ propRules as $ property => $ spec ) {
168- if (!\is_array ($ spec ) || !isset ($ spec ['strategy ' ])) {
169- throw new InvalidConfigurationException (sprintf ('itk_dev_entity.anonymization.rules[%s][%s] must be { strategy: ..., replacement?: ... } ' , $ class , $ property ));
180+ if (!\is_string ($ property ) || !\is_array ($ spec ) || !isset ($ spec ['strategy ' ]) || !\is_string ($ spec ['strategy ' ])) {
181+ throw new InvalidConfigurationException (sprintf ('itk_dev_entity.anonymization.rules[%s][%s] must be { strategy: ..., replacement?: ... } ' , $ class , (string ) $ property ));
182+ }
183+ $ replacement = $ spec ['replacement ' ] ?? null ;
184+ if (null !== $ replacement && !\is_string ($ replacement )) {
185+ throw new InvalidConfigurationException (sprintf ('itk_dev_entity.anonymization.rules[%s][%s].replacement must be a string or null. ' , $ class , $ property ));
170186 }
171187 $ byProp [$ property ] = [
172188 'property ' => $ property ,
173- 'strategy ' => ( string ) $ spec ['strategy ' ],
174- 'replacement ' => $ spec [ ' replacement ' ] ?? null ,
189+ 'strategy ' => $ spec ['strategy ' ],
190+ 'replacement ' => $ replacement ,
175191 ];
176192 }
177- $ discovered [$ class ] = array_values ($ byProp );
193+ $ merged [$ class ] = array_values ($ byProp );
178194 }
179195
180- return $ discovered ;
196+ return $ merged ;
181197 }
182198
183199 /**
@@ -209,7 +225,6 @@ private function discoverAuditIgnoredColumns(string $class): array
209225 private function discoverEntities (ContainerBuilder $ container , array $ paths ): array
210226 {
211227 $ projectDir = $ container ->getParameter ('kernel.project_dir ' );
212- \assert (\is_string ($ projectDir ));
213228
214229 $ entities = [];
215230 foreach ($ paths as $ path ) {
@@ -241,6 +256,8 @@ private function discoverEntities(ContainerBuilder $container, array $paths): ar
241256 * Walk the class hierarchy looking for #[ITKDevEntity]. The attribute is non-inherited
242257 * at the language level, but the bundle treats inherited declarations as opting in —
243258 * so subclasses of AbstractITKDevEntity automatically count.
259+ *
260+ * @param \ReflectionClass<object> $ref
244261 */
245262 private function hasITKDevEntityAttribute (\ReflectionClass $ ref ): bool
246263 {
0 commit comments