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) |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
// entity is now safely inserted to database, merge now |
160
|
|
|
$merged = $this->em->merge($entity); |
|
|
|
|
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) || |
|
|
|
|
208
|
|
|
($this->platform instanceof SqlitePlatform && $e->errorInfo[1] === Connection::SQLITE_ERR_UNIQUE) || |
|
|
|
|
209
|
|
|
($this->platform instanceof PostgreSqlPlatform && $e->errorInfo[1] === Connection::POSTGRE_ERR_UNIQUE); |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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:If that code throws an exception and the
EntityManager
is closed. Any other code which depends on the same instance of theEntityManager
during this request will fail.On the other hand, if you instead inject the
ManagerRegistry
, thegetManager()
method guarantees that you will always get a usable manager instance.