I have a OneToMany Relationship between a ChartPage and a BaseChart:
1 ChartPage holds 1 BaseChart and 1 BaseChart holds many ChartPages
Charts are managed in a different bundle of my application, so they can be removed individually. What I like to have is, that Doctrine automatically removes the ChartPage.Chart reference when the Chart is deleted, but nothing else (not remove the ChartPage).
The other way around should leave everything as is: When I remove the ChartPage with a referenced BaseChart - nothing should happen (not delete the BaseChart)
I tried every combination with one of these: cascade="{detach,merge,refresh,remove,persist}" that I could think of, but I can't figure it out..
This is my mapping:
<?php
/**
* Class ChartPage
* #package VBCMS\Bundle\AdminBundle\Document\Page
* #Serializer\AccessType("public_method")
* #MongoDB\Document()
*/
class ChartPage extends BasePage {
/**
* #var BaseChart
* #Serializer\Type("VBCMS\Bundle\StatisticBundle\Document\BaseChart")
* #Serializer\Accessor(setter="setChartDeserialize")
* #MongoDB\ReferenceOne(
* targetDocument="VBCMS\Bundle\StatisticBundle\Document\BaseChart",
* mappedBy="pages",
* cascade={"persist,detach,merge"}
* )
*/
protected $chart;
}
/
/**
* Class BaseChart
* #package VBCMS\Bundle\StatisticBundle\Document
* #Serializer\AccessType("public_method")
* #MongoDB\Document(
* collection="Chart",
* repositoryClass="VBCMS\Bundle\StatisticBundle\Repository\ChartRepository"
* )
*/
class BaseChart {
/**
* #var BasePage[]|Collection
* #Serializer\Exclude()
* #MongoDB\ReferenceMany(
* targetDocument="VBCMS\Bundle\AdminBundle\Document\Page\ChartPage",
* inversedBy="chart",
* cascade={"persist,detach,merge"}
* )
*/
protected $pages;
}
The only idea I have left is to build a custom preRemove EventListener that sets the references back to NULL before a BasePage ist removed, but I hoped I could avoid this manual mess.
Doctrine MongoDB ODM's cascade functionality only operates in one direction. If you perform some lifecycle event on object A, which has a reference to B, we could cascade the persist/remove/etc over to B. There is a concept of orphan removal in ODM, which allows for automated removal of objects embedded or referenced in a one-to-one or one-to-many relationship. I don't believe it's documented in the ODM manual, but it's very similar to what is described in the ORM documentation for the feature.
In your case, you don't want any cascading functionality on removal of A; you want B to remain as-is.
On the flip side, you would like all references to B among A objects to be cleaned up when you manually remove a B object. Using a pre or postRemove listener is your best option for this, and provided you have indexed the reference on A, it should be a very trivial multi-update query to set the references to null where they once referred to the instance of B that is removed.
Related
I'm trying to create MongoDB database with some references within Symfony.
In my context I have 2 documents Customer and Meeting, One Customer can have Many Meeting so that what I did :
Meeting.php
<?php
namespace FrontOfficeBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document
*/
class Meeting
{
/**
* #MongoDB\Id
* #MongoDB\ReferenceOne(targetDocument="Customer")
*/
protected $id;
/**
* #MongoDB\Field(type="timestamp")
*/
protected $creationDate;
...
Customer.php
<?php
namespace FrontOfficeBundle\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document
*/
class Customer
{
/**
* #MongoDB\Id()
* #MongoDB\ReferenceMany(targetDocument="Meeting")
*/
protected $id;
/**
* #MongoDB\Field(type="string")
*/
protected $username;
...
and then when I run the command line:
php bin/console doctrine:mongodb:schema:update
I got :
No identifier/primary key specified for Document 'FrontOfficeBundle\Document\Meeting'. Every Document must have an identifier/primary key.
I tried by using #MongoDB\UniqueIndex() but no way.
I think that #MongoDB\Id is supposed as an identifier !!!
Versions
Symfony 3.2
MongoDB 3.4.4
Any ideas ?
Thanks you.
Finally I found a solution, first I added a field called $meetings in the document Customer and an other $customer in the document meeting like this :
Customer.php
/**
* #MongoDB\ReferenceMany(targetDocument="meeting", mappedBy="customer")
*/
protected $meetings;
Meeting.php
/**
* #MongoDB\ReferenceOne(targetDocument="customer", inversedBy="meetings")
*/
protected $customer;
Then run the command line to generate setters and getters:
php bin/console doctrine:mongodb:generate:documents mybundleBundle
And when I run a fixtures (Creating one customer then its meeting), everything work fine, you'll notice that the relationship 1 to many has been defined in your document (I'm using compass to display mongoDB) as a One-to-Squillions model (for more details about 1-to-N relationship : (https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design-part-1) that's mean the son document (meeting) contains the reference of the parent as shown below :
Customer
Meeting
In MongoDB the #MongDB/Id is a primary key and the foreign key and never try to define references to this unique field.
Thanks for your attention.
What sounds a bit academic in the title is actually quite straightforward: I have set up a TYPO3 6.1 extbase extension that I've equipped with a scheduler task. The task is supposed to import a CSV file and save it into the extension's database fields.
But how do I tell the scheduler task to use the extension's model etc. and save the received data into the persistence layer?
I've seen this answer to a similar question: Execute repository functions in scheduler task and I think it points the right way, but I think need a full example to start understanding how the dependency injection works.
First you have to consider the aspect of performance:
If you want to insert a big amount of data, you should not use the Extbase persistence for such a task. Because if you do so, it will generate an object for each row you want to insert and persist it immediately. This is quite slow and has a big memory footprint.
If you don't have much data or you split the jobs (e.g. perform 100 import jobs per scheduler run), then use the Extbase persistence.
You can have both in CommandController context, and since CommandControllers are straight-forward to set up, you should go for them instead of an own Scheduler task.
Using Extbase persistence
In the CommandController, inject your repository:
/**
* myRepository
*
* #var \Venor\Myext\Domain\Repository\MyRepository
* #inject
*/
protected $myRepository
Then iterate through the rows you want to import (foreach) and create a new object for every row and add it to your repository:
$myObject = $this->objectManager->get('Vendor\Myext\Domain\Model\MyModel');
$myObject->setProperty('foo');
$myObject->setOtherProperty('bar');
$this->myRepository->add($myObject);
To actually save the objects to the database, you need to persist them. So you also inject the persistenceManager:
/**
* #var \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
* #inject
*/
protected $persistenceManager;
And use it:
$this->persistenceManager->persistAll();
You should not do that for every single object (for performance reasons); but for memory usage reasons you should not wait until after thousands of objects to persist, either. So you just insert an iterator to your foreach loop and persist every 20th, 40th, or whatever loop.
Please don't forget that the Scheduler works in Backend context, so the TypoScript must be available by module.tx_yourext. If you want to share the same settings/storagePid with the frontend part of your app, use
module.tx_yourext.persistence < plugin.tx_yourext.persistence
[...]
The TypoScript needs to be present in the root page of your website for backend modules/CommandControllers to use them. I suggest you add the stuff to myext/Configuration/TypoScript/setup.txt and add the static template of your extension to the root page.
Using DataHandler
The TYPO3 DataHandler (formerly TCEmain) is the engine the TYPO3 backend uses for inserting and modifying database records. It is very powerful.
Instead of an object, inside your loop you create an array containing all the data. The first array index is the table, the next level is the affected record, where NEW means that a new record is created. Then you can just set every field of a table with the desired value
$data = array();
$data['be_users']['NEW'] = array(
'pid' => 0,
'username' => $staffMember['IDPerson'],
'password' => md5(GeneralUtility::generateRandomBytes(40)), // random password
'usergroup' => '1,2',
'email' => $staffMember['Email'],
'realName' => $staffMember['Nachname'] . ' ' . $staffMember['Vorname'],
'lang' => 'de',
);
Now you can make an Instance of DataHandler and persist the changes:
/** #var $tce t3lib_TCEmain */
$tce = GeneralUtility::makeInstance('TYPO3\CMS\Core\DataHandling\DataHandler');
$tce->bypassAccessCheckForRecords = TRUE;
$tce->start($data, array());
$tce->admin = TRUE;
$tce->process_datamap();
$newRecordsUidArray = $tce->substNEWwithIDs['NEW'];
Please note the line $tce->admin = TRUE. This suggests to DataHandler that an admin is performing the action. This is convenient because you don't have to set allowed exclude fields for the Scheduler user and can also insert records to PID 0. But it is a possible security flaw, so carefully consider its usage.
Records inserted/updated by DataHandler logged correctly, can be reverted etc.. You can find some examples (such as adding pictures, resolving MM relations) here. In this case all DataHandler related functions were moved to an external repository class that is injected to the CommandController as described above (it's just named in Extbase convention).
A good overview of DataHandler functions can be found here.
In Addition to lorenz's answer: Beginner's Guide to set up a Command Controller Scheduler task:
My example is an import task. Change the Name part "Import" to your needs.
Create a new file EXT:Classes/Controller/ImportCommandController.php
<?php
namespace NAMESPACE\Myextension\Controller;
/***************************************************************
* Copyright notice
*
* (c) 2014
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
*
*
* #package Myextension
* #license http://www.gnu.org/licenses/gpl.html GNU General Public License, version 3 or later
*
*/
class ImportCommandController extends \TYPO3\CMS\Extbase\Mvc\Controller\CommandController {
/**
* itemRepository
*
* #var \NAMESPACE\Myextension\Domain\Repository\ItemRepository
* #inject
*/
protected $itemRepository;
/**
* #var \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager
* #inject
*/
protected $persistenceManager;
/**
*
* #param \integer $storagePid
* #param \string $url
*
* #return bool
*/
// very nice: parameters will be fields in the scheduler!
public function importCommand($storagePid = 0,$url = NULL) {
$source = utf8_encode(utf8_encode(file_get_contents($url)));
// set storage page ourselves
// not sure if really necessary
$querySettings = $this->itemRepository->createQuery()->getQuerySettings();
$querySettings->setRespectStoragePage(FALSE);
$this->itemRepository->setDefaultQuerySettings($querySettings);
// do your stuff here
$source = 'foo';
$rendered = 'bar';
// then store it
// this seems to be only necessary if we don't have an existing item yet
// but as we don't know that here, we have to do it
$item = $this->objectManager->get('STUBR\Therapiestellen\Domain\Model\Item');
// find all existing items
$all = $this->itemRepository->findAll();
// if we have an item already, take the first (and only one)
if(count($all) > 0){
$item = $all->getFirst();
}
// set / update properties
$item->setSource($source);
$item->setRendered($r);
$item->setPid($storagePid);
// either update or add it
if(count($all) > 0){
$this->itemRepository->update($item);
}
else {
$this->itemRepository->add($item);
}
// persist it
$this->persistenceManager->persistAll();
}
}
?>
In EXT:ext_localconf.php, add the command controller:
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'NAMESPACE\\Myextension\\Controller\\ImportCommandController';
Configure in Scheduler:
That's basically it!
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.
I have 2 entities: User and Avatar. Each user can choice one avatar from a list, so I think this is a One2One unidirectional relationship. The problem is that the field avatar_id is always NULL in the db when I save the form.
Let's see the code:
class User implements UserInterface
{
/**
* #var int $avatarId
*
* #ORM\Column(name="avatar_id", type="integer", nullable=true)
*/
private $avatarId;
/**
* #var Avatar
*
* #ORM\OneToOne(targetEntity="Avatar", cascade={"persist", "remove"})
*/
private $avatar;
}
When I var_dump the user object before saving, the field avatarId contains the Avatar object but the id is not saved. What I'm doing wrong?
["avatarId":"Test\UserBundle\Entity\User":private]=>
object(Test\UserBundle\Entity\Avatar)#419 (5) {
["id":"Test\User\Bundle\Entity\Avatar":private]=>
int(3)
["imageName":"Test\UserBundle\Entity\Avatar":private]=>
string(14) "death-dark.jpg"
}
You don't need the avatarId field, since the avatar field will take care of it automatically. You can use the #JoinColumn annotation to set the referencing column name explicitly.
A few things:
ID columns are typically generated with the following annotations:
/**
* #ORM\Id #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
This tells the ORM to automatically generate a value when you create a new object, thus when you dump the object it will have a value before persisting.
And a note on your OneToOne relationship, if Avatars can be used by multiple people from that list, that would be a ManyToOne relationship (Many users to one avatar, one avatar to many users).
I have 3 models that I have setup thus far in a simple application I am working on:
So far I have these models:
UserAccountEntity - Top level Table (Has a One-Many Relationship to UserAccountEntityStrings)
UserAccountEntityStrings - Child Table (Has a Many-One relation ship to UserAccountEntity and EavAttributes
EavAttributes - Lookup Table
When I query data from my top level table, I get the schema,association information for the child table. But I do not get any of the persisted data from the child table.
What I expected the results to be were, the data from the top level model and the data from the associated child model. Any help with this is greatly appreciated.
A note that may be helpful, I am using Zend 1.11.10 and Doctrine 2
This is what my query looks like:
$users = $em->createQuery('select u from Fiobox\Entity\UserModule\UserAccountEntity u')->execute();
Zend_Debug::dump($users[0]);
This is the association in my top level model:
/**
*
* #param \Doctrine\Common\Collections\Collection $property
* #OneToMany(targetEntity="UserAccountEntityStrings",mappedBy="UserAccountEntity", cascade={"persist","remove"})
*/
private $strings;
These are the associations in my child model:
/**
*
* #var UserAccountEntity
* #ManyToOne(targetEntity="UserAccountEntity")
* #JoinColumns({
* #JoinColumn(name="entity_id", referencedColumnName="entity_id")
* })
*/
private $user;
/**
* #var EavAttribute
* #ManyToOne(targetEntity="Fiobox\Entity\EavModule\EavAttributes")
* #JoinColumn(name="attribute_id", referencedColumnName="attribute_id")
*/
private $attributes;
Have you actually tried anything?
Doctrine will lazy load stuff for you. Your var_dump probably shows persistent collections of proxy objects for your child objects. But if you access them, they'll be loaded automatically:
<?php
$users = $em->createQuery('select u from Fiobox\Entity\UserModule\UserAccountEntity u')->fetchAll();
foreach($users as $u){
foreach($u->strings as $s){
var_dump($s);
}
}
If you know that you're going to need all that child data, you might as well force a fetch-join in your DQL:
<?php
$users = $em->createQuery('select u, s from Fiobox\Entity\UserModule\UserAccountEntity u JOIN u.strings s')->fetchAll();