Completed
Push — master ( f780f6...450221 )
by Julián
01:54
created

RepositoryTrait::getNew()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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