Symfony : Detached entity cannot be removed in test suite w/ phpunit - php

I'm actually doing some functionnal testing on my api and I'm facing a problem which I don't really understand.
I want to test an API which interact with a remote web hosting server. The goal is to manage VirtualHosts, DNS zones, database etc...
I have a test remote server and to avoid conflicts, I remove the created stuff after the test (in the TearDown() function) and create the base (in the setUp() function). In the setUp() I also load fixtures in the setup.
After a test which add a child of my main entity, I want to clean the remote server :
$service = $this->fixtures->getReference('service-web');
$this->container->get('webmanager')->deleteHosting($service, true);
The deleteHosting() function deletes all remote stuff (the true parameter is the "force" parameter, which ensure that the function doesn't stop after an error.
The $service variable contains my main service entity. I also have in this entity a One-To-Many relationship with addonDomain's entity.
My functionnal test creates an addonDomain.
The test is OK, but when I try to delete my service, the attached entity makes Doctrine yell as hell : Doctrine\ORM\ORMInvalidArgumentException: Detached entity AppBundle\Entity\Service\Web\AddonDomain#0000000054814da900000000073c524a cannot be removed.
I tried a lot of things but none of them works (ie : using the doctrine manager to retrieve the entity instead of using the fixture.
Thanks a lot for your help,
best regards.
SOLVED !
In fact, I'm using the container and the entity manager of the test class and not of the client himself. That was the problem...

Although this is an old question, I had the same problem recently while working with Symfony 4.4 and Detached entity error in Doctrine helped me solve the problem. Basically, as #Can Vural suggested, calling merge before remove is solving the problem, but the missing bit is that you have to assign the entity again so:
$entity = $doctrineManager->merge($detachedEntity);
$doctrineManager->remove($entity);
$doctrineManager->flush();
This solved my problem.
Please keep in mind that merge is deprecated and will be removed in Doctrine 3: https://github.com/doctrine/orm/blob/master/UPGRADE.md#bc-break-removed-entitymanagermerge-and-entitymanagerdetach-methods

Ive came accross the same problem and merge did not solve it for me in functional test so instead I am querying the entities again and then remove them:
class ControllerTest extends WebTestCase {
public function testCanGet() {
$em = static::getContainer()->get(EntitiyManagerInterface::class);
$entity = new Entity();
$em->persist($entity);
$em->flush();
$client = static::createClient();
$client->request("GET", "/route");
$this->assertResponseIsSuccessful();
// Remove entity
$em->remove($em->getRepository(Entity::class)->find($entity->getId()));
$em->flush();
}
}

Related

Doctrine2 - Best way for retrieving two differents objects of a same entity

I'm working on a tool for concurrency with Doctrine 2.
I'm facing a "best practice" issue to retrieve a new instance of an entity without cache (the idea after that is to be able to compare some properties from 2 differents objects of the same entity and return the differences)
Some code might help (+ the doc: (http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html)):
// This is my current implementation.
$entity = $em->find(1);
$entity->setName('TEST');
// This entity as a "version" field equal to 2 in DB for example
try {
$em->lock($entity, LockMode::OPTIMISTIC, 1); // Will throw an OptimisticLockException
} catch(OptimisticLockException $e) {
$em->detach($entity);
$dbEntity = $this->find($entity->getId());
$em->detach($dbEntity);
$entity = $em->merge($entity);
var_dump($entity->getName()); // TEST
var_dump($dbEntity->getName()); // The old value
... do more stuff, like comparing the two objects ...
}
Is using the detach + merge methods a good practice for this behavior ? Any better idea to improve this code ?
--
Edit 1:
Actually, after adding some tests, the "merge" method is not what I expected: the object is not "re-attach" to the unit of work.
This behaviour is not what I want because the developer can't performs changes + flush on his entity after using my tool.
--
Edit 2:
After digging in the documentation and the source code, the "merge" method is actually what I wanted: a new instance of the entity is attached, not the one I provided ($entity in my example).
Since this code (In my tool) is in a method which purpose is to returns the $dbEntity object of my $entity object, passing the $entity reference (&$entity) solve my "Edit 1" issue.

For Symfony2 functional testing, what is the best practice for verifying database contents?

I see that there are a number of ways to load fixture data into a database. But after a functional test, what is the best/standard way to confirm what was written to the database was correct?
The phpunit package has a whole section for this, where you can load a dataset and then use things like assertTablesEqual() to compare a table's contents with the expected contents. But that doesn't seem to be usable under Symfony2, and I can't find any other standard method.
How do others solve this problem?
Symfony2 use doctrine ORM by default, or you can set other database gestion (MongoDB by exemple). Check the app\config\parameters.php file to set the database connection and the app\config\config.php to check/set the type of gestion. With a ORM, you do not need to check alot of stuff as the phpunit package, because it is already integrated into the protocole and much more. Check here for more details.
If you want to load datafixtures, you can export your actual database to save it, or either create a new one only for testing and switch databases in the app\config\parameters.php by create a new one like this app\config\parameters_dev.php. In this case, the website and your local version won't use the same database. You can also edit the app\config\parameters.php and prevent to upload it with the .gitgnore file.
Here is an example from a test set that includes database results. If you need to interact directly with the database in your test the entity manager can be made available to the test. For more information, see this bit of documentation. Note that results are more usually presented in a web page and read by the DOM crawler.
public function setUp()
{
self::bootKernel();
$this->em = static::$kernel->getContainer()
->get('doctrine')
->getManager()
;
$this->tool = static::$kernel->getContainer()
->get('truckee.toolbox')
;
$classes = array(
'Truckee\VolunteerBundle\DataFixtures\SampleData\LoadFocusSkillData',
'Truckee\VolunteerBundle\DataFixtures\SampleData\LoadMinimumData',
'Truckee\VolunteerBundle\DataFixtures\SampleData\LoadStaffUserGlenshire',
'Truckee\VolunteerBundle\DataFixtures\SampleData\LoadStaffUserMelanzane',
'Truckee\VolunteerBundle\DataFixtures\SampleData\LoadOpportunity',
'Truckee\VolunteerBundle\DataFixtures\SampleData\LoadVolunteer',
);
$this->loadFixtures($classes);
$this->client = $this->createClient();
$this->client->followRedirects();
}
public function testOutboxUser()
{
$crawler = $this->login('admin');
$link = $crawler->selectLink("Send alerts to organizations")->link();
$crawler = $this->client->click($link);
$outboxObj = $this->em->getRepository('TruckeeVolunteerBundle:AdminOutbox')->findAll();
$outbox = $outboxObj[0];
$recipient = $outbox->getRecipientId();
$type = $this->tool->getTypeFromId($recipient);
$this->assertEquals('staff', $type);
}

Symfony2 / Doctrine - create entity only in the test environment

I want to create some entities that would run only in test environment, during my unit tests. I dont think that there is some ebbded solution (am I wrong?), so probably another way is to create entities in the test folderand use them.
But there is something that I dont understand. Symfony sets the BundleName\Entity folders as folders where entities are, and Tests/Entity folder will not work with my entities. So, how do I explicitly set my Tests/Entity folder to work (read/install/register entities) in my test case? I assume this is made by configurating of doctrine entity manager?
Found this function in one of my projects, may be it can help you a bit. This function create new EntityManager, where you can define your entities namespace.
/**
* #return EntityManager
*/
public static function createTestEntityManager()
{
if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers())) {
self::markTestSkipped('This test requires SQLite support in your environment');
}
$config = new \Doctrine\ORM\Configuration();
$config->setEntityNamespaces(array('SerializerBundleTests' => 'Top10\SerializerBundle\Tests\Entity'));
$config->setAutoGenerateProxyClasses(true);
$config->setProxyDir(\sys_get_temp_dir());
$config->setProxyNamespace('SerializerBundleTests\Entity');
$config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader()));
$config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache());
$config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache());
$params = array(
'driver' => 'pdo_sqlite',
'memory' => true,
);
return EntityManager::create($params, $config);
}
I may be wrong, but I think that Doctrine's entity manager doesn't allow you to add mapping for additional entities on the fly.
As a "crutchy" workaround, you could create an additional bundle that contains entities you want to use with your tests, then leave your Symfony2 config for the test environment the same (if you have "auto_mapping" set to true; see this reference document), while changing you orm config for dev and prod environment. You'll need to disable auto mapping and specify the bundles explicitly, excluding the bundle with your test-only entities from "mappings".
Or maybe you don't even need to specify the value of "auto_mapping". The relevant part of your config ("entity_managers", "mappings") for the dev\prod environment should look somewhat similar to the one in the cookbook entry.
Again, I wish to emphasize that I'm not 100% sure that this answer is correct, as my knowledge of Symfony2 has become a bit fuzzy.

symfony admin generator form object

Hey guys, I've used the Symfony admin generator for a module.
Everything is working, but when the form for my model is instantiated, I need to pass in my own option.
I could do this myself by overriding the executeNew, executeCreate functions in myModuleActions.class.php (which extends myModuleAutoActions).
But I was hoping for a neater solution?
Perhaps overriding one of the configuration classes is the way to go. I basically need to add the current sf_user object ($this->getUser) as an "sf_user" option for the form, to avoid using sfContext in the myModuleForm.
Any ideas?
Welcome to Stack Overflow, jolly18.
I would just use sfContext. For example, in my app, I have a subform that creates a new Note object and assigns the user to it. In my form's configure() I have:
$new_note->setAuthor(sfContext::getInstance()->getUser()->getUsername());
I see the book calls this "The fastest but ugly way" because it makes "a big coupling between the form and the context, making the testing and reusability more difficult." But in practice... this works well and I can move on.
if module was generated using admin-generator :
in apps/backend/modules/books/actions/actions.class.php
modify: in
executeEdit(){
//leave rest unchanged
$values=array('activity_id'=>$activity_id, 'book_id'=>$book_id, 'todo_id'=>$todo_id, 'user_id'=>$this->getUser()->getGuardUser()->getId());
$this->form = new TabelBooksForm($TabelBooks, $values);
}
modify: in
executeNew(){
//leave rest unchanged
$values=array('activity_id'=>$activity_id, 'book_id'=>$book_id, 'todo_id'=>$todo_id, 'user_id'=>$this->getUser()->getGuardUser()->getId());
$this->form = new TabelBooksForm(array(), $values);
}
in TabelBooksForm.class.php
public function configure()
{
if ($this->isNew()) {
$this->setWidget('book_id', new sfWidgetFormInputHidden());
$this->setDefault('book_id', $this->getOption('book_id'));
$this->setWidget('activity_id', new sfWidgetFormInputHidden());
$this->setDefault('activity_id', $this->getOption('activity_id'));
$this->setWidget('todo_id', new sfWidgetFormInputHidden());
$this->setDefault('todo_id', $this->getOption('todo_id'));
}
}
i've been facing this problem for a while but symfony always surprises me with some neat code that i was not aware of.
I assume you'r using sfPropelPlugin, quite standar, if you checkout the code generated in cache (note: this code will be available once you tried to open the module from the browser, so firts try to look at it so we dont get in trouble :P) you may see something like:
cache/{application_name}(generally frontend or backend)/dev(enviromnemt)/autoModule_name( look here for the module)/:
lib
action
The action folder contains an action.class.php file that defines all actions generated by the generator (executeNew, Edit, Create, Update, etc). If you look a the implementation of executeNew and executeEdit, you can see that they ask a configuration instace the actual form to display, here is an example:
public function executeNew(sfWebRequest $request)
{
$this->form = $this->configuration->getForm();
$this->PaymentOrder = $this->form->getObject();
}
The configuration var containt an instance of a configuration class defined in the lib folder i mentioned earlier. That class tweaks the form to fit the object needs (generally by setting a fresh object instance).
So here comes the magic, the classes you see in your module extend from those in cache, so by pure logic, if you modifi the getForm() method in the main module/lib folder to fit your needs, you wont have to hack forms by getting user valuer where you shouldn't.
Hope this helps!

Disable Cakephp's Auto Model "feature"

In cake 1.2 there is a feature that allows the developer to no have to create models, but rather have cake do the detective work at run time and create the model for you. This process happens each time and is neat but in my case very hazardous. I read about this somewhere and now I'm experiencing the bad side of this.
I've created a plugin with all the files and everything appeared to be just great. That is until i tried to use some of the model's associations and functions. Then cake claims that this model i've created doesn't exist. I've narrowed it down to cake using this auto model feature instead of throwing and error! So i have no idea what's wrong!
Does anybody know how to disable this auto model feature? It's a good thought, but I can't seem to find where i've gone wrong with my plugin and an error would be very helpful!
There's always the possibility to actually create the model file and set var $useTable = false.
If this is not what you're asking for and the model and its associations actually do exist, but Cake seems to be unable to find them, you'll have to triple check the names of all models and their class names in both the actual model definition and in the association definitions.
AFAIK you can't disable the auto modelling.
Cake 1.2
It's a hack and it's ugly cus you need to edit core cake files but this is how i do it:
\cake\libs\class_registry.php : line 127ish
if (App::import($type, $plugin . $class)) {
${$class} =& new $class($options);
} elseif ($type === 'Model') {
/* Print out whatever debug info we have then exit */
pr($objects);
die("unable to find class $type, $plugin$class");
/* We don't want to base this on the app model */
${$class} =& new AppModel($options);
}
Cake 2
Costa recommends changing $strict to true in the init function on line 95 of Cake\Utility\ClassRegistry.php
See Cake Api Docs for init
ClassRegistry.php - init function
Use
var $useTable = false;
in your model definition.
Delete all cached files (all files under app/tmp, keep the folders)
In most cases where models seem to be acting in unexpected ways, often they dont include changes you've made, it is because that cake is useing an old cached version of the model.
Uh...where do we start. First, as Alexander suggested, clear your app cache.
If you still get the same behaviour, there is probably something wrong with the class and/or file names.
Remember the rules, for controller:
* classname: BlastsController
* filename: blasts_controller.php
for model:
* classname: Blast
* filename: blast.php
Don't foget to handle the irregular inflections properly.

Categories