DEV Community

Nacho Colomina Torregrosa
Nacho Colomina Torregrosa

Posted on

A practical example of using Symfony PropertyInfo component

As symfony property info documentation says, this component allows developers to get information about class properties such as typehint, whether is nullable or not etc.
In this post, I would like to show you a practical example of using it.

Imagine we've a blog application and we expose an api by which we can send posts and comments. When sending a comment we receive a payload like this:

{
   "post_id" : 668,
   "user_id" : 335,
   "text" : "Yes! I agree with you",
   "date" : "2023-02-14 16:15:14"
}
Enter fullscreen mode Exit fullscreen mode

Let's imagine now we manage our database with doctrine and our platform holds the following entities (among others):

#[ORM\Entity(repositoryClass:PostRepository::class)]
class Post {

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $title;

    #[ORM\Column(type: Types::TEXT)]
    private string $text;

    #[ORM\Column]
    private \DateTimeImmutable $date;
}
Enter fullscreen mode Exit fullscreen mode
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User {

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $name;

    #[ORM\Column(length: 255)]
    private string $email;
}
Enter fullscreen mode Exit fullscreen mode
#[ORM\Entity(repositoryClass: CommentRepository::class)]
class Comment {

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne]
    private Post $post;

    #[ORM\ManyToOne]
    private User $user;

    #[ORM\Column(type: Types::TEXT)]
    private string $text;

    #[ORM\Column]
    private \DateTimeImmutable $date;
}
Enter fullscreen mode Exit fullscreen mode

Now, as the comment payload comes with post and user identifiers, we will need to transform it to entities so we can create entity comment properly. To achieve this, we're creating first an input DTO which will hold payload properties. We will fill later input DTO using symfony serializer.

Let's see how our DTO looks like:

class CommentInputDto {
    public function __construct(
        public readonly int $postId,
        public readonly int $userId,
        public readonly string $text,
        public readonly string $date
    ){ }
}
Enter fullscreen mode Exit fullscreen mode

In this post we use entity id as an identifiers but in a real world would be a better idea using, for instance, encoded uuid's and not expose your database ids, especially if they are auto-incremental.

Now, let's see how deserialize payload data to our input dto. In our controller route, we should do the following:

  class BlogController extends AbstractController 
  { 
      public function commentAction(Request $request, SerializerInterface $serializer): JsonResponse
    {
        $inputDto = $serializer->deserialize($request->getContent(), CommentInputDto::class, 'json');
    }
  }
Enter fullscreen mode Exit fullscreen mode

At this point, we have to transform postId and userId values into their corresponding entities.

Let's see how property info can help us here. First of all let's add a getIdentifiers method to our input Dto and two more properties (post and user) which will hold the corresponding entities.

class CommentInputDto {

    private ?Post $post = null;
    private ?User $user = null;

    public function __construct(
        public readonly int $postId,
        public readonly int $userId,
        public readonly string $text,
        public readonly string $date
    ){ }

    public function getIdentifiers(): array
    {
       return [
          "postId",
          "userId"
       ];
    }

    public function getUser(): ?User
    {
       return $this->user;
    }

    public function setUser(?User $user): void
    {
       $this->user = $user;
    }

    public function getPost(): ?Post
    {
       return $this->post;
    }

    public function setPost(?Post $post): void
    {
       $this->post = $post;
    }
}
Enter fullscreen mode Exit fullscreen mode

And now let's write the code which will transform ids to entities:

$propertyInfo = new PropertyInfoExtractor([new ReflectionExtractor()], [new ReflectionExtractor()]);

foreach ($inputDto->getIdentifiers() as $idProp) {
      $objectProp = preg_replace('#(\_id|Id)$#', '', $idProp);
      $types = $propertyInfo->getTypes(get_class($input), $objectProp);
      if(!empty($types)){

          $objectPropSetter = 'set' . u($objectProp)->camel();
          $idValue          = $input->$idProp;

          $propertyInfoType = $types[0];
          $entityClass      = $propertyInfoType->getClassName();
          $object           = $this->em->getRepository($entityClass)->findOneBy(['id' => $idValue]);
          $input->$objectPropSetter($object);
     }
}
Enter fullscreen mode Exit fullscreen mode

In order to camelize strings, we relie on symfony string component functions

Let's see above code step by step:

  • Create propertyInfo object. Reflection extractor extract info for all class properties.
  • Loop all input dto identifiers
  • Foreach identifier property:
    • Gets its corresponding object property removing _id or Id from idProp. For instance:
      • postId: post
    • Get types from object prop. If there are types gets the first (we assume properties only have one typehint, no unions).
    • Gets object prop setter from $objectProp and id value from $input->$idProp. For instance:
      • $objectPropSetter -> "setPost"
      • $idValue -> 668
    • Get typehint (using propertyInfoType->getClassName()) from object property. Typehint is the corresponding entity class. For instance:
      • private ?Post $post : "App\Entity\Post"
    • Retrieve entity from doctrine using $entityClass and id value holded in $idValue
    • Set retrieved entity using $objectPropSetter

And that's all, following this way we can transform our id's to entities and we could use it with many payloads. Those payloads should have defined its corresponding input dto.

Obviously, there can be better ways to do this. Maybe many developers would prefer keeping Input Dto inmutable and moving id's to entity transformation to another service. I only want to show how PropertyInfo could help in this kind of situations.

I hope this can help or, at least, serve as a basis for other ideas

Top comments (3)

Collapse
 
vinceamstoutz profile image
Vincent Amstoutz

Thanks for your sharing!

Collapse
 
aerendir profile image
Adamo Crespi

This is a good exercise but I think this is not the best approach. A better one would be to use a Symfony Serializer Normalizer.

  1. You already use it to populate the InputDto object
  2. You don’t have to add the method getIdentifiers() (another point to remember to update… It is so “Laravelish”)
  3. You can rely on auto wiring in the normalizer to inject the repository class and have composition over inheritance respected.

I like this post for the good example of a possible use of the PropertyInfo component, but I think that , IN THIS SPECIFIC CASE, it is not the best component to use.

Hoping this comment will give you another useful point of view ❤️ thank you for your one…

Collapse
 
icolomina profile image
Nacho Colomina Torregrosa

Hi Adamo! Many thanks for answering. It's good to read another point of view.

Yes, you are right. Using a symfony denormalizer would be a better idea. You can use the denormalizer to get post and user entities and you will get a ready inputDto without using PropertyInfo.

Many Thanks!!