ORMPurger.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\Common\DataFixtures\Purger;
  4. use Doctrine\Common\DataFixtures\Sorter\TopologicalSorter;
  5. use Doctrine\DBAL\Platforms\AbstractPlatform;
  6. use Doctrine\DBAL\Schema\Identifier;
  7. use Doctrine\ORM\EntityManagerInterface;
  8. use Doctrine\ORM\Mapping\ClassMetadata;
  9. use function array_reverse;
  10. use function array_search;
  11. use function assert;
  12. use function count;
  13. use function is_callable;
  14. use function method_exists;
  15. use function preg_match;
  16. /**
  17. * Class responsible for purging databases of data before reloading data fixtures.
  18. */
  19. class ORMPurger implements PurgerInterface, ORMPurgerInterface
  20. {
  21. public const PURGE_MODE_DELETE = 1;
  22. public const PURGE_MODE_TRUNCATE = 2;
  23. /** @var EntityManagerInterface|null */
  24. private $em;
  25. /**
  26. * If the purge should be done through DELETE or TRUNCATE statements
  27. *
  28. * @var int
  29. */
  30. private $purgeMode = self::PURGE_MODE_DELETE;
  31. /**
  32. * Table/view names to be excluded from purge
  33. *
  34. * @var string[]
  35. */
  36. private $excluded;
  37. /**
  38. * Construct new purger instance.
  39. *
  40. * @param EntityManagerInterface $em EntityManagerInterface instance used for persistence.
  41. * @param string[] $excluded array of table/view names to be excluded from purge
  42. */
  43. public function __construct(?EntityManagerInterface $em = null, array $excluded = [])
  44. {
  45. $this->em = $em;
  46. $this->excluded = $excluded;
  47. }
  48. /**
  49. * Set the purge mode
  50. *
  51. * @param int $mode
  52. *
  53. * @return void
  54. */
  55. public function setPurgeMode($mode)
  56. {
  57. $this->purgeMode = $mode;
  58. }
  59. /**
  60. * Get the purge mode
  61. *
  62. * @return int
  63. */
  64. public function getPurgeMode()
  65. {
  66. return $this->purgeMode;
  67. }
  68. /** @inheritDoc */
  69. public function setEntityManager(EntityManagerInterface $em)
  70. {
  71. $this->em = $em;
  72. }
  73. /**
  74. * Retrieve the EntityManagerInterface instance this purger instance is using.
  75. *
  76. * @return EntityManagerInterface
  77. */
  78. public function getObjectManager()
  79. {
  80. return $this->em;
  81. }
  82. /** @inheritDoc */
  83. public function purge()
  84. {
  85. $classes = [];
  86. foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) {
  87. if ($metadata->isMappedSuperclass || (isset($metadata->isEmbeddedClass) && $metadata->isEmbeddedClass)) {
  88. continue;
  89. }
  90. $classes[] = $metadata;
  91. }
  92. $commitOrder = $this->getCommitOrder($this->em, $classes);
  93. // Get platform parameters
  94. $platform = $this->em->getConnection()->getDatabasePlatform();
  95. // Drop association tables first
  96. $orderedTables = $this->getAssociationTables($commitOrder, $platform);
  97. // Drop tables in reverse commit order
  98. for ($i = count($commitOrder) - 1; $i >= 0; --$i) {
  99. $class = $commitOrder[$i];
  100. if (
  101. (isset($class->isEmbeddedClass) && $class->isEmbeddedClass) ||
  102. $class->isMappedSuperclass ||
  103. ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName)
  104. ) {
  105. continue;
  106. }
  107. $orderedTables[] = $this->getTableName($class, $platform);
  108. }
  109. $connection = $this->em->getConnection();
  110. $filterExpr = $connection->getConfiguration()->getFilterSchemaAssetsExpression();
  111. $emptyFilterExpression = empty($filterExpr);
  112. $schemaAssetsFilter = method_exists($connection->getConfiguration(), 'getSchemaAssetsFilter') ? $connection->getConfiguration()->getSchemaAssetsFilter() : null;
  113. foreach ($orderedTables as $tbl) {
  114. // If we have a filter expression, check it and skip if necessary
  115. if (! $emptyFilterExpression && ! preg_match($filterExpr, $tbl)) {
  116. continue;
  117. }
  118. // If the table is excluded, skip it as well
  119. if (array_search($tbl, $this->excluded) !== false) {
  120. continue;
  121. }
  122. // Support schema asset filters as presented in
  123. if (is_callable($schemaAssetsFilter) && ! $schemaAssetsFilter($tbl)) {
  124. continue;
  125. }
  126. if ($this->purgeMode === self::PURGE_MODE_DELETE) {
  127. $connection->executeUpdate($this->getDeleteFromTableSQL($tbl, $platform));
  128. } else {
  129. $connection->executeUpdate($platform->getTruncateTableSQL($tbl, true));
  130. }
  131. }
  132. }
  133. /**
  134. * @param ClassMetadata[] $classes
  135. *
  136. * @return ClassMetadata[]
  137. */
  138. private function getCommitOrder(EntityManagerInterface $em, array $classes)
  139. {
  140. $sorter = new TopologicalSorter();
  141. foreach ($classes as $class) {
  142. if (! $sorter->hasNode($class->name)) {
  143. $sorter->addNode($class->name, $class);
  144. }
  145. // $class before its parents
  146. foreach ($class->parentClasses as $parentClass) {
  147. $parentClass = $em->getClassMetadata($parentClass);
  148. $parentClassName = $parentClass->getName();
  149. if (! $sorter->hasNode($parentClassName)) {
  150. $sorter->addNode($parentClassName, $parentClass);
  151. }
  152. $sorter->addDependency($class->name, $parentClassName);
  153. }
  154. foreach ($class->associationMappings as $assoc) {
  155. if (! $assoc['isOwningSide']) {
  156. continue;
  157. }
  158. $targetClass = $em->getClassMetadata($assoc['targetEntity']);
  159. assert($targetClass instanceof ClassMetadata);
  160. $targetClassName = $targetClass->getName();
  161. if (! $sorter->hasNode($targetClassName)) {
  162. $sorter->addNode($targetClassName, $targetClass);
  163. }
  164. // add dependency ($targetClass before $class)
  165. $sorter->addDependency($targetClassName, $class->name);
  166. // parents of $targetClass before $class, too
  167. foreach ($targetClass->parentClasses as $parentClass) {
  168. $parentClass = $em->getClassMetadata($parentClass);
  169. $parentClassName = $parentClass->getName();
  170. if (! $sorter->hasNode($parentClassName)) {
  171. $sorter->addNode($parentClassName, $parentClass);
  172. }
  173. $sorter->addDependency($parentClassName, $class->name);
  174. }
  175. }
  176. }
  177. return array_reverse($sorter->sort());
  178. }
  179. /**
  180. * @param array $classes
  181. *
  182. * @return array
  183. */
  184. private function getAssociationTables(array $classes, AbstractPlatform $platform)
  185. {
  186. $associationTables = [];
  187. foreach ($classes as $class) {
  188. foreach ($class->associationMappings as $assoc) {
  189. if (! $assoc['isOwningSide'] || $assoc['type'] !== ClassMetadata::MANY_TO_MANY) {
  190. continue;
  191. }
  192. $associationTables[] = $this->getJoinTableName($assoc, $class, $platform);
  193. }
  194. }
  195. return $associationTables;
  196. }
  197. private function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
  198. {
  199. if (isset($class->table['schema']) && ! method_exists($class, 'getSchemaName')) {
  200. return $class->table['schema'] . '.' . $this->em->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform);
  201. }
  202. return $this->em->getConfiguration()->getQuoteStrategy()->getTableName($class, $platform);
  203. }
  204. /**
  205. * @param mixed[] $assoc
  206. */
  207. private function getJoinTableName(
  208. array $assoc,
  209. ClassMetadata $class,
  210. AbstractPlatform $platform
  211. ): string {
  212. if (isset($assoc['joinTable']['schema']) && ! method_exists($class, 'getSchemaName')) {
  213. return $assoc['joinTable']['schema'] . '.' . $this->em->getConfiguration()->getQuoteStrategy()->getJoinTableName($assoc, $class, $platform);
  214. }
  215. return $this->em->getConfiguration()->getQuoteStrategy()->getJoinTableName($assoc, $class, $platform);
  216. }
  217. private function getDeleteFromTableSQL(string $tableName, AbstractPlatform $platform): string
  218. {
  219. $tableIdentifier = new Identifier($tableName);
  220. return 'DELETE FROM ' . $tableIdentifier->getQuotedName($platform);
  221. }
  222. }