From f064de2af1ef0995ae1fe08da168a5dd71dfcccc Mon Sep 17 00:00:00 2001 From: romanb Date: Fri, 3 Jul 2009 17:36:41 +0000 Subject: [PATCH] [2.0] Fixed issue with self-referential one-to-many associations not being persisted correctly when IDENTITY key generation was used. Included now passing OneToManySelfReferentialTest. --- .../Persisters/JoinedSubclassPersister.php | 10 +- .../Persisters/StandardEntityPersister.php | 59 ++++++---- lib/Doctrine/ORM/UnitOfWork.php | 109 +++++++++++------- .../Tests/Models/ECommerce/ECommerceCart.php | 4 +- .../Models/ECommerce/ECommerceCategory.php | 6 +- .../Models/ECommerce/ECommerceProduct.php | 6 +- .../Models/ECommerce/ECommerceShipping.php | 3 +- .../AbstractManyToManyAssociationTestCase.php | 6 +- .../Tests/ORM/Functional/AllTests.php | 1 + ...neToManySelfReferentialAssociationTest.php | 8 +- 10 files changed, 128 insertions(+), 84 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index f8bdaf08c..d108c1a24 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -187,8 +187,8 @@ class JoinedSubclassPersister extends StandardEntityPersister $this->_prepareData($entity, $updateData); $id = array_combine( - $this->_class->getIdentifierFieldNames(), - $this->_em->getUnitOfWork()->getEntityIdentifier($entity) + $this->_class->getIdentifierFieldNames(), + $this->_em->getUnitOfWork()->getEntityIdentifier($entity) ); foreach ($updateData as $tableName => $data) { @@ -205,15 +205,15 @@ class JoinedSubclassPersister extends StandardEntityPersister public function delete($entity) { $id = array_combine( - $this->_class->getIdentifierFieldNames(), - $this->_em->getUnitOfWork()->getEntityIdentifier($entity) + $this->_class->getIdentifierFieldNames(), + $this->_em->getUnitOfWork()->getEntityIdentifier($entity) ); // If the database platform supports FKs, just // delete the row from the root table. Cascades do the rest. if ($this->_conn->getDatabasePlatform()->supportsForeignKeyConstraints()) { $this->_conn->delete($this->_em->getClassMetadata($this->_class->rootEntityName) - ->primaryTable['name'], $id); + ->primaryTable['name'], $id); } else { // Delete the parent tables, starting from this class' table up to the root table $this->_conn->delete($this->_class->primaryTable['name'], $id); diff --git a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php index b59b1bb50..8d77ce20a 100644 --- a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php @@ -179,16 +179,18 @@ class StandardEntityPersister { $updateData = array(); $this->_prepareData($entity, $updateData); - $id = array_combine($this->_class->getIdentifierFieldNames(), - $this->_em->getUnitOfWork()->getEntityIdentifier($entity)); + $id = array_combine( + $this->_class->getIdentifierFieldNames(), + $this->_em->getUnitOfWork()->getEntityIdentifier($entity) + ); $tableName = $this->_class->primaryTable['name']; - + if ($this->_evm->hasListeners(Events::preUpdate)) { $this->_preUpdate($entity); } - + $this->_conn->update($tableName, $updateData[$tableName], $id); - + if ($this->_evm->hasListeners(Events::postUpdate)) { $this->_postUpdate($entity); } @@ -239,9 +241,10 @@ class StandardEntityPersister } /** - * Prepares the data changeset of an entity for database insertion. - * The array that is passed as the second parameter is filled with - * => pairs, grouped by table name, during this preparation. + * Prepares the data changeset of an entity for database insertion (INSERT/UPDATE). + * + * During this preparation the array that is passed as the second parameter is filled with + * => pairs, grouped by table name. * * Example: * @@ -256,7 +259,7 @@ class StandardEntityPersister * * @param object $entity * @param array $result The reference to the data array. - * @param boolean $isInsert + * @param boolean $isInsert Whether the preparation is for an INSERT (or UPDATE, if FALSE). */ protected function _prepareData($entity, array &$result, $isInsert = false) { @@ -276,37 +279,43 @@ class StandardEntityPersister continue; } - // Special case: One-one self-referencing of the same class. - if ($newVal !== null && $assocMapping->sourceEntityName == $assocMapping->targetEntityName) { + // Special case: One-one self-referencing of the same class with IDENTITY type key generation. + if ($this->_class->isIdGeneratorIdentity() && $newVal !== null && + $assocMapping->sourceEntityName == $assocMapping->targetEntityName) { $oid = spl_object_hash($newVal); $isScheduledForInsert = $uow->isRegisteredNew($newVal); if (isset($this->_queuedInserts[$oid]) || $isScheduledForInsert) { // The associated entity $newVal is not yet persisted, so we must - // set $newVal = null, in order to insert a null value and update later. + // set $newVal = null, in order to insert a null value and schedule an + // extra update on the UnitOfWork. + $uow->scheduleExtraUpdate($entity, array( + $field => array(null, $newVal) + )); $newVal = null; } else if ($isInsert && ! $isScheduledForInsert && $uow->getEntityState($newVal) == UnitOfWork::STATE_MANAGED) { - // $newVal is already fully persisted - // Clear changeset of $newVal, so that only the identifier is updated. - // Not sure this is really rock-solid here but it seems to work. - $uow->clearEntityChangeSet($oid); - $uow->propertyChanged($newVal, $field, $entity, $entity); + // $newVal is already fully persisted. + // Schedule an extra update for it, so that the foreign key(s) are properly set. + $uow->scheduleExtraUpdate($newVal, array( + $field => array(null, $entity) + )); } } - + foreach ($assocMapping->sourceToTargetKeyColumns as $sourceColumn => $targetColumn) { - $otherClass = $this->_em->getClassMetadata($assocMapping->targetEntityName); if ($newVal === null) { $result[$this->getOwningTable($field)][$sourceColumn] = null; } else { + $otherClass = $this->_em->getClassMetadata($assocMapping->targetEntityName); $result[$this->getOwningTable($field)][$sourceColumn] = - $otherClass->reflFields[$otherClass->fieldNames[$targetColumn]]->getValue($newVal); + $otherClass->reflFields[$otherClass->fieldNames[$targetColumn]] + ->getValue($newVal); } } } else if ($newVal === null) { $result[$this->getOwningTable($field)][$columnName] = null; } else { $result[$this->getOwningTable($field)][$columnName] = Type::getType( - $this->_class->fieldMappings[$field]['type'])->convertToDatabaseValue($newVal, $platform); + $this->_class->fieldMappings[$field]['type'])->convertToDatabaseValue($newVal, $platform); } } } @@ -404,7 +413,7 @@ class StandardEntityPersister return 'SELECT ' . $columnList . ' FROM ' . $this->_class->getTableName() . ' WHERE ' . $conditionSql; } - + /** * Dispatches the preInsert event for the given entity. * @@ -417,7 +426,7 @@ class StandardEntityPersister ); $this->_evm->dispatchEvent(Events::preInsert, $eventArgs); } - + /** * Dispatches the postInsert event for the given entity. * @@ -427,7 +436,7 @@ class StandardEntityPersister { $this->_evm->dispatchEvent(Events::postInsert); } - + /** * Dispatches the preUpdate event for the given entity. * @@ -440,7 +449,7 @@ class StandardEntityPersister ); $this->_evm->dispatchEvent(Events::preUpdate, $eventArgs); } - + /** * Dispatches the postUpdate event for the given entity. * diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 2aa95e6a8..6d8a73dac 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -136,27 +136,29 @@ class UnitOfWork implements PropertyChangedListener */ private $_entityUpdates = array(); + private $_extraUpdates = array(); + /** * A list of all pending entity deletions. * * @var array */ private $_entityDeletions = array(); - + /** * All pending collection deletions. * * @var array */ private $_collectionDeletions = array(); - + /** * All pending collection creations. * * @var array */ private $_collectionCreations = array(); - + /** * All collection updates. * @@ -245,6 +247,10 @@ class UnitOfWork implements PropertyChangedListener foreach ($commitOrder as $class) { $this->_executeUpdates($class); } + // Extra updates that were requested by persisters. + if ($this->_extraUpdates) { + $this->_executeExtraUpdates(); + } // Collection deletions (deletions of complete collections) foreach ($this->_collectionDeletions as $collectionToDelete) { @@ -278,12 +284,22 @@ class UnitOfWork implements PropertyChangedListener $this->_entityInsertions = array(); $this->_entityUpdates = array(); $this->_entityDeletions = array(); + $this->_extraUpdates = array(); $this->_entityChangeSets = array(); $this->_collectionUpdates = array(); $this->_collectionDeletions = array(); $this->_visitedCollections = array(); } + protected function _executeExtraUpdates() + { + foreach ($this->_extraUpdates as $oid => $update) { + list ($entity, $changeset) = $update; + $this->_entityChangeSets[$oid] = $changeset; + $this->getEntityPersister(get_class($entity))->update($entity); + } + } + /** * Gets the changeset for an entity. * @@ -388,21 +404,19 @@ class UnitOfWork implements PropertyChangedListener private function _computeEntityChanges($class, $entity) { $oid = spl_object_hash($entity); - + if ( ! $class->isInheritanceTypeNone()) { $class = $this->_em->getClassMetadata(get_class($entity)); } - + $actualData = array(); foreach ($class->reflFields as $name => $refProp) { if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) { $actualData[$name] = $refProp->getValue($entity); } - if ($class->isCollectionValuedAssociation($name) - && $actualData[$name] !== null - && ! ($actualData[$name] instanceof PersistentCollection) - ) { + if ($class->isCollectionValuedAssociation($name) && $actualData[$name] !== null + && ! ($actualData[$name] instanceof PersistentCollection)) { // If $actualData[$name] is Collection then unwrap the array if ($actualData[$name] instanceof Collection) { $actualData[$name] = $actualData[$name]->unwrap(); @@ -410,7 +424,7 @@ class UnitOfWork implements PropertyChangedListener $assoc = $class->associationMappings[$name]; // Inject PersistentCollection $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc->targetEntityName), - $actualData[$name] ? $actualData[$name] : array()); + $actualData[$name] ? $actualData[$name] : array()); $coll->setOwner($entity, $assoc); $coll->setDirty( ! $coll->isEmpty()); $class->reflFields[$name]->setValue($entity, $coll); @@ -527,7 +541,7 @@ class UnitOfWork implements PropertyChangedListener $this->_originalEntityData[$oid] = $data; } else if ($state == self::STATE_DELETED) { throw DoctrineException::updateMe("Deleted entity in collection detected during flush." - . " Make sure you properly remove deleted entities from collections."); + . " Make sure you properly remove deleted entities from collections."); } // MANAGED associated entities are already taken into account // during changeset calculation anyway, since they are in the identity map. @@ -609,12 +623,13 @@ class UnitOfWork implements PropertyChangedListener $entityChangeSet = array_merge( $this->_entityInsertions, $this->_entityUpdates, - $this->_entityDeletions); + $this->_entityDeletions + ); } // TODO: We can cache computed commit orders in the metadata cache! // Check cache at this point here! - + // See if there are any new classes in the changeset, that are not in the // commit order graph yet (dont have a node). $newNodes = array(); @@ -622,9 +637,9 @@ class UnitOfWork implements PropertyChangedListener $className = get_class($entity); if ( ! $this->_commitOrderCalculator->hasNodeWithKey($className)) { $this->_commitOrderCalculator->addNodeWithItem( - $className, // index/key - $this->_em->getClassMetadata($className) // item - ); + $className, // index/key + $this->_em->getClassMetadata($className) // item + ); $newNodes[] = $this->_commitOrderCalculator->getNodeForKey($className); } } @@ -640,9 +655,9 @@ class UnitOfWork implements PropertyChangedListener // If the target class does not yet have a node, create it if ( ! $this->_commitOrderCalculator->hasNodeWithKey($targetClassName)) { $this->_commitOrderCalculator->addNodeWithItem( - $targetClassName, // index/key - $targetClass // item - ); + $targetClassName, // index/key + $targetClass // item + ); } // add dependency $otherNode = $this->_commitOrderCalculator->getNodeForKey($targetClassName); @@ -657,7 +672,7 @@ class UnitOfWork implements PropertyChangedListener /** * Registers a new entity. The entity will be scheduled for insertion. * If the entity already has an identifier, it will be added to the identity map. - * + * * @param object $entity * @todo Rename to scheduleForInsert(). */ @@ -715,6 +730,11 @@ class UnitOfWork implements PropertyChangedListener } } + public function scheduleExtraUpdate($entity, array $changeset) + { + $this->_extraUpdates[spl_object_hash($entity)] = array($entity, $changeset); + } + /** * Checks whether an entity is registered as dirty in the unit of work. * Note: Is not very useful currently as dirty entities are only registered @@ -731,7 +751,7 @@ class UnitOfWork implements PropertyChangedListener /** * Registers a deleted entity. - * + * * @todo Rename to scheduleForDelete(). */ public function registerDeleted($entity) @@ -781,8 +801,8 @@ class UnitOfWork implements PropertyChangedListener $oid = spl_object_hash($entity); $this->removeFromIdentityMap($entity); unset($this->_entityInsertions[$oid], $this->_entityUpdates[$oid], - $this->_entityDeletions[$oid], $this->_entityIdentifiers[$oid], - $this->_entityStates[$oid]); + $this->_entityDeletions[$oid], $this->_entityIdentifiers[$oid], + $this->_entityStates[$oid]); } public function isEntityRegistered($entity) @@ -832,12 +852,12 @@ class UnitOfWork implements PropertyChangedListener $oid = spl_object_hash($entity); if ( ! isset($this->_entityStates[$oid])) { /*if (isset($this->_entityInsertions[$oid])) { - $this->_entityStates[$oid] = self::STATE_NEW; - } else if ( ! isset($this->_entityIdentifiers[$oid])) { - // Either NEW (if no ID) or DETACHED (if ID) - } else { - $this->_entityStates[$oid] = self::STATE_DETACHED; - }*/ + $this->_entityStates[$oid] = self::STATE_NEW; + } else if ( ! isset($this->_entityIdentifiers[$oid])) { + // Either NEW (if no ID) or DETACHED (if ID) + } else { + $this->_entityStates[$oid] = self::STATE_DETACHED; + }*/ if (isset($this->_entityIdentifiers[$oid]) && ! isset($this->_entityInsertions[$oid])) { $this->_entityStates[$oid] = self::STATE_DETACHED; } else { @@ -918,10 +938,8 @@ class UnitOfWork implements PropertyChangedListener if ($idHash === '') { return false; } - - return isset($this->_identityMap - [$classMetadata->rootEntityName] - [$idHash]); + + return isset($this->_identityMap[$classMetadata->rootEntityName][$idHash]); } /** @@ -954,6 +972,11 @@ class UnitOfWork implements PropertyChangedListener foreach ($commitOrder as $class) { $this->_executeInserts($class); } + // Extra updates that were requested by persisters. + if ($this->_extraUpdates) { + $this->_executeExtraUpdates(); + $this->_extraUpdates = array(); + } // remove them from _entityInsertions and _entityChangeSets $this->_entityInsertions = array_diff_key($this->_entityInsertions, $insertNow); $this->_entityChangeSets = array_diff_key($this->_entityChangeSets, $insertNow); @@ -1035,7 +1058,7 @@ class UnitOfWork implements PropertyChangedListener /** * Deletes an entity as part of the current unit of work. - * + * * This method is internally called during delete() cascades as it tracks * the already visited entities to prevent infinite recursions. * @@ -1092,7 +1115,7 @@ class UnitOfWork implements PropertyChangedListener $id = $class->getIdentifierValues($entity); if ( ! $id) { - throw new InvalidArgumentException('New entity passed to merge().'); + throw new \InvalidArgumentException('New entity passed to merge().'); } $managedCopy = $this->tryGetById($id, $class->rootEntityName); @@ -1134,7 +1157,7 @@ class UnitOfWork implements PropertyChangedListener /** * Cascades a merge operation to associated entities. - * + * * @param object $entity * @param object $managedCopy * @param array $visited @@ -1237,34 +1260,34 @@ class UnitOfWork implements PropertyChangedListener $this->_collectionUpdates = array(); $this->_commitOrderCalculator->clear(); } - + public function scheduleCollectionUpdate(PersistentCollection $coll) { $this->_collectionUpdates[] = $coll; } - + public function isCollectionScheduledForUpdate(PersistentCollection $coll) { //... } - + public function scheduleCollectionDeletion(PersistentCollection $coll) { //TODO: if $coll is already scheduled for recreation ... what to do? // Just remove $coll from the scheduled recreations? $this->_collectionDeletions[] = $coll; } - + public function isCollectionScheduledForDeletion(PersistentCollection $coll) { //... } - + public function scheduleCollectionRecreation(PersistentCollection $coll) { $this->_collectionRecreations[] = $coll; } - + public function isCollectionScheduledForRecreation(PersistentCollection $coll) { //... @@ -1498,7 +1521,7 @@ class UnitOfWork implements PropertyChangedListener $this->_originalEntityData[$oid] = $data; $this->addToIdentityMap($entity); } - + /** * INTERNAL: * Clears the property changeset of the entity with the given OID. diff --git a/tests/Doctrine/Tests/Models/ECommerce/ECommerceCart.php b/tests/Doctrine/Tests/Models/ECommerce/ECommerceCart.php index 997a94fe6..f1ea7f0bc 100644 --- a/tests/Doctrine/Tests/Models/ECommerce/ECommerceCart.php +++ b/tests/Doctrine/Tests/Models/ECommerce/ECommerceCart.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\Models\ECommerce; +use Doctrine\Common\Collections\Collection; + /** * ECommerceCart * Represents a typical cart of a shopping application. @@ -40,7 +42,7 @@ class ECommerceCart public function __construct() { - $this->products = new \Doctrine\Common\Collections\Collection; + $this->products = new Collection; } public function getId() { diff --git a/tests/Doctrine/Tests/Models/ECommerce/ECommerceCategory.php b/tests/Doctrine/Tests/Models/ECommerce/ECommerceCategory.php index 920cacec2..70ae1ffd3 100644 --- a/tests/Doctrine/Tests/Models/ECommerce/ECommerceCategory.php +++ b/tests/Doctrine/Tests/Models/ECommerce/ECommerceCategory.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\Models\ECommerce; +use Doctrine\Common\Collections\Collection; + /** * ECommerceCategory * Represents a tag applied on particular products. @@ -42,8 +44,8 @@ class ECommerceCategory public function __construct() { - $this->products = new \Doctrine\Common\Collections\Collection(); - $this->children = new \Doctrine\Common\Collections\Collection(); + $this->products = new Collection(); + $this->children = new Collection(); } public function getId() diff --git a/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php b/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php index 3bc4b0b20..0094d7969 100644 --- a/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php +++ b/tests/Doctrine/Tests/Models/ECommerce/ECommerceProduct.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\Models\ECommerce; +use Doctrine\Common\Collections\Collection; + /** * ECommerceProduct * Represents a type of product of a shopping application. @@ -45,8 +47,8 @@ class ECommerceProduct public function __construct() { - $this->features = new \Doctrine\Common\Collections\Collection; - $this->categories = new \Doctrine\Common\Collections\Collection; + $this->features = new Collection; + $this->categories = new Collection; } public function getId() diff --git a/tests/Doctrine/Tests/Models/ECommerce/ECommerceShipping.php b/tests/Doctrine/Tests/Models/ECommerce/ECommerceShipping.php index fb64ca638..b1ee90a53 100644 --- a/tests/Doctrine/Tests/Models/ECommerce/ECommerceShipping.php +++ b/tests/Doctrine/Tests/Models/ECommerce/ECommerceShipping.php @@ -13,8 +13,7 @@ namespace Doctrine\Tests\Models\ECommerce; class ECommerceShipping { /** - * @Column(type="integer") - * @Id + * @Id @Column(type="integer") * @GeneratedValue(strategy="AUTO") */ private $id; diff --git a/tests/Doctrine/Tests/ORM/Functional/AbstractManyToManyAssociationTestCase.php b/tests/Doctrine/Tests/ORM/Functional/AbstractManyToManyAssociationTestCase.php index f0e240a62..bbfa6b9a3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AbstractManyToManyAssociationTestCase.php +++ b/tests/Doctrine/Tests/ORM/Functional/AbstractManyToManyAssociationTestCase.php @@ -38,7 +38,9 @@ class AbstractManyToManyAssociationTestCase extends \Doctrine\Tests\OrmFunctiona public function assertCollectionEquals(Collection $first, Collection $second) { - if (count($first) != count($second)) { + return $first->forAll(function($k, $e) use($second) { return $second->contains($e); }); + + /*if (count($first) != count($second)) { return false; } foreach ($first as $element) { @@ -46,6 +48,6 @@ class AbstractManyToManyAssociationTestCase extends \Doctrine\Tests\OrmFunctiona return false; } } - return true; + return true;*/ } } diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 7178fad2f..3a0a3e208 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -31,6 +31,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManyBidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ManyToManyUnidirectionalAssociationTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ManyToManyBidirectionalAssociationTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\OneToManySelfReferentialAssociationTest'); return $suite; } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php index 0b7b6c98b..6d7199028 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToManySelfReferentialAssociationTest.php @@ -32,6 +32,8 @@ class OneToManySelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunctio $this->parent->addChild($this->secondChild); $this->_em->save($this->parent); + $this->_em->flush(); + $this->assertForeignKeyIs($this->parent->getId(), $this->firstChild); $this->assertForeignKeyIs($this->parent->getId(), $this->secondChild); } @@ -39,6 +41,7 @@ class OneToManySelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunctio public function testSavesAnEmptyCollection() { $this->_em->save($this->parent); + $this->_em->flush(); $this->assertEquals(0, count($this->parent->getChildren())); } @@ -46,6 +49,7 @@ class OneToManySelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunctio public function testDoesNotSaveAnInverseSideSet() { $this->parent->brokenAddChild($this->firstChild); $this->_em->save($this->parent); + $this->_em->flush(); $this->assertForeignKeyIs(null, $this->firstChild); } @@ -80,10 +84,10 @@ class OneToManySelfReferentialAssociationTest extends \Doctrine\Tests\OrmFunctio $this->assertTrue($children[0] instanceof ECommerceCategory); $this->assertSame($parent, $children[0]->getParent()); - $this->assertTrue(strstr($children[0]->getName(), ' books')); + $this->assertEquals(' books', strstr($children[0]->getName(), ' books')); $this->assertTrue($children[1] instanceof ECommerceCategory); $this->assertSame($parent, $children[1]->getParent()); - $this->assertTrue(strstr($children[1]->getName(), ' books')); + $this->assertEquals(' books', strstr($children[1]->getName(), ' books')); } /* TODO: not yet implemented