Passed
Push — master ( 1905ce...c7e97e )
by Sys
09:49 queued 08:07
created

StubCreator::__construct()   B

Complexity

Conditions 9
Paths 22

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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