Completed
Push — master ( 298f70...630643 )
by Julián
01:48
created

RepositoryTrait::runManagerAction()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
c 0
b 0
f 0
rs 8.5125
cc 6
eloc 13
nc 6
nop 3
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
    /**
29
     * Supported magic methods.
30
     *
31
     * @var array
32
     */
33
    protected static $supportedMethods = [
34
        'findBy',
35
        'findOneBy',
36
        'findPaginatedBy',
37
        'removeBy',
38
        'removeOneBy',
39
        'countBy',
40
    ];
41
42
    /**
43
     * Auto flush changes.
44
     *
45
     * @var bool
46
     */
47
    protected $autoFlush = false;
48
49
    /**
50
     * New object factory.
51
     *
52
     * @var callable
53
     */
54
    protected $objectFactory;
55
56
    /**
57
     * Get automatic manager flushing.
58
     *
59
     * @return bool
60
     */
61
    public function isAutoFlush(): bool
62
    {
63
        return $this->autoFlush;
64
    }
65
66
    /**
67
     * Set automatic manager flushing.
68
     *
69
     * @param bool $autoFlush
70
     */
71
    public function setAutoFlush(bool $autoFlush = true)
72
    {
73
        $this->autoFlush = $autoFlush;
74
    }
75
76
    /**
77
     * Manager flush.
78
     */
79
    public function flush()
80
    {
81
        $this->getManager()->flush();
82
    }
83
84
    /**
85
     * Find one object by a set of criteria or create a new one.
86
     *
87
     * @param array $criteria
88
     *
89
     * @throws \RuntimeException
90
     *
91
     * @return object
92
     */
93
    public function findOneByOrGetNew(array $criteria)
94
    {
95
        $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...
96
97
        if ($object === null) {
98
            $object = $this->getNew();
99
        }
100
101
        return $object;
102
    }
103
104
    /**
105
     * Get a new managed object instance.
106
     *
107
     * @throws \RuntimeException
108
     *
109
     * @return object
110
     */
111
    public function getNew()
112
    {
113
        $object = call_user_func($this->getObjectFactory());
114
115
        if (!$this->canBeManaged($object)) {
116
            throw new \RuntimeException(
117
                sprintf(
118
                    'Object factory must return an instance of %s. "%s" returned',
119
                    $this->getClassName(),
120
                    is_object($object) ? get_class($object) : gettype($object)
121
                )
122
            );
123
        }
124
125
        return $object;
126
    }
127
128
    /**
129
     * Get object factory.
130
     *
131
     * @return callable
132
     */
133
    private function getObjectFactory(): callable
134
    {
135
        if ($this->objectFactory === null) {
136
            $className = $this->getClassName();
137
138
            $this->objectFactory = function () use ($className) {
139
                return new $className();
140
            };
141
        }
142
143
        return $this->objectFactory;
144
    }
145
146
    /**
147
     * Set object factory.
148
     *
149
     * @param callable $objectFactory
150
     */
151
    public function setObjectFactory(callable $objectFactory)
152
    {
153
        $this->objectFactory = $objectFactory;
154
    }
155
156
    /**
157
     * Add objects.
158
     *
159
     * @param object|object[]|\Traversable $objects
160
     * @param bool                         $flush
161
     *
162
     * @throws \InvalidArgumentException
163
     */
164
    public function add($objects, bool $flush = false)
165
    {
166
        $this->runManagerAction('persist', $objects, $flush);
167
    }
168
169
    /**
170
     * Remove all objects.
171
     *
172
     * @param bool $flush
173
     */
174
    public function removeAll(bool $flush = false)
175
    {
176
        $this->runManagerAction('remove', $this->findAll(), $flush);
177
    }
178
179
    /**
180
     * Remove object filtered by a set of criteria.
181
     *
182
     * @param array $criteria
183
     * @param bool  $flush
184
     */
185
    public function removeBy(array $criteria, bool $flush = false)
186
    {
187
        $this->runManagerAction('remove', $this->findBy($criteria), $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...
188
    }
189
190
    /**
191
     * Remove first object filtered by a set of criteria.
192
     *
193
     * @param array $criteria
194
     * @param bool  $flush
195
     */
196
    public function removeOneBy(array $criteria, bool $flush = false)
197
    {
198
        $this->runManagerAction('remove', $this->findOneBy($criteria), $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...
199
    }
200
201
    /**
202
     * Remove objects.
203
     *
204
     * @param object|object[]|\Traversable|string|int $objects
205
     * @param bool                                    $flush
206
     *
207
     * @throws \InvalidArgumentException
208
     */
209
    public function remove($objects, bool $flush = false)
210
    {
211
        if (!is_object($objects) && !is_array($objects) && !$objects instanceof \Traversable) {
212
            $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...
213
        }
214
215
        $this->runManagerAction('remove', $objects, $flush);
216
    }
217
218
    /**
219
     * Refresh objects.
220
     *
221
     * @param object|object[]|\Traversable $objects
222
     *
223
     * @throws \InvalidArgumentException
224
     */
225
    public function refresh($objects)
226
    {
227
        $backupAutoFlush = $this->autoFlush;
228
229
        $this->autoFlush = false;
230
        $this->runManagerAction('refresh', $objects, false);
231
232
        $this->autoFlush = $backupAutoFlush;
233
    }
234
235
    /**
236
     * Detach objects.
237
     *
238
     * @param object|object[]|\Traversable $objects
239
     *
240
     * @throws \InvalidArgumentException
241
     */
242
    public function detach($objects)
243
    {
244
        $backupAutoFlush = $this->autoFlush;
245
246
        $this->autoFlush = false;
247
        $this->runManagerAction('detach', $objects, false);
248
249
        $this->autoFlush = $backupAutoFlush;
250
    }
251
252
    /**
253
     * Get all objects count.
254
     *
255
     * @return int
256
     */
257
    public function countAll(): int
258
    {
259
        return $this->countBy([]);
260
    }
261
262
    /**
263
     * Get object count filtered by a set of criteria.
264
     *
265
     * @param mixed $criteria
266
     *
267
     * @return int
268
     */
269
    abstract public function countBy($criteria): int;
270
271
    /**
272
     * Adds support for magic methods.
273
     *
274
     * @param string $method
275
     * @param array  $arguments
276
     *
277
     * @throws \BadMethodCallException
278
     *
279
     * @return mixed
280
     */
281
    public function __call($method, $arguments)
282
    {
283
        if (count($arguments) === 0) {
284
            throw new \BadMethodCallException(sprintf(
285
                'You need to call %s::%s with a parameter',
286
                $this->getClassName(),
287
                $method
288
            ));
289
        }
290
291
        foreach (static::$supportedMethods as $supportedMethod) {
292
            if (strpos($method, $supportedMethod) === 0) {
293
                if ($supportedMethod === 'findOneBy' && preg_match('/OrGetNew$/', $method)) {
294
                    $field = substr($method, strlen($supportedMethod), -8);
295
                    $method = 'findOneByOrGetNew';
296
                } else {
297
                    $field = substr($method, strlen($supportedMethod));
298
                    $method = $supportedMethod;
299
                }
300
301
                return $this->callSupportedMethod($method, Inflector::camelize($field), $arguments);
302
            }
303
        }
304
305
        throw new \BadMethodCallException(sprintf(
306
            'Undefined method "%s". Method call must start with one of "%s"!',
307
            $method,
308
            implode('", "', static::$supportedMethods)
309
        ));
310
    }
311
312
    /**
313
     * Internal method call.
314
     *
315
     * @param string $method
316
     * @param string $fieldName
317
     * @param array  $arguments
318
     *
319
     * @throws \BadMethodCallException
320
     *
321
     * @return mixed
322
     */
323
    protected function callSupportedMethod(string $method, string $fieldName, array $arguments)
324
    {
325
        $classMetadata = $this->getClassMetadata();
326
327
        if (!$classMetadata->hasField($fieldName) && !$classMetadata->hasAssociation($fieldName)) {
328
            throw new \BadMethodCallException(sprintf(
329
                'Invalid call to %s::%s. Field "%s" does not exist',
330
                $this->getClassName(),
331
                $method,
332
                $fieldName
333
            ));
334
        }
335
336
        // @codeCoverageIgnoreStart
337
        $parameters = array_merge(
338
            [$fieldName => $arguments[0]],
339
            array_slice($arguments, 1)
340
        );
341
342
        return call_user_func_array([$this, $method], $parameters);
343
        // @codeCoverageIgnoreEnd
344
    }
345
346
    /**
347
     * Run manager action.
348
     *
349
     * @param string                       $action
350
     * @param object|object[]|\Traversable $objects
351
     * @param bool                         $flush
352
     *
353
     * @throws \InvalidArgumentException
354
     */
355
    protected function runManagerAction(string $action, $objects, bool $flush)
356
    {
357
        $manager = $this->getManager();
358
359
        if (!is_array($objects) && !$objects instanceof \Traversable) {
360
            $objects = array_filter([$objects]);
361
        }
362
363
        foreach ($objects as $object) {
364
            if (!$this->canBeManaged($object)) {
365
                throw new \InvalidArgumentException(
366
                    sprintf(
367
                        'Managed object must be a %s. "%s" given',
368
                        $this->getClassName(),
369
                        is_object($object) ? get_class($object) : gettype($object)
370
                    )
371
                );
372
            }
373
374
            $manager->$action($object);
375
        }
376
377
        $this->flushObjects($objects, $flush);
0 ignored issues
show
Bug introduced by
It seems like $objects defined by array_filter(array($objects)) on line 360 can also be of type array; however, Jgut\Doctrine\Repository...ryTrait::flushObjects() does only seem to accept object|array<integer,object>, 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...
378
    }
379
380
    /**
381
     * Flush managed objects.
382
     *
383
     * @param object|object[]|\Traversable $objects
384
     * @param bool                         $flush
385
     */
386
    protected function flushObjects($objects, bool $flush)
387
    {
388
        if ($objects instanceof \Traversable) {
389
            $objects = iterator_to_array($objects);
390
        }
391
392
        if ($flush || $this->autoFlush) {
393
            $this->getManager()->flush($objects);
394
        }
395
    }
396
397
    /**
398
     * Check if the object is of the proper type.
399
     *
400
     * @param object $object
401
     *
402
     * @return bool
403
     */
404
    protected function canBeManaged($object): bool
405
    {
406
        $managedClass = $this->getClassName();
407
408
        return $object instanceof $managedClass;
409
    }
410
411
    /**
412
     * Returns the fully qualified class name of the objects managed by the repository.
413
     *
414
     * @return string
415
     */
416
    abstract public function getClassName(): string;
417
418
    /**
419
     * Get object manager.
420
     *
421
     * @return \Doctrine\Common\Persistence\ObjectManager
422
     */
423
    abstract protected function getManager();
424
425
    /**
426
     * Get class metadata.
427
     *
428
     * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata
429
     */
430
    abstract protected function getClassMetadata();
431
}
432