ODM: Cannot achieve bi-directional relationship - doctrine

I have two documents. I am trying to find all papers that are associated to a specific person. The documents are saving in their collections, and a reference is being created from Person to Paper, but not the other way around.
/** #ODM\Document */
class Paper
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\ReferenceOne(targetDocument="Person", cascade={"all"}, mappedBy="papers") */
protected $person;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
/** #ODM\Document */
class Person
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\ReferenceMany(targetDocument="Paper", cascade={"all"}, inversedBy="person") */
protected $papers;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
CREATE A NEW BI-DIRECTIONAL REFERENCE
$person = $dm->getRespository('Person')->find($person_id);
$paper = new Paper();
$person->papers->add($paper);
$dm->persist($person);
$dm->flush();
Later in the code, this query returns 0 results; shouldn't it be returning papers written by specified person?
$papers = $dm->createQueryBuilder('Paper')
->field('person.$id')->equals(new \MongoId($person_id_as_string))
->getQuery()->execute();

If Paper::person is annotated with "mappedBy" it means that Paper is not the "owning side" and doctrine will not persist any changes to Paper::person.
To make your query work, make Paper the owning side so Paper stores the reference to Person.
/** #ODM\Document */
class Person
{
/** #ODM\ReferenceMany(targetDocument="Paper", mappedBy="person") */
protected $papers;
}
/** #ODM\Document */
class Paper
{
/** #ODM\ReferenceOne(targetDocument="Person", inversedBy="papers") */
protected $person;
}
Creating a paper and persisting a reference to person:
$person = $dm->getRespository('Person')->find($person_id);
$paper = new Paper();
$paper->person = $person;
$dm->persist($paper);
$dm->flush();
Querying Papers by $person:
$papers = $dm->createQueryBuilder('Paper')
->field('person')->references($person)
->getQuery()->execute();

Related

Foreign Key issue in a virtual many-to-many user relationship when removing user

In my Symfony app, I have to handle some kind of friendship requests. It's basically a many to many relation between users, but with an accepted boolean property, and a createdAt DateTime, hence why I created an independent "friendship" entity instead of just a Many to many relations in the user entity.
Here is the issue though: I have a Sender property (one to many relation to user) and a recipient property (one to many relation to user as well). I set the orphanRemoval to true on the Friendships property in the user.
However, if I delete a user with only friendships where he was the sender, it works well and deletes the friendship entities. But if he is a recipient of a friend request, it just doesn't work and returns a Foreign Key constraint error, more precisely
SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or
update a parent row: a foreign key constraint fails
(portail.friendship, CONSTRAINT FK_7234A45FE92F8F78
FOREIGN KEY (recipient_id) REFERENCES user (id)).
I reckon it has something to do with the fact that user appears twice in my friendship entity, and obviously, it seems like only the sender is mentioned in the addFriendship() / removeFriendship() methods, but I'm not sure how to fix it, and I'd like to know if maybe I didn't tackle the issue the right way, and what I could do to change this and make it work (ie: remove all Friendships related to the User, whether he's the sender or recipient).
Below is the friendship entity, as well as the part of the User entity mentioning the Friendship entity relation.
<?php
namespace App\Entity;
use App\Repository\FriendshipRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=FriendshipRepository::class)
*/
class Friendship
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendships")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $sender;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendships")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $recipient;
/**
* #ORM\Column(type="boolean")
*/
private ?bool $accepted;
/**
* #ORM\Column(type="datetime_immutable")
*/
private ?\DateTimeImmutable $createdAt;
public function getId(): ?int
{
return $this->id;
}
public function getSender(): ?User
{
return $this->sender;
}
public function setSender(?User $sender): self
{
$this->sender = $sender;
return $this;
}
public function getRecipient(): ?User
{
return $this->recipient;
}
public function setRecipient(?User $recipient): self
{
$this->recipient = $recipient;
return $this;
}
public function getAccepted(): ?bool
{
return $this->accepted;
}
public function setAccepted(bool $accepted): self
{
$this->accepted = $accepted;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
}
Part of the user
/**
* #ORM\OneToMany(targetEntity=Friendship::class, mappedBy="sender", orphanRemoval=true)
*/
private $friendships;
/**
* #return Collection|Friendship[]
*/
public function getFriendships(): Collection
{
return $this->friendships;
}
public function addFriendship(Friendship $friendship): self
{
if (!$this->friendships->contains($friendship)) {
$this->friendships[] = $friendship;
$friendship->setSender($this);
}
return $this;
}
public function removeFriendship(Friendship $friendship): self
{
if ($this->friendships->removeElement($friendship)) {
// set the owning side to null (unless already changed)
if ($friendship->getSender() === $this) {
$friendship->setSender(null);
}
}
return $this;
}
Arleigh was indeed correct. What I did wrong was set the same name, ie Friendships, while updating my friendship entity to get both relations in the user, whether he was a sender or recipient. I thought this would allow me to more easily get all friendships related to a user in my controllers and twig template, regardless of them being a recipient or a sender but in the end, all it did was make it all go wrong.
I fixed it by having two separate properties in my user, the friendshipRequestsSent(mapped by the sender field of the Friendship entity) and friendshipRequestsReceived (mapped by the recpient field of the Friendship entity) as well as the methods and typing of the properties associated. This is what it looks like in my User entity now
/**
* #ORM\OneToMany(targetEntity=Friendship::class, mappedBy="sender", orphanRemoval=true)
*/
private $friendshipRequestsSent;
/**
* #ORM\OneToMany(targetEntity=Friendship::class, mappedBy="recipient", orphanRemoval=true)
*/
private $friendshipRequestsReceived;
public function __construct()
{
$this->userSkills = new ArrayCollection();
$this->experiences = new ArrayCollection();
$this->missions = new ArrayCollection();
$this->friendshipRequestsSent = new ArrayCollection();
$this->friendshipRequestsReceived = new ArrayCollection();
}
/**
* #return Collection|Friendship[]
*/
public function getFriendshipRequestsSent(): Collection
{
return $this->friendshipRequestsSent;
}
public function addFriendshipRequestSent(Friendship $friendship): self
{
if (!$this->friendshipRequestsSent->contains($friendship)) {
$this->friendshipRequestsSent[] = $friendship;
$friendship->setSender($this);
}
return $this;
}
public function removeFriendshipRequestSent(Friendship $friendship): self
{
if ($this->friendshipRequestsSent->removeElement($friendship)) {
// set the owning side to null (unless already changed)
if ($friendship->getSender() === $this) {
$friendship->setSender(null);
}
}
return $this;
}
/**
* #return Collection|Friendship[]
*/
public function getFriendshipRequestsReceived(): Collection
{
return $this->friendshipRequestsReceived;
}
public function addFriendshipRequestReceived(Friendship $friendship): self
{
if (!$this->friendshipRequestsReceived->contains($friendship)) {
$this->friendshipRequestsReceived[] = $friendship;
$friendship->setSender($this);
}
return $this;
}
public function removeFriendshipRequestReceived(Friendship $friendship): self
{
if ($this->friendshipRequestsReceived->removeElement($friendship)) {
// set the owning side to null (unless already changed)
if ($friendship->getSender() === $this) {
$friendship->setSender(null);
}
}
return $this;
}
And this is how my Friendship entity sender and recipient properties are declared
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendshipRequestsSent")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $sender;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="friendshipRequestsReceived")
* #ORM\JoinColumn(nullable=false)
*/
private ?User $recipient;

Laravel Virgin: Setting up and destroying database in phpunit integration tests

Using the nilportuguess' eloquent repository library, I made the following (with bugs) repository:
namespace App\Repositories;
use NilPortugues\Foundation\Infrastructure\Model\Repository\Eloquent\EloquentRepository;
use App\Model\Rover;
class RoverRepository extends EloquentRepository
{
/**
* {#inheritdoc}
*/
protected function modelClassName()
{
return Rover::class;
}
/**
* {#inheritdoc}
*/
public function find(Identity $id, Fields $fields = null)
{
$eloquentModel = parent::find($id, $fields);
return $eloquentModel->toArray();
}
/**
* {#inheritdoc}
*/
public function findBy(Filter $filter = null, Sort $sort = null, Fields $fields = null)
{
$eloquentModelArray = parent::findBy($filter, $sort, $fields);
return $this->fromEloquentArray($eloquentModelArray);
}
/**
* {#inheritdoc}
*/
public function findAll(Pageable $pageable = null)
{
$page = parent::findAll($pageable);
return new Page(
$this->fromEloquentArray($page->content()),
$page->totalElements(),
$page->pageNumber(),
$page->totalPages(),
$page->sortings(),
$page->filters(),
$page->fields()
);
}
/**
* #param array $eloquentModelArray
* #return array
*/
protected function fromEloquentArray(array $eloquentModelArray) :array
{
$results = [];
foreach ($eloquentModelArray as $eloquentModel) {
//This is required to handle findAll returning array, not objects.
$eloquentModel = (object) $eloquentModel;
$results[] = $eloquentModel->attributesToArray();
}
return $results;
}
}
And In order to locate them I thought to make an Integration test on an sqlite inmemory db:
namespace Test\Database\Integration\Repositories;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Repositories\RoverRepository;
use App\Model\Rover;
use App\Model\Grid;
class RoverRepositoryTest extends TestCase
{
use RefreshDatabase;
private $repository=null;
public function setUp(): void
{
parent::setUp();
$grid=factory(Grid::class)->create([
'width'=>5,
'height'=>5
]);
$rover=factory(Rover::class, 5)->create([
'grid_id' => $grid->id,
'grid_pos_x' => rand(0, $grid->width),
'grid_pos_y' => rand(0, $grid->height),
]);
//How do I run Migrations and generate the db?
$this->repository = new RoverRepository();
}
public function tearDown(): void
{
parent::tearDown();
//How I truncate and destroy Database?
}
/**
* Testing Base Search
*
* #return void
*/
public function testBasicSearch(): void
{
//Some Db test
}
}
But I have some questions:
How do I save the generated via factory Models?
How do I nuke my database in tearDown()?

Extra data on a collection operation

Does anybody know how to add extra data on a collection?
The doc says much about how to add extra data on an item which translates into decorating the ItemNormalizer service, and it works pretty well.
But I’m struggling in finding out which normalizer to decorate when it comes to add some data on a collection of entities. The extra data could be anything: the current user logged in, a detailed pager, some debug parameters, ... that are not related to a specific entity, but rather on the request itself.
The only working solution for now is to hook on a Kernel event but that's definitely not the code I like to write:
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SerializeListener implements EventSubscriberInterface
{
/**
* #var Security
*/
private $security;
/**
* #var NormalizerInterface
*/
private $normalizer;
public function __construct(
Security $security,
NormalizerInterface $normalizer
) {
$this->security = $security;
$this->normalizer = $normalizer;
}
public function addCurrentUser(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->has('_api_respond')) {
$serialized = $event->getControllerResult();
$data = json_decode($serialized, true);
$data['hydra:user'] = $this->normalizer->normalize(
$this->security->getUser(),
$request->attributes->get('_format'),
$request->attributes->get('_api_normalization_context')
);
$event->setControllerResult(json_encode($data));
}
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
'addCurrentUser',
EventPriorities::POST_SERIALIZE,
],
];
}
}
Any ideas?
Thank you,
Ben
Alright, I finally managed to do this.
namespace App\Api;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class ApiCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
/**
* #var NormalizerInterface|NormalizerAwareInterface
*/
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
if (!$decorated instanceof NormalizerAwareInterface) {
throw new \InvalidArgumentException(
sprintf('The decorated normalizer must implement the %s.', NormalizerAwareInterface::class)
);
}
$this->decorated = $decorated;
}
/**
* #inheritdoc
*/
public function normalize($object, $format = null, array $context = [])
{
$data = $this->decorated->normalize($object, $format, $context);
if ('collection' === $context['operation_type'] && 'get' === $context['collection_operation_name']) {
$data['hydra:meta'] = ['foo' => 'bar'];
}
return $data;
}
/**
* #inheritdoc
*/
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
/**
* #inheritdoc
*/
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->decorated->setNormalizer($normalizer);
}
}
# config/services.yaml
services:
App\Api\ApiCollectionNormalizer:
decorates: 'api_platform.hydra.normalizer.collection'
arguments: [ '#App\Api\ApiCollectionNormalizer.inner' ]
Keep it for the records :)

ODM: References not being created on both documents

Say I have two simple Documents like this, where a person can have many papers, but a paper can only belong to one person.
namespace Dashboard\Document;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/** #ODM\Document(db="testing", collection="person")
* #ODM\InheritanceType("COLLECTION_PER_CLASS")
*/
class Person
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\Field(type="string") */
protected $slug;
/** #ODM\Field(type="string") */
protected $name;
/** #ODM\ReferenceMany(targetDocument="Paper", cascade={"all"}) */
protected $papers;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
namespace Dashboard\Document;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/** #ODM\Document(db="testing", collection="paper")
* #ODM\InheritanceType("COLLECTION_PER_CLASS")
*/
class Paper
{
/**
* #ODM\Id
*/
protected $id;
/** #ODM\Field(type="string") */
protected $name;
/** #ODM\ReferenceOne(targetDocument="Person", cascade={"all"}) */
protected $person;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
public function toArray() {
return get_object_vars($this);
}
}
I thought I read somewhere when you create a reference on one end, Doctrine ODM will auto create the references on both sides for you. So if I execute the statement below, I will see a reference to Person from a Paper document, AS WELL AS references to Paper(s) in a Person document.
//For demo sake; $person already contains a Person document
try {
$paper = $dm->getRepository('\Dashboard\Document\Paper')
->find($paperId);
} catch (\Doctrine\ODM\MongoDB\MongoDBException $e) {
$this->setStatusFailure($e->getMessage());
$this->sendResponse();
}
$paper->person = $person;
$dm->persist($paper);
$dm->flush();
When I do that, and check the mongodb, the reference from paper-->person is there. But I see no reference person-->paper shown in the db. I thought the cascade annotations helped with this, but obviously I'm missing something.
How can I ensure the reference is contained on both ends, so I can run queries to see all the papers that belong a single person? Does this have to be done manually, or can I have doctrine handle this for me?
UPDATE
The first paragraph on this page made me think it was possible.
http://docs.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/reference/bidirectional-references.html
Turns out I should have read that whole page. If I use mappedBy & inversedBy, and always persist the document that has inversedBy in it's Annotation, then I get that bi-directional relationship
/** #ODM\ReferenceOne(targetDocument="Person", cascade={"all"}, inversedBy="papers") */
protected $person;
//Will give me a relationship I can query on both sides
$person->papers->add($paper);

Doctrine 1.* cache invalidation

I'm using APC cache for expensive query which retrieve job with related files, payments, events etc.
I would like to know if there are available solutions for cache invalidation in Doctrine 1.*.
I came up with following working solution, it does work, I just don't want to invent a wheel.
Please suggest me if there are better/other existing solutions.
Record listener tries to clear cache with given id's on postSave event:
class My_Doctrine_Record_Listener_ClearCache extends Doctrine_Record_Listener
{
/**
* Clear cache by table tags
*
* #param Doctrine_Event $event
* #return null
*/
public function postSave(Doctrine_Event $event)
{
$cache = new Doctrine_Cache_Apc();
/* #var $model Doctrine_Record */
$model = $event->getInvoker();
$name = get_class($model);
/* #var $table Doctrine_Table */
$table = $model->getTable($name);
if (method_exists($table, 'getCacheTags')) {
foreach ($table->getCacheTags() as $tag) {
$id = preg_replace('/%([\w]+)%/e', '$model->{\\1}', $tag);
$cache->delete($id);
}
}
}
}
This is what I have in tables:
class FileTable extends Doctrine_Table
{
/* ... */
public function getCacheTags()
{
return array(
'job_view_%job_id%'
);
}
/* ... */
}
class JobTable extends Doctrine_Table
{
/* ... */
public function getCacheTags()
{
return array(
'job_view_%id%'
);
}
/* ... */
}
The above solution has been used for 7 years in the production environment, so it's safe to say - it does work good enough.

Resources