Locastic / ApiPlatformTranslationBundle

Translation bundle for ApiPlatform based on Sylius translation
MIT License
85 stars 28 forks source link

Post requests failing on a fresh install #32

Closed GS-Wacan closed 3 years ago

GS-Wacan commented 3 years ago

I just installed Locastic Translation Bundle in a new API Platform Symfony project. Everything works fine except POST requests, that return me an error.

I followed the detailed guide in the Readme and created my translatable entity, and setup the Locale.

I can translate existing content using PATCH requests, GET requests acknoledge my locale correctly and provide me translated informations in the right language with no error, so I think everything is setup correctly.

The error that POST requests return :

Return value of Locastic\\ApiPlatformTranslationBundle\\Model\\AbstractTranslatable::getTranslations() must implement interface Doctrine\\Common\\Collections\\Collection, null returned

"file": "C:\\Users\\user\\dev\\symfony-project\\vendor\\locastic\\api-platform-translation-bundle\\src\\Model\\TranslatableTrait.php",
"line": 138,

Here's my entity ( Challenge.php )

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use App\Repository\ChallengeRepository;
use Doctrine\Common\Collections\Collection;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Annotation\Groups;
use Locastic\ApiPlatformTranslationBundle\Model\AbstractTranslatable;
use Locastic\ApiPlatformTranslationBundle\Model\TranslationInterface;

/**
 * @ApiResource(
 *      attributes={
 *              "filters"={"translation.groups"},
 *              "normalization_context"={"groups"={"chall:read"}},
 *              "denormalization_context"={"groups"={"chall:write"}}
 *      },
 *      collectionOperations={
 *         "get",
 *         "post"={
 *             "normalization_context"={"groups"={"translations"}}
 *         }
 *      },
 *      itemOperations={
 *         "get",
 *         "put"={
 *              "normalization_context"={"groups"={"translations"}}
 *          },
 *         "delete"
 *      }
 * )
 * @ORM\Entity(repositoryClass=ChallengeRepository::class)
 */
class Challenge extends AbstractTranslatable
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"chall:read"})
     */
    private $title;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"chall:read", "chall:write"})
     */
    private $code;

    /**
     * @ORM\Column(type="text")
     * @Groups({"chall:read"})
     */
    private $description;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     * @Groups({"chall:read", "chall:write"})
     */
    private $startAt;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     * @Groups({"chall:read", "chall:write"})
     */
    private $endAt;

    /**
     * @ORM\Column(type="datetime", nullable=true)
     * @Groups({"chall:read", "chall:write"})
     */
    private $registryLimitDate;

    /**
     * @ORM\Column(type="integer", nullable=true)
     * @Groups({"chall:read", "chall:write"})
     */
    private $maxSlots;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     * @Groups({"chall:read", "chall:write"})
     */
    private $image;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     * @Groups({"chall:read", "chall:write"})
     */
    private $status;

    /**
     * @ORM\ManyToMany(targetEntity=Course::class, mappedBy="challenges")
     * @Groups({"chall:read", "chall:write"})
     */
    private $courses;

    /**
     * @ORM\OneToMany(targetEntity="ChallengeTranslation", mappedBy="translatable", fetch="EXTRA_LAZY", indexBy="locale", cascade={"PERSIST"}, orphanRemoval=true)
     * @Groups({"chall:write", "translations"})
     */
    protected $translations;

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

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

    public function getTitle(): ?string
    {
        return $this->getTranslation()->getTitle();
    }

    public function setTitle(string $title): self
    {
        $this->getTranslation()->setTitle($title);

        return $this;
    }

    public function getCode(): ?string
    {
        return $this->code;
    }

    public function setCode(string $code): self
    {
        $this->code = $code;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->getTranslation()->getDescription();
    }

    public function setDescription(string $description): self
    {
        $this->getTranslation()->setDescription($title);

        return $this;
    }

    public function getStartAt(): ?\DateTimeInterface
    {
        return $this->startAt;
    }

    public function setStartAt(?\DateTimeInterface $startAt): self
    {
        $this->startAt = $startAt;

        return $this;
    }

    public function getEndAt(): ?\DateTimeInterface
    {
        return $this->endAt;
    }

    public function setEndAt(?\DateTimeInterface $endAt): self
    {
        $this->endAt = $endAt;

        return $this;
    }

    public function getRegistryLimitDate(): ?\DateTimeInterface
    {
        return $this->registryLimitDate;
    }

    public function setRegistryLimitDate(?\DateTimeInterface $registryLimitDate): self
    {
        $this->registryLimitDate = $registryLimitDate;

        return $this;
    }

    public function getMaxSlots(): ?int
    {
        return $this->maxSlots;
    }

    public function setMaxSlots(?int $maxSlots): self
    {
        $this->maxSlots = $maxSlots;

        return $this;
    }

    public function getImage(): ?string
    {
        return $this->image;
    }

    public function setImage(?string $image): self
    {
        $this->image = $image;

        return $this;
    }

    public function getStatus(): ?string
    {
        return $this->status;
    }

    public function setStatus(?string $status): self
    {
        $this->status = $status;

        return $this;
    }

    /**
     * @return Collection|Course[]
     */
    public function getCourses(): Collection
    {
        return $this->courses;
    }

    public function addCourse(Course $course): self
    {
        if (!$this->courses->contains($course)) {
            $this->courses[] = $course;
            $course->addChallenge($this);
        }

        return $this;
    }

    public function removeCourse(Course $course): self
    {
        if ($this->courses->contains($course)) {
            $this->courses->removeElement($course);
            $course->removeChallenge($this);
        }

        return $this;
    }

    protected function createTranslation(): TranslationInterface
    {
        return new ChallengeTranslation();
    }
}

And my translated entity ( ChallengeTranslation.php )

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use App\Repository\ChallengeTranslationRepository;
use Symfony\Component\Serializer\Annotation\Groups;
use Locastic\ApiPlatformTranslationBundle\Model\AbstractTranslation;

/**
 * @ORM\Entity(repositoryClass=ChallengeTranslationRepository::class)
 */
class ChallengeTranslation extends AbstractTranslation
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Challenge", inversedBy="translations")
     */
    protected $translatable;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"chall:read", "chall:write", "translations"})
     */
    private $title;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"chall:read", "chall:write", "translations"})
     */
    private $description;

    /**
     * @ORM\Column(type="string")
     * @Groups({"chall:write", "translations"})
     */
    protected $locale;

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

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(string $title): self
    {
        $this->title = $title;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(string $description): self
    {
        $this->description = $description;

        return $this;
    }
}

Is this a common issue ? A little help would be much appreciated as almost everything is working !

paullla commented 3 years ago

Hi @GS-Wacan, I think you forgot to call parent constructor in Challenge and that's why translation collection didn't initialise. Let me know if this helps :)

GS-Wacan commented 3 years ago

Thank you for your quick answer ! It works perfectly now :).

I must have messed around with the properties when trying to figure out how it works and remove this line 👍