A practical example of using Symfony PropertyInfo component
Tags: php, symfony, reflection
Published: 2023-02-15 09:45:43

As [symfony property info documentation](https://symfony.com/doc/current/components/property_info.html) 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:

```json
{
   "post_id" : 668,
   "user_id" : 335,
   "text" : "Yes! I agree with you",
   "date" : "2023-02-14 16:15:14"
}
```
Let's imagine now we manage our database with doctrine and our platform holds the following entities (among others):

```php
#[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;
}
```

```php
#[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;
}
```

```php
#[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;
}
```
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](https://symfony.com/doc/current/components/serializer.html).

Let's see how our DTO looks like:

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

> 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:

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

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.

```php
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;
    }
}
```

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

```php
$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);
     }
}
```
> In order to camelize strings, we relie on [symfony string component functions](https://symfony.com/doc/current/components/string.html)

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