Issues (34)

src/Command/InstallDirectoryCommand.php (5 issues)

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\Command;
15
16
use Symfony\Component\Console\Command\Command;
17
use Symfony\Component\Console\Input\InputArgument;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\Yaml\Yaml;
21
use Tardigrades\SectionField\Service\ApplicationManagerInterface;
22
use Tardigrades\SectionField\Service\FieldManagerInterface;
23
use Tardigrades\SectionField\Service\FieldTypeManagerInterface;
24
use Tardigrades\SectionField\Service\FieldTypeNotFoundException;
25
use Tardigrades\SectionField\Service\LanguageManagerInterface;
26
use Tardigrades\SectionField\Service\NotFoundException;
27
use Tardigrades\SectionField\Service\SectionManagerInterface;
28
use Tardigrades\SectionField\ValueObject\ApplicationConfig;
29
use Tardigrades\SectionField\ValueObject\ConfigWithHandleInterface;
30
use Tardigrades\SectionField\ValueObject\FieldConfig;
31
use Tardigrades\SectionField\ValueObject\FullyQualifiedClassName;
32
use Tardigrades\SectionField\ValueObject\LanguageConfig;
33
use Tardigrades\SectionField\ValueObject\SectionConfig;
34
use Tardigrades\SectionField\ValueObject\Type;
35
36
class InstallDirectoryCommand extends Command
37
{
38
    /** @var ApplicationConfig[] */
39
    private $applications = [];
40
41
    /** @var ?LanguageConfig */
42
    private $languages = null;
43
44
    /** @var SectionConfig[] */
45
    private $sections = [];
46
47
    /** @var FieldConfig[] */
48
    private $fields = [];
49
50
    /** @var ApplicationManagerInterface */
51
    private $applicationManager;
52
53
    /** @var LanguageManagerInterface */
54
    private $languageManager;
55
56
    /** @var SectionManagerInterface */
57
    private $sectionManager;
58
59
    /** @var FieldManagerInterface */
60
    private $fieldManager;
61
62
    /** @var FieldTypeManagerInterface */
63
    private $fieldTypeManager;
64
65
    public function __construct(
66
        ApplicationManagerInterface $applicationManager,
67
        LanguageManagerInterface $languageManager,
68
        SectionManagerInterface $sectionManager,
69
        FieldManagerInterface $fieldManager,
70
        FieldTypeManagerInterface $fieldTypeManager
71
    ) {
72
        parent::__construct('sf:install-directory');
73
        $this->applicationManager = $applicationManager;
74
        $this->languageManager = $languageManager;
75
        $this->sectionManager = $sectionManager;
76
        $this->fieldManager = $fieldManager;
77
        $this->fieldTypeManager = $fieldTypeManager;
78
    }
79
80
    protected function configure(): void
81
    {
82
        $this
83
            ->setDescription('Install all configuration in a directory')
84
            ->setHelp('This command installs all .yml files in a directory, recursively. Pass it the path to' .
85
                'the directory, for example "app/config/my-sexy-field-config".')
86
            ->addArgument('directory', InputArgument::REQUIRED, 'The config directory');
87
    }
88
89
    /**
90
     * @param InputInterface $input
91
     * @param OutputInterface $output
92
     * @throws \Exception
93
     */
94
    protected function execute(InputInterface $input, OutputInterface $output): void
95
    {
96
        foreach (static::getAllYamls($input->getArgument('directory')) as $fileName => $config) {
97
            $this->classifyFile($fileName, $config);
98
        }
99
100
        $this->verifyConfig();
101
102
        $this->createLanguages($this->languages);
0 ignored issues
show
It seems like $this->languages can also be of type null; however, parameter $languages of Tardigrades\Command\Inst...mand::createLanguages() does only seem to accept Tardigrades\SectionField...ueObject\LanguageConfig, maybe add an additional type check? ( Ignorable by Annotation )

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

102
        $this->createLanguages(/** @scrutinizer ignore-type */ $this->languages);
Loading history...
103
        $output->writeln("<info>Languages created!</info>");
104
105
        $applicationCount = $this->createApplications($this->applications);
106
        $output->writeln("<info>$applicationCount applications created!</info>");
107
108
        $fieldTypeCount = $this->installFieldTypes($this->fields);
109
        $output->writeln("<info>$fieldTypeCount field types installed!</info>");
110
111
        $fieldCount = $this->createFields($this->fields);
112
        $output->writeln("<info>$fieldCount fields created!</info>");
113
114
        $sectionCount = $this->createSections($this->sections);
115
        $output->writeln("<info>$sectionCount sections created!</info>");
116
    }
117
118
    /**
119
     * @throws \Exception
120
     */
121
    private function verifyConfig(): void
122
    {
123
        if (count($this->applications) === 0) {
124
            throw new \Exception("Could not find any application config files");
125
        }
126
        if (is_null($this->languages)) {
127
            throw new \Exception("Could not find a language config file");
128
        }
129
    }
130
131
    /**
132
     * @param FieldConfig[] $fields
133
     * @return int the number of field types detected and installed
134
     */
135
    private function installFieldTypes(array $fields): int
136
    {
137
        $fieldTypes = [];
138
        foreach ($fields as $field) {
139
            $fieldType = $field->toArray()['field']['type'];
140
            if (!in_array($fieldType, $fieldTypes)) {
141
                $fieldTypes[] = $fieldType;
142
            }
143
        }
144
        // All custom fieldtypes are public services, therefore we can find its namespace through its classname.
145
        $debugString =  `bin/console debug:container`;
146
        foreach ($fieldTypes as $fieldType) {
147
            try {
148
                $this->fieldTypeManager->readByType(Type::fromString($fieldType));
149
                // Already installed, so continue
150
                // There's no clean way of updating field types
151
            } catch (FieldTypeNotFoundException $exception) {
152
                // Not installed, so install
153
                if ($fieldType === 'DateTimeField') {
154
                    // DateTime has "Field" at the end of its name to avoid confusion with \DateTime.
155
                    // All other field types follow Tardigrades\FieldType\{fieldType}\{fieldType}.
156
                    // This solution is hacky, but there's no good way to detect classes before they've been loaded.
157
                    $className = "Tardigrades\\FieldType\\DateTime\\$fieldType";
158
                } else {
159
                    // 2 steps to prepare the data. First all the characters between words are reduced to ' ',
160
                    // then all the words are extracted to an array.
161
                    $workingString = trim(preg_replace('!\s+!', ' ', $debugString));
162
                    $arrayOfWords = explode(' ', $workingString);
163
                    foreach ($arrayOfWords as $word) {
164
                        if (strpos($word, $fieldType) !== false) {
165
                            // We still need to make sure it's an exact match.
166
                            $items = explode('\\', $word);
167
                            foreach ($items as $item) {
168
                                if ($item === $fieldType) {
169
                                    $className = $word;
170
                                    break;
171
                                }
172
                            }
173
                        } else {
174
                            $className = "Tardigrades\\FieldType\\$fieldType\\$fieldType";
175
                        }
176
                    }
177
                }
178
                $this->fieldTypeManager->createWithFullyQualifiedClassName(
179
                    FullyQualifiedClassName::fromString($className)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $className does not seem to be defined for all execution paths leading up to this point.
Loading history...
180
                );
181
            }
182
        }
183
184
        return count($fieldTypes);
185
    }
186
187
    /**
188
     * @param SectionConfig[] $sections
189
     * @return int the number of sections created
190
     */
191
    private function createSections(array $sections): int
192
    {
193
        return $this->createOrUpdateConfigs($sections, $this->sectionManager);
194
    }
195
196
    /**
197
     * @param FieldConfig[] $fields
198
     * @return int the number of fields created
199
     */
200
    private function createFields(array $fields): int
201
    {
202
        return $this->createOrUpdateConfigs($fields, $this->fieldManager);
203
    }
204
205
    /**
206
     * @param ApplicationConfig[] $applications
207
     * @return int the number of applications created
208
     */
209
    private function createApplications(array $applications): int
210
    {
211
        return $this->createOrUpdateConfigs($applications, $this->applicationManager);
212
    }
213
214
    /**
215
     * @param ConfigWithHandleInterface[] $configs
216
     * @param ApplicationManagerInterface|FieldManagerInterface|SectionManagerInterface $manager
217
     * @return int
218
     */
219
    private function createOrUpdateConfigs(array $configs, $manager): int
220
    {
221
        foreach ($configs as $config) {
222
            try {
223
                $handle = $config->getHandle();
224
                $object = $manager->readByHandle($handle);
225
                $manager->updateByConfig($config, $object);
0 ignored issues
show
It seems like $object can also be of type Tardigrades\Entity\ApplicationInterface and Tardigrades\Entity\FieldInterface; however, parameter $section of Tardigrades\SectionField...rface::updateByConfig() does only seem to accept Tardigrades\Entity\SectionInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

225
                $manager->updateByConfig($config, /** @scrutinizer ignore-type */ $object);
Loading history...
It seems like $object can also be of type Tardigrades\Entity\FieldInterface and Tardigrades\Entity\SectionInterface; however, parameter $application of Tardigrades\SectionField...rface::updateByConfig() does only seem to accept Tardigrades\Entity\ApplicationInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

225
                $manager->updateByConfig($config, /** @scrutinizer ignore-type */ $object);
Loading history...
It seems like $object can also be of type Tardigrades\Entity\ApplicationInterface and Tardigrades\Entity\SectionInterface; however, parameter $field of Tardigrades\SectionField...rface::updateByConfig() does only seem to accept Tardigrades\Entity\FieldInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

225
                $manager->updateByConfig($config, /** @scrutinizer ignore-type */ $object);
Loading history...
226
            } catch (NotFoundException $exception) {
227
                $manager->createByConfig($config);
228
            }
229
        }
230
        return count($configs);
231
    }
232
233
    /**
234
     * @param LanguageConfig $languages
235
     */
236
    private function createLanguages(LanguageConfig $languages): void
237
    {
238
        $this->languageManager->createByConfig($languages);
239
    }
240
241
    /**
242
     * @param string $fileName
243
     * @param mixed $config
244
     * @throws \Exception
245
     */
246
    private function classifyFile(string $fileName, $config): void
247
    {
248
        if (!is_array($config)) {
249
            throw new \Exception("Malformed file $fileName");
250
        }
251
        $keys = array_keys($config);
252
        if (count($keys) !== 1) {
253
            throw new \Exception("Malformed file $fileName");
254
        }
255
        [$key] = $keys;
256
        switch ($key) {
257
            case 'field':
258
                $this->fields[] = FieldConfig::fromArray($config);
259
                break;
260
            case 'section':
261
                $this->sections[] = SectionConfig::fromArray($config);
262
                break;
263
            case 'application':
264
                $this->applications[] = ApplicationConfig::fromArray($config);
265
                break;
266
            case 'language':
267
                if (!is_nulL($this->languages)) {
268
                    throw new \Exception("Found multiple language config files");
269
                }
270
                $this->languages = LanguageConfig::fromArray($config);
271
                break;
272
            default:
273
                throw new \Exception("Could not identify file $fileName with key $key");
274
        }
275
    }
276
277
    /**
278
     * Get the contents of all yaml files in a directory, recursively.
279
     * @param string $directory
280
     * @return \Generator
281
     */
282
    private static function getAllYamls(string $directory): \Generator
283
    {
284
        /** @var \SplFileInfo $file */
285
        foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)) as $file) {
286
            if ($file->isFile() && $file->getExtension() === 'yml') {
287
                $fileName = $file->getPathname();
288
                yield $fileName => Yaml::parse(file_get_contents($fileName));
289
            }
290
        }
291
    }
292
}
293