diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 6e5393c3..15e131ad 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -29,6 +29,7 @@
->in(__DIR__ . '/src/Services/SonetGroup/')
->in(__DIR__ . '/src/Services/IMOpenLines/')
->in(__DIR__ . '/src/Services/Landing/')
+ ->in(__DIR__ . '/src/Services/Catalog/')
->name('*.php')
->exclude(['vendor', 'storage', 'docker', 'docs']) // Exclude directories
->ignoreDotFiles(true)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4bc3b52..22468c82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@
### Added
+- Added service `Services\Catalog\Section` with support methods,
+ see [catalog.section.* methods](https://apidocs.bitrix24.com/api-reference/catalog/section/index.html) ([#583](https://github.com/bitrix24/b24phpsdk/issues/583)):
+ - `add` creates a new trade-catalog section, with batch calls support
+ - `update` updates an existing trade-catalog section, with batch calls support
+ - `get` gets a trade-catalog section by its identifier
+ - `list` gets the list of trade-catalog sections by filter
+ - `delete` deletes a trade-catalog section, with batch calls support
+ - `getFields` returns the description of trade-catalog section fields
- Added service `Services\Landing\Site\Service\Site` with support methods,
see [landing.site.* methods](https://github.com/bitrix24/b24phpsdk/issues/267):
- `add` adds a site
diff --git a/Makefile b/Makefile
index c2fc4a9a..0e08c5b7 100644
--- a/Makefile
+++ b/Makefile
@@ -492,6 +492,13 @@ test-integration-landing-role:
test-integration-landing-repowidget:
docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_landing_repowidget
+.PHONY: test-integration-catalog-section
+test-integration-catalog-section:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_catalog_section
+.PHONY: test-integration-catalog-section-annotations
+test-integration-catalog-section-annotations:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_catalog_section_annotations
+
# work dev environment
.PHONY: php-dev-server-up
php-dev-server-up:
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d0187ae5..b7bd6749 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -262,6 +262,12 @@
./tests/Integration/Services/Landing/RepoWidget/
+
+ ./tests/Integration/Services/Catalog/Section/Service/
+
+
+ ./tests/Integration/Services/Catalog/Section/Result/SectionItemResultTest.php
+
diff --git a/src/Services/Catalog/Catalog/Result/CatalogItemResult.php b/src/Services/Catalog/Catalog/Result/CatalogItemResult.php
index e62fc7cb..189852bb 100644
--- a/src/Services/Catalog/Catalog/Result/CatalogItemResult.php
+++ b/src/Services/Catalog/Catalog/Result/CatalogItemResult.php
@@ -29,4 +29,4 @@
*/
class CatalogItemResult extends AbstractCatalogItem
{
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Catalog/Result/CatalogResult.php b/src/Services/Catalog/Catalog/Result/CatalogResult.php
index aa468aa8..c5222f11 100644
--- a/src/Services/Catalog/Catalog/Result/CatalogResult.php
+++ b/src/Services/Catalog/Catalog/Result/CatalogResult.php
@@ -21,4 +21,4 @@ public function catalog(): CatalogItemResult
{
return new CatalogItemResult($this->getCoreResponse()->getResponseData()->getResult()['catalog']);
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Catalog/Result/CatalogsResult.php b/src/Services/Catalog/Catalog/Result/CatalogsResult.php
index add5f259..f721c3bb 100644
--- a/src/Services/Catalog/Catalog/Result/CatalogsResult.php
+++ b/src/Services/Catalog/Catalog/Result/CatalogsResult.php
@@ -32,4 +32,4 @@ public function getCatalogs(): array
return $res;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Catalog/Service/Catalog.php b/src/Services/Catalog/Catalog/Service/Catalog.php
index d9cb55fe..0da76ea3 100644
--- a/src/Services/Catalog/Catalog/Service/Catalog.php
+++ b/src/Services/Catalog/Catalog/Service/Catalog.php
@@ -82,4 +82,4 @@ public function fields(): FieldsResult
{
return new FieldsResult($this->core->call('catalog.catalog.getFields'));
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/CatalogServiceBuilder.php b/src/Services/Catalog/CatalogServiceBuilder.php
index 57c91b88..6724d597 100644
--- a/src/Services/Catalog/CatalogServiceBuilder.php
+++ b/src/Services/Catalog/CatalogServiceBuilder.php
@@ -17,6 +17,7 @@
use Bitrix24\SDK\Core\Credentials\Scope;
use Bitrix24\SDK\Services\AbstractServiceBuilder;
use Bitrix24\SDK\Services\Catalog;
+
#[ApiServiceBuilderMetadata(new Scope(['catalog']))]
class CatalogServiceBuilder extends AbstractServiceBuilder
{
@@ -44,4 +45,20 @@ public function catalog(): Catalog\Catalog\Service\Catalog
return $this->serviceCache[__METHOD__];
}
-}
\ No newline at end of file
+
+ public function section(): Catalog\Section\Service\Section
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Catalog\Section\Service\Section(
+ new Catalog\Section\Service\Batch(
+ new Catalog\Section\Batch($this->core, $this->log),
+ $this->log
+ ),
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+}
diff --git a/src/Services/Catalog/Common/ProductType.php b/src/Services/Catalog/Common/ProductType.php
index 0bf8ef09..ad568a47 100644
--- a/src/Services/Catalog/Common/ProductType.php
+++ b/src/Services/Catalog/Common/ProductType.php
@@ -20,4 +20,4 @@ enum ProductType: int
case SKU = 3;
case productOffer = 4;
case genericOffer = 5;
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Common/Result/AbstractCatalogItem.php b/src/Services/Catalog/Common/Result/AbstractCatalogItem.php
index e2929d73..9761daed 100644
--- a/src/Services/Catalog/Common/Result/AbstractCatalogItem.php
+++ b/src/Services/Catalog/Common/Result/AbstractCatalogItem.php
@@ -109,4 +109,4 @@ protected function getKeyWithUserfieldByFieldName(string $fieldName)
return $this->$fieldName;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Result/ProductItemResult.php b/src/Services/Catalog/Product/Result/ProductItemResult.php
index c576d9b7..58c80851 100644
--- a/src/Services/Catalog/Product/Result/ProductItemResult.php
+++ b/src/Services/Catalog/Product/Result/ProductItemResult.php
@@ -52,4 +52,4 @@
*/
class ProductItemResult extends AbstractCatalogItem
{
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Result/ProductResult.php b/src/Services/Catalog/Product/Result/ProductResult.php
index 2751fda8..fc83fa8c 100644
--- a/src/Services/Catalog/Product/Result/ProductResult.php
+++ b/src/Services/Catalog/Product/Result/ProductResult.php
@@ -26,4 +26,4 @@ public function product(): ProductItemResult
return new ProductItemResult($this->getCoreResponse()->getResponseData()->getResult()['product']);
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Result/ProductsResult.php b/src/Services/Catalog/Product/Result/ProductsResult.php
index aa644fa3..268d6d8f 100644
--- a/src/Services/Catalog/Product/Result/ProductsResult.php
+++ b/src/Services/Catalog/Product/Result/ProductsResult.php
@@ -31,4 +31,4 @@ public function getProducts(): array
return $res;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Service/Batch.php b/src/Services/Catalog/Product/Service/Batch.php
index 45dddcb7..42970546 100644
--- a/src/Services/Catalog/Product/Service/Batch.php
+++ b/src/Services/Catalog/Product/Service/Batch.php
@@ -25,7 +25,7 @@
{
public function __construct(
protected BatchOperationsInterface $batch,
- protected LoggerInterface $log)
- {
+ protected LoggerInterface $log
+ ) {
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Service/Product.php b/src/Services/Catalog/Product/Service/Product.php
index 730fd7c9..d37d34f6 100644
--- a/src/Services/Catalog/Product/Service/Product.php
+++ b/src/Services/Catalog/Product/Service/Product.php
@@ -25,7 +25,6 @@
use Bitrix24\SDK\Services\Catalog\Common\ProductType;
use Bitrix24\SDK\Services\Catalog\Product\Result\ProductResult;
use Bitrix24\SDK\Services\Catalog\Product\Result\ProductsResult;
-
use Psr\Log\LoggerInterface;
#[ApiServiceMetadata(new Scope(['catalog']))]
@@ -35,8 +34,7 @@ public function __construct(
public Batch $batch,
CoreInterface $core,
LoggerInterface $logger
- )
- {
+ ) {
parent::__construct($core, $logger);
}
@@ -71,7 +69,9 @@ public function get(int $productId): ProductResult
)]
public function add(array $productFields): ProductResult
{
- return new ProductResult($this->core->call('catalog.product.add', [
+ return new ProductResult($this->core->call(
+ 'catalog.product.add',
+ [
'fields' => $productFields
]
));
@@ -140,4 +140,4 @@ public function fieldsByFilter(int $iblockId, ProductType $productType, ?array $
return new FieldsResult($this->core->call('catalog.product.getFieldsByFilter', ['filter' => $filter]));
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Section/Batch.php b/src/Services/Catalog/Section/Batch.php
new file mode 100644
index 00000000..b257245e
--- /dev/null
+++ b/src/Services/Catalog/Section/Batch.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+use Generator;
+
+/**
+ * Class Batch
+ *
+ * Overrides base Batch to handle parameter naming differences in catalog.section.* REST methods:
+ * - delete uses lowercase 'id' instead of 'ID'
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-delete.html
+ * @see https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-list.html
+ */
+class Batch extends \Bitrix24\SDK\Core\Batch
+{
+ /**
+ * Determines the ID key — lowercase 'id' for catalog section
+ */
+ #[\Override]
+ protected function determineKeyId(string $apiMethod, ?array $additionalParameters): string
+ {
+ return 'id';
+ }
+
+ /**
+ * Delete entity items with batch call using lowercase 'id' parameter
+ *
+ * @param int[] $entityItemId
+ * @param array|null $additionalParameters
+ *
+ * @return Generator|ResponseData[]
+ * @throws BaseException
+ */
+ #[\Override]
+ public function deleteEntityItems(
+ string $apiMethod,
+ array $entityItemId,
+ ?array $additionalParameters = null
+ ): Generator {
+ $this->logger->debug(
+ 'deleteEntityItems.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'entityItems' => $entityItemId,
+ 'additionalParameters' => $additionalParameters,
+ ]
+ );
+
+ try {
+ $this->clearCommands();
+ foreach ($entityItemId as $cnt => $itemId) {
+ if (!is_int($itemId)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'invalid type «%s» of section id «%s» at position %s, section id must be integer type',
+ gettype($itemId),
+ $itemId,
+ $cnt
+ )
+ );
+ }
+
+ $this->registerCommand($apiMethod, ['id' => $itemId]);
+ }
+
+ foreach ($this->getTraversable(true) as $cnt => $deletedItemResult) {
+ yield $cnt => $deletedItemResult;
+ }
+ } catch (InvalidArgumentException $exception) {
+ $errorMessage = sprintf('batch delete section items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+ throw $exception;
+ } catch (\Throwable $exception) {
+ $errorMessage = sprintf('batch delete section items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+
+ throw new BaseException($errorMessage, $exception->getCode(), $exception);
+ }
+
+ $this->logger->debug('deleteEntityItems.finish');
+ }
+}
diff --git a/src/Services/Catalog/Section/Result/SectionAddedBatchResult.php b/src/Services/Catalog/Section/Result/SectionAddedBatchResult.php
new file mode 100644
index 00000000..7f7a5876
--- /dev/null
+++ b/src/Services/Catalog/Section/Result/SectionAddedBatchResult.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Result;
+
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+
+class SectionAddedBatchResult
+{
+ public function __construct(private readonly ResponseData $responseData)
+ {
+ }
+
+ public function getResponseData(): ResponseData
+ {
+ return $this->responseData;
+ }
+
+ public function section(): SectionItemResult
+ {
+ return new SectionItemResult($this->responseData->getResult()['section']);
+ }
+}
diff --git a/src/Services/Catalog/Section/Result/SectionFieldsResult.php b/src/Services/Catalog/Section/Result/SectionFieldsResult.php
new file mode 100644
index 00000000..48b1f0ab
--- /dev/null
+++ b/src/Services/Catalog/Section/Result/SectionFieldsResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class SectionFieldsResult extends AbstractResult
+{
+ /**
+ * @return array>
+ * @throws BaseException
+ */
+ public function getFieldsDescription(): array
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()['section'];
+ }
+}
diff --git a/src/Services/Catalog/Section/Result/SectionItemResult.php b/src/Services/Catalog/Section/Result/SectionItemResult.php
new file mode 100644
index 00000000..d1f88d65
--- /dev/null
+++ b/src/Services/Catalog/Section/Result/SectionItemResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $id
+ * @property-read int $iblockId
+ * @property-read int|null $iblockSectionId
+ * @property-read string $name
+ * @property-read string|null $xmlId
+ * @property-read string|null $code
+ * @property-read int|null $sort
+ * @property-read bool|null $active
+ * @property-read string|null $description
+ * @property-read string|null $descriptionType
+ */
+class SectionItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/Catalog/Section/Result/SectionResult.php b/src/Services/Catalog/Section/Result/SectionResult.php
new file mode 100644
index 00000000..0df7a328
--- /dev/null
+++ b/src/Services/Catalog/Section/Result/SectionResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class SectionResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function section(): SectionItemResult
+ {
+ return new SectionItemResult($this->getCoreResponse()->getResponseData()->getResult()['section']);
+ }
+}
diff --git a/src/Services/Catalog/Section/Result/SectionUpdatedBatchResult.php b/src/Services/Catalog/Section/Result/SectionUpdatedBatchResult.php
new file mode 100644
index 00000000..f381da3d
--- /dev/null
+++ b/src/Services/Catalog/Section/Result/SectionUpdatedBatchResult.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Result;
+
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+
+class SectionUpdatedBatchResult
+{
+ public function __construct(private readonly ResponseData $responseData)
+ {
+ }
+
+ public function getResponseData(): ResponseData
+ {
+ return $this->responseData;
+ }
+
+ public function section(): SectionItemResult
+ {
+ return new SectionItemResult($this->responseData->getResult()['section']);
+ }
+}
diff --git a/src/Services/Catalog/Section/Result/SectionsResult.php b/src/Services/Catalog/Section/Result/SectionsResult.php
new file mode 100644
index 00000000..d3290dc2
--- /dev/null
+++ b/src/Services/Catalog/Section/Result/SectionsResult.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class SectionsResult extends AbstractResult
+{
+ /**
+ * @return SectionItemResult[]
+ * @throws BaseException
+ */
+ public function getSections(): array
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ return array_map(
+ static fn (array $item): SectionItemResult => new SectionItemResult($item),
+ $result['sections'] ?? []
+ );
+ }
+}
diff --git a/src/Services/Catalog/Section/Service/Batch.php b/src/Services/Catalog/Section/Service/Batch.php
new file mode 100644
index 00000000..0f0f60c8
--- /dev/null
+++ b/src/Services/Catalog/Section/Service/Batch.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Service;
+
+use Bitrix24\SDK\Attributes\ApiBatchMethodMetadata;
+use Bitrix24\SDK\Attributes\ApiBatchServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\DeletedItemBatchResult;
+use Bitrix24\SDK\Services\Catalog\Section;
+use Bitrix24\SDK\Services\Catalog\Section\Result\SectionAddedBatchResult;
+use Bitrix24\SDK\Services\Catalog\Section\Result\SectionUpdatedBatchResult;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+#[ApiBatchServiceMetadata(new Scope(['catalog']))]
+class Batch
+{
+ public function __construct(protected Section\Batch $batch, protected LoggerInterface $log)
+ {
+ }
+
+ /**
+ * Batch adding trade-catalog sections
+ *
+ * @param array $sections
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'catalog.section.add',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-add.html',
+ 'Batch adding trade-catalog sections'
+ )]
+ public function add(array $sections): Generator
+ {
+ $items = [];
+ foreach ($sections as $section) {
+ $items[] = ['fields' => $section];
+ }
+
+ foreach ($this->batch->addEntityItems('catalog.section.add', $items) as $key => $item) {
+ yield $key => new SectionAddedBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch delete trade-catalog sections
+ *
+ * @param int[] $sectionId
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'catalog.section.delete',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-delete.html',
+ 'Batch delete trade-catalog sections'
+ )]
+ public function delete(array $sectionId): Generator
+ {
+ foreach ($this->batch->deleteEntityItems('catalog.section.delete', $sectionId) as $key => $item) {
+ yield $key => new DeletedItemBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch update trade-catalog sections
+ *
+ * @param array $sections keyed by section id
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'catalog.section.update',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-update.html',
+ 'Batch update trade-catalog sections'
+ )]
+ public function update(array $sections): Generator
+ {
+ $items = [];
+ foreach ($sections as $id => $section) {
+ $items[$id] = ['fields' => $section];
+ }
+
+ foreach ($this->batch->updateEntityItems('catalog.section.update', $items) as $key => $item) {
+ yield $key => new SectionUpdatedBatchResult($item);
+ }
+ }
+}
diff --git a/src/Services/Catalog/Section/Service/Section.php b/src/Services/Catalog/Section/Service/Section.php
new file mode 100644
index 00000000..f5d0a59e
--- /dev/null
+++ b/src/Services/Catalog/Section/Service/Section.php
@@ -0,0 +1,152 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\Section\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Contracts\CoreInterface;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\DeletedItemResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\Catalog\Section\Result\SectionFieldsResult;
+use Bitrix24\SDK\Services\Catalog\Section\Result\SectionResult;
+use Bitrix24\SDK\Services\Catalog\Section\Result\SectionsResult;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['catalog']))]
+class Section extends AbstractService
+{
+ public function __construct(public Batch $batch, CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Adds a new trade-catalog section
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-add.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.section.add',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-add.html',
+ 'Adds a new trade-catalog section'
+ )]
+ public function add(array $fields): SectionResult
+ {
+ return new SectionResult($this->core->call('catalog.section.add', ['fields' => $fields]));
+ }
+
+ /**
+ * Updates a trade-catalog section by its identifier
+ *
+ * Note: despite the API documentation listing `iblockId` as optional on update, the live
+ * API rejects the call with "Required fields: iblockId" if it is omitted from $fields.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-update.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.section.update',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-update.html',
+ 'Updates a trade-catalog section by its identifier'
+ )]
+ public function update(int $id, array $fields): SectionResult
+ {
+ return new SectionResult($this->core->call('catalog.section.update', ['id' => $id, 'fields' => $fields]));
+ }
+
+ /**
+ * Returns a trade-catalog section by its identifier
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.section.get',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-get.html',
+ 'Returns a trade-catalog section by its identifier'
+ )]
+ public function get(int $id): SectionResult
+ {
+ return new SectionResult($this->core->call('catalog.section.get', ['id' => $id]));
+ }
+
+ /**
+ * Returns a list of trade-catalog sections by filter
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-list.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.section.list',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-list.html',
+ 'Returns a list of trade-catalog sections by filter'
+ )]
+ public function list(array $select = [], array $filter = []): SectionsResult
+ {
+ return new SectionsResult(
+ $this->core->call(
+ 'catalog.section.list',
+ ['select' => $select, 'filter' => $filter]
+ )
+ );
+ }
+
+ /**
+ * Deletes a trade-catalog section by identifier
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.section.delete',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-delete.html',
+ 'Deletes a trade-catalog section by identifier'
+ )]
+ public function delete(int $id): DeletedItemResult
+ {
+ return new DeletedItemResult($this->core->call('catalog.section.delete', ['id' => $id]));
+ }
+
+ /**
+ * Returns the fields of a trade-catalog section
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-get-fields.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.section.getFields',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/section/catalog-section-get-fields.html',
+ 'Returns the description of trade-catalog section fields'
+ )]
+ public function getFields(): SectionFieldsResult
+ {
+ return new SectionFieldsResult($this->core->call('catalog.section.getFields'));
+ }
+}
diff --git a/tests/Integration/Services/Catalog/Section/Result/SectionItemResultTest.php b/tests/Integration/Services/Catalog/Section/Result/SectionItemResultTest.php
new file mode 100644
index 00000000..e6069cfa
--- /dev/null
+++ b/tests/Integration/Services/Catalog/Section/Result/SectionItemResultTest.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\Catalog\Section\Result;
+
+use Bitrix24\SDK\Services\Catalog\Section\Result\SectionItemResult;
+use Bitrix24\SDK\Services\Catalog\Section\Service\Section;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(SectionItemResult::class)]
+class SectionItemResultTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Section $sectionService;
+
+ private int $sectionId;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $serviceBuilder = Fabric::getServiceBuilder(true);
+ $this->sectionService = $serviceBuilder->getCatalogScope()->section();
+ $iblockId = $serviceBuilder->getCatalogScope()->catalog()
+ ->list([], [], [], 1)->getCatalogs()[0]->iblockId;
+
+ $this->sectionId = $this->sectionService->add([
+ 'name' => sprintf('test section annotations %s', time()),
+ 'iblockId' => $iblockId,
+ ])->section()->id;
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ $this->sectionService->delete($this->sectionId);
+ }
+
+ #[Test]
+ #[TestDox('all fields in SectionItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllFieldsAreAnnotated(): void
+ {
+ $rawItem = $this->sectionService->get($this->sectionId)
+ ->getCoreResponse()->getResponseData()->getResult()['section'];
+ $this->assertBitrix24AllResultItemFieldsAnnotated(array_keys($rawItem), SectionItemResult::class);
+ }
+
+ #[Test]
+ #[TestDox('all fields in SectionItemResult have valid type annotation in phpdoc')]
+ public function testAllFieldsHasValidTypeAnnotation(): void
+ {
+ $allFields = $this->sectionService->getFields()->getFieldsDescription();
+ foreach ($allFields as $field => $params) {
+ $newParams = [];
+ foreach ($params as $key => $value) {
+ $newParams[mb_strtolower((string)$key)] = $value;
+ }
+
+ $allFields[$field] = $newParams;
+ }
+
+ $this->assertBitrix24AllResultItemFieldsHasValidTypeAnnotation($allFields, SectionItemResult::class);
+ }
+}
diff --git a/tests/Integration/Services/Catalog/Section/Service/BatchTest.php b/tests/Integration/Services/Catalog/Section/Service/BatchTest.php
new file mode 100644
index 00000000..0719b3fd
--- /dev/null
+++ b/tests/Integration/Services/Catalog/Section/Service/BatchTest.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\Catalog\Section\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Catalog\Section\Service\Batch;
+use Bitrix24\SDK\Services\Catalog\Section\Service\Section;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ private Section $sectionService;
+
+ private int $iblockId;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $serviceBuilder = Fabric::getServiceBuilder(true);
+ $this->sectionService = $serviceBuilder->getCatalogScope()->section();
+ $this->iblockId = $serviceBuilder->getCatalogScope()->catalog()
+ ->list([], [], [], 1)->getCatalogs()[0]->iblockId;
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('test Batch::add, Batch::update, Batch::delete')]
+ public function testAddUpdateDelete(): void
+ {
+ $addedIds = [];
+ foreach ($this->sectionService->batch->add([
+ ['name' => sprintf('batch section %s', time()), 'iblockId' => $this->iblockId],
+ ]) as $addedItemResult) {
+ $addedIds[] = $addedItemResult->section()->id;
+ }
+
+ $this->assertCount(1, $addedIds);
+
+ $updatePayload = [];
+ foreach ($addedIds as $addedId) {
+ $updatePayload[$addedId] = ['name' => 'updated batch section', 'iblockId' => $this->iblockId];
+ }
+
+ $updatedCount = 0;
+ foreach ($this->sectionService->batch->update($updatePayload) as $updatedItemResult) {
+ $this->assertSame('updated batch section', $updatedItemResult->section()->name);
+ $updatedCount++;
+ }
+
+ $this->assertSame(1, $updatedCount);
+
+ $deletedCount = 0;
+ foreach ($this->sectionService->batch->delete($addedIds) as $deletedItemResult) {
+ $this->assertTrue($deletedItemResult->isSuccess());
+ $deletedCount++;
+ }
+
+ $this->assertSame(1, $deletedCount);
+ }
+}
diff --git a/tests/Integration/Services/Catalog/Section/Service/SectionTest.php b/tests/Integration/Services/Catalog/Section/Service/SectionTest.php
new file mode 100644
index 00000000..bf29f3f5
--- /dev/null
+++ b/tests/Integration/Services/Catalog/Section/Service/SectionTest.php
@@ -0,0 +1,101 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\Catalog\Section\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Catalog\Section\Service\Section;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Section::class)]
+class SectionTest extends TestCase
+{
+ private Section $sectionService;
+
+ private int $iblockId;
+
+ /**
+ * @var int[]
+ */
+ private array $createdSectionIds = [];
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $serviceBuilder = Fabric::getServiceBuilder(true);
+ $this->sectionService = $serviceBuilder->getCatalogScope()->section();
+ $this->iblockId = $serviceBuilder->getCatalogScope()->catalog()
+ ->list([], [], [], 1)->getCatalogs()[0]->iblockId;
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->createdSectionIds as $createdSectionId) {
+ try {
+ $this->sectionService->delete($createdSectionId);
+ } catch (\Throwable) {
+ // already removed, ignore
+ }
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('test Section::add, Section::update, Section::get, Section::list, Section::delete')]
+ public function testAddUpdateGetListDelete(): void
+ {
+ $name = sprintf('test section %s', time());
+ $sectionResult = $this->sectionService->add([
+ 'name' => $name,
+ 'iblockId' => $this->iblockId,
+ ]);
+ $sectionId = $sectionResult->section()->id;
+ $this->createdSectionIds[] = $sectionId;
+
+ $this->assertSame($name, $sectionResult->section()->name);
+ $this->assertSame($this->iblockId, $sectionResult->section()->iblockId);
+
+ $updatedName = sprintf('updated test section %s', time());
+ $updateResult = $this->sectionService->update($sectionId, ['name' => $updatedName, 'iblockId' => $this->iblockId]);
+ $this->assertSame($updatedName, $updateResult->section()->name);
+
+ $getResult = $this->sectionService->get($sectionId);
+ $this->assertSame($updatedName, $getResult->section()->name);
+
+ $sectionsResult = $this->sectionService->list([], ['iblockId' => $this->iblockId, 'id' => $sectionId]);
+ $this->assertCount(1, $sectionsResult->getSections());
+
+ $this->assertTrue($this->sectionService->delete($sectionId)->isSuccess());
+ $this->createdSectionIds = array_diff($this->createdSectionIds, [$sectionId]);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('test Section::getFields')]
+ public function testGetFields(): void
+ {
+ $fieldsDescription = $this->sectionService->getFields()->getFieldsDescription();
+ $this->assertIsArray($fieldsDescription);
+ $this->assertArrayHasKey('iblockId', $fieldsDescription);
+ $this->assertArrayHasKey('name', $fieldsDescription);
+ }
+}
diff --git a/tests/Unit/Services/Catalog/Section/Service/SectionTest.php b/tests/Unit/Services/Catalog/Section/Service/SectionTest.php
new file mode 100644
index 00000000..cb3c1b1f
--- /dev/null
+++ b/tests/Unit/Services/Catalog/Section/Service/SectionTest.php
@@ -0,0 +1,98 @@
+mockCore('catalog.section.add', [
+ 'fields' => ['name' => 'Kids Toys', 'iblockId' => 14],
+ ]);
+
+ self::assertInstanceOf(
+ SectionResult::class,
+ $this->makeService($core)->add(['name' => 'Kids Toys', 'iblockId' => 14])
+ );
+ }
+
+ public function testUpdateBuildsParameters(): void
+ {
+ $core = $this->mockCore('catalog.section.update', [
+ 'id' => 32,
+ 'fields' => ['name' => 'Updated name'],
+ ]);
+
+ self::assertInstanceOf(
+ SectionResult::class,
+ $this->makeService($core)->update(32, ['name' => 'Updated name'])
+ );
+ }
+
+ public function testGetBuildsParameters(): void
+ {
+ $core = $this->mockCore('catalog.section.get', ['id' => 31]);
+
+ self::assertInstanceOf(SectionResult::class, $this->makeService($core)->get(31));
+ }
+
+ public function testListBuildsParameters(): void
+ {
+ $core = $this->mockCore('catalog.section.list', [
+ 'select' => ['id', 'name'],
+ 'filter' => ['iblockId' => 14],
+ ]);
+
+ self::assertInstanceOf(
+ SectionsResult::class,
+ $this->makeService($core)->list(['id', 'name'], ['iblockId' => 14])
+ );
+ }
+
+ public function testDeleteBuildsParameters(): void
+ {
+ $core = $this->mockCore('catalog.section.delete', ['id' => 31]);
+
+ self::assertInstanceOf(DeletedItemResult::class, $this->makeService($core)->delete(31));
+ }
+
+ public function testGetFieldsBuildsParameters(): void
+ {
+ $core = $this->mockCore('catalog.section.getFields', []);
+
+ self::assertInstanceOf(SectionFieldsResult::class, $this->makeService($core)->getFields());
+ }
+
+ private function makeService(CoreInterface $core): Section
+ {
+ return new Section(new Batch(new SectionBatch($core, new NullLogger()), new NullLogger()), $core, new NullLogger());
+ }
+
+ private function mockCore(string $method, array $parameters): CoreInterface
+ {
+ $response = $this->createStub(Response::class);
+ $core = $this->createMock(CoreInterface::class);
+ $core->expects($this->once())
+ ->method('call')
+ ->with($method, $parameters)
+ ->willReturn($response);
+
+ return $core;
+ }
+}