• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / orm / 23255296146

18 Mar 2026 04:24PM UTC coverage: 47.104%. Remained the same
23255296146

push

github

BenMorel
Avoid \Exception that somehow confuses ECS

1 of 5 new or added lines in 1 file covered. (20.0%)

402 existing lines in 24 files now uncovered.

553 of 1174 relevant lines covered (47.1%)

10.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.33
/src/ObjectFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\ORM;
6

7
use Closure;
8
use InvalidArgumentException;
9
use ReflectionClass;
10
use ReflectionException;
11
use ReflectionNamedType;
12
use ReflectionProperty;
13

14
use function get_object_vars;
15
use function gettype;
16
use function is_array;
17
use function sprintf;
18

19
/**
20
 * Creates, reads and writes persistent objects.
21
 *
22
 * Important note: this does not aim to support private properties in parent classes.
23
 *
24
 * This class is performance sensitive, and uses techniques benchmarked here:
25
 * https://gist.github.com/BenMorel/9a920538862e4df0d7041f8812f069e5
26
 */
27
class ObjectFactory
28
{
29
    /**
30
     * A map of fully qualified class name to ReflectionClass instance.
31
     *
32
     * @var array<class-string, ReflectionClass>
33
     */
34
    private array $classes = [];
35

36
    /**
37
     * A map of full qualified class name to map of property name to Closure.
38
     *
39
     * Each closure converts a property value to the correct type.
40
     *
41
     * @var array<class-string, array<string, Closure>>
42
     */
43
    private array $propertyConverters = [];
44

45
    /**
46
     * Instantiates an empty object, without calling the class constructor.
47
     *
48
     * By default, this method returns an object whose *persistent* properties are not initialized.
49
     * Transient properties are still initialized to their default value, if any.
50
     * Properties may be initialized by passing a map of property name to value.
51
     *
52
     * @param ClassMetadata        $classMetadata The class metadata of the entity or embeddable.
53
     * @param array<string, mixed> $values        An optional map of property name to value to write.
54
     *
55
     * @throws ReflectionException If the class does not exist.
56
     */
57
    public function instantiate(ClassMetadata $classMetadata, array $values = []): object
58
    {
59
        $className = $classMetadata->className;
30✔
60

61
        if (isset($this->classes[$className])) {
30✔
62
            $reflectionClass = $this->classes[$className];
22✔
63
        } else {
64
            $reflectionClass = $this->classes[$className] = new ReflectionClass($className);
12✔
65
        }
66

67
        $object = $reflectionClass->newInstanceWithoutConstructor();
30✔
68

69
        (function () use ($classMetadata, $values, $reflectionClass): void {
30✔
70
            // Unset persistent properties
71
            // @todo PHP 7.4: for even better performance, only unset typed properties that have a default value, as
72
            //       unset() will have no effect on those that have no default value (will require a new metadata prop).
73
            foreach ($classMetadata->properties as $prop) {
30✔
74
                unset($this->{$prop});
29✔
75
            }
76

77
            // Set values
78
            foreach ($values as $key => $value) {
30✔
79
                if ($value === null) {
27✔
80
                    /**
81
                     * @todo temporary fix: do not set null values when typed property is not nullable;
82
                     *       needs investigation to see why these null values are being passed in the first place
83
                     */
84

85
                    $reflectionType = $reflectionClass->getProperty($key)->getType();
3✔
86

87
                    if ($reflectionType !== null && ! $reflectionType->allowsNull()) {
3✔
88
                        continue;
2✔
89
                    }
90
                }
91

92
                $this->{$key} = $value;
27✔
93
            }
94
        })->bindTo($object, $className)();
30✔
95

96
        return $object;
30✔
97
    }
98

99
    /**
100
     * Instantiates a data transfer object with a nested array of scalar values.
101
     *
102
     * The class must have public properties only, and no constructor.
103
     *
104
     * @template T
105
     *
106
     * @param class-string<T>      $className
107
     * @param array<string, mixed> $values
108
     *
109
     * @return T
110
     *
111
     * @throws ReflectionException      If the class does not exist.
112
     * @throws InvalidArgumentException If the class is not a valid DTO or an unexpected value is found.
113
     */
114
    public function instantiateDTO(string $className, array $values): object
115
    {
116
        $propertyConverters = $this->getPropertyConverters($className);
1✔
117

118
        $object = new $className();
1✔
119

120
        foreach ($values as $name => $value) {
1✔
121
            if (! isset($propertyConverters[$name])) {
1✔
UNCOV
122
                throw new InvalidArgumentException(sprintf('There is no property named $%s in class %s.', $name, $className));
×
123
            }
124

125
            $propertyConverter = $propertyConverters[$name];
1✔
126
            $object->{$name} = $propertyConverter($value);
1✔
127
        }
128

129
        return $object;
1✔
130
    }
131

132
    /**
133
     * Reads *initialized* object properties.
134
     *
135
     * Properties that are not initialized, or have been unset(), are not included in the array.
136
     * This method assumes that there are no private properties in parent classes.
137
     *
138
     * @param object $object The object to read.
139
     *
140
     * @return array<string, mixed> A map of property names to values.
141
     */
142
    public function read(object $object): array
143
    {
144
        return (function () {
12✔
145
            return get_object_vars($this);
12✔
146
        })->bindTo($object, $object)();
12✔
147
    }
148

149
    /**
150
     * Writes an object's properties.
151
     *
152
     * This method does not support writing private properties in parent classes.
153
     *
154
     * @param object               $object The object to write.
155
     * @param array<string, mixed> $values A map of property names to values.
156
     */
157
    public function write(object $object, array $values): void
158
    {
159
        (function () use ($values): void {
7✔
160
            foreach ($values as $key => $value) {
7✔
161
                $this->{$key} = $value;
7✔
162
            }
163
        })->bindTo($object, $object)();
7✔
164
    }
165

166
    /**
167
     * Returns the property converters for the given class, indexed by property name.
168
     *
169
     * @param class-string $className
170
     *
171
     * @return array<string, Closure>
172
     *
173
     * @throws ReflectionException      If the class does not exist.
174
     * @throws InvalidArgumentException If the class is not a valid DTO or an unexpected value is found.
175
     */
176
    private function getPropertyConverters(string $className): array
177
    {
178
        if (isset($this->propertyConverters[$className])) {
1✔
179
            return $this->propertyConverters[$className];
×
180
        }
181

182
        $reflectionClass = new ReflectionClass($className);
1✔
183

184
        if ($reflectionClass->isAbstract()) {
1✔
UNCOV
185
            throw new InvalidArgumentException(sprintf('Cannot instantiate abstract class %s.', $className));
×
186
        }
187

188
        if ($reflectionClass->isInterface()) {
1✔
UNCOV
189
            throw new InvalidArgumentException(sprintf('Cannot instantiate interface %s.', $className));
×
190
        }
191

192
        if ($reflectionClass->isInternal()) {
1✔
UNCOV
193
            throw new InvalidArgumentException(sprintf('Cannot instantiate internal class %s.', $className));
×
194
        }
195

196
        if ($reflectionClass->getConstructor() !== null) {
1✔
UNCOV
197
            throw new InvalidArgumentException(sprintf('Class %s must not have a constructor.', $className));
×
198
        }
199

200
        $properties = $reflectionClass->getProperties();
1✔
201

202
        $result = [];
1✔
203

204
        foreach ($properties as $property) {
1✔
205
            $name = $property->getName();
1✔
206

207
            if ($property->isStatic()) {
1✔
UNCOV
208
                throw new InvalidArgumentException(sprintf('Property $%s of class %s must not be static.', $name, $className));
×
209
            }
210

211
            if (! $property->isPublic()) {
1✔
212
                throw new InvalidArgumentException(sprintf('Property $%s of class %s must be public.', $name, $className));
×
213
            }
214

215
            $result[$name] = $this->getPropertyValueConverter($property);
1✔
216
        }
217

218
        $this->propertyConverters[$className] = $result;
1✔
219

220
        return $result;
1✔
221
    }
222

223
    /**
224
     * @return Closure(mixed): mixed
225
     *
226
     * @throws InvalidArgumentException If an unexpected value is found.
227
     */
228
    private function getPropertyValueConverter(ReflectionProperty $property): Closure
229
    {
230
        $type = $property->getType();
1✔
231

232
        $propertyName = $property->getName();
1✔
233
        $className = $property->getDeclaringClass()->getName();
1✔
234

235
        if ($type instanceof ReflectionNamedType) {
1✔
236
            $propertyType = $type->getName();
1✔
237

238
            if ($type->isBuiltin()) {
1✔
239
                return match ($propertyType) {
1✔
240
                    'string' => fn ($value) => $value,
1✔
241
                    'int' => fn ($value) => (int) $value,
1✔
UNCOV
242
                    'float' => fn ($value) => (float) $value,
×
UNCOV
243
                    'bool' => fn ($value) => (bool) $value,
×
244
                    default => throw new InvalidArgumentException(sprintf('Unexpected non-scalar type "%s" for property $%s in class %s.', $propertyType, $propertyName, $className))
1✔
245
                };
1✔
246
            }
247

248
            return function ($value) use ($propertyName, $className, $type) {
1✔
249
                if (! is_array($value)) {
1✔
UNCOV
250
                    throw new InvalidArgumentException(sprintf('Expected array for property $%s of class %s, got %s.', $propertyName, $className, gettype($value)));
×
251
                }
252

253
                /** @var array<string, mixed> $value */
254
                return $this->instantiateDTO($type->getName(), $value);
1✔
255
            };
1✔
256
        }
257

UNCOV
258
        return fn ($value) => $value;
×
259
    }
260
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc