Passed
Push — master ( 06cf74...574b00 )
by Arthur
02:51
created

GeneratorTrait::generateResources()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 30
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 0
1
<?php
2
3
namespace SoliDry\Controllers;
4
5
use SoliDry\Blocks\Controllers;
6
use SoliDry\Blocks\Entities;
7
use SoliDry\Blocks\FileManager;
8
use SoliDry\Blocks\FormRequest;
9
use SoliDry\Blocks\Migrations;
10
use SoliDry\Blocks\Routes;
11
use SoliDry\Blocks\Tests;
12
use SoliDry\Exceptions\DirectoryException;
13
use SoliDry\Helpers\Console;
14
use SoliDry\Types\ConsoleInterface;
15
use SoliDry\Types\CustomsInterface;
16
use SoliDry\Types\DefaultInterface;
17
use SoliDry\Types\DirsInterface;
18
use SoliDry\Types\ErrorsInterface;
19
use SoliDry\Types\PhpInterface;
20
use SoliDry\Types\ApiInterface;
21
use Symfony\Component\Yaml\Yaml;
22
23
/**
24
 * Trait GeneratorTrait
25
 *
26
 * @package SoliDry\Controllers
27
 */
28
trait GeneratorTrait
29
{
30
    // all generated entities/resources
31
    private $forms;
32
    private $mappers;
33
    private $routes;
34
    private $migrations;
35
    private $controllers;
36
    private $tests;
37
38
    // gen dir found in history
39
    private $genDir;
40
41
    /**
42
     * Standard generation
43
     */
44
    private function generateResources(): void
45
    {
46
        $this->outputEntity();
47
48
        // create controller
49
        $this->solveControllers();
50
51
        // create FormRequest
52
        $this->solveFormRequest();
53
54
        // create entities/models
55
        $this->solveEntities();
56
57
        // create routes
58
        $this->routes = new Routes($this);
59
        $this->routes->create();
60
61
        // create tests
62
        if (empty($this->options[ConsoleInterface::OPTION_TESTS]) === false) {
63
            try {
64
                FileManager::createPath($this->formatFuncTestsPath());
0 ignored issues
show
Bug introduced by
It seems like formatFuncTestsPath() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

64
                FileManager::createPath($this->/** @scrutinizer ignore-call */ formatFuncTestsPath());
Loading history...
65
            } catch (DirectoryException $e) {
66
                $this->error($e->getTraceAsString());
0 ignored issues
show
Bug introduced by
It seems like error() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

66
                $this->/** @scrutinizer ignore-call */ 
67
                       error($e->getTraceAsString());
Loading history...
67
            }
68
69
            $this->tests = new Tests($this);
70
            $this->tests->createEntity($this->formatFuncTestsPath(), DefaultInterface::FUNCTIONAL_POSTFIX);
71
        }
72
73
        $this->createMigrations();
74
    }
75
76
    /**
77
     *  Generation with merge option
78
     */
79
    private function mergeResources(): void
80
    {
81
        $this->outputEntity();
82
        $this->solveControllers();
83
84
        $this->solveFormRequest();
85
86
        $this->solveEntities();
87
88
        // create routes
89
        $this->routes = new Routes($this);
90
        $this->routes->create();
91
92
        $this->createMigrations();
93
    }
94
95
    /**
96
     *  Creates Controllers and leaves those generated in case of merge
97
     */
98
    private function solveControllers(): void
99
    {
100
        $this->controllers = new Controllers($this);
101
        $controllerPath = $this->formatControllersPath();
0 ignored issues
show
Bug introduced by
It seems like formatControllersPath() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

101
        /** @scrutinizer ignore-call */ 
102
        $controllerPath = $this->formatControllersPath();
Loading history...
102
        if (empty($this->options[ConsoleInterface::OPTION_REGENERATE]) === false
103
            && file_exists($this->controllers->getEntityFile($controllerPath,
104
                DefaultInterface::CONTROLLER_POSTFIX)) === true) {
105
            $this->controllers->recreateEntity($controllerPath, DefaultInterface::CONTROLLER_POSTFIX);
106
        } else {
107
            $this->controllers->createDefault();
108
            $this->controllers->createEntity($controllerPath, DefaultInterface::CONTROLLER_POSTFIX);
109
        }
110
    }
111
112
    private function solveFormRequest(): void
113
    {
114
        $this->forms = new FormRequest($this);
115
        $formRequestPath = $this->formatRequestsPath();
0 ignored issues
show
Bug introduced by
It seems like formatRequestsPath() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

115
        /** @scrutinizer ignore-call */ 
116
        $formRequestPath = $this->formatRequestsPath();
Loading history...
116
        if (empty($this->options[ConsoleInterface::OPTION_REGENERATE]) === false
117
            && file_exists($this->forms->getEntityFile($formRequestPath,
118
                DefaultInterface::FORM_REQUEST_POSTFIX)) === true) {
119
            $this->forms->recreateEntity($formRequestPath, DefaultInterface::FORM_REQUEST_POSTFIX);
120
        } else {
121
            $this->forms->createEntity($formRequestPath, DefaultInterface::FORM_REQUEST_POSTFIX);
122
        }
123
    }
124
125
    /**
126
     *  Decide whether to generate new Entities or mutate existing
127
     */
128
    private function solveEntities(): void
129
    {
130
        // create entities/models
131
        $this->mappers = new Entities($this);
132
        $this->mappers->createPivot();
133
        $entitiesPath = $this->formatEntitiesPath();
0 ignored issues
show
Bug introduced by
It seems like formatEntitiesPath() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

133
        /** @scrutinizer ignore-call */ 
134
        $entitiesPath = $this->formatEntitiesPath();
Loading history...
134
        if (empty($this->options[ConsoleInterface::OPTION_MERGE]) === false
135
            && file_exists($this->forms->getEntityFile($entitiesPath)) === true) {
136
            $this->mappers->recreateEntity($entitiesPath);
137
        } else {
138
            $this->mappers->createEntity($entitiesPath);
139
        }
140
    }
141
142
    private function outputEntity(): void
143
    {
144
        Console::out(
145
            '===============' . PhpInterface::SPACE . $this->objectName
146
            . PhpInterface::SPACE . DirsInterface::ENTITIES_DIR
147
        );
148
    }
149
150
    /**
151
     *  Creates migrations for every entity if there is merge option - adds additional
152
     */
153
    private function createMigrations(): void
154
    {
155
        if (empty($this->options[ConsoleInterface::OPTION_MIGRATIONS]) === false) {
156
            $this->migrations = new Migrations($this);
157
            $this->migrations->create();
158
            $this->migrations->createPivot();
159
        }
160
    }
161
162
    /**
163
     *  Collects all attrs, Types and diffs for further code-generation
164
     *
165
     * @throws \Symfony\Component\Yaml\Exception\ParseException
166
     */
167
    private function setMergedTypes(): void
168
    {
169
        $opMerge = $this->options[ConsoleInterface::OPTION_MERGE];
170
        $timeCheck = strtotime($opMerge); // only for validation - with respect to diff timezones
171
172
        if (false !== $timeCheck) {
173
            try {
174
                $this->mergeTime($opMerge);
175
            } catch (DirectoryException $e) {
176
                $this->error($e->getTraceAsString());
177
            }
178
        } else if (is_numeric($opMerge) !== false) {
179
            $this->mergeStep($opMerge);
180
        } else if ($opMerge === ConsoleInterface::MERGE_DEFAULT_VALUE) {
181
            $this->mergeLast();
182
        }
183
    }
184
185
    /**
186
     * Merges history OAS files with current by time in the past
187
     *
188
     * @param string $opMerge
189
     * @throws \Symfony\Component\Yaml\Exception\ParseException
190
     * @throws DirectoryException
191
     */
192
    private function mergeTime(string $opMerge): void
193
    {
194
        $dateTime = explode(PhpInterface::SPACE, $opMerge);
195
        $this->composeTypes($this->composeTimeFiles($dateTime), $this->files);
196
    }
197
198
    /**
199
     * @param array $dateTime
200
     * @return array
201
     * @throws DirectoryException
202
     */
203
    private function composeTimeFiles(array $dateTime): array
204
    {
205
        $time = str_replace(':', '', $dateTime[1]);
206
207
        $this->genDir = $dateTime[0];
208
        $path = DirsInterface::GEN_DIR . DIRECTORY_SEPARATOR . $this->genDir . DIRECTORY_SEPARATOR;
209
210
        if (is_dir($path) === false) {
211
            throw new DirectoryException('The directory: ' . $path . ' was not found.',
212
                ErrorsInterface::CODE_DIR_NOT_FOUND);
213
        }
214
215
        $files = glob($path . $time . '*');
216
        foreach ($files as &$fullPath) {
217
            $fullPath = str_replace($path, '', $fullPath);
218
        }
219
220
        $files = array_diff($files, DirsInterface::EXCLUDED_DIRS);
221
222
        return $this->adjustFiles($files);
223
    }
224
225
    /**
226
     * Merges history OAS files with current by backward steps
227
     *
228
     * @param int $step
229
     * @throws \Symfony\Component\Yaml\Exception\ParseException
230
     */
231
    private function mergeStep(int $step): void
232
    {
233
        $dirs = scandir(DirsInterface::GEN_DIR . DIRECTORY_SEPARATOR, SCANDIR_SORT_DESCENDING);
234
        if ($dirs !== false) {
235
            $dirs = array_diff($dirs, DirsInterface::EXCLUDED_DIRS);
236
            $this->composeTypes($this->composeStepFiles($dirs, $step), $this->files);
237
        }
238
    }
239
240
    /**
241
     * Composes files for step back in history via .gen dir
242
     *
243
     * @param array $dirs
244
     * @param int $step
245
     * @return array
246
     */
247
    private function composeStepFiles(array $dirs, int $step): array
248
    {
249
        $filesToPass = [];
250
        foreach ($dirs as $dir) {
251
            $files = scandir(DirsInterface::GEN_DIR . DIRECTORY_SEPARATOR . $dir, SCANDIR_SORT_DESCENDING);
252
            $files = array_diff($files, DirsInterface::EXCLUDED_DIRS);
253
254
            $prefixFlag = '';
255
            foreach ($files as $kFile => $file) {
256
                $prefix = substr($file, 0, 6); // Hms
257
                $template = '/^' . $prefix . '.*$/i';
258
259
                if ($prefix !== $prefixFlag) {
260
                    --$step;
261
                    $prefixFlag = $prefix;
262
                    if ($step > 0) {
263
                        $skip = preg_grep($template, $files);
264
                        $files = array_diff($files, $skip);
265
                    }
266
                }
267
268
                if ($step <= 0) {
269
                    $files = preg_grep($template, $files);
270
                    $this->genDir = $dir;
271
                    $filesToPass = $files;
272
                    break 2;
273
                }
274
            }
275
        }
276
277
        return $this->adjustFiles($filesToPass);
278
    }
279
280
    /**
281
     *  Merges current state with the last one
282
     *
283
     * @throws \Symfony\Component\Yaml\Exception\ParseException
284
     */
285
    private function mergeLast(): void
286
    {
287
        $lastFiles = $this->getLastFiles();
288
        if (empty($lastFiles) === false) {
289
            $this->composeTypes($lastFiles, $this->files);
290
        }
291
    }
292
293
    /**
294
     * Gets last files according to main file named "openapi" by spec of OAS
295
     * and it's included files defined in "uses" property
296
     *
297
     * @return array
298
     * @throws \Symfony\Component\Yaml\Exception\ParseException
299
     */
300
    private function getLastFiles(): array
301
    {
302
        $dirs = scandir(DirsInterface::GEN_DIR . DIRECTORY_SEPARATOR, SCANDIR_SORT_DESCENDING);
303
        if ($dirs !== false) {
304
            $dirs = array_diff($dirs, DirsInterface::EXCLUDED_DIRS);
305
            $this->genDir = $dirs[0]; // desc last date YYYY-mm-dd
306
307
            $files = scandir(DirsInterface::GEN_DIR . DIRECTORY_SEPARATOR . $this->genDir, SCANDIR_SORT_DESCENDING);
308
            $files = array_diff($files, DirsInterface::EXCLUDED_DIRS);
309
310
            $lastFiles = [];
311
            foreach ($files as $file) {
312
                if (($pos = strpos($file, ApiInterface::OPEN_API_KEY)) !== false) {
313
                    $lastFiles[] = $file;
314
                    $content = Yaml::parse(file_get_contents($this->formatGenPathByDir() . $file));
0 ignored issues
show
Bug introduced by
It seems like formatGenPathByDir() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

314
                    $content = Yaml::parse(file_get_contents($this->/** @scrutinizer ignore-call */ formatGenPathByDir() . $file));
Loading history...
315
                    if (empty($content[ApiInterface::RAML_KEY_USES]) === false) {
316
                        foreach ($content[ApiInterface::RAML_KEY_USES] as $subFile) {
317
                            $lastFiles[] = substr($file, 0, $pos) . basename($subFile);
318
                        }
319
                    }
320
                    break;
321
                }
322
            }
323
324
            return $this->adjustFiles($lastFiles);
325
        }
326
327
        return [];
328
    }
329
330
    /**
331
     * Gets history files and merges them with current OAS files
332
     *
333
     * @param array $files      files from .gen/ dir saved history
334
     * @param array $inputFiles file that were passed as an option + files from uses RAML property
335
     * @throws \Symfony\Component\Yaml\Exception\ParseException
336
     */
337
    private function composeTypes(array $files, array $inputFiles): void
338
    {
339
        $attrsCurrent = [];
340
        $attrsHistory = [];
341
342
        $path = DirsInterface::GEN_DIR . DIRECTORY_SEPARATOR . $this->genDir . DIRECTORY_SEPARATOR;
343
        foreach ($files as $file) {
344
            foreach ($inputFiles as $inFile) {
345
346
                if (mb_strpos($file, basename($inFile), null, PhpInterface::ENCODING_UTF8) !== false) {
347
                    $dataCurrent = Yaml::parse(file_get_contents($inFile));
348
                    $dataHistory = Yaml::parse(file_get_contents($path . $file));
349
350
                    $this->currentTypes = $dataCurrent[ApiInterface::API_COMPONENTS][ApiInterface::API_SCHEMAS];
0 ignored issues
show
Bug Best Practice introduced by
The property currentTypes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
351
                    $this->historyTypes = $dataHistory[ApiInterface::API_COMPONENTS][ApiInterface::API_SCHEMAS];
0 ignored issues
show
Bug Best Practice introduced by
The property historyTypes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
352
                    $this->types += array_merge_recursive($this->historyTypes, $this->currentTypes);
353
354
                    $attrsCurrent += array_filter($this->currentTypes, function ($k) {
355
                        return strpos($k, CustomsInterface::CUSTOM_TYPES_ATTRIBUTES) !== false;
356
                    }, ARRAY_FILTER_USE_KEY);
357
                    $attrsHistory += array_filter($this->historyTypes, function ($k) {
358
                        return strpos($k, CustomsInterface::CUSTOM_TYPES_ATTRIBUTES) !== false;
359
                    }, ARRAY_FILTER_USE_KEY);
360
                }
361
            }
362
        }
363
364
        $this->composeDiffs($attrsCurrent, $attrsHistory);
365
    }
366
367
    /**
368
     * Compares attributes for current and previous history and sets the diffTypes prop
369
     * to process additional migrations creation
370
     *
371
     * @param array $attrsCurrent Current attributes
372
     * @param array $attrsHistory History attributes
373
     */
374
    private function composeDiffs(array $attrsCurrent, array $attrsHistory): void
375
    {
376
        // make diffs on current array to add columns/indices to migrations
377
        foreach ($attrsCurrent as $k => $v) {
378
            if (empty($attrsHistory[$k][ApiInterface::RAML_PROPS]) === false
379
                && (empty($v[ApiInterface::RAML_PROPS]) === false)) {
380
381
                foreach ($v[ApiInterface::RAML_PROPS] as $attr => $attrValue) {
382
                    if (empty($attrsHistory[$k][ApiInterface::RAML_PROPS][$attr])) { // if there is no such element in history data - collect
383
                        $this->diffTypes[$k][$attr] = $attrValue;
0 ignored issues
show
Bug Best Practice introduced by
The property diffTypes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
384
                    }
385
                }
386
            }
387
        }
388
389
        // reflect array from history to append lost props
390
        foreach ($attrsHistory as $k => $v) {
391
            if (empty($attrsCurrent[$k][ApiInterface::RAML_PROPS]) === false
392
                && (empty($v[ApiInterface::RAML_PROPS]) === false)) {
393
394
                foreach ($v[ApiInterface::RAML_PROPS] as $attr => $attrValue) {
395
                    if (empty($attrsCurrent[$k][ApiInterface::RAML_PROPS][$attr])) { // if there is no such element in current data - collect
396
                        $this->diffTypes[$k][$attr] = $attrValue;
397
                    }
398
                }
399
            }
400
        }
401
    }
402
403
    /**
404
     * Gets an unordered array of files and returns an ordered one
405
     * stating from *openapi.yaml
406
     *
407
     * @param array $files
408
     * @return array
409
     */
410
    private function adjustFiles(array $files): array
411
    {
412
        $tmpFile = '';
413
        foreach ($files as $k => $file) {
414
            if (strpos($file, ApiInterface::OPEN_API_KEY) !== false) {
415
                $tmpFile = $file;
416
                unset($files[$k]);
417
                break;
418
            }
419
        }
420
        array_unshift($files, $tmpFile);
421
422
        return $files;
423
    }
424
}