I'm trying to update a related entity via the main one. For example I have products and offers. I do a POST
to /products
{
"name": "sample product",
"offers": [
{
"description": "sample offer"
}
]
}
And I get this response
{
"@context": "/contexts/Product",
"@id": "/products/1",
"@type": "Product",
"name": "sample product",
"offers": [
{
"@id": "/offers/1",
"@type": "https://schema.org/Offer",
"description": "sample offer"
}
]
}
So far, so good. Now, I want to perform a PUT
operation at /products/1
{
"name": "product updated",
"offers": [
{
"@id": "/offers/1",
"description": "offer updated"
}
]
}
And get this error
An exception occurred while executing a query: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'product_id' cannot be null
If I remove #[ORM\JoinColumn(nullable:false)]
from Offer.php it works, but product_id
is set to null
(the rest of the row remains) and a new row is inserted.
What I want is to get the row updated (just update description field). Is that possible?
I'm using the example given at https://api-platform.com/docs/core/getting-started/#mapping-the-entities with some minor modifications (see comments)
My code is:
Product.php
<?php
// api/src/Entity/Product.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Annotation\Groups; //ADDED THIS
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
normalizationContext: ["groups" => ["products"]],
denormalizationContext: ["groups" => ["products"]],
)]
class Product // The class name will be used to name exposed resources
{
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
/**
* A name property - this description will be available in the API documentation too.
*
*/
#[ORM\Column]
#[Assert\NotBlank]
#[Groups(["products"])] //ADDED THIS
public string $name = '';
// Notice the "cascade" option below, this is mandatory if you want Doctrine to automatically persist the related entity
/**
* @var Offer[]|ArrayCollection
*
*/
#[ORM\OneToMany(targetEntity: Offer::class, mappedBy: 'product', cascade: ['persist'])]
#[Groups(["products"])] //ADDED THIS
public iterable $offers;
public function __construct()
{
$this->offers = new ArrayCollection(); // Initialize $offers as a Doctrine collection
}
public function getId(): ?int
{
return $this->id;
}
// Adding both an adder and a remover as well as updating the reverse relation is mandatory
// if you want Doctrine to automatically update and persist (thanks to the "cascade" option) the related entity
public function addOffer(Offer $offer): void
{
$offer->product = $this;
$this->offers->add($offer);
}
public function removeOffer(Offer $offer): void
{
$offer->product = null;
$this->offers->removeElement($offer);
}
// ...
//ADDED THIS SET AND GET
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
}
Offer.php
<?php
// api/src/Entity/Offer.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups; //ADDED THIS
use Symfony\Component\Validator\Constraints as Assert;
/**
* An offer from my shop - this description will be automatically extracted from the PHPDoc to document the API.
*
*/
#[ORM\Entity]
#[ApiResource(types: ['https://schema.org/Offer'])]
class Offer
{
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
#[ORM\Column(type: 'text')]
#[Groups(["products"])] //ADDED THIS
public string $description = '';
#[ORM\Column]
#[Assert\Range(minMessage: 'The price must be superior to 0.', min: 0)]
public float $price = 1.0; //MODIFIED THIS
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'offers')]
#[ORM\JoinColumn(nullable:false)] //ADDED THIS
public ?Product $product = null;
public function getId(): ?int
{
return $this->id;
}
//ADDED THIS SET AND GET
public function setDescription($description)
{
$this->description = $description;
return $this;
}
public function getDescription()
{
return $this->description;
}
}
Well, apparently it was as simple as doing
{
"name": "product updated",
"offers": [
{
"id": "/offers/1",
"description": "offer updated"
}
]
}
notice the id
without @
!!!
UPDATE 2024-01-10: In version 3.2.10 this doesn't work
UPDATE 2024-01-19: This is the new way to do it
{
"@id": "/products/1",
"name": "product updated",
"offers": [
{
"@id": "/offers/1",
"description": "offer updated"
}
]
}
Notice the IRIs for both product
and offer
. But that would delete other offers related to the product if any. If you want to keep them do this
{
"@id": "/products/1",
"name": "product updated",
"offers": [
{
"@id": "/offers/1",
"description": "offer updated"
},
"/offers/2",
"/offers/3"
]
}