Test Failed
Pull Request — master (#856)
by Maxim
06:34
created

ConfigDeclaration::declareGetters()   A

Complexity

Conditions 6
Paths 15

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 28
ccs 19
cts 19
cp 1
rs 9.0444
c 0
b 0
f 0
cc 6
nc 15
nop 1
crap 6
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
20
class ConfigDeclaration extends AbstractDeclaration
21
{
22
    public const TYPE = 'config';
23
24
    public function __construct(
25 6
        ScaffolderConfig $config,
26
        protected readonly FilesInterface $files,
27
        protected readonly SlugifyInterface $slugify,
28
        protected readonly ConfigDeclaration\TypeAnnotations $typeAnnotations,
29
        protected readonly ConfigDeclaration\TypeHints $typeHints,
30
        protected readonly ConfigDeclaration\Defaults $defaultValues,
31
        protected string $name,
32
        protected ?string $comment = null,
33
        private readonly string $directory = '',
34
        ?string $namespace = null
35
    ) {
36 6
        parent::__construct($config, $name, $comment, $namespace);
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 2
            $filename,
91 2
            $file->render() . PHP_EOL . (new Dumper())->dump(new Literal('return [];')),
92 2
            FilesInterface::READONLY,
93 2
            true
94 2
        );
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): void
108
    {
109 3
        $getters = [];
110 3
        $gettersByKey = [];
111 3
112
        foreach ($defaults as $key => $value) {
113 3
            $key = (string)$key;
114 3
            $getter = $this->makeGetterName($key);
115 3
            $getters[] = $getter;
116 3
117
            $method = $this->class->addMethod($getter)->setPublic();
118 3
            $method->setBody(\sprintf('return $this->config[\'%s\'];', $key));
119 3
            $method->setComment(\sprintf('@return %s', $this->typeAnnotations->getAnnotation($value)));
120 3
121
            if (\is_array($value)) {
122 3
                $gettersByKey[] = ['key' => $key, 'value' => $value];
123 2
            }
124
125
            $returnTypeHint = $this->typeHints->getHint(\gettype($value));
126 3
            if ($returnTypeHint !== null) {
127 3
                $method->setReturnType($returnTypeHint);
128 3
            }
129
        }
130
131
        foreach ($gettersByKey as $item) {
132 3
            $method = $this->declareGettersByKey($getters, $item['key'], $item['value']);
133 2
            if ($method !== null) {
134 2
                $getters[] = $method->getName();
135 2
            }
136
        }
137
    }
138
139 3
    private function declareGettersByKey(array $methodNames, string $key, array $value): ?Method
140
    {
141
        //Won't create if there's less than 2 sub-items
142 2
        if (\count($value) < 2) {
143
            return null;
144
        }
145 2
146 2
        $singularKey = $this->singularize($key);
147
        $name = $this->makeGetterName($singularKey);
148
        if (\in_array($name, $methodNames, true)) {
149 2
            $name = $this->makeGetterName($singularKey, 'get', 'by');
150 2
        }
151 2
152 2
        //Name conflict, won't merge
153
        if (\in_array($name, $methodNames, true)) {
154
            return null;
155
        }
156 2
157 2
        $keyType = defineArrayType(\array_keys($value), '-mixed-');
158
        $valueType = defineArrayType(\array_values($value), '-mixed-');
159
        //We need a fixed structure here
160 2
        if ($keyType === '-mixed-' || $valueType === '-mixed-') {
161 2
            return null;
162
        }
163 2
164 2
        //Won't create for associated arrays
165
        if ($this->typeAnnotations->mapType($keyType) === 'int' && \array_is_list($value)) {
166
            return null;
167
        }
168 2
169 2
        $method = $this->class->addMethod($name)->setPublic();
170
        $method->setBody(\sprintf('return $this->config[\'%s\'][$%s];', $key, $singularKey));
171
        $method->setReturnType($valueType);
172 2
        $method->setComment([
173 2
            \sprintf('@param %s %s', $this->typeAnnotations->mapType($keyType), $singularKey),
174 2
            \sprintf('@return %s', $this->typeAnnotations->getAnnotation(array_values($value)[0])),
175 2
        ]);
176 2
177 2
        $param = $method->addParameter($singularKey);
178 2
        $paramTypeHint = $this->typeHints->getHint($keyType);
179
        if ($paramTypeHint !== null) {
180 2
            $param->setType($paramTypeHint);
181 2
        }
182 2
183 2
        return $method;
184
    }
185
186 2
    private function makeGetterName(string $name, string $prefix = 'get', string $postfix = ''): string
187
    {
188
        $chunks = [];
189 3
        if (!empty($prefix)) {
190
            $chunks[] = $prefix;
191 3
        }
192 3
193 3
        $name = $this->slugify->slugify($name, ['lowercase' => false]);
194
        $chunks[] = \count($chunks) !== 0 ? $this->classify($name) : $name;
195
        if (!empty($postfix)) {
196 3
            $chunks[] = \ucfirst($postfix);
197 3
        }
198 3
199 2
        return \implode('', $chunks);
200
    }
201
202 3
    private function classify(string $name): string
203
    {
204
        return ( new InflectorFactory() )
205 3
            ->build()
206
            ->classify($name);
207 3
    }
208 3
209 3
    private function singularize(string $name): string
210
    {
211
        return ( new InflectorFactory() )
212 2
            ->build()
213
            ->singularize($name);
214 2
    }
215
}
216