Search code examples
symfonysymfony4api-platform.com

validate embedded model in api platform


I have this Order model:

<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Validator\Constraints as Assert;
/**
 * @ApiResource(
 *     itemOperations={
 *          "get"={
 *              "normalization_context"={
 *                  "groups"={"order:view"}
 *              }
 *          },
 *          "patch"={
 *              "normalization_context"={
 *                  "groups"={"order:view"}
 *              },
 *              "denormalization_context"={
 *                  "groups"={"upsert"}
 *              }
 *          },
 *          "delete"
 *     },
 *     collectionOperations={
 *          "get"={
 *              "normalization_context"={
 *                  "groups"={"order:index"}
 *              }
 *          },
 *          "post"={
 *              "normalization_context"={
 *                  "groups"={"order:view"}
 *              },
 *              "denormalization_context"={
 *                  "groups"={"order:create"}
 *              }
 *          }
 *     }
 * )
 * @ORM\Entity(repositoryClass="App\Repository\OrderRepository")
 * @ORM\Table(name="`order`")
 */
class Order
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Groups({"order:index", "order:view"})
     */
    private $id;
    /**
     * @ORM\Column(type="integer")
     * @Assert\PositiveOrZero()
     * @Groups({"order:index", "order:view"})
     */
    private $amount;
    /**
     * @ORM\OneToMany(targetEntity="App\Entity\OrderItem", mappedBy="orderId")
     * @Groups({"order:index", "order:view", "order:create"})
     */
    private $orderItems;
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Merchant", inversedBy="orders")
     * @ORM\JoinColumn(nullable=false)
     * @Groups({"order:index", "order:view"})
     */
    private $merchant;
    /**
     * Order constructor.
     */
    public function __construct()
    {
        $this->orderItems = new ArrayCollection();
    }
    /**
     * @return string
     */
    public function __toString(): string {
        return $this->id;
    }
    /**
     * @return int|null
     */
    public function getId(): ?int
    {
        return $this->id;
    }
    /**
     * @return int|null
     */
    public function getAmount(): ?int
    {
        return $this->amount;
    }
    /**
     * @param int $amount
     * @return Order
     */
    public function setAmount(int $amount): self
    {
        $this->amount = $amount;
        return $this;
    }
    /**
     * @Assert\Valid
     */
    public function getOrderItems()
    {
        return $this->orderItems->getValues();
    }
    /**
     * @param OrderItem $orderItem
     * @return Order
     */
    public function addOrderItem(OrderItem $orderItem): self
    {
        if (!$this->orderItems->contains($orderItem)) {
            $this->orderItems[] = $orderItem;
            $orderItem->setOrderId($this);
        }
        return $this;
    }
    /**
     * @param OrderItem $orderItem
     * @return Order
     */
    public function removeOrderItem(OrderItem $orderItem): self
    {
        if ($this->orderItems->contains($orderItem)) {
            $this->orderItems->removeElement($orderItem);
            // set the owning side to null (unless already changed)
            if ($orderItem->getOrderId() === $this) {
                $orderItem->setOrderId(null);
            }
        }
        return $this;
    }
    /**
     * @return Merchant|null
     */
    public function getMerchant(): ?Merchant
    {
        return $this->merchant;
    }
    /**
     * @param Merchant|null $merchant
     * @return Order
     */
    public function setMerchant(?Merchant $merchant): self
    {
        $this->merchant = $merchant;
        return $this;
    }
}

and this OrderItem model

<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Validator\Constraints as Assert;
/**
 * @ApiResource()
 * @ORM\Entity(repositoryClass="App\Repository\OrderItemRepository")
 */
class OrderItem
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Groups({"view", "order:view", "order:index"})
     */
    private $id;
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Product")
     * @ORM\JoinColumn(nullable=false)
     * @SerializedName("product")
     * @Assert\NotBlank()
     * @Groups({"view", "order:view", "order:index", "order:create"})
     */
    private $productId;
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Order", inversedBy="orderItems")
     * @SerializedName("order")
     * @ORM\JoinColumn(nullable=false)
     */
    private $orderId;
    /**
     * @ORM\Column(type="integer")
     * @Assert\NotBlank()
     * @Assert\PositiveOrZero()
     * @Groups({"view", "order:view", "order:index", "order:create"})
     */
    private $num;
    /**
     * @ORM\Column(type="integer")
     * @SerializedName("total_amount")
     * @Assert\PositiveOrZero()
     * @Groups({"view", "order:view", "order:index"})
     */
    private $totalAmount;
    /**
     * @return string
     */
    public function __toString(): string {
        return $this->id;
    }
    /**
     * @return int|null
     */
    public function getId(): ?int
    {
        return $this->id;
    }
    /**
     * @return Product|null
     */
    public function getProductId(): ?Product
    {
        return $this->productId;
    }
    /**
     * @param Product|null $productId
     * @return OrderItem
     */
    public function setProductId(?Product $productId): self
    {
        $this->productId = $productId;
        return $this;
    }
    /**
     * @return Order|null
     */
    public function getOrderId(): ?Order
    {
        return $this->orderId;
    }
    /**
     * @param Order|null $orderId
     * @return OrderItem
     */
    public function setOrderId(?Order $orderId): self
    {
        $this->orderId = $orderId;
        return $this;
    }
    /**
     * @return int|null
     */
    public function getNum(): ?int
    {
        return $this->num;
    }
    /**
     * @param int $num
     * @return OrderItem
     */
    public function setNum(int $num): self
    {
        $this->num = $num;
        return $this;
    }
    /**
     * @return int|null
     */
    public function getItemAmount(): ?int
    {
        return $this->itemAmount;
    }
    /**
     * @param int $itemAmount
     * @return OrderItem
     */
    public function setItemAmount(int $itemAmount): self
    {
        $this->itemAmount = $itemAmount;
        return $this;
    }
    /**
     * @return int|null
     */
    public function getTotalAmount(): ?int
    {
        return $this->totalAmount;
    }
    /**
     * @param int $totalAmount
     * @return OrderItem
     */
    public function setTotalAmount(int $totalAmount): self
    {
        $this->totalAmount = $totalAmount;
        return $this;
    }
}

I'm trying to create an order with its item in a single post request using embedded nested items instead of item IRIs. As documentation suggests I add @Assert\Valid for orderItems getter method inside Order model. but it doesn't validate my orderItem data in the post request.


Solution

  • I could fix it.
    Valid constraint is only validate the embedded model itself, so if you don't provide the key for the embedded model or just set an empty array validation does nothing.
    So you need to add another validation for the attribute itself.
    in this case because I want that my orderItems contains at least one item I add Count validation constraint for this property as below.

    /**
     * @Assert\Count(
     *      min = "1",
     *      minMessage = "You must specify at least one OrderItem"
     * )
     * @Assert\Valid()
     */
    public function getOrderItems()
    {
        return $this->orderItems;
    }
    

    So it force the request to contains at list one OrderItem