Completed
Push — master ( 00d60f...aabe5c )
by Dion
14s
created

DoctrineSectionManager::getRelationshipsOfAll()   B

Complexity

Conditions 7
Paths 29

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 8.3146
c 0
b 0
f 0
cc 7
nc 29
nop 0
1
<?php
2
3
/*
4
 * This file is part of the SexyField package.
5
 *
6
 * (c) Dion Snoeijen <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare (strict_types = 1);
13
14
namespace Tardigrades\SectionField\Service;
15
16
use Doctrine\ORM\EntityManagerInterface;
17
use Tardigrades\Entity\FieldInterface;
18
use Tardigrades\Entity\Section as SectionEntity;
19
use Tardigrades\Entity\SectionHistory;
20
use Tardigrades\Entity\SectionHistoryInterface;
21
use Tardigrades\Entity\SectionInterface;
22
use Tardigrades\SectionField\ValueObject\Handle;
23
use Tardigrades\SectionField\ValueObject\Id;
24
use Tardigrades\SectionField\ValueObject\SectionConfig;
25
use Tardigrades\SectionField\ValueObject\Version;
26
27
class DoctrineSectionManager implements SectionManagerInterface
28
{
29
    /** @var EntityManagerInterface */
30
    private $entityManager;
31
32
    /** @var DoctrineFieldManager */
33
    private $fieldManager;
34
35
    /** @var SectionHistoryManagerInterface */
36
    private $sectionHistoryManager;
37
38
    /** @var array */
39
    private $opposingRelationships = [
40
        'many-to-one' => 'one-to-many',
41
        'one-to-many' => 'many-to-one',
42
        'many-to-many' => 'many-to-many',
43
        'one-to-one' => 'one-to-one'
44
    ];
45
46
    /** @var array */
47
    private $opposingRealtionshipTypes = [
48
        'bidirectional' => 'unidirectional',
49
        'unidirectional' => 'bidirectional'
50
    ];
51
52
    public function __construct(
53
        EntityManagerInterface $entityManager,
54
        FieldManagerInterface $fieldManager,
55
        SectionHistoryManagerInterface $sectionHistoryManager
56
    ) {
57
        $this->entityManager = $entityManager;
58
        $this->fieldManager = $fieldManager;
0 ignored issues
show
Documentation Bug introduced by
$fieldManager is of type Tardigrades\SectionField...e\FieldManagerInterface, but the property $fieldManager was declared to be of type Tardigrades\SectionField...ce\DoctrineFieldManager. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
59
        $this->sectionHistoryManager = $sectionHistoryManager;
60
    }
61
62
    public function create(SectionInterface $entity): SectionInterface
63
    {
64
        $entity->setVersion(1);
65
        $this->entityManager->persist($entity);
66
        $this->entityManager->flush();
67
68
        return $entity;
69
    }
70
71
    public function read(Id $id): SectionInterface
72
    {
73
        $sectionRepository = $this->entityManager->getRepository(SectionEntity::class);
74
75
        /** @var $section SectionInterface */
76
        $section = $sectionRepository->find($id->toInt());
77
78
        if (empty($section)) {
79
            throw new SectionNotFoundException();
80
        }
81
82
        return $section;
83
    }
84
85
    public function readByIds(array $ids): array
86
    {
87
        $sectionIds = [];
88
        foreach ($ids as $id) {
89
            $sectionIds[] = '\'' . $id . '\'';
90
        }
91
        $whereIn = implode(',', $sectionIds);
92
        $query = $this->entityManager->createQuery(
93
            "SELECT section FROM Tardigrades\Entity\Section section WHERE section.id IN ({$whereIn})"
94
        );
95
        $results = $query->getResult();
96
        if (empty($results)) {
97
            throw new SectionNotFoundException();
98
        }
99
100
        return $results;
101
    }
102
103
    public function readAll(): array
104
    {
105
        $sectionRepository = $this->entityManager->getRepository(SectionEntity::class);
106
        $sections = $sectionRepository->findAll();
107
108
        if (empty($sections)) {
109
            throw new SectionNotFoundException();
110
        }
111
112
        return $sections;
113
    }
114
115
    public function update(SectionInterface $entity): void
116
    {
117
        $this->entityManager->persist($entity);
118
        $this->entityManager->flush();
119
    }
120
121
    public function delete(SectionInterface $entity): void
122
    {
123
        $this->entityManager->remove($entity);
124
        $this->entityManager->flush();
125
    }
126
127
    public function createByConfig(SectionConfig $sectionConfig): SectionInterface
128
    {
129
        $section = new SectionEntity();
130
        $section->setVersion(1);
131
        $this->updateByConfig($sectionConfig, $section);
132
133
        return $section;
134
    }
135
136
    /**
137
     * Restore an old version of a section by it's handle and version
138
     *
139
     * 1. Fetch the currently active section
140
     * 2. Copy the section history data to the active section entity, including the version
141
     * 3. Move the currently active section to history
142
     * 4. Clean the field associations from the updated active section
143
     * 5. Fetch the fields from the 'old' section config
144
     * 6. Assign the fields to the section
145
     * 7. Persist the active section
146
     *
147
     * This section might be generated from an updated config yml. Meaning the current config might
148
     * not comply with the active section configuration anymore.
149
     *
150
     * This will only update the config stored in the database. The generators will have to be called
151
     * to make the version change complete.
152
     *
153
     * @param $sectionFromHistory
154
     * @return SectionInterface
155
     */
156
    public function restoreFromHistory(SectionInterface $sectionFromHistory): SectionInterface
157
    {
158
        /** @var SectionInterface $activeSection */
159
        $activeSection = $this->readByHandle($sectionFromHistory->getHandle()); // 1
160
161
        /** @var SectionInterface $newSectionHistory */
162
        $newSectionHistory = $this->copySectionDataToSectionHistoryEntity($activeSection); // 2
163
        $newSectionHistory->setVersioned(new \DateTime()); // 3
0 ignored issues
show
Bug introduced by
The method setVersioned() does not exist on Tardigrades\Entity\SectionInterface. Did you maybe mean setVersion()? ( Ignorable by Annotation )

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

163
        $newSectionHistory->/** @scrutinizer ignore-call */ 
164
                            setVersioned(new \DateTime()); // 3

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
164
        $version = $this->getHighestVersion($newSectionHistory->getHandle());
165
        $newSectionHistory->setVersion(1 + $version->toInt());
166
        $this->sectionHistoryManager->create($newSectionHistory); // 3
167
168
        $updatedActiveSection = $this->copySectionHistoryDataToSectionEntity($sectionFromHistory, $activeSection); // 4
169
170
        $updatedActiveSection->removeFields(); // 5
171
        $this->entityManager->persist($updatedActiveSection);
172
        $this->entityManager->flush();
173
174
        $fields = $this->fieldManager->readByHandles($updatedActiveSection->getConfig()->getFields()); // 6
175
176
        foreach ($fields as $field) {
177
            $updatedActiveSection->addField($field);
178
        } // 7
179
180
        $this->entityManager->persist($updatedActiveSection);
181
        $this->entityManager->flush();
182
183
        return $updatedActiveSection;
184
    }
185
186
    /**
187
     * A section config is stored in the section history before it's updated.
188
     *
189
     * 1. Copy the data from the Section entity to the SectionHistory entity
190
     * 2. Persist it in the entity history
191
     * 3. Bump the version (+1)
192
     * 4. Clear the field many to many relationships
193
     * 5. Fetch the fields based on the config
194
     * 6. Add them to the Section entity
195
     * 7. Set the fields with the config values
196
     * 8. Set the config
197
     * 9. Persist the entity
198
     *
199
     * This new section might be created from an updated config yml. It's recommended to copy the old
200
     * config yml before you update it. That way the config yml's history will be in compliance with the
201
     * config stored in the database.
202
     *
203
     * This will only update the config stored in the database. The generators will have to be called
204
     * to make the version change complete.
205
     *
206
     * @param SectionConfig $sectionConfig
207
     * @param SectionInterface $section
208
     * @param bool $history
209
     *
210
     * @return SectionInterface
211
     */
212
    public function updateByConfig(
213
        SectionConfig $sectionConfig,
214
        SectionInterface $section,
215
        bool $history = false
216
    ): SectionInterface {
217
218
        if ($history) {
219
            /** @var SectionInterface $sectionHistory */
220
            $sectionHistory = $this->copySectionDataToSectionHistoryEntity($section); // 1
221
            $sectionHistory->setVersioned(new \DateTime()); // 2
222
            $this->sectionHistoryManager->create($sectionHistory); // 2
223
            $version = $this->getHighestVersion($section->getHandle());
224
            $section->setVersion(1 + $version->toInt()); // 3
225
        }
226
227
        $section->removeFields(); // 4
228
229
        $fields = $this->fieldManager->readByHandles($sectionConfig->getFields()); // 5
230
231
        foreach ($fields as $field) {
232
            $section->addField($field);
233
        } // 6
234
235
        $section->setName((string) $sectionConfig->getName()); // 7
236
        $section->setHandle((string) $sectionConfig->getHandle()); // 7
237
        $section->setConfig($sectionConfig->toArray()); // 8
238
239
        $this->entityManager->persist($section); // 9
240
        $this->entityManager->flush(); // 9
241
242
        return $section;
243
    }
244
245
    public function getHighestVersion(Handle $handle): Version
246
    {
247
        $version = Version::fromInt(1);
248
        $handle = (string) $handle;
249
        // @codingStandardsIgnoreStart
250
251
        $query = $this->entityManager->createQuery(
252
            "SELECT MAX(section.version) FROM Tardigrades\Entity\SectionHistory section WHERE section.handle = '{$handle}'"
253
        );
254
255
        // @codingStandardsIgnoreEnd
256
257
        $results = $query->getResult();
258
        if (empty($results)) {
259
            return $version;
260
        }
261
262
        return Version::fromInt((int) $results[0][1]);
263
    }
264
265
    public function readByHandle(Handle $handle): SectionInterface
266
    {
267
        $sectionRepository = $this->entityManager->getRepository(SectionEntity::class);
268
269
        /** @var SectionEntity $section */
270
        $section = $sectionRepository->findBy(['handle' => $handle]);
271
272
        if (empty($section)) {
273
            throw new SectionNotFoundException();
274
        }
275
276
        return $section[0];
277
    }
278
279
    public function readByHandles(array $handles): array
280
    {
281
        $sectionHandles = [];
282
        foreach ($handles as $handle) {
283
            $sectionHandles[] = '\'' . $handle . '\'';
284
        }
285
        $whereIn = implode(',', $sectionHandles);
286
        $query = $this->entityManager->createQuery(
287
            "SELECT section FROM Tardigrades\Entity\Section section WHERE section.handle IN ({$whereIn})"
288
        );
289
        $results = $query->getResult();
290
        if (empty($results)) {
291
            throw new SectionNotFoundException();
292
        }
293
294
        return $results;
295
    }
296
297
    public function getRelationshipsOfAll(): array
298
    {
299
        $relationships = [];
300
        $sections = $this->readAll();
301
        /** @var SectionInterface $section */
302
        foreach ($sections as $section) {
303
            $fields = $this->fieldManager->readByHandles($section->getConfig()->getFields());
304
305
            $sectionHandle = (string) $section->getHandle();
306
            if (!isset($relationships[$sectionHandle])) {
307
                $relationships[$sectionHandle] = [];
308
            }
309
310
            /** @var FieldInterface $field */
311
            foreach ($fields as $field) {
312
                try {
313
                    $fieldHandle = (string) $field->getHandle();
314
                    $relationships[$sectionHandle][$fieldHandle] = [
315
                        'kind' => $field->getConfig()->getRelationshipKind(),
316
                        'to' => $field->getConfig()->getRelationshipTo(),
317
                        'from' => $sectionHandle,
318
                        'fullyQualifiedClassName' => $field->getFieldType()->getFullyQualifiedClassName()
319
                    ];
320
321
                    $fieldConfig = $field->getConfig()->toArray();
322
323
                    if (!empty($fieldConfig['field']['relationship-type'])) {
324
                        $relationships[$sectionHandle][$fieldHandle]['relationship-type'] =
325
                            $fieldConfig['field']['relationship-type'];
326
                    }
327
328
                    if (!empty($fieldConfig['field']['from'])) {
329
                        $relationships[$sectionHandle][$fieldHandle]['from'] = $fieldConfig['field']['from'];
330
                    }
331
                } catch (\Exception $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
332
                }
333
            }
334
        }
335
336
        $relationships = $this->fillOpposingRelationshipSides($relationships);
337
338
        return $relationships;
339
    }
340
341
    private function copySectionDataToSectionHistoryEntity(SectionInterface $section): SectionHistoryInterface
342
    {
343
        $sectionHistory = new SectionHistory();
344
345
        $sectionHistory->setVersion(($section->getVersion()->toInt()));
346
        $sectionHistory->setConfig($section->getConfig()->toArray());
347
        $sectionHistory->setName((string) $section->getName());
348
        $sectionHistory->setHandle((string) $section->getHandle());
349
        $sectionHistory->setCreated($section->getCreated());
350
        $sectionHistory->setUpdated($section->getUpdated());
351
        $sectionHistory->setSection($section);
352
353
        return $sectionHistory;
354
    }
355
356
    private function copySectionHistoryDataToSectionEntity(
357
        SectionInterface $sectionHistory,
358
        SectionInterface $section
359
    ): SectionInterface {
360
361
        $section->setVersion($sectionHistory->getVersion()->toInt());
362
        $section->setConfig($sectionHistory->getConfig()->toArray());
363
        $section->setHandle((string) $sectionHistory->getHandle());
364
        $section->setName((string) $sectionHistory->getName());
365
        $section->setCreated($sectionHistory->getCreated());
366
        $section->setUpdated($sectionHistory->getUpdated());
367
368
        return $section;
369
    }
370
371
    private function fillOpposingRelationshipSides(array $relationships): array
372
    {
373
        foreach ($relationships as $sectionHandle => $relationshipFields) {
374
            if (count($relationshipFields)) {
375
                foreach ($relationshipFields as $fieldHandle => $kindToFieldType) {
376
                    # Unidirectional associations don't fill opposing relationships
377
                    if (isset($kindToFieldType['relationship-type']) &&
378
                        $kindToFieldType['relationship-type'] === 'unidirectional') {
379
                        continue;
380
                    }
381
                    // At this point, use two fields for the many to many relationship, one on each side.
382
                    // So ignore it here.
383
                    if ($kindToFieldType['kind'] !== 'many-to-many' && $kindToFieldType['kind'] !== 'one-to-one') {
384
                        $relationships[$kindToFieldType['to']][$fieldHandle . '-opposite'] = [
385
                            'kind' => $this->opposingRelationships[$kindToFieldType['kind']],
386
                            'to' => $sectionHandle,
387
                            'fullyQualifiedClassName' => $kindToFieldType['fullyQualifiedClassName']
388
                        ];
389
                        if (!empty($kindToFieldType['relationship-type'])) {
390
                            $relationships[$kindToFieldType['to']][$fieldHandle . '-opposite']['relationship-type'] =
391
                                $this->opposingRealtionshipTypes[$kindToFieldType['relationship-type']];
392
                        }
393
                    }
394
                }
395
            }
396
        }
397
398
        return $relationships;
399
    }
400
}
401