Test Failed
Push — master ( a309d2...0e4b58 )
by butschster
17:34 queued 09:02
created

ConfigDeclaration::create()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 17
ccs 8
cts 9
cp 0.8889
rs 9.9
c 0
b 0
f 0
cc 4
nc 4
nop 2
crap 4.0218
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 7
    private bool $reverse = false;
25
26
    public function __construct(
27
        ScaffolderConfig $config,
28
        protected readonly FilesInterface $files,
29
        protected readonly DirectoriesInterface $dirs,
30
        protected readonly SlugifyInterface $slugify,
31
        protected readonly ConfigDeclaration\TypeAnnotations $typeAnnotations,
32
        protected readonly ConfigDeclaration\TypeHints $typeHints,
33
        protected readonly ConfigDeclaration\Defaults $defaultValues,
34
        protected string $name,
35
        protected ?string $comment = null,
36 7
        private readonly string $directory = '',
37
        ?string $namespace = null,
38
    ) {
39 7
        parent::__construct($config, $name, $comment, $namespace);
40
    }
41 7
42
    public function create(bool $reverse, string $configName): void
43 7
    {
44 7
        $this->reverse = $reverse;
45 3
        $this->class->addConstant('CONFIG', $configName)->setPublic();
46
47
        $filename = $this->makeConfigFilename($configName);
48
        if ($reverse) {
49 3
            if (!$this->files->exists($filename)) {
50 3
                throw new ScaffolderException(\sprintf("Config filename %s doesn't exist", $filename));
51
            }
52 3
53
            $defaultsFromFile = require $filename;
54 4
            $this->declareGetters($defaultsFromFile);
55 2
56
            $this->class->getProperty('config')->setValue($this->defaultValues->get($defaultsFromFile));
57
        } elseif (!$this->files->exists($filename)) {
58
            $this->touchConfigFile($filename);
59
        }
60
    }
61
62
    /**
63 7
     * Declare constant and property.
64
     */
65 7
    public function declare(): void
66
    {
67 7
        $this->namespace->addUse(InjectableConfig::class);
68 7
69
        $this->class->setExtends(InjectableConfig::class);
70 7
        $this->class->setFinal();
71 7
72 7
        $this->class
73 7
            ->addProperty('config')
74 7
            ->setProtected()
75 7
            ->setType('array')
76 7
            ->setValue([])
77
            ->setComment(
78
                <<<'DOC'
79 7
                Default values for the config.
80 7
                Will be merged with application config in runtime.
81
                DOC,
82
            );
83 7
    }
84
85 7
    public function getInstructions(): array
86
    {
87
        $configFile = $this->makeConfigFilename(
88 2
            $this->class->getConstant('CONFIG')->getValue()
89
        );
90 2
91
        $configFile = \str_replace($this->dirs->get('root'), '', $configFile);
92 2
93 2
        return [
94
            \sprintf('You can now add your config values to the \'<comment>%s</comment>\' file.', $configFile),
95 2
            'Read more about Config Objects in the documentation: https://spiral.dev/docs/framework-config',
96 2
        ];
97 2
    }
98 2
99 2
    private function makeConfigFilename(string $filename): string
100 2
    {
101
        return \sprintf('%s%s.php', $this->directory, $filename);
102
    }
103 2
104
    private function touchConfigFile(string $filename): void
105 2
    {
106
        $this->files->touch($filename);
107 2
108
        $file = new FileDeclaration();
109
        $file->setComment($this->phpDocSeeReference());
110 3
111
        $this->files->write(
112 3
            $filename,
113 3
            $file->render() . PHP_EOL . (new Dumper())->dump(new Literal('return [];')),
114
            FilesInterface::READONLY,
115 3
            true,
116 3
        );
117 3
    }
118 3
119
    private function phpDocSeeReference(): string
120 3
    {
121 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

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