Passed
Push — master ( 67a020...fe0e28 )
by butschster
04:26 queued 17s
created

ConfigDeclaration   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 205
Duplicated Lines 0 %

Test Coverage

Coverage 99.07%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 31
eloc 96
dl 0
loc 205
ccs 107
cts 108
cp 0.9907
rs 9.92
c 1
b 1
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A declare() 0 14 1
A __construct() 0 14 1
A singularize() 0 5 1
A classify() 0 5 1
A getInstructions() 0 11 1
B declareGettersByKey() 0 41 9
A makeConfigFilename() 0 3 1
A phpDocSeeReference() 0 5 1
A touchConfigFile() 0 12 1
A declareGetters() 0 27 6
A create() 0 16 4
A makeGetterName() 0 14 4
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\Dumper;
10
use Nette\PhpGenerator\Literal;
11
use Spiral\Boot\DirectoriesInterface;
12
use Spiral\Core\InjectableConfig;
13
use Spiral\Files\FilesInterface;
14
use Spiral\Reactor\FileDeclaration;
15
use Spiral\Reactor\Partial\Method;
16
use Spiral\Scaffolder\Config\ScaffolderConfig;
17
use Spiral\Scaffolder\Exception\ScaffolderException;
18
19
use function Spiral\Scaffolder\defineArrayType;
20
21
class ConfigDeclaration extends AbstractDeclaration implements HasInstructions
22
{
23
    public const TYPE = 'config';
24
25 8
    public function __construct(
26
        ScaffolderConfig $config,
27
        protected readonly FilesInterface $files,
28
        protected readonly DirectoriesInterface $dirs,
29
        protected readonly SlugifyInterface $slugify,
30
        protected readonly ConfigDeclaration\TypeAnnotations $typeAnnotations,
31
        protected readonly ConfigDeclaration\TypeHints $typeHints,
32
        protected readonly ConfigDeclaration\Defaults $defaultValues,
33
        protected string $name,
34
        protected ?string $comment = null,
35
        private readonly string $directory = '',
36
        ?string $namespace = null,
37
    ) {
38 8
        parent::__construct($config, $name, $comment, $namespace);
39
    }
40
41 8
    public function create(bool $reverse, string $configName): void
42
    {
43 8
        $this->class->addConstant('CONFIG', $configName)->setPublic();
44
45 8
        $filename = $this->makeConfigFilename($configName);
46 8
        if ($reverse) {
47 3
            if (!$this->files->exists($filename)) {
48
                throw new ScaffolderException(\sprintf("Config filename %s doesn't exist", $filename));
49
            }
50
51 3
            $defaultsFromFile = require $filename;
52 3
            $this->declareGetters($defaultsFromFile);
53
54 3
            $this->class->getProperty('config')->setValue($this->defaultValues->get($defaultsFromFile));
55 5
        } elseif (!$this->files->exists($filename)) {
56 3
            $this->touchConfigFile($filename);
57
        }
58
    }
59
60
    /**
61
     * Declare constant and property.
62
     */
63 8
    public function declare(): void
64
    {
65 8
        $this->namespace->addUse(InjectableConfig::class);
66
67 8
        $this->class->setExtends(InjectableConfig::class);
68 8
        $this->class->setFinal();
69
70 8
        $this->class
71 8
            ->addProperty('config')
72 8
            ->setProtected()
73 8
            ->setType('array')
74 8
            ->setValue([])
75 8
            ->setComment(
76 8
                <<<'DOC'
77
                Default values for the config.
78
                Will be merged with application config in runtime.
79 8
                DOC,
80 8
            );
81
    }
82
83 8
    public function getInstructions(): array
84
    {
85 8
        $configFile = $this->makeConfigFilename(
86 8
            $this->class->getConstant('CONFIG')->getValue()
87 8
        );
88
89 8
        $configFile = \str_replace($this->dirs->get('root'), '', $configFile);
90
91 8
        return [
92 8
            \sprintf('You can now add your config values to the \'<comment>%s</comment>\' file.', $configFile),
93 8
            'Read more about Config Objects in the documentation: https://spiral.dev/docs/framework-config',
94 8
        ];
95
    }
96
97 8
    private function makeConfigFilename(string $filename): string
98
    {
99 8
        return \sprintf('%s%s.php', $this->directory, $filename);
100
    }
101
102 3
    private function touchConfigFile(string $filename): void
103
    {
104 3
        $this->files->touch($filename);
105
106 3
        $file = new FileDeclaration();
107 3
        $file->setComment($this->phpDocSeeReference());
108
109 3
        $this->files->write(
110 3
            $filename,
111 3
            $file->render() . PHP_EOL . (new Dumper())->dump(new Literal('return [];')),
112 3
            FilesInterface::READONLY,
113 3
            true,
114 3
        );
115
    }
116
117 3
    private function phpDocSeeReference(): string
118
    {
119 3
        $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

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