Passed
Push — master ( c776c7...570285 )
by Kirill
04:05
created

ConfigDeclaration::makeConfigFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * Spiral Framework. Scaffolder
5
 *
6
 * @license MIT
7
 * @author  Valentin V (vvval)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Scaffolder\Declaration;
13
14
use Cocur\Slugify\SlugifyInterface;
15
use Doctrine\Inflector\Rules\English\InflectorFactory;
16
use Spiral\Core\InjectableConfig;
17
use Spiral\Files\FilesInterface;
18
use Spiral\Reactor\ClassDeclaration;
19
use Spiral\Reactor\DependedInterface;
20
use Spiral\Reactor\FileDeclaration;
21
use Spiral\Reactor\Partial\Method;
22
use Spiral\Reactor\Partial\Source;
23
use Spiral\Scaffolder\Config\ScaffolderConfig;
24
use Spiral\Scaffolder\Exception\ScaffolderException;
25
26
use function Spiral\Scaffolder\defineArrayType;
27
use function Spiral\Scaffolder\isAssociativeArray;
28
29
class ConfigDeclaration extends ClassDeclaration implements DependedInterface
30
{
31
    /** @var ScaffolderConfig */
32
    private $config;
33
34
    /** @var FilesInterface */
35
    private $files;
36
37
    /** @var SlugifyInterface */
38
    private $slugify;
39
40
    /** @var ConfigDeclaration\TypeAnnotations */
41
    private $typeAnnotations;
42
43
    /** @var ConfigDeclaration\TypeHints */
44
    private $typeHints;
45
46
    /** @var ConfigDeclaration\Defaults */
47
    private $defaultValues;
48
49
    /** @var string */
50
    private $configName;
51
52
    /** @var string */
53
    private $directory;
54
55
    /**
56
     * @param ScaffolderConfig                  $config
57
     * @param FilesInterface                    $files
58
     * @param SlugifyInterface                  $slugify
59
     * @param ConfigDeclaration\TypeAnnotations $typeAnnotations
60
     * @param ConfigDeclaration\TypeHints       $typeHints
61
     * @param ConfigDeclaration\Defaults        $defaultValues
62
     * @param string                            $configName
63
     * @param string                            $name
64
     * @param string                            $comment
65
     * @param string                            $directory
66
     */
67
    public function __construct(
68
        ScaffolderConfig $config,
69
        FilesInterface $files,
70
        SlugifyInterface $slugify,
71
        ConfigDeclaration\TypeAnnotations $typeAnnotations,
72
        ConfigDeclaration\TypeHints $typeHints,
73
        ConfigDeclaration\Defaults $defaultValues,
74
        string $configName,
75
        string $name,
76
        string $comment = '',
77
        string $directory = ''
78
    ) {
79
        parent::__construct($name, 'InjectableConfig', [], $comment);
80
81
        $this->config = $config;
82
        $this->files = $files;
83
        $this->slugify = $slugify;
84
        $this->typeAnnotations = $typeAnnotations;
85
        $this->typeHints = $typeHints;
86
        $this->defaultValues = $defaultValues;
87
        $this->directory = $directory;
88
        $this->configName = $configName;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function getDependencies(): array
95
    {
96
        return [InjectableConfig::class => null];
97
    }
98
99
    /**
100
     * @param bool $reverse
101
     */
102
    public function create(bool $reverse): void
103
    {
104
        $filename = $this->makeConfigFilename($this->configName);
105
        if ($reverse) {
106
            if (!$this->files->exists($filename)) {
107
                throw new ScaffolderException("Config filename $filename doesn't exist");
108
            }
109
110
            $defaultsFromFile = require $filename;
111
            $this->declareGetters($defaultsFromFile);
112
            $this->declareStructure($this->configName, $this->defaultValues->get($defaultsFromFile));
113
        } else {
114
            if (!$this->files->exists($filename)) {
115
                $this->touchConfigFile($filename);
116
            }
117
118
            $this->declareStructure($this->configName, []);
119
        }
120
    }
121
122
    /**
123
     * @param string $filename
124
     * @return string
125
     */
126
    private function makeConfigFilename(string $filename): string
127
    {
128
        return "{$this->directory}{$filename}.php";
129
    }
130
131
    /**
132
     * @param string $filename
133
     */
134
    private function touchConfigFile(string $filename): void
135
    {
136
        $this->files->touch($filename);
137
138
        $file = new FileDeclaration();
139
        $file->setDirectives('strict_types=1');
140
        $file->setComment($this->phpDocSeeReference());
141
        $file->addElement(new Source(['', 'return [];']));
142
        $file->render();
143
144
        $this->files->write(
145
            $filename,
146
            $file->render(),
147
            FilesInterface::READONLY,
148
            true
149
        );
150
    }
151
152
    /**
153
     * @return string
154
     */
155
    private function phpDocSeeReference(): string
156
    {
157
        $namespace = trim($this->config->classNamespace('config', $this->getName()), '\\');
158
159
        return "@see \\$namespace\\{$this->getName()}";
160
    }
161
162
    /**
163
     * @param array $defaults
164
     * @return double[]|float[]
165
     */
166
    private function declareGetters(array $defaults): array
167
    {
168
        $output = [];
169
        $getters = [];
170
        $gettersByKey = [];
171
172
        foreach ($defaults as $key => $value) {
173
            $key = (string)$key;
174
            $getter = $this->makeGetterName($key);
175
            $getters[] = $getter;
176
177
            $method = $this->method($getter)->setPublic();
178
            $method->setSource("return \$this->config['$key'];");
179
            $method->setComment("@return {$this->typeAnnotations->getAnnotation($value)}");
180
181
            if (is_array($value)) {
182
                $gettersByKey[] = compact('key', 'value');
183
            }
184
185
            $returnTypeHint = $this->typeHints->getHint(gettype($value));
186
            if ($returnTypeHint !== null) {
187
                $method->setReturn($returnTypeHint);
188
            }
189
        }
190
191
        foreach ($gettersByKey as $item) {
192
            $method = $this->declareGettersByKey($getters, $item['key'], $item['value']);
193
            if ($method !== null) {
194
                $getters[] = $method->getName();
195
            }
196
        }
197
198
        return $output;
199
    }
200
201
    /**
202
     * @param array  $methodNames
203
     * @param string $key
204
     * @param array  $value
205
     * @return Method|null
206
     */
207
    private function declareGettersByKey(array $methodNames, string $key, array $value): ?Method
208
    {
209
        //Won't create if there's less than 2 sub-items
210
        if (count($value) < 2) {
211
            return null;
212
        }
213
214
        $singularKey = $this->singularize($key);
215
        $name = $this->makeGetterName($singularKey);
216
        if (in_array($name, $methodNames, true)) {
217
            $name = $this->makeGetterName($singularKey, 'get', 'by');
218
        }
219
220
        //Name conflict, won't merge
221
        if (in_array($name, $methodNames, true)) {
222
            return null;
223
        }
224
225
        $keyType = defineArrayType(array_keys($value), '-mixed-');
226
        $valueType = defineArrayType(array_values($value), '-mixed-');
227
        //We need a fixed structure here
228
        if ($keyType === '-mixed-' || $valueType === '-mixed-') {
229
            return null;
230
        }
231
232
        //Won't create for associated arrays
233
        if ($this->typeAnnotations->mapType($keyType) === 'int' && !isAssociativeArray($value)) {
234
            return null;
235
        }
236
237
        $method = $this->method($name)->setPublic();
238
        $method->setSource("return \$this->config['$key'][\$$singularKey];");
239
        $method->setReturn($valueType);
240
        $method->setComment([
241
            "@param {$this->typeAnnotations->mapType($keyType)} $singularKey",
242
            "@return {$this->typeAnnotations->getAnnotation(array_values($value)[0])}"
243
        ]);
244
245
        $param = $method->parameter($singularKey);
246
        $paramTypeHint = $this->typeHints->getHint($keyType);
247
        if ($paramTypeHint !== null) {
248
            $param->setType($paramTypeHint);
249
        }
250
251
        return $method;
252
    }
253
254
    /**
255
     * @param string $name
256
     * @param string $prefix
257
     * @param string $postfix
258
     * @return string
259
     */
260
    private function makeGetterName(string $name, string $prefix = 'get', string $postfix = ''): string
261
    {
262
        $chunks = [];
263
        if (!empty($prefix)) {
264
            $chunks[] = $prefix;
265
        }
266
267
        $name = $this->slugify->slugify($name, ['lowercase' => false]);
268
        $chunks[] = count($chunks) !== 0 ? $this->classify($name) : $name;
269
        if (!empty($postfix)) {
270
            $chunks[] = ucfirst($postfix);
271
        }
272
273
        return implode('', $chunks);
274
    }
275
276
    /**
277
     * Declare constant and property.
278
     *
279
     * @param string $configName
280
     * @param array  $defaults
281
     */
282
    private function declareStructure(string $configName, array $defaults): void
283
    {
284
        $this->constant('CONFIG')->setPublic()->setValue($configName);
285
        $this->property('config')->setProtected()->setDefaultValue($defaults)
286
            ->setComment('@internal For internal usage. Will be hydrated in the constructor.');
287
    }
288
289
    /**
290
     * @param string $name
291
     * @return string
292
     */
293
    private function classify(string $name): string
294
    {
295
        return ( new InflectorFactory() )
296
            ->build()
297
            ->classify($name);
298
    }
299
300
    /**
301
     * @param string $name
302
     * @return string
303
     */
304
    private function singularize(string $name): string
305
    {
306
        return ( new InflectorFactory() )
307
            ->build()
308
            ->singularize($name);
309
    }
310
}
311