Completed
Push — master ( 45baa6...8d52b0 )
by Julián
01:49
created

RepositoryTrait::removeBy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
3
/*
4
 * doctrine-base-repositories (https://github.com/juliangut/doctrine-base-repositories).
5
 * Doctrine2 utility repositories.
6
 *
7
 * @license MIT
8
 * @link https://github.com/juliangut/doctrine-base-repositories
9
 * @author Julián Gutiérrez <[email protected]>
10
 */
11
12
declare(strict_types=1);
13
14
namespace Jgut\Doctrine\Repository;
15
16
use Doctrine\Common\Util\Inflector;
17
18
/**
19
 * Repository trait.
20
 *
21
 * @method mixed find()
22
 * @method mixed findAll()
23
 * @method mixed findBy()
24
 * @method mixed findOneBy()
25
 */
26
trait RepositoryTrait
27
{
28
    protected static $supportedMethods = ['findBy', 'findOneBy', 'findPaginatedBy', 'removeBy', 'removeOneBy'];
29
30
    /**
31
     * Auto flush changes.
32
     *
33
     * @var bool
34
     */
35
    protected $autoFlush = false;
36
37
    /**
38
     * New object factory.
39
     *
40
     * @var callable
41
     */
42
    protected $objectFactory;
43
44
    /**
45
     * Get automatic manager flushing.
46
     *
47
     * @return bool
48
     */
49
    public function isAutoFlush(): bool
50
    {
51
        return $this->autoFlush;
52
    }
53
54
    /**
55
     * Set automatic manager flushing.
56
     *
57
     * @param bool $autoFlush
58
     */
59
    public function setAutoFlush(bool $autoFlush = true)
60
    {
61
        $this->autoFlush = $autoFlush;
62
    }
63
64
    /**
65
     * Manager flush.
66
     */
67
    public function flush()
68
    {
69
        $this->getManager()->flush();
70
    }
71
72
    /**
73
     * Set object factory.
74
     *
75
     * @param callable $objectFactory
76
     */
77
    public function setObjectFactory(callable $objectFactory)
78
    {
79
        $this->objectFactory = $objectFactory;
80
    }
81
82
    /**
83
     * Find one object by a set of criteria or create a new one.
84
     *
85
     * @param array $criteria
86
     *
87
     * @throws \RuntimeException
88
     *
89
     * @return object
90
     */
91
    public function findOneByOrGetNew(array $criteria)
92
    {
93
        $object = $this->findOneBy($criteria);
0 ignored issues
show
Unused Code introduced by
The call to RepositoryTrait::findOneBy() has too many arguments starting with $criteria.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
94
95
        if ($object === null) {
96
            $object = $this->getNew();
97
        }
98
99
        return $object;
100
    }
101
102
    /**
103
     * Get a new managed object instance.
104
     *
105
     * @throws \RuntimeException
106
     *
107
     * @return object
108
     */
109
    public function getNew()
110
    {
111
        $className = $this->getClassName();
112
113
        if ($this->objectFactory === null) {
114
            return new $className();
115
        }
116
117
        $object = call_user_func($this->objectFactory);
118
119
        if (!$this->canBeManaged($object)) {
120
            throw new \RuntimeException(
121
                sprintf(
122
                    'Object factory must return an instance of %s. "%s" returned',
123
                    $className,
124
                    is_object($object) ? get_class($object) : gettype($object)
125
                )
126
            );
127
        }
128
129
        return $object;
130
    }
131
132
    /**
133
     * Add objects.
134
     *
135
     * @param object|object[]|\Traversable $objects
136
     * @param bool                         $flush
137
     *
138
     * @throws \InvalidArgumentException
139
     */
140
    public function add($objects, bool $flush = false)
141
    {
142
        $this->runManagerAction($objects, 'persist', $flush);
143
    }
144
145
    /**
146
     * Remove all objects.
147
     *
148
     * @param bool $flush
149
     */
150
    public function removeAll(bool $flush = false)
151
    {
152
        $this->runManagerAction($this->findAll(), 'remove', $flush);
153
    }
154
155
    /**
156
     * Remove object filtered by a set of criteria.
157
     *
158
     * @param array $criteria
159
     * @param bool  $flush
160
     */
161
    public function removeBy(array $criteria, bool $flush = false)
162
    {
163
        $this->runManagerAction($this->findBy($criteria), 'remove', $flush);
0 ignored issues
show
Unused Code introduced by
The call to RepositoryTrait::findBy() has too many arguments starting with $criteria.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
164
    }
165
166
    /**
167
     * Remove first object filtered by a set of criteria.
168
     *
169
     * @param array $criteria
170
     * @param bool  $flush
171
     */
172
    public function removeOneBy(array $criteria, bool $flush = false)
173
    {
174
        $this->runManagerAction($this->findOneBy($criteria), 'remove', $flush);
0 ignored issues
show
Unused Code introduced by
The call to RepositoryTrait::findOneBy() has too many arguments starting with $criteria.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
175
    }
176
177
    /**
178
     * Remove objects.
179
     *
180
     * @param object|object[]|\Traversable|string|int $objects
181
     * @param bool                                    $flush
182
     *
183
     * @throws \InvalidArgumentException
184
     */
185
    public function remove($objects, bool $flush = false)
186
    {
187
        if (!is_object($objects) && !is_array($objects) && !$objects instanceof \Traversable) {
188
            $objects = $this->find($objects);
0 ignored issues
show
Unused Code introduced by
The call to RepositoryTrait::find() has too many arguments starting with $objects.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
189
        }
190
191
        $this->runManagerAction($objects, 'remove', $flush);
192
    }
193
194
    /**
195
     * Refresh objects.
196
     *
197
     * @param object|object[]|\Traversable $objects
198
     *
199
     * @throws \InvalidArgumentException
200
     */
201
    public function refresh($objects)
202
    {
203
        $backupAutoFlush = $this->autoFlush;
204
205
        $this->autoFlush = false;
206
        $this->runManagerAction($objects, 'refresh', false);
207
208
        $this->autoFlush = $backupAutoFlush;
209
    }
210
211
    /**
212
     * Detach objects.
213
     *
214
     * @param object|object[]|\Traversable $objects
215
     *
216
     * @throws \InvalidArgumentException
217
     */
218
    public function detach($objects)
219
    {
220
        $backupAutoFlush = $this->autoFlush;
221
222
        $this->autoFlush = false;
223
        $this->runManagerAction($objects, 'detach', false);
224
225
        $this->autoFlush = $backupAutoFlush;
226
    }
227
228
    /**
229
     * Get all objects count.
230
     *
231
     * @return int
232
     */
233
    public function countAll(): int
234
    {
235
        return $this->countBy([]);
236
    }
237
238
    /**
239
     * Get object count filtered by a set of criteria.
240
     *
241
     * @param mixed $criteria
242
     *
243
     * @return int
244
     */
245
    abstract public function countBy($criteria): int;
246
247
    /**
248
     * Adds support for magic methods.
249
     *
250
     * @param string $method
251
     * @param array  $arguments
252
     *
253
     * @throws \BadMethodCallException
254
     *
255
     * @return mixed
256
     */
257
    public function __call($method, $arguments)
258
    {
259
        if (count($arguments) === 0) {
260
            throw new \BadMethodCallException(sprintf(
261
                'You need to call %s::%s with a parameter',
262
                $this->getClassName(),
263
                $method
264
            ));
265
        }
266
267
        foreach (static::$supportedMethods as $supportedMethod) {
268
            if (strpos($method, $supportedMethod) === 0) {
269
                $field = substr($method, strlen($supportedMethod));
270
                $method = substr($method, 0, strlen($supportedMethod));
271
272
                return $this->callSupportedMethod($method, Inflector::camelize($field), $arguments);
273
            }
274
        }
275
276
        throw new \BadMethodCallException(sprintf(
277
            'Undefined method "%s". Method call must start with one of "%s"!',
278
            $method,
279
            implode('", "', static::$supportedMethods)
280
        ));
281
    }
282
283
    /**
284
     * Internal method call.
285
     *
286
     * @param string $method
287
     * @param string $fieldName
288
     * @param array  $arguments
289
     *
290
     * @throws \BadMethodCallException
291
     *
292
     * @return mixed
293
     */
294
    protected function callSupportedMethod(string $method, string $fieldName, array $arguments)
295
    {
296
        $classMetadata = $this->getClassMetadata();
297
298
        if (!$classMetadata->hasField($fieldName) && !$classMetadata->hasAssociation($fieldName)) {
299
            throw new \BadMethodCallException(sprintf(
300
                'Invalid call to %s::%s. Field "%s" does not exist',
301
                $this->getClassName(),
302
                $method,
303
                $fieldName
304
            ));
305
        }
306
307
        // @codeCoverageIgnoreStart
308
        $parameters = array_merge(
309
            [$fieldName => $arguments[0]],
310
            array_slice($arguments, 1)
311
        );
312
313
        return call_user_func_array([$this, $method], $parameters);
314
        // @codeCoverageIgnoreEnd
315
    }
316
317
    /**
318
     * Run manager action.
319
     *
320
     * @param object|object[]|\Traversable $objects
321
     * @param string                       $action
322
     * @param bool                         $flush
323
     *
324
     * @throws \InvalidArgumentException
325
     */
326
    protected function runManagerAction($objects, string $action, bool $flush)
327
    {
328
        $manager = $this->getManager();
329
330
        if (!is_array($objects) && !$objects instanceof \Traversable) {
331
            $objects = array_filter([$objects]);
332
        }
333
334
        foreach ($objects as $object) {
335
            if (!$this->canBeManaged($object)) {
336
                throw new \InvalidArgumentException(
337
                    sprintf(
338
                        'Managed object must be a %s. "%s" given',
339
                        $this->getClassName(),
340
                        is_object($object) ? get_class($object) : gettype($object)
341
                    )
342
                );
343
            }
344
345
            $manager->$action($object);
346
        }
347
348
        $this->doFlush($objects, $flush instanceof \Traversable ? iterator_to_array($flush) : $flush);
0 ignored issues
show
Documentation introduced by
$objects is of type array|object<Traversable>, but the function expects a array<integer,object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $flush instanceof \Trave..._array($flush) : $flush can also be of type array; however, Jgut\Doctrine\Repository...ositoryTrait::doFlush() does only seem to accept boolean, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
349
    }
350
351
    /**
352
     * Flush managed objects.
353
     *
354
     * @param object[] $objects
355
     * @param bool     $flush
356
     */
357
    protected function doFlush(array $objects, bool $flush)
358
    {
359
        if ($flush || $this->autoFlush) {
360
            $this->getManager()->flush($objects);
361
        }
362
    }
363
364
    /**
365
     * Check if the object is of the proper type.
366
     *
367
     * @param object $object
368
     *
369
     * @return bool
370
     */
371
    protected function canBeManaged($object): bool
372
    {
373
        $managedClass = $this->getClassName();
374
375
        return $object instanceof $managedClass;
376
    }
377
378
    /**
379
     * Returns the fully qualified class name of the objects managed by the repository.
380
     *
381
     * @return string
382
     */
383
    abstract public function getClassName(): string;
384
385
    /**
386
     * Get object manager.
387
     *
388
     * @return \Doctrine\Common\Persistence\ObjectManager
389
     */
390
    abstract protected function getManager();
391
392
    /**
393
     * Get class metadata.
394
     *
395
     * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata
396
     */
397
    abstract protected function getClassMetadata();
398
}
399