Passed
Push — master ( 137960...4d87d9 )
by Sys
11:11
created

StubCreator::__construct()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 15
rs 9.6111
cc 5
nc 10
nop 2
1
<?php
2
/** @noinspection PhpInternalEntityUsedInspection */
3
4
5
namespace TgScraper\Common;
6
7
8
use InvalidArgumentException;
9
use JetBrains\PhpStorm\ArrayShape;
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
     */
42
    public function __construct(private array $schema, string $namespace = '')
43
    {
44
        if (str_ends_with($namespace, '\\')) {
45
            $namespace = substr($namespace, 0, -1);
46
        }
47
        if (!empty($namespace)) {
48
            if (!Helpers::isNamespaceIdentifier($namespace)) {
49
                throw new InvalidArgumentException('Namespace invalid');
50
            }
51
        }
52
        if (!TgScraper::validateSchema($this->schema)) {
53
            throw new InvalidArgumentException('Schema invalid');
54
        }
55
        $this->getExtendedTypes();
56
        $this->namespace = $namespace;
57
    }
58
59
    /**
60
     * Builds the abstract and the extended class lists.
61
     */
62
    private function getExtendedTypes()
63
    {
64
        foreach ($this->schema['types'] as $type) {
65
            if (!empty($type['extended_by'])) {
66
                $this->abstractClasses[] = $type['name'];
67
                foreach ($type['extended_by'] as $extendedType) {
68
                    $this->extendedClasses[$extendedType] = $type['name'];
69
                }
70
            }
71
        }
72
    }
73
74
    /**
75
     * @param string $str
76
     * @return string
77
     */
78
    private static function toCamelCase(string $str): string
79
    {
80
        return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $str))));
81
    }
82
83
    /**
84
     * @param array $fieldTypes
85
     * @param PhpNamespace $phpNamespace
86
     * @return array
87
     */
88
    #[ArrayShape([
89
        'types' => "string",
90
        'comments' => "string"
91
    ])]
92
    private function parseFieldTypes(array $fieldTypes, PhpNamespace $phpNamespace): array
93
    {
94
        $types = [];
95
        $comments = [];
96
        foreach ($fieldTypes as $fieldType) {
97
            $comments[] = $fieldType;
98
            if (str_starts_with($fieldType, 'Array')) {
99
                $types[] = 'array';
100
                continue;
101
            }
102
            if (ucfirst($fieldType) == $fieldType) {
103
                $fieldType = $phpNamespace->getName() . '\\' . $fieldType;
104
            }
105
            $types[] = $fieldType;
106
        }
107
        $comments = empty($comments) ? '' : sprintf('@var %s', implode('|', $comments));
108
        return [
109
            'types' => implode('|', $types),
110
            'comments' => $comments
111
        ];
112
    }
113
114
    /**
115
     * @param array $apiTypes
116
     * @param PhpNamespace $phpNamespace
117
     * @return array
118
     */
119
    #[ArrayShape([
120
        'types' => "string",
121
        'comments' => "string"
122
    ])]
123
    private function parseApiFieldTypes(array $apiTypes, PhpNamespace $phpNamespace): array
124
    {
125
        $types = [];
126
        $comments = [];
127
        foreach ($apiTypes as $apiType) {
128
            $comments[] = $apiType;
129
            if (str_starts_with($apiType, 'Array')) {
130
                $types[] = 'array';
131
                continue;
132
            }
133
            if (ucfirst($apiType) == $apiType) {
134
                $apiType = $this->namespace . '\\Types\\' . $apiType;
135
                $phpNamespace->addUse($apiType);
136
            }
137
            $types[] = $apiType;
138
        }
139
        $comments = empty($comments) ? '' : sprintf('@var %s', implode('|', $comments));
140
        return [
141
            'types' => implode('|', $types),
142
            'comments' => $comments
143
        ];
144
    }
145
146
    /**
147
     * @param string $namespace
148
     * @return PhpFile[]
149
     */
150
    #[ArrayShape([
151
        'Response' => "\Nette\PhpGenerator\PhpFile",
152
        'TypeInterface' => "\Nette\PhpGenerator\ClassType"
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
        $response = $responseNamespace->addClass('Response')
162
            ->setType('class');
163
        $response->addProperty('ok')
164
            ->setPublic()
165
            ->setType(Type::BOOL);
166
        $response->addProperty('result')
167
            ->setPublic()
168
            ->setType(Type::MIXED)
169
            ->setNullable(true)
170
            ->setValue(null);
171
        $response->addProperty('errorCode')
172
            ->setPublic()
173
            ->setType(Type::INT)
174
            ->setNullable(true)
175
            ->setValue(null);
176
        $response->addProperty('description')
177
            ->setPublic()
178
            ->setType(Type::STRING)
179
            ->setNullable(true)
180
            ->setValue(null);
181
        $response->addImplement($namespace . '\\TypeInterface');
182
        return [
183
            'Response' => $responseFile,
184
            'TypeInterface' => $interfaceFile
185
        ];
186
    }
187
188
    /**
189
     * @return PhpFile[]
190
     */
191
    private function generateTypes(): array
192
    {
193
        $namespace = $this->namespace . '\\Types';
194
        $types = $this->generateDefaultTypes($namespace);
195
        foreach ($this->schema['types'] as $type) {
196
            $file = new PhpFile;
197
            $phpNamespace = $file->addNamespace($namespace);
198
            $typeClass = $phpNamespace->addClass($type['name'])
199
                ->setType('class');
200
            if (in_array($type['name'], $this->abstractClasses)) {
201
                $typeClass->setAbstract();
202
            }
203
            if (array_key_exists($type['name'], $this->extendedClasses)) {
204
                $typeClass->addExtend($namespace . '\\' . $this->extendedClasses[$type['name']]);
205
            } else {
206
                $typeClass->addImplement($namespace . '\\TypeInterface');
207
            }
208
            foreach ($type['fields'] as $field) {
209
                ['types' => $fieldTypes, 'comments' => $fieldComments] = $this->parseFieldTypes(
210
                    $field['types'],
211
                    $phpNamespace
212
                );
213
                $fieldName = self::toCamelCase($field['name']);
214
                $typeProperty = $typeClass->addProperty($fieldName)
215
                    ->setPublic()
216
                    ->setType($fieldTypes);
217
                if ($field['optional']) {
218
                    $typeProperty->setNullable(true)
219
                        ->setValue(null);
220
                    $fieldComments .= '|null';
221
                }
222
                if (!empty($fieldComments)) {
223
                    $typeProperty->addComment($fieldComments);
224
                }
225
            }
226
            $types[$type['name']] = $file;
227
        }
228
        return $types;
229
    }
230
231
    /**
232
     * @return string
233
     */
234
    private function generateApi(): string
235
    {
236
        $file = new PhpFile;
237
        $file->addComment('@noinspection PhpUnused');
238
        $file->addComment('@noinspection PhpUnusedParameterInspection');
239
        $phpNamespace = $file->addNamespace($this->namespace);
240
        $apiClass = $phpNamespace->addClass('API')
241
            ->setTrait();
242
        $sendRequest = $apiClass->addMethod('sendRequest')
243
            ->setPublic()
244
            ->setAbstract()
245
            ->setReturnType(Type::MIXED);
246
        $sendRequest->addParameter('method')
247
            ->setType(Type::STRING);
248
        $sendRequest->addParameter('args')
249
            ->setType(Type::ARRAY);
250
        foreach ($this->schema['methods'] as $method) {
251
            $function = $apiClass->addMethod($method['name'])
252
                ->setPublic()
253
                ->addBody('$args = get_defined_vars();')
254
                ->addBody('return $this->sendRequest(__FUNCTION__, $args);');
255
            $fields = $method['fields'];
256
            usort(
257
                $fields,
258
                function ($a, $b) {
259
                    return $a['optional'] - $b['optional'];
260
                }
261
            );
262
            foreach ($fields as $field) {
263
                $types = $this->parseApiFieldTypes($field['types'], $phpNamespace)['types'];
264
                $fieldName = self::toCamelCase($field['name']);
265
                $parameter = $function->addParameter($fieldName)
266
                    ->setType($types);
267
                if ($field['optional']) {
268
                    $parameter->setNullable()
269
                        ->setDefaultValue(null);
270
                }
271
            }
272
            $returnTypes = $this->parseApiFieldTypes($method['return_types'], $phpNamespace)['types'];
273
            $function->setReturnType($returnTypes);
274
        }
275
        return $file;
276
    }
277
278
    /**
279
     * @return array
280
     */
281
    #[ArrayShape([
282
        'types' => "\Nette\PhpGenerator\PhpFile[]",
283
        'api' => "string"
284
    ])]
285
    public function generateCode(): array
286
    {
287
        return [
288
            'types' => $this->generateTypes(),
289
            'api' => $this->generateApi()
290
        ];
291
    }
292
293
}