Closure.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <?php
  2. namespace Gedmo\Tree\Strategy\ORM;
  3. use Gedmo\Tree\Strategy,
  4. Doctrine\ORM\EntityManager,
  5. Doctrine\ORM\Proxy\Proxy,
  6. Gedmo\Tree\TreeListener;
  7. /**
  8. * This strategy makes tree act like
  9. * a closure table.
  10. *
  11. * @author Gustavo Adrian <comfortablynumb84@gmail.com>
  12. * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  13. * @package Gedmo.Tree.Strategy.ORM
  14. * @subpackage Closure
  15. * @link http://www.gediminasm.org
  16. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  17. */
  18. class Closure implements Strategy
  19. {
  20. /**
  21. * TreeListener
  22. *
  23. * @var AbstractTreeListener
  24. */
  25. protected $listener = null;
  26. /**
  27. * List of pending Nodes, which needs to
  28. * be post processed because of having a parent Node
  29. * which requires some additional calculations
  30. *
  31. * @var array
  32. */
  33. protected $pendingChildNodeInserts = array();
  34. /**
  35. * List of pending Nodes to remove
  36. *
  37. * @var array
  38. */
  39. protected $pendingNodesForRemove = array();
  40. /**
  41. * {@inheritdoc}
  42. */
  43. public function __construct(TreeListener $listener)
  44. {
  45. $this->listener = $listener;
  46. }
  47. /**
  48. * {@inheritdoc}
  49. */
  50. public function getName()
  51. {
  52. return Strategy::CLOSURE;
  53. }
  54. /**
  55. * {@inheritdoc}
  56. */
  57. public function processPrePersist($em, $node)
  58. {
  59. }
  60. /**
  61. * {@inheritdoc}
  62. */
  63. public function processPreRemove($em, $node)
  64. {
  65. }
  66. /**
  67. * {@inheritdoc}
  68. */
  69. public function processScheduledInsertion($em, $entity)
  70. {
  71. $this->pendingChildNodeInserts[] = $entity;
  72. $meta = $em->getClassMetadata(get_class($entity));
  73. $config = $this->listener->getConfiguration($em, $meta->name);
  74. if (isset( $config['childCount'])) {
  75. // We set by default 0 on insertions for childCount field
  76. $meta->getReflectionProperty($config['childCount'])->setValue($entity, 0);
  77. }
  78. }
  79. /**
  80. * {@inheritdoc}
  81. */
  82. public function processPostPersist($em, $entity)
  83. {
  84. if (count($this->pendingChildNodeInserts)) {
  85. while ($e = array_shift($this->pendingChildNodeInserts)) {
  86. $this->insertNode($em, $e);
  87. }
  88. // If "childCount" property is in the schema, we recalculate child count of all entities
  89. $meta = $em->getClassMetadata(get_class($entity));
  90. $config = $this->listener->getConfiguration($em, $meta->name);
  91. if (isset($config['childCount'])) {
  92. $this->recalculateChildCountForEntities($em, get_class($entity));
  93. }
  94. }
  95. }
  96. /**
  97. * Insert node and closures
  98. *
  99. * @param EntityManager $em
  100. * @param object $entity
  101. * @param bool $addNodeChildrenToAncestors
  102. * @throws \Gedmo\Exception\RuntimeException - if closure insert fails
  103. */
  104. public function insertNode(EntityManager $em, $entity, $addNodeChildrenToAncestors = false)
  105. {
  106. $meta = $em->getClassMetadata(get_class($entity));
  107. $config = $this->listener->getConfiguration($em, $meta->name);
  108. $identifier = $meta->getSingleIdentifierFieldName();
  109. $id = $this->extractIdentifier($em, $entity);
  110. $closureMeta = $em->getClassMetadata($config['closure']);
  111. $entityTable = $meta->getTableName();
  112. $closureTable = $closureMeta->getTableName();
  113. $entries = array();
  114. $childrenIDs = array();
  115. $ancestorsIDs = array();
  116. // If node has children it means it already has a self referencing row, so we skip its insertion
  117. if ($addNodeChildrenToAncestors === false) {
  118. $entries[] = array(
  119. 'ancestor' => $id,
  120. 'descendant' => $id,
  121. 'depth' => 0
  122. );
  123. }
  124. $parent = $meta->getReflectionProperty($config['parent'])->getValue($entity);
  125. if ($parent) {
  126. $parentId = $meta->getReflectionProperty($identifier)->getValue($parent);
  127. $dql = "SELECT c.ancestor, c.depth FROM {$closureMeta->name} c";
  128. $dql .= " WHERE c.descendant = {$parentId}";
  129. $ancestors = $em->createQuery($dql)->getArrayResult();
  130. foreach ($ancestors as $ancestor) {
  131. $entries[] = array(
  132. 'ancestor' => $ancestor['ancestor'],
  133. 'descendant' => $id,
  134. 'depth' => $ancestor['depth'] + 1
  135. );
  136. $ancestorsIDs[] = $ancestor['ancestor'];
  137. if ($addNodeChildrenToAncestors === true) {
  138. $dql = "SELECT c.descendant, c.depth FROM {$closureMeta->name} c";
  139. $dql .= " WHERE c.ancestor = {$id} AND c.ancestor != c.descendant";
  140. $children = $em->createQuery($dql)
  141. ->getArrayResult();
  142. foreach ($children as $child) {
  143. $entries[] = array(
  144. 'ancestor' => $ancestor['ancestor'],
  145. 'descendant' => $child['descendant'],
  146. 'depth' => $child['depth'] + 1
  147. );
  148. $childrenIDs[] = $child['descendant'];
  149. }
  150. }
  151. }
  152. }
  153. foreach ($entries as $closure) {
  154. if (!$em->getConnection()->insert($closureTable, $closure)) {
  155. throw new \Gedmo\Exception\RuntimeException('Failed to insert new Closure record');
  156. }
  157. }
  158. }
  159. /**
  160. * {@inheritdoc}
  161. */
  162. public function processScheduledUpdate($em, $entity)
  163. {
  164. $entityClass = get_class($entity);
  165. $config = $this->listener->getConfiguration($em, $entityClass);
  166. $meta = $em->getClassMetadata($entityClass);
  167. $uow = $em->getUnitOfWork();
  168. $changeSet = $uow->getEntityChangeSet($entity);
  169. if (array_key_exists($config['parent'], $changeSet)) {
  170. $this->updateNode($em, $entity, $changeSet[$config['parent']]);
  171. }
  172. // If "childCount" property is in the schema, we recalculate child count of all entities
  173. if (isset($config['childCount'])) {
  174. $this->recalculateChildCountForEntities($em, get_class($entity));
  175. }
  176. }
  177. /**
  178. * Update node and closures
  179. *
  180. * @param EntityManager $em
  181. * @param object $entity
  182. * @param array $change - changeset of parent
  183. */
  184. public function updateNode(EntityManager $em, $entity, array $change)
  185. {
  186. $meta = $em->getClassMetadata(get_class($entity));
  187. $config = $this->listener->getConfiguration($em, $meta->name);
  188. $closureMeta = $em->getClassMetadata($config['closure']);
  189. $oldParent = $change[0];
  190. $nodeId = $this->extractIdentifier($em, $entity);
  191. $table = $closureMeta->getTableName();
  192. if ($oldParent) {
  193. $this->removeClosurePathsOfNodeID($em, $table, $nodeId);
  194. $this->insertNode($em, $entity, true);
  195. }
  196. }
  197. /**
  198. * {@inheritdoc}
  199. */
  200. public function processScheduledDelete($em, $entity)
  201. {
  202. $this->removeNode($em, $entity);
  203. // If "childCount" property is in the schema, we recalculate child count of all entities
  204. $meta = $em->getClassMetadata(get_class($entity));
  205. $config = $this->listener->getConfiguration($em, $meta->name);
  206. if (isset($config['childCount'])) {
  207. $this->recalculateChildCountForEntities($em, get_class( $entity ));
  208. }
  209. }
  210. /**
  211. * Remove node and associated closures
  212. *
  213. * @param EntityManager $em
  214. * @param object $entity
  215. * @param bool $maintainSelfReferencingRow
  216. * @param bool $maintainSelfReferencingRowOfChildren
  217. */
  218. public function removeNode(EntityManager $em, $entity, $maintainSelfReferencingRow = false, $maintainSelfReferencingRowOfChildren = false)
  219. {
  220. $meta = $em->getClassMetadata(get_class($entity));
  221. $config = $this->listener->getConfiguration($em, $meta->name);
  222. $closureMeta = $em->getClassMetadata($config['closure']);
  223. $id = $this->extractIdentifier($em, $entity);
  224. $this->removeClosurePathsOfNodeID($em, $closureMeta->getTableName(), $id, $maintainSelfReferencingRow, $maintainSelfReferencingRowOfChildren);
  225. }
  226. /**
  227. * Remove closures for node $nodeId
  228. *
  229. * @param EntityManager $em
  230. * @param string $table
  231. * @param integer $nodeId
  232. * @param bool $maintainSelfReferencingRow
  233. * @param bool $maintainSelfReferencingRowOfChildren
  234. * @throws \Gedmo\Exception\RuntimeException - if deletion of closures fails
  235. */
  236. public function removeClosurePathsOfNodeID(EntityManager $em, $table, $nodeId, $maintainSelfReferencingRow = true, $maintainSelfReferencingRowOfChildren = true)
  237. {
  238. $subquery = "SELECT c1.id FROM {$table} c1 ";
  239. $subquery .= "WHERE c1.descendant IN (SELECT c2.descendant FROM {$table} c2 WHERE c2.ancestor = :id) ";
  240. $subquery .= "AND (c1.ancestor IN (SELECT c3.ancestor FROM {$table} c3 WHERE c3.descendant = :id ";
  241. if ($maintainSelfReferencingRow === true) {
  242. $subquery .= "AND c3.descendant != c3.ancestor ";
  243. }
  244. if ( $maintainSelfReferencingRowOfChildren === false) {
  245. $subquery .= " OR c1.descendant = c1.ancestor ";
  246. }
  247. $subquery .= " )) ";
  248. $subquery = "DELETE FROM {$table} WHERE {$table}.id IN (SELECT temp_table.id FROM ({$subquery}) temp_table)";
  249. if (!$em->getConnection()->executeQuery($subquery, array('id' => $nodeId))) {
  250. throw new \Gedmo\Exception\RuntimeException('Failed to delete old Closure records');
  251. }
  252. }
  253. /**
  254. * Childcount recalculation
  255. *
  256. * @param EntityManager $em
  257. * @param string $entityClass
  258. * @throws \Gedmo\Exception\RuntimeException - if update fails
  259. */
  260. public function recalculateChildCountForEntities(EntityManager $em, $entityClass)
  261. {
  262. $meta = $em->getClassMetadata($entityClass);
  263. $config = $this->listener->getConfiguration($em, $meta->name);
  264. $entityIdentifierField = $meta->getIdentifierColumnNames();
  265. $entityIdentifierField = $entityIdentifierField[0];
  266. $childCountField = $config['childCount'];
  267. $closureMeta = $em->getClassMetadata($config['closure']);
  268. $entityTable = $meta->getTableName();
  269. $closureTable = $closureMeta->getTableName();
  270. $subquery = "(SELECT COUNT( c2.descendant ) FROM {$closureTable} c2 WHERE c2.ancestor = c1.{$entityIdentifierField} AND c2.ancestor != c2.descendant)";
  271. $sql = "UPDATE {$entityTable} c1 SET c1.{$childCountField} = {$subquery}";
  272. if (!$em->getConnection()->executeQuery($sql)) {
  273. throw new \Gedmo\Exception\RuntimeException('Failed to update child count field of entities');
  274. }
  275. }
  276. /**
  277. * Extracts identifiers from object or proxy
  278. *
  279. * @param EntityManager $em
  280. * @param object $entity
  281. * @param bool $single
  282. * @return mixed - array or single identifier
  283. */
  284. private function extractIdentifier(EntityManager $em, $entity, $single = true)
  285. {
  286. if ($entity instanceof Proxy) {
  287. $id = $em->getUnitOfWork()->getEntityIdentifier($entity);
  288. } else {
  289. $meta = $em->getClassMetadata(get_class($entity));
  290. $id = array();
  291. foreach ($meta->identifier as $name) {
  292. $id[$name] = $meta->getReflectionProperty($name)->getValue($entity);
  293. }
  294. }
  295. if ($single) {
  296. $id = current($id);
  297. }
  298. return $id;
  299. }
  300. /**
  301. * {@inheritdoc}
  302. */
  303. public function onFlushEnd($em)
  304. {
  305. }
  306. }