I have two entities (simplified):
class EncryptedMasterKey {
/**
* #ORM\ManyToOne(targetEntity="ExchangeFile", inversedBy="encryptedMasterKeys")
* #ORM\JoinColumn(name="exchange_file_id", referencedColumnName="id")
*
* #var ExchangeFile
*/
protected $exchangeFile;
}
and
class ExchangeFile {
/**
* #ORM\OneToMany(targetEntity="EncryptedMasterKey", mappedBy="exchangeFile", orphanRemoval=true, cascade={"persist", "remove"})
*/
protected $encryptedMasterKeys;
}
There can be many EncryptedMasterKeys for one ExchangeFile in the database. If the ExchangeFile is deleted, all related encrypted MasterKeys are deleted (orphanRemoval=true and cascade={"persist", "remove"} make sure this is the case). So far, so good.
Now as the actual file lies encrypted on the hard disk, there must be at least one EncryptedMasterKey so that the file can be decrypted. So when a EncryptedMasterKey is deleted and I discover that it is the last one for it's ExchangeFile, I also have to delete the ExchangeFile because it cannot be decrypted any more. An ExchangeFile cannot live without at least one EncryptedMasterKey.
How do I achieve this? #ORM\PreRemove in the EncryptedMasterKey class does't really help me because I don't have access to the Entity Manager:
class EncryptedMasterKey {
//...
/** #ORM\PreRemove */
public function removeOrphanExchangeFile()
{
if ($this->exchangeFile->isTheOnlyMasterKey($this))
// I don't have access to the Entity Manager,
// so how do I delete the ExchangeFile?
}
}
Is there any elegant solution to this?
Thanks for your time.
You can use an event subscriber and create a class like following:
class MyEncryptedMasterSubscriber implements \Doctrine\Common\EventSubscriber
{
public function getSubscribedEvents()
{
return array(\Doctrine\ORM\Events::onFlush);
}
public function onFlush(\Doctrine\ORM\Events\OnFlushEventArgs $eventArgs)
{
$uow = $eventArgs->getEntityManager()->getUnitOfWork();
foreach ($uow->getScheduledEntityDeletions() AS $entity) {
if (
$entity instanceof EncryptedMasterKey
&& $entity->getExchangeFile()->isTheOnlyMasterKey($entity)
) {
$uow->scheduleForDelete($entity->getExchangeFile());
}
}
}
}
You can read more about how to register subscribers in the particular case of Symfony 2 on the documentation for it.
Related
I'm considering using Doctrine2 in a project for the first time. In my research, one thing that alarmed me is this:
<?php
/**
* #Entity(repositoryClass="BugRepository")
* #Table(name="bugs")
**/
class Bug
{
//...
}
It appears I can only associate one Repository with each entity? What would I do if this Repository class grows too large (too many methods/code) and I would like to separate it into multiple Repository classes?
For exactly your situation there is Repository as a Service:
<?php
/**
* #Entity
* #Table(name="bugs")
**/
class Bug
{
//...
}
Then create first repository:
<?php
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
final class FirstBugRepository
{
/**
* #var EntityRepository
*/
private $repository;
public function __construct(EntityManager $entityManager)
{
$this->repository = $entityManager->getRepository(Bug::class);
}
// ... some methods
}
And next one:
<?php
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
final class SecondBugRepository
{
/**
* #var EntityRepository
*/
private $repository;
public function __construct(EntityManager $entityManager)
{
$this->repository = $entityManager->getRepository(Bug::class);
}
// ... other methods
}
Do you want to know more and a full example? Check post How to use Repository with Doctrine as Service in Symfony
I'm trying to figure out a way to cascade an operation on all entities associated with an entity. For example, if I have a User, and a user has entities, and those entities have entities, I want to perform an operation on every entity. Specifically I want to perform validation on every entity in that tree.
How would I implement something like 'getAssociatedEntities()':
class User {
/**
* #ManyToOne(targetEntity="Comment")
*/
private $comment;
/**
* #ManyToOne(targetEntity="Something")
*/
private $something;
}
$user->setComment($comment);
$user->setSomething($something);
$associated_entities = $user->getAssociatedEntities(); // NOT A REAL METHOD
foreach ($associated_entities AS $entity) {
validate($entity);
}
I realize I could use lifecycle callbacks to perform validation. However, setting validation annotations makes things sooooo much easier. I can validate every entity I persist, BUT I can't validate its associated entities.
I can validate every entity I persist, but I can't validate its
associated entities.
Of course you can perform any action to entitied associated with main entity if:
You add cascade={"all"} to #ManyToOne definition (required !).
#HasLifecycleCallbacks for each sub-entity you want action performed and mark methods #PrePersist or #PostPersist or more in documentation.
For example I use this method to cascade deletion of image file in the Image entity, while the any entity that contains Image entity is beeing deleted:
Here I have an livecycle callback:
/**
* An image.
*
* #ORM\Entity
* #ORM\Table(name="images")
* #ORM\HasLifecycleCallbacks
*/
class ImageEntity extends AbstractEntity
{
/**
* #ORM\PreRemove
*/
public function preRemoveDeleteFile()
{
// remoce file
}
}
Here I have an entity that contains Image (where there is a cascade operation defined):
/**
* A competency group name.
*
* #ORM\Entity
* #ORM\Table(name="product_meta_image_cover")
*/
class ProductMetaImageCoverEntity extends AbstractEntity
{
/**
* #var \ModuleModel\Entity\ImageEntity
* #ORM\OneToOne(targetEntity="ModuleModel\Entity\ImageEntity", cascade={"all"}, orphanRemoval=true)
* #ORM\JoinColumn(onDelete="CASCADE")
*/
protected $image;
}
Being able to use Doctrine speeds up a lot of things however it feels somewhat clunky to me having to set / use the entity manager in all of my controllers. I would prefer to have all of the database logic in 1 specific module. Perhaps I'm just thinking about this the wrong way, and someone can point me in the right direction.
Currently I have my Entity which functions just fine and I can do insertions into the database fine with the following
namespace Manage\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class ViewController extends AbstractActionController {
public function somethingAction(){
$objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$user = new \Manage\Entity\User();
$user->setname('foo');
$user->settitle('bar');
$objectManager->persist($user);
$objectManager->flush();
}
}
However whenever I want to select something from the database I have to make sure to add
use Doctrine\ORM\EntityManager;
And then the following list of Controller functions...
/**
* #var EntityManager
*/
protected $entityManager;
/**
* Sets the EntityManager
*
* #param EntityManager $em
* #access protected
* #return PostController
*/
protected function setEntityManager(EntityManager $em) {
$this->entityManager = $em;
return $this;
}
/**
* Returns the EntityManager
*
* Fetches the EntityManager from ServiceLocator if it has not been initiated
* and then returns it
*
* #access protected
* #return EntityManager
*/
protected function getEntityManager() {
if (null === $this->entityManager) {
$this->setEntityManager($this->getServiceLocator()->get('Doctrine\ORM\EntityManager'));
}
return $this->entityManager;
}
Once I have added all of that I can now do a query in my getsomethingAction like so...
public function getsomethingAction() {
$repository = $this->getEntityManager()->getRepository('Manage\Entity\User');
$list = $repository->findAll();
var_dump($list);
return new ViewModel();
}
To me that feels very clunky... I can do an insert without needing all the extra functions but I cannot do a select? Is it possible to extend the Entity class in order to get the find / findAll etc functions that is provided by calling $repository = $this->getEntityManager()->getRepository('Manage\Entity\User'); directly inside the entity?
By that I mean I would prefer to be able to run the find directly on the entity as I would when I set the data... like below:
public function getsomethingAction(){
$list = new \Manage\Entity\User();
$l = $list->findAll();
var_dump($l);
return new ViewModel();
}
Ok so my main objective so far has been to move the complex logic out of the controllers into a re-usable model. So with this example answer I'm creating an interface where the complex logic would live however it also allows me to still use the model in a controller to get data from the database... here is the Model...
namespace Manage\Model;
use Doctrine\ORM\EntityManager;
class ApiInterface {
/**
* #var EntityManager
*/
protected $entityManager;
protected $sl;
/**
* Sets the EntityManager
*
* #param EntityManager $em
* #access protected
* #return PostController
*/
protected function setEntityManager(EntityManager $em) {
$this->entityManager = $em;
return $this;
}
/**
* Returns the EntityManager
*
* Fetches the EntityManager from ServiceLocator if it has not been initiated
* and then returns it
*
* #access protected
* #return EntityManager
*/
protected function getEntityManager() {
if (null === $this->entityManager) {
$this->setEntityManager($this->sl->get('Doctrine\ORM\EntityManager'));
}
return $this->entityManager;
}
public function __construct($ServiceLocator) {
$this->sl = $ServiceLocator;
}
public function get() {
$repository = $this->getEntityManager()->getRepository('Manage\Entity\ApiList');
return $repository;
}
public function set() {
return new \Manage\Entity\ApiList();
}
public function save($data) {
$objectManager = $this->sl->get('Doctrine\ORM\EntityManager');
$objectManager->persist($data);
$objectManager->flush();
}
public function doComplexLogic($foo,$bar){
// Can now use both set() and get() to inspect/modify/add data
}
}
So now inside my controller I can do something that gets some basic data from the table like:
public function getapiAction() {
$api = new \Manage\Model\ApiInterface($this->getServiceLocator());
var_dump($api->get()->findAll());
return new ViewModel();
}
And to quickly set data from a controller I can do:
public function setapiAction() {
$apiInterface = new \Manage\Model\ApiInterface($this->getServiceLocator());
$api= $apiInterface->set();
$user->setfoo('blah');
$user->setbar('moo');
$apiInterface->save($api);
return new ViewModel();
}
And it also allows me to run complex logic from the controller by taking the complexity out of the controller like so...
public function complexAction(){
$foo = $this->params()->fromQuery();
$bar = $this->params()->fromPost();
$apiInterface = new \Manage\Model\ApiInterface($this->getServiceLocator());
$apiInterface->doComplexLogic($foo, $bar);
}
Please let me know in comments if this answer would be the proper way to do things, I realize it's very simple and generic but I wanted to keep it that way so others can understand what / why and if this is a good approach / not etc.
I'm doing test unit of my entity:
namespace PathtomyBundle\Tests;
require_once dirname(__DIR__).'/../../../app/AppKernel.php';
use Doctrine\ORM\Tools\SchemaTool;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
/**
* #var Symfony\Component\HttpKernel\AppKernel
*/
protected $kernel;
/**
* #var Doctrine\ORM\EntityManager
*/
protected $entityManager;
/**
* #var Symfony\Component\DependencyInjection\Container
*/
protected $container;
public function setUp()
{
// Boot the AppKernel in the test environment and with the debug.
$this->kernel = new \AppKernel('test', true);
$this->kernel->boot();
// Store the container and the entity manager in test case properties
$this->container = $this->kernel->getContainer();
$this->entityManager = $this->container->get('doctrine')->getEntityManager();
// Build the schema for sqlite
//$this->generateSchema();
parent::setUp();
}
public function tearDown()
{
// Shutdown the kernel.
$this->kernel->shutdown();
parent::tearDown();
}
protected function generateSchema()
{
// Get the metadatas of the application to create the schema.
$metadatas = $this->getMetadatas();
if ( ! empty($metadatas)) {
// Create SchemaTool
$tool = new SchemaTool($this->entityManager);
$tool->createSchema($metadatas);
} else {
throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
}
}
/**
* Overwrite this method to get specific metadatas.
*
* #return Array
*/
protected function getMetadatas()
{
return $this->entityManager->getMetadataFactory()->getAllMetadata();
}
}
and also:
namespace pathtomybundle\Tests\Entity;
use pathtomybundle\Tests\TestCase;
use pathtomybundle\Entity\Calendars;
require_once dirname(__DIR__).'/TestCase.php';
class CalendarsDbTest extends TestCase
{
protected $Calendars;
public function setUp()
{
parent::setUp();
$this->Calendars = new Calendars();
}
public function testGenerateCalendars()
{
$this->Calendars->setBeginDate(new \DateTime('now'));
$this->Calendars->setDescription('Description');
$this->Calendars->setEndDate(new \DateTime('now'));
$this->Calendars->setType('sur titre');
// Save the ExCalendars
$this->entityManager->persist($this->Calendars);
$this->entityManager->flush();
}
public function testUser(){
$this->assertEquals('Description', $this->Calendars->getDescription() );
}
So my questions are:
Why does it raise this error "Failed asserting that null matches expected"?
Why getDescription() returns NULL?
How to test two table with One-to-Many relationship for example my Table Calendars with another table in database?
Edit
For the third question :
For example I have two Tables Job and Calenders with Many-to-One relationship so I will have a Job_Id field in Calendars Table,so how I will do my test Unit with a foreign key "job_id"
In Calendars Entity :
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="job_id")
* })
*/
private $jobId;
Edit-2-
when I run my phpunit test "phpunit -c app" to test setters function and persist in database so I have a with every test a new data insered in databse, my question is it possible to do a lot of test but I insert data in database just for one time because actually I must remove data from database with every test.
2 - another question : to create a database_test i use "$this->generateSchema();
" so after create a database for the first time and when the test call "TestCase"class (the code above) again so he tried to create the database_test again then I must remove the line after the first time and it's not good,so what I can do to run this line just for one time in the first time when i run my test?
Edit-3
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="id")
* })
*/
private $job;
it's normal?
Every test in test case creates his own CalendarsDbTest object. So, in fact, $this->Calendar is different object in each test (if you want share it between tests you need create it in setUp method)
Is the same as above (there is null because you never call setDescription with $this->Calendars - it's different object than it is in first test)
I'm not sure what exactly you mean. Can you show more precise (for example method you want test) what you mean?
edit:
The answer is: you don't test it. Why? Because unit test is UNIT test - you should test here only your entity. Persistence, keeping relations etc. are Doctrine resposibility and should be tested there - you don't worry about it.
The only thing you should test is setter/getter for $jobId property (btw. it should be "$job" rather than "$jobId" because it's object of Job class - not an integer), eg.:
class CalendarTest extends \PHPUnit_Framework_TestCase
{
(...)
public function testSetGetJob()
{
$job = new Job();
$job->setSomeProperty('someValue');
$expectedJob = clone $job; // you clone this because in setter you pass object by reference
$calendar = new Calendar();
$calendar->setJob($job);
$this->assertEquals($expectedJob, $calendar->getJob());
}
(...)
}
I have the following setup "Many Users can have Many Projects (Collaborators)"
/**
* #Entity #HasLifeCycleCallbacks
* #Table(name="projects")
*/
class Project implements \Zend_Acl_Resource_Interface {
/**
* #ManyToMany(targetEntity="User", mappedBy="projects")
* #OrderBy({"displayName" = "ASC", "username" = "ASC"})
*/
protected $collaborators;
..
}
/**
* #Entity
* #Table(name="users")
*/
class User implements \Zend_Acl_Role_Interface {
/**
* #ManyToMany(targetEntity="Project", inversedBy="collaborators")
*/
protected $projects;
...
}
I tried to remove a collaborator using the following
$user = Application_DAO_User::findById($this->_getParam('userid'));
$proj = Application_DAO_Project::getProjectById($this->_getParam('id'));
Application_DAO_Project::removeCollaborator($proj, $user); // <---
// Application_DAO_User
public static function findById($id) {
return self::getStaticEm()->find('Application\Models\User', $id);
}
// Application_DAO_Project
public static function getProjectById($id) {
return self::getStaticEm()->find('Application\Models\Project', $id);
}
public static function removeCollaborator(Project $proj, User $collaborator) { // <---
$proj->getCollaborators()->remove($collaborator);
$collaborator->getProjects()->remove($proj);
self::getStaticEm()->flush();
}
And there isn't any errors but the database stays the same ...
This may be well over due but was just experiencing the same problem myself... According to the doctrine 2 documents, the function ArrayCollection->remove($i) is for removing by array index.
What you are after is:
getCollaborators()->removeElement($collaborator);
I went round in circles trying to figure this out until I realised that for this to work:
getCollaborators()->removeElement($collaborator);
$collaborator would have to be the actual object from the collaborators ArrayCollection. That is, if you pass in a new Collaborator object with the same parameters it won't remove it. That's because ArrayCollection uses array_search to look for the object you want to remove.
Hope that saves someone else a few hours...