ODM: References not being created on both documents - doctrine

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);

Related

How to delete all Review of a News when News is deleted from db?

I have Two Models News and Review
This is News model
<?php
namespace Modules\Newsletter\Entities;
use Brexis\LaravelWorkflow\Traits\WorkflowTrait;
//use Hyn\Tenancy\Abstracts\TenantModel as TenancyModel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
/**
* This is for storing news
* Class News
* #package Modules\Newsletter\Entities
*/
//class News extends TenancyModel {
class News extends Model {
use WorkflowTrait;
protected $table = 'news_info';
protected $fillable = [
'title', 'header', 'description', 'status', 'created_by', 'media_url', 'media_thumbnail', 'media_type'
];
/**
* This creates relationship between News and Reviews
* #return mixed
*/
public function reviews() {
return $this->morphMany(NewsReview::class, 'reviewable');
}
/**
* This is for creating relationship between News and Review and counting reactions according to is_visible=1
* #return mixed
*/
public function reviewsCountByvisible() {
return $this->morphMany(NewsReview::class, 'reviewable')
->select(
'reviewable_id',
DB::raw("COUNT(CASE WHEN review_reaction=0 THEN 1 ELSE NULL END) as review_bad"),
DB::raw("COUNT(CASE WHEN review_reaction=1 THEN 1 ELSE NULL END) as review_average"),
DB::raw("COUNT(CASE WHEN review_reaction=2 THEN 1 ELSE NULL END) as review_good")
)->where('is_visible', '=', 1)
->groupBy('reviewable_id');
}
}
This is Review model
<?php
namespace Modules\Newsletter\Entities;
use App\User;
//use Hyn\Tenancy\Abstracts\TenantModel as TenancyModel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* This is for storing Reviews of a News
* Class NewsReview
* #package Modules\Newsletter\Entities
*/
//class NewsReview extends TenancyModel {
class NewsReview extends Model {
use SoftDeletes;
protected $fillable = [
'review_text',
'review_reaction',
'is_visible',
'reviewed_by',
'reviewable_id',
'reviewable_type'
];
/**
* This is for creating relation between Reviews and News
* #return mixed
*/
public function reviewable() {
return $this->morphTo();
}
/**
* #return mixed
*/
public function news() {
return $this->belongsTo(News::class);
}
/**
* #return mixed
*/
public function reviewer() {
return $this->hasOne(User::class, 'id', 'reviewed_by');
}
}
This is the function I am using to delete news
public function delete($id){
$news=News::find($id);
$news->delete();
}
I want that if I delete a news then all reviews related to this news should be deleted.
Can anyone help me.Help will be highly appreciated.
you can do that by using model events defined in your AppServiceProvider file :
public function boot()
{
News::deleted(function (News $$news) {
$news->reviews()->delete();
});
}
Alternatively to using events as already suggested, you could also override your News' delete method and handle deletion there:
class News {
public function delete(){
// delete associated reviews
$this->reviews()->delete();
// call parent delete, which deletes the model itself
parent::delete();
}
}
This will only work when directly calling the delete method on a news instance though.
You could also take care of that at the database level via constraints. E.g. by definining:
Schema::table('news_reviews', function (Blueprint $table) {
$table->unsignedBigInteger('news_id');
$table->foreign('news_id')->references('id')->on('news')
->onDelete('cascade'); // this will
});
This has the advantage that you are guaranteed to never have any stray reviews left in your database and that you don't have to manually delete reviews, BUT your application will not be notified when reviews are deleted (because it happens at the database level) and it's less obvious for other developers, so you should document that behaviour.
Please find below working example of morphMany relation model delete. My News model is :
class News extends Model
{
protected $table = 'news';
public function reviews()
{
return $this->morphMany('App\Review', 'reviewable');
}
}
And this is review model
class Review extends Model
{
public function reviewable()
{
return $this->morphTo();
}
}
$news->reviews() is the relationship query to return all of the reviews for a news. If you call delete() on that, it will delete all of those records.
$news = News::find(1);
$news->reviews()->delete();
Please Replace your code, this:
public function delete($id){
$news=News::find($id);
$news->delete();
}
To this :
public function delete($id){
$news=News::find($id);
$news->reviews()->delete();
$news->delete();
}

Laravel Relationship Find UUID

I have make a Trait for UUID. I use a lot of relationschip inside my code. On a relationship you can do find() and findOrFail() but i have write a code for findU() and findUOrFail() but i can't use it inside a relationship. How can i fix it?
Trait:
<?php
namespace App\Modules\Base\Traits;
use Ramsey\Uuid\Uuid;
/**
* Trait Uuids
*
* #package Modules\Core\Traits
*/
trait Uuids
{
/**
* Boot function from laravel.
*/
public static function bootUuids ()
{
static::creating(function ($model) {
$model->uuid = Uuid::uuid4()->toString();
});
}
/**
* #param $uuid
*
* #return mixed
*/
public static function findU ($uuid)
{
return static::where('uuid', '=', $uuid)->first();
}
/**
* #param $uuid
*
* #return mixed
*/
public static function findUOrFail($uuid)
{
$post = static::where('uuid', '=', $uuid)->first();
if( is_null($post) ) {
return abort(404);
} else {
return $post;
}
}
}
Controller:
/**
* Show
*/
public function show(Request $request, $uuid)
{
return responder()->success($request->user()->projects()->findUOrFail($uuid))->respond();
}
Error:
Call to undefined method Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany::findUOrFail()
Assuming you don't need id since you're using uuid
In your migration file you need:
$table->uuid('uuid');
$table->primary('uuid');
In your model:
use Uuids;
protected $primaryKey = 'uuid';
public $incrementing = false;
Or much easier
In your migration file:
$table->uuid('id');
$table->primary('id');
In your model:
use Uuids;
public $incrementing = false;
You don't need to override findOrFail or find
It should help to have the function referenced directly in the model rather than trying to access it directly in a trait. I am assuming that you are including the Uuids trait above in your projects model. If so, try creating a method on the projects model like this:
public function tryFindUOrFail($uuid)
{
return $this->findUOrFail($uuid);
}
Then you would write your show method as:
return responder()->success($request->user()->projects()->tryFindUOrFail($uuid))->respond();
If this doesn't work, you may need to include your method with the $appends array so that it is directly accessible through the relationship.

ODM: Cannot achieve bi-directional relationship

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();

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.

removeElement() and clear() doesn't work in doctrine 2 with array collection property

I'm trying to get some simple CRUD done with doctrine 2 but when it's time to update a record with one property set as an array collection I don't seem to get removeElement() to work as it's supposed to. I even tried doing it in this ridiculously ugly way:
foreach($entity->getCountries() as $c) {
$entity->getCountries()->removeElement($c);
$this->em->persist($entity);
$this->em->flush();
}
and it didn't work... Anyone knows how to handle this? I've asked for a solution to this in many different forms and haven't got a good response so far... seems there's lack of good examples of Doctrine 2 CRUD handling. I'll post more code at request.
Edit
//in user entity
/**
*
* #param \Doctring\Common\Collections\Collection $property
* #OneToMany(targetEntity="Countries",mappedBy="user", cascade={"persist", "remove"})
*/
private $countries;
//in countries entity
/**
*
* #var User
* #ManyToOne(targetEntity="User", inversedBy="id")
* #JoinColumns({
* #JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
I do something similar in a project with Events which have participants not unlike your User/Country relationship. I will just lay out the process and you can see if there's anything you are doing differently.
On the Participant entity
/**
* #ManyToOne(targetEntity="Event", inversedBy="participants", fetch="LAZY")
* #JoinColumn(name="event_id", referencedColumnName="id", nullable="TRUE")
* #var Event
*/
protected $event;
On the Event entity:
/**
* #OneToMany(targetEntity="Participant", mappedBy="event")
* #var \Doctrine\Common\Collections\ArrayCollection
*/
protected $participants;
Also in Event#__constructor I initialize like this:
$this->participants = new \Doctrine\Common\Collections\ArrayCollection();
Here is how I update an event:
public function update(Event $event, Event $changes)
{
// Remove participants
$removed = array();
foreach($event->participants as $participant)
{
if(!$changes->isAttending($participant->person))
{
$removed[] = $participant;
}
}
foreach($removed as $participant)
{
$event->removeParticipant($participant);
$this->em->remove($participant);
}
// Add new participants
foreach($changes->participants as $participant)
{
if(!$event->isAttending($participant->person))
{
$event->addParticipant($participant);
$this->em->perist($participant);
}
}
$event->copyFrom($changes);
$event->setUpdated();
$this->em->flush();
}
The methods on the Event entity are:
public function removeParticipant(Participant $participant)
{
$this->participants->removeElement($participant);
$participant->unsetEvent();
}
public function addParticipant(Participant $participant)
{
$participant->setEvent($this);
$this->participants[] = $participant;
}
The methods on the Participant entity are:
public function setEvent(Event $event)
{
$this->event = $event;
}
public function unsetEvent()
{
$this->event = null;
}
UPDATE: isAttending method
/**
* Checks if the given person is a
* participant of the event
*
* #param Person $person
* #return boolean
*/
public function isAttending(Person $person)
{
foreach($this->participants as $participant)
{
if($participant->person->id == $person->id)
return true;
}
return false;
}
New answer
In your countries entity, should you not have:
#ManyToOne(targetEntity="User", inversedBy="countries")
instead of inversedBy="id"?
Initial answer
You need to set the countries field in your entity as remove cascade. For example, on a bidirectional one to many relationship:
class Entity
{
/**
*
* #OneToMany(targetEntity="Country", mappedBy="entity", cascade={"remove"})
*/
private $countries;
}
This way, when saving your entity, doctrine will also save changes in collections attached to your entity (such as countries). Otherwise you have to explicitly remove the countries you want to remove before flushing, e.g.
$this->em()->remove($aCountry);
This is also valid for persist, merge and detach operations. More information here.

Resources