NonLockingUniqueInserter::doInsert()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 9.424
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
/**
4
 * This file is part of the Kdyby (http://www.kdyby.org)
5
 *
6
 * Copyright (c) 2008 Filip Procházka ([email protected])
7
 *
8
 * For the full copyright and license information, please view the file license.txt that was distributed with this source code.
9
 */
10
11
namespace Kdyby\Doctrine\Tools;
12
13
use Doctrine;
14
use Doctrine\DBAL\DBALException;
15
use Doctrine\DBAL\Platforms\MySqlPlatform;
16
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
17
use Doctrine\DBAL\Platforms\SqlitePlatform;
18
use Doctrine\DBAL\Statement;
19
use Doctrine\DBAL\Types\Type;
20
use Kdyby;
21
use Kdyby\Doctrine\Connection;
22
use Kdyby\Doctrine\EntityManager;
23
use Kdyby\Doctrine\Mapping\ClassMetadata;
24
use Nette;
25
26
27
28
/**
29
 * @author Filip Procházka <[email protected]>
30
 * @author Martin Štekl <[email protected]>
31
 */
32
class NonLockingUniqueInserter
33
{
34
35
	use \Kdyby\StrictObjects\Scream;
36
37
	/**
38
	 * @var \Kdyby\Doctrine\EntityManager
39
	 */
40
	private $em;
41
42
	/**
43
	 * @var \Kdyby\Doctrine\Connection
44
	 */
45
	private $db;
46
47
	/**
48
	 * @var \Doctrine\DBAL\Platforms\AbstractPlatform
49
	 */
50
	private $platform;
51
52
	/**
53
	 * @var \Doctrine\ORM\Mapping\QuoteStrategy
54
	 */
55
	private $quotes;
56
57
	/**
58
	 * @var \Doctrine\ORM\UnitOfWork
59
	 */
60
	private $uow;
61
62
63
64
	/**
65
	 * @param EntityManager $em
66
	 */
67
	public function __construct(EntityManager $em)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $em. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
68
	{
69
		$this->em = $em;
70
		/** @var \Kdyby\Doctrine\Connection $db */
71
		$db = $em->getConnection();
72
		$this->db = $db;
73
		$this->platform = $db->getDatabasePlatform();
74
		$this->quotes = $em->getConfiguration()->getQuoteStrategy();
75
		$this->uow = $em->getUnitOfWork();
76
	}
77
78
79
80
	/**
81
	 * When entity have columns for required associations, this will fail.
82
	 * Calls $em->flush().
83
	 *
84
	 * Warning: You must NOT use the passed entity further in your application.
85
	 * Use the returned one instead!
86
	 *
87
	 * @todo fix error codes! PDO is returning database-specific codes
88
	 *
89
	 * @param object $entity
90
	 * @throws \Doctrine\DBAL\DBALException
91
	 * @throws \Exception
92
	 * @return bool|object
93
	 */
94
	public function persist($entity)
95
	{
96
		$this->db->beginTransaction();
97
98
		try {
99
			$persisted = $this->doInsert($entity);
100
			$this->db->commit();
101
102
			return $persisted;
103
104
		} catch (Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
105
			$this->db->rollBack();
106
107
			return FALSE;
108
109
		} catch (Kdyby\Doctrine\DuplicateEntryException $e) {
110
			$this->db->rollBack();
111
112
			return FALSE;
113
114
		} catch (DBALException $e) {
115
			$this->db->rollBack();
116
117
			if ($this->isUniqueConstraintViolation($e)) {
118
				return FALSE;
119
			}
120
121
			throw $this->db->resolveException($e);
0 ignored issues
show
Deprecated Code introduced by
The method Kdyby\Doctrine\Connection::resolveException() has been deprecated.

This method has been deprecated.

Loading history...
122
123
		} catch (\Exception $e) {
124
			$this->db->rollBack();
125
			throw $e;
126
127
		} catch (\Throwable $e) {
128
			$this->db->rollBack();
129
			throw $e;
130
		}
131
	}
132
133
134
135
	private function doInsert($entity)
136
	{
137
		/** @var \Kdyby\Doctrine\Mapping\ClassMetadata $meta */
138
		$meta = $this->em->getClassMetadata(get_class($entity));
139
140
		// fields that have to be inserted
141
		$fields = $this->getFieldsWithValues($meta, $entity);
142
		// associations that have to be inserted
143
		$associations = $this->getAssociationsWithValues($meta, $entity);
144
		// discriminator column
145
		$discriminator = $this->getDiscriminatorColumn($meta);
146
147
		// prepare statement && execute
148
		$this->prepareInsert($meta, array_merge($fields, $associations, $discriminator))->execute();
149
150
		// assign ID to entity
151
		if ($idGen = $meta->idGenerator) {
152
			if ($idGen->isPostInsertGenerator()) {
153
				$id = $idGen->generate($this->em, $entity);
154
				$identifierFields = $meta->getIdentifierFieldNames();
155
				$meta->setFieldValue($entity, reset($identifierFields), $id);
0 ignored issues
show
Security Bug introduced by
It seems like reset($identifierFields) targeting reset() can also be of type false; however, Doctrine\ORM\Mapping\Cla...taInfo::setFieldValue() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
156
			}
157
		}
158
159
		// entity is now safely inserted to database, merge now
160
		$merged = $this->em->merge($entity);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ORM\EntityManager::merge() has been deprecated with message: 2.7 This method is being removed from the ORM and won't have any replacement

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
161
		$this->em->flush([$merged]);
162
163
		// when you merge entity, you get a new reference
164
		return $merged;
165
	}
166
167
168
169
	private function prepareInsert(ClassMetadata $meta, array $data)
170
	{
171
		// construct sql
172
		$columns = [];
173
		foreach ($data as $column) {
174
			$columns[] = $column['quotedColumn'];
175
		}
176
177
		$insertSql = 'INSERT INTO ' . $this->quotes->getTableName($meta, $this->platform)
178
			. ' (' . implode(', ', $columns) . ')'
179
			. ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
180
181
		// create statement
182
		$statement = new Statement($insertSql, $this->db);
183
184
		// bind values
185
		$paramIndex = 1;
186
		foreach ($data as $column) {
187
			$statement->bindValue($paramIndex++, $column['value'], $column['type']);
188
		}
189
190
		return $statement;
191
	}
192
193
194
195
	/**
196
	 * @param \Exception|\PDOException $e
197
	 * @return bool
198
	 */
199
	private function isUniqueConstraintViolation($e)
200
	{
201
		if (!$e instanceof \PDOException && !(($e = $e->getPrevious()) instanceof \PDOException)) {
202
			return FALSE;
203
		}
204
		/** @var \PDOException $e */
205
206
		return
207
			($this->platform instanceof MySqlPlatform && $e->errorInfo[1] === Connection::MYSQL_ERR_UNIQUE) ||
0 ignored issues
show
Deprecated Code introduced by
The constant Kdyby\Doctrine\Connection::MYSQL_ERR_UNIQUE has been deprecated.

This class constant has been deprecated.

Loading history...
208
			($this->platform instanceof SqlitePlatform && $e->errorInfo[1] === Connection::SQLITE_ERR_UNIQUE) ||
0 ignored issues
show
Deprecated Code introduced by
The constant Kdyby\Doctrine\Connection::SQLITE_ERR_UNIQUE has been deprecated.

This class constant has been deprecated.

Loading history...
209
			($this->platform instanceof PostgreSqlPlatform && $e->errorInfo[1] === Connection::POSTGRE_ERR_UNIQUE);
0 ignored issues
show
Deprecated Code introduced by
The constant Kdyby\Doctrine\Connection::POSTGRE_ERR_UNIQUE has been deprecated.

This class constant has been deprecated.

Loading history...
210
	}
211
212
213
214
	private function getFieldsWithValues(ClassMetadata $meta, $entity)
215
	{
216
		$fields = [];
217
		foreach ($meta->getFieldNames() as $fieldName) {
218
			$mapping = $meta->getFieldMapping($fieldName);
219
			if (!empty($mapping['id']) && $meta->usesIdGenerator()) { // autogenerated id
220
				continue;
221
			}
222
			$fields[$fieldName]['value'] = $meta->getFieldValue($entity, $fieldName);
223
			$fields[$fieldName]['quotedColumn'] = $this->quotes->getColumnName($fieldName, $meta, $this->platform);
224
			$fields[$fieldName]['type'] = Type::getType($mapping['type']);
225
		}
226
227
		return $fields;
228
	}
229
230
231
232
	private function getAssociationsWithValues(ClassMetadata $meta, $entity)
233
	{
234
		$associations = [];
235
		foreach ($meta->getAssociationNames() as $associationName) {
236
			$mapping = $meta->getAssociationMapping($associationName);
237
			if (!empty($mapping['id']) && $meta->usesIdGenerator()) { // autogenerated id
238
				continue;
239
			}
240
			if (!($mapping['type'] & ClassMetadata::TO_ONE)) { // is not to one relation
241
				continue;
242
			}
243
			if (empty($mapping['isOwningSide'])) { // is not owning side
244
				continue;
245
			}
246
247
			foreach ($mapping['joinColumns'] as $joinColumn) {
248
				$targetColumn = $joinColumn['referencedColumnName'];
249
				$targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
250
				$newVal = $meta->getFieldValue($entity, $associationName);
251
				$newValId = $newVal !== NULL ? $this->uow->getEntityIdentifier($newVal) : [];
252
253
				switch (TRUE) {
254
					case $newVal === NULL:
255
						$value = NULL;
256
						break;
257
258
					case $targetClass->containsForeignIdentifier:
259
						$value = $newValId[$targetClass->getFieldForColumn($targetColumn)];
260
						break;
261
262
					default:
263
						$value = $newValId[$targetClass->fieldNames[$targetColumn]];
264
						break;
265
				}
266
267
				$sourceColumn = $joinColumn['name'];
268
				$quotedColumn = $this->quotes->getJoinColumnName($joinColumn, $meta, $this->platform);
269
				$associations[$sourceColumn]['value'] = $value;
270
				$associations[$sourceColumn]['quotedColumn'] = $quotedColumn;
271
				$associations[$sourceColumn]['type'] = $targetClass->getTypeOfColumn($targetColumn);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ORM\Mapping\Cla...Info::getTypeOfColumn() has been deprecated with message: 3.0 remove this. this method is bogus and unreliable, since it cannot resolve the type of a column that is derived by a referenced field on a different entity.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
272
			}
273
		}
274
275
		return $associations;
276
	}
277
278
279
280
	private function getDiscriminatorColumn(ClassMetadata $meta)
281
	{
282
		if (!$meta->isInheritanceTypeSingleTable()) {
283
			return [];
284
		}
285
286
		$column = $meta->discriminatorColumn;
287
288
		return [
289
			$column['fieldName'] => [
290
				'value' => $meta->discriminatorValue,
291
				'quotedColumn' => $this->platform->quoteIdentifier($column['name']),
292
				'type' => Type::getType($column['type']),
293
			],
294
		];
295
	}
296
297
}
298