Skip to content

Commit ca24152

Browse files
committed
Add API metadata attributes and command for coverage docs
Implemented API metadata attributes for service and method documentation. Added GenerateCoverageDocumentationCommand to generate API coverage documentation in Markdown format. This enhances maintainability and provides comprehensive API documentation automatically. Signed-off-by: mesilov <[email protected]>
1 parent 9adf3a7 commit ca24152

File tree

14 files changed

+647
-54
lines changed

14 files changed

+647
-54
lines changed

bin/console

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env php
22
<?php
33

4+
use Bitrix24\SDK\Attributes\Services\AttributesParser;
5+
use Bitrix24\SDK\Services\ServiceBuilderFactory;
46
use Bitrix24\SDK\Tools\Commands\CopyPropertyValues;
57
use Bitrix24\SDK\Tools\Commands\GenerateContactsCommand;
8+
use Bitrix24\SDK\Infrastructure\Console\Commands;
69
use Bitrix24\SDK\Tools\Commands\PerformanceBenchmarks\ListCommand;
710
use Bitrix24\SDK\Tools\Commands\ShowFieldsDescriptionCommand;
811
use Monolog\Handler\StreamHandler;
@@ -12,6 +15,8 @@ use Symfony\Component\Console\Application;
1215
use Symfony\Component\Console\Input\ArgvInput;
1316
use Symfony\Component\Dotenv\Dotenv;
1417
use Symfony\Component\ErrorHandler\Debug;
18+
use Symfony\Component\EventDispatcher\EventDispatcher;
19+
use Typhoon\Reflection\TyphoonReflector;
1520

1621
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
1722
echo 'Warning: The console should be invoked via the CLI version of PHP, not the ' . PHP_SAPI . ' SAPI' . PHP_EOL;
@@ -55,4 +60,10 @@ $application->add(new GenerateContactsCommand($log));
5560
$application->add(new ListCommand($log));
5661
$application->add(new ShowFieldsDescriptionCommand($log));
5762
$application->add(new CopyPropertyValues($log));
63+
$application->add(new Commands\GenerateCoverageDocumentationCommand(
64+
new AttributesParser(TyphoonReflector::build(), new Symfony\Component\Filesystem\Filesystem()),
65+
new ServiceBuilderFactory(new EventDispatcher(), $log),
66+
new Symfony\Component\Finder\Finder(),
67+
new Symfony\Component\Filesystem\Filesystem(),
68+
$log));
5869
$application->run($input);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## All bitrix24-php-sdk methods
2+
3+
| **Scope** | **API method with documentation** | **Description** | Method in SDK |
4+
|-----------|----------------------------------------|------------------|----------------|
5+
|`catalog`|[catalog.catalog.get](https://training.bitrix24.com/rest_help/catalog/catalog/catalog_catalog_get.php)|The method gets field values of commercial catalog by ID.|[`Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog::get`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Catalog/Service/Catalog.php#L34-L37)<br/>Return type<br/>[`Bitrix24\SDK\Services\Catalog\Catalog\Result\CatalogResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Catalog/Result/CatalogResult.php)|
6+
|`catalog`|[catalog.catalog.list](https://training.bitrix24.com/rest_help/catalog/catalog/catalog_catalog_list.php)|The method gets field value of commercial catalog product list|[`Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog::list`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Catalog/Service/Catalog.php#L56-L64)<br/>Return type<br/>[`Bitrix24\SDK\Services\Catalog\Catalog\Result\CatalogsResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Catalog/Result/CatalogsResult.php)|
7+
|`catalog`|[catalog.catalog.getFields](https://training.bitrix24.com/rest_help/catalog/catalog/catalog_catalog_getfields.php)|Retrieves the fields for the catalog.|[`Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog::fields`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Catalog/Service/Catalog.php#L79-L82)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\FieldsResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/FieldsResult.php)|
8+
|`catalog`|[catalog.product.get](https://training.bitrix24.com/rest_help/catalog/product/catalog_product_get.php)|The method gets field value of commercial catalog product by ID.|[`Bitrix24\SDK\Services\Catalog\Product\Service\Product::get`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Service/Product.php#L49-L52)<br/>Return type<br/>[`Bitrix24\SDK\Services\Catalog\Product\Result\ProductResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Result/ProductResult.php)|
9+
|`catalog`|[catalog.product.add](https://training.bitrix24.com/rest_help/catalog/product/catalog_product_add.php)|The method adds a commercial catalog product.|[`Bitrix24\SDK\Services\Catalog\Product\Service\Product::add`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Service/Product.php#L68-L74)<br/>Return type<br/>[`Bitrix24\SDK\Services\Catalog\Product\Result\ProductResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Result/ProductResult.php)|
10+
|`catalog`|[catalog.product.delete](https://training.bitrix24.com/rest_help/catalog/product/catalog_product_delete.php)|The method deletes commercial catalog product by ID|[`Bitrix24\SDK\Services\Catalog\Product\Service\Product::delete`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Service/Product.php#L90-L93)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\DeletedItemResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/DeletedItemResult.php)|
11+
|`catalog`|[catalog.product.list](https://training.bitrix24.com/rest_help/catalog/product/catalog_product_list.php)|The method gets list of commercial catalog products by filter.|[`Bitrix24\SDK\Services\Catalog\Product\Service\Product::list`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Service/Product.php#L107-L115)<br/>Return type<br/>[`Bitrix24\SDK\Services\Catalog\Product\Result\ProductsResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Result/ProductsResult.php)|
12+
|`catalog`|[catalog.product.getFieldsByFilter](https://training.bitrix24.com/rest_help/catalog/product/catalog_product_getfieldsbyfilter.php)|The method returns commercial catalog product fields by filter.|[`Bitrix24\SDK\Services\Catalog\Product\Service\Product::fieldsByFilter`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/Catalog/Product/Service/Product.php#L133-L144)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\FieldsResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/FieldsResult.php)|
13+
|`crm`|[crm.deal.add](https://training.bitrix24.com/rest_help/crm/deals/crm_deal_add.php)|Add new deal|[`Bitrix24\SDK\Services\CRM\Deal\Service\Deal::add`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Deal/Service/Deal.php#L96-L107)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\AddedItemResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/AddedItemResult.php)|
14+
|`crm`|[crm.deal.delete](https://training.bitrix24.com/rest_help/crm/deals/crm_deal_delete.php)|Delete deal|[`Bitrix24\SDK\Services\CRM\Deal\Service\Deal::delete`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Deal/Service/Deal.php#L125-L135)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\DeletedItemResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/DeletedItemResult.php)|
15+
|`crm`|[crm.activity.add](https://training.bitrix24.com/rest_help/crm/rest_activity/crm_activity_add.php)|Creates and adds a new activity.|[`Bitrix24\SDK\Services\CRM\Activity\Service\Activity::add`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Service/Activity.php#L101-L111)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\AddedItemResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/AddedItemResult.php)<br/><br/>⚡️Batch methods: <br/><ul><li>`Bitrix24\SDK\Services\CRM\Activity\Service\Batch::add`<br/>Return type: `Generator<int<1, max>, Bitrix24\SDK\Core\Result\AddedItemBatchResult, mixed, mixed>`</li></ul>|
16+
|`crm`|[crm.activity.delete](https://training.bitrix24.com/rest_help/crm/rest_activity/crm_activity_delete.php)|Deletes the specified activity and all the associated objects.|[`Bitrix24\SDK\Services\CRM\Activity\Service\Activity::delete`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Service/Activity.php#L129-L139)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\DeletedItemResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/DeletedItemResult.php)<br/><br/>⚡️Batch methods: <br/><ul><li>`Bitrix24\SDK\Services\CRM\Activity\Service\Batch::delete`<br/>Return type: `Generator<int<1, max>, Bitrix24\SDK\Core\Result\DeletedItemBatchResult, mixed, mixed>`</li></ul>|
17+
|`crm`|[crm.activity.fields](https://training.bitrix24.com/rest_help/crm/rest_activity/crm_activity_fields.php)|Returns the description of activity fields|[`Bitrix24\SDK\Services\CRM\Activity\Service\Activity::fields`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Service/Activity.php#L155-L158)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\FieldsResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/FieldsResult.php)|
18+
|`crm`|[crm.activity.get](https://training.bitrix24.com/rest_help/crm/rest_activity/crm_activity_get.php)|Returns activity by the specified activity ID|[`Bitrix24\SDK\Services\CRM\Activity\Service\Activity::get`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Service/Activity.php#L176-L186)<br/>Return type<br/>[`Bitrix24\SDK\Services\CRM\Activity\Result\ActivityResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Result/ActivityResult.php)|
19+
|`crm`|[crm.activity.list](https://training.bitrix24.com/rest_help/crm/rest_activity/crm_activity_list.php)|Returns a list of activity selected by the filter specified as the parameter. See the example for the filter notation.|[`Bitrix24\SDK\Services\CRM\Activity\Service\Activity::list`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Service/Activity.php#L297-L310)<br/>Return type<br/>[`Bitrix24\SDK\Services\CRM\Activity\Result\ActivitiesResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Result/ActivitiesResult.php)<br/><br/>⚡️Batch methods: <br/><ul><li>`Bitrix24\SDK\Services\CRM\Activity\ReadModel\EmailFetcher::getList`<br/>Return type: `Generator<int<1, max>, Bitrix24\SDK\Services\CRM\Activity\Result\Email\EmailActivityItemResult, mixed, mixed>`</li><li>`Bitrix24\SDK\Services\CRM\Activity\Service\Batch::list`<br/>Return type: `Generator<int<1, max>, Bitrix24\SDK\Services\CRM\Activity\Result\ActivityItemResult, mixed, mixed>`</li></ul>|
20+
|`crm`|[crm.activity.update](https://training.bitrix24.com/rest_help/crm/rest_activity/crm_activity_update.php)|Updates the specified (existing) activity.|[`Bitrix24\SDK\Services\CRM\Activity\Service\Activity::update`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Services/CRM/Activity/Service/Activity.php#L373-L384)<br/>Return type<br/>[`Bitrix24\SDK\Core\Result\UpdatedItemResult`](https://github.com/mesilov/bitrix24-php-sdk/blob/feature/390-prepare-publish-2-0/src/Core/Result/UpdatedItemResult.php)|
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bitrix24\SDK\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_METHOD)]
10+
class ApiBatchMethodMetadata
11+
{
12+
public function __construct(
13+
public string $name,
14+
public string $documentationUrl,
15+
public ?string $description = null,
16+
public bool $isDeprecated = false,
17+
public ?string $deprecationMessage = null
18+
)
19+
{
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bitrix24\SDK\Attributes;
6+
7+
use Attribute;
8+
use Bitrix24\SDK\Core\Credentials\Scope;
9+
10+
#[Attribute(Attribute::TARGET_CLASS)]
11+
class ApiBatchServiceMetadata
12+
{
13+
public function __construct(
14+
public Scope $scope,
15+
public ?string $documentationUrl = null,
16+
public ?string $description = null,
17+
public bool $isDeprecated = false,
18+
public ?string $deprecationMessage = null
19+
)
20+
{
21+
}
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bitrix24\SDK\Attributes;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_METHOD)]
10+
class ApiEndpointMetadata
11+
{
12+
public function __construct(
13+
public string $name,
14+
public string $documentationUrl,
15+
public ?string $description = null,
16+
public bool $isDeprecated = false,
17+
public ?string $deprecationMessage = null
18+
)
19+
{
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bitrix24\SDK\Attributes;
6+
7+
use Attribute;
8+
use Bitrix24\SDK\Core\Credentials\Scope;
9+
10+
#[Attribute(Attribute::TARGET_CLASS)]
11+
class ApiServiceMetadata
12+
{
13+
public function __construct(
14+
public Scope $scope,
15+
public ?string $documentationUrl = null,
16+
public ?string $description = null,
17+
public bool $isDeprecated = false,
18+
public ?string $deprecationMessage = null
19+
)
20+
{
21+
}
22+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bitrix24\SDK\Attributes\Services;
6+
7+
8+
use Bitrix24\SDK\Attributes\ApiBatchMethodMetadata;
9+
use Bitrix24\SDK\Attributes\ApiBatchServiceMetadata;
10+
use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
11+
use Bitrix24\SDK\Attributes\ApiServiceMetadata;
12+
use ReflectionClass;
13+
use Symfony\Component\Filesystem\Filesystem;
14+
use Typhoon\Reflection\TyphoonReflector;
15+
use function Typhoon\Type\stringify;
16+
17+
readonly class AttributesParser
18+
{
19+
public function __construct(
20+
private TyphoonReflector $typhoonReflector,
21+
private Filesystem $filesystem,
22+
)
23+
{
24+
}
25+
26+
/**
27+
* @param class-string[] $sdkClassNames
28+
* @return array<string, mixed>
29+
*/
30+
public function getSupportedInSdkApiMethods(array $sdkClassNames, string $sdkBaseDir): array
31+
{
32+
$supportedInSdkMethods = [];
33+
foreach ($sdkClassNames as $className) {
34+
$reflectionServiceClass = new ReflectionClass($className);
35+
$apiServiceAttribute = $reflectionServiceClass->getAttributes(ApiServiceMetadata::class);
36+
if ($apiServiceAttribute === []) {
37+
continue;
38+
}
39+
$apiServiceAttribute = $apiServiceAttribute[0];
40+
/**
41+
* @var ApiServiceMetadata $apiServiceAttrInstance
42+
*/
43+
$apiServiceAttrInstance = $apiServiceAttribute->newInstance();
44+
// process api service
45+
$serviceMethods = $reflectionServiceClass->getMethods();
46+
foreach ($serviceMethods as $method) {
47+
$attributes = $method->getAttributes(ApiEndpointMetadata::class);
48+
foreach ($attributes as $attribute) {
49+
/**
50+
* @var ApiEndpointMetadata $instance
51+
*/
52+
$instance = $attribute->newInstance();
53+
54+
// find return type file name
55+
$returnTypeFileName = null;
56+
if ($method->getReturnType() !== null) {
57+
$returnTypeName = $method->getReturnType()->getName();
58+
if (class_exists($returnTypeName)) {
59+
$reflectionReturnType = new ReflectionClass($returnTypeName);
60+
$returnTypeFileName = substr($this->filesystem->makePathRelative($reflectionReturnType->getFileName(), $sdkBaseDir), 0, -1);
61+
}
62+
}
63+
64+
$supportedInSdkMethods[$instance->name] = [
65+
'sdk_scope' => $apiServiceAttrInstance->scope->getScopeCodes()[0],
66+
'name' => $instance->name,
67+
'documentation_url' => $instance->documentationUrl,
68+
'description' => $instance->description,
69+
'is_deprecated' => $instance->isDeprecated,
70+
'deprecation_message' => $instance->deprecationMessage,
71+
'sdk_method_name' => $method->getName(),
72+
'sdk_method_file_name' => substr($this->filesystem->makePathRelative($method->getFileName(), $sdkBaseDir), 0, -1),
73+
'sdk_method_file_start_line' => $method->getStartLine(),
74+
'sdk_method_file_end_line' => $method->getEndLine(),
75+
'sdk_class_name' => $className,
76+
'sdk_return_type_class' => $method->getReturnType()?->getName(),
77+
'sdk_return_type_file_name' => $returnTypeFileName
78+
];
79+
}
80+
}
81+
}
82+
return $supportedInSdkMethods;
83+
}
84+
85+
/**
86+
* @param class-string[] $sdkClassNames
87+
* @return array<string, mixed>
88+
*/
89+
public function getSupportedInSdkBatchMethods(array $sdkClassNames): array
90+
{
91+
$supportedInSdkMethods = [];
92+
foreach ($sdkClassNames as $className) {
93+
$reflectionServiceClass = new ReflectionClass($className);
94+
$apiServiceAttribute = $reflectionServiceClass->getAttributes(ApiBatchServiceMetadata::class);
95+
if ($apiServiceAttribute === []) {
96+
continue;
97+
}
98+
//try to get type information from phpdoc annotations
99+
$typhoonClassMeta = $this->typhoonReflector->reflectClass($className);
100+
/**
101+
* @var ApiBatchServiceMetadata $apiServiceAttrInstance
102+
*/
103+
$apiServiceAttribute = $apiServiceAttribute[0];
104+
$apiServiceAttrInstance = $apiServiceAttribute->newInstance();
105+
// process api service
106+
$serviceMethods = $reflectionServiceClass->getMethods();
107+
foreach ($serviceMethods as $method) {
108+
$attributes = $method->getAttributes(ApiBatchMethodMetadata::class);
109+
foreach ($attributes as $attribute) {
110+
/**
111+
* @var ApiBatchMethodMetadata $instance
112+
*/
113+
$instance = $attribute->newInstance();
114+
$sdkReturnTypeTyphoon = null;
115+
if ($method->getReturnType() !== null) {
116+
// get return type from phpdoc annotation
117+
$sdkReturnTypeTyphoon = stringify($typhoonClassMeta->methods()[$method->getName()]->returnType());
118+
}
119+
120+
$supportedInSdkMethods[$instance->name][] = [
121+
'sdk_scope' => $apiServiceAttrInstance->scope->getScopeCodes()[0],
122+
'name' => $instance->name,
123+
'documentation_url' => $instance->documentationUrl,
124+
'description' => $instance->description,
125+
'is_deprecated' => $instance->isDeprecated,
126+
'deprecation_message' => $instance->deprecationMessage,
127+
'sdk_method_name' => $method->getName(),
128+
'sdk_method_file_name' => $method->getFileName(),
129+
'sdk_method_file_start_line' => $method->getStartLine(),
130+
'sdk_method_file_end_line' => $method->getEndLine(),
131+
'sdk_method_return_type_typhoon' => $sdkReturnTypeTyphoon,
132+
'sdk_class_name' => $className,
133+
];
134+
}
135+
}
136+
}
137+
return $supportedInSdkMethods;
138+
}
139+
}

0 commit comments

Comments
 (0)