Completed
Push — master ( 06fd1e...6a29f2 )
by Filip
02:15
created

NonLockingUniqueInserter::prepareInsert()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
cc 3
eloc 12
nc 4
nop 2
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 extends Nette\Object
33
{
34
35
	/**
36
	 * @var \Kdyby\Doctrine\EntityManager
37
	 */
38
	private $em;
39
40
	/**
41
	 * @var \Kdyby\Doctrine\Connection
42
	 */
43
	private $db;
44
45
	/**
46
	 * @var \Doctrine\DBAL\Platforms\AbstractPlatform
47
	 */
48
	private $platform;
49
50
	/**
51
	 * @var \Doctrine\ORM\Mapping\QuoteStrategy
52
	 */
53
	private $quotes;
54
55
	/**
56
	 * @var \Doctrine\ORM\UnitOfWork
57
	 */
58
	private $uow;
59
60
61
62
	/**
63
	 * @param EntityManager $em
64
	 */
65
	public function __construct(EntityManager $em)
66
	{
67
		$this->em = $em;
68
		$this->db = $em->getConnection();
69
		$this->platform = $this->db->getDatabasePlatform();
70
		$this->quotes = $em->getConfiguration()->getQuoteStrategy();
71
		$this->uow = $this->em->getUnitOfWork();
72
	}
73
74
75
76
	/**
77
	 * When entity have columns for required associations, this will fail.
78
	 * Calls $em->flush().
79
	 *
80
	 * Warning: You must NOT use the passed entity further in your application.
81
	 * Use the returned one instead!
82
	 *
83
	 * @todo fix error codes! PDO is returning database-specific codes
84
	 *
85
	 * @param object $entity
86
	 * @throws \Doctrine\DBAL\DBALException
87
	 * @throws \Exception
88
	 * @return bool|object
89
	 */
90
	public function persist($entity)
91
	{
92
		$this->db->beginTransaction();
93
94
		try {
95
			$persisted = $this->doInsert($entity);
96
			$this->db->commit();
97
98
			return $persisted;
99
100
		} catch (Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\...raintViolationException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
101
			$this->db->rollback();
102
103
			return FALSE;
104
105
		} catch (Kdyby\Doctrine\DuplicateEntryException $e) {
106
			$this->db->rollback();
107
108
			return FALSE;
109
110
		} catch (DBALException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\DBALException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
111
			$this->db->rollback();
112
113
			if ($this->isUniqueConstraintViolation($e)) {
114
				return FALSE;
115
			}
116
117
			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...
118
119
		} catch (\Exception $e) {
120
			$this->db->rollback();
121
			throw $e;
122
123
		} catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
124
			$this->db->rollback();
125
			throw $e;
126
		}
127
	}
128
129
130
131
	private function doInsert($entity)
132
	{
133
		// get entity metadata
134
		$meta = $this->em->getClassMetadata(get_class($entity));
135
136
		// fields that have to be inserted
137
		$fields = $this->getFieldsWithValues($meta, $entity);
138
		// associations that have to be inserted
139
		$associations = $this->getAssociationsWithValues($meta, $entity);
140
		// discriminator column
141
		$discriminator = $this->getDiscriminatorColumn($meta);
142
143
		// prepare statement && execute
144
		$this->prepareInsert($meta, array_merge($fields, $associations, $discriminator))->execute();
145
146
		// assign ID to entity
147
		if ($idGen = $meta->idGenerator) {
148
			if ($idGen->isPostInsertGenerator()) {
149
				$id = $idGen->generate($this->em, $entity);
150
				$identifierFields = $meta->getIdentifierFieldNames();
151
				$meta->setFieldValue($entity, reset($identifierFields), $id);
152
			}
153
		}
154
155
		// entity is now safely inserted to database, merge now
156
		$merged = $this->em->merge($entity);
157
		$this->em->flush([$merged]);
158
159
		// when you merge entity, you get a new reference
160
		return $merged;
161
	}
162
163
164
165
	private function prepareInsert(ClassMetadata $meta, array $data)
166
	{
167
		// construct sql
168
		$columns = [];
169
		foreach ($data as $column) {
170
			$columns[] = $column['quotedColumn'];
171
		}
172
173
		$insertSql = 'INSERT INTO ' . $this->quotes->getTableName($meta, $this->platform)
174
			. ' (' . implode(', ', $columns) . ')'
175
			. ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
176
177
		// create statement
178
		$statement = new Statement($insertSql, $this->db);
179
180
		// bind values
181
		$paramIndex = 1;
182
		foreach ($data as $column) {
183
			$statement->bindValue($paramIndex++, $column['value'], $column['type']);
184
		}
185
186
		return $statement;
187
	}
188
189
190
191
	/**
192
	 * @param \Exception|\PDOException $e
193
	 * @return bool
194
	 */
195
	private function isUniqueConstraintViolation($e)
196
	{
197
		if (!$e instanceof \PDOException && !(($e = $e->getPrevious()) instanceof \PDOException)) {
198
			return FALSE;
199
		}
200
		/** @var \PDOException $e */
201
202
		return
203
			($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...
Bug introduced by
The class Doctrine\DBAL\Platforms\MySqlPlatform does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
204
			($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...
Bug introduced by
The class Doctrine\DBAL\Platforms\SqlitePlatform does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
205
			($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...
Bug introduced by
The class Doctrine\DBAL\Platforms\PostgreSqlPlatform does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
206
	}
207
208
209
210
	private function getFieldsWithValues(ClassMetadata $meta, $entity)
211
	{
212
		$fields = [];
213
		foreach ($meta->getFieldNames() as $fieldName) {
214
			$mapping = $meta->getFieldMapping($fieldName);
215
			if (!empty($mapping['id']) && $meta->usesIdGenerator()) { // autogenerated id
216
				continue;
217
			}
218
			$fields[$fieldName]['value'] = $meta->getFieldValue($entity, $fieldName);
219
			$fields[$fieldName]['quotedColumn'] = $this->quotes->getColumnName($fieldName, $meta, $this->platform);
220
			$fields[$fieldName]['type'] = Type::getType($mapping['type']);
221
		}
222
223
		return $fields;
224
	}
225
226
227
228
	private function getAssociationsWithValues(ClassMetadata $meta, $entity)
229
	{
230
		$associations = [];
231
		foreach ($meta->getAssociationNames() as $associationName) {
232
			$mapping = $meta->getAssociationMapping($associationName);
233
			if (!empty($mapping['id']) && $meta->usesIdGenerator()) { // autogenerated id
234
				continue;
235
			}
236
			if (!($mapping['type'] & ClassMetadata::TO_ONE)) { // is not to one relation
237
				continue;
238
			}
239
			if (empty($mapping['isOwningSide'])) { // is not owning side
240
				continue;
241
			}
242
243
			foreach ($mapping['joinColumns'] as $joinColumn) {
244
				$targetColumn = $joinColumn['referencedColumnName'];
245
				$targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
246
				$newVal = $meta->getFieldValue($entity, $associationName);
247
				$newValId = $newVal !== NULL ? $this->uow->getEntityIdentifier($newVal) : [];
248
249
				switch (TRUE) {
250
					case $newVal === NULL:
251
						$value = NULL;
252
						break;
253
254
					case $targetClass->containsForeignIdentifier:
255
						$value = $newValId[$targetClass->getFieldForColumn($targetColumn)];
256
						break;
257
258
					default:
259
						$value = $newValId[$targetClass->fieldNames[$targetColumn]];
260
						break;
261
				}
262
263
				$sourceColumn = $joinColumn['name'];
264
				$quotedColumn = $this->quotes->getJoinColumnName($joinColumn, $meta, $this->platform);
265
				$associations[$sourceColumn]['value'] = $value;
266
				$associations[$sourceColumn]['quotedColumn'] = $quotedColumn;
267
				$associations[$sourceColumn]['type'] = $targetClass->getTypeOfColumn($targetColumn);
268
			}
269
		}
270
271
		return $associations;
272
	}
273
274
275
276
	private function getDiscriminatorColumn(ClassMetadata $meta)
277
	{
278
		if (!$meta->isInheritanceTypeSingleTable()) {
279
			return [];
280
		}
281
282
		$column = $meta->discriminatorColumn;
283
284
		return [
285
			$column['fieldName'] => [
286
				'value' => $meta->discriminatorValue,
287
				'quotedColumn' => $this->platform->quoteIdentifier($column['name']),
288
				'type' => Type::getType($column['type']),
289
			],
290
		];
291
	}
292
293
}
294