Failed Conditions
Push — 2.8.x ( 0ee171...60c486 )
by Benjamin
22:42 queued 16:29
created

OneToManyPersister   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Test Coverage

Coverage 94.74%

Importance

Changes 0
Metric Value
wmc 18
eloc 84
dl 0
loc 233
ccs 90
cts 95
cp 0.9474
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A delete() 0 19 3
A containsKey() 0 19 2
A slice() 0 6 1
A contains() 0 15 2
A count() 0 11 1
A get() 0 20 2
A update() 0 6 1
A deleteJoinedEntityCollection() 0 52 3
A deleteEntityCollection() 0 18 2
A loadCriteria() 0 3 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Persisters\Collection;
21
22
use Doctrine\Common\Collections\Criteria;
23
use Doctrine\DBAL\Types\Type;
24
use Doctrine\ORM\PersistentCollection;
25
use Doctrine\ORM\Utility\PersisterHelper;
26
27
/**
28
 * Persister for one-to-many collections.
29
 *
30
 * @author  Roman Borschel <[email protected]>
31
 * @author  Guilherme Blanco <[email protected]>
32
 * @author  Alexander <[email protected]>
33
 * @since   2.0
34
 */
35
class OneToManyPersister extends AbstractCollectionPersister
36
{
37
    /**
38
     * {@inheritdoc}
39
     */
40 2
    public function delete(PersistentCollection $collection)
41
    {
42
        // The only valid case here is when you have weak entities. In this
43
        // scenario, you have @OneToMany with orphanRemoval=true, and replacing
44
        // the entire collection with a new would trigger this operation.
45 2
        $mapping = $collection->getMapping();
46
47 2
        if ( ! $mapping['orphanRemoval']) {
48
            // Handling non-orphan removal should never happen, as @OneToMany
49
            // can only be inverse side. For owning side one to many, it is
50
            // required to have a join table, which would classify as a ManyToManyPersister.
51
            return;
52
        }
53
54 2
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
55
56 2
        return $targetClass->isInheritanceTypeJoined()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $targetClass->isI...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...
57 1
            ? $this->deleteJoinedEntityCollection($collection)
58 2
            : $this->deleteEntityCollection($collection);
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 367
    public function update(PersistentCollection $collection)
65
    {
66
        // This can never happen. One to many can only be inverse side.
67
        // For owning side one to many, it is required to have a join table,
68
        // then classifying it as a ManyToManyPersister.
69 367
        return;
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 2
    public function get(PersistentCollection $collection, $index)
76
    {
77 2
        $mapping = $collection->getMapping();
78
79 2
        if ( ! isset($mapping['indexBy'])) {
80
            throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
81
        }
82
83 2
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
84
85 2
        return $persister->load(
86
            [
87 2
                $mapping['mappedBy'] => $collection->getOwner(),
88 2
                $mapping['indexBy']  => $index
89
            ],
90 2
            null,
91 2
            $mapping,
92 2
            [],
93 2
            null,
94 2
            1
95
        );
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 10
    public function count(PersistentCollection $collection)
102
    {
103 10
        $mapping   = $collection->getMapping();
104 10
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
105
106
        // only works with single id identifier entities. Will throw an
107
        // exception in Entity Persisters if that is not the case for the
108
        // 'mappedBy' field.
109 10
        $criteria = new Criteria(Criteria::expr()->eq($mapping['mappedBy'], $collection->getOwner()));
110
111 10
        return $persister->count($criteria);
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117 5
    public function slice(PersistentCollection $collection, $offset, $length = null)
118
    {
119 5
        $mapping   = $collection->getMapping();
120 5
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
121
122 5
        return $persister->getOneToManyCollection($mapping, $collection->getOwner(), $offset, $length);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 4
    public function containsKey(PersistentCollection $collection, $key)
129
    {
130 4
        $mapping = $collection->getMapping();
131
132 4
        if ( ! isset($mapping['indexBy'])) {
133
            throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
134
        }
135
136 4
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
137
138
        // only works with single id identifier entities. Will throw an
139
        // exception in Entity Persisters if that is not the case for the
140
        // 'mappedBy' field.
141 4
        $criteria = new Criteria();
142
143 4
        $criteria->andWhere(Criteria::expr()->eq($mapping['mappedBy'], $collection->getOwner()));
144 4
        $criteria->andWhere(Criteria::expr()->eq($mapping['indexBy'], $key));
145
146 4
        return (bool) $persister->count($criteria);
147
    }
148
149
     /**
150
     * {@inheritdoc}
151
     */
152 9
    public function contains(PersistentCollection $collection, $element)
153
    {
154 9
        if ( ! $this->isValidEntityState($element)) {
155 3
            return false;
156
        }
157
158 7
        $mapping   = $collection->getMapping();
159 7
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
160
161
        // only works with single id identifier entities. Will throw an
162
        // exception in Entity Persisters if that is not the case for the
163
        // 'mappedBy' field.
164 7
        $criteria = new Criteria(Criteria::expr()->eq($mapping['mappedBy'], $collection->getOwner()));
165
166 7
        return $persister->exists($element, $criteria);
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
173
    {
174
        throw new \BadMethodCallException("Filtering a collection by Criteria is not supported by this CollectionPersister.");
175
    }
176
177
    /**
178
     * @param PersistentCollection $collection
179
     *
180
     * @return int
181
     *
182
     * @throws \Doctrine\DBAL\DBALException
183
     */
184 1
    private function deleteEntityCollection(PersistentCollection $collection)
185
    {
186 1
        $mapping     = $collection->getMapping();
187 1
        $identifier  = $this->uow->getEntityIdentifier($collection->getOwner());
188 1
        $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
189 1
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
190 1
        $columns     = [];
191 1
        $parameters  = [];
192
193 1
        foreach ($targetClass->associationMappings[$mapping['mappedBy']]['joinColumns'] as $joinColumn) {
0 ignored issues
show
Bug introduced by
Accessing associationMappings 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 1
            $columns[]    = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
195 1
            $parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn['referencedColumnName'])];
196
        }
197
198 1
        $statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
199 1
            . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
200
201 1
        return $this->conn->executeUpdate($statement, $parameters);
202
    }
203
204
    /**
205
     * Delete Class Table Inheritance entities.
206
     * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables.
207
     *
208
     * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =)
209
     *
210
     * @param PersistentCollection $collection
211
     *
212
     * @return int
213
     *
214
     * @throws \Doctrine\DBAL\DBALException
215
     */
216 1
    private function deleteJoinedEntityCollection(PersistentCollection $collection)
217
    {
218 1
        $mapping     = $collection->getMapping();
219 1
        $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
220 1
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
221 1
        $rootClass   = $this->em->getClassMetadata($targetClass->rootEntityName);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
222
223
        // 1) Build temporary table DDL
224 1
        $tempTable         = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName());
225 1
        $idColumnNames     = $rootClass->getIdentifierColumnNames();
0 ignored issues
show
Bug introduced by
The method getIdentifierColumnNames() 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

225
        /** @scrutinizer ignore-call */ 
226
        $idColumnNames     = $rootClass->getIdentifierColumnNames();

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...
226 1
        $idColumnList      = implode(', ', $idColumnNames);
227 1
        $columnDefinitions = [];
228
229 1
        foreach ($idColumnNames as $idColumnName) {
230 1
            $columnDefinitions[$idColumnName] = [
231 1
                'notnull' => true,
232 1
                'type'    => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $this->em)),
233
            ];
234
        }
235
236 1
        $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable
237 1
            . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')';
238
239 1
        $this->conn->executeUpdate($statement);
240
241
        // 2) Build insert table records into temporary table
242 1
        $query = $this->em->createQuery(
243 1
            ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames())
244 1
            . ' FROM ' . $targetClass->name . ' t0 WHERE t0.' . $mapping['mappedBy'] . ' = :owner'
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
245 1
        )->setParameter('owner', $collection->getOwner());
246
247 1
        $statement  = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . $query->getSQL();
0 ignored issues
show
Bug introduced by
Are you sure $query->getSQL() of type array 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

247
        $statement  = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . /** @scrutinizer ignore-type */ $query->getSQL();
Loading history...
248 1
        $parameters = array_values($sourceClass->getIdentifierValues($collection->getOwner()));
249 1
        $numDeleted = $this->conn->executeUpdate($statement, $parameters);
250
251
        // 3) Delete records on each table in the hierarchy
252 1
        $classNames = array_merge($targetClass->parentClasses, [$targetClass->name], $targetClass->subClasses);
0 ignored issues
show
Bug introduced by
Accessing subClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
Accessing parentClasses on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
253
254 1
        foreach (array_reverse($classNames) as $className) {
255 1
            $tableName = $this->quoteStrategy->getTableName($this->em->getClassMetadata($className), $this->platform);
256 1
            $statement = 'DELETE FROM ' . $tableName . ' WHERE (' . $idColumnList . ')'
257 1
                . ' IN (SELECT ' . $idColumnList . ' FROM ' . $tempTable . ')';
258
259 1
            $this->conn->executeUpdate($statement);
260
        }
261
262
        // 4) Drop temporary table
263 1
        $statement = $this->platform->getDropTemporaryTableSQL($tempTable);
264
265 1
        $this->conn->executeUpdate($statement);
266
267 1
        return $numDeleted;
268
    }
269
}
270