Problem of relation called twice on apiplatform with symfony - api

I have two entities: Tag, Post.
In relation Many to Many
I have an API route in GET: /api/tags/{id}
I want to return the posts associated to a tag by the tag.
But I also want to return the tags associated with the posts objects returned by the Tag.
This produces an error:
"The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the "api_platform.eager_loading.max_joins" configuration key (https://api-platform.com/docs/core/performance/#eager-loading), or limit the maximum serialization depth using the "enable_max_depth" option of the Symfony serializer (https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth)."
The tag entity :
#[ApiResource(
normalizationContext: ['groups' => ['read:collection:Tag']],
collectionOperations:['get'],
paginationEnabled: false,
itemOperations: [
'get' => [
'normalization_context' => ['groups' => [
'read:item:Tag',
'read:collection:Tag'
]]
],
]
)]
class Tag
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
#[Groups(['read:collection:Post', 'read:item:Category', 'read:collection:Tag'])]
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
#[Groups(['read:collection:Post', 'read:item:Category', 'read:collection:Tag'])]
private $name;
/**
* #ORM\ManyToMany(targetEntity=Post::class, mappedBy="tags")
*/
#[Groups(['read:item:Tag'])]
private $posts;
The tag field in the Post entity :
/**
* #ORM\ManyToMany(targetEntity=Tag::class, inversedBy="posts")
*/
#[Groups(['read:collection:Post', 'read:item:Category','read:item:Tag'])]
private $tags;
I tried limiting the maximum serialization depth using the "enable_max_depth" option as indicated in the error message, it does not work.

I have encountered this problem for a many-to-many relationship when the same serialization group is used for both the entity's relationship property as well as the property that relates back from the associated entity.
In your case, I think you need to remove the read:item:Tag group from the $tags property on the Post entity.
/**
* #ORM\ManyToMany(targetEntity=Tag::class, inversedBy="posts")
*/
#[Groups(['read:collection:Post', 'read:item:Category'])]
private $tags;
I realize this may affect how data is retrieved from the posts endpoint, but it seems to be necessary for relationships in order to avoid recursion. Group infrastructure can get confusing.

Related

Symfony - softdeleteable delete the entity but let me query it after

I want to implement softdelete in my symfony application.
I'm using Symfony5 and ApiPlatform for the back.
I've run the following command to install the bundle
composer require stof/doctrine-extensions-bundle
Installation worked as composer.json now has :
"stof/doctrine-extensions-bundle": "^1.5",
So I've updated my stof_doctrine_extensions.yaml file as follow:
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
softdeleteable: true
I added the #Gedmo annotation to the entity I want to apply softdeleteable on
Added the deletedAt field, generated the migration and reload the database.
Here is my Entity.php file:
/**
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=false)
* #ORM\Entity(repositoryClass=EntityRepository::class)
*/
class Entity
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"entity:read", "entity:list", "user:read", "user:list"})
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups({"entity:write", "entity:read", "entity:list"})
*/
protected $firstName;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $deletedAt;
}
I've also written a simple test to see if the feature was working:
public function testRoleAdminCanSoftDeleteEntity(): void
{
$this->buildEntityManager();
$res = $this->buildDeleteRequest(
GenericRoutes::ROUTE.'/11',
GenericCredentials::ADMIN_CREDENTIALS
);
$entity = $this->em->getRepository(Entity::class)->findEntityByEntityField(json_decode(GenericCredentials::CREDENTIALS)->email);
$this->assertNull($entity);
$this->assertResponseIsSuccessful();
$this->assertResponseStatusCodeSame(204);
}
I receive a 204 but the specialist isn't empty.
I've also used the swagger to manually delete the entity/{id} I wanted to remove.
I also receive a 204 and then can query the same entity/{id} right after.
I've verified in the database and the deleted_at field is updated to the moment I ran the DELETE request.
Does anyone has any idea why I still can query this user which I just soft deleted ?
So, regarding to my comment.
You just need to configure filter in the doctrine config, smth like this:
doctrine:
dbal:
...
orm:
...
filters:
softdeleteable:
class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter
enabled: true
But in my opinion, it would be much more easy to not using this extension, it is just one more dependency in your project, and just create yourself nullable deletedAt property to the entity and set it to DateTime('now') on delete, and then you can get not deleted entries from the repo just adding one more criteria to findBy - 'deletedAt' => null

JMSSerializer Bundle - Circular Reference Error (Only on Prod Azure Environment) - Symfony4/Doctrine2 REST API

So I know somewhat similar issues have been discussed numerous times before but I haven't had any luck finding a solution with this specific issue.
Running locally (using MAMP) I have no issues with my API responses. However once deployed to the production Azure server (via Ansible) I run into the dreaded error:
request.CRITICAL: Uncaught PHP Exception Symfony\Component\Serializer\Exception\CircularReferenceException: "A circular reference has been detected when serializing the object of class "App\ServiceProviderBundle\Entity\Plan
I'm confident that all of my doctrine associations are setup correctly yet something is triggering an infinite loop.
Here is a simplified entity relationship and the main associations from within my doctrine classes.
Any comments or help would be greatly suggested - many thanks!
Plan -> (hasMany) Bundle -> (hasMany) -> Product
class Plan {
/**
* #ORM\OneToMany(targetEntity="App\ServiceProviderBundle\Entity\Bundle", mappedBy="plan")
*/
private $bundles;
}
class Bundle {
/**
* #ORM\ManyToOne(targetEntity="App\ServiceProviderBundle\Entity\Plan", inversedBy="bundles")
* #ORM\JoinColumn(nullable=true)
*/
private $plan;
/**
* #SerializedName("products")
* #ORM\OneToMany(targetEntity="App\ServiceProviderBundle\Entity\BundleProduct", mappedBy="bundle",
* cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=false)
*/
private $bundleProducts;
}
class BundleProduct {
/**
* #ORM\ManyToOne(targetEntity="App\ServiceProviderBundle\Entity\Bundle", inversedBy="bundleProducts")
* #ORM\JoinColumn(nullable=false)
*/
private $bundle;
}
Use #Exclude annotation like that:
class BundleProduct {
/**
* #ORM\ManyToOne(targetEntity="App\ServiceProviderBundle\Entity\Bundle", inversedBy="bundleProducts")
* #ORM\JoinColumn(nullable=false)
* #Exclude
*/
private $bundle;
}

Modelling aggregate root or domain service?

I want to model a wishlisting feature for my domain.
My invariants are:
You can't add product that is already in your wishlist
You can't add product that you own.
The second invariant made me wonder - should I model this feature as reconstituted Aggregate (outside of ORM because of $ownedProductIds that be fetched from UserProductRepository):
final class User extends EventSourcedAggregateRoot
{
// ...
/**
* #param UserId $userId
* #param ObjectCollection $ownedProductIds
* #param ObjectCollection $wishlistedProductIds
* #return $this
*/
public static function reconstituteFrom(
UserId $userId,
ObjectCollection $ownedProductIds,
ObjectCollection $wishlistedProductIds
)
{
$user = new User();
$user->userId = $userId;
$user->ownedProductIds = $ownedProductIds;
$user->wishlistedProductIds = $wishlistedProductIds;
return $user;
}
/**
* #param Product $product
* #throws ProductAlreadyPurchased Thrown when trying to add already bought product
* #throws ProductAlreadyWishlisted Thrown when trying to add already wishlisted product
*/
public function addProductToWishlist(Product $product)
{
$productId = $product->getId();
if ($this->ownedProductIds->contains($productId)) {
throw new ProductAlreadyPurchased($this->userId, $productId);
}
if ($this->wishlistedProductIds->contains($productId)) {
throw new ProductAlreadyWishlisted($this->userId, $productId);
}
$this->apply(new ProductWishlisted($this->userId, $product));
}
// ...
}
or rather create a stateless domain service:
final class Wishlist
{
public function addProductToWishlist(Product $product, UserId $userId)
{
$ownedProductids = $this->userProductRepository->findProductsOfUser($userId);
$wishlistedProductsIds = $this->userWishlistProductIdRepository->findProductsOfUser($userId);
// business rules as in User class
}
}
As User has all the information needed to enforce the invariants, I would leave it there. I typically only create Domain Services when some operation doesn't seem to belong in one entity (TransferMoney() method not fitting in Account class is the poster child for this).
Note though that your model is currently really simplistic. As it is, it may make sense to name the aggregate User, but in a real situation chances are you will make breakthroughs that completely change it.

ORM relation mapping issues in Doctrine and ZF2

I was puzzled by Doctrine\ORM\Mapping issue. I have two entities and they are Many-To-One, Unidirectional releationship. I want to operate Chipinfo(add/update/delete) without impact on producer. Only persist Chipinfo while no persist producer..
class Chipinfo implements
{
/**
* #var integer
*
* #ORM\Column(name="ChipID", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $chipid;
/**
* #var \Shop\Entity\Producer
*
* #ORM\ManyToOne(targetEntity="Shop\Entity\Producer")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="pId", referencedColumnName="producerid")
* })
*/
protected $pid;
}
class Producer{
/**
* #ORM\Id
* #ORM\Column(name="producerid", type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $producerId;
/**
* #ORM\Column(name="producername", type="string")
*/
protected $producerName;
}
ChipInfo and Producer are Many-To-One unidirection relationship: A chipinfo can only be built by one producer while one producer can build multiple chipinfos. What I want is that: add/update/removal of items in Chipinfo would not do any impact on Producer.
$chip = new Chipinfo();
$formData = $this->initFormData($form->getData());
$chip->populate($formData);
$this->getEntityManager()->persist($chip);
$this->getEntityManager()->flush();
private function initFormData(&$raw){
$raw['eid'] = new Encapuser($this->findEntity("Shop\Entity\Encapuser", $raw['eid']));
$this->log($raw->eid);
$raw['vid'] = new Vendors($this->findEntity("Shop\Entity\Vendors", $raw['vid']));
$raw['pid'] = new Producer($this->findEntity("Shop\Entity\Producer", $raw['pid']));
$this->log($raw);
return $raw;
}
would throws errors:
A new entity was found through the relationship 'Shop\Entity\Chipinfo#pid' that was not configured to cascade persist operations for entity: Shop\Entity\Producer#00000000349002ee00000000c955fd11. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Shop\Entity\Producer#__toString()' to get a clue.
Then I configured the pid as:
#ORM\ManyToOne(targetEntity="Shop\Entity\Producer", cascade={"persist"})
Though the error disappear, but this is not what I want. Because when i call flush() for chipinfo with existing producer A, a new A which is duplicated is inserted.
Therefore, my questions are:
1) How should i configure #manyToone field, I did not get clue from http://doctrine-orm.readthedocs.org/en/2.0.x/reference/working-with-associations.html#transitive-persistence-cascade-operations
2) Should I add:
#ORM\OneToMany, targetEntity="Shop\Entity\Producer"
private #chip;
in producer? If so, would operations(add/delete/update) on producer require construct a #chip?
You have to set the existing producer (the entity retrieved by doctrine) in the $pid field of your entity.
Why do you create a new Producer ? What does findEntity do ? It does not seem to retrieve the actual entity from Doctrine, why is that ?
Usually what you would do is this :
$chip->setPid($this->getEntityManager()->getRepository('Shop\Entity\Producer')->find($pid));

Doctrine2 ArrayCollection

Ok, I have a User entity as follows
<?php
class User
{
/**
* #var integer
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
* #var \Application\Entity\Url[]
* #OneToMany(targetEntity="Url", mappedBy="user", cascade={"persist", "remove"})
*/
protected $urls;
public function __construct()
{
$this->urls = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addUrl($url)
{
// This is where I have a problem
}
}
Now, what I want to do is check if the User has already the $url in the $urls ArrayCollection before persisting the $url.
Now some of the examples I found says we should do something like
if (!$this->getUrls()->contains($url)) {
// add url
}
but this doesn't work as this compares the element values. As the $url doesn't have id value yet, this will always fail and $url will be dublicated.
So I'd really appreciate if someone could explain how I can add an element to the ArrayCollection without persisting it and avoiding the duplication?
Edit
I have managed to achive this via
$p = function ($key, $element) use ($url)
{
if ($element->getUrlHash() == $url->getUrlHash()) {
return true;
} else {
return false;
}
};
But doesn't this still load all urls and then performs the check? I don't think this is efficient as there might be thousands of urls per user.
This is not yet possible in a "domain driven" way, ie. just using objects. You should execute a query to check for the existance:
SELECT count(u.id) FROM User u WHERE ?1 IN u.urls AND u.id = ?2
With Doctrine 2.1 this will be possible using a combination of two new features:
Extra Lazy Collections
#IndexBy for collections, so you would define #OneToMany(targetEntity="Url", indexBy="location")
ExtraLazy Collection Support for index by using ->contains().
Points 1 and 2 are already implemented in Doctrine 2 master, but 3 is still missing.
You should try using the exists method on the collection and manually compare values.