22
33namespace Azura \Normalizer ;
44
5+ use ArrayObject ;
56use Azura \Normalizer \Attributes \DeepNormalize ;
67use Azura \Normalizer \Exception \NoGetterAvailableException ;
7- use ArrayObject ;
8+ use Azura \ Normalizer \ TypeExtractor \ EntityTypeExtractor ;
89use Doctrine \Common \Collections \Collection ;
9- use Doctrine \Inflector \Inflector ;
10- use Doctrine \Inflector \InflectorFactory ;
1110use Doctrine \ORM \EntityManagerInterface ;
1211use Doctrine \ORM \Proxy \DefaultProxyClassNameResolver ;
1312use InvalidArgumentException ;
1413use ReflectionClass ;
1514use ReflectionException ;
1615use ReflectionProperty ;
17- use Symfony \Component \PropertyInfo \PropertyTypeExtractorInterface ;
1816use Symfony \Component \Serializer \Mapping \AttributeMetadataInterface ;
1917use Symfony \Component \Serializer \Mapping \Factory \ClassMetadataFactoryInterface ;
2018use Symfony \Component \Serializer \Normalizer \AbstractNormalizer ;
2119use Symfony \Component \Serializer \Normalizer \AbstractObjectNormalizer ;
2220
2321final class DoctrineEntityNormalizer extends AbstractObjectNormalizer
2422{
25- private const CLASS_METADATA = 'class_metadata ' ;
26- private const ASSOCIATION_MAPPINGS = 'association_mappings ' ;
23+ public const CLASS_METADATA = 'class_metadata ' ;
24+ public const ASSOCIATION_MAPPINGS = 'association_mappings ' ;
2725
2826 public const NORMALIZE_TO_IDENTIFIERS = 'form_mode ' ;
2927
30- private readonly Inflector $ inflector ;
28+ private EntityTypeExtractor $ typeExtractor ;
3129
3230 public function __construct (
3331 private readonly EntityManagerInterface $ em ,
3432 ?ClassMetadataFactoryInterface $ classMetadataFactory = null ,
35- ?PropertyTypeExtractorInterface $ propertyTypeExtractor = null ,
3633 array $ defaultContext = []
3734 ) {
3835 $ defaultContext [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES ] = true ;
3936
37+ $ this ->typeExtractor = new EntityTypeExtractor ();
38+
4039 parent ::__construct (
4140 classMetadataFactory: $ classMetadataFactory ,
42- propertyTypeExtractor: $ propertyTypeExtractor ,
41+ propertyTypeExtractor: $ this -> typeExtractor ,
4342 defaultContext: $ defaultContext
4443 );
45-
46- $ this ->inflector = InflectorFactory::create ()->build ();
4744 }
4845
4946 /**
@@ -153,7 +150,7 @@ protected function getAllowedAttributes(
153150
154151 protected function extractAttributes (object $ object , ?string $ format = null , array $ context = []): array
155152 {
156- $ rawProps = ( new ReflectionClass ($ object) )->getProperties (
153+ $ rawProps = new ReflectionClass ($ object )->getProperties (
157154 ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED
158155 );
159156
@@ -186,39 +183,34 @@ protected function isAllowedAttribute(
186183 return false ;
187184 }
188185
189- $ reflectionClass = new ReflectionClass ($ classOrObject );
190- if (!$ reflectionClass ->hasProperty ($ attribute )) {
191- return false ;
192- }
186+ $ class = \is_object ($ classOrObject ) ? $ classOrObject ::class : $ classOrObject ;
193187
194188 if (isset ($ context [self ::CLASS_METADATA ]->associationMappings [$ attribute ])) {
195- if (!$ this ->supportsDeepNormalization ($ reflectionClass , $ attribute )) {
189+ if (!$ this ->supportsDeepNormalization ($ class , $ attribute )) {
196190 return false ;
197191 }
198192 }
199193
200- return $ this ->hasGetter ($ reflectionClass , $ attribute );
194+ return $ this ->hasGetter ($ class , $ attribute );
201195 }
202196
203197 /**
204- * @param ReflectionClass<object> $reflectionClass
198+ * @param class-string $className
205199 * @param string $attribute
206- * @return bool
200+ * @return bool Whether a getter exists that can return for this property.
207201 */
208- private function hasGetter (ReflectionClass $ reflectionClass , string $ attribute ): bool
202+ private function hasGetter (string $ className , string $ attribute ): bool
209203 {
210- if ($ reflectionClass -> hasProperty ( $ attribute ) && $ reflectionClass -> getProperty ( $ attribute )-> isPublic ( )) {
204+ if (null !== $ this -> typeExtractor -> getAccessorMethod ( $ className , $ attribute )) {
211205 return true ;
212206 }
213207
214- // Default to "getStatus", "getConfig", etc...
215- $ getterMethod = $ this ->getMethodName ($ attribute , 'get ' );
216- if ($ reflectionClass ->hasMethod ($ getterMethod )) {
217- return true ;
218- }
208+ try {
209+ $ reflProp = new ReflectionProperty ($ className , $ attribute );
210+ return $ reflProp ->isPublic ();
211+ } catch (ReflectionException ) {}
219212
220- $ rawMethod = $ this ->getMethodName ($ attribute );
221- return $ reflectionClass ->hasMethod ($ rawMethod );
213+ return false ;
222214 }
223215
224216 protected function getAttributeValue (
@@ -230,7 +222,7 @@ protected function getAttributeValue(
230222 $ formMode = $ context [self ::NORMALIZE_TO_IDENTIFIERS ] ?? false ;
231223
232224 if (isset ($ context [self ::CLASS_METADATA ]->associationMappings [$ attribute ])) {
233- if (!$ this ->supportsDeepNormalization (new ReflectionClass ( $ object) , $ attribute )) {
225+ if (!$ this ->supportsDeepNormalization ($ object::class , $ attribute )) {
234226 throw new NoGetterAvailableException (
235227 sprintf (
236228 'Deep normalization disabled for property %s. ' ,
@@ -241,6 +233,8 @@ protected function getAttributeValue(
241233 }
242234
243235 $ value = $ this ->getProperty ($ object , $ attribute );
236+
237+ // Special handling for Doctrine "many-to-x" relationships (Collections)
244238 if ($ value instanceof Collection) {
245239 if ($ formMode ) {
246240 $ value = array_filter (array_map (
@@ -261,81 +255,72 @@ function(object $valObj) {
261255 }
262256
263257 /**
264- * @param ReflectionClass<object> $reflectionClass
258+ * @param class-string $className
265259 * @param string $attribute
266260 * @return bool
267- * @throws ReflectionException
268261 */
269- private function supportsDeepNormalization (ReflectionClass $ reflectionClass , string $ attribute ): bool
262+ private function supportsDeepNormalization (string $ className , string $ attribute ): bool
270263 {
271- $ deepNormalizeAttrs = $ reflectionClass -> getProperty ( $ attribute )-> getAttributes (
272- DeepNormalize::class
273- );
264+ try {
265+ $ reflProp = new ReflectionProperty ( $ className , $ attribute );
266+ $ deepNormalizeAttrs = $ reflProp -> getAttributes (DeepNormalize::class );
274267
275- if (empty ($ deepNormalizeAttrs )) {
268+ if (empty ($ deepNormalizeAttrs )) {
269+ return false ;
270+ }
271+
272+ /** @var DeepNormalize $deepNormalize */
273+ $ deepNormalize = current ($ deepNormalizeAttrs )->newInstance ();
274+ return $ deepNormalize ->getDeepNormalize ();
275+ } catch (\ReflectionException ) {
276276 return false ;
277277 }
278-
279- /** @var DeepNormalize $deepNormalize */
280- $ deepNormalize = current ($ deepNormalizeAttrs )->newInstance ();
281- return $ deepNormalize ->getDeepNormalize ();
282278 }
283279
284280 private function getProperty (object $ entity , string $ key ): mixed
285281 {
286- // Public item hook.
287- if (property_exists ($ entity , $ key )) {
288- $ reflProp = new ReflectionProperty ($ entity , $ key );
282+ if (null !== $ accessor = $ this ->typeExtractor ->getAccessorMethod ($ entity ::class, $ key )) {
283+ [$ method , $ prefix ] = $ accessor ;
284+ return $ method ->invoke ($ entity );
285+ }
286+
287+ try {
288+ $ reflProp = new ReflectionProperty ($ entity ::class, $ key );
289289 if ($ reflProp ->isPublic ()) {
290290 return $ reflProp ->getValue ($ entity );
291291 }
292- }
293-
294- // Default to "getStatus", "getConfig", etc...
295- $ getterMethod = $ this ->getMethodName ($ key , 'get ' );
296- if (method_exists ($ entity , $ getterMethod )) {
297- return $ entity ->{$ getterMethod }();
298- }
299-
300- // but also allow "isEnabled" instead of "getIsEnabled"
301- $ rawMethod = $ this ->getMethodName ($ key );
302- if (method_exists ($ entity , $ rawMethod )) {
303- return $ entity ->{$ rawMethod }();
304- }
292+ } catch (ReflectionException ) {}
305293
306294 throw new NoGetterAvailableException (sprintf ('No getter is available for property %s. ' , $ key ));
307295 }
308296
309- /**
310- * Converts "getvar_name_blah" to "getVarNameBlah".
311- */
312- private function getMethodName (string $ var , string $ prefix = '' ): string
313- {
314- return $ this ->inflector ->camelize (($ prefix ? $ prefix . '_ ' : '' ) . $ var );
315- }
316-
317297 protected function setAttributeValue (
318298 object $ object ,
319299 string $ attribute ,
320300 mixed $ value ,
321301 ?string $ format = null ,
322302 array $ context = []
323303 ): void {
304+ // Special handling for Doctrine entity relationship fields.
324305 if (isset ($ context [self ::ASSOCIATION_MAPPINGS ][$ attribute ])) {
325- // Handle a mapping to another entity.
326306 $ mapping = $ context [self ::ASSOCIATION_MAPPINGS ][$ attribute ];
327307
328308 if ('one ' === $ mapping ['type ' ]) {
309+ // Allow passing either a related object or simply its ID to a "one-to-x" relationship.
310+
311+ /** @var class-string $entity */
312+ $ entity = $ mapping ['entity ' ];
313+
329314 if (empty ($ value )) {
330315 $ this ->setProperty ($ object , $ attribute , null );
331- } else {
332- /** @var class-string $entity */
333- $ entity = $ mapping ['entity ' ];
334- if (($ fieldItem = $ this ->em ->find ($ entity , $ value )) instanceof $ entity ) {
335- $ this ->setProperty ($ object , $ attribute , $ fieldItem );
336- }
316+ } else if ($ value instanceof $ entity ) {
317+ $ this ->setProperty ($ object , $ attribute , $ value );
318+ } else if (($ fieldItem = $ this ->em ->find ($ entity , $ value )) instanceof $ entity ) {
319+ $ this ->setProperty ($ object , $ attribute , $ fieldItem );
337320 }
338321 } elseif ($ mapping ['is_owning_side ' ]) {
322+ // Convert an array of entities or identifiers to a Doctrine collection for "many-to-x" relationships.
323+
339324 $ collection = $ this ->getProperty ($ object , $ attribute );
340325
341326 if ($ collection instanceof Collection) {
@@ -355,81 +340,30 @@ protected function setAttributeValue(
355340 }
356341 }
357342 } else {
358- $ this ->setStandardValue ($ object , $ attribute , $ value );
359- }
360- }
361-
362- private function setStandardValue (
363- object $ object ,
364- string $ attribute ,
365- mixed $ value ,
366- ?string $ format = null ,
367- array $ context = []
368- ): void {
369- $ reflClass = new ReflectionClass ($ object );
370-
371- if ($ reflClass ->hasProperty ($ attribute )) {
372- $ reflProp = $ reflClass ->getProperty ($ attribute );
373-
374- if ($ reflProp ->isPublic () && !$ reflProp ->isProtectedSet () && !$ reflProp ->isPrivateSet ()) {
375- $ propType = $ reflProp ->getSettableType ();
376- if (null === $ value ) {
377- if ($ propType ->allowsNull ()) {
378- $ reflProp ->setValue ($ object , null );
379- return ;
380- }
381- } else {
382- $ reflProp ->setValue ($ object , $ value );
383- return ;
384- }
385- }
386- }
387-
388- $ methodName = $ this ->getMethodName ($ attribute , 'set ' );
389- if ($ reflClass ->hasMethod ($ methodName )) {
390- // If setter parameter is a special class, normalize to it.
391- $ methodParams = $ reflClass ->getMethod ($ methodName )->getParameters ();
392- $ parameter = $ methodParams [0 ];
393-
394- if (null === $ value ) {
395- if ($ parameter ->allowsNull ()) {
396- $ object ->$ methodName (null );
397- return ;
398- }
399- } else {
400- $ object ->$ methodName (
401- $ this ->denormalizeParameter (
402- $ reflClass ,
403- $ parameter ,
404- $ attribute ,
405- $ value ,
406- $ this ->createChildContext ($ context , $ attribute , $ format ),
407- $ format
408- )
409- );
410- return ;
411- }
343+ $ this ->setProperty ($ object , $ attribute , $ value );
412344 }
413345 }
414346
415347 private function setProperty (
416348 object $ entity ,
417- string $ attribute ,
349+ string $ key ,
418350 mixed $ value
419351 ): void {
420- // Public item hook.
421- if (property_exists ($ entity , $ attribute )) {
422- $ reflProp = new ReflectionProperty ($ entity , $ attribute );
352+ // Prefer setter if it exists.
353+ if (null !== $ mutator = $ this ->typeExtractor ->getMutatorMethod ($ entity ::class, $ key )) {
354+ [$ method , $ prefix ] = $ mutator ;
355+ $ method ->invoke ($ entity , $ value );
356+ return ;
357+ }
358+
359+ // Try directly setting on the property.
360+ try {
361+ $ reflProp = new ReflectionProperty ($ entity ::class, $ key );
423362 if ($ reflProp ->isPublic () && !$ reflProp ->isProtectedSet () && !$ reflProp ->isPrivateSet ()) {
424363 $ reflProp ->setValue ($ entity , $ value );
425364 return ;
426365 }
427- }
428-
429- $ methodName = $ this ->getMethodName ($ attribute , 'set ' );
430- if (method_exists ($ entity , $ methodName )) {
431- $ entity ->$ methodName ($ value );
432- }
366+ } catch (ReflectionException ) {}
433367 }
434368
435369 private function isEntity (mixed $ class ): bool
0 commit comments