RepositoryTrait::callSupportedMethod()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
cc 3
eloc 12
nc 2
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($id, $lockMode = null, $lockVersion = null)
22
 * @method mixed findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
23
 * @method mixed findOneBy(array $criteria, array $orderBy = null)
24
 * @method mixed findAll()
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
     * Methods that support exception throwing on fail.
44
     *
45
     * @var array
46
     */
47
    protected static $falibleMethods = [
48
        'findBy',
49
        'findOneBy',
50
        'findPaginatedBy',
51
    ];
52
53
    /**
54
     * Auto flush changes.
55
     *
56
     * @var bool
57
     */
58
    protected $autoFlush = false;
59
60
    /**
61
     * New object factory.
62
     *
63
     * @var callable
64
     */
65
    protected $objectFactory;
66
67
    /**
68
     * Get automatic manager flushing.
69
     *
70
     * @return bool
71
     */
72
    public function isAutoFlush(): bool
73
    {
74
        return $this->autoFlush;
75
    }
76
77
    /**
78
     * Set automatic manager flushing.
79
     *
80
     * @param bool $autoFlush
81
     */
82
    public function setAutoFlush(bool $autoFlush = true)
83
    {
84
        $this->autoFlush = $autoFlush;
85
    }
86
87
    /**
88
     * Manager flush.
89
     */
90
    public function flush()
91
    {
92
        $this->getManager()->flush();
93
    }
94
95
    /**
96
     * Find elements or throw an exception if none found.
97
     *
98
     * @param array      $criteria
99
     * @param array|null $orderBy
100
     * @param int|null   $limit
101
     * @param int|null   $offset
102
     *
103
     * @throws FindException
104
     *
105
     * @return object[]
106
     */
107
    public function findByOrFail(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
108
    {
109
        $objects = $this->findBy($criteria, $orderBy, $limit, $offset);
0 ignored issues
show
Bug introduced by
It seems like $orderBy defined by parameter $orderBy on line 107 can also be of type null; however, Jgut\Doctrine\Repository\RepositoryTrait::findBy() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
110
111
        if (\count($objects) === 0) {
112
            throw new FindException('FindBy did not return any results');
113
        }
114
115
        return $objects;
116
    }
117
118
    /**
119
     * Find elements or throw an exception if none found.
120
     *
121
     * @param array $criteria
122
     *
123
     * @throws FindException
124
     *
125
     * @return object
126
     */
127
    public function findOneByOrFail(array $criteria)
128
    {
129
        $object = $this->findOneBy($criteria);
130
131
        if ($object === null) {
132
            throw new FindException('FindOneBy did not return any results');
133
        }
134
135
        return $object;
136
    }
137
138
    /**
139
     * Find one object by a set of criteria or create a new one.
140
     *
141
     * @param array $criteria
142
     *
143
     * @throws \RuntimeException
144
     *
145
     * @return object
146
     */
147
    public function findOneByOrGetNew(array $criteria)
148
    {
149
        $object = $this->findOneBy($criteria);
150
151
        if ($object === null) {
152
            $object = $this->getNew();
153
        }
154
155
        return $object;
156
    }
157
158
    /**
159
     * Get a new managed object instance.
160
     *
161
     * @throws \RuntimeException
162
     *
163
     * @return object
164
     */
165
    public function getNew()
166
    {
167
        $object = \call_user_func($this->getObjectFactory());
168
169
        if (!$this->canBeManaged($object)) {
170
            throw new \RuntimeException(
171
                \sprintf(
172
                    'Object factory must return an instance of %s. "%s" returned',
173
                    $this->getClassName(),
174
                    \is_object($object) ? \get_class($object) : \gettype($object)
175
                )
176
            );
177
        }
178
179
        return $object;
180
    }
181
182
    /**
183
     * Get object factory.
184
     *
185
     * @return callable
186
     */
187
    private function getObjectFactory(): callable
188
    {
189
        if ($this->objectFactory === null) {
190
            $className = $this->getClassName();
191
192
            $this->objectFactory = function () use ($className) {
193
                return new $className();
194
            };
195
        }
196
197
        return $this->objectFactory;
198
    }
199
200
    /**
201
     * Set object factory.
202
     *
203
     * @param callable $objectFactory
204
     */
205
    public function setObjectFactory(callable $objectFactory)
206
    {
207
        $this->objectFactory = $objectFactory;
208
    }
209
210
    /**
211
     * Add objects.
212
     *
213
     * @param object|iterable $objects
214
     * @param bool            $flush
215
     *
216
     * @throws \InvalidArgumentException
217
     */
218
    public function add($objects, bool $flush = false)
219
    {
220
        $this->runManagerAction('persist', $objects, $flush);
221
    }
222
223
    /**
224
     * Remove all objects.
225
     *
226
     * @param bool $flush
227
     */
228
    public function removeAll(bool $flush = false)
229
    {
230
        $this->runManagerAction('remove', $this->findAll(), $flush);
231
    }
232
233
    /**
234
     * Remove object filtered by a set of criteria.
235
     *
236
     * @param array $criteria
237
     * @param bool  $flush
238
     */
239
    public function removeBy(array $criteria, bool $flush = false)
240
    {
241
        $this->runManagerAction('remove', $this->findBy($criteria), $flush);
242
    }
243
244
    /**
245
     * Remove first object filtered by a set of criteria.
246
     *
247
     * @param array $criteria
248
     * @param bool  $flush
249
     */
250
    public function removeOneBy(array $criteria, bool $flush = false)
251
    {
252
        $this->runManagerAction('remove', $this->findOneBy($criteria), $flush);
253
    }
254
255
    /**
256
     * Remove objects.
257
     *
258
     * @param object|iterable|string|int $objects
259
     * @param bool                       $flush
260
     *
261
     * @throws \InvalidArgumentException
262
     */
263
    public function remove($objects, bool $flush = false)
264
    {
265
        if (!\is_object($objects) && !is_iterable($objects)) {
266
            $objects = $this->find($objects);
267
        }
268
269
        $this->runManagerAction('remove', $objects, $flush);
270
    }
271
272
    /**
273
     * Refresh objects.
274
     *
275
     * @param object|iterable $objects
276
     *
277
     * @throws \InvalidArgumentException
278
     */
279
    public function refresh($objects)
280
    {
281
        $backupAutoFlush = $this->autoFlush;
282
283
        $this->autoFlush = false;
284
        $this->runManagerAction('refresh', $objects, false);
285
286
        $this->autoFlush = $backupAutoFlush;
287
    }
288
289
    /**
290
     * Detach objects.
291
     *
292
     * @param object|iterable $objects
293
     *
294
     * @throws \InvalidArgumentException
295
     */
296
    public function detach($objects)
297
    {
298
        $backupAutoFlush = $this->autoFlush;
299
300
        $this->autoFlush = false;
301
        $this->runManagerAction('detach', $objects, false);
302
303
        $this->autoFlush = $backupAutoFlush;
304
    }
305
306
    /**
307
     * Get all objects count.
308
     *
309
     * @return int
310
     */
311
    public function countAll(): int
312
    {
313
        return $this->countBy([]);
314
    }
315
316
    /**
317
     * Get object count filtered by a set of criteria.
318
     *
319
     * @param mixed $criteria
320
     *
321
     * @return int
322
     */
323
    abstract public function countBy($criteria): int;
324
325
    /**
326
     * Adds support for magic methods.
327
     *
328
     * @param string $method
329
     * @param array  $arguments
330
     *
331
     * @throws \BadMethodCallException
332
     *
333
     * @return mixed
334
     */
335
    public function __call($method, $arguments)
336
    {
337
        if (\count($arguments) === 0) {
338
            throw new \BadMethodCallException(\sprintf(
339
                'You need to call %s::%s with a parameter',
340
                $this->getClassName(),
341
                $method
342
            ));
343
        }
344
345
        $baseMethod = $this->getSupportedMethod($method);
346
347
        if (\in_array($baseMethod, static::$falibleMethods, true) && \preg_match('/OrFail$/', $method)) {
348
            $field = \substr($method, \strlen($baseMethod), -6);
349
            $method = $baseMethod . 'OrFail';
350
        } elseif ($baseMethod === 'findOneBy' && \preg_match('/OrGetNew$/', $method)) {
351
            $field = \substr($method, \strlen($baseMethod), -8);
352
            $method = 'findOneByOrGetNew';
353
        } else {
354
            $field = \substr($method, \strlen($baseMethod));
355
            $method = $baseMethod;
356
        }
357
358
        return $this->callSupportedMethod($method, Inflector::camelize($field), $arguments);
359
    }
360
361
    /**
362
     * Get supported magic method.
363
     *
364
     * @param string $method
365
     *
366
     * @throws \BadMethodCallException
367
     *
368
     * @return string
369
     */
370
    private function getSupportedMethod(string $method): string
371
    {
372
        foreach (static::$supportedMethods as $supportedMethod) {
373
            if (\strpos($method, $supportedMethod) === 0) {
374
                return $supportedMethod;
375
            }
376
        }
377
378
        throw new \BadMethodCallException(\sprintf(
379
            'Undefined method "%s". Method call must start with one of "%s"!',
380
            $method,
381
            \implode('", "', static::$supportedMethods)
382
        ));
383
    }
384
385
    /**
386
     * Internal method call.
387
     *
388
     * @param string $method
389
     * @param string $fieldName
390
     * @param array  $arguments
391
     *
392
     * @throws \BadMethodCallException
393
     *
394
     * @return mixed
395
     */
396
    protected function callSupportedMethod(string $method, string $fieldName, array $arguments)
397
    {
398
        $classMetadata = $this->getClassMetadata();
399
400
        if (!$classMetadata->hasField($fieldName) && !$classMetadata->hasAssociation($fieldName)) {
401
            throw new \BadMethodCallException(\sprintf(
402
                'Invalid call to %s::%s. Field "%s" does not exist',
403
                $this->getClassName(),
404
                $method,
405
                $fieldName
406
            ));
407
        }
408
409
        // @codeCoverageIgnoreStart
410
        $parameters = \array_merge(
411
            [$fieldName => $arguments[0]],
412
            \array_slice($arguments, 1)
413
        );
414
415
        return \call_user_func_array([$this, $method], $parameters);
416
        // @codeCoverageIgnoreEnd
417
    }
418
419
    /**
420
     * Run manager action.
421
     *
422
     * @param string          $action
423
     * @param object|iterable $objects
424
     * @param bool            $flush
425
     *
426
     * @throws \InvalidArgumentException
427
     */
428
    protected function runManagerAction(string $action, $objects, bool $flush)
429
    {
430
        $manager = $this->getManager();
431
432
        if (!is_iterable($objects)) {
433
            $objects = \array_filter([$objects]);
434
        }
435
436
        foreach ($objects as $object) {
437
            if (!$this->canBeManaged($object)) {
438
                throw new \InvalidArgumentException(
439
                    \sprintf(
440
                        'Managed object must be a %s. "%s" given',
441
                        $this->getClassName(),
442
                        \is_object($object) ? \get_class($object) : \gettype($object)
443
                    )
444
                );
445
            }
446
447
            $manager->$action($object);
448
        }
449
450
        // @codeCoverageIgnoreStart
451
        if ($objects instanceof \Traversable) {
452
            $objects = \iterator_to_array($objects);
453
        }
454
        // @codeCoverageIgnoreEnd
455
456
        $this->flushObjects($objects, $flush);
0 ignored issues
show
Bug introduced by
It seems like $objects defined by parameter $objects on line 428 can also be of type object; however, Jgut\Doctrine\Repository...ryTrait::flushObjects() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
457
    }
458
459
    /**
460
     * Flush managed objects.
461
     *
462
     * @param array $objects
463
     * @param bool  $flush
464
     */
465
    protected function flushObjects(array $objects, bool $flush)
466
    {
467
        if ($flush || $this->autoFlush) {
468
            $this->getManager()->flush($objects);
469
        }
470
    }
471
472
    /**
473
     * Check if the object is of the proper type.
474
     *
475
     * @param object $object
476
     *
477
     * @return bool
478
     */
479
    protected function canBeManaged($object): bool
480
    {
481
        $managedClass = $this->getClassName();
482
483
        return $object instanceof $managedClass;
484
    }
485
486
    /**
487
     * Returns the fully qualified class name of the objects managed by the repository.
488
     *
489
     * @return string
490
     */
491
    abstract public function getClassName(): string;
492
493
    /**
494
     * Get object manager.
495
     *
496
     * @return \Doctrine\Common\Persistence\ObjectManager
497
     */
498
    abstract protected function getManager();
499
500
    /**
501
     * Get class metadata.
502
     *
503
     * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata
504
     */
505
    abstract protected function getClassMetadata();
506
}
507