Completed
Pull Request — master (#245)
by Tomáš
02:43
created

NonLockingUniqueInserter::persist()   B

Complexity

Conditions 6
Paths 11

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 33
rs 8.439
cc 6
eloc 20
nc 11
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 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)
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...
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 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 (DBALException $e) {
106
			$this->db->rollback();
107
108
			if ($this->isUniqueConstraintViolation($e)) {
109
				return FALSE;
110
			}
111
112
			throw $e;
113
114
		} catch (\Exception $e) {
115
			$this->db->rollback();
116
			throw $e;
117
118
		} 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...
119
			$this->db->rollback();
120
			throw $e;
121
		}
122
	}
123
124
125
126
	private function doInsert($entity)
127
	{
128
		// get entity metadata
129
		$meta = $this->em->getClassMetadata(get_class($entity));
130
131
		// fields that have to be inserted
132
		$fields = $this->getUniqueAndRequiredFields($meta, $entity);
0 ignored issues
show
Compatibility introduced by
$meta of type object<Doctrine\ORM\Mapping\ClassMetadata> is not a sub-type of object<Kdyby\Doctrine\Mapping\ClassMetadata>. It seems like you assume a child class of the class Doctrine\ORM\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
133
		// associations that have to be inserted
134
		$associations = $this->getUniqueAndRequiredAssociations($meta, $entity);
0 ignored issues
show
Compatibility introduced by
$meta of type object<Doctrine\ORM\Mapping\ClassMetadata> is not a sub-type of object<Kdyby\Doctrine\Mapping\ClassMetadata>. It seems like you assume a child class of the class Doctrine\ORM\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
135
		// discriminator column
136
		$discriminator = $this->getDiscriminatorColumn($meta);
0 ignored issues
show
Compatibility introduced by
$meta of type object<Doctrine\ORM\Mapping\ClassMetadata> is not a sub-type of object<Kdyby\Doctrine\Mapping\ClassMetadata>. It seems like you assume a child class of the class Doctrine\ORM\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
137
138
		// prepare statement && execute
139
		$this->prepareInsert($meta, array_merge($fields, $associations, $discriminator))->execute();
0 ignored issues
show
Compatibility introduced by
$meta of type object<Doctrine\ORM\Mapping\ClassMetadata> is not a sub-type of object<Kdyby\Doctrine\Mapping\ClassMetadata>. It seems like you assume a child class of the class Doctrine\ORM\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
140
141
		// assign ID to entity
142
		if ($idGen = $meta->idGenerator) {
143
			if ($idGen->isPostInsertGenerator()) {
144
				$id = $idGen->generate($this->em, $entity);
145
				$identifierFields = $meta->getIdentifierFieldNames();
146
				$meta->setFieldValue($entity, reset($identifierFields), $id);
147
			}
148
		}
149
150
		// entity is now safely inserted to database, merge now
151
		$merged = $this->em->merge($entity);
152
		$this->em->flush([$merged]);
153
154
		// when you merge entity, you get a new reference
155
		return $merged;
156
	}
157
158
159
160
	private function prepareInsert(ClassMetadata $meta, array $data)
161
	{
162
		// construct sql
163
		$columns = [];
164
		foreach ($data as $column) {
165
			$columns[] = $column['quotedColumn'];
166
		}
167
168
		$insertSql = 'INSERT INTO ' . $this->quotes->getTableName($meta, $this->platform)
169
			. ' (' . implode(', ', $columns) . ')'
170
			. ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
171
172
		// create statement
173
		$statement = new Statement($insertSql, $this->db);
174
175
		// bind values
176
		$paramIndex = 1;
177
		foreach ($data as $column) {
178
			$statement->bindValue($paramIndex++, $column['value'], $column['type']);
179
		}
180
181
		return $statement;
182
	}
183
184
185
186
	/**
187
	 * @param \Exception|\PDOException $e
188
	 * @return bool
189
	 */
190
	private function isUniqueConstraintViolation($e)
191
	{
192
		if (!$e instanceof \PDOException && !(($e = $e->getPrevious()) instanceof \PDOException)) {
193
			return FALSE;
194
		}
195
		/** @var \PDOException $e */
196
197
		return
198
			($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...
199
			($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...
200
			($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...
201
	}
202
203
204
205
	private function getUniqueAndRequiredFields(ClassMetadata $meta, $entity)
206
	{
207
		$fields = [];
208
		foreach ($meta->getFieldNames() as $fieldName) {
209
			$mapping = $meta->getFieldMapping($fieldName);
210
			if (!empty($mapping['id']) && $meta->usesIdGenerator()) { // autogenerated id
211
				continue;
212
			}
213
			if (!empty($mapping['nullable']) && empty($mapping['unique'])) { // is nullable and is not unique
214
				continue;
215
			}
216
			$fields[$fieldName]['value'] = $meta->getFieldValue($entity, $fieldName);
217
			$fields[$fieldName]['quotedColumn'] = $this->quotes->getColumnName($fieldName, $meta, $this->platform);
218
			$fields[$fieldName]['type'] = Type::getType($mapping['type']);
219
		}
220
221
		return $fields;
222
	}
223
224
225
226
	private function getUniqueAndRequiredAssociations(ClassMetadata $meta, $entity)
227
	{
228
		$associations = [];
229
		foreach ($meta->getAssociationNames() as $associationName) {
230
			$mapping = $meta->getAssociationMapping($associationName);
231
			if (!empty($mapping['id']) && $meta->usesIdGenerator()) { // autogenerated id
232
				continue;
233
			}
234
			if (!($mapping['type'] & ClassMetadata::TO_ONE)) { // is not to one relation
235
				continue;
236
			}
237
			if (empty($mapping['isOwningSide'])) { // is not owning side
238
				continue;
239
			}
240
241
			foreach ($mapping['joinColumns'] as $joinColumn) {
242
				if (!empty($joinColumn['nullable']) && empty($joinColumn['unique'])) { // is nullable and is not unique
243
					continue;
244
				}
245
246
				$targetColumn = $joinColumn['referencedColumnName'];
247
				$targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
248
				$newVal = $meta->getFieldValue($entity, $associationName);
249
				if ($newVal !== NULL) {
250
					$newValId = $this->uow->getEntityIdentifier($newVal);
251
				}
252
253
				switch (TRUE) {
254
					case $newVal === NULL:
255
						$value = NULL;
256
						break;
257
258
					case $targetClass->containsForeignIdentifier:
259
						$value = $newValId[$targetClass->getFieldForColumn($targetColumn)];
0 ignored issues
show
Bug introduced by
The variable $newValId does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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: this method is bogous 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