KnpLabs / DoctrineBehaviors

Doctrine2 behavior traits that help handling Blameable, Loggable, Sluggable, SoftDeletable, Uuidable, Timestampable, Translatable, Tree behavior
http://knplabs.com
MIT License
911 stars 287 forks source link

KNP Translatable and API Platform #663

Open marvoh opened 2 years ago

marvoh commented 2 years ago

I'm attempting to combine KNP translatable entities as per documentation with the API platform with some hints from this Stackoverflow post.

Whenever I make a request with a payload that looks like this for example: {"isEnabled": true, "code": "en", "newTranslations": [ {"en": {"name":"English"}},{"de": {"name":"Englisch"}} ]}, I get the error message below:

"Could not denormalize object of type \"Knp\\DoctrineBehaviors\\Contract\\Entity\\TranslationInterface[]\", no supporting normalizer found."

I'm I supposed to implement my own normalizer?

TomasVotruba commented 2 years ago

Hi, thanks for reporting the issue. I haven't seen the code in the link on SO. Could you re-try with docs in here?

If that fails, could you send failing test case in PR to reproduce this error? I'll check what I can do to fix it then.

marvoh commented 2 years ago

This is how my entities look like: Language.php

<?php

namespace App\Entity;

use App\Repository\LanguageRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
use Knp\DoctrineBehaviors\Contract\Entity\TranslatableInterface;
use Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait;
use App\Behaviour\TranslatableOverride;
use Knp\DoctrineBehaviors\Contract\Entity\TranslationInterface;

/**
 * @ORM\Entity(repositoryClass=LanguageRepository::class)
 * @ORM\HasLifecycleCallbacks()
 * @ApiResource(
 *  normalizationContext={"groups" = {"read"}},
 *  denormalizationContext={"groups" = {"write"}}
 * )
 */
class Language implements TranslatableInterface
{
    use TranslatableTrait;
    use TranslatableOverride;
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     * @Groups({"read"})
     */
    private ?int $id = null;

    /**
     * @ORM\Column(type="datetime_immutable")
     * @Groups({"read"})
     */
    private $createdAt;

    /**
     * @ORM\Column(type="datetime_immutable", nullable=true)
     * @Groups({"read"})
     */
    private $updatedAt;

    /**
     * @ORM\Column(type="boolean")
     * @Groups({"read", "write"})
     */
    private $isEnabled;

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

    private $timezone = 'Africa/Nairobi';

    /**
     * @var Collection
     * @Groups({"read"})
     */
    protected $translations;

    /**
     * @var Collection
     * @Groups({"write"})
     */
    protected $newTranslations;

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

    public function getCreatedAt(): ?\DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeImmutable $createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    public function getUpdatedAt(): ?\DateTimeImmutable
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(?\DateTimeImmutable $updatedAt): self
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    public function getIsEnabled(): ?bool
    {
        return $this->isEnabled;
    }

    public function setIsEnabled(bool $isEnabled): self
    {
        $this->isEnabled = $isEnabled;

        return $this;
    }

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

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

        return $this;
    }

    /**
     * @throws \Exception
     * @ORM\PrePersist()
    */
    public function beforeUpdate(){
        $this->SetUpdatedAt(new \DateTimeImmutable('now', new \DateTimeZone($this->timezone)));
    }

    /**
     * @throws \Exception
     * @ORM\PrePersist()
    */
    public function beforeSave(){
        $this->setCreatedAt(new \DateTimeImmutable('now', new \DateTimeZone($this->timezone)));
    }
}

LanguageTranslation.php

<?php

namespace App\Entity;

use Knp\DoctrineBehaviors\Contract\Entity\TranslationInterface;
use Knp\DoctrineBehaviors\Model\Translatable\TranslationTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ORM\Entity
 * @ApiResource()
 */
class LanguageTranslation implements TranslationInterface
{
    use TranslationTrait;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $name;

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

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }
}

I would then expect to add/edit some translations : We need to fill the newTranslations attribute inside the Language when we are sending to the api:

"newTranslations": {
    "en": {
      "description": "Firstname"
    },
    "fr": {
      "description": "Prénom"
    }
}

I created a new trait that I called TranslatableOverride. I imported it on directly on my Entity next to ORMBehaviors\Translatable\Translation:

<?php

declare(strict_types=1);

namespace App\Behaviour;

use Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait;
trait TranslatableOverride
{

    /**
     * Set collection of new translations.
     *
     * @return ArrayCollection
     */
    public function setNewTranslations($newTranslations)
    {
        if ($newTranslations) {
            foreach ($newTranslations as $locale => $translations) {
                foreach ($translations as $key => $value) {
                    $tr = $this->translate($locale);
                    $setter = 'set' . ucfirst($key);
                    if (method_exists($tr, $setter)) {
                        $tr->{$setter}($value);
                    }
                }
            }

            $this->mergeNewTranslations();
        }
    }
}
TomasVotruba commented 2 years ago

Thanks for sharing. We'll need the test case in our /tests and see the Github Actions failing. Could you send it in pull-request?

MichaelBrauner commented 2 years ago

I think this is an issue with api platform not knowing how to normalize the translated class.

I would try to insert this inside your translated class first. Maybe the API-Platform Objectnormalizer will be able to work with it.

  public function __call($method, $arguments)
    {
        return $this->proxyCurrentLocaleTranslation($method, $arguments);
    }

If not - than you can create a custom Normalizer with php bin/console make:serializer:normalizer and put this in:

public function supportsNormalization($data, $format = null): bool
{
    return $data instanceof \Knp\DoctrineBehaviors\Contract\Entity\TranslationInterface;
}

Now you fixed your error for sure.