Skip to content

#[ApiProperty(readableLink: true, writableLink: false)] wrong OpenApi spec generation #5988

@Fr13nzzz

Description

@Fr13nzzz

API Platform version(s) affected: 3.2.5

Description
When setting #[ApiProperty(readableLink: true, writableLink: false)] to a property, writableLink is not respected in the openapi spec generation under certain circumstances:

  1. The entity serialization groups are organized by the same names. For example operation based names, so that multiple entities have e.g. serialization group "post" for "POST" operations.
  2. A parent entity uses writableLink=true for a child entity
  3. The child entity also uses writableLink=true for its child entity
  4. The childs schema is generated after the parent entity schema.

If this is the case writableLink=false will not be generated properly in the first childrecord for its childrecords properties configured with writableLink=false.

How to reproduce
Following classes and configurations are needed:

Company.php:

#[ApiResource(
    operations: [
        new Post(denormalizationContext: ['groups' => ['post']]),
    ]
)]
#[ORM\Entity(repositoryClass: CompanyRepository::class)]
class Company
{
    #[ORM\Id]
    #[ORM\Column(type: UuidType::NAME, unique: true, nullable: false)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
    private ?Uuid $id = null;

    #[Groups(['post'])]
    #[ApiProperty(readableLink: true, writableLink: true)]
    #[ORM\OneToMany(mappedBy: 'company', targetEntity: User::class, cascade: ['persist'], orphanRemoval: true)]
    #[ORM\JoinColumn(nullable: false)]
    private iterable $users;

    public function __construct()
    {
        $this->users = new ArrayCollection();
    }

    public function getId(): Uuid
    {
        return $this->id;
    }

    public function getUsers(): iterable
    {
        return $this->users;
    }

    public function setUsers(iterable $users): self
    {
        $this->users = $users;
        return $this;
    }

    public function addUser(User $user): self
    {
        if (!$this->users->contains($user)) {
            $this->users->add($user);
            $user->setCompany($this);
        }
        return $this;
    }

    public function removeUser(User $user): self
    {
        $this->users->removeElement($user);
        return $this;
    }

    public function getUsers(): iterable
    {
        return $this->users;
    }

    public function addUser(User $user): self
    {
        if (!$this->users->contains($user)) {
            $this->users->add($user);
            $user->setCompany($this);
        }
        return $this;
    }

    public function removeUser(User $user): self
    {
        $this->users->removeElement($user);
        return $this;
    }
}

User.php

#[ApiResource(
    operations: [
        new Post(denormalizationContext: ['groups' => ['post']]),
    ]
)]
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
    #[ORM\Id]
    #[ORM\Column(type: UuidType::NAME, unique: true)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
    private ?Uuid $id = null;

    #[ORM\ManyToOne(targetEntity: Company::class, inversedBy: 'users')]
    private ?Company $company = null;

    #[Groups(['post'])]
    #[ORM\OneToOne(mappedBy: 'owner', targetEntity: UserSettings::class)]
    #[ApiProperty(readableLink: true, writableLink: false)]
    private ?UserSettings $userSettings = null;

    public function getId(): Uuid
    {
        return $this->id;
    }

    public function getUserSettings(): UserSettings
    {
        return $this->userSettings;
    }

    public function setUserSettings(UserSettings $userSettings): self
    {
        $this->userSettings = $userSettings;
        return $this;
    }
}

UserSettings.php

#[ApiResource(
    operations: [
        new Post(denormalizationContext: ['groups' => ['post']]),
    ]
)]
#[ORM\Entity(repositoryClass: UserSettingsRepository::class)]
class UserSettings
{
    #[ORM\Id]
    #[ORM\Column(type: UuidType::NAME, unique: true)]
    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
    private ?Uuid $id = null;

    #[Groups(['post'])]
    #[ORM\Column(length: 255, nullable: true)]
    private ?string $settingOne = null;

    #[Groups(['post'])]
    #[ORM\Column(length: 255, nullable: true)]
    private ?string $settingTwo = null;

    #[ORM\OneToOne(inversedBy: 'userSettings')]
    #[ORM\JoinColumn(nullable: false)]
    private User $owner;

    public function getId(): Uuid
    {
        return $this->id;
    }

    public function getOwner(): ?User
    {
        return $this->owner;
    }

    public function setOwner(User $owner): self
    {
        $this->owner = $owner;

        return $this;
    }

    public function getSettingOne(): ?string
    {
        return $this->settingOne;
    }

    public function setSettingOne(?string $settingOne): self
    {
        $this->settingOne = $settingOne;
        return $this;
    }

    public function getSettingTwo(): ?string
    {
        return $this->settingTwo;
    }

    public function setSettingTwo(?string $settingTwo): self
    {
        $this->settingTwo = $settingTwo;
        return $this;
    }
}

As company uses the user-post scheme and the entity starts with a c. The user-post scheme is generated when creating the company scheme, which leads to this behaviour.

This is a usecase to me and i think also to others who use operation based serialization groups.

Possible Solution
Passing the schema type of the parent entity when building child schemes. I will provide a pull request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions