Completed
Push — master ( 709a83...d469df )
by Julián
10:11
created

RepositoryTrait::flushObjects()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
rs 9.4285
cc 3
eloc 3
nc 2
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
     * @param int   $lockMode
87
     * @param int   $lockVersion
0 ignored issues
show
Documentation introduced by
Should the type for parameter $lockVersion not be null|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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