Completed
Push — master ( 630643...821f3d )
by Julián
01:49
created

RepositoryTrait::getSupportedMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 1
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
        $baseMethod = $this->getSupportedMethod($method);
292
293
        if ($baseMethod === 'findOneBy' && preg_match('/OrGetNew$/', $method)) {
294
            $field = substr($method, strlen($baseMethod), -8);
295
            $method = 'findOneByOrGetNew';
296
        } else {
297
            $field = substr($method, strlen($baseMethod));
298
            $method = $baseMethod;
299
        }
300
301
        return $this->callSupportedMethod($method, Inflector::camelize($field), $arguments);
302
    }
303
304
    /**
305
     * Get supported magic method.
306
     *
307
     * @param string $method
308
     *
309
     * @throws \BadMethodCallException
310
     *
311
     * @return string
312
     */
313
    private function getSupportedMethod(string $method): string
314
    {
315
        foreach (static::$supportedMethods as $supportedMethod) {
316
            if (strpos($method, $supportedMethod) === 0) {
317
                return $supportedMethod;
318
            }
319
        }
320
321
        throw new \BadMethodCallException(sprintf(
322
            'Undefined method "%s". Method call must start with one of "%s"!',
323
            $method,
324
            implode('", "', static::$supportedMethods)
325
        ));
326
    }
327
328
    /**
329
     * Internal method call.
330
     *
331
     * @param string $method
332
     * @param string $fieldName
333
     * @param array  $arguments
334
     *
335
     * @throws \BadMethodCallException
336
     *
337
     * @return mixed
338
     */
339
    protected function callSupportedMethod(string $method, string $fieldName, array $arguments)
340
    {
341
        $classMetadata = $this->getClassMetadata();
342
343
        if (!$classMetadata->hasField($fieldName) && !$classMetadata->hasAssociation($fieldName)) {
344
            throw new \BadMethodCallException(sprintf(
345
                'Invalid call to %s::%s. Field "%s" does not exist',
346
                $this->getClassName(),
347
                $method,
348
                $fieldName
349
            ));
350
        }
351
352
        // @codeCoverageIgnoreStart
353
        $parameters = array_merge(
354
            [$fieldName => $arguments[0]],
355
            array_slice($arguments, 1)
356
        );
357
358
        return call_user_func_array([$this, $method], $parameters);
359
        // @codeCoverageIgnoreEnd
360
    }
361
362
    /**
363
     * Run manager action.
364
     *
365
     * @param string                       $action
366
     * @param object|object[]|\Traversable $objects
367
     * @param bool                         $flush
368
     *
369
     * @throws \InvalidArgumentException
370
     */
371
    protected function runManagerAction(string $action, $objects, bool $flush)
372
    {
373
        $manager = $this->getManager();
374
375
        if (!$this->isTraversable($objects)) {
376
            $objects = array_filter([$objects]);
377
        }
378
379
        foreach ($objects as $object) {
380
            if (!$this->canBeManaged($object)) {
381
                throw new \InvalidArgumentException(
382
                    sprintf(
383
                        'Managed object must be a %s. "%s" given',
384
                        $this->getClassName(),
385
                        is_object($object) ? get_class($object) : gettype($object)
386
                    )
387
                );
388
            }
389
390
            $manager->$action($object);
391
        }
392
393
        $this->flushObjects($objects, $flush);
0 ignored issues
show
Bug introduced by
It seems like $objects defined by array_filter(array($objects)) on line 376 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...
394
    }
395
396
    /**
397
     * Flush managed objects.
398
     *
399
     * @param object|object[]|\Traversable $objects
400
     * @param bool                         $flush
401
     */
402
    protected function flushObjects($objects, bool $flush)
403
    {
404
        if ($flush || $this->autoFlush) {
405
            // @codeCoverageIgnoreStart
406
            if ($objects instanceof \Traversable) {
407
                $objects = iterator_to_array($objects);
408
            }
409
            // @codeCoverageIgnoreEnd
410
411
            $this->getManager()->flush($objects);
412
        }
413
    }
414
415
    /**
416
     * Check if the object is of the proper type.
417
     *
418
     * @param object $object
419
     *
420
     * @return bool
421
     */
422
    protected function canBeManaged($object): bool
423
    {
424
        $managedClass = $this->getClassName();
425
426
        return $object instanceof $managedClass;
427
    }
428
429
    /**
430
     * Returns the fully qualified class name of the objects managed by the repository.
431
     *
432
     * @return string
433
     */
434
    abstract public function getClassName(): string;
435
436
    /**
437
     * Get object manager.
438
     *
439
     * @return \Doctrine\Common\Persistence\ObjectManager
440
     */
441
    abstract protected function getManager();
442
443
    /**
444
     * Get class metadata.
445
     *
446
     * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata
447
     */
448
    abstract protected function getClassMetadata();
449
450
    /**
451
     * Is traversable.
452
     *
453
     * @param mixed $object
454
     *
455
     * @return bool
456
     */
457
    private function isTraversable($object): bool
458
    {
459
        return is_array($object) || $object instanceof \Traversable;
460
    }
461
}
462