GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( d399db...857eb8 )
by Steevan
02:14
created

ReadOnlyHydrator::createNewEntity()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 24
rs 8.6845
cc 4
eloc 18
nc 4
nop 2
1
<?php
2
3
namespace steevanb\DoctrineReadOnlyHydrator\Hydrator;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\Common\Proxy\AbstractProxyFactory;
7
use Doctrine\Common\Proxy\ProxyGenerator;
8
use Doctrine\ORM\Internal\Hydration\ArrayHydrator;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use steevanb\DoctrineReadOnlyHydrator\Entity\ReadOnlyEntityInterface;
11
use steevanb\DoctrineReadOnlyHydrator\Exception\MethodNotFoundException;
12
use steevanb\DoctrineReadOnlyHydrator\Exception\PrivateMethodShouldNotAccessPropertiesException;
13
14
class ReadOnlyHydrator extends ArrayHydrator
15
{
16
    const HYDRATOR_NAME = 'readOnly';
17
18
    /**
19
     * @param array $data
20
     * @param array $result
21
     */
22
    protected function hydrateRowData(array $data, array &$result)
23
    {
24
        $arrayData = array();
25
        parent::hydrateRowData($data, $arrayData);
26
27
        $rootAlias = key($this->getPrivatePropertyValue(ArrayHydrator::class, '_rootAliases', $this));
28
        $result[] = $this->hydrateRowDataReadOnly($this->_rsm->aliasMap[$rootAlias], $arrayData[0]);
29
    }
30
31
    /**
32
     * @param string $className
33
     * @param array $data
34
     * @return object
35
     * @throws \Exception
36
     */
37
    protected function hydrateRowDataReadOnly($className, array $data)
38
    {
39
        $classMetaData = $this->_em->getClassMetadata($className);
40
        $mappings = $classMetaData->getAssociationMappings();
41
        $entity = $this->createEntity($classMetaData, $data);
0 ignored issues
show
Compatibility introduced by
$classMetaData of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
42
        $reflection = new \ReflectionObject($entity);
43
44
        foreach ($data as $name => $value) {
45
            if (array_key_exists($name, $mappings)) {
46
                $mapping = $mappings[$name];
47
                switch ($mapping['type']) {
48
                    case ClassMetadata::ONE_TO_ONE:
49
                        $value = static::hydrateOneToOne($mapping, $value);
50
                        break;
51
                    case ClassMetadata::ONE_TO_MANY:
52
                        $value = static::hydrateOneToMany($mapping, $value);
53
                        break;
54
                    case ClassMetadata::MANY_TO_ONE:
55
                        $value = static::hydrateManyToOne($mapping, $value);
56
                        break;
57
                    case ClassMetadata::MANY_TO_MANY:
58
                        $value = static::hydrateManyToMany($mapping, $value);
59
                        break;
60
                    default:
61
                        throw new \Exception('Unknow mapping type "' . $mapping['type'] . '".');
62
                }
63
            }
64
65
            if (
66
                $classMetaData->inheritanceType === ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
67
                && isset($entity->$name) === false
68
            ) {
69
                continue;
70
            }
71
            $property = $reflection->getProperty($name);
72
            $isAccessible = $property->isPublic() === false;
73
            $property->setAccessible(true);
74
            $property->setValue($entity, $value);
75
            $property->setAccessible($isAccessible);
76
        }
77
78
        return $entity;
79
    }
80
81
    /**
82
     * @param ClassMetadata $classMetaData
83
     * @param array $data
84
     * @return mixed
85
     * @throws \Exception
86
     */
87
    protected function createEntity(ClassMetadata $classMetaData, array $data)
88
    {
89
        switch ($classMetaData->inheritanceType) {
90
            case ClassMetadata::INHERITANCE_TYPE_NONE:
91
                $className = $classMetaData->name;
92
                break;
93
            case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE:
94
                if (array_key_exists($classMetaData->discriminatorColumn['name'], $data) === false) {
95
                    $exception = 'Discriminator column "' . $classMetaData->discriminatorColumn['name'] . '" ';
96
                    $exception .= 'for "' . $classMetaData->name . '" does not exists in $data.';
97
                    throw new \Exception($exception);
98
                }
99
                $discriminator = $data[$classMetaData->discriminatorColumn['name']];
100
                $className = $classMetaData->discriminatorMap[$discriminator];
101
                break;
102
            default:
103
                throw new \Exception('Unknow inheritance type "' . $classMetaData->inheritanceType . '".');
104
        }
105
106
        $methods = array();
107
        $reflection = new \ReflectionClass($className);
108
        $properties = array_merge($classMetaData->getFieldNames(), array_keys($classMetaData->associationMappings));
109
        foreach ($reflection->getMethods() as $method) {
110
            if ($method->getName() === '__construct') {
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
111
                continue;
112
            }
113
114
            $usedProperties = $this->getUsedProperties($method, $properties);
115
            if (count($usedProperties) > 0) {
116
                if ($method->isPrivate()) {
117
                    throw new PrivateMethodShouldNotAccessPropertiesException(
118
                        $className,
119
                        $method->getName(),
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
120
                        $usedProperties
121
                    );
122
                }
123
124
                $methods[] = $this->createProxyMethod($method, $usedProperties);
125
            }
126
        }
127
128
        $methodCode = implode("\n\n", $methods);
129
        $namespace = substr($classMetaData->getName(), 0, strrpos($classMetaData->getName(), '\\'));
130
        $shortClassName = substr($classMetaData->getName(), strrpos($classMetaData->getName(), '\\') + 1);
131
        $generator = static::class;
132
        $readOnlyInterface = ReadOnlyEntityInterface::class;
133
134
        $php = <<<PHP
135
<?php
136
137
namespace ReadOnlyProxies\__CG__\\$namespace;
138
139
/**
140
 * DO NOT EDIT THIS FILE - IT WAS CREATED BY $generator
141
 */
142
class $shortClassName extends $className implements $readOnlyInterface
143
{
144
    protected \$loadedProperties;
145
146
    public function __construct(array \$loadedProperties)
147
    {
148
        \$this->loadedProperties = \$loadedProperties;
149
    }
150
151
$methodCode
152
}
153
PHP;
154
        !dd($php);
155
156
157
158
        $entity = (new \ReflectionClass($className))->newInstanceWithoutConstructor();
159
160
        return $entity;
161
    }
162
163
    /**
164
     * @param array $mapping
165
     * @param array $data
166
     * @return ArrayCollection
167
     */
168
    protected function hydrateOneToOne(array $mapping, $data)
169
    {
170
        return static::hydrateRowDataReadOnly($mapping['targetEntity'], $data);
171
    }
172
173
    /**
174
     * @param array $mapping
175
     * @param array $data
176
     * @return ArrayCollection
177
     */
178 View Code Duplication
    protected function hydrateOneToMany(array $mapping, $data)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
179
    {
180
        $entities = [];
181
        foreach ($data as $linkedData) {
182
            $entities[] = static::hydrateRowDataReadOnly($mapping['targetEntity'], $linkedData);
183
        }
184
185
        return new ArrayCollection($entities);
186
    }
187
188
    /**
189
     * @param array $mapping
190
     * @param array $data
191
     * @return ArrayCollection
192
     */
193
    protected function hydrateManyToOne(array $mapping, $data)
194
    {
195
        return static::hydrateRowDataReadOnly($mapping['targetEntity'], $data);
196
    }
197
198
    /**
199
     * @param array $mapping
200
     * @param array $data
201
     * @return ArrayCollection
202
     */
203 View Code Duplication
    protected function hydrateManyToMany(array $mapping, $data)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
    {
205
        $entities = [];
206
        foreach ($data as $linkedData) {
207
            $entities[] = static::hydrateRowDataReadOnly($mapping['targetEntity'], $linkedData);
208
        }
209
210
        return new ArrayCollection($entities);
211
    }
212
213
    /**
214
     * @param string $className
215
     * @param string $property
216
     * @param object $object
217
     * @return mixed
218
     */
219
    protected function getPrivatePropertyValue($className, $property, $object)
220
    {
221
        $reflection = new \ReflectionProperty($className, $property);
222
        $accessible = $reflection->isPublic();
223
        $reflection->setAccessible(true);
224
        $value = $reflection->getValue($object);
225
        $reflection->setAccessible($accessible === false);
226
227
        return $value;
228
    }
229
230
    /**
231
     * As Doctrine\ORM\EntityManager::newHydrator() call new FooHydrator($this), we can't set parameters to Hydrator.
232
     * So, we will use proxyDirectory from Doctrine\Common\Proxy\AbstractProxyFactory.
233
     * It's directory used by Doctrine\ORM\Internal\Hydration\ObjectHydrator.
234
     *
235
     * @return string
236
     */
237
    protected function getProxyDirectory()
238
    {
239
        /** @var ProxyGenerator $proxyGenerator */
240
        $proxyGenerator = $this->getPrivatePropertyValue(
241
            AbstractProxyFactory::class,
242
            'proxyGenerator',
243
            $this->_em->getProxyFactory()
244
        );
245
246
        $directory = $this->getPrivatePropertyValue(get_class($proxyGenerator), 'proxyDirectory', $proxyGenerator);
247
        $readOnlyDirectory = $directory . DIRECTORY_SEPARATOR . 'ReadOnly';
248
        if (is_dir($readOnlyDirectory) === false) {
249
            mkdir($readOnlyDirectory);
250
        }
251
252
        return $readOnlyDirectory;
253
    }
254
255
    /**
256
     * @param \ReflectionMethod $reflectionMethod
257
     * @param array $properties
258
     * @return string|false
259
     */
260
    protected function getUsedProperties(\ReflectionMethod $reflectionMethod, $properties)
261
    {
262
        $classLines = file($reflectionMethod->getFileName());
263
        $methodLines = array_slice(
264
            $classLines,
265
            $reflectionMethod->getStartLine() - 1,
266
            $reflectionMethod->getEndLine() - $reflectionMethod->getStartLine() + 1
267
        );
268
        $code = '<?php' . "\n" . implode("\n", $methodLines) . "\n" . '?>';
269
270
        $return = array();
271
        $nextStringIsProperty = false;
272
        foreach (token_get_all($code) as $token) {
273
            if (is_array($token)) {
274
                if ($token[0] === T_VARIABLE && $token[1] === '$this') {
275
                    $nextStringIsProperty = true;
276
                } elseif ($nextStringIsProperty && $token[0] === T_STRING) {
277
                    $nextStringIsProperty = false;
278
                    if (in_array($token[1], $properties)) {
279
                        $return[$token[1]] = true;
280
                    }
281
                }
282
            }
283
        }
284
285
        return array_keys($return);
286
    }
287
288
    /**
289
     * @param \ReflectionMethod $reflectionMethod
290
     * @param array $properties
291
     * @return string
292
     */
293
    protected function createProxyMethod(\ReflectionMethod $reflectionMethod, $properties)
294
    {
295
        if ($reflectionMethod->isPublic()) {
296
            $signature = 'public';
297
        } else {
298
            $signature = 'protected';
299
        }
300
        $signature .= ' function ' . $reflectionMethod->getName() . '()';
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
301
        $method = $reflectionMethod->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
302
303
        array_walk($properties, function(&$name) {
304
            $name = "'" . $name . "'";
305
        });
306
        $propertiesToAssert = implode(', ', $properties);
307
308
        $php = <<<PHP
309
    $signature
310
    {
311
        \$this->assertReadOnlyPropertiesAreLoaded($propertiesToAssert);
312
313
        return call_user_func_array(array('parent', '$method'), func_get_args());
314
    }
315
PHP;
316
317
        return $php;
318
    }
319
}
320