Passed
Pull Request — master (#806)
by Maxim
19:17
created

ConfigDeclaration::declareGettersByKey()   B

Complexity

Conditions 9
Paths 11

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 25
nc 11
nop 3
dl 0
loc 45
ccs 26
cts 26
cp 1
crap 9
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Scaffolder\Declaration;
6
7
use Cocur\Slugify\SlugifyInterface;
8
use Doctrine\Inflector\Rules\English\InflectorFactory;
9
use Nette\PhpGenerator\Literal;
10
use Nette\PhpGenerator\Dumper;
11
use Spiral\Core\InjectableConfig;
12
use Spiral\Files\FilesInterface;
13
use Spiral\Reactor\FileDeclaration;
14
use Spiral\Reactor\Partial\Method;
15
use Spiral\Scaffolder\Config\ScaffolderConfig;
16
use Spiral\Scaffolder\Exception\ScaffolderException;
17
18
use function Spiral\Scaffolder\defineArrayType;
19
use function Spiral\Scaffolder\isAssociativeArray;
20
21
class ConfigDeclaration extends AbstractDeclaration
22
{
23
    public const TYPE = 'config';
24
25 6
    public function __construct(
26
        ScaffolderConfig $config,
27
        private readonly FilesInterface $files,
28
        private readonly SlugifyInterface $slugify,
29
        private readonly ConfigDeclaration\TypeAnnotations $typeAnnotations,
30
        private readonly ConfigDeclaration\TypeHints $typeHints,
31
        private readonly ConfigDeclaration\Defaults $defaultValues,
32
        string $name,
33
        ?string $comment = null,
34
        private readonly string $directory = ''
35
    ) {
36 6
        parent::__construct($config, $name, $comment);
37
    }
38
39 6
    public function create(bool $reverse, string $configName): void
40
    {
41 6
        $this->class->addConstant('CONFIG', $configName)->setPublic();
42
43 6
        $filename = $this->makeConfigFilename($configName);
44 6
        if ($reverse) {
45 3
            if (!$this->files->exists($filename)) {
46
                throw new ScaffolderException(\sprintf("Config filename %s doesn't exist", $filename));
47
            }
48
49 3
            $defaultsFromFile = require $filename;
50 3
            $this->declareGetters($defaultsFromFile);
51
52 3
            $this->class->getProperty('config')->setValue($this->defaultValues->get($defaultsFromFile));
53
        } else {
54 3
            if (!$this->files->exists($filename)) {
55 2
                $this->touchConfigFile($filename);
56
            }
57
        }
58
    }
59
60
    /**
61
     * Declare constant and property.
62
     */
63 6
    public function declare(): void
64
    {
65 6
        $this->namespace->addUse(InjectableConfig::class);
66
67 6
        $this->class->setExtends(InjectableConfig::class);
68
69 6
        $this->class
70 6
            ->addProperty('config')
71 6
            ->setProtected()
72 6
            ->setType('array')
73 6
            ->setValue([])
74 6
            ->setComment('@internal For internal usage. Will be hydrated in the constructor.');
75
    }
76
77 6
    private function makeConfigFilename(string $filename): string
78
    {
79 6
        return \sprintf('%s%s.php', $this->directory, $filename);
80
    }
81
82 2
    private function touchConfigFile(string $filename): void
83
    {
84 2
        $this->files->touch($filename);
85
86 2
        $file = new FileDeclaration();
87 2
        $file->setComment($this->phpDocSeeReference());
88
89 2
        $this->files->write(
90
            $filename,
91 2
            $file->render() . PHP_EOL . (new Dumper())->dump(new Literal('return [];')),
92
            FilesInterface::READONLY,
93
            true
94
        );
95
    }
96
97 2
    private function phpDocSeeReference(): string
98
    {
99 2
        $namespace = \trim($this->config->classNamespace('config', $this->class->getName()), '\\');
0 ignored issues
show
Bug introduced by
It seems like $this->class->getName() can also be of type null; however, parameter $name of Spiral\Scaffolder\Config...onfig::classNamespace() does only seem to accept string, 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

99
        $namespace = \trim($this->config->classNamespace('config', /** @scrutinizer ignore-type */ $this->class->getName()), '\\');
Loading history...
100
101 2
        return \sprintf('@see \%s\%s', $namespace, $this->class->getName());
102
    }
103
104
    /**
105
     * @return double[]|float[]
106
     */
107 3
    private function declareGetters(array $defaults): array
108
    {
109 3
        $output = [];
110 3
        $getters = [];
111 3
        $gettersByKey = [];
112
113 3
        foreach ($defaults as $key => $value) {
114 3
            $key = (string)$key;
115 3
            $getter = $this->makeGetterName($key);
116 3
            $getters[] = $getter;
117
118 3
            $method = $this->class->addMethod($getter)->setPublic();
119 3
            $method->setBody(\sprintf('return $this->config[\'%s\'];', $key));
120 3
            $method->setComment(\sprintf('@return %s', $this->typeAnnotations->getAnnotation($value)));
121
122 3
            if (\is_array($value)) {
123 2
                $gettersByKey[] = ['key' => $key, 'value' => $value];
124
            }
125
126 3
            $returnTypeHint = $this->typeHints->getHint(\gettype($value));
127 3
            if ($returnTypeHint !== null) {
128 3
                $method->setReturnType($returnTypeHint);
129
            }
130
        }
131
132 3
        foreach ($gettersByKey as $item) {
133 2
            $method = $this->declareGettersByKey($getters, $item['key'], $item['value']);
134 2
            if ($method !== null) {
135 2
                $getters[] = $method->getName();
136
            }
137
        }
138
139 3
        return $output;
140
    }
141
142 2
    private function declareGettersByKey(array $methodNames, string $key, array $value): ?Method
143
    {
144
        //Won't create if there's less than 2 sub-items
145 2
        if (\count($value) < 2) {
146 2
            return null;
147
        }
148
149 2
        $singularKey = $this->singularize($key);
150 2
        $name = $this->makeGetterName($singularKey);
151 2
        if (\in_array($name, $methodNames, true)) {
152 2
            $name = $this->makeGetterName($singularKey, 'get', 'by');
153
        }
154
155
        //Name conflict, won't merge
156 2
        if (\in_array($name, $methodNames, true)) {
157 2
            return null;
158
        }
159
160 2
        $keyType = defineArrayType(\array_keys($value), '-mixed-');
161 2
        $valueType = defineArrayType(\array_values($value), '-mixed-');
162
        //We need a fixed structure here
163 2
        if ($keyType === '-mixed-' || $valueType === '-mixed-') {
164 2
            return null;
165
        }
166
167
        //Won't create for associated arrays
168 2
        if ($this->typeAnnotations->mapType($keyType) === 'int' && !isAssociativeArray($value)) {
169 2
            return null;
170
        }
171
172 2
        $method = $this->class->addMethod($name)->setPublic();
173 2
        $method->setBody(\sprintf('return $this->config[\'%s\'][$%s];', $key, $singularKey));
174 2
        $method->setReturnType($valueType);
175 2
        $method->setComment([
176 2
            \sprintf('@param %s %s', $this->typeAnnotations->mapType($keyType), $singularKey),
177 2
            \sprintf('@return %s', $this->typeAnnotations->getAnnotation(array_values($value)[0])),
178
        ]);
179
180 2
        $param = $method->addParameter($singularKey);
181 2
        $paramTypeHint = $this->typeHints->getHint($keyType);
182 2
        if ($paramTypeHint !== null) {
183 2
            $param->setType($paramTypeHint);
184
        }
185
186 2
        return $method;
187
    }
188
189 3
    private function makeGetterName(string $name, string $prefix = 'get', string $postfix = ''): string
190
    {
191 3
        $chunks = [];
192 3
        if (!empty($prefix)) {
193 3
            $chunks[] = $prefix;
194
        }
195
196 3
        $name = $this->slugify->slugify($name, ['lowercase' => false]);
197 3
        $chunks[] = \count($chunks) !== 0 ? $this->classify($name) : $name;
198 3
        if (!empty($postfix)) {
199 2
            $chunks[] = \ucfirst($postfix);
200
        }
201
202 3
        return \implode('', $chunks);
203
    }
204
205 3
    private function classify(string $name): string
206
    {
207 3
        return ( new InflectorFactory() )
208 3
            ->build()
209 3
            ->classify($name);
210
    }
211
212 2
    private function singularize(string $name): string
213
    {
214 2
        return ( new InflectorFactory() )
215 2
            ->build()
216 2
            ->singularize($name);
217
    }
218
}
219