1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Zenstruck\Foundry\Proxy; |
4
|
|
|
|
5
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
6
|
|
|
use Zenstruck\Foundry\Configuration; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Generates model proxies to autorefresh property values |
10
|
|
|
* from the database. |
11
|
|
|
* |
12
|
|
|
* @author Wouter de Jong <[email protected]> |
13
|
|
|
*/ |
14
|
|
|
class ProxyGenerator |
15
|
|
|
{ |
16
|
|
|
/** @var Configuration */ |
17
|
|
|
private $configuration; |
18
|
|
|
/** @var ValueReplacingAccessInterceptorValueHolderFactory */ |
19
|
|
|
private $factory; |
20
|
|
|
|
21
|
|
|
public function __construct(Configuration $configuration, ?ValueReplacingAccessInterceptorValueHolderFactory $factory = null) |
22
|
|
|
{ |
23
|
|
|
$this->configuration = $configuration; |
24
|
|
|
$this->factory = $factory ?? new ValueReplacingAccessInterceptorValueHolderFactory(); |
25
|
|
|
} |
26
|
|
|
|
27
|
|
|
public function generate(object $object, array $methods = []): object |
28
|
|
|
{ |
29
|
|
|
$objectManager = $this->configuration->objectManagerFor(\get_class($object)); |
30
|
|
|
$interceptor = function(object $proxy, object $model) use ($objectManager): void { |
31
|
|
|
$autoRefreshEnabled = \property_exists($model, '_foundry_autoRefresh') ? $model->_foundry_autoRefresh : true; |
32
|
|
|
if (!$autoRefreshEnabled) { |
33
|
|
|
return; |
34
|
|
|
} |
35
|
|
|
|
36
|
|
|
$modelClassMetadata = $objectManager->getClassMetadata(\get_class($model)); |
37
|
|
|
|
38
|
|
|
// only check for changes if the object is managed in the current om |
39
|
|
|
if ($objectManager instanceof EntityManagerInterface && $objectManager->contains($model)) { |
40
|
|
|
// cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed |
41
|
|
|
$objectManager->getUnitOfWork()->computeChangeSet($modelClassMetadata, $model); |
42
|
|
|
|
43
|
|
|
if (!empty($objectManager->getUnitOfWork()->getEntityChangeSet($model))) { |
44
|
|
|
throw new \RuntimeException(\sprintf('Cannot auto refresh "%s" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing (see https://github.com/zenstruck/foundry#auto-refresh for details).', \get_class($model))); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
$objectManager->refresh($model); |
48
|
|
|
|
49
|
|
|
return; |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
// refetch the model as it's no longer managed |
53
|
|
|
$modelId = $modelClassMetadata->getIdentifierValues($model); |
54
|
|
|
if (!$modelId) { |
|
|
|
|
55
|
|
|
// TODO: throw exception? |
56
|
|
|
return; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
$proxy->setWrappedValueHolder($objectManager->find(\get_class($model), $modelId)); |
60
|
|
|
}; |
61
|
|
|
|
62
|
|
|
$methodsToIntercept = $this->filterGlob(\get_class_methods($object), $methods); |
63
|
|
|
|
64
|
|
|
return $this->factory->createProxy($object, \array_fill_keys($methodsToIntercept, $interceptor)); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
private function filterGlob(array $objectMethods, array $methodFilter): array |
68
|
|
|
{ |
69
|
|
|
$methods = []; |
70
|
|
|
if (!$methodFilter) { |
|
|
|
|
71
|
|
|
$methods = $objectMethods; |
72
|
|
|
} else { |
73
|
|
|
foreach ($methodFilter as $filter) { |
74
|
|
|
// no *, assume filter is an exact match and no glob |
75
|
|
|
if (false === \mb_strpos($filter, '*')) { |
76
|
|
|
$methods[] = $filter; |
77
|
|
|
continue; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
// transform glob (e.g. "get*") into a regex |
81
|
|
|
$methodNameRegex = '/^'.\str_replace('*', '[[:alnum:]]+', $filter).'$/'; |
82
|
|
|
$methods = \array_merge($methods, \array_filter($objectMethods, function($objectMethod) use ($methodNameRegex) { |
83
|
|
|
return 1 !== \preg_match($methodNameRegex, $objectMethod); |
84
|
|
|
})); |
85
|
|
|
} |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
return \array_unique($methods); |
89
|
|
|
} |
90
|
|
|
} |
91
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.