Major refactoring, should be at least stable now

This commit is contained in:
Sys 2021-06-17 17:56:16 +02:00
parent ee8f6009ff
commit 7a92b63cc6
No known key found for this signature in database
GPG Key ID: 3CD2C29F8AB39BFD
9 changed files with 640 additions and 388 deletions

81
.gitignore vendored Normal file
View File

@ -0,0 +1,81 @@
### Composer template
composer.phar
/vendor/
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

View File

@ -1,2 +1,19 @@
# tgbotapi-generator
A PHP framework used to extract bot API schema (and maybe to generate PHP classes from it).
# TGScraper
A PHP library used to extract JSON data (and auto-generate PHP classes) from [Telegram bot API documentation page](https://core.telegram.org/bots/api).
**Note: the scraper is, obviously, based on a hack and you shouldn't rely on automagically generated files from it, since they are prone to errors. I'll try to fix them ASAP, but manual review is always required (at least for now).**
## Installation
Install the library with composer:
```bash
$ composer require sysbot/tgscraper
```
## Using from command line
Once installed, you can use the CLI to interact with the library:
```bash
$ vendor/bin/tgscraper
```

95
bin/tgscraper Normal file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env php8.0
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use TgScraper\Generator;
$args = getopt('o:u::n::h::');
if (array_key_exists('h', $args)) {
echo 'Usage: tgscraper -o <output> [-u <url>] [-n <namespace>] [-s <scheme>]' . PHP_EOL . PHP_EOL;
echo 'Options:' . PHP_EOL;
echo ' -n <namespace> namespace to use (for PHP stubs only)' . PHP_EOL;
echo ' -o <output> output file/directory (the directory must exist) for JSON schema or PHP stubs' . PHP_EOL;
echo ' -s <scheme> path to custom scheme to use (for PHP stubs only)' . PHP_EOL;
echo ' -u <url> URL to use for fetching bot API data' . PHP_EOL . PHP_EOL;
echo 'Note: based on the "-o" value, the script will automatically detect whether to output a JSON schema or the PHP stubs.' . PHP_EOL;
exit;
}
$output = $args['o'] ?? null;
$namespace = $args['n'] ?? 'Generated';
$url = $args['u'] ?? Generator::BOT_API_URL;
$scheme = $args['s'] ?? null;
if (empty($output)) {
fwrite(STDERR, 'ERROR: "-o" option missing!' . PHP_EOL . PHP_EOL);
fwrite(STDERR, 'Use "tgscraper -h" for help.' . PHP_EOL);
exit(1);
}
if (!empty($url) and !filter_var($url, FILTER_VALIDATE_URL)) {
echo '> WARNING: URL not valid, the default one will be used.' . PHP_EOL;
$url = Generator::BOT_API_URL;
}
$generator = new Generator($url);
$data = null;
if (is_dir($output)) {
if (!empty($scheme)) {
$data = file_get_contents($scheme);
if (false === $data) {
echo sprintf('> WARNING: Cannot read data from "%s", URL will be used instead.%s', $scheme, PHP_EOL);
echo sprintf('> Using "%s" as URL.%s', $url, PHP_EOL);
try {
echo '> Extracting JSON scheme.' . PHP_EOL;
$data = $generator->toJson();
} catch (Exception $e) {
echo '> ERROR: Unable to extract scheme:' . $e->getMessage() . PHP_EOL;
exit(1);
}
} else {
echo sprintf('> Loaded data from "%s".%s', $scheme, PHP_EOL);
}
echo sprintf('> Outputting PHP stubs to %s.%s', realpath($output), PHP_EOL);
$result = $generator->toStubs($output, $namespace, $data);
if (!$result) {
echo sprintf('> ERROR: unable to create stubs.%s', PHP_EOL);
}
exit(!$result);
}
echo sprintf('> Using "%s" as URL.%s', $url, PHP_EOL);
try {
echo '> Extracting JSON scheme.' . PHP_EOL;
$data = $generator->toJson();
} catch (Exception $e) {
echo '> ERROR: Unable to extract scheme:' . $e->getMessage() . PHP_EOL;
exit(1);
}
echo sprintf('> Outputting PHP stubs to %s.%s', realpath($output), PHP_EOL);
$result = $generator->toStubs($output, $namespace, $data);
if (!$result) {
echo '> ERROR: unable to create stubs.%s' . PHP_EOL;
}
exit(!$result);
}
echo sprintf('> Using "%s" as URL.%s', $url, PHP_EOL);
touch($output);
echo sprintf('> Outputting JSON schema to %s.%s', realpath($output), PHP_EOL);
try {
$data = $generator->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo '> ERROR: Unable to generate scheme:' . $e->getMessage() . PHP_EOL;
exit(1);
}
$error = file_put_contents($output, $data) === false;
if ($error) {
echo '> ERROR: unable to write to file.' . PHP_EOL;
}
exit($error);

View File

@ -1,22 +1,29 @@
{
"name": "sys-001/tgbotapi-gen",
"name": "sysbot/tgscraper",
"description": "Utility to extract scheme from Telegram Bot API webpage.",
"type": "project",
"require": {
"php": ">=8.0",
"ext-json": "*",
"nette/php-generator": "^3.5",
"paquettg/php-html-parser": "^2.2"
},
"autoload": {
"psr-4": {
"TGBotApi\\": "src/"
"TgScraper\\": "src/"
}
},
"require": {
"php": "^7.2",
"ext-json": "*",
"nette/php-generator": "^3.3",
"paquettg/php-html-parser": "^2.0"
},
"bin": [
"bin/tgscraper"
],
"authors": [
{
"name": "sys-001",
"email": "honfu7891@etlgr.com"
"email": "sys@sys001.ml",
"homepage": "https://sys001.ml",
"role": "Developer"
}
]
],
"support": {
"issues": "https://github.com/Sysbot-org/tgscraper/issues"
}
}

66
composer.lock generated
View File

@ -4,35 +4,39 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e0c67734f3ab978c6aebf8a35453ba15",
"content-hash": "bb4b87080b33e528f8e53f4c0a4568cd",
"packages": [
{
"name": "nette/php-generator",
"version": "v3.3.3",
"version": "v3.5.3",
"source": {
"type": "git",
"url": "https://github.com/nette/php-generator.git",
"reference": "a4ff22c91681fefaa774cf952a2b69c2ec9477c1"
"reference": "119f01a7bd590469cb01b538f20a125a28853626"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/php-generator/zipball/a4ff22c91681fefaa774cf952a2b69c2ec9477c1",
"reference": "a4ff22c91681fefaa774cf952a2b69c2ec9477c1",
"url": "https://api.github.com/repos/nette/php-generator/zipball/119f01a7bd590469cb01b538f20a125a28853626",
"reference": "119f01a7bd590469cb01b538f20a125a28853626",
"shasum": ""
},
"require": {
"nette/utils": "^2.4.2 || ^3.0",
"nette/utils": "^3.1.2",
"php": ">=7.1"
},
"require-dev": {
"nette/tester": "^2.0",
"nikic/php-parser": "^4.4",
"phpstan/phpstan": "^0.12",
"tracy/tracy": "^2.3"
},
"suggest": {
"nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
"dev-master": "3.5-dev"
}
},
"autoload": {
@ -64,24 +68,31 @@
"php",
"scaffolding"
],
"time": "2020-01-20T11:40:42+00:00"
"support": {
"issues": "https://github.com/nette/php-generator/issues",
"source": "https://github.com/nette/php-generator/tree/v3.5.3"
},
"time": "2021-02-24T18:40:21+00:00"
},
{
"name": "nette/utils",
"version": "v3.1.0",
"version": "v3.2.2",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "d6cd63d77dd9a85c3a2fae707e1255e44c2bc182"
"reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/d6cd63d77dd9a85c3a2fae707e1255e44c2bc182",
"reference": "d6cd63d77dd9a85c3a2fae707e1255e44c2bc182",
"url": "https://api.github.com/repos/nette/utils/zipball/967cfc4f9a1acd5f1058d76715a424c53343c20c",
"reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2 <8.1"
},
"conflict": {
"nette/di": "<3.0.6"
},
"require-dev": {
"nette/tester": "~2.0",
@ -90,7 +101,7 @@
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize() and toAscii()",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
@ -100,7 +111,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
"dev-master": "3.2-dev"
}
},
"autoload": {
@ -111,8 +122,8 @@
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0",
"GPL-3.0"
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
@ -124,7 +135,7 @@
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
@ -142,7 +153,11 @@
"utility",
"validation"
],
"time": "2020-01-03T18:13:31+00:00"
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v3.2.2"
},
"time": "2021-03-03T22:53:25+00:00"
},
{
"name": "paquettg/php-html-parser",
@ -196,6 +211,10 @@
"html",
"parser"
],
"support": {
"issues": "https://github.com/paquettg/php-html-parser/issues",
"source": "https://github.com/paquettg/php-html-parser/tree/2.2.1"
},
"time": "2020-01-20T12:59:15+00:00"
},
{
@ -242,6 +261,10 @@
"encoding",
"string"
],
"support": {
"issues": "https://github.com/paquettg/string-encoder/issues",
"source": "https://github.com/paquettg/string-encoder/tree/1.0.1"
},
"time": "2018-12-21T02:25:09+00:00"
}
],
@ -252,8 +275,9 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.2",
"php": ">=8.0",
"ext-json": "*"
},
"platform-dev": []
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View File

@ -1,7 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$json_scheme = TGBotApi\Generator::toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
echo $json_scheme;

View File

@ -1,101 +1,140 @@
<?php
namespace TGBotApi;
namespace TgScraper;
use Exception;
use JsonException;
use PHPHtmlParser\Dom;
use PHPHtmlParser\Exceptions\{ChildNotFoundException,
CircularException,
CurlException,
NotLoadedException,
ParentNotFoundException,
StrictException
};
class Generator
{
private const EMPTY_FIELDS = [
'getWebhookInfo',
'getMe',
'InputFile',
'InputMedia',
'InlineQueryResult',
'InputMessageContent',
'PassportElementError',
'CallbackGame',
'getMyCommands',
'logOut',
'close'
];
private const BOOL_RETURNS = [
'answerShippingQuery',
'answerPreCheckoutQuery'
];
/**
* @param string $target_directory
* @param string $namespace_prefix
* @return bool
* @throws \Exception
*/
public static function toClasses(string $target_directory = '', string $namespace_prefix = ''): bool
public const BOT_API_URL = 'https://core.telegram.org/bots/api';
public function __construct(private string $url = self::BOT_API_URL)
{
}
/**
* @param string $directory
* @param string $namespace
* @param string|null $scheme
* @return bool
*/
public function toStubs(string $directory = '', string $namespace = '', string $scheme = null): bool
{
$target_directory = self::getTargetDirectory($target_directory);
mkdir($target_directory . '/Methods', 0755);
mkdir($target_directory . '/Types', 0755);
try {
$stub_provider = new StubProvider($namespace_prefix);
$code = $stub_provider->generateCode(self::extractScheme());
foreach ($code['methods'] as $class_name => $method) {
file_put_contents($target_directory . '/Methods/' . $class_name . '.php', $method);
$directory = self::getTargetDirectory($directory);
} catch (Exception $e) {
echo 'Unable to use target directory:' . $e->getMessage();
return false;
}
mkdir($directory . '/Types', 0755);
try {
if (!empty($scheme)) {
try {
$data = json_decode($scheme, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
$data = null;
}
}
$data = $data ?? self::extractScheme();
$creator = new StubCreator($data, $namespace);
$code = $creator->generateCode();
foreach ($code['types'] as $class_name => $type) {
file_put_contents($target_directory . '/Types/' . $class_name . '.php', $type);
$filename = sprintf('%s/Types/%s.php', $directory, $class_name);
file_put_contents($filename, $type);
}
} catch (\Exception $e) {
file_put_contents($directory . '/API.php', $code['api']);
} catch (Exception $e) {
echo $e->getMessage();
return false;
}
return true;
}
/**
* @param string $target_directory
* @return string
* @throws Exception
*/
private static function getTargetDirectory(string $target_directory): string
{
mkdir($target_directory, 0755);
$target_directory = realpath($target_directory);
if (false == $target_directory) {
$target_directory = __DIR__ . '/generated';
if (!file_exists($target_directory)) {
mkdir($target_directory, 0755);
$target_path = realpath($target_directory);
if (false == $target_path) {
if (!mkdir($target_directory)) {
$target_path = __DIR__ . '/../generated';
if (!file_exists($target_path)) {
mkdir($target_path, 0755);
}
}
}
if (realpath($target_path) == false) {
throw new Exception('Could not create target directory');
}
return $target_directory;
}
/**
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\ParentNotFoundException
* @throws \PHPHtmlParser\Exceptions\StrictException
* @throws ChildNotFoundException
* @throws CircularException
* @throws CurlException
* @throws NotLoadedException
* @throws ParentNotFoundException
* @throws StrictException
*/
private static function extractScheme(): array
private function extractScheme(): array
{
$dom = new Dom;
$dom->loadFromURL('https://core.telegram.org/bots/api');
$dom->loadFromURL($this->url);
$elements = $dom->find('h4');
$i = 0;
$data = [];
/* @var Dom\AbstractNode $element */
foreach ($elements as $element) {
if (false === strpos($name = $element->text, ' ')) {
if (!str_contains($name = $element->text, ' ')) {
$is_method = self::isMethod($name);
$path = $is_method ? 'methods' : 'types';
$empty = in_array($name, self::EMPTY_FIELDS);
/* @var Dom $fields_table */
$fields_table = $dom->find('table')[$i];
$unparsed_fields = $fields_table->find('tbody')->find('tr');
/* @var Dom\AbstractNode $element */
/** @noinspection PhpUndefinedFieldInspection */
$data[$path][] = self::generateElement($name, $element->nextSibling()->nextSibling()->innerHtml,
($empty ? null : $unparsed_fields), $is_method);
if (!$empty) {
$i++;
$temp = $element;
$description = '';
$table = null;
while (true) {
try {
$element = $element->nextSibling();
} catch (ChildNotFoundException $e) {
break;
}
$tag = $element->tag->name() ?? null;
if (empty($temp->text()) or empty($tag) or $tag == 'text') {
continue;
} elseif (str_starts_with($tag, 'h')) {
break;
} elseif ($tag == 'p') {
$description .= PHP_EOL . $element->innerHtml();
} elseif ($tag == 'table') {
$table = $element->find('tbody')->find('tr');
break;
}
}
/* @var Dom\AbstractNode $element */
$data[$path][] = self::generateElement(
$name,
trim($description),
$table,
$is_method
);
}
}
return $data;
@ -103,8 +142,7 @@ class Generator
private static function isMethod(string $name): bool
{
$first_letter = substr($name, 0, 1);
return (strtolower($first_letter) == $first_letter);
return lcfirst($name) == $name;
}
/**
@ -113,11 +151,11 @@ class Generator
* @param Dom\Collection|null $unparsed_fields
* @param bool $is_method
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\StrictException
* @throws ChildNotFoundException
* @throws CircularException
* @throws CurlException
* @throws NotLoadedException
* @throws StrictException
*/
private static function generateElement(
string $name,
@ -149,8 +187,8 @@ class Generator
* @param Dom\Collection|null $fields
* @param bool $is_method
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws ChildNotFoundException
* @throws NotLoadedException
*/
private static function parseFields(?Dom\Collection $fields, bool $is_method): array
{
@ -159,20 +197,27 @@ class Generator
foreach ($fields as $field) {
/* @var Dom $field */
$field_data = $field->find('td');
$name = $field_data[0]->text;
if (empty($name)) {
continue;
}
$parsed_data = [
'name' => $field_data[0]->text,
'name' => $name,
'type' => strip_tags($field_data[1]->innerHtml)
];
$parsed_data['types'] = self::parseFieldTypes($parsed_data['type']);
unset($parsed_data['type']);
if ($is_method) {
$parsed_data['required'] = ($field_data[2]->text == 'Yes');
$parsed_data['types'] = self::parseMethodFieldTypes($parsed_data['type']);
unset($parsed_data['type']);
$parsed_data['description'] = htmlspecialchars_decode(strip_tags($field_data[3]->innerHtml ?? $field_data[3]->text ?? ''),
ENT_QUOTES);
$parsed_data['required'] = $field_data[2]->text == 'Yes';
$parsed_data['description'] = htmlspecialchars_decode(
strip_tags($field_data[3]->innerHtml ?? $field_data[3]->text ?? ''),
ENT_QUOTES
);
} else {
$parsed_data['type'] = self::parseObjectFieldType($parsed_data['type']);
$parsed_data['description'] = htmlspecialchars_decode(strip_tags($field_data[2]->innerHtml),
ENT_QUOTES);
$parsed_data['description'] = htmlspecialchars_decode(
strip_tags($field_data[2]->innerHtml),
ENT_QUOTES
);
}
$parsed_fields[] = $parsed_data;
}
@ -183,47 +228,57 @@ class Generator
* @param string $raw_type
* @return array
*/
private static function parseMethodFieldTypes(string $raw_type): array
private static function parseFieldTypes(string $raw_type): array
{
$types = explode(' or ', $raw_type);
$types = [];
foreach (explode(' or ', $raw_type) as $raw_or_type) {
if (stripos($raw_or_type, 'array') === 0) {
$types[] = str_replace(' and', ',', $raw_or_type);
continue;
}
foreach (explode(' and ', $raw_or_type) as $unparsed_type) {
$types[] = $unparsed_type;
}
}
$parsed_types = [];
foreach ($types as $type) {
$type = trim(str_replace(['number', 'of'], '', $type));
$multiples_count = substr_count(strtolower($type), 'array');
$parsed_types[] = trim(str_replace(['Array', 'Integer', 'String', 'Boolean', 'Float'],
['', 'int', 'string', 'bool', 'float'], $type)) . str_repeat('[]', $multiples_count);
$parsed_type = trim(
str_replace(
['Array', 'Integer', 'String', 'Boolean', 'Float', 'True'],
['', 'int', 'string', 'bool', 'float', 'bool'],
$type
)
);
for ($i = 0; $i < $multiples_count; $i++) {
$parsed_type = sprintf('Array<%s>', $parsed_type);
}
$parsed_types[] = $parsed_type;
}
return $parsed_types;
}
/**
* @param string $raw_type
* @return string
*/
private static function parseObjectFieldType(string $raw_type): string
{
$type = trim(str_replace(['number', 'of'], '', $raw_type));
$multiples_count = substr_count(strtolower($type), 'array');
return trim(str_replace(['Array', 'Integer', 'String', 'Boolean', 'Float'],
['', 'int', 'string', 'bool', 'float'], $type)) . str_repeat('[]', $multiples_count);
}
/**
* @param string $description
* @return array
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\StrictException
* @throws ChildNotFoundException
* @throws CircularException
* @throws CurlException
* @throws NotLoadedException
* @throws StrictException
* @noinspection PhpUndefinedFieldInspection
*/
private static function parseReturnTypes(string $description): array
{
$return_types = [];
$phrases = explode('.', $description);
$phrases = array_filter($phrases, function ($phrase) {
return (false !== stripos($phrase, 'returns') or false !== stripos($phrase, 'is returned'));
});
$phrases = array_filter(
$phrases,
function ($phrase) {
return (false !== stripos($phrase, 'returns') or false !== stripos($phrase, 'is returned'));
}
);
foreach ($phrases as $phrase) {
$dom = new Dom;
$dom->load($phrase);
@ -231,11 +286,16 @@ class Generator
$em = $dom->find('em');
foreach ($a as $element) {
if ($element->text == 'Messages') {
$return_types[] = 'Message[]';
$return_types[] = 'Array<Message>';
continue;
}
$multiples_count = substr_count(strtolower($phrase), 'array');
$return_types[] = $element->text . str_repeat('[]', $multiples_count);
$return_type = $element->text;
for ($i = 0; $i < $multiples_count; $i++) {
$return_type = sprintf('Array<%s>', $return_type);
}
$return_types[] = $return_type;
}
foreach ($em as $element) {
if (in_array($element->text, ['False', 'force', 'Array'])) {
@ -251,16 +311,16 @@ class Generator
/**
* @param int $options
* @return string
* @throws \PHPHtmlParser\Exceptions\ChildNotFoundException
* @throws \PHPHtmlParser\Exceptions\CircularException
* @throws \PHPHtmlParser\Exceptions\CurlException
* @throws \PHPHtmlParser\Exceptions\NotLoadedException
* @throws \PHPHtmlParser\Exceptions\ParentNotFoundException
* @throws \PHPHtmlParser\Exceptions\StrictException
* @throws ChildNotFoundException
* @throws CircularException
* @throws CurlException
* @throws NotLoadedException
* @throws ParentNotFoundException
* @throws StrictException
*/
public static function toJson(int $options = 0): string
public function toJson(int $options = 0): string
{
$scheme = self::extractScheme();
$scheme = $this->extractScheme();
return json_encode($scheme, $options);
}

213
src/StubCreator.php Normal file
View File

@ -0,0 +1,213 @@
<?php
/** @noinspection PhpInternalEntityUsedInspection */
namespace TgScraper;
use InvalidArgumentException;
use Nette\PhpGenerator\Helpers;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PhpNamespace;
use Nette\PhpGenerator\Type;
/**
* Class StubCreator
* @package TgScraper
*/
class StubCreator
{
/**
* @var string
*/
private string $namespace;
/**
* StubCreator constructor.
* @param array $scheme
* @param string $namespace
*/
public function __construct(private array $scheme, string $namespace = '')
{
if (str_ends_with($namespace, '\\')) {
$namespace = substr($namespace, 0, -1);
}
if (!empty($namespace)) {
if (!Helpers::isNamespaceIdentifier($namespace)) {
throw new InvalidArgumentException('Namespace invalid');
}
}
$this->namespace = $namespace;
}
/**
* @param array $fieldTypes
* @return array
*/
private function parseFieldTypes(array $fieldTypes, PhpNamespace $phpNamespace): array
{
$types = [];
$comments = [];
foreach ($fieldTypes as $fieldType) {
$comments[] = $fieldType;
if (str_starts_with($fieldType, 'Array')) {
$types[] = 'array';
continue;
}
if (ucfirst($fieldType) == $fieldType) {
$fieldType = $phpNamespace->getName() . '\\' . $fieldType;
}
$types[] = $fieldType;
}
$comments = empty($comments) ? '' : sprintf('@var %s', implode('|', $comments));
return [
'types' => implode('|', $types),
'comments' => $comments
];
}
/**
* @param array $apiTypes
* @param PhpNamespace $phpNamespace
* @return array
*/
private function parseApiFieldTypes(array $apiTypes, PhpNamespace $phpNamespace): array
{
$types = [];
$comments = [];
foreach ($apiTypes as $apiType) {
$comments[] = $apiType;
if (str_starts_with($apiType, 'Array')) {
$types[] = 'array';
continue;
}
if (ucfirst($apiType) == $apiType) {
$apiType = $this->namespace . '\\Types\\' . $apiType;
$phpNamespace->addUse($apiType);
}
$types[] = $apiType;
}
$comments = empty($comments) ? '' : sprintf('@var %s', implode('|', $comments));
return [
'types' => implode('|', $types),
'comments' => $comments
];
}
/**
* @param string $namespace
* @return PhpFile[]
*/
private function generateDefaultTypes(string $namespace): array
{
$file = new PhpFile;
$phpNamespace = $file->addNamespace($namespace);
$response = $phpNamespace->addClass('Response')
->setType('class');
$response->addProperty('ok')
->setPublic();
$response->addProperty('result')
->setPublic();
$response->addProperty('error_code')
->setPublic();
$response->addProperty('description')
->setPublic();
return [
'Response' => $file
];
}
/**
* @return PhpFile[]
*/
private function generateTypes(): array
{
$namespace = $this->namespace . '\\Types';
$types = $this->generateDefaultTypes($namespace);
foreach ($this->scheme['types'] as $type) {
$file = new PhpFile;
$phpNamespace = $file->addNamespace($namespace);
$typeClass = $phpNamespace->addClass($type['name'])
->setType('class');
foreach ($type['fields'] as $field) {
['types' => $fieldTypes, 'comments' => $fieldComments] = $this->parseFieldTypes(
$field['types'],
$phpNamespace
);
$type_property = $typeClass->addProperty($field['name'])
->setPublic()
->setType($fieldTypes);
if (!empty($fieldComments)) {
$type_property->addComment($fieldComments);
}
}
$types[$type['name']] = $file;
}
return $types;
}
/**
* @return string
*/
private function generateApi(): string
{
$file = new PhpFile;
$file->addComment('@noinspection PhpUnused');
$file->addComment('@noinspection PhpUnusedParameterInspection');
$file->addComment('@noinspection PhpIncompatibleReturnTypeInspection');
$file->addComment('@noinspection PhpVoidFunctionResultUsedInspection');
$phpNamespace = $file->addNamespace($this->namespace);
$apiClass = $phpNamespace->addClass('API')
->setType('class');
$apiClass->addMethod('__construct')
->setPublic()
->addPromotedParameter('client')
->setType('\GuzzleHttp\Client')
->setPrivate();
$sendRequest = $apiClass->addMethod('sendRequest')
->setPublic();
$sendRequest->addParameter('method')
->setType(Type::STRING);
$sendRequest->addParameter('args')
->setType(Type::ARRAY);
$sendRequest->addBody('//TODO: add your logic here');
foreach ($this->scheme['methods'] as $method) {
$function = $apiClass->addMethod($method['name'])
->setPublic()
->addBody('$args = get_defined_vars();')
->addBody('return $this->sendRequest(__FUNCTION__, $args);');
$fields = $method['fields'];
usort(
$fields,
function ($a, $b) {
return $b['required'] - $a['required'];
}
);
foreach ($fields as $field) {
$types = $this->parseApiFieldTypes($field['types'], $phpNamespace)['types'];
$parameter = $function->addParameter($field['name'])
->setType($types);
if (!$field['required']) {
$parameter->setNullable()
->setDefaultValue(null);
}
}
$returnTypes = $this->parseApiFieldTypes($method['return_types'], $phpNamespace)['types'];
$function->setReturnType($returnTypes);
}
return $file;
}
/**
* @return array
*/
public function generateCode(): array
{
return [
'types' => $this->generateTypes(),
'api' => $this->generateApi()
];
}
}

View File

@ -1,238 +0,0 @@
<?php
namespace TGBotApi;
use Nette\{PhpGenerator, PhpGenerator\Type};
class StubProvider
{
private $namespace_prefix = '';
private $methods = [];
private $types = [];
/**
* StubProvider constructor.
* @param string $namespace_prefix
* @throws \Exception
*/
public function __construct(string $namespace_prefix = '')
{
if (substr($namespace_prefix, -1) == '\\') {
$namespace_prefix = substr($namespace_prefix, 0, -1);
}
if (!empty($namespace_prefix)) {
if (!PhpGenerator\Helpers::isNamespaceIdentifier($namespace_prefix)) {
throw new \Exception('Invalid namespace prefix provided');
}
$namespace_prefix .= '\\';
}
$types_namespace = $namespace_prefix . 'Types';
$this->namespace_prefix = $namespace_prefix;
$this->methods = $this->createDefaultMethods();
$this->types = $this->createDefaultTypes($types_namespace);
}
private function createDefaultMethods(): array
{
$method_inteface = (new PhpGenerator\ClassType('MethodInterface'))
->setInterface();
$method_inteface->addMethod('getParams')
->setPublic()
->setReturnType(Type::ARRAY);
$method_inteface->addMethod('getMethodName')
->setPublic()
->setStatic()
->setReturnType(Type::STRING);
$method_inteface->addMethod('isMultipart')
->setPublic()
->setReturnType(Type::BOOL);
$method_inteface->addMethod('getResultParams')
->setPublic()
->setStatic()
->setReturnType(Type::ARRAY);
$method_abstract = (new PhpGenerator\ClassType('DefaultMethod'))
->setClass()
->setAbstract()
->addImplement('MethodInterface');
$method_abstract->addConstant('METHOD_NAME', '')
->setPrivate();
$method_abstract->addConstant('RESULT_TYPE', '')
->setPrivate();
$method_abstract->addConstant('MULTIPLE_RESULTS', false)
->setPrivate();
$method_abstract->addProperty('multipart', false)
->setPrivate();
$method_abstract->addMethod('getMethodName')
->setPublic()
->setStatic()
->setReturnType(Type::STRING)
->setBody('return static::METHOD_NAME;');
$method_abstract->addMethod('isMultipart')
->setPublic()
->setReturnType(Type::BOOL)
->setBody('return $this->multipart;');
$method_abstract->addMethod('getResultParams')
->setPublic()
->setStatic()
->setReturnType(Type::ARRAY)
->addBody('return [')
->addBody(' \'type\' => static::RESULT_TYPE,')
->addBody(' \'multiple\' => static::MULTIPLE_RESULTS')
->addBody('];');
return [
'MethodInterface' => $this->addNamespace($method_inteface, 'Methods'),
'DefaultMethod' => $this->addNamespace($method_abstract, 'Methods')
];
}
private function addNamespace(string $code, string $sub_namespace): string
{
return '<?php' . str_repeat(PHP_EOL,
2) . 'namespace ' . $this->namespace_prefix . $sub_namespace . ';' . str_repeat(PHP_EOL, 2) . $code;
}
private function createDefaultTypes(string $namespace): array
{
$response = (new PhpGenerator\ClassType('Response'))
->setType('class');
$response->addProperty('ok')
->setPublic();
$response->addProperty('result')
->setPublic();
$response->addProperty('error_code')
->setPublic();
$response->addProperty('description')
->setPublic();
$response->addMethod('parseResponse')
->setReturnType(Type::SELF)
->setReturnNullable(true)
->setPublic()
->setStatic()
->setBody('if (null == $response) {
return null;
}
$parsed_response = (new self())
->setOk($response->ok ?? null)
->setErrorCode($response->error_code ?? null)
->setDescription($response->description ?? null);
if (empty($response->result)) {
$parsed_response->setResult(null);
} elseif (!empty($response->result->migrate_to_chat_id) or !empty($response->result->retry_after)) {
$parsed_response->setResult(ResponseParameters::parseResponseParameters($response->result ?? null));
} elseif (!empty($response->result_type)) {
$result_class = sprintf(\'' . $namespace . '\\%s\', $response->result_type->class);
$parsed_response->setResult(call_user_func([$result_class, $response->result_type->method],
$response->result ?? null));
} else {
$parsed_response->setResult($response->result ?? null);
}
return $parsed_response;')
->addParameter('response')
->setType('\stdClass')
->setNullable(true);
$this->createSetters($response, [
['name' => 'ok', 'type' => 'bool', 'nullable' => true],
['name' => 'result', 'type' => null, 'nullable' => false],
['name' => 'error_code', 'type' => 'int', 'nullable' => true],
['name' => 'description', 'type' => 'string', 'nullable' => true]
]);
return ['Response' => $this->addNamespace($response, 'Types')];
}
private function createSetters(PhpGenerator\ClassType $class, array $properties): void
{
foreach ($properties as $property) {
$class->addMethod('set' . $this->getCamelCaseName($property['name']))
->setPublic()
->setReturnType(Type::SELF)
->setBody('$this->' . $property['name'] . ' = $' . $property['name'] . ';' . PHP_EOL . 'return $this;')
->addParameter($property['name'])
->setType($property['type'])
->setNullable($property['nullable']);
}
return;
}
private function getCamelCaseName(string $snake_case): string
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $snake_case)));
}
public function generateCode(array $scheme): array
{
foreach ($scheme['types'] as $type) {
$type_class = (new PhpGenerator\ClassType($type['name']))
->setType('class');
$fields = [];
foreach ($type['fields'] as $field) {
$type_class->addProperty($field['name'])
->setPublic()
->setType($field['type']);
$fields[] = [
'name' => $field['name'],
'type' => $field['type'],
'nullable' => true
];
}
$this->createSetters($type_class, $fields);
$this->types[$type['name']] = $this->addNamespace($type_class, 'Types');
}
foreach ($scheme['methods'] as $method) {
$method_class = (new PhpGenerator\ClassType(ucfirst($method['name'])))
->setType('class')
->addExtend('DefaultMethod');
$method_class->addConstant('METHOD_NAME', $method['name'])
->setPrivate();
$return_type = $method['return_types'][0];
$multiple_results = false;
if (substr($return_type, -2) == '[]') {
$return_type = substr($return_type, 0, -2);
$multiple_results = true;
}
$method_class->addConstant('RESULT_TYPE', $return_type)
->setPrivate();
$method_class->addConstant('MULTIPLE_RESULTS', $multiple_results)
->setPrivate();
$fields = [];
$get_params = $method_class->addMethod('getParams')
->setPublic()
->setReturnType(Type::ARRAY)
->addBody('return [');
$last_index = array_key_last($method['fields']);
foreach ($method['fields'] as $index => $field) {
$field_type = count($field['types']) == 0 ? $field['types'][0] : null; //maybe there will be a better implementation
$method_class->addProperty($field['name'])
->setPublic()
->setType($field_type);
$comma = $index != $last_index ? ',' : '';
$get_params->addBody(' \'' . $field['name'] . '\' => $this->' . $field['name'] . $comma);
$fields[] = [
'name' => $field['name'],
'type' => $field_type,
'nullable' => true
];
}
$get_params->addBody('];');
$this->createConstructor($method_class, $fields);
$this->methods[ucfirst($method['name'])] = $this->addNamespace($method_class, 'Methods');
}
return [
'methods' => $this->methods,
'types' => $this->types
];
}
private function createConstructor(PhpGenerator\ClassType $class, array $properties): void
{
$method = $class->addMethod('__construct');
foreach ($properties as $property) {
$method->addBody('$this->' . $property['name'] . ' = $' . $property['name'] . ';')
->addParameter($property['name'])
->setType($property['type'])
->setNullable($property['nullable']);
}
return;
}
}