From f60f62fa763abefbe73c41b98967c9d2e8e21a00 Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatenko Date: Wed, 22 Jul 2026 14:59:02 +0400 Subject: [PATCH 1/2] Add catalog.productProperty methods --- .php-cs-fixer.php | 3 +- CHANGELOG.md | 8 + Makefile | 12 ++ phpunit.xml.dist | 9 + .../Catalog/Result/CatalogItemResult.php | 2 +- .../Catalog/Catalog/Result/CatalogResult.php | 2 +- .../Catalog/Catalog/Result/CatalogsResult.php | 2 +- .../Catalog/Catalog/Service/Catalog.php | 2 +- .../Catalog/CatalogServiceBuilder.php | 20 +- src/Services/Catalog/Common/ProductType.php | 2 +- .../Common/Result/AbstractCatalogItem.php | 2 +- .../Product/Result/ProductItemResult.php | 2 +- .../Catalog/Product/Result/ProductResult.php | 2 +- .../Catalog/Product/Result/ProductsResult.php | 2 +- .../Catalog/Product/Service/Batch.php | 6 +- .../Catalog/Product/Service/Product.php | 8 +- .../Catalog/ProductProperty/Batch.php | 109 ++++++++++ .../AddedProductPropertyBatchResult.php | 30 +++ .../DeletedProductPropertyBatchResult.php | 30 +++ .../Result/DeletedProductPropertyResult.php | 34 +++ .../Result/ProductPropertiesResult.php | 39 ++++ .../Result/ProductPropertyFieldsResult.php | 34 +++ .../Result/ProductPropertyItemResult.php | 49 +++++ .../Result/ProductPropertyResult.php | 35 ++++ .../UpdatedProductPropertyBatchResult.php | 30 +++ .../Catalog/ProductProperty/Service/Batch.php | 134 ++++++++++++ .../Service/ProductProperty.php | 166 +++++++++++++++ ...oductPropertyItemResultAnnotationsTest.php | 109 ++++++++++ .../ProductProperty/Service/BatchTest.php | 193 ++++++++++++++++++ .../Service/ProductPropertyTest.php | 188 +++++++++++++++++ .../Service/ProductPropertyTest.php | 179 ++++++++++++++++ 31 files changed, 1425 insertions(+), 18 deletions(-) create mode 100644 src/Services/Catalog/ProductProperty/Batch.php create mode 100644 src/Services/Catalog/ProductProperty/Result/AddedProductPropertyBatchResult.php create mode 100644 src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyBatchResult.php create mode 100644 src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyResult.php create mode 100644 src/Services/Catalog/ProductProperty/Result/ProductPropertiesResult.php create mode 100644 src/Services/Catalog/ProductProperty/Result/ProductPropertyFieldsResult.php create mode 100644 src/Services/Catalog/ProductProperty/Result/ProductPropertyItemResult.php create mode 100644 src/Services/Catalog/ProductProperty/Result/ProductPropertyResult.php create mode 100644 src/Services/Catalog/ProductProperty/Result/UpdatedProductPropertyBatchResult.php create mode 100644 src/Services/Catalog/ProductProperty/Service/Batch.php create mode 100644 src/Services/Catalog/ProductProperty/Service/ProductProperty.php create mode 100644 tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php create mode 100644 tests/Integration/Services/Catalog/ProductProperty/Service/BatchTest.php create mode 100644 tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php create mode 100644 tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 6e5393c3..846a8c0a 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) @@ -40,4 +41,4 @@ ->setFinder($finder) ->setRules([ '@PSR12' => true, // PSR-12 coding standards - ]); \ No newline at end of file + ]); diff --git a/CHANGELOG.md b/CHANGELOG.md index f4bc3b52..2a76091e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ ### Added +- Added service `Services\Catalog\ProductProperty` with support methods, + see [catalog.productProperty.* methods](https://apidocs.bitrix24.com/api-reference/catalog/product-property/index.html) ([#538](https://github.com/bitrix24/b24phpsdk/issues/538)): + - `add` creates a new product or variation property, with batch calls support + - `update` updates an existing product or variation property, with batch calls support + - `get` gets information about a product or variation property by its identifier + - `list` gets the list of product and variation properties by filter, with batch calls support + - `delete` deletes a product or variation property, with batch calls support + - `getFields` returns the description of product or variation property 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..8b287086 100644 --- a/Makefile +++ b/Makefile @@ -492,6 +492,18 @@ test-integration-landing-role: test-integration-landing-repowidget: docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_landing_repowidget +.PHONY: integration-tests-catalog-product-property +integration-tests-catalog-product-property: + docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_catalog_product_property + +.PHONY: integration-tests-catalog-product-property-service +integration-tests-catalog-product-property-service: + docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_catalog_product_property_service + +.PHONY: integration-tests-catalog-product-property-annotations +integration-tests-catalog-product-property-annotations: + docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_catalog_product_property_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..1eab5506 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -262,6 +262,15 @@ ./tests/Integration/Services/Landing/RepoWidget/ + + ./tests/Integration/Services/Catalog/ProductProperty/ + + + ./tests/Integration/Services/Catalog/ProductProperty/Service/ + + + ./tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.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..d5809ca5 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,21 @@ public function catalog(): Catalog\Catalog\Service\Catalog return $this->serviceCache[__METHOD__]; } -} \ No newline at end of file + + public function productProperty(): Catalog\ProductProperty\Service\ProductProperty + { + if (!isset($this->serviceCache[__METHOD__])) { + $batch = new Catalog\ProductProperty\Batch( + $this->core, + $this->log + ); + $this->serviceCache[__METHOD__] = new Catalog\ProductProperty\Service\ProductProperty( + new Catalog\ProductProperty\Service\Batch($batch, $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..b0056db5 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 ] )); diff --git a/src/Services/Catalog/ProductProperty/Batch.php b/src/Services/Catalog/ProductProperty/Batch.php new file mode 100644 index 00000000..e225f129 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Batch.php @@ -0,0 +1,109 @@ + + * + * 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\ProductProperty; + +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 a parameter naming difference in catalog.productProperty.* REST + * methods: delete uses lowercase 'id' instead of 'ID'. + * + * @see https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-delete.html + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty + */ +class Batch extends \Bitrix24\SDK\Core\Batch +{ + /** + * Determines the ID key — lowercase 'id' for catalog product property + */ + #[\Override] + protected function determineKeyId(string $apiMethod, ?array $additionalParameters): string + { + return 'id'; + } + + /** + * Delete entity items with batch call + * + * `catalog.productProperty.delete` expects a lowercase `id` parameter, unlike the base class + * which sends `ID` (uppercase) — override to match the real API contract. + * + * @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 entity id «%s» at position %s, entity 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 entity items: %s', $exception->getMessage()); + $this->logger->error( + $errorMessage, + [ + 'trace' => $exception->getTrace(), + ] + ); + throw $exception; + } catch (\Throwable $exception) { + $errorMessage = sprintf('batch delete entity 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/ProductProperty/Result/AddedProductPropertyBatchResult.php b/src/Services/Catalog/ProductProperty/Result/AddedProductPropertyBatchResult.php new file mode 100644 index 00000000..e8b2d5f8 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/AddedProductPropertyBatchResult.php @@ -0,0 +1,30 @@ + + * + * 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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Result\AddedItemBatchResult; + +/** + * Class AddedProductPropertyBatchResult + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty\Result + */ +class AddedProductPropertyBatchResult extends AddedItemBatchResult +{ + #[\Override] + public function getId(): int + { + return (int)$this->getResponseData()->getResult()['productProperty']['id']; + } +} diff --git a/src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyBatchResult.php b/src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyBatchResult.php new file mode 100644 index 00000000..af73ed68 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyBatchResult.php @@ -0,0 +1,30 @@ + + * + * 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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Result\DeletedItemBatchResult; + +/** + * Class DeletedProductPropertyBatchResult + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty\Result + */ +class DeletedProductPropertyBatchResult extends DeletedItemBatchResult +{ + #[\Override] + public function isSuccess(): bool + { + return (bool)$this->getResponseData()->getResult(); + } +} diff --git a/src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyResult.php b/src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyResult.php new file mode 100644 index 00000000..2d4c62a7 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/DeletedProductPropertyResult.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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Core\Result\DeletedItemResult; + +/** + * Class DeletedProductPropertyResult + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty\Result + */ +class DeletedProductPropertyResult extends DeletedItemResult +{ + /** + * @throws BaseException + */ + #[\Override] + public function isSuccess(): bool + { + return (bool)$this->getCoreResponse()->getResponseData()->getResult(); + } +} diff --git a/src/Services/Catalog/ProductProperty/Result/ProductPropertiesResult.php b/src/Services/Catalog/ProductProperty/Result/ProductPropertiesResult.php new file mode 100644 index 00000000..47a90c18 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/ProductPropertiesResult.php @@ -0,0 +1,39 @@ + + * + * 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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Core\Result\AbstractResult; + +/** + * Class ProductPropertiesResult + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty\Result + */ +class ProductPropertiesResult extends AbstractResult +{ + /** + * @return ProductPropertyItemResult[] + * @throws BaseException + */ + public function getProductProperties(): array + { + $items = []; + foreach ($this->getCoreResponse()->getResponseData()->getResult()['productProperties'] as $item) { + $items[] = new ProductPropertyItemResult($item); + } + + return $items; + } +} diff --git a/src/Services/Catalog/ProductProperty/Result/ProductPropertyFieldsResult.php b/src/Services/Catalog/ProductProperty/Result/ProductPropertyFieldsResult.php new file mode 100644 index 00000000..a094eecd --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/ProductPropertyFieldsResult.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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Core\Result\FieldsResult; + +/** + * Class ProductPropertyFieldsResult + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty\Result + */ +class ProductPropertyFieldsResult extends FieldsResult +{ + /** + * @throws BaseException + */ + #[\Override] + public function getFieldsDescription(): array + { + return $this->getCoreResponse()->getResponseData()->getResult()['productProperty']; + } +} diff --git a/src/Services/Catalog/ProductProperty/Result/ProductPropertyItemResult.php b/src/Services/Catalog/ProductProperty/Result/ProductPropertyItemResult.php new file mode 100644 index 00000000..29614252 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/ProductPropertyItemResult.php @@ -0,0 +1,49 @@ + + * + * 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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Result\AbstractAnnotatedItem; +use Carbon\CarbonImmutable; + +/** + * Class ProductPropertyItemResult + * + * @property-read bool $active + * @property-read string|null $code + * @property-read int $colCount + * @property-read string|null $defaultValue + * @property-read string|null $fileType + * @property-read bool $filtrable + * @property-read string|null $hint + * @property-read int $iblockId + * @property-read int $id + * @property-read bool $isRequired + * @property-read int|null $linkIblockId + * @property-read string $listType + * @property-read bool $multiple + * @property-read int|null $multipleCnt + * @property-read string $name + * @property-read string $propertyType + * @property-read int $rowCount + * @property-read bool $searchable + * @property-read int|null $sort + * @property-read CarbonImmutable $timestampX + * @property-read string|null $userType + * @property-read array|null $userTypeSettings + * @property-read bool|null $withDescription + * @property-read string|null $xmlId + */ +class ProductPropertyItemResult extends AbstractAnnotatedItem +{ +} diff --git a/src/Services/Catalog/ProductProperty/Result/ProductPropertyResult.php b/src/Services/Catalog/ProductProperty/Result/ProductPropertyResult.php new file mode 100644 index 00000000..5154cb27 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/ProductPropertyResult.php @@ -0,0 +1,35 @@ + + * + * 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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Core\Result\AbstractResult; + +/** + * Class ProductPropertyResult + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty\Result + */ +class ProductPropertyResult extends AbstractResult +{ + /** + * @throws BaseException + */ + public function productProperty(): ProductPropertyItemResult + { + return new ProductPropertyItemResult( + $this->getCoreResponse()->getResponseData()->getResult()['productProperty'] + ); + } +} diff --git a/src/Services/Catalog/ProductProperty/Result/UpdatedProductPropertyBatchResult.php b/src/Services/Catalog/ProductProperty/Result/UpdatedProductPropertyBatchResult.php new file mode 100644 index 00000000..3d85ae56 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Result/UpdatedProductPropertyBatchResult.php @@ -0,0 +1,30 @@ + + * + * 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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Result\UpdatedItemBatchResult; + +/** + * Class UpdatedProductPropertyBatchResult + * + * @package Bitrix24\SDK\Services\Catalog\ProductProperty\Result + */ +class UpdatedProductPropertyBatchResult extends UpdatedItemBatchResult +{ + #[\Override] + public function isSuccess(): bool + { + return (bool)$this->getResponseData()->getResult(); + } +} diff --git a/src/Services/Catalog/ProductProperty/Service/Batch.php b/src/Services/Catalog/ProductProperty/Service/Batch.php new file mode 100644 index 00000000..d18237a5 --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Service/Batch.php @@ -0,0 +1,134 @@ + + * + * 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\ProductProperty\Service; + +use Bitrix24\SDK\Attributes\ApiBatchMethodMetadata; +use Bitrix24\SDK\Attributes\ApiBatchServiceMetadata; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; +use Bitrix24\SDK\Core\Credentials\Scope; +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\AddedProductPropertyBatchResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\DeletedProductPropertyBatchResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyItemResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\UpdatedProductPropertyBatchResult; +use Generator; +use Psr\Log\LoggerInterface; + +#[ApiBatchServiceMetadata(new Scope(['catalog']))] +readonly class Batch +{ + /** + * Batch constructor + */ + public function __construct(protected BatchOperationsInterface $batch, protected LoggerInterface $log) + { + } + + /** + * Batch list method for product properties + * + * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-list.html + * + * @return Generator + * @throws BaseException + */ + #[ApiBatchMethodMetadata( + 'catalog.productProperty.list', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-list.html', + 'Batch list method for product properties' + )] + public function list(array $select = [], array $filter = [], array $order = [], ?int $limit = null): Generator + { + $itemsGenerator = $this->batch->getTraversableListWithCount( + 'catalog.productProperty.list', + $order, + $filter, + $select, + $limit + ); + foreach ($itemsGenerator as $key => $value) { + yield $key => new ProductPropertyItemResult($value); + } + } + + /** + * Batch adding product properties + * + * @param array $productProperties + * + * @return Generator + * @throws BaseException + */ + #[ApiBatchMethodMetadata( + 'catalog.productProperty.add', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-add.html', + 'Batch adding product properties' + )] + public function add(array $productProperties): Generator + { + $items = []; + foreach ($productProperties as $item) { + $items[] = ['fields' => $item]; + } + + foreach ($this->batch->addEntityItems('catalog.productProperty.add', $items) as $key => $item) { + yield $key => new AddedProductPropertyBatchResult($item); + } + } + + /** + * Batch update product properties + * + * Update elements in array with structure: + * id => [ // Property id + * 'fields' => [] // Property fields to update, must include iblockId + * ] + * + * @param array $entityItems + * + * @return Generator + * @throws BaseException + */ + #[ApiBatchMethodMetadata( + 'catalog.productProperty.update', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-update.html', + 'Batch update product properties' + )] + public function update(array $entityItems): Generator + { + foreach ($this->batch->updateEntityItems('catalog.productProperty.update', $entityItems) as $key => $item) { + yield $key => new UpdatedProductPropertyBatchResult($item); + } + } + + /** + * Batch delete product properties + * + * @param int[] $productPropertyId + * + * @return Generator + * @throws BaseException + */ + #[ApiBatchMethodMetadata( + 'catalog.productProperty.delete', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-delete.html', + 'Batch delete product properties' + )] + public function delete(array $productPropertyId): Generator + { + foreach ($this->batch->deleteEntityItems('catalog.productProperty.delete', $productPropertyId) as $key => $item) { + yield $key => new DeletedProductPropertyBatchResult($item); + } + } +} diff --git a/src/Services/Catalog/ProductProperty/Service/ProductProperty.php b/src/Services/Catalog/ProductProperty/Service/ProductProperty.php new file mode 100644 index 00000000..e594de5e --- /dev/null +++ b/src/Services/Catalog/ProductProperty/Service/ProductProperty.php @@ -0,0 +1,166 @@ + + * + * 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\ProductProperty\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\Services\AbstractService; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\DeletedProductPropertyResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertiesResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyFieldsResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyResult; +use Psr\Log\LoggerInterface; + +#[ApiServiceMetadata(new Scope(['catalog']))] +class ProductProperty extends AbstractService +{ + /** + * ProductProperty constructor + */ + public function __construct(public Batch $batch, CoreInterface $core, LoggerInterface $logger) + { + parent::__construct($core, $logger); + } + + /** + * Adds a product or variation property to the commercial catalog + * + * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-add.html + * + * @throws BaseException + * @throws TransportException + */ + #[ApiEndpointMetadata( + 'catalog.productProperty.add', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-add.html', + 'Adds a product or variation property to the commercial catalog' + )] + public function add(array $fields): ProductPropertyResult + { + return new ProductPropertyResult( + $this->core->call('catalog.productProperty.add', ['fields' => $fields]) + ); + } + + /** + * Updates fields of a product or variation property in the commercial catalog + * + * NOTE: despite the official docs marking `iblockId` as optional in `fields`, the live API + * requires it — omitting it fails with "Required fields: iblockId". Callers must always pass + * `iblockId` in $fields. + * + * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-update.html + * + * @throws BaseException + * @throws TransportException + */ + #[ApiEndpointMetadata( + 'catalog.productProperty.update', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-update.html', + 'Updates fields of a product or variation property in the commercial catalog' + )] + public function update(int $id, array $fields): ProductPropertyResult + { + return new ProductPropertyResult( + $this->core->call('catalog.productProperty.update', ['id' => $id, 'fields' => $fields]) + ); + } + + /** + * Returns the values of the product or variation property fields by its identifier + * + * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-get.html + * + * @throws BaseException + * @throws TransportException + */ + #[ApiEndpointMetadata( + 'catalog.productProperty.get', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-get.html', + 'Returns the values of the product or variation property fields by its identifier' + )] + public function get(int $id): ProductPropertyResult + { + return new ProductPropertyResult($this->core->call('catalog.productProperty.get', ['id' => $id])); + } + + /** + * Returns a list of product and variation properties by filter + * + * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-list.html + * + * @throws BaseException + * @throws TransportException + */ + #[ApiEndpointMetadata( + 'catalog.productProperty.list', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-list.html', + 'Returns a list of product and variation properties by filter' + )] + public function list(array $select = [], array $filter = [], array $order = []): ProductPropertiesResult + { + return new ProductPropertiesResult( + $this->core->call( + 'catalog.productProperty.list', + [ + 'select' => $select, + 'filter' => $filter, + 'order' => $order, + ] + ) + ); + } + + /** + * Removes a product or variation property by its identifier + * + * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-delete.html + * + * @throws BaseException + * @throws TransportException + */ + #[ApiEndpointMetadata( + 'catalog.productProperty.delete', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-delete.html', + 'Removes a product or variation property by its identifier' + )] + public function delete(int $id): DeletedProductPropertyResult + { + return new DeletedProductPropertyResult( + $this->core->call('catalog.productProperty.delete', ['id' => $id]) + ); + } + + /** + * Returns the description of product or variation property fields + * + * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-get-fields.html + * + * @throws BaseException + * @throws TransportException + */ + #[ApiEndpointMetadata( + 'catalog.productProperty.getFields', + 'https://apidocs.bitrix24.com/api-reference/catalog/product-property/catalog-product-property-get-fields.html', + 'Returns the description of product or variation property fields' + )] + public function getFields(): ProductPropertyFieldsResult + { + return new ProductPropertyFieldsResult($this->core->call('catalog.productProperty.getFields')); + } +} diff --git a/tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php b/tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php new file mode 100644 index 00000000..bc43b55d --- /dev/null +++ b/tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php @@ -0,0 +1,109 @@ + + * + * 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\ProductProperty\Result; + +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; +use Bitrix24\SDK\Core\Exceptions\TransportException; +use Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyItemResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Service\ProductProperty; +use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions; +use Bitrix24\SDK\Tests\Integration\Fabric; +use Faker\Factory as FakerFactory; +use Faker\Generator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\TestCase; + +#[CoversClass(ProductPropertyItemResult::class)] +class ProductPropertyItemResultAnnotationsTest extends TestCase +{ + use CustomBitrix24Assertions; + + private ProductProperty $productPropertyService; + + private Catalog $catalogService; + + private Generator $faker; + + /** + * @throws InvalidArgumentException + */ + #[\Override] + protected function setUp(): void + { + $this->productPropertyService = Fabric::getServiceBuilder()->getCatalogScope()->productProperty(); + $this->catalogService = Fabric::getServiceBuilder()->getCatalogScope()->catalog(); + $this->faker = FakerFactory::create(); + } + + /** + * Helper: create a product property, fetch it via get() to obtain the full field set, then delete it. + * + * @return array + * + * @throws BaseException + * @throws TransportException + */ + private function getFirstProductPropertyRawItem(): array + { + $iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + + $id = $this->productPropertyService->add([ + 'iblockId' => $iblockId, + 'name' => 'SDK_ANNOT_TEST_' . $this->faker->uuid(), + 'propertyType' => 'S', + ])->productProperty()->id; + + $rawItem = $this->productPropertyService->get($id) + ->getCoreResponse()->getResponseData()->getResult()['productProperty'] ?? []; + + try { + $this->productPropertyService->delete($id); + } catch (BaseException) { + // Server-side error during cleanup; must not affect annotations test + } + + self::assertNotEmpty($rawItem, 'get() must return a product property item to run this test'); + + return $rawItem; + } + + #[Test] + #[TestDox('all fields in ProductPropertyItemResult are annotated in phpdoc and match with raw api response')] + public function testAllSystemFieldsAnnotated(): void + { + $rawItem = $this->getFirstProductPropertyRawItem(); + + $this->assertBitrix24AllResultItemFieldsAnnotated( + array_keys($rawItem), + ProductPropertyItemResult::class + ); + } + + #[Test] + #[TestDox('all fields in ProductPropertyItemResult have valid type casting in magic getters')] + public function testAllSystemFieldsHasValidTypeAnnotation(): void + { + $rawItem = $this->getFirstProductPropertyRawItem(); + $productPropertyItemResult = new ProductPropertyItemResult($rawItem); + + $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations( + $productPropertyItemResult, + ProductPropertyItemResult::class + ); + } +} diff --git a/tests/Integration/Services/Catalog/ProductProperty/Service/BatchTest.php b/tests/Integration/Services/Catalog/ProductProperty/Service/BatchTest.php new file mode 100644 index 00000000..5366396b --- /dev/null +++ b/tests/Integration/Services/Catalog/ProductProperty/Service/BatchTest.php @@ -0,0 +1,193 @@ + + * + * 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\ProductProperty\Service; + +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; +use Bitrix24\SDK\Core\Exceptions\TransportException; +use Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Service\ProductProperty; +use Bitrix24\SDK\Tests\Integration\Fabric; +use Faker\Factory as FakerFactory; +use Faker\Generator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\TestCase; + +/** + * Class BatchTest + * + * @package Bitrix24\SDK\Tests\Integration\Services\Catalog\ProductProperty\Service + */ +#[CoversClass(\Bitrix24\SDK\Services\Catalog\ProductProperty\Service\Batch::class)] +class BatchTest extends TestCase +{ + protected ProductProperty $productPropertyService; + + protected Catalog $catalogService; + + private Generator $faker; + + private int $iblockId; + + /** + * @throws InvalidArgumentException + * @throws BaseException + * @throws TransportException + */ + #[\Override] + protected function setUp(): void + { + $this->productPropertyService = Fabric::getServiceBuilder()->getCatalogScope()->productProperty(); + $this->catalogService = Fabric::getServiceBuilder()->getCatalogScope()->catalog(); + $this->faker = FakerFactory::create(); + $this->iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + } + + /** + * Helper: silently delete a product property by id. + */ + private function safeDelete(int $id): void + { + try { + $this->productPropertyService->delete($id); + } catch (BaseException) { + // Server-side error; ignored during cleanup + } + } + + /** + * Helper: silently batch-delete product properties by ids. + * + * @param int[] $ids + */ + private function safeBatchDelete(array $ids): void + { + try { + foreach ($this->productPropertyService->batch->delete($ids) as $deleted) { + unset($deleted); + } + } catch (BaseException) { + // Server-side error; ignored during cleanup + } + } + + /** + * @throws BaseException + * @throws TransportException + */ + #[TestDox('Batch list product properties')] + public function testBatchList(): void + { + $id = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_BATCH_LIST_' . $this->faker->uuid(), + 'propertyType' => 'S', + ])->productProperty()->id; + + $cnt = 0; + foreach ($this->productPropertyService->batch->list([], ['iblockId' => $this->iblockId]) as $item) { + $cnt++; + } + + self::assertGreaterThanOrEqual(1, $cnt); + + $this->safeDelete($id); + } + + /** + * @throws BaseException + * @throws TransportException + */ + #[TestDox('Batch add product properties')] + public function testBatchAdd(): void + { + $items = []; + for ($i = 1; $i <= 3; $i++) { + $items[] = [ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_BATCH_ADD_' . $this->faker->uuid(), + 'propertyType' => 'S', + ]; + } + + $ids = []; + $cnt = 0; + foreach ($this->productPropertyService->batch->add($items) as $added) { + $cnt++; + $ids[] = $added->getId(); + } + + self::assertEquals(count($items), $cnt); + + $this->safeBatchDelete($ids); + } + + /** + * @throws BaseException + * @throws TransportException + */ + #[TestDox('Batch update product properties')] + public function testBatchUpdate(): void + { + $ids = []; + for ($i = 1; $i <= 3; $i++) { + $ids[] = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_BATCH_UPD_' . $this->faker->uuid(), + 'propertyType' => 'S', + ])->productProperty()->id; + } + + $updatePayload = []; + foreach ($ids as $id) { + $updatePayload[$id] = [ + 'fields' => [ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_BATCH_UPD_UPDATED_' . $this->faker->uuid(), + ], + ]; + } + + foreach ($this->productPropertyService->batch->update($updatePayload) as $updated) { + $this->assertTrue($updated->isSuccess()); + } + + $this->safeBatchDelete($ids); + } + + /** + * @throws BaseException + * @throws TransportException + */ + #[TestDox('Batch delete product properties')] + public function testBatchDelete(): void + { + $ids = []; + for ($i = 1; $i <= 3; $i++) { + $ids[] = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_BATCH_DEL_' . $this->faker->uuid(), + 'propertyType' => 'S', + ])->productProperty()->id; + } + + $delCnt = 0; + foreach ($this->productPropertyService->batch->delete($ids) as $deleted) { + $delCnt++; + } + + self::assertEquals(count($ids), $delCnt); + } +} diff --git a/tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php b/tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php new file mode 100644 index 00000000..f1bdf4ac --- /dev/null +++ b/tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php @@ -0,0 +1,188 @@ + + * + * 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\ProductProperty\Service; + +use Bitrix24\SDK\Core\Exceptions\BaseException; +use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; +use Bitrix24\SDK\Core\Exceptions\TransportException; +use Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyItemResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Service\ProductProperty; +use Bitrix24\SDK\Tests\Integration\Fabric; +use Faker\Factory as FakerFactory; +use Faker\Generator; +use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\TestCase; + +/** + * Class ProductPropertyTest + * + * @package Bitrix24\SDK\Tests\Integration\Services\Catalog\ProductProperty\Service + */ +#[CoversMethod(ProductProperty::class, 'add')] +#[CoversMethod(ProductProperty::class, 'delete')] +#[CoversMethod(ProductProperty::class, 'get')] +#[CoversMethod(ProductProperty::class, 'list')] +#[CoversMethod(ProductProperty::class, 'update')] +#[CoversMethod(ProductProperty::class, 'getFields')] +#[\PHPUnit\Framework\Attributes\CoversClass(ProductProperty::class)] +class ProductPropertyTest extends TestCase +{ + private ProductProperty $productPropertyService; + + private Catalog $catalogService; + + private Generator $faker; + + private int $iblockId; + + /** + * @throws InvalidArgumentException + * @throws BaseException + * @throws TransportException + */ + #[\Override] + protected function setUp(): void + { + $this->productPropertyService = Fabric::getServiceBuilder()->getCatalogScope()->productProperty(); + $this->catalogService = Fabric::getServiceBuilder()->getCatalogScope()->catalog(); + $this->faker = FakerFactory::create(); + $this->iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + } + + /** + * Helper: silently delete a product property. + */ + private function safeDelete(int $id): void + { + try { + $this->productPropertyService->delete($id); + } catch (BaseException) { + // Server-side error; ignored during cleanup + } + } + + /** + * @throws BaseException + * @throws TransportException + */ + public function testAdd(): void + { + $id = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_TEST_' . $this->faker->uuid(), + 'propertyType' => 'S', + 'active' => 'Y', + ])->productProperty()->id; + self::assertGreaterThanOrEqual(1, $id); + + $this->safeDelete($id); + } + + /** + * @throws BaseException + * @throws TransportException + */ + public function testGet(): void + { + $id = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_TEST_' . $this->faker->uuid(), + 'propertyType' => 'S', + 'active' => 'Y', + ])->productProperty()->id; + + $productPropertyItemResult = $this->productPropertyService->get($id)->productProperty(); + self::assertInstanceOf(ProductPropertyItemResult::class, $productPropertyItemResult); + self::assertEquals($id, $productPropertyItemResult->id); + + $this->safeDelete($id); + } + + /** + * @throws BaseException + * @throws TransportException + */ + public function testList(): void + { + $id = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_TEST_' . $this->faker->uuid(), + 'propertyType' => 'S', + 'active' => 'Y', + ])->productProperty()->id; + + $list = $this->productPropertyService->list([], ['id' => $id])->getProductProperties(); + self::assertCount(1, $list); + self::assertEquals($id, $list[0]->id); + + $this->safeDelete($id); + } + + /** + * @throws BaseException + * @throws TransportException + */ + public function testUpdate(): void + { + $id = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_TEST_' . $this->faker->uuid(), + 'propertyType' => 'S', + 'active' => 'Y', + ])->productProperty()->id; + + $updatedName = 'SDK_TEST_UPDATED_' . $this->faker->uuid(); + $updated = $this->productPropertyService->update($id, [ + 'iblockId' => $this->iblockId, + 'name' => $updatedName, + ])->productProperty(); + self::assertEquals($updatedName, $updated->name); + + $this->safeDelete($id); + } + + /** + * @throws BaseException + * @throws TransportException + */ + public function testDelete(): void + { + $id = $this->productPropertyService->add([ + 'iblockId' => $this->iblockId, + 'name' => 'SDK_TEST_' . $this->faker->uuid(), + 'propertyType' => 'S', + 'active' => 'Y', + ])->productProperty()->id; + + $deletedProductPropertyResult = $this->productPropertyService->delete($id); + self::assertTrue($deletedProductPropertyResult->isSuccess()); + + $list = $this->productPropertyService->list([], ['id' => $id])->getProductProperties(); + self::assertCount(0, $list); + } + + /** + * @throws BaseException + * @throws TransportException + */ + public function testGetFields(): void + { + $fieldsDescription = $this->productPropertyService->getFields()->getFieldsDescription(); + self::assertIsArray($fieldsDescription); + self::assertArrayHasKey('id', $fieldsDescription); + self::assertArrayHasKey('iblockId', $fieldsDescription); + self::assertArrayHasKey('propertyType', $fieldsDescription); + } +} diff --git a/tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php b/tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php new file mode 100644 index 00000000..5f26eb4a --- /dev/null +++ b/tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php @@ -0,0 +1,179 @@ + + * + * 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\Unit\Services\Catalog\ProductProperty\Service; + +use Bitrix24\SDK\Core\ApiLevelErrorHandler; +use Bitrix24\SDK\Core\Commands\Command; +use Bitrix24\SDK\Core\Contracts\CoreInterface; +use Bitrix24\SDK\Core\Response\Response; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\DeletedProductPropertyResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertiesResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyFieldsResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyResult; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Service\Batch; +use Bitrix24\SDK\Services\Catalog\ProductProperty\Service\ProductProperty; +use Bitrix24\SDK\Tests\Unit\Stubs\NullBatch; +use Bitrix24\SDK\Tests\Unit\Stubs\NullCore; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Component\HttpClient\Response\MockResponse; + +#[CoversClass(ProductProperty::class)] +class ProductPropertyTest extends TestCase +{ + private ProductProperty $service; + + #[\Override] + protected function setUp(): void + { + $this->service = new ProductProperty( + new Batch(new NullBatch(), new NullLogger()), + new NullCore(), + new NullLogger() + ); + } + + #[Test] + public function testAddReturnsProductPropertyResult(): void + { + $this->assertInstanceOf( + ProductPropertyResult::class, + $this->service->add(['iblockId' => 19, 'name' => 'Size', 'propertyType' => 'S']) + ); + } + + #[Test] + public function testUpdateReturnsProductPropertyResult(): void + { + $this->assertInstanceOf( + ProductPropertyResult::class, + $this->service->update(115, ['iblockId' => 19, 'name' => 'Size']) + ); + } + + #[Test] + public function testGetReturnsProductPropertyResult(): void + { + $this->assertInstanceOf(ProductPropertyResult::class, $this->service->get(115)); + } + + #[Test] + public function testListReturnsProductPropertiesResult(): void + { + $this->assertInstanceOf(ProductPropertiesResult::class, $this->service->list()); + } + + #[Test] + public function testDeleteReturnsDeletedProductPropertyResult(): void + { + $this->assertInstanceOf(DeletedProductPropertyResult::class, $this->service->delete(115)); + } + + #[Test] + public function testGetFieldsReturnsProductPropertyFieldsResult(): void + { + $this->assertInstanceOf(ProductPropertyFieldsResult::class, $this->service->getFields()); + } + + #[Test] + #[TestDox('add() sends fields nested under the fields key')] + public function testAddSendsNestedFields(): void + { + [$method, $captured] = $this->call( + static fn (ProductProperty $service) => $service->add(['iblockId' => 19, 'name' => 'Size']) + ); + + $this->assertSame('catalog.productProperty.add', $method); + $this->assertSame(['iblockId' => 19, 'name' => 'Size'], $captured['fields']); + } + + #[Test] + #[TestDox('update() sends id and nested fields')] + public function testUpdateSendsIdAndFields(): void + { + [$method, $captured] = $this->call( + static fn (ProductProperty $service) => $service->update(115, ['iblockId' => 19, 'name' => 'Size']) + ); + + $this->assertSame('catalog.productProperty.update', $method); + $this->assertSame(115, $captured['id']); + $this->assertSame(['iblockId' => 19, 'name' => 'Size'], $captured['fields']); + } + + #[Test] + #[TestDox('get() sends the property id')] + public function testGetSendsId(): void + { + [$method, $captured] = $this->call(static fn (ProductProperty $service) => $service->get(115)); + + $this->assertSame('catalog.productProperty.get', $method); + $this->assertSame(115, $captured['id']); + } + + #[Test] + #[TestDox('list() sends select, filter and order')] + public function testListSendsSelectFilterOrder(): void + { + [$method, $captured] = $this->call( + static fn (ProductProperty $service) => $service->list(['id', 'name'], ['iblockId' => 19], ['id' => 'ASC']) + ); + + $this->assertSame('catalog.productProperty.list', $method); + $this->assertSame(['id', 'name'], $captured['select']); + $this->assertSame(['iblockId' => 19], $captured['filter']); + $this->assertSame(['id' => 'ASC'], $captured['order']); + } + + #[Test] + #[TestDox('delete() sends the property id')] + public function testDeleteSendsId(): void + { + [$method, $captured] = $this->call(static fn (ProductProperty $service) => $service->delete(115)); + + $this->assertSame('catalog.productProperty.delete', $method); + $this->assertSame(115, $captured['id']); + } + + /** + * @return array{0: string, 1: array} + */ + private function call(callable $action): array + { + $method = null; + $captured = []; + $response = new Response( + new MockResponse(''), + new Command('', []), + new ApiLevelErrorHandler(new NullLogger()), + new NullLogger() + ); + + $core = $this->createStub(CoreInterface::class); + $core->method('call')->willReturnCallback( + function (string $apiMethod, array $parameters = []) use (&$method, &$captured, $response): Response { + $method = $apiMethod; + $captured = $parameters; + + return $response; + } + ); + + $action(new ProductProperty(new Batch(new NullBatch(), new NullLogger()), $core, new NullLogger())); + + return [$method, $captured]; + } +} From 3fd0f8511875e9542edbfa7dd69e439907726115 Mon Sep 17 00:00:00 2001 From: Dmitriy Ignatenko Date: Wed, 22 Jul 2026 18:38:34 +0400 Subject: [PATCH 2/2] Fix on liter and tests results --- .../Catalog/Product/Service/Product.php | 2 +- .../Result/ProductPropertyItemResult.php | 47 +++++++++++++- .../CustomBitrix24Assertions.php | 16 +++++ ...oductPropertyItemResultAnnotationsTest.php | 62 +++---------------- .../Service/ProductPropertyTest.php | 4 +- .../Service/ProductPropertyTest.php | 10 +-- 6 files changed, 79 insertions(+), 62 deletions(-) diff --git a/src/Services/Catalog/Product/Service/Product.php b/src/Services/Catalog/Product/Service/Product.php index b0056db5..d37d34f6 100644 --- a/src/Services/Catalog/Product/Service/Product.php +++ b/src/Services/Catalog/Product/Service/Product.php @@ -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/ProductProperty/Result/ProductPropertyItemResult.php b/src/Services/Catalog/ProductProperty/Result/ProductPropertyItemResult.php index 29614252..db700f32 100644 --- a/src/Services/Catalog/ProductProperty/Result/ProductPropertyItemResult.php +++ b/src/Services/Catalog/ProductProperty/Result/ProductPropertyItemResult.php @@ -13,7 +13,7 @@ namespace Bitrix24\SDK\Services\Catalog\ProductProperty\Result; -use Bitrix24\SDK\Core\Result\AbstractAnnotatedItem; +use Bitrix24\SDK\Core\Result\AbstractItem; use Carbon\CarbonImmutable; /** @@ -44,6 +44,49 @@ * @property-read bool|null $withDescription * @property-read string|null $xmlId */ -class ProductPropertyItemResult extends AbstractAnnotatedItem +class ProductPropertyItemResult extends AbstractItem { + /** + * @param int|string $offset + * + * @return bool|CarbonImmutable|int|mixed|null + */ + #[\Override] + public function __get($offset) + { + switch ($offset) { + case 'active': + case 'filtrable': + case 'isRequired': + case 'multiple': + case 'searchable': + return $this->data[$offset] === 'Y'; + case 'withDescription': + if ($this->data[$offset] !== null) { + return $this->data[$offset] === 'Y'; + } + + return null; + case 'colCount': + case 'iblockId': + case 'id': + case 'linkIblockId': + case 'multipleCnt': + case 'rowCount': + case 'sort': + if ($this->data[$offset] !== '' && $this->data[$offset] !== null) { + return (int)$this->data[$offset]; + } + + return null; + case 'timestampX': + if ($this->data[$offset] !== null && $this->data[$offset] !== '') { + return CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]); + } + + return null; + } + + return $this->data[$offset] ?? null; + } } diff --git a/tests/CustomAssertions/CustomBitrix24Assertions.php b/tests/CustomAssertions/CustomBitrix24Assertions.php index dd1d32b7..5bba93c0 100644 --- a/tests/CustomAssertions/CustomBitrix24Assertions.php +++ b/tests/CustomAssertions/CustomBitrix24Assertions.php @@ -90,6 +90,7 @@ protected function assertBitrix24AllResultItemFieldsHasValidTypeAnnotation( // mapping internal bitrix24 types to bitrix24 sdk types switch ($fieldData['type']) { case 'string': + case 'text': case 'crm_currency': case 'crm_status': if (str_contains($fieldCode, 'ACTIVE')) { @@ -252,6 +253,20 @@ protected function assertBitrix24AllResultItemFieldsHasValidTypeAnnotation( ); break; case 'char': + if ($fieldCode === 'listType') { + $this->assertTrue( + str_contains($propsFromAnnotations[$fieldCode], 'string'), + sprintf( + 'class «%s» field «%s» has invalid type phpdoc annotation «%s», field type from bitrix24 is «%s», expected sdk-type «%s»', + $resultItemClassName, + $fieldCode, + $propsFromAnnotations[$fieldCode], + $fieldData['type'], + 'string' + ) + ); + break; + } $this->assertTrue( str_contains($propsFromAnnotations[$fieldCode], 'bool'), sprintf( @@ -472,6 +487,7 @@ protected function assertBitrix24AllResultItemFieldsHasValidTypeAnnotation( case 'attached_diskfile': case 'disk_file': case 'datatype': + case 'productpropertysettings': $this->assertTrue( str_contains($propsFromAnnotations[$fieldCode], 'array'), sprintf( diff --git a/tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php b/tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php index bc43b55d..61b5a9ba 100644 --- a/tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php +++ b/tests/Integration/Services/Catalog/ProductProperty/Result/ProductPropertyItemResultAnnotationsTest.php @@ -13,16 +13,12 @@ namespace Bitrix24\SDK\Tests\Integration\Services\Catalog\ProductProperty\Result; -use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; -use Bitrix24\SDK\Core\Exceptions\TransportException; -use Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog; +use Bitrix24\SDK\Core\Fields\FieldsFilter; use Bitrix24\SDK\Services\Catalog\ProductProperty\Result\ProductPropertyItemResult; use Bitrix24\SDK\Services\Catalog\ProductProperty\Service\ProductProperty; use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions; use Bitrix24\SDK\Tests\Integration\Fabric; -use Faker\Factory as FakerFactory; -use Faker\Generator; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestDox; @@ -35,10 +31,6 @@ class ProductPropertyItemResultAnnotationsTest extends TestCase private ProductProperty $productPropertyService; - private Catalog $catalogService; - - private Generator $faker; - /** * @throws InvalidArgumentException */ @@ -46,63 +38,29 @@ class ProductPropertyItemResultAnnotationsTest extends TestCase protected function setUp(): void { $this->productPropertyService = Fabric::getServiceBuilder()->getCatalogScope()->productProperty(); - $this->catalogService = Fabric::getServiceBuilder()->getCatalogScope()->catalog(); - $this->faker = FakerFactory::create(); - } - - /** - * Helper: create a product property, fetch it via get() to obtain the full field set, then delete it. - * - * @return array - * - * @throws BaseException - * @throws TransportException - */ - private function getFirstProductPropertyRawItem(): array - { - $iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; - - $id = $this->productPropertyService->add([ - 'iblockId' => $iblockId, - 'name' => 'SDK_ANNOT_TEST_' . $this->faker->uuid(), - 'propertyType' => 'S', - ])->productProperty()->id; - - $rawItem = $this->productPropertyService->get($id) - ->getCoreResponse()->getResponseData()->getResult()['productProperty'] ?? []; - - try { - $this->productPropertyService->delete($id); - } catch (BaseException) { - // Server-side error during cleanup; must not affect annotations test - } - - self::assertNotEmpty($rawItem, 'get() must return a product property item to run this test'); - - return $rawItem; } #[Test] #[TestDox('all fields in ProductPropertyItemResult are annotated in phpdoc and match with raw api response')] public function testAllSystemFieldsAnnotated(): void { - $rawItem = $this->getFirstProductPropertyRawItem(); - - $this->assertBitrix24AllResultItemFieldsAnnotated( - array_keys($rawItem), - ProductPropertyItemResult::class + $propListFromApi = (new FieldsFilter())->filterSystemFields( + array_keys($this->productPropertyService->getFields()->getFieldsDescription()) ); + + $this->assertBitrix24AllResultItemFieldsAnnotated($propListFromApi, ProductPropertyItemResult::class); } #[Test] #[TestDox('all fields in ProductPropertyItemResult have valid type casting in magic getters')] public function testAllSystemFieldsHasValidTypeAnnotation(): void { - $rawItem = $this->getFirstProductPropertyRawItem(); - $productPropertyItemResult = new ProductPropertyItemResult($rawItem); + $allFields = $this->productPropertyService->getFields()->getFieldsDescription(); + $systemFieldsCodes = (new FieldsFilter())->filterSystemFields(array_keys($allFields)); + $systemFields = array_filter($allFields, static fn($code): bool => in_array($code, $systemFieldsCodes, true), ARRAY_FILTER_USE_KEY); - $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations( - $productPropertyItemResult, + $this->assertBitrix24AllResultItemFieldsHasValidTypeAnnotation( + $systemFields, ProductPropertyItemResult::class ); } diff --git a/tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php b/tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php index f1bdf4ac..35b43944 100644 --- a/tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php +++ b/tests/Integration/Services/Catalog/ProductProperty/Service/ProductPropertyTest.php @@ -144,11 +144,11 @@ public function testUpdate(): void ])->productProperty()->id; $updatedName = 'SDK_TEST_UPDATED_' . $this->faker->uuid(); - $updated = $this->productPropertyService->update($id, [ + $productPropertyItemResult = $this->productPropertyService->update($id, [ 'iblockId' => $this->iblockId, 'name' => $updatedName, ])->productProperty(); - self::assertEquals($updatedName, $updated->name); + self::assertEquals($updatedName, $productPropertyItemResult->name); $this->safeDelete($id); } diff --git a/tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php b/tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php index 5f26eb4a..4c27a96b 100644 --- a/tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php +++ b/tests/Unit/Catalog/ProductProperty/Service/ProductPropertyTest.php @@ -94,7 +94,7 @@ public function testGetFieldsReturnsProductPropertyFieldsResult(): void public function testAddSendsNestedFields(): void { [$method, $captured] = $this->call( - static fn (ProductProperty $service) => $service->add(['iblockId' => 19, 'name' => 'Size']) + static fn (ProductProperty $productProperty): ProductPropertyResult => $productProperty->add(['iblockId' => 19, 'name' => 'Size']) ); $this->assertSame('catalog.productProperty.add', $method); @@ -106,7 +106,7 @@ public function testAddSendsNestedFields(): void public function testUpdateSendsIdAndFields(): void { [$method, $captured] = $this->call( - static fn (ProductProperty $service) => $service->update(115, ['iblockId' => 19, 'name' => 'Size']) + static fn (ProductProperty $productProperty): ProductPropertyResult => $productProperty->update(115, ['iblockId' => 19, 'name' => 'Size']) ); $this->assertSame('catalog.productProperty.update', $method); @@ -118,7 +118,7 @@ public function testUpdateSendsIdAndFields(): void #[TestDox('get() sends the property id')] public function testGetSendsId(): void { - [$method, $captured] = $this->call(static fn (ProductProperty $service) => $service->get(115)); + [$method, $captured] = $this->call(static fn (ProductProperty $productProperty): ProductPropertyResult => $productProperty->get(115)); $this->assertSame('catalog.productProperty.get', $method); $this->assertSame(115, $captured['id']); @@ -129,7 +129,7 @@ public function testGetSendsId(): void public function testListSendsSelectFilterOrder(): void { [$method, $captured] = $this->call( - static fn (ProductProperty $service) => $service->list(['id', 'name'], ['iblockId' => 19], ['id' => 'ASC']) + static fn (ProductProperty $productProperty): ProductPropertiesResult => $productProperty->list(['id', 'name'], ['iblockId' => 19], ['id' => 'ASC']) ); $this->assertSame('catalog.productProperty.list', $method); @@ -142,7 +142,7 @@ public function testListSendsSelectFilterOrder(): void #[TestDox('delete() sends the property id')] public function testDeleteSendsId(): void { - [$method, $captured] = $this->call(static fn (ProductProperty $service) => $service->delete(115)); + [$method, $captured] = $this->call(static fn (ProductProperty $productProperty): DeletedProductPropertyResult => $productProperty->delete(115)); $this->assertSame('catalog.productProperty.delete', $method); $this->assertSame(115, $captured['id']);