StubCreator::generateApi()   B
last analyzed

Complexity

Conditions 8
Paths 8

Size

Total Lines 55
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 45
c 0
b 0
f 0
dl 0
loc 55
rs 7.9555
cc 8
nc 8
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/** @noinspection PhpArrayShapeAttributeCanBeAddedInspection */
3
4
/** @noinspection PhpInternalEntityUsedInspection */
5
6
7
namespace TgScraper\Common;
8
9
use InvalidArgumentException;
10
use Nette\PhpGenerator\Helpers;
11
use Nette\PhpGenerator\PhpFile;
12
use Nette\PhpGenerator\PhpNamespace;
13
use Nette\PhpGenerator\Type;
14
use TgScraper\TgScraper;
15
16
/**
17
 * Class StubCreator
18
 * @package TgScraper\Common
19
 */
20
class StubCreator
21
{
22
23
24
    /**
25
     * @var string
26
     */
27
    private string $namespace;
28
    /**
29
     * @var array
30
     */
31
    private array $abstractClasses = [];
32
    /**
33
     * @var array
34
     */
35
    private array $extendedClasses = [];
36
37
    /**
38
     * StubCreator constructor.
39
     * @param array $schema
40
     * @param string $namespace
41
     * @throws InvalidArgumentException
42
     */
43
    public function __construct(private array $schema, string $namespace = '')
44
    {
45
        if (str_ends_with($namespace, '\\')) {
46
            $namespace = substr($namespace, 0, -1);
47
        }
48
        if (!empty($namespace)) {
49
            if (!Helpers::isNamespaceIdentifier($namespace)) {
50
                throw new InvalidArgumentException('Namespace invalid');
51
            }
52
        }
53
        if (!TgScraper::validateSchema($this->schema)) {
54
            throw new InvalidArgumentException('Schema invalid');
55
        }
56
        $this->getExtendedTypes();
57
        $this->namespace = $namespace;
58
    }
59
60
    /**
61
     * Builds the abstract and the extended class lists.
62
     */
63
    private function getExtendedTypes(): void
64
    {
65
        foreach ($this->schema['types'] as $type) {
66
            if (!empty($type['extended_by'])) {
67
                $this->abstractClasses[] = $type['name'];
68
                foreach ($type['extended_by'] as $extendedType) {
69
                    $this->extendedClasses[$extendedType] = $type['name'];
70
                }
71
            }
72
        }
73
    }
74
75
    /**
76
     * @param string $str
77
     * @return string
78
     */
79
    private static function toCamelCase(string $str): string
80
    {
81
        return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $str))));
82
    }
83
84
    /**
85
     * @param array $fieldTypes
86
     * @param PhpNamespace $phpNamespace
87
     * @return array
88
     */
89
    private function parseFieldTypes(array $fieldTypes, PhpNamespace $phpNamespace): array
90
    {
91
        $types = [];
92
        $comments = [];
93
        foreach ($fieldTypes as $fieldType) {
94
            $comments[] = $fieldType;
95
            if (str_starts_with($fieldType, 'Array')) {
96
                $types[] = 'array';
97
                continue;
98
            }
99
            if (ucfirst($fieldType) == $fieldType) {
100
                $fieldType = $phpNamespace->getName() . '\\' . $fieldType;
101
            }
102
            $types[] = $fieldType;
103
        }
104
        $comments = empty($comments) ? '' : sprintf('@var %s', implode('|', $comments));
105
        return [
106
            'types' => implode('|', $types),
107
            'comments' => $comments
108
        ];
109
    }
110
111
    /**
112
     * @param array $apiTypes
113
     * @param PhpNamespace $phpNamespace
114
     * @return array
115
     */
116
    private function parseApiFieldTypes(array $apiTypes, PhpNamespace $phpNamespace): array
117
    {
118
        $types = [];
119
        $comments = [];
120
        foreach ($apiTypes as $apiType) {
121
            $comments[] = $apiType;
122
            if (str_starts_with($apiType, 'Array')) {
123
                $types[] = 'array';
124
                $text = $apiType;
125
                while (preg_match('/Array<(.+)>/', $text, $matches) === 1) {
126
                    $text = $matches[1];
127
                }
128
                $subTypes = explode('|', $text);
129
                foreach ($subTypes as $subType) {
130
                    if (ucfirst($subType) == $subType) {
131
                        $subType = $this->namespace . '\\Types\\' . $subType;
132
                        $phpNamespace->addUse($subType);
133
                    }
134
                }
135
                continue;
136
            }
137
            if (ucfirst($apiType) == $apiType) {
138
                $apiType = $this->namespace . '\\Types\\' . $apiType;
139
                $phpNamespace->addUse($apiType);
140
            }
141
            $types[] = $apiType;
142
        }
143
        $comments = empty($comments) ? '' : sprintf('@param %s', implode('|', $comments));
144
        return [
145
            'types' => implode('|', $types),
146
            'comments' => $comments
147
        ];
148
    }
149
150
    /**
151
     * @param string $namespace
152
     * @return PhpFile[]
153
     */
154
    private function generateDefaultTypes(string $namespace): array
155
    {
156
        $interfaceFile = new PhpFile;
157
        $interfaceNamespace = $interfaceFile->addNamespace($namespace);
158
        $interfaceNamespace->addInterface('TypeInterface');
159
        $responseFile = new PhpFile;
160
        $responseNamespace = $responseFile->addNamespace($namespace);
161
        $responseNamespace->addUse('stdClass');
162
        $response = $responseNamespace->addClass('Response');
163
        $response->addProperty('ok')
164
            ->setPublic()
165
            ->setType(Type::BOOL);
0 ignored issues
show
Deprecated Code introduced by
The constant Nette\PhpGenerator\Type::BOOL has been deprecated: use Type::Bool ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

165
            ->setType(/** @scrutinizer ignore-deprecated */ Type::BOOL);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
166
        $response->addProperty('result')
167
            ->setPublic()
168
            ->setType(sprintf('stdClass|%s\\TypeInterface|array|int|string|bool', $namespace))
169
            ->setNullable()
170
            ->setValue(null);
171
        $response->addProperty('errorCode')
172
            ->setPublic()
173
            ->setType(Type::INT)
0 ignored issues
show
Deprecated Code introduced by
The constant Nette\PhpGenerator\Type::INT has been deprecated: use Type::Int ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

173
            ->setType(/** @scrutinizer ignore-deprecated */ Type::INT)

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
174
            ->setNullable()
175
            ->setValue(null);
176
        $response->addProperty('description')
177
            ->setPublic()
178
            ->setType(Type::STRING)
0 ignored issues
show
Deprecated Code introduced by
The constant Nette\PhpGenerator\Type::STRING has been deprecated: use Type::String ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

178
            ->setType(/** @scrutinizer ignore-deprecated */ Type::STRING)

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
179
            ->setNullable()
180
            ->setValue(null);
181
        $response->addProperty('parameters')
182
            ->setPublic()
183
            ->setType(sprintf('stdClass|%s\\ResponseParameters', $namespace))
184
            ->setNullable()
185
            ->setValue(null);
186
        $response->addImplement($namespace . '\\TypeInterface');
187
        return [
188
            'Response' => $responseFile,
189
            'TypeInterface' => $interfaceFile
190
        ];
191
    }
192
193
    /**
194
     * @return PhpFile[]
195
     */
196
    private function generateTypes(): array
197
    {
198
        $namespace = $this->namespace . '\\Types';
199
        $types = $this->generateDefaultTypes($namespace);
200
        foreach ($this->schema['types'] as $type) {
201
            $file = new PhpFile;
202
            $phpNamespace = $file->addNamespace($namespace);
203
            $typeClass = $phpNamespace->addClass($type['name']);
204
            if (in_array($type['name'], $this->abstractClasses)) {
205
                $typeClass->setAbstract();
206
            }
207
            if (array_key_exists($type['name'], $this->extendedClasses)) {
208
                $typeClass->setExtends($namespace . '\\' . $this->extendedClasses[$type['name']]);
209
            } else {
210
                $typeClass->addImplement($namespace . '\\TypeInterface');
211
            }
212
            foreach ($type['fields'] as $field) {
213
                ['types' => $fieldTypes, 'comments' => $fieldComments] = $this->parseFieldTypes(
214
                    $field['types'],
215
                    $phpNamespace
216
                );
217
                $fieldName = self::toCamelCase($field['name']);
218
                $typeProperty = $typeClass->addProperty($fieldName)
219
                    ->setPublic()
220
                    ->setType($fieldTypes);
221
                $default = $field['default'] ?? null;
222
                if (!empty($default)) {
223
                    $typeProperty->setValue($default);
224
                }
225
                if ($field['optional']) {
226
                    $typeProperty->setNullable();
227
                    if (!$typeProperty->isInitialized()) {
228
                        $typeProperty->setValue(null);
229
                    }
230
                    $fieldComments .= '|null';
231
                }
232
                if (!empty($fieldComments)) {
233
                    $fieldComments .= ' ' . $field['description'];
234
                    $typeProperty->addComment($fieldComments);
235
                }
236
            }
237
            $types[$type['name']] = $file;
238
        }
239
        return $types;
240
    }
241
242
    /**
243
     * @return PhpFile
244
     */
245
    private function generateApi(): PhpFile
246
    {
247
        $file = new PhpFile;
248
        $file->addComment('@noinspection PhpUnused');
249
        $file->addComment('@noinspection PhpUnusedParameterInspection');
250
        $phpNamespace = $file->addNamespace($this->namespace);
251
        $apiClass = $phpNamespace->addTrait('API');
252
        $sendRequest = $apiClass->addMethod('sendRequest')
253
            ->setPublic()
254
            ->setAbstract()
255
            ->setReturnType(Type::MIXED);
0 ignored issues
show
Deprecated Code introduced by
The constant Nette\PhpGenerator\Type::MIXED has been deprecated: use Type::Mixed ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

255
            ->setReturnType(/** @scrutinizer ignore-deprecated */ Type::MIXED);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
256
        $sendRequest->addParameter('method')
257
            ->setType(Type::STRING);
0 ignored issues
show
Deprecated Code introduced by
The constant Nette\PhpGenerator\Type::STRING has been deprecated: use Type::String ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

257
            ->setType(/** @scrutinizer ignore-deprecated */ Type::STRING);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
258
        $sendRequest->addParameter('args')
259
            ->setType(Type::ARRAY);
0 ignored issues
show
Deprecated Code introduced by
The constant Nette\PhpGenerator\Type::ARRAY has been deprecated: use Type::Array ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

259
            ->setType(/** @scrutinizer ignore-deprecated */ Type::ARRAY);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
260
        foreach ($this->schema['methods'] as $method) {
261
            $function = $apiClass->addMethod($method['name'])
262
                ->setPublic()
263
                ->addBody('$args = get_defined_vars();')
264
                ->addBody('return $this->sendRequest(__FUNCTION__, $args);');
265
            $function->addComment($method['description']);
266
            $fields = $method['fields'];
267
            usort(
268
                $fields,
269
                function ($a, $b) {
270
                    return $a['optional'] - $b['optional'];
271
                }
272
            );
273
            foreach ($fields as $field) {
274
                ['types' => $types, 'comments' => $comment] = $this->parseApiFieldTypes($field['types'], $phpNamespace);
275
                $fieldName = self::toCamelCase($field['name']);
276
                $parameter = $function->addParameter($fieldName)
277
                    ->setType($types);
278
                $default = $field['default'] ?? null;
279
                if (!empty($default) and (!is_string($default) or lcfirst($default) == $default)) {
280
                    $parameter->setDefaultValue($default);
281
                }
282
                if ($field['optional']) {
283
                    $parameter->setNullable();
284
                    if (!$parameter->hasDefaultValue()) {
285
                        $parameter->setDefaultValue(null);
286
                    }
287
                    $comment .= '|null';
288
                }
289
                $comment .= sprintf(' $%s %s', $fieldName, $field['description']);
290
                $function->addComment($comment);
291
            }
292
            ['types' => $returnTypes, 'comments' => $returnComment] = $this->parseApiFieldTypes(
293
                $method['return_types'],
294
                $phpNamespace
295
            );
296
            $function->setReturnType($returnTypes);
297
            $function->addComment(str_replace('param', 'return', $returnComment));
298
        }
299
        return $file;
300
    }
301
302
    /**
303
     * @return array{types: PhpFile[], api: PhpFile}
304
     */
305
    public function generateCode(): array
306
    {
307
        return [
308
            'types' => $this->generateTypes(),
309
            'api' => $this->generateApi()
310
        ];
311
    }
312
313
}