Passed
Branch master (4b377c)
by Andreas
03:22
created

subscriber::postGenerateSchemaTable()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.0466

Importance

Changes 0
Metric Value
cc 4
eloc 6
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 9
ccs 6
cts 7
cp 0.8571
crap 4.0466
rs 10
1
<?php
2
/**
3
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
4
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
6
 */
7
8
namespace midgard\portable\storage;
9
10
use midgard\portable\storage\interfaces\metadata;
11
use midgard\portable\api\dbobject;
12
use midgard\portable\api\repligard;
13
use Doctrine\Common\EventSubscriber;
14
use Doctrine\Persistence\Mapping\ClassMetadata;
15
use Doctrine\ORM\Events;
16
use Doctrine\ORM\EntityManagerInterface;
17
use Doctrine\ORM\Event\OnFlushEventArgs;
18
use Doctrine\DBAL\Schema\Column;
19
use Doctrine\DBAL\Types\Type;
20
use Doctrine\DBAL\Events as dbal_events;
21
use Doctrine\DBAL\Event\SchemaCreateTableEventArgs;
22
use Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs;
23
use Doctrine\ORM\Tools\ToolEvents;
24
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
25
use Doctrine\DBAL\Types\Types;
26
use Doctrine\Persistence\Event\LifecycleEventArgs;
27
use Doctrine\DBAL\Platforms\SqlitePlatform;
28
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
29
30
class subscriber implements EventSubscriber
31
{
32
    const ACTION_NONE = 0;
33
    const ACTION_DELETE = 1;
34
    const ACTION_PURGE = 2;
35
    const ACTION_CREATE = 3;
36
    const ACTION_UPDATE = 4;
37
38 115
    public function onFlush(OnFlushEventArgs $args)
39
    {
40 115
        $em = $args->getEntityManager();
41 115
        $uow = $em->getUnitOfWork();
42
43 115
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
44 115
            $this->on_create($entity, $em);
45
        }
46
47 115
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
48 39
            $this->on_update($entity, $em);
49
        }
50
51 115
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
52 17
            $this->on_remove($entity, $em);
53
        }
54
    }
55
56 81
    public function postLoad(LifecycleEventArgs $args)
57
    {
58 81
        $entity = $args->getObject();
59 81
        if ($entity instanceof dbobject) {
60 81
            $om = $args->getObjectManager();
61 81
            $entity->injectObjectManager($om, $om->getClassMetadata(get_class($entity)));
62
        }
63
    }
64
65 115
    private function on_create(dbobject $entity, EntityManagerInterface $em)
66
    {
67 115
        $cm = $em->getClassMetadata(get_class($entity));
68 115
        if (!($entity instanceof repligard)) {
69 115
            if (empty($entity->guid)) {
0 ignored issues
show
Bug Best Practice introduced by
The property $guid is declared protected in midgard\portable\api\dbobject. Since you implement __get, consider adding a @property or @property-read.
Loading history...
70 102
                $entity->set_guid(connection::generate_guid());
0 ignored issues
show
Bug introduced by
The method set_guid() does not exist on midgard\portable\api\dbobject. It seems like you code against a sub-type of midgard\portable\api\dbobject such as midgard\portable\api\mgdobject. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

70
                $entity->/** @scrutinizer ignore-call */ 
71
                         set_guid(connection::generate_guid());
Loading history...
71 102
                $em->getUnitOfWork()->recomputeSingleEntityChangeSet($cm, $entity);
72
            }
73
74 115
            $om = new objectmanager($em);
75 115
            $repligard_cm = $em->getClassMetadata(connection::get_fqcn('midgard_repligard'));
76 115
            $repligard_entry = $om->new_instance($repligard_cm->getName());
77 115
            $repligard_entry->typename = $cm->getReflectionClass()->getShortName();
0 ignored issues
show
Bug Best Practice introduced by
The property typename does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
78 115
            $repligard_entry->guid = $entity->guid;
79 115
            $repligard_entry->object_action = self::ACTION_CREATE;
0 ignored issues
show
Bug Best Practice introduced by
The property object_action does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
80 115
            $em->persist($repligard_entry);
81 115
            $em->getUnitOfWork()->computeChangeSet($repligard_cm, $repligard_entry);
82
        }
83
84 115
        if ($entity instanceof metadata) {
85 97
            $entity->metadata->created = new \midgard_datetime();
0 ignored issues
show
Bug Best Practice introduced by
The property metadata does not exist on midgard\portable\api\repligard. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property metadata does not exist on midgard\portable\api\dbobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
86
            // we copy here instead of creating a new, because otherwise we might have
87
            // a one second difference if the code runs at the right millisecond
88 97
            $entity->metadata->revised = $entity->metadata->created;
89 97
            if ($user = connection::get_user()) {
90 7
                $entity->metadata_creator = $user->person;
0 ignored issues
show
Bug Best Practice introduced by
The property metadata_creator does not exist on midgard\portable\api\repligard. Since you implemented __set, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property metadata_creator does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
91 7
                $entity->metadata_revisor = $user->person;
0 ignored issues
show
Bug Best Practice introduced by
The property metadata_revisor does not exist on midgard\portable\api\repligard. Since you implemented __set, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property metadata_revisor does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
92
            }
93 97
            $entity->metadata->size = $this->calculate_size($cm, $entity);
94 97
            $em->getUnitOfWork()->recomputeSingleEntityChangeSet($cm, $entity);
95
        }
96
    }
97
98 39
    private function on_update(dbobject $entity, EntityManagerInterface $em)
99
    {
100 39
        if ($entity instanceof repligard) {
101
            return;
102
        }
103 39
        $check_repligard = true;
104 39
        $deleted = false;
105 39
        if ($entity instanceof metadata) {
106 38
            $deleted = $entity->{metadata::DELETED_FIELD};
107 38
            $cs = $em->getUnitOfWork()->getEntityChangeSet($entity);
108
            // We only need to update repligard if we're coming from create (revision 0)
109
            // or if we delete/undelete
110 38
            if (   !array_key_exists('metadata_deleted', $cs)
111 38
                && $entity->metadata_revision > 0)  {
0 ignored issues
show
Bug Best Practice introduced by
The property metadata_revision does not exist on midgard\portable\api\dbobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
112 4
                $check_repligard = false;
113
            }
114
115 38
            $create_revision = true;
116 38
            if (array_key_exists('metadata_islocked', $cs)) {
117 2
                $lock_fields = array_flip(['metadata_locked', 'metadata_islocked', 'metadata_locker']);
118 2
                $create_revision = !empty(array_diff_key($cs, $lock_fields));
119
            }
120
121 38
            if ($create_revision) {
122 36
                $cm = $em->getClassMetadata(get_class($entity));
123 36
                $entity->metadata_revised = new \midgard_datetime();
0 ignored issues
show
Bug Best Practice introduced by
The property metadata_revised does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
124 36
                $entity->metadata_revision++;
125 36
                if ($user = connection::get_user()) {
126 5
                    $entity->metadata_revisor = $user->person;
0 ignored issues
show
Bug Best Practice introduced by
The property metadata_revisor does not exist on midgard\portable\api\dbobject. Since you implemented __set, consider adding a @property annotation.
Loading history...
127
                }
128 36
                $entity->metadata->size = $this->calculate_size($cm, $entity);
0 ignored issues
show
Bug Best Practice introduced by
The property metadata does not exist on midgard\portable\api\dbobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
129 36
                $em->getUnitOfWork()->recomputeSingleEntityChangeSet($cm, $entity);
130
            }
131
        }
132
133 39
        if ($check_repligard) {
134 39
            $repligard_entry = $em->getRepository(connection::get_fqcn('midgard_repligard'))->findOneBy(['guid' => $entity->guid]);
0 ignored issues
show
Bug Best Practice introduced by
The property $guid is declared protected in midgard\portable\api\dbobject. Since you implement __get, consider adding a @property or @property-read.
Loading history...
135
136 39
            if ($deleted) {
137 26
                $repligard_entry->object_action = self::ACTION_DELETE;
138
            } else {
139 21
                $repligard_entry->object_action = self::ACTION_UPDATE;
140
            }
141 39
            $em->persist($repligard_entry);
142 39
            $em->getUnitOfWork()->computeChangeSet($em->getClassMetadata(connection::get_fqcn('midgard_repligard')), $repligard_entry);
143
        }
144
    }
145
146 17
    private function on_remove(dbobject $entity, EntityManagerInterface $em)
147
    {
148 17
        if (!($entity instanceof repligard)) {
149 17
            $repligard_entry = $em->getRepository(connection::get_fqcn('midgard_repligard'))->findOneBy(['guid' => $entity->guid]);
0 ignored issues
show
Bug Best Practice introduced by
The property $guid is declared protected in midgard\portable\api\dbobject. Since you implement __get, consider adding a @property or @property-read.
Loading history...
150 17
            if (empty($repligard_entry)) {
151
                connection::log()->error('No repligard entry found for GUID ' . $entity->guid);
152
            } else {
153 17
                $repligard_entry->object_action = self::ACTION_PURGE;
154 17
                $em->persist($repligard_entry);
155 17
                $em->getUnitOfWork()->computeChangeSet($em->getClassMetadata(connection::get_fqcn('midgard_repligard')), $repligard_entry);
156
            }
157
        }
158
    }
159
160 97
    private function calculate_size(ClassMetadata $cm, metadata $entity) : int
161
    {
162 97
        $size = 0;
163 97
        foreach ($cm->getAssociationNames() as $name) {
164 93
            $size += strlen($entity->$name);
165
        }
166 97
        foreach ($cm->getFieldNames() as $name) {
167 97
            $size += strlen($entity->$name);
168
        }
169 97
        return $size;
170
    }
171
172 6
    public function onSchemaCreateTable(SchemaCreateTableEventArgs $args)
173
    {
174 6
        $platform = $args->getPlatform();
175 6
        $columns = $args->getColumns();
176 6
        $modified = false;
177
178 6
        foreach ($columns as $name => &$config) {
179 6
            if ($platform instanceof SqlitePlatform) {
180 6
                if (   !empty($config['comment'])
181 6
                    && $config['comment'] == 'BINARY') {
182 1
                    $modified = true;
183 1
                    $config['columnDefinition'] = $config['type']->getSQLDeclaration($config, $platform) . ' COLLATE BINARY' . $platform->getDefaultValueDeclarationSQL($config);
184
                }
185
            }
186 6
            if ($platform instanceof AbstractMySQLPlatform) {
187
                if (!empty($config['comment'])) {
188
                    if ($config['comment'] == 'BINARY') {
189
                        $modified = true;
190
                        $config['columnDefinition'] = $config['type']->getSQLDeclaration($config, $platform) . ' CHARACTER SET utf8 COLLATE utf8_bin' . $platform->getDefaultValueDeclarationSQL($config);
191
                    }
192
                    if (substr(strtolower(trim($config['comment'])), 0, 3) == 'set') {
193
                        $modified = true;
194
                        $config['columnDefinition'] = $config['comment'] . $platform->getDefaultValueDeclarationSQL($config);
195
                    }
196
                    if (!empty($config['columnDefinition']) && $platform->supportsInlineColumnComments()) {
197
                        $config['columnDefinition'] .=  " COMMENT " . $platform->quoteStringLiteral($config['comment']);
198
                    }
199
                }
200
            }
201
        }
202
203 6
        if (!$modified) {
204 5
            return;
205
        }
206
207 1
        $args->preventDefault();
208
209
        //The following is basically copied from the respective Doctrine function, since there seems to be no way
210
        //to just modify columns and pass them back to the SchemaManager
211 1
        $table = $args->getTable();
212 1
        $options = $args->getOptions();
213
214 1
        $queryFields = $platform->getColumnDeclarationListSQL($columns);
215
216 1
        if (!empty($options['uniqueConstraints'])) {
217
            foreach ($options['uniqueConstraints'] as $name => $definition) {
218
                $queryFields .= ', ' . $platform->getUniqueConstraintDeclarationSQL($name, $definition);
219
            }
220
        }
221
222 1
        if (!empty($options['foreignKeys'])) {
223
            foreach ($options['foreignKeys'] as $foreignKey) {
224
                $queryFields .= ', ' . $platform->getForeignKeyDeclarationSQL($foreignKey);
225
            }
226
        }
227
228 1
        $name = str_replace('.', '__', $table->getName());
229 1
        $args->addSql('CREATE TABLE ' . $name . ' (' . $queryFields . ')');
230
231 1
        if (isset($options['alter']) && true === $options['alter']) {
232
            return;
233
        }
234
235 1
        if (!empty($options['indexes'])) {
236
            foreach ($options['indexes'] as $indexDef) {
237
                $args->addSql($platform->getCreateIndexSQL($indexDef, $name));
238
            }
239
        }
240
241 1
        if (!empty($options['unique'])) {
242
            foreach ($options['unique'] as $indexDef) {
243
                $args->addSql($platform->getCreateIndexSQL($indexDef, $name));
244
            }
245
        }
246
    }
247
248
    /**
249
     * This function contains workarounds for reading existing Midgard databases
250
     *
251
     * ENUM fields are converted to string for now (Like in the XML reader)
252
     */
253 2
    public function onSchemaColumnDefinition(SchemaColumnDefinitionEventArgs $args)
254
    {
255 2
        $column = array_change_key_case($args->getTableColumn(), CASE_LOWER);
256 2
        $type = strtok($column['type'], '()');
257
258 2
        if ($type == 'enum') {
259
            $options = [
260
                'length' => 255,
261
                'default' => $column['default'] ?? null,
262
                'notnull' => $column['null'] != 'YES',
263
                'comment' => $column['type']
264
            ];
265
266
            $args->preventDefault();
267
            $args->setColumn(new Column($column['field'], Type::getType(Types::STRING), $options));
268
        }
269
    }
270
271
    /**
272
     * This is mostly a workaround for the fact that SchemaTool wants to create FKs on
273
     * each run since it doesn't detect that MyISAM tables don't support them
274
     *
275
     * @see https://github.com/doctrine/orm/issues/4270
276
     * @param GenerateSchemaTableEventArgs $args
277
     */
278 4
    public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $args)
279
    {
280 4
        $table = $args->getClassTable();
281 4
        if (   !$table->hasOption('engine')
282 4
            || $table->getOption('engine') !== 'MyISAM') {
283
            return;
284
        }
285 4
        foreach ($table->getForeignKeys() as $key) {
286 3
            $table->removeForeignKey($key->getName());
287
        }
288
    }
289
290 9
    public function getSubscribedEvents()
291
    {
292
        return [
293 9
            Events::postLoad,
294
            Events::onFlush,
295
            dbal_events::onSchemaCreateTable, dbal_events::onSchemaColumnDefinition,
296
            ToolEvents::postGenerateSchemaTable
297
        ];
298
    }
299
}
300