Passed
Push — master ( 6b66e0...046ff6 )
by Daniel
07:24
created

addDiscriminatorMap()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 1
1
<?php
2
3
namespace Silverback\ApiComponentBundle\Doctrine\Extension;
4
5
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
8
use Doctrine\ORM\Mapping\ClassMetadata;
9
use Doctrine\ORM\Mapping\MappingException;
10
use Silverback\ApiComponentBundle\Entity\Component\AbstractComponent;
11
use Silverback\ApiComponentBundle\Entity\Component\Collection\Collection;
12
use Silverback\ApiComponentBundle\Entity\Component\Content\Content;
13
use Silverback\ApiComponentBundle\Entity\Component\Feature\Columns\FeatureColumns;
14
use Silverback\ApiComponentBundle\Entity\Component\Feature\Columns\FeatureColumnsItem;
15
use Silverback\ApiComponentBundle\Entity\Component\Feature\Stacked\FeatureStacked;
16
use Silverback\ApiComponentBundle\Entity\Component\Feature\Stacked\FeatureStackedItem;
17
use Silverback\ApiComponentBundle\Entity\Component\Feature\TextList\FeatureTextList;
18
use Silverback\ApiComponentBundle\Entity\Component\Feature\TextList\FeatureTextListItem;
19
use Silverback\ApiComponentBundle\Entity\Component\Form\Form;
20
use Silverback\ApiComponentBundle\Entity\Component\Gallery\Gallery;
21
use Silverback\ApiComponentBundle\Entity\Component\Gallery\GalleryItem;
22
use Silverback\ApiComponentBundle\Entity\Component\Hero\Hero;
23
use Silverback\ApiComponentBundle\Entity\Component\Image\SimpleImage;
24
use Silverback\ApiComponentBundle\Entity\Component\Layout\SideColumn;
25
use Silverback\ApiComponentBundle\Entity\Component\Navigation\Menu\Menu;
26
use Silverback\ApiComponentBundle\Entity\Component\Navigation\Menu\MenuItem;
27
use Silverback\ApiComponentBundle\Entity\Component\Navigation\NavBar\NavBar;
28
use Silverback\ApiComponentBundle\Entity\Component\Navigation\NavBar\NavBarItem;
29
use Silverback\ApiComponentBundle\Entity\Component\Navigation\Tabs\Tabs;
30
use Silverback\ApiComponentBundle\Entity\Component\Navigation\Tabs\TabsItem;
31
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
32
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
33
34
class DiscriminatorMappingExtension
35
{
36
    private static $mappingKeys = [
37
        'content' => Content::class,
38
        'form' => Form::class,
39
        'gallery' => Gallery::class,
40
        'gallery_item' => GalleryItem::class,
41
        'hero' => Hero::class,
42
        'feature_columns' => FeatureColumns::class,
43
        'feature_columns_item' => FeatureColumnsItem::class,
44
        'feature_stacked' => FeatureStacked::class,
45
        'feature_stacked_item' => FeatureStackedItem::class,
46
        'feature_text_list' => FeatureTextList::class,
47
        'feature_text_list_item' => FeatureTextListItem::class,
48
        'nav_bar' => NavBar::class,
49
        'nav_bar_item' => NavBarItem::class,
50
        'tabs' => Tabs::class,
51
        'tabs_item' => TabsItem::class,
52
        'menu' => Menu::class,
53
        'menu_item' => MenuItem::class,
54
        'collection' => Collection::class,
55
        'simple_image' => SimpleImage::class,
56
        'layout_side_column' => SideColumn::class
57
    ];
58
59
    /**
60
     * @var MappingDriver
61
     */
62
    private $driver;
63
64
    /**
65
     * @param EntityManagerInterface $em
66
     */
67
    public function __construct(EntityManagerInterface $em)
68
    {
69
        $this->driver = $em->getConfiguration()->getMetadataDriverImpl();
70
    }
71
72
    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void
73
    {
74
        $classMetadata = $eventArgs->getClassMetadata();
75
        $reflection = $classMetadata->getReflectionClass();
76
        if ($reflection->getName() !== AbstractComponent::class) {
77
            return;
78
        }
79
80
        $this->addDiscriminatorMap($classMetadata);
81
    }
82
83
    protected function addDiscriminatorMap(ClassMetadata $class): void
84
    {
85
        // ! $class->discriminatorMap &&
86
        if ($class->isRootEntity() && ! $class->isInheritanceTypeNone()) {
87
            $this->addDefaultDiscriminatorMap($class);
88
        }
89
    }
90
91
    /**
92
     * Adds a default discriminator map if no one is given
93
     *
94
     * If an entity is of any inheritance type and does not contain a
95
     * discriminator map, then the map is generated automatically. This process
96
     * is usually expensive computation wise, however by using caching, the only
97
     * part which is always called checks whether the class names have changed. If not,
98
     * no additional computation is required.
99
     *
100
     * The automatically generated discriminator map contains the lowercase short name of
101
     * each class as key.
102
     *
103
     * @param ClassMetadata $class
104
     */
105
    private function addDefaultDiscriminatorMap(ClassMetadata $class): void
106
    {
107
        $cache = new FilesystemAdapter();
108
        $allClasses = $this->driver->getAllClassNames();
109
        $cachedClasses = $cache->getItem('silverback_api_component.doctrine.driver_classes');
110
        $cachedMap = $cache->getItem('silverback_api_component.doctrine.components_discriminator_map');
111
        if (!$cachedClasses->isHit() || $cachedClasses->get() !== $allClasses) {
112
            $cachedClasses->set($allClasses);
113
            $cache->save($cachedClasses);
114
115
            $cachedMap->set($this->getDiscriminatorMap($class, $allClasses));
116
            $cache->save($cachedMap);
117
        }
118
        $map = $cachedMap->get();
119
        $class->setDiscriminatorMap($map);
120
    }
121
122
    /**
123
     * @param ClassMetadata $class
124
     * @param array $allClasses
125
     * @return array
126
     * @throws MappingException
127
     */
128
    private function getDiscriminatorMap(ClassMetadata $class, array $allClasses): array
129
    {
130
        $fqcn = $class->getName();
131
        $map = [$this->getShortName($class->name) => $fqcn];
132
133
        $duplicates = [];
134
        foreach ($allClasses as $subClassCandidate) {
135
            if (is_subclass_of($subClassCandidate, $fqcn)) {
136
                $shortName = $this->getShortName($subClassCandidate);
137
138
                if (isset($map[$shortName])) {
139
                    $duplicates[] = $shortName;
140
                }
141
142
                $map[$shortName] = $subClassCandidate;
143
            }
144
        }
145
        if ($duplicates) {
146
            throw MappingException::duplicateDiscriminatorEntry($class->name, $duplicates, $map);
147
        }
148
        return $map;
149
    }
150
151
152
    /**
153
     * Gets the lower-case short name of a class.
154
     *
155
     * @param string $className
156
     *
157
     * @return string
158
     */
159
    private function getShortName($className): string
160
    {
161
        $nameConverter = new CamelCaseToSnakeCaseNameConverter();
162
        $name = array_search($className, self::$mappingKeys, true);
163
        if (!$name) {
164
            $parts = explode("\\", $className);
165
            $lastPart = end($parts);
166
            $name = $nameConverter->normalize($lastPart);
167
        }
168
        return $name;
169
    }
170
}
171