Failed Conditions
Push — master ( 8be1e3...e3936d )
by Marco
14s
created

OneToManyPersister::deleteJoinedEntityCollection()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 68
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 3.0001

Importance

Changes 0
Metric Value
cc 3
eloc 40
nc 4
nop 1
dl 0
loc 68
ccs 41
cts 42
cp 0.9762
crap 3.0001
rs 9.2447
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Persisters\Collection;
6
7
use Doctrine\Common\Collections\Criteria;
8
use Doctrine\ORM\Mapping\InheritanceType;
9
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
10
use Doctrine\ORM\PersistentCollection;
11
12
/**
13
 * Persister for one-to-many collections.
14
 */
15
class OneToManyPersister extends AbstractCollectionPersister
16
{
17
    /**
18
     * {@inheritdoc}
19
     */
20 2
    public function delete(PersistentCollection $collection)
21
    {
22
        // The only valid case here is when you have weak entities. In this
23
        // scenario, you have @OneToMany with orphanRemoval=true, and replacing
24
        // the entire collection with a new would trigger this operation.
25 2
        $association = $collection->getMapping();
26
27 2
        if (! $association->isOrphanRemoval()) {
28
            // Handling non-orphan removal should never happen, as @OneToMany
29
            // can only be inverse side. For owning side one to many, it is
30
            // required to have a join table, which would classify as a ManyToManyPersister.
31
            return;
32
        }
33
34 2
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
35
36 2
        return $targetClass->inheritanceType === InheritanceType::JOINED
0 ignored issues
show
Bug Best Practice introduced by
The expression return $targetClass->inh...Collection($collection) returns the type integer which is incompatible with the return type mandated by Doctrine\ORM\Persisters\...tionPersister::delete() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
37 1
            ? $this->deleteJoinedEntityCollection($collection)
38 2
            : $this->deleteEntityCollection($collection);
39
    }
40
41
    /**
42
     * {@inheritdoc}
43
     */
44 363
    public function update(PersistentCollection $collection)
45
    {
46
        // This can never happen. One to many can only be inverse side.
47
        // For owning side one to many, it is required to have a join table,
48
        // then classifying it as a ManyToManyPersister.
49 363
        return;
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 2
    public function get(PersistentCollection $collection, $index)
56
    {
57 2
        $association = $collection->getMapping();
58
59 2
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $association->getIndexedBy() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
60
            throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
61
        }
62
63 2
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
64
        $criteria  = [
65 2
            $association->getMappedBy()  => $collection->getOwner(),
66 2
            $association->getIndexedBy() => $index,
67
        ];
68
69 2
        return $persister->load($criteria, null, $association, [], null, 1);
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 14
    public function count(PersistentCollection $collection)
76
    {
77 14
        $association = $collection->getMapping();
78 14
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
79
80
        // only works with single id identifier entities. Will throw an
81
        // exception in Entity Persisters if that is not the case for the
82
        // 'mappedBy' field.
83
        $criteria = [
84 14
            $association->getMappedBy()  => $collection->getOwner(),
85
        ];
86
87 14
        return $persister->count($criteria);
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 5
    public function slice(PersistentCollection $collection, $offset, $length = null)
94
    {
95 5
        $association = $collection->getMapping();
96 5
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
97
98 5
        return $persister->getOneToManyCollection($association, $collection->getOwner(), $offset, $length);
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 4
    public function containsKey(PersistentCollection $collection, $key)
105
    {
106 4
        $association = $collection->getMapping();
107
108 4
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $association->getIndexedBy() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
109
            throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
110
        }
111
112 4
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
113
114
        // only works with single id identifier entities. Will throw an
115
        // exception in Entity Persisters if that is not the case for the
116
        // 'mappedBy' field.
117
        $criteria = [
118 4
            $association->getMappedBy()  => $collection->getOwner(),
119 4
            $association->getIndexedBy() => $key,
120
        ];
121
122 4
        return (bool) $persister->count($criteria);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 10
    public function contains(PersistentCollection $collection, $element)
129
    {
130 10
        if (! $this->isValidEntityState($element)) {
131 3
            return false;
132
        }
133
134 8
        $association = $collection->getMapping();
135 8
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
136
137
        // only works with single id identifier entities. Will throw an
138
        // exception in Entity Persisters if that is not the case for the
139
        // 'mappedBy' field.
140 8
        $criteria = new Criteria(
141 8
            Criteria::expr()->eq($association->getMappedBy(), $collection->getOwner())
142
        );
143
144 8
        return $persister->exists($element, $criteria);
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 11
    public function removeElement(PersistentCollection $collection, $element)
151
    {
152 11
        $association = $collection->getMapping();
153
154 11
        if (! $association->isOrphanRemoval()) {
155
            // no-op: this is not the owning side, therefore no operations should be applied
156 8
            return false;
157
        }
158
159 3
        if (! $this->isValidEntityState($element)) {
160 1
            return false;
161
        }
162
163 2
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
164
165 2
        return $persister->delete($element);
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
172
    {
173
        throw new \BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
174
    }
175
176
    /**
177
     * @return int
178
     *
179
     * @throws \Doctrine\DBAL\DBALException
180
     */
181 1
    private function deleteEntityCollection(PersistentCollection $collection)
182
    {
183 1
        $association  = $collection->getMapping();
184 1
        $identifier   = $this->uow->getEntityIdentifier($collection->getOwner());
185 1
        $sourceClass  = $this->em->getClassMetadata($association->getSourceEntity());
186 1
        $targetClass  = $this->em->getClassMetadata($association->getTargetEntity());
187 1
        $inverseAssoc = $targetClass->getProperty($association->getMappedBy());
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

187
        /** @scrutinizer ignore-call */ 
188
        $inverseAssoc = $targetClass->getProperty($association->getMappedBy());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
188 1
        $columns      = [];
189 1
        $parameters   = [];
190
191 1
        foreach ($inverseAssoc->getJoinColumns() as $joinColumn) {
192 1
            $columns[]    = $this->platform->quoteIdentifier($joinColumn->getColumnName());
193 1
            $parameters[] = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
194
        }
195
196 1
        $tableName = $targetClass->table->getQuotedQualifiedName($this->platform);
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
197 1
        $statement = 'DELETE FROM ' . $tableName . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
198
199 1
        return $this->conn->executeUpdate($statement, $parameters);
200
    }
201
202
    /**
203
     * Delete Class Table Inheritance entities.
204
     * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables.
205
     *
206
     * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =)
207
     *
208
     * @return int
209
     *
210
     * @throws \Doctrine\DBAL\DBALException
211
     */
212 1
    private function deleteJoinedEntityCollection(PersistentCollection $collection)
213
    {
214 1
        $association     = $collection->getMapping();
215 1
        $targetClass     = $this->em->getClassMetadata($association->getTargetEntity());
216 1
        $rootClass       = $this->em->getClassMetadata($targetClass->getRootClassName());
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

216
        $rootClass       = $this->em->getClassMetadata($targetClass->/** @scrutinizer ignore-call */ getRootClassName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
217 1
        $sourcePersister = $this->uow->getEntityPersister($association->getSourceEntity());
218
219
        // 1) Build temporary table DDL
220 1
        $tempTable         = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName());
0 ignored issues
show
Bug introduced by
The method getTemporaryIdTableName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

220
        $tempTable         = $this->platform->getTemporaryTableName($rootClass->/** @scrutinizer ignore-call */ getTemporaryIdTableName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
221 1
        $idColumns         = $rootClass->getIdentifierColumns($this->em);
0 ignored issues
show
Bug introduced by
The method getIdentifierColumns() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getIdentifier()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

221
        /** @scrutinizer ignore-call */ 
222
        $idColumns         = $rootClass->getIdentifierColumns($this->em);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
222 1
        $idColumnNameList  = implode(', ', array_keys($idColumns));
223 1
        $columnDefinitions = [];
224
225 1
        foreach ($idColumns as $columnName => $column) {
226 1
            $type = $column->getType();
227
228 1
            $columnDefinitions[$columnName] = [
229 1
                'notnull' => true,
230 1
                'type'    => $type,
231
            ];
232
        }
233
234 1
        $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable
235 1
            . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')';
236
237 1
        $this->conn->executeUpdate($statement);
238
239
        // 2) Build insert table records into temporary table
240 1
        $dql   = ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames())
241 1
               . ' FROM ' . $targetClass->getClassName() . ' t0 WHERE t0.' . $association->getMappedBy() . ' = :owner';
0 ignored issues
show
Bug introduced by
The method getClassName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

241
               . ' FROM ' . $targetClass->/** @scrutinizer ignore-call */ getClassName() . ' t0 WHERE t0.' . $association->getMappedBy() . ' = :owner';

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
242 1
        $query = $this->em->createQuery($dql)->setParameter('owner', $collection->getOwner());
243
244 1
        $statement  = 'INSERT INTO ' . $tempTable . ' (' . $idColumnNameList . ') ' . $query->getSQL();
0 ignored issues
show
Bug introduced by
Are you sure $query->getSQL() of type string[] can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

244
        $statement  = 'INSERT INTO ' . $tempTable . ' (' . $idColumnNameList . ') ' . /** @scrutinizer ignore-type */ $query->getSQL();
Loading history...
245 1
        $parameters = array_values($sourcePersister->getIdentifier($collection->getOwner()));
246 1
        $numDeleted = $this->conn->executeUpdate($statement, $parameters);
247
248
        // 3) Create statement used in DELETE ... WHERE ... IN (subselect)
249 1
        $deleteSQLTemplate = sprintf(
250 1
            'DELETE FROM %%s WHERE (%s) IN (SELECT %s FROM %s)',
251 1
            $idColumnNameList,
252 1
            $idColumnNameList,
253 1
            $tempTable
254
        );
255
256
        // 4) Delete records on each table in the hierarchy
257 1
        $hierarchyClasses = array_merge(
258 1
            array_map(
259 1
                function ($className) {
260
                    return $this->em->getClassMetadata($className);
261 1
                },
262 1
                array_reverse($targetClass->getSubClasses())
0 ignored issues
show
Bug introduced by
The method getSubClasses() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

262
                array_reverse($targetClass->/** @scrutinizer ignore-call */ getSubClasses())

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
263
            ),
264 1
            [$targetClass],
265 1
            $targetClass->getAncestorsIterator()->getArrayCopy()
0 ignored issues
show
Bug introduced by
The method getAncestorsIterator() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

265
            $targetClass->/** @scrutinizer ignore-call */ 
266
                          getAncestorsIterator()->getArrayCopy()

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
266
        );
267
268 1
        foreach ($hierarchyClasses as $class) {
269 1
            $statement = sprintf($deleteSQLTemplate, $class->table->getQuotedQualifiedName($this->platform));
270
271 1
            $this->conn->executeUpdate($statement);
272
        }
273
274
        // 5) Drop temporary table
275 1
        $statement = $this->platform->getDropTemporaryTableSQL($tempTable);
276
277 1
        $this->conn->executeUpdate($statement);
278
279 1
        return $numDeleted;
280
    }
281
}
282