Failed Conditions
Push — master ( 901929...f6cc12 )
by Jonathan
11:18
created

OneToManyPersister::count()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 13
ccs 5
cts 5
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
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\DBAL\DBALException;
9
use Doctrine\ORM\Mapping\InheritanceType;
10
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
11
use Doctrine\ORM\PersistentCollection;
12
use function array_keys;
13
use function array_map;
14
use function array_merge;
15
use function array_reverse;
16
use function array_values;
17
use function implode;
18
use function sprintf;
19
20
/**
21
 * Persister for one-to-many collections.
22
 */
23
class OneToManyPersister extends AbstractCollectionPersister
24
{
25
    /**
26
     * {@inheritdoc}
27
     */
28 2
    public function delete(PersistentCollection $collection)
29
    {
30
        // The only valid case here is when you have weak entities. In this
31
        // scenario, you have @OneToMany with orphanRemoval=true, and replacing
32
        // the entire collection with a new would trigger this operation.
33 2
        $association = $collection->getMapping();
34
35 2
        if (! $association->isOrphanRemoval()) {
36
            // Handling non-orphan removal should never happen, as @OneToMany
37
            // can only be inverse side. For owning side one to many, it is
38
            // required to have a join table, which would classify as a ManyToManyPersister.
39
            return;
40
        }
41
42 2
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
43
44 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...
45 1
            ? $this->deleteJoinedEntityCollection($collection)
46 2
            : $this->deleteEntityCollection($collection);
47
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52 361
    public function update(PersistentCollection $collection)
53
    {
54
        // This can never happen. One to many can only be inverse side.
55
        // For owning side one to many, it is required to have a join table,
56
        // then classifying it as a ManyToManyPersister.
57 361
        return;
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 2
    public function get(PersistentCollection $collection, $index)
64
    {
65 2
        $association = $collection->getMapping();
66
67 2
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
68
            throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
69
        }
70
71 2
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
72
        $criteria  = [
73 2
            $association->getMappedBy()  => $collection->getOwner(),
74 2
            $association->getIndexedBy() => $index,
75
        ];
76
77 2
        return $persister->load($criteria, null, $association, [], null, 1);
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83 14
    public function count(PersistentCollection $collection)
84
    {
85 14
        $association = $collection->getMapping();
86 14
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
87
88
        // only works with single id identifier entities. Will throw an
89
        // exception in Entity Persisters if that is not the case for the
90
        // 'mappedBy' field.
91
        $criteria = [
92 14
            $association->getMappedBy()  => $collection->getOwner(),
93
        ];
94
95 14
        return $persister->count($criteria);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 5
    public function slice(PersistentCollection $collection, $offset, $length = null)
102
    {
103 5
        $association = $collection->getMapping();
104 5
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
105
106 5
        return $persister->getOneToManyCollection($association, $collection->getOwner(), $offset, $length);
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112 4
    public function containsKey(PersistentCollection $collection, $key)
113
    {
114 4
        $association = $collection->getMapping();
115
116 4
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
117
            throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
118
        }
119
120 4
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
121
122
        // only works with single id identifier entities. Will throw an
123
        // exception in Entity Persisters if that is not the case for the
124
        // 'mappedBy' field.
125
        $criteria = [
126 4
            $association->getMappedBy()  => $collection->getOwner(),
127 4
            $association->getIndexedBy() => $key,
128
        ];
129
130 4
        return (bool) $persister->count($criteria);
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 10
    public function contains(PersistentCollection $collection, $element)
137
    {
138 10
        if (! $this->isValidEntityState($element)) {
139 3
            return false;
140
        }
141
142 8
        $association = $collection->getMapping();
143 8
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
144
145
        // only works with single id identifier entities. Will throw an
146
        // exception in Entity Persisters if that is not the case for the
147
        // 'mappedBy' field.
148 8
        $criteria = new Criteria(
149 8
            Criteria::expr()->eq($association->getMappedBy(), $collection->getOwner())
150
        );
151
152 8
        return $persister->exists($element, $criteria);
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 11
    public function removeElement(PersistentCollection $collection, $element)
159
    {
160 11
        $association = $collection->getMapping();
161
162 11
        if (! $association->isOrphanRemoval()) {
163
            // no-op: this is not the owning side, therefore no operations should be applied
164 8
            return false;
165
        }
166
167 3
        if (! $this->isValidEntityState($element)) {
168 1
            return false;
169
        }
170
171 2
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
172
173 2
        return $persister->delete($element);
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
180
    {
181
        throw new \BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
182
    }
183
184
    /**
185
     * @return int
186
     *
187
     * @throws DBALException
188
     */
189 1
    private function deleteEntityCollection(PersistentCollection $collection)
190
    {
191 1
        $association  = $collection->getMapping();
192 1
        $identifier   = $this->uow->getEntityIdentifier($collection->getOwner());
193 1
        $sourceClass  = $this->em->getClassMetadata($association->getSourceEntity());
194 1
        $targetClass  = $this->em->getClassMetadata($association->getTargetEntity());
195 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

195
        /** @scrutinizer ignore-call */ 
196
        $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...
196 1
        $columns      = [];
197 1
        $parameters   = [];
198
199 1
        foreach ($inverseAssoc->getJoinColumns() as $joinColumn) {
200 1
            $columns[]    = $this->platform->quoteIdentifier($joinColumn->getColumnName());
201 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...
202
        }
203
204 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...
205 1
        $statement = 'DELETE FROM ' . $tableName . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
206
207 1
        return $this->conn->executeUpdate($statement, $parameters);
208
    }
209
210
    /**
211
     * Delete Class Table Inheritance entities.
212
     * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables.
213
     *
214
     * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =)
215
     *
216
     * @return int
217
     *
218
     * @throws DBALException
219
     */
220 1
    private function deleteJoinedEntityCollection(PersistentCollection $collection)
221
    {
222 1
        $association     = $collection->getMapping();
223 1
        $targetClass     = $this->em->getClassMetadata($association->getTargetEntity());
224 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

224
        $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...
225 1
        $sourcePersister = $this->uow->getEntityPersister($association->getSourceEntity());
226
227
        // 1) Build temporary table DDL
228 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

228
        $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...
229 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

229
        /** @scrutinizer ignore-call */ 
230
        $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...
230 1
        $idColumnNameList  = implode(', ', array_keys($idColumns));
231 1
        $columnDefinitions = [];
232
233 1
        foreach ($idColumns as $columnName => $column) {
234 1
            $type = $column->getType();
235
236 1
            $columnDefinitions[$columnName] = [
237 1
                'notnull' => true,
238 1
                'type'    => $type,
239
            ];
240
        }
241
242 1
        $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable
243 1
            . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')';
244
245 1
        $this->conn->executeUpdate($statement);
246
247
        // 2) Build insert table records into temporary table
248 1
        $dql   = ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames())
249 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

249
               . ' 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...
250 1
        $query = $this->em->createQuery($dql)->setParameter('owner', $collection->getOwner());
251
252 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

252
        $statement  = 'INSERT INTO ' . $tempTable . ' (' . $idColumnNameList . ') ' . /** @scrutinizer ignore-type */ $query->getSQL();
Loading history...
253 1
        $parameters = array_values($sourcePersister->getIdentifier($collection->getOwner()));
254 1
        $numDeleted = $this->conn->executeUpdate($statement, $parameters);
255
256
        // 3) Create statement used in DELETE ... WHERE ... IN (subselect)
257 1
        $deleteSQLTemplate = sprintf(
258 1
            'DELETE FROM %%s WHERE (%s) IN (SELECT %s FROM %s)',
259 1
            $idColumnNameList,
260 1
            $idColumnNameList,
261 1
            $tempTable
262
        );
263
264
        // 4) Delete records on each table in the hierarchy
265 1
        $hierarchyClasses = array_merge(
266 1
            array_map(
267
                function ($className) {
268
                    return $this->em->getClassMetadata($className);
269 1
                },
270 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

270
                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...
271
            ),
272 1
            [$targetClass],
273 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

273
            $targetClass->/** @scrutinizer ignore-call */ 
274
                          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...
274
        );
275
276 1
        foreach ($hierarchyClasses as $class) {
277 1
            $statement = sprintf($deleteSQLTemplate, $class->table->getQuotedQualifiedName($this->platform));
278
279 1
            $this->conn->executeUpdate($statement);
280
        }
281
282
        // 5) Drop temporary table
283 1
        $statement = $this->platform->getDropTemporaryTableSQL($tempTable);
284
285 1
        $this->conn->executeUpdate($statement);
286
287 1
        return $numDeleted;
288
    }
289
}
290