Cloning embedded document only works if value is changed - mongodb

I have a Mongo database to which I am connected through the following document classes in Symfony, using Doctrine ODM (I am only showing the relevant properties).
/**
* #MongoDB\Document
* #MongoDB\InheritanceType("COLLECTION_PER_CLASS")
*/
class Feature {
/** #MongoDB\EmbedOne(targetDocument="FeatureProperties") */
protected $properties;
}
/** #MongoDB\Document */
class City extends Feature {}
/** #MongoDB\EmbeddedDocument */
class FeatureProperties {
/** #MongoDB\String */
protected $name;
}
I am trying to clone a City in my controller, including its properties, essentially its properties.name.
To do this, I get the city using QueryBuilder and call it $city_origin.
Then, here's how I clone it:
$new_city = clone $city_origin;
$city_name = $city_origin->getProperties()->getName();
// $city_name .= ' ';
$documentManager->detach($city_origin);
$new_city->setId('_' . $city_origin->getId());
$new_city->getProperties()->setName($city_name);
$documentManager->persist($new_city);
$documentManager->flush();
The new document is created, but properties and properties.name do not exist.
Now the strange thing is, if I uncomment the line where I modify the $city_name, then the properties.name is created with the modified value.
If instead of fetching the $city_name from $city_origin I input its value manually (as a string), the attribute is not created either. If I give the name of another City in my collection, there is no issue.
Ideally, I would like to clone the entire document with its embedded documents: how can I do that?

When you clone a object, you get a copy of this object whit the same reference of the embedded object.
When you do :
$documentManager->detach($city_origin);
Doctrine will detach all the object related to $city_origin because of your entity definition :
cascade=DETACH or cascade=ALL
http://docs.doctrine-project.org/en/2.0.x/reference/working-with-objects.html#detaching-entities
You need to define the clone function in the city class and clone all the object you need.
http://www.php.net/manual/en/language.oop5.cloning.php

Related

TYPO3 Extension: How to make a relation to another extension’s model optional?

I have an events extension (for TYPO3 9 LTS and 10 LTS), say MyVendor\MyEvents and a Locations extension, say MyVendor\MyLocations.
The Model MyVendor\MyEvents\Domain\Model\Events has a property eventLocation which is defined to be an object of MyVendor\MyLocations\Domain\Model\Locations.
Now I want to make the relation to MyVendor\MyLocations\Domain\Model\Locations optional. I have found a way for the TCA to show a different form field in the backend depending on the MyLocations extension being installed. But I have no idea how to make all the type definitions in the Events model conditional. They are crucial for the extension to work:
namespace MyVendor\MyEvents\Domain\Model
class Events extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
/**
* #var \MyVendor\MyLocations\Domain\Model\Locations
*/
protected $eventLocation = NULL;
/**
* #return \MyVendor\MyLocations\Domain\Model\Locations $eventLocation
*/
public function getEventLocation()
{
return $this->eventLocation;
}
/**
* #param \MyVendor\MyLocations\Domain\Model\Locations $eventLocation
* #return void
*/
public function setEventLocation(\MyVendor\MyLocations\Domain\Model\Locations $eventLocation)
{
$this->eventLocation = $eventLocation;
}
}
In case MyVendor\MyLocations is loaded it needs to be defined as above, in case it isn’t loaded it should be just an integer.
In the TCA I am using if (TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('my_locations')) for showing a different field in the backend form for an event.
The Locations Model is in a separate extension because I am using it in a third extension as well.
In your events extension you could setup a repository for locations. Then you can map this repository to your location extensions model via TypoScript.

Doctrine and MongoDb: Define required fields in MongoDb-Document (

I'm using MongoDB with Doctrine for a Symfony project.
For a document (I call it class Product) I need to define a field (e.g. name), but I want to act doctrine in a way that the document cannot be saved without defining a it. Meaning, whenever creating or updating a new Product, the property $name has to be defined.
For doctrine and MySql this is quite simple. There you have to say:
class Product {
...
/** #ORM\Column(type="string", nullable=false) */
private $name;
...
}
Is there a similar way in MongoDb?
The following code (which I found in a similar thread) doesn't work:
class Product {
...
/**
* #MongoDB\Field(type="string")
* #Constraints\NotBlank
*/
protected $name;
...
}

ManyToOne with FOSUSerBundle ignoring exclusion policy

Building a JSON response for an API type thing, to retrieve a specific set of data that includes a ManyToOne relationship in the entity for my entity that extends FOSUSerBundle's User entity (called Account in my case).
The problem is, the Account entity thats included as a field in the response, is wanted, but I dont want to include all of the password and role type stuff.
I've been browing the internet for a couple hours now, and I've followed many guides on this, and I've cleared my cache every single time, and to no avail; So here's where I ended up:
// app/config/config.yml
jms_serializer:
metadata:
auto_detection: true
directories:
FOSUserBundle:
namespace_prefix: "FOS\\UserBundle"
path: "%kernel.root_dir%/Resources/serializer/FOS"
I've for below I've tried User.Model.yml and Model.User.yml and User.Entity.yml as well in a vain thought that the file name actually matters
// app/Resources/serializer/FOS/Entity.User.yml
FOS\UserBundle\Model\User:
exclusion_policy: ALL
properties:
id:
expose: true
and what I get still looks like this:
{
"status":"ok",
"api_version":"1.0",
"code":200,
"data":{
"video":{
"id":1,
"published":true,
"visibility":true,
"title":"Megaman 2",
"slug":"megaman-2",
"summary":"A rap song about Megaman",
"description":"A rap song\r\nAbout megaman",
"youtube_id":"R6L9bUouDr8",
"date_published":"2014-07-02T14:09:26-0700",
"date_created":"2014-07-02T14:09:26-0700",
"date_updated":"2014-07-02T14:09:26-0700",
"author_id":3,
"author":{
"id":3,
"username":"kharrison",
"username_canonical":"kharrison",
"email":"(sorry private)",
"email_canonical":"(sorry, private)",
"enabled":true,
"salt":"(sorry, private)",
"password":"(sorry, private)",
"last_login":"2014-07-04T15:17:34-0700",
"locked":false,
"expired":false,
"roles":[
"ROLE_SUPER_ADMIN"
],
"credentials_expired":false,
"display_name":"Kyle Harrison",
"slug":"kyle-harrison",
"bio":"Test"
}
}
}
}
The "author" field, is my Account entity thats being run through the JMSSerializer
I want to exclude ALL of that, except the user ID, Display name, and slug.
And finally this is how the API works:
// My/Bundle/Controller/BaseAPIController.php
//......... other code
/**
* #param string $status
* #param integer $code
* #return Response
*/
public function render_api($status, $code)
{
$this->apiResponse->setStatus($status);
$this->apiResponse->setCode($code);
return new Response($this->apiResponse->serialize($this->get('jms_serializer')), $this->apiResponse->getCode(), ["Content-type"=>"application/json"]);
}
//............. other code
and finally, that calls this:
// My/Bundle/Models
class APIResponse {
protected $status;
protected $apiVersion;
protected $code;
protected $data;
public function __construct($apiVersion, $status = "OK", $code = 500)
{
$this->status = $status;
$this->code = $code;
$this->apiVersion = $apiVersion;
$this->data = [];
}
// ... getters and setters
/**
* #return mixed
*/
public function serialize($serializer) {
return $serializer->serialize($this, "json");
}
}
I've for below I've tried User.Model.yml and Model.User.yml and
User.Entity.yml as well in a vain thought that the file name actually
matters.
It does matter, actually. It's a concatenation of the namespace and class name. In this case, you're trying to configure the FOS\UserBundle\Model\User class, so the file name should be Model.User.yml. (FOS\UserBundle\ should be excluded from the file name, since you configured it as namespace_prefix in your config.yml)
Also make sure that your Account class doesn't re-declare (overwrite) the properties, as the serializer config only works if you configure it for the class that actually declares the properties.
Ok So, the actual answer, couldn't have been arrived to via the information I provided. But Nic's Answer did lead me towards the solution. The description of how the the serializer looks at and deciphers the config file lead me to the real problem at hand.
This is what I failed to show:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends BaseUser
{
The problem lays with the Alias I provided the FOS\UserBundle\Model\User namespace. I no longer remember why I wrote that that way. However, the moment I remove the Alias and rewrote the extends to resemble this instead:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends User
{
combined with the new correct filename from Nic's answer, the config based Exclusion policy for JMSSerializerBundle totally kicks in, and every instance of FOSUserBundle's items are now completely hidden, except for the fields I've now explicitly told it to expose.
This is exactly what I wanted :)
Thanks everyone for your help! Cheers
~k
I'm not sure it's the exact way you want it, more a way around:
way around 1: Select only the properties you want (via the entity manager) and then serialize the array obtained.
It's what I do with what I call my API (which is not a class as you but controllers)

How to annotate sub-documents in Doctrine MongoDB?

I want to implement php classes that should model the following :
(Symfony,DoctrineMongoDBBundle)
Notification Collection { from { id , fname } , to , sentDate }
Member Collection { id , fname , lname , email , phone , regDate , ... }
And i want tell to ODM : "from field is an object that holds only 2 values ,id of the sender and his fname"
What annotation i must use? Should i define another class like from.php ? Or i can create two classes as the following:
/*
*#Document
*/
class Notification {
/*
*#Id
*/
protected $id;
/*
*#EmbedOne(targetDocument="Member")
*/
protected $from;
/*
*#ReferenceOne(targetDocument="Member")
*/
protected $to;
/*
*#Date
*/
protected $sentDate;
}
/*
*#Document
*/
class Member {
/*
*#Id
*/
protected $id;
/*
*#String
*/
protected $fname;
/*
*#String
*/
protected $lname;
/*
*#String
*/
protected $email;
.
.
.
}
If it's correct,in my controller how can i control "from" field to hold only id and fname from a Member object?
suppose this:
$senderUser;
$newNotification = new Notification();
$newNotification->setFrom($senderUser);
Then $newNotification->from set to a Member object that hold all info about a Member.But i want only id and fname of the sender to persist! (because needs of my project)
Excuse my English grammatical errors.
Thanks for any help...
If you insist on nesting the notification's originating user ID and first name in a from field, then you will need an Embed One relationship to a From document, which in turn has the following fields:
id: this should be a Reference One relationship to a Member document. I would suggest using the simple option for the reference, so that you only store the Member's ID value, rather than a DBRef object.
fname: this should be a string field. ODM has no facility for keeping it up-to-date with the referenced Member document, so you will need to ensure that on your own.
Personally, the Notification document seems small enough that I would simply create fromMember and fromMemberFirstName fields directly on Notification. Then, you could add some logic in the setFromMember() method that also sets the fromMemberFirstName field from the passed Member argument.
If you want to take things a step further, you could explore using events to monitor Member objects for changed fname fields and issue queries to update Notification documents, but that's a separate conversation.

How to delete document from a referenced array of documents in doctrine ODM with mongodb

I have a php object mapping to a mongodb document(called Node) with a structure
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
class Node{
/**
* #MongoDB\Id
*/
protected $id;
/**
* #MongoDB\String
*/
protected $domain;
/**
* #MongoDB\ReferenceMany(targetDocument="NodeItem",cascade=
* {"persist"},simple="true")
*/
protected $items = array();
//getter and setters below
}
And a referenced document called, NodeItem,
class NodeItem {
/**
* #MongoDB\Id
*/
protected $id;
/**
* #MongoDB\String
*/
protected $name;
/**
* #MongoDB\ReferenceOne(targetDocument="Node", cascade={"persist"},
* simple="true")
*/
protected Node;
//setter and getters
}
As reflected by the annotations above 'Node' references MANY 'NodeItems' stored in a $items array and 'NodeItems' references ONE 'Node'. So those are bi-directional referenced collections.
My Question is how to effectively delete a few 'NodeItem' documents from its collection (based on the array of available ids), so that the deleted NodeItem documents are also deleted from the $items array references in 'Node' (cascaded delete I think is what I am asking for?).
I wrote a function that has code like this :
$qb = $this->dm->createQueryBuilder('SomeBundleBundle:NodeItem');
/*
* deletes from NodeItem collection
*/
foreach($NodeItemsArray as $itemId){
$qb->remove()->field('id')->equals($itemId)->getQuery()->execute();
}
But the above function only deletes the documents from NodeItem collection, but the associated items in the $items array of 'Node' are not deleted. Also, the {cascade:persist} in the annotations doesn't seem to help. The code is implemented in Symfony 2 framework
Some help is appreciated !
The only way to achieve that is with a listener on the onRemove event.
But has mentioned by #jmikola, you'll have to use the $dm->remove() method, and not the QueryBuilder (since it doesn't support events yet).
so, to delete the Item do:
//Get the NodeItem you want in the items array and delete it:
$items = $node->getItems();
$dm->remove($items->get(2)); //Remove the third item as an example
And register the event:
class CascadeNodeDeleterListener {
public function preRemove(LifecycleEventArgs $eventArgs) {
$odm = $eventArgs->getDocumentManager(); /* #var $odm DocumentManager */
$object = $eventArgs->getDocument();
if($object instanceOf NodeItem) {
$node = $object->getNode();
$items = $node->getItems();
$items->removeElement($object);
$class = $dm->getClassMetadata(get_class($node));
$dm->getUnitOfWork()->recomputeSingleDocumentChangeSet($class, $node);
}
}
}
In services.yml:
<service id="listener" class="CascadeNodeDeleterListener">
<tag name="doctrine.common.event_listener" event="onRemove" />
</service>
See Doctrine ODM Events from more info.
Cascade behavior in ODM is only respected by UnitOfWork operations. MongoDB does not natively support cascades and triggers (yet, anyway). In your case, query builder will construct and execute a query like the following:
db.node_items.remove({"_id": ObjectId("...")})
UnitOfWork is not involved at all (there are no staged operations or flushing) and no cascade is triggered.
On the other hand, say you had a managed $nodeItem object. Passing it to DocumentManager::remove() would invoke UnitOfWork and cause any referenced documents mapped with cascade=REMOVE or cascade=ALL to also be removed. Of course, you'd have to call flush() to execute the operations in MongoDB.
Based on your current code, the only operation that will be cascaded is DocumentManager::persist(). In practice, I assume you'd create a Node, construct and add a few NodeItems to it, and persist the Node (allowing its items to be persisted automatically).
If NodeItems only ever belong to a single Node, you may want to avoid cascade=REMOVE and simply do $nodeItem->getNode()->getItems()->removeElement($nodeItem) after you remove the $nodeItem itself, but before flush().
Also, I noticed you're initializing your collection field to an array. Later on, ODM is going to hydrate this field as a Doctrine\ODM\MongoDB\PersistentCollection instance, which could lead to ambiguity. As a best practice, you should initialize such fields as Doctrine\Common\Collections\ArrayCollection in your constructor. That way, you can always expect them to be Collection instances.