Completed
Push — master ( 91676c...354384 )
by Alexander
02:23
created

MetadataLoadInterceptor::getSubscribedEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
declare(strict_types = 1);
3
/*
4
 * Go! AOP framework
5
 *
6
 * @copyright Copyright 2012, Lisachenko Alexander <[email protected]>
7
 *
8
 * This source file is subject to the license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
namespace Go\Bridge\Doctrine;
12
13
use Doctrine\Common\EventSubscriber;
14
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
15
use Doctrine\ORM\Events;
16
use Doctrine\ORM\Mapping\ClassMetadata;
17
use Go\Core\AspectContainer;
18
19
/**
20
 * Class MetadataLoadInterceptor
21
 *
22
 * Support for weaving Doctrine entities.
23
 */
24
final class MetadataLoadInterceptor implements EventSubscriber
25
{
26
    /**
27
     * {@inheritdoc}
28
     */
29
    public function getSubscribedEvents()
30
    {
31
        return [
32
            Events::loadClassMetadata
33
        ];
34
    }
35
36
    /**
37
     * Handles \Doctrine\ORM\Events::loadClassMetadata event by modifying metadata of Go! AOP proxied classes.
38
     *
39
     * This method intercepts loaded metadata of Doctrine's entities which are weaved by Go! AOP,
40
     * and denotes them as mapped superclass. If weaved entities uses mappings from traits
41
     * (such as Timestampable, Blameable, etc... from https://github.com/Atlantic18/DoctrineExtensions),
42
     * it will remove all mappings from proxied class for fields inherited from traits in order to prevent
43
     * collision with concrete subclass of weaved entity. Fields from trait will be present in concrete subclass
44
     * of weaved entitites.
45
     *
46
     * @see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses
47
     * @see https://github.com/Atlantic18/DoctrineExtensions
48
     */
49 2
    public function loadClassMetadata(LoadClassMetadataEventArgs $args): void
50
    {
51
        /**
52
         * @var ClassMetadata $metadata
53
         */
54 2
        $metadata = $args->getClassMetadata();
55
56 2
        if (1 === preg_match(sprintf('/.+(%s)$/', AspectContainer::AOP_PROXIED_SUFFIX), $metadata->name)) {
57 1
            $metadata->isMappedSuperclass           = true;
58 1
            $metadata->isEmbeddedClass              = false;
59 1
            $metadata->table                        = [];
60 1
            $metadata->customRepositoryClassName    = null;
61
62 1
            $this->removeMappingsFromTraits($metadata);
63
        }
64 2
    }
65
66
    /**
67
     * Remove fields in Go! AOP proxied class metadata that are inherited
68
     * from traits.
69
     */
70 1
    private function removeMappingsFromTraits(ClassMetadata $metadata): void
71
    {
72 1
        $traits = $this->getTraits($metadata->name);
73
74 1
        foreach ($traits as $trait) {
75 1
            $trait = new \ReflectionClass($trait);
76
77
            /**
78
             * @var \ReflectionProperty $property
79
             */
80 1
            foreach ($trait->getProperties() as $property) {
81 1
                $name = $property->getName();
82
83 1
                if (isset($metadata->fieldMappings[$name])) {
84 1
                    $mapping = $metadata->fieldMappings[$name];
85
86
                    unset(
87 1
                        $metadata->fieldMappings[$name],
88 1
                        $metadata->fieldNames[$mapping['columnName']],
89 1
                        $metadata->columnNames[$name]
90
                    );
91
                }
92
            }
93
        }
94 1
    }
95
96
    /**
97
     * Get ALL traits used by one class.
98
     *
99
     * This method is copied from https://github.com/RunOpenCode/traitor-bundle/blob/master/src/RunOpenCode/Bundle/Traitor/Utils/ClassUtils.php
100
     *
101
     * @param object|string $objectOrClass Instance of class or FQCN
102
     * @param bool $autoload Weather to autoload class.
103
     *
104
     * @throws \InvalidArgumentException
105
     * @throws \RuntimeException
106
     */
107 1
    private function getTraits($objectOrClass, $autoload = true): array
108
    {
109 1
        if (is_object($objectOrClass)) {
110
            $objectOrClass = get_class($objectOrClass);
111
        }
112
113 1
        if (!is_string($objectOrClass)) {
114
            throw new \InvalidArgumentException(sprintf('Full qualified class name expected, got: "%s".', gettype($objectOrClass)));
115
        }
116
117 1
        if (!class_exists($objectOrClass)) {
118
            throw new \RuntimeException(sprintf('Class "%s" does not exists or it can not be autoloaded.', $objectOrClass));
119
        }
120
121 1
        $traits = [];
122
        // Get traits of all parent classes
123
        do {
124 1
            $traits = array_merge(class_uses($objectOrClass, $autoload), $traits);
125 1
        } while ($objectOrClass = get_parent_class($objectOrClass));
126
127 1
        $traitsToSearch = $traits;
128
129 1
        while (count($traitsToSearch) > 0) {
130 1
            $newTraits = class_uses(array_pop($traitsToSearch), $autoload);
131 1
            $traits = array_merge($newTraits, $traits);
132 1
            $traitsToSearch = array_merge($newTraits, $traitsToSearch);
133
        }
134
135 1
        foreach ($traits as $trait => $same) {
136 1
            $traits = array_merge(class_uses($trait, $autoload), $traits);
137
        }
138
139
        return array_unique(array_map(function ($fqcn) {
140 1
            return ltrim($fqcn, '\\');
141 1
        }, $traits));
142
    }
143
}
144