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) { |
|
|
|
|
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) { |
|
|
|
|
111
|
|
|
$this->db->rollback(); |
112
|
|
|
|
113
|
|
|
if ($this->isUniqueConstraintViolation($e)) { |
114
|
|
|
return FALSE; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
throw $this->db->resolveException($e); |
|
|
|
|
118
|
|
|
|
119
|
|
|
} catch (\Exception $e) { |
120
|
|
|
$this->db->rollback(); |
121
|
|
|
throw $e; |
122
|
|
|
|
123
|
|
|
} catch (\Throwable $e) { |
|
|
|
|
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) || |
|
|
|
|
204
|
|
|
($this->platform instanceof SqlitePlatform && $e->errorInfo[1] === Connection::SQLITE_ERR_UNIQUE) || |
|
|
|
|
205
|
|
|
($this->platform instanceof PostgreSqlPlatform && $e->errorInfo[1] === Connection::POSTGRE_ERR_UNIQUE); |
|
|
|
|
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
|
|
|
|
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.