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