ManyToManyPersister.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. <?php
  2. /*
  3. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14. *
  15. * This software consists of voluntary contributions made by many individuals
  16. * and is licensed under the MIT license. For more information, see
  17. * <http://www.doctrine-project.org>.
  18. */
  19. namespace Doctrine\ORM\Persisters;
  20. use Doctrine\ORM\Mapping\ClassMetadata;
  21. use Doctrine\ORM\PersistentCollection;
  22. use Doctrine\ORM\UnitOfWork;
  23. /**
  24. * Persister for many-to-many collections.
  25. *
  26. * @author Roman Borschel <roman@code-factory.org>
  27. * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
  28. * @author Alexander <iam.asm89@gmail.com>
  29. * @since 2.0
  30. */
  31. class ManyToManyPersister extends AbstractCollectionPersister
  32. {
  33. /**
  34. * {@inheritdoc}
  35. *
  36. * @override
  37. */
  38. protected function getDeleteRowSQL(PersistentCollection $coll)
  39. {
  40. $columns = array();
  41. $mapping = $coll->getMapping();
  42. $class = $this->em->getClassMetadata(get_class($coll->getOwner()));
  43. $tableName = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
  44. foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
  45. $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  46. }
  47. foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
  48. $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  49. }
  50. return 'DELETE FROM ' . $tableName
  51. . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
  52. }
  53. /**
  54. * {@inheritdoc}
  55. *
  56. * @override
  57. *
  58. * @internal Order of the parameters must be the same as the order of the columns in getDeleteRowSql.
  59. */
  60. protected function getDeleteRowSQLParameters(PersistentCollection $coll, $element)
  61. {
  62. return $this->collectJoinTableColumnParameters($coll, $element);
  63. }
  64. /**
  65. * {@inheritdoc}
  66. *
  67. * @throws \BadMethodCallException Not used for OneToManyPersister
  68. */
  69. protected function getUpdateRowSQL(PersistentCollection $coll)
  70. {
  71. throw new \BadMethodCallException("Insert Row SQL is not used for ManyToManyPersister");
  72. }
  73. /**
  74. * {@inheritdoc}
  75. *
  76. * @override
  77. *
  78. * @internal Order of the parameters must be the same as the order of the columns in getInsertRowSql.
  79. */
  80. protected function getInsertRowSQL(PersistentCollection $coll)
  81. {
  82. $columns = array();
  83. $mapping = $coll->getMapping();
  84. $class = $this->em->getClassMetadata(get_class($coll->getOwner()));
  85. $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
  86. foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
  87. $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  88. }
  89. foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
  90. $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  91. }
  92. return 'INSERT INTO ' . $joinTable . ' (' . implode(', ', $columns) . ')'
  93. . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
  94. }
  95. /**
  96. * {@inheritdoc}
  97. *
  98. * @override
  99. *
  100. * @internal Order of the parameters must be the same as the order of the columns in getInsertRowSql.
  101. */
  102. protected function getInsertRowSQLParameters(PersistentCollection $coll, $element)
  103. {
  104. return $this->collectJoinTableColumnParameters($coll, $element);
  105. }
  106. /**
  107. * Collects the parameters for inserting/deleting on the join table in the order
  108. * of the join table columns as specified in ManyToManyMapping#joinTableColumns.
  109. *
  110. * @param \Doctrine\ORM\PersistentCollection $coll
  111. * @param object $element
  112. *
  113. * @return array
  114. */
  115. private function collectJoinTableColumnParameters(PersistentCollection $coll, $element)
  116. {
  117. $params = array();
  118. $mapping = $coll->getMapping();
  119. $isComposite = count($mapping['joinTableColumns']) > 2;
  120. $identifier1 = $this->uow->getEntityIdentifier($coll->getOwner());
  121. $identifier2 = $this->uow->getEntityIdentifier($element);
  122. if ($isComposite) {
  123. $class1 = $this->em->getClassMetadata(get_class($coll->getOwner()));
  124. $class2 = $coll->getTypeClass();
  125. }
  126. foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
  127. $isRelationToSource = isset($mapping['relationToSourceKeyColumns'][$joinTableColumn]);
  128. if ( ! $isComposite) {
  129. $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2);
  130. continue;
  131. }
  132. if ($isRelationToSource) {
  133. $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
  134. continue;
  135. }
  136. $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
  137. }
  138. return $params;
  139. }
  140. /**
  141. * {@inheritdoc}
  142. *
  143. * @override
  144. */
  145. protected function getDeleteSQL(PersistentCollection $coll)
  146. {
  147. $columns = array();
  148. $mapping = $coll->getMapping();
  149. $class = $this->em->getClassMetadata(get_class($coll->getOwner()));
  150. $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
  151. foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
  152. $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  153. }
  154. return 'DELETE FROM ' . $joinTable
  155. . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
  156. }
  157. /**
  158. * {@inheritdoc}
  159. *
  160. * @override
  161. *
  162. * @internal Order of the parameters must be the same as the order of the columns in getDeleteSql.
  163. */
  164. protected function getDeleteSQLParameters(PersistentCollection $coll)
  165. {
  166. $mapping = $coll->getMapping();
  167. $identifier = $this->uow->getEntityIdentifier($coll->getOwner());
  168. // Optimization for single column identifier
  169. if (count($mapping['relationToSourceKeyColumns']) === 1) {
  170. return array(reset($identifier));
  171. }
  172. // Composite identifier
  173. $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
  174. $params = array();
  175. foreach ($mapping['relationToSourceKeyColumns'] as $columnName => $refColumnName) {
  176. $params[] = isset($sourceClass->fieldNames[$refColumnName])
  177. ? $identifier[$sourceClass->fieldNames[$refColumnName]]
  178. : $identifier[$sourceClass->getFieldForColumn($columnName)];
  179. }
  180. return $params;
  181. }
  182. /**
  183. * {@inheritdoc}
  184. */
  185. public function count(PersistentCollection $coll)
  186. {
  187. $conditions = array();
  188. $params = array();
  189. $mapping = $coll->getMapping();
  190. $association = $mapping;
  191. $class = $this->em->getClassMetadata($mapping['sourceEntity']);
  192. $id = $this->em->getUnitOfWork()->getEntityIdentifier($coll->getOwner());
  193. if ( ! $mapping['isOwningSide']) {
  194. $targetEntity = $this->em->getClassMetadata($mapping['targetEntity']);
  195. $association = $targetEntity->associationMappings[$mapping['mappedBy']];
  196. }
  197. $joinColumns = ( ! $mapping['isOwningSide'])
  198. ? $association['joinTable']['inverseJoinColumns']
  199. : $association['joinTable']['joinColumns'];
  200. foreach ($joinColumns as $joinColumn) {
  201. $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
  202. $referencedName = $joinColumn['referencedColumnName'];
  203. $conditions[] = 't.' . $columnName . ' = ?';
  204. $params[] = ($class->containsForeignIdentifier)
  205. ? $id[$class->getFieldForColumn($referencedName)]
  206. : $id[$class->fieldNames[$referencedName]];
  207. }
  208. $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
  209. list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($mapping);
  210. if ($filterSql) {
  211. $conditions[] = $filterSql;
  212. }
  213. $sql = 'SELECT COUNT(*)'
  214. . ' FROM ' . $joinTableName . ' t'
  215. . $joinTargetEntitySQL
  216. . ' WHERE ' . implode(' AND ', $conditions);
  217. return $this->conn->fetchColumn($sql, $params);
  218. }
  219. /**
  220. * @param \Doctrine\ORM\PersistentCollection $coll
  221. * @param int $offset
  222. * @param int|null $length
  223. *
  224. * @return array
  225. */
  226. public function slice(PersistentCollection $coll, $offset, $length = null)
  227. {
  228. $mapping = $coll->getMapping();
  229. return $this->em->getUnitOfWork()->getEntityPersister($mapping['targetEntity'])->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length);
  230. }
  231. /**
  232. * @param \Doctrine\ORM\PersistentCollection $coll
  233. * @param object $element
  234. *
  235. * @return boolean
  236. */
  237. public function contains(PersistentCollection $coll, $element)
  238. {
  239. $uow = $this->em->getUnitOfWork();
  240. // Shortcut for new entities
  241. $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW);
  242. if ($entityState === UnitOfWork::STATE_NEW) {
  243. return false;
  244. }
  245. // Entity is scheduled for inclusion
  246. if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) {
  247. return false;
  248. }
  249. list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, true);
  250. $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
  251. return (bool) $this->conn->fetchColumn($sql, $params);
  252. }
  253. /**
  254. * @param \Doctrine\ORM\PersistentCollection $coll
  255. * @param object $element
  256. *
  257. * @return boolean
  258. */
  259. public function removeElement(PersistentCollection $coll, $element)
  260. {
  261. $uow = $this->em->getUnitOfWork();
  262. // shortcut for new entities
  263. $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW);
  264. if ($entityState === UnitOfWork::STATE_NEW) {
  265. return false;
  266. }
  267. // If Entity is scheduled for inclusion, it is not in this collection.
  268. // We can assure that because it would have return true before on array check
  269. if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) {
  270. return false;
  271. }
  272. list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, false);
  273. $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
  274. return (bool) $this->conn->executeUpdate($sql, $params);
  275. }
  276. /**
  277. * @param \Doctrine\ORM\PersistentCollection $coll
  278. * @param object $element
  279. * @param boolean $addFilters Whether the filter SQL should be included or not.
  280. *
  281. * @return array
  282. */
  283. private function getJoinTableRestrictions(PersistentCollection $coll, $element, $addFilters)
  284. {
  285. $uow = $this->em->getUnitOfWork();
  286. $filterMapping = $coll->getMapping();
  287. $mapping = $filterMapping;
  288. if ( ! $mapping['isOwningSide']) {
  289. $sourceClass = $this->em->getClassMetadata($mapping['targetEntity']);
  290. $targetClass = $this->em->getClassMetadata($mapping['sourceEntity']);
  291. $sourceId = $uow->getEntityIdentifier($element);
  292. $targetId = $uow->getEntityIdentifier($coll->getOwner());
  293. $mapping = $sourceClass->associationMappings[$mapping['mappedBy']];
  294. } else {
  295. $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
  296. $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
  297. $sourceId = $uow->getEntityIdentifier($coll->getOwner());
  298. $targetId = $uow->getEntityIdentifier($element);
  299. }
  300. $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform);
  301. $whereClauses = array();
  302. $params = array();
  303. foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
  304. $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?';
  305. if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) {
  306. $params[] = ($targetClass->containsForeignIdentifier)
  307. ? $targetId[$targetClass->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])]
  308. : $targetId[$targetClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]];
  309. continue;
  310. }
  311. // relationToSourceKeyColumns
  312. $params[] = ($sourceClass->containsForeignIdentifier)
  313. ? $sourceId[$sourceClass->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])]
  314. : $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]];
  315. }
  316. if ($addFilters) {
  317. $quotedJoinTable .= ' t';
  318. list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
  319. if ($filterSql) {
  320. $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
  321. $whereClauses[] = $filterSql;
  322. }
  323. }
  324. return array($quotedJoinTable, $whereClauses, $params);
  325. }
  326. /**
  327. * Generates the filter SQL for a given mapping.
  328. *
  329. * This method is not used for actually grabbing the related entities
  330. * but when the extra-lazy collection methods are called on a filtered
  331. * association. This is why besides the many to many table we also
  332. * have to join in the actual entities table leading to additional
  333. * JOIN.
  334. *
  335. * @param array $mapping Array containing mapping information.
  336. *
  337. * @return string The SQL query part to add to a query.
  338. */
  339. public function getFilterSql($mapping)
  340. {
  341. $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
  342. $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName);
  343. $filterSql = $this->generateFilterConditionSQL($rootClass, 'te');
  344. if ('' === $filterSql) {
  345. return array('', '');
  346. }
  347. $conditions = array();
  348. $association = $mapping;
  349. if ( ! $mapping['isOwningSide']) {
  350. $class = $this->em->getClassMetadata($mapping['targetEntity']);
  351. $association = $class->associationMappings[$mapping['mappedBy']];
  352. }
  353. // A join is needed if there is filtering on the target entity
  354. $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform);
  355. $joinSql = ' JOIN ' . $tableName . ' te' . ' ON';
  356. $joinColumns = $mapping['isOwningSide']
  357. ? $association['joinTable']['inverseJoinColumns']
  358. : $association['joinTable']['joinColumns'];
  359. foreach ($joinColumns as $joinColumn) {
  360. $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
  361. $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
  362. $conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName;
  363. }
  364. $joinSql .= implode(' AND ', $conditions);
  365. return array($joinSql, $filterSql);
  366. }
  367. /**
  368. * Generates the filter SQL for a given entity and table alias.
  369. *
  370. * @param ClassMetadata $targetEntity Metadata of the target entity.
  371. * @param string $targetTableAlias The table alias of the joined/selected table.
  372. *
  373. * @return string The SQL query part to add to a query.
  374. */
  375. protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
  376. {
  377. $filterClauses = array();
  378. foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
  379. if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
  380. $filterClauses[] = '(' . $filterExpr . ')';
  381. }
  382. }
  383. $sql = implode(' AND ', $filterClauses);
  384. return $sql ? "(" . $sql . ")" : "";
  385. }
  386. }