API Platform immutable property - php

I'm using API Platform on a Symfony 4.3 project and I just want to have an immutable property (userId in this case) which can be set on POST but cannot be changed with PUT. So far, the only way to accomplish this was to drop the userId setter and use the constructor to initially set the value.
This setup still shows the property in Swagger for PUT (image below), but more troublesome is that it accepts that property without modifying the record. It's a silenced ignore and I would prefer a 400 Bad Request return code to let the client know his request was not processed as expected.
Is there any other way I could accomplish a similar behavior with API Platform? Already tried serialization groups, maybe with the wrong settings though.
<?php
declare(strict_types = 1);
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\NumericFilter;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\SubscriptionRepository")
*
* #UniqueEntity("userId")
*
* #ApiResource()
*/
class Subscription
{
/**
* #var int
*
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var int
*
* #ORM\Column(type="integer")
*
* #ApiFilter(NumericFilter::class)
*/
private $userId;
/**
* Subscription constructor.
*
* #param int $userId
*/
public function __construct(int $userId)
{
$this->userId = $userId;
}
...
?>

I think you will need to explicitly set normalization_context.groups for PUT request to {"read"} (or something else, depending on your configuration).
See Operations documentation

Related

Symfony Doctrine does not hydrate the whole chain

I simplified my 3 entities as much as possible below, it shows a simple relationship of Currency <- 1:1 -> Balance <- 1:N -> BalanceLog
Entity/Currency.php
/**
* #ORM\Entity(repositoryClass=CurrencyRepository::class)
*/
class Currency
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=3)
*/
private ?string $code;
/**
* #ORM\OneToOne(targetEntity="Balance", mappedBy="currency")
**/
private ?Balance $balance;
// ...
}
Entity/Balance.php
/**
* #ORM\Entity(repositoryClass=BalanceRepository::class)
*/
class Balance
{
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="Currency", inversedBy="balance")
* #ORM\JoinColumn(name="currency", referencedColumnName="code", nullable=false)
**/
private ?Currency $currency;
/**
* #ORM\OneToMany(targetEntity="App\Entity\BalanceLog", mappedBy="balance")
*/
private Collection $balance_logs;
// ...
}
Entity/BalanceLog.php
/**
* #ORM\Entity(repositoryClass=BalanceLogRepository::class)
*/
class BalanceLog
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Balance", inversedBy="balance_logs")
* #ORM\JoinColumn(name="balance_currency", referencedColumnName="currency")
**/
private ?Balance $balance;
// ...
}
The issue happens when I call:
$balanceLog = $this->getDoctrine()
->getRepository('App:BalanceLog')->findAll();
This hydrates the BalanceLog::$balance to the proper instance of Balance type, but it does not hydrate the BalanceLog::$balance->currency to Currency instance. Instead it wants to use string only
Resulting in error:
Typed property App\Entity\Balance::$currency must be an instance of App\Entity\Currency or null, string used
The dirty fix is to make Balance::$currency without fixed type of ?Currency. Then it will accept string and the code "works". But it is not correct. The Balance::$currency should be of Currency type, not sometimes string, sometimes currency.
I tried to make my own method in BalanceLogRepository, and for whatever reason this works just fine:
public function findByBalance(Balance $balance) : iterable
{
$query = $this->createQueryBuilder('bl');
$query->andWhere('bl.balance = :balance')
->setParameter('balance', $balance);
return $query->getQuery()->getResult();
}
So I am even more perplexed as to why the default findAll or findBy does not do recursive hydration
After further investigation I found a very weird behavior:
if I prepend this code:
$balance = $this->getDoctrine()->getRepository('App:Balance')->find('USD');
in front of
$balanceLog = $this->getDoctrine()->getRepository('App:BalanceLog')->findAll();
in my controller, then the error is gone. Its as if the App:Balance ORM schema of Balance with dependencies were not properly loaded until I try to fetch the Balance object directly apriori.
I did some debugging and it looks that BalanceLog does not create a full Balance Entity instance, but instead a Proxy. The solution was to add eager loading to the BalanceLog class
class BalanceLog
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private ?int $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Balance", inversedBy="balance_logs", fetch="EAGER")
* #ORM\JoinColumn(name="balance_currency", referencedColumnName="currency")
**/
private ?Balance $balance;
// ...
}
The UnitOfWork.php then does not use Proxy but instead loads the Entity as a whole.
If somebody wonders why querying Balance beforehand made the code work, its because of sophisticated caching mechanism of Doctrine. It saved Balance instance for primary key USD and then when BalanceLog was populated, it used this instance instead of creating a Proxy.
I still think that Proxy should not enforce strictly typed property from Entity though, but this is something for Doctrine developers to decide.

ODM inheritance document with type SINGLE_COLLECTION schema create error

I have inheritance document with type SINGLE_COLLECTION configured in my Symfony 4.4 app.
When i run command bin/console doctrine:mongodb:schema:create, then error occurs a collection 'db.Person' already exists.
Everything was done according to the documentation: https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/2.0/reference/inheritance-mapping.html#single-collection-inheritance
src/Document/Person.php
<?php
namespace App\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document
* #MongoDB\InheritanceType("SINGLE_COLLECTION")
* #MongoDB\DiscriminatorField("type")
* #MongoDB\DiscriminatorMap({"person"=Person::class, "employee"=Employee::class})
*/
class Person
{
/**
* #var integer|null
*
* #MongoDB\Id
*/
protected $id;
/**
* #var string|null
*
* #MongoDB\Field(type="string")
*/
protected $name;
}
src/Document/Employee.php
<?php
namespace App\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document
*/
class Employee extends Person
{
/**
* #var string|null
*
* #MongoDB\Field(type="string")
*/
protected $grade;
}
It looks like command is trying to create DB collection for every Document class, ignoring declaration of SINGLE_COLLECTION type.
How to fix it?
ODM's odm:schema:create is iterating through all metadatas and tries to create a collection without considering possible relations between them. A proper fix would be in the ODM's SchemaManager to either check whether a collection exists prior to creating or catching exception and ignoring it in case of existing collection.
I have created https://github.com/doctrine/mongodb-odm/issues/2182 to track this issue. If you have some time to spare we will appreciate a PR!

Symfony / API Platform - Custom Operation - PUT method for file replacement using VichUploaderBundle

I am using Vich Uploader Bundle in combination with API Platform to store files. I've followed the instructions from the official documentation
https://api-platform.com/docs/core/file-upload/#handling-file-upload
Everything works well except I want to include PUT calls to replace existing MediaObject entities. My attempt was as follows:
I have created a custom operationand mapped it to the PUT method in the entity as follows:
<?php
// api/src/Entity/MediaObject.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\CreateMediaObject;
use App\Controller\EditMediaObject;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity
* #ApiResource(
*
* ...
*
* itemOperations={
* "get",
* "put"={
* "controller"=EditMediaObject::class,
* "deserialize"=false,
* "validation_groups"={"Default", "media_object_create"},
* "swagger_context"={
* "consumes"={
* "multipart/form-data",
* },
* "parameters"={
* {
* "in"="formData",
* "name"="file",
* "type"="file",
* "description"="The file to upload",
* },
* },
* },
* },
* "delete"
* },
* )
* #Vich\Uploadable
*/
class MediaObject
{
/**
* #var int|null
*
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
* #ORM\Id
*/
protected $id;
/**
* #var string|null
*
* #ApiProperty(iri="http://schema.org/contentUrl")
* #Groups({"media_object_read"})
*/
public $contentUrl;
/**
* #var File|null
*
* #Assert\NotNull(groups={"media_object_create"})
* #Vich\UploadableField(mapping="media_object", fileNameProperty="filePath")
*/
public $file;
/**
* #var string|null
*
* #ORM\Column(nullable=true)
*/
public $filePath;
public function getId(): ?int
{
return $this->id;
}
}
The correponding Controller:
<?php
// api/src/Controller/EditMediaObject.php
namespace App\Controller;
use App\Entity\MediaObject;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
final class EditMediaObject
{
/**
* #var RequestStack
*/
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function __invoke(MediaObject $data): MediaObject
{
$request = $this->requestStack->getCurrentRequest();
$uploadedFile = $request->attributes->get('file');
if (!$uploadedFile) {
throw new BadRequestHttpException ('"file" is required');
}
$data->file = $uploadedFile;
return $data;
}
}
The $data object is susccessfully populated. However I dont have access to the initial request parameters (particularly 'file').
Is this because of the preliminary OPTIONS call? If so, how can I forward / preserve the file to attach it to the MediaObject?
Thanks in advance
Edit:
In case anyone is interested, it is NOT possible to send file parameters over PUT requests in PHP. (source: PHP multipart form data PUT request?)
Why are you looking at $request->attributes?
You should be looking at $request->files.
See https://symfony.com/doc/current/components/http_foundation.html#accessing-request-data
You need to send a POST request with an extra parameter, _method = PUT, It will change the POST request as if it is a PUT request.
I have recently had a very similar problem, though, I'm not using the entire symfony library, I'm using the HttpFoundation/Request and HttpFoundation/Response objects.
At first I was puzzled by how the request seemed completely empty.
After spending more time than I would have liked looking into it, this is an issue that can be traced back to the internals of PHP.
I managed to find this PECL extension
I'm not really very familiar with pecl, and couldn't seem to get it working using pear. but I'm using CentOS and Remi PHP which has a yum package.
I ran yum install php-pecl-apfd and it literally fixed the issue straight away. I believe there are other packages in various flavours of linux and I'm sure anybody with more knowledge of pear/pecl/general php extensions could get it running on windows or mac with no issue.
By fixed, I mean, my request object was populated as expected (request->all() and request->files for example).
This seems to have worked for both PUT and PATCH requests with and without files (using multipart forms).
If you're using Docker, you'll need to restart your containers.

Symfony3 The identifier id is missing for a query of AppBundle\Entity\

I am new to symfony and I am trying to pass a object from a list to a new page using:
Update
The list ({% for item in list %}) of Foo objects is fine, the objects are fully loaded from the database, but the link does not work because of this Doctrine error:
The identifier id is missing for a query of AppBundle\Entity\Foo
Here is the Foo class:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="foo")
*/
class Foo {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string")
*/
private $description;
//getters and setters
And here is the controller method:
/**
* #Route("/foo/fooUpdate/", name="fooUpdate")
*/
public function updateAction($id, Request $request) {
$foo = $this->getDoctrine()->getRepository('AppBundle:Foo')->find($id);
return $this->render('/foo/fooUpdate.html.twig', array(
'foo' => $foo
));
}
The link itself looks like it is working, because the URL of the error is:
http://127.0.0.1:8000/foo/fooUpdate/?id=1
So what am I missing?
Also, is there any way of doing this using POST, so the id will not appear in the URL?
You are missing the id in the route:
#Route("/foo/fooUpdate/{id}", name="fooUpdate")
Actually, you should also set defaults:
/**
* #Route("/foo/fooUpdate/{id}",
* defaults={"id" = 0},
* name="fooUpdate")
*/
The defaults are used in case you don't pass in a parameter.
As far as not setting the id in the URL, look into sessions:
https://symfony.com/doc/current/components/http_foundation/sessions.html
You should be able to do that, but it's more work (coding).

Multiple level of different kind of inheritance

For my project, I'm trying to use the inheritance feature of Doctrine. I need to represent medias (through different tables : one table for uploaded documents, one for linked videos, ... and so on).
But, the videos can vary from provider to provider (such as Youtube, Dailymotion, you name it). So, I was thinking of doing another inheritance, proper to the Video table, through a SINGLE_TABLE inheritance.
But, when I declare my entities, it seems that if I add the SINGLE_TABLE inheritance annotation on the AbstractVideo entity, which extends the AbstractMedia Entity, the Video table is never created (nor detected). Here is a snippet of these two entities :
<?php
namespace Acme\Demo\Entity;
use Datetime;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="Media")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string")
*/
abstract class AbstractMedia
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// some other fields
}
/**
* #ORM\Entity
* #ORM\Table(name="Video")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="provider", type="string")
* #ORM\DiscriminatorMap({})
*/
abstract class AbstractVideo extends AbstractMedia
{
/** #ORM\Column(type="string") */
private $name;
// some other fields
}
I already tried to have a mapped entity to a Foo entity, extending the AbstractVideo, but then when I try to persist something, it says that it is not a valid entity.
Any ideas, or should I really avoid such deep inheritance ? Thanks
Not really sure if this is exactly what you need, but this is from a production code I use.
We inherit the file, with other entities, and those are also inherited.
The important part is to add the inheriting(extending) entities to disciminator map.
/**
* File
*
* #ORM\Table(name = "file")
* #ORM\Entity(repositoryClass="Living\ApiBundle\Entity\File\FileRepository")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string", length=64)
* #ORM\DiscriminatorMap({
* "file" = "Something\Entity\File\File",
* "image" = "Something\Entity\Image\Image",
* "specialImage" = "Something\Entity\Image\SpecialImage",
* })
*/
class File implements FileEntityInterface
.....
/**
* ImageFile
*
* #ORM\Table(name="image")
* #ORM\Entity(repositoryClass="Living\ApiBundle\Entity\Image\ImageRepository")
*/
class Image extends File implements ImageEntityInterface
As #OCramius said in a comment to my question, this is not supported by Doctrine ORM. So to do what I wanted to do, I will store a value object in the data property of my object, storing the property of "child classes" instead of having deep different kind of inheritance.
<?php
class Video extends AbstractMedia
{
// returns the value object youtube, dailymotion, ... etc
public function getData();
}
class Youtube
{
//public function ...
}
class Dailymotion
{
// public funciton ...
}

Categories