Passed
Push — master ( 4bf53d...ce4836 )
by Sys
09:47
created

StubCreator::generateCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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