diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0137cbe9..2a6b4634 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -9,19 +9,19 @@ on: env: COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" BITRIX24_PHP_SDK_PLAYGROUND_WEBHOOK: ${{ secrets.BITRIX24_PHP_SDK_PLAYGROUND_WEBHOOK }} - TEST2_ENV: 12345 jobs: tests: name: "Integration tests" - - runs-on: ubuntu-latest - + runs-on: ${{ matrix.operating-system }} strategy: + fail-fast: false matrix: php-version: - - "7.4" + - "8.2" + - "8.3" dependencies: [ highest ] + operating-system: [ ubuntu-latest, windows-latest ] steps: @@ -36,7 +36,6 @@ jobs: ini-values: variables_order=EGPCS env: BITRIX24_PHP_SDK_PLAYGROUND_WEBHOOK: ${{ secrets.BITRIX24_PHP_SDK_PLAYGROUND_WEBHOOK }} - TEST2_ENV: 12345 - name: "Install dependencies" run: | diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index ea58e758..64ea73b4 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -7,17 +7,16 @@ name: PHPStan checks jobs: static-analysis: name: "PHPStan" - runs-on: "ubuntu-latest" + runs-on: ${{ matrix.operating-system }} strategy: fail-fast: false matrix: php-version: - - "7.4" - - "8.0" - dependencies: - - "lowest" - - "highest" + - "8.2" + - "8.3" + dependencies: [ highest ] + operating-system: [ ubuntu-latest, windows-latest ] steps: - name: "Checkout" @@ -28,7 +27,7 @@ jobs: with: coverage: "none" php-version: "${{ matrix.php-version }}" - extensions: mbstring + extensions: json, bcmath, curl, intl, mbstring tools: composer:v2 - name: "Install lowest dependencies" diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 725bb64a..00dc1d47 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -1,8 +1,8 @@ name: "PHPUnit tests" on: - - push - - pull_request + push: + pull_request: env: COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" @@ -11,13 +11,16 @@ jobs: tests: name: "PHPUnit tests" - runs-on: ubuntu-latest + runs-on: ${{ matrix.operating-system }} strategy: + fail-fast: false matrix: php-version: - - "7.4" + - "8.2" + - "8.3" dependencies: [ highest ] + operating-system: [ ubuntu-latest, windows-latest ] steps: - name: "Checkout" @@ -28,6 +31,7 @@ jobs: with: coverage: "none" php-version: "${{ matrix.php-version }}" + extensions: json, bcmath, curl, intl, mbstring - name: "Install dependencies" run: | diff --git a/.github/workflows/vendor-check.yml b/.github/workflows/vendor-check.yml index 2ec82413..74662014 100644 --- a/.github/workflows/vendor-check.yml +++ b/.github/workflows/vendor-check.yml @@ -8,19 +8,20 @@ on: env: COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" BITRIX24_PHP_SDK_PLAYGROUND_WEBHOOK: ${{ secrets.BITRIX24_PHP_SDK_PLAYGROUND_WEBHOOK }} - TEST2_ENV: 12345 jobs: tests: name: "Vendor integration tests" - runs-on: ubuntu-latest + runs-on: ${{ matrix.operating-system }} strategy: matrix: php-version: - - "7.4" + - "8.2" + - "8.3" dependencies: [ highest ] + operating-system: [ ubuntu-latest, windows-latest ] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e933af..e31e28c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,131 @@ # bitrix24-php-sdk change log +## 2.0-beta.1 — 25.03.2023 + +### Added + +* ❗️add php 8.3 support, drop 8.1 and 8.0 support +* add `Symfony\Component\Uid\Uuid` requirements +* add contracts for bitrix24 applications based on bitrix24-php-sdk - `Bitrix24\SDK\Application\Contracts`, now + added `Bitrix24Account` +* add [service builder factory](https://github.com/mesilov/bitrix24-php-sdk/issues/328) +* add method `Bitrix24\SDK\Core\Credentials\Scope::initFromString` +* add method `Bitrix24\SDK\Application\ApplicationStatus::initFromString` +* add system CRM multi-field type `Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\Phone` +* add scope `user`,`user_basic`,`user_brief`,`user.userfield` and + services [add scope user support](https://github.com/mesilov/bitrix24-php-sdk/issues/339) + * `Bitrix24\SDK\Services\User\Service\User::fields` - get user fields + * `Bitrix24\SDK\Services\User\Service\User::current` - get current user + * `Bitrix24\SDK\Services\User\Service\User::add` - add user + * `Bitrix24\SDK\Services\User\Service\User::get` - get user + * `Bitrix24\SDK\Services\User\Service\User::update` - update user + * `Bitrix24\SDK\Services\User\Service\User::search` - search users +* add method `\Bitrix24\SDK\Services\CRM\Contact\Service\Batch::update()` for batch update contacts +* add [crm item support](https://github.com/mesilov/bitrix24-php-sdk/issues/330) +* add enum `DealStageSemanticId` +* add Duplicate search support for `Bitrix24\SDK\Services\CRM\Duplicates\Service\Duplicate` +* add `x-request-id` [header support](https://github.com/mesilov/bitrix24-php-sdk/issues/354) +* add CRM multifields support [header support](https://github.com/mesilov/bitrix24-php-sdk/issues/338) + * `Email` + * `Phone` + * `Website` + * `IM` +* add [Catalog](https://github.com/mesilov/bitrix24-php-sdk/issues/364) scope services support + +### Changed + +* ❗️Batch interface `BatchInterface` [renamed](https://github.com/mesilov/bitrix24-php-sdk/issues/324) + to `Bitrix24\SDK\Core\Contracts\BatchOperationsInterface` +* ❗`Bitrix24\SDK\Services\Telephony\Requests\Events` moved to separated namespaces: + * from `Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallInit` + to `Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallInit\OnVoximplantCallInit` + * from `Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallStart` + to `Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallStart\OnVoximplantCallStart` + * from `Bitrix24\SDK\Services\Telephony\Requests\Events\OnExternalCallStart` + to `Bitrix24\SDK\Services\Telephony\Requests\Events\OnExternalCallStart\OnExternalCallStart` + * from `Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallEnd` + to `Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallEnd\OnVoximplantCallEnd` +* ❗Changes in `Bitrix24\SDK\Application\Contracts\Bitrix24Account\Bitrix24AccountInterface`: + * method `getContactPerson` renamed to `getContactPersonId` + * added method `getApplicationVersion` + * added method `updateApplicationVersion` + * added method `getApplicationScope` + * added method `applicationInstalled` + * added method `applicationUninstalled` + * added method `markAsDeactivated` + * added method `getBitrix24UserId` + * removed method `markAccountAsDeleted` + * changed method `markAsActive` +* ❗Changes in `Bitrix24\SDK\Application\Contracts\Bitrix24Account\Bitrix24AccountRepositoryInterface`: + * method `saveAccount` renamed to `save` + * method `deleteAccount` renamed to `delete` + * method `findAccountByMemberId` renamed to `findByMemberId` + * method `getAccountByMemberId` renamed to `getByMemberId` + * method `findAccountByContactPersonId` renamed to `findByContactPersonId` + * method `findAccountByDomainUrl` renamed to `findByDomainUrl` + * add method `findAllActive` + * add method `findAllDeactivated` + +### Bugfix + +* fix [typehint at ContactItemResult](https://github.com/mesilov/bitrix24-php-sdk/issues/320) +* fix [return types in DealCategoryItemResult](https://github.com/mesilov/bitrix24-php-sdk/issues/322) +* fix [add auth node in telephony voximplant events requests](https://github.com/mesilov/bitrix24-php-sdk/issues/331) +* fix [add helper metods isError for registerCallResult fortelephony](https://github.com/mesilov/bitrix24-php-sdk/issues/335) +* fix [add return type for crm multifields phone, email, im](https://github.com/mesilov/bitrix24-php-sdk/issues/338) +* fix errors in `ShowFieldsDescriptionCommand` metadata reader CLI command +* fix errors for `ApplicationProfile` with empty scope +* fix errors in `Core` with auth attempt to non-exists portal + +### etc + +* move CLI entry point to `bin/console` + ## 2.0-alpha.7 — 8.08.2022 ### Added * add new scope `Telephony` and services [add Telephony support](https://github.com/mesilov/bitrix24-php-sdk/issues/291) -* add new scope `UserConsent` and services [add UserConsent support](https://github.com/mesilov/bitrix24-php-sdk/issues/285) -* add new scope `Placements` and services [add Placements support](https://github.com/mesilov/bitrix24-php-sdk/issues/274) -* add new scope `IMOpenLines` and services [add IM Open Lines support](https://github.com/mesilov/bitrix24-php-sdk/issues/302) -* add in scope `CRM` new service `Leads` in scope «CRM» [add Leads support](https://github.com/mesilov/bitrix24-php-sdk/issues/282) -* add in scope `CRM` new service `Activity` in scope «CRM» [add Activity support](https://github.com/mesilov/bitrix24-php-sdk/issues/283) +* add new scope `UserConsent` and + services [add UserConsent support](https://github.com/mesilov/bitrix24-php-sdk/issues/285) +* add new scope `Placements` and + services [add Placements support](https://github.com/mesilov/bitrix24-php-sdk/issues/274) +* add new scope `IMOpenLines` and + services [add IM Open Lines support](https://github.com/mesilov/bitrix24-php-sdk/issues/302) +* add in scope `CRM` new service `Leads` in scope + «CRM» [add Leads support](https://github.com/mesilov/bitrix24-php-sdk/issues/282) +* add in scope `CRM` new service `Activity` in scope + «CRM» [add Activity support](https://github.com/mesilov/bitrix24-php-sdk/issues/283) * add in scope `CRM` for entity Deal method `Services\CRM\Deal\Service\Batch::update` batch update deals * add in scope `CRM` for entity Contact method `Services\CRM\Contact\Service\Batch::delete` batch delete contacts -* add in scope `CRM` [read models](https://github.com/mesilov/bitrix24-php-sdk/issues/300) for activity `Services\CRM\Activity\ReadModel` +* add in scope `CRM` [read models](https://github.com/mesilov/bitrix24-php-sdk/issues/300) for + activity `Services\CRM\Activity\ReadModel` for activity types: `EmailFetcher`, `OpenLineFetcher`, `VoximplantFetcher`, `WebFormFetcher` -* add in scope «Main» new service `Events` [add incoming events support](https://github.com/mesilov/bitrix24-php-sdk/issues/296) +* add in scope «Main» new + service `Events` [add incoming events support](https://github.com/mesilov/bitrix24-php-sdk/issues/296) * add support Application level events: `ONAPPINSTALL` and `ONAPPUNINSTALL` [add incoming events support](https://github.com/mesilov/bitrix24-php-sdk/issues/296) * add support Application level event: `PortalDomainUrlChangedEvent` -* add method `Core\Batch::updateEntityItems` for [update items in batch mode](https://github.com/mesilov/bitrix24-php-sdk/issues/268) and +* add method `Core\Batch::updateEntityItems` + for [update items in batch mode](https://github.com/mesilov/bitrix24-php-sdk/issues/268) and integration test * add method to interface `Core\Contracts\BatchInterface::updateEntityItems` for update items in batch mode * add in scope `Placements` service `Placement\Service\UserFieldType` for work with user fields embedding -* add in scope `Telephony` add events: `OnExternalCallBackStart`, `OnExternalCallStart`, `OnVoximplantCallEnd`, `OnVoximplantCallEnd` - , `OnVoximplantCallInit`, `OnVoximplantCallStart` see [add telephony events](https://github.com/mesilov/bitrix24-php-sdk/issues/304) +* add in scope `Telephony` add + events: `OnExternalCallBackStart`, `OnExternalCallStart`, `OnVoximplantCallEnd`, `OnVoximplantCallEnd` + , `OnVoximplantCallInit`, `OnVoximplantCallStart` + see [add telephony events](https://github.com/mesilov/bitrix24-php-sdk/issues/304) * add `ApplicationStatus` with application status codes description * add fabric method `AccessToken::initFromPlacementRequest` when application init form placement request * add fabric method `ApplicationProfile::initFromArray` when application profile stored in ENV-variables * add `Bitrix24\SDK\Application\Requests\Placement\PlacementRequest` for application data from placements * add fabric method `Credentials::initFromPlacementRequest` when application init form placement request * add method `Services\Main\Service::getServerTime` returns current server time in the format YYYY-MM-DDThh:mm:ss±hh:mm. -* add method `Services\Main\Service::getCurrentUserProfile` return basic Information about the current user without any scopes +* add method `Services\Main\Service::getCurrentUserProfile` return basic Information about the current user without any + scopes * add method `Services\Main\Service::getAccessName` returns access permission names. -* add method `Services\Main\Service::checkUserAccess` Checks if the current user has at least one permission of those specified by the +* add method `Services\Main\Service::checkUserAccess` Checks if the current user has at least one permission of those + specified by the ACCESS parameter. * add method `Services\Main\Service::getMethodAffordability` Method returns 2 parameters - isExisting and isAvailable * add money type support by [phpmoney](https://github.com/moneyphp/money) @@ -40,16 +133,20 @@ ### Changed -* update scope list [расширить и актуализировать доступные скоупы](https://github.com/mesilov/bitrix24-php-sdk/issues/280) +* update scope + list [расширить и актуализировать доступные скоупы](https://github.com/mesilov/bitrix24-php-sdk/issues/280) * bump `symfony/*` to `6.*` version requirement. * method `Services\Main\Service::getAvailableMethods` marks as deprecated * method `Services\Main\Service::getAllMethods` marks as deprecated * method `Services\Main\Service::getMethodsByScope` marks as deprecated * ❗️fabric methods `Bitrix24\SDK\Core\Credentials` - renamed and now are [consistent](https://github.com/mesilov/bitrix24-php-sdk/issues/303): `createFromWebhook`, `createFromOAuth` + renamed and now + are [consistent](https://github.com/mesilov/bitrix24-php-sdk/issues/303): `createFromWebhook`, `createFromOAuth` , `createFromPlacementRequest` -* ❗️deleted [unused class](https://github.com/mesilov/bitrix24-php-sdk/issues/303) `Bitrix24\SDK\Core\Response\DTO\ResponseDataCollection` -* ❗️deleted [redundant class](https://github.com/mesilov/bitrix24-php-sdk/issues/303) `Bitrix24\SDK\Core\Response\DTO\Result` +* +❗️deleted [unused class](https://github.com/mesilov/bitrix24-php-sdk/issues/303) `Bitrix24\SDK\Core\Response\DTO\ResponseDataCollection` +* +❗️deleted [redundant class](https://github.com/mesilov/bitrix24-php-sdk/issues/303) `Bitrix24\SDK\Core\Response\DTO\Result` * ❗️deleted [method](https://github.com/mesilov/bitrix24-php-sdk/issues/303) `CoreBuilder::withWebhookUrl`, use method `CoreBuilder::withCredentials` @@ -57,7 +154,8 @@ * add bugfix for batch method for reverse order queries * fix type compatible errors for `Core\Result\AbstractItem` -* fix error in `NetworkTimingParser`, [error in NetworkTimingsErrorInfo](https://github.com/mesilov/bitrix24-php-sdk/issues/277) +* fix error + in `NetworkTimingParser`, [error in NetworkTimingsErrorInfo](https://github.com/mesilov/bitrix24-php-sdk/issues/277) * fix error in `RenewedAccessToken` DTO, remove `Scope` enum [UnknownScopeCodeException - in refresh token response](https://github.com/mesilov/bitrix24-php-sdk/issues/295) @@ -71,18 +169,22 @@ * add «fast» batch-query without counting elements in result recordset [Добавить поддержку выгрузки большого количества данных без подсчёта элементов -1](https://github.com/mesilov/bitrix24-php-sdk/issues/248) -* add `Credentials` in CoreBuilder [set credentials from core builder](https://github.com/mesilov/bitrix24-php-sdk/pull/246) +* add `Credentials` in + CoreBuilder [set credentials from core builder](https://github.com/mesilov/bitrix24-php-sdk/pull/246) * add method `Core\Batch::deleteEntityItems` for delete items in batch mode and integration test * add integration test for read strategy `FilterWithBatchWithoutCountOrderTest` * add type check in method `Core\Batch::deleteEntityItems` - only integer id allowed * add interface `Core\Contracts\DeletedItemResultInterface` * add in scope «CRM» `Services\CRM\Deal\Service\Batch::delete` batch delete deals * add `symfony/stopwatch` component for integration tests -* add `/Infrastructure/HttpClient/TransportLayer/NetworkTimingsParser` for parse `curl_info` network data structures for debug logs +* add `/Infrastructure/HttpClient/TransportLayer/NetworkTimingsParser` for parse `curl_info` network data structures for + debug logs in `Bitrix24\SDK\Core\Response::__destruct()` -* add `/Infrastructure/HttpClient/TransportLayer/ResponseInfoParser` for parse `bitrix24_rest_api` timing info for debug logs +* add `/Infrastructure/HttpClient/TransportLayer/ResponseInfoParser` for parse `bitrix24_rest_api` timing info for debug + logs in `Bitrix24\SDK\Core\Response::__destruct()` -* add `Bitrix24\SDK\Core\BulkItemsReader` for data-intensive applications for bulk export data from Bitrix24, read strategies located in +* add `Bitrix24\SDK\Core\BulkItemsReader` for data-intensive applications for bulk export data from Bitrix24, read + strategies located in folder `ReadStrategies`, in services read model **must** use most effective read strategy. * add integration tests in GitHub Actions pipeline 🎉, now integration tests run on push on `dev-branch` * add incoming webhook for run integration tests `vendor-check.yml` from vendor CI\CD pipeline @@ -93,7 +195,8 @@ * switch `symfony/http-client-contracts` to `^2.5` version requirement. * switch `symfony/event-dispatcher` to `5.4.*` version requirement. * switch `ramsey/uuid` to `^4.2.3` version requirement. -* switch `psr/log` to `^1.1.4 || ^2.0 || ^3.0` [version requirement](https://github.com/mesilov/bitrix24-php-sdk/issues/245). +* switch `psr/log` + to `^1.1.4 || ^2.0 || ^3.0` [version requirement](https://github.com/mesilov/bitrix24-php-sdk/issues/245). ## 2.0-alpha.5 – 28.11.2021 @@ -116,7 +219,8 @@ ### Changed -* update type definition for `ContactItemResult`, now return types will be cast to real types: DateTimeInterface, int, boolean etc +* update type definition for `ContactItemResult`, now return types will be cast to real types: DateTimeInterface, int, + boolean etc ## 2.0-alpha.4 – 25.11.2021 @@ -238,10 +342,12 @@ branch version 1.x – bugfix and security releases only ## 0.5.0 (4.09.2016) -* add class `Bitrix24\CRM\Quote` see pr [Added support for Quote API calls](https://github.com/mesilov/bitrix24-php-sdk/pull/53/) +* add class `Bitrix24\CRM\Quote` see + pr [Added support for Quote API calls](https://github.com/mesilov/bitrix24-php-sdk/pull/53/) * add support http status 301 moved permanently in class `Bitrix24` see issue [301 Moved Permanently #49](https://github.com/mesilov/bitrix24-php-sdk/issues/49) -* fixed bug in class `Bitrix24` see pr [Issue in the isAccessTokenExpire method](https://github.com/mesilov/bitrix24-php-sdk/pull/54) +* fixed bug in class `Bitrix24` see + pr [Issue in the isAccessTokenExpire method](https://github.com/mesilov/bitrix24-php-sdk/pull/54) ## 0.4.1 (4.08.2016) diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ee1097e7 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +default: + @echo "make needs target:" + @egrep -e '^\S+' ./Makefile | grep -v default | sed -r 's/://' | sed -r 's/^/ - /' + +phpstan: + vendor/bin/phpstan analyse \ No newline at end of file diff --git a/README.md b/README.md index 97104ce4..162cf165 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Bitrix24 REST API PHP SDK A powerful PHP library for the Bitrix24 REST API -### Build status +## Build status | CI\CD [status](https://github.com/mesilov/bitrix24-php-sdk/actions) on `master` | |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -15,26 +15,16 @@ A powerful PHP library for the Bitrix24 REST API Integration tests run in GitHub actions with real Bitrix24 portal - -### BITRIX24-PHP-SDK Documentation - -- [Russian](/docs/RU/documentation.md) -- [English](/docs/EN/documentation.md) - -### BITRIX24-PHP-SDK ✨FEATURES✨ +## BITRIX24-PHP-SDK ✨FEATURES✨ Support both auth modes: - [x] work with auth tokens for Bitrix24 applications in marketplace - [x] work with incoming webhooks for simple integration projects for current portal -Low-level tools to devs: - -- Domain core events: - - [x] Access Token expired - - [ ] Bitrix24 portal domain url changed -- [ ] Rate-limit strategy -- [ ] Retry strategy for safe methods +Domain core events: + - [x] Access Token expired + - [x] Bitrix24 portal domain url changed API - level features @@ -52,7 +42,12 @@ Performance improvements 🚀 - [ ] composite batch queries to many entities (work in progress) - [ ] read without count flag -### Development principles +Low-level tools to devs: +- [ ] Rate-limit strategy +- [ ] Retry strategy for safe methods + + +## Development principles - Good developer experience - auto-completion of methods at the IDE @@ -65,7 +60,7 @@ Performance improvements 🚀 - Performance first: - minimal impact on client code - ability to work with large amounts of data with constant memory consumption - - efficient operation of the API using butch requests + - efficient operation of the API using batch requests - Modern technology stack - based on [Symfony HttpClient](https://symfony.com/doc/current/http_client.html) - actual PHP versions language features @@ -73,62 +68,42 @@ Performance improvements 🚀 - test coverage: unit, integration, contract - typical examples typical for different modes of operation and they are optimized for memory \ performance -### Sponsors - -Help bitrix24-php-sdk by [boosty.to/bitrix24-php-sdk](https://boosty.to/bitrix24-php-sdk) its development! - -### Architecture +## Architecture ### Abstraction layers ``` -- http protocol -- json data +- http2 protocol via json data structures - symfony http client - \Bitrix24\SDK\Core\ApiClient - work with b24 rest-api endpoints input: arrays \ strings output: Symfony\Contracts\HttpClient\ResponseInterface, operate with strings process: network operations -- \Bitrix24\SDK\Services\Main - work with b24 rest-api entities - input: arrays \ strings (?) or queries? +- \Bitrix24\SDK\Services\* - work with b24 rest-api entities + input: arrays \ strings output: b24 response dto - process: b24 entities, operate with + process: b24 entities, operate with immutable objects ``` +## Sponsors -### File Structure - -``` - /Core - ApiClient.php - default api-client, work on http abstraction layer, return - Symfony\Contracts\HttpClient\ResponseInterface - /Services - /CRM - /Deals - /Client - Deals.php - /Exceptions - /Tasks - … - Main.php - default bitrix24 rest api service provide basic funcions, work with data transfer objects -``` +Help bitrix24-php-sdk by [boosty.to/bitrix24-php-sdk](https://boosty.to/bitrix24-php-sdk) -### Requirements +## Requirements -- php: >=7.4 +- php: >=8.2 - ext-json: * - ext-curl: * -### Example - -### Installation +## Installation Add `"mesilov/bitrix24-php-sdk": "2.x"` to `composer.json` of your application. Or clone repo to your project. -### Tests +## Tests Tests locate in folder `tests` and we have two test types. In folder tests create file `.env.local` and fill environment variables from `.env`. -#### Unit tests +### Unit tests **Fast**, in-memory tests without a network I\O For run unit tests you must call in command line @@ -136,11 +111,11 @@ In folder tests create file `.env.local` and fill environment variables from `.e composer phpunit-run-unit-test ``` -#### Integration tests +### Integration tests **Slow** tests with full lifecycle with your **test** Bitrix24 portal via webhook. -❗️Do not run integration tests with production portals ❗️ +❗️Do not run integration tests with production portals For run integration test you must: @@ -172,220 +147,28 @@ Call in command line composer phpstan-analyse ``` -### Submitting bugs and feature requests +## Submitting bugs and feature requests Bugs and feature request are tracked on [GitHub](https://github.com/mesilov/bitrix24-php-sdk/issues) -### License +## License bitrix24-php-sdk is licensed under the MIT License - see the `MIT-LICENSE.txt` file for details -### Author +## Authors -Maxim Mesilov - -
-See also the list of [contributors](https://github.com/mesilov/bitrix24-php-sdk/graphs/contributors) which participated in this project. +Maksim Mesilov - mesilov.maxim@gmail.com -### Need custom Bitrix24 application? ## +See also the list of [contributors](https://github.com/mesilov/bitrix24-php-sdk/graphs/contributors) which participated in this project. -email: +## Need custom Bitrix24 application? +mesilov.maxim@gmail.com for private consultations or dedicated support -### Documentation +## Documentation [Bitrix24 API documentation - Russian](http://dev.1c-bitrix.ru/rest_help/) [Bitrix24 API documentation - English](https://training.bitrix24.com/rest_help/) -[Register new Bitrix24 account](https://www.bitrix24.ru/create.php?p=255670) - -## Русский - -### Принципы по которым ведётся разработка - -- хороший DX (Developer Experience) - - автодополнение методов на уровне IDE - - типизированные сигнатуры вызова методов - - типизированные результаты вызова методов – используются нативные типы: int, array, bool, string - - хелперы для типовых операций -- хорошая документация - - документация по работе конкретного метода содержащая ссылку на офф документацию - - документация по работе с SDK -- производительность: - - минимальное влияние на клиентский код - - возможность работать с большими объёмами данных с константным потреблением памяти - - эффективная работа c API с использованием батч-запросов -- современный стек технологий: - - библиотеки для работы с сетью и возможностью асинхронной работы - - фичи новых версий PHP -- надёжной: - - покрытие тестами: unit, интеграционные, контрактные - - есть типовые примеры характерные для разных режимов работы и они оптимизированы по памяти \ быстродействию - -### Спонсоры - -Помогите развитию bitrix24-php-sdk подписавшись на [boosty.to/bitrix24-php-sdk](https://boosty.to/bitrix24-php-sdk)! - -### Ключевые особенности - -### Слои SDK - -### Service – API-интерфейс для работы с конкретной сущностью - -Зона ответственности: - -- контракт на API-методы сущности - -Входящие данные: - -- сигнатура вызова конкретного API-метода - -Возвращаемый результат: - -- `Core\Response` (????) **к обсуждению** - -В зависимости от метода может быть разный возвращаемый результат: - -- результат выполнения операции типа bool -- идентификатор созданной сущности типа int -- сущность + пользовательские поля с префиксами UF_ типа array -- массив сущностей типа array -- пустой массив как результат пустого фильтра. - -Если возвращать `Core\Response`, то в клиентском коде будут проблемы: - -- длинные цепочки в клиентском коде для получения возвращаемого результата - -```php -// добавили сделку в Б24 -$dealId = $dealsService->add($newDeal)->getResponseData()->getResult()->getResultData()[0]; -// получили массив сделок -$deals = $dealsService->list([], [], [], 0)->getResponseData()->getResult()->getResultData(); -``` - -- отсутствие релевантной вызываемому методу типизации возвращаемого результата. - -Ожидание: - -```php - add(array $newDeal):int // идентификатор новой сделки - list(array $order, array $filter, array $select, int $start):array //массив сделок + постраничка - get(int $dealId):array // конкретная сделка -``` - -Текущая реализация — возвращается унифицированный результат: - -```php -add(array $newDeal):Core\Response -list(array $order, array $filter, array $select, int $start):Core\Response -``` - -#### Core – вызов произвольных API-методов - -Зона ответственности: - -- вызов **произвольных** API-методов -- обработка ошибок уровня API -- запрос нового токена и повторение запроса, если получили ошибку `expired_token` - -Входящие данные: - -- `string $apiMethod` – название api-метода -- `array $parameters = []` – аргументы метода - -Возвращаемый результат: `Core\Response` – **унифицированный** объект-обёртка, содержит: - -- `Symfony\Contracts\HttpClient\ResponseInterface` — объект ответа от сервера, может быть асинхронным -- `Core\Commands\Command` — информация о команде\аргументах которая была исполнена, используется при разборе пакетных запросов. - -Для получения результата запроса к API используется метод `Response::getResponseData`, который декодирует тело ответа вызвав -метод `Symfony\Contracts\HttpClient::toArray` -Возвращается стандартизированный DTO `ResponseData` от API-сервера с полями: - -- `Result` - DTO c результатом исполнения запроса; -- `Time` — DTO c таймингом прохождения запроса через сервера Битрикс24; -- `Pagination` — DTO постраничной навигации с полями `next` и `total`; - -В случае обнаружения ошибок уровня домена будет выброшено соответствующее типизированное исключение. - -Объект `Result` содержит метод `getResultData`, который возвращает массив с результатом исполнения API-запроса. В зависимости от вызванного -метода там может быть: - -- результат выполнения операции типа bool -- идентификатор созданной сущности типа int -- сущность + пользовательские поля с префиксами UF_ типа array -- массив сущностей типа array -- пустой массив как результат пустого фильтра. - -#### ApiClient — работа с сетью и эндпоинтами API-серверов - -Зона ответственности: - -- передача данных по сети -- соблюдение контракта на эндпоинты с которыми идёт работы -- «подпись» запросов токенами \ передача их в нужные входящие адреса если используется авторизация по вебхукам - -Используется: -Symfony HttpClient - -Входящие данные: - -- тип http-запроса -- массив с параметрами - -Возвращаемые результаты: -— `Symfony\Contracts\HttpClient\ResponseInterface` - -#### Формат передачи данных по сети - -JSON по HTTP/2 или HTTP/1.1 - -## Спонсоры - -### Тесты - -Тесты расположены в папке `tests` и бывают двух типов: юнит и интеграционные. -В папке `tests` создайте файл `.env.local` и заполните переменные из файла `.env`. - -#### Юнит тесты - -**Быстрые**, выполняются без сетевого взаимодействия с Битрикс 24. - -```shell -composer phpunit-run-unit-test -``` - -#### Интеграционные тесты - -**Медленные** тесты покрывают полный жизненный цикл CRUD операций подключение к Битрикс 24 происходи с помощью веб-хука. - -❗ Не запускайте интеграционные тесты на ваших production порталах они удалят все ваши данные ❗️ - -Для запуска интеграционных тестов вам нужно: - -1. Создать [Новый портал Битрикс 24](https://www.bitrix24.ru/create.php?p=255670) для запуска тестов. -2. Перейти в левое меню и нажать "Карта сайта". -3. Найти меню для "Разработчиков" -4. Кликнуть в меню «Другое» -5. Кликнуть в меню «Входящий веб-хук» -6. Выбрать все нужные расширения и нажать кнопку "сохранить". -7. Создать файл `/tests/.env.local` с переменными окружения которые скопировать из файла `/tests/.env` . - -```yaml -APP_ENV=dev -BITRIX24_WEBHOOK=https:// your portal webhook url -INTEGRATION_TEST_LOG_LEVEL=500 -``` - -8. Запуск из командной строки. - -```shell -composer composer phpunit-run-integration-tests -``` - -#### Статический анализ кодовой базы – phpstan - -Запуск из командной строки. - -```shell - composer phpstan-analyse -``` +[Register new Bitrix24 account](https://www.bitrix24.ru/create.php?p=255670) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..96736b8c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 2.x | :white_check_mark: | +| 1.x | :x: | +| 0.x | :x: | + +## Reporting a Vulnerability +Create issue with vulnerability details \ No newline at end of file diff --git a/tools/bin/console b/bin/console similarity index 71% rename from tools/bin/console rename to bin/console index c0c6a43f..5a561cd4 100644 --- a/tools/bin/console +++ b/bin/console @@ -1,11 +1,13 @@ #!/usr/bin/env php hasParameterOption('--no-debug', true)) { putenv('APP_DEBUG=' . $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); } -(new Dotenv())->bootEnv(dirname(__DIR__) . '/.env'); +(new Dotenv())->bootEnv(dirname(__DIR__) . '/tools/.env'); if ($_SERVER['APP_DEBUG']) { umask(0000); @@ -44,12 +46,13 @@ if ($_SERVER['APP_DEBUG']) { } } -$log = new Logger('demo-data-generator'); +$log = new Logger('bitrix24-php-sdk-cli'); $log->pushHandler(new StreamHandler($_ENV['LOGS_FILE'], (int)$_ENV['LOGS_LEVEL'])); -$log->pushProcessor(new \Monolog\Processor\MemoryUsageProcessor(true, true)); +$log->pushProcessor(new MemoryUsageProcessor(true, true)); $application = new Application(); $application->add(new GenerateContactsCommand($log)); $application->add(new ListCommand($log)); $application->add(new ShowFieldsDescriptionCommand($log)); +$application->add(new CopyPropertyValues($log)); $application->run($input); \ No newline at end of file diff --git a/composer.json b/composer.json index 7a54f0ad..0aba0659 100644 --- a/composer.json +++ b/composer.json @@ -12,34 +12,39 @@ "license": "MIT", "authors": [ { - "name": "Maxim Mesilov", + "name": "Maksim Mesilov", "homepage": "https://github.com/mesilov/" } ], + "config": { + "sort-packages": true + }, "require": { - "php": "7.4.*|8.*", + "php": "8.2.* || 8.3.*", "ext-json": "*", "ext-bcmath": "*", "ext-curl": "*", - "psr/log": "^1.1.4 || ^2.0 || ^3.0", + "ext-intl": "*", + "psr/log": "1.1.*", "fig/http-message-util": "1.1.*", - "symfony/http-client": "5.4.* || 6.*", - "symfony/http-client-contracts": "^2.5 || ^3.1", - "symfony/http-foundation": "5.4.* || 6.*", - "symfony/event-dispatcher": "5.4.* || 6.*", - "ramsey/uuid": "^4.2.3", - "moneyphp/money": "3.* || 4.*" + "ramsey/uuid": "4.7.*", + "moneyphp/money": "4.3.*", + "symfony/http-client": "7.0.*", + "symfony/http-client-contracts": "3.4.*", + "symfony/http-foundation": "7.0.*", + "symfony/event-dispatcher": "7.0.*", + "symfony/uid": "7.0.*" }, "require-dev": { - "monolog/monolog": "2.1.*", - "symfony/console": "5.4.* || 6.*", - "symfony/dotenv": "5.4.* || 6.*", - "symfony/debug-bundle": "5.4.* || 6.*", - "phpstan/phpstan": "1.*", - "phpunit/phpunit": "9.5.*", - "symfony/stopwatch": "5.4.* || 6.*", + "monolog/monolog": "2.9.*", + "phpstan/phpstan": "1.10.*", + "phpunit/phpunit": "10.5.*", + "symfony/console": "7.0.*", + "symfony/dotenv": "7.0.*", + "symfony/debug-bundle": "7.0.*", + "symfony/stopwatch": "7.0.*", "roave/security-advisories": "dev-master", - "ext-intl": "*" + "fakerphp/faker": "1.23.*" }, "autoload": { "psr-4": { @@ -63,4 +68,4 @@ "vendor/bin/phpstan analyse --memory-limit 1G" ] } -} \ No newline at end of file +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a3e33d9d..eb16a1a7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,25 +1,19 @@ - - - - ./src - - - - - - - - ./tests/Unit - - - ./tests/Integration - - + + + + + + + ./tests/Unit + + + ./tests/Integration + + + + + ./src + + diff --git a/src/Application/ApplicationStatus.php b/src/Application/ApplicationStatus.php index 54500ccf..fc65ebc4 100644 --- a/src/Application/ApplicationStatus.php +++ b/src/Application/ApplicationStatus.php @@ -20,7 +20,7 @@ class ApplicationStatus /** * @param string $statusShortCode * - * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + * @throws InvalidArgumentException */ public function __construct(string $statusShortCode) { @@ -107,13 +107,24 @@ public function getStatusCode(): string } /** - * @param \Symfony\Component\HttpFoundation\Request $request + * @param Request $request * * @return self - * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + * @throws InvalidArgumentException */ public static function initFromRequest(Request $request): self { return new self($request->request->getAlpha('status')); } + + /** + * @param string $shortStatusCode + * + * @return self + * @throws InvalidArgumentException + */ + public static function initFromString(string $shortStatusCode): self + { + return new self($shortStatusCode); + } } \ No newline at end of file diff --git a/src/Application/Contracts/Bitrix24Account/Bitrix24AccountInterface.php b/src/Application/Contracts/Bitrix24Account/Bitrix24AccountInterface.php new file mode 100644 index 00000000..654f377b --- /dev/null +++ b/src/Application/Contracts/Bitrix24Account/Bitrix24AccountInterface.php @@ -0,0 +1,114 @@ + + */ + public function findAllActive(): array; + + /** + * @return array + */ + public function findAllDeactivated(): array; +} \ No newline at end of file diff --git a/src/Application/Contracts/Bitrix24Account/Bitrix24AccountStatus.php b/src/Application/Contracts/Bitrix24Account/Bitrix24AccountStatus.php new file mode 100644 index 00000000..bcb29b22 --- /dev/null +++ b/src/Application/Contracts/Bitrix24Account/Bitrix24AccountStatus.php @@ -0,0 +1,16 @@ +credentials = $credentials; $this->client = $client; + $this->requestIdGenerator = $requestIdGenerator; $this->logger = $logger; $this->logger->debug( 'ApiClient.init', @@ -53,18 +64,18 @@ public function __construct(Credentials\Credentials $credentials, HttpClientInte protected function getDefaultHeaders(): array { return [ - 'Accept' => 'application/json', - 'Accept-Charset' => 'utf-8', - 'User-Agent' => sprintf('%s-v-%s-php-%s', self::SDK_USER_AGENT, self::SDK_VERSION, PHP_VERSION), + 'Accept' => 'application/json', + 'Accept-Charset' => 'utf-8', + 'User-Agent' => sprintf('%s-v-%s-php-%s', self::SDK_USER_AGENT, self::SDK_VERSION, PHP_VERSION), 'X-BITRIX24-PHP-SDK-PHP-VERSION' => PHP_VERSION, - 'X-BITRIX24-PHP-SDK-VERSION' => self::SDK_VERSION, + 'X-BITRIX24-PHP-SDK-VERSION' => self::SDK_VERSION, ]; } /** - * @return Credentials\Credentials + * @return Credentials */ - public function getCredentials(): Credentials\Credentials + public function getCredentials(): Credentials { return $this->credentials; } @@ -73,11 +84,14 @@ public function getCredentials(): Credentials\Credentials * @return RenewedAccessToken * @throws InvalidArgumentException * @throws TransportExceptionInterface - * @throws \JsonException + * @throws TransportException */ public function getNewAccessToken(): RenewedAccessToken { - $this->logger->debug('getNewAccessToken.start'); + $requestId = $this->requestIdGenerator->getRequestId(); + $this->logger->debug('getNewAccessToken.start', [ + 'requestId' => $requestId + ]); if ($this->getCredentials()->getApplicationProfile() === null) { throw new InvalidArgumentException('application profile not set'); } @@ -91,28 +105,41 @@ public function getNewAccessToken(): RenewedAccessToken $this::BITRIX24_OAUTH_SERVER_URL, http_build_query( [ - 'grant_type' => 'refresh_token', - 'client_id' => $this->getCredentials()->getApplicationProfile()->getClientId(), + 'grant_type' => 'refresh_token', + 'client_id' => $this->getCredentials()->getApplicationProfile()->getClientId(), 'client_secret' => $this->getCredentials()->getApplicationProfile()->getClientSecret(), 'refresh_token' => $this->getCredentials()->getAccessToken()->getRefreshToken(), + $this->requestIdGenerator->getQueryStringParameterName() => $requestId ] ) ); $requestOptions = [ - 'headers' => $this->getDefaultHeaders(), + 'headers' => array_merge( + $this->getDefaultHeaders(), + [ + $this->requestIdGenerator->getHeaderFieldName() => $requestId + ] + ), ]; $response = $this->client->request($method, $url, $requestOptions); - $result = $response->toArray(false); - $newAccessToken = RenewedAccessToken::initFromArray($result); - - $this->logger->debug('getNewAccessToken.finish'); - - return $newAccessToken; + $responseData = $response->toArray(false); + if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) { + $newAccessToken = RenewedAccessToken::initFromArray($responseData); + + $this->logger->debug('getNewAccessToken.finish', [ + 'requestId' => $requestId + ]); + return $newAccessToken; + } + if ($response->getStatusCode() === StatusCodeInterface::STATUS_BAD_REQUEST) { + throw new TransportException(sprintf('getting new access token failure: %s', $responseData['error'])); + } + throw new TransportException('getting new access token failure with unknown http-status code %s', $response->getStatusCode()); } /** - * @param string $apiMethod + * @param string $apiMethod * @param array $parameters * * @return ResponseInterface @@ -121,12 +148,14 @@ public function getNewAccessToken(): RenewedAccessToken */ public function getResponse(string $apiMethod, array $parameters = []): ResponseInterface { + $requestId = $this->requestIdGenerator->getRequestId(); $this->logger->info( 'getResponse.start', [ - 'apiMethod' => $apiMethod, - 'domainUrl' => $this->credentials->getDomainUrl(), + 'apiMethod' => $apiMethod, + 'domainUrl' => $this->credentials->getDomainUrl(), 'parameters' => $parameters, + 'requestId' => $requestId ] ); @@ -141,10 +170,17 @@ public function getResponse(string $apiMethod, array $parameters = []): Response } $parameters['auth'] = $this->getCredentials()->getAccessToken()->getAccessToken(); } - + // duplicate request id in query string for current version of bitrix24 api + // vendor don't use request id from headers =( + $url .= '?' . $this->requestIdGenerator->getQueryStringParameterName() . '=' . $requestId; $requestOptions = [ - 'json' => $parameters, - 'headers' => $this->getDefaultHeaders(), + 'json' => $parameters, + 'headers' => array_merge( + $this->getDefaultHeaders(), + [ + $this->requestIdGenerator->getHeaderFieldName() => $requestId + ] + ), // disable redirects, try to catch portal change domain name event 'max_redirects' => 0, ]; @@ -153,8 +189,9 @@ public function getResponse(string $apiMethod, array $parameters = []): Response $this->logger->info( 'getResponse.end', [ - 'apiMethod' => $apiMethod, + 'apiMethod' => $apiMethod, 'responseInfo' => $response->getInfo(), + 'requestId' => $requestId ] ); diff --git a/src/Core/ApiLevelErrorHandler.php b/src/Core/ApiLevelErrorHandler.php index 685b9331..b25deaee 100644 --- a/src/Core/ApiLevelErrorHandler.php +++ b/src/Core/ApiLevelErrorHandler.php @@ -6,6 +6,7 @@ use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Exceptions\MethodNotFoundException; +use Bitrix24\SDK\Core\Exceptions\OperationTimeLimitExceededException; use Bitrix24\SDK\Core\Exceptions\QueryLimitExceededException; use Psr\Log\LoggerInterface; @@ -20,6 +21,8 @@ class ApiLevelErrorHandler { protected LoggerInterface $logger; protected const ERROR_KEY = 'error'; + protected const RESULT_KEY = 'result'; + protected const RESULT_ERROR_KEY = 'result_error'; protected const ERROR_DESCRIPTION_KEY = 'error_description'; /** @@ -33,33 +36,89 @@ public function __construct(LoggerInterface $logger) } /** - * @param array $responseBody + * @param array $responseBody * * @throws QueryLimitExceededException * @throws BaseException */ public function handle(array $responseBody): void { - if (!array_key_exists(self::ERROR_KEY, $responseBody)) { - $this->logger->debug('handle.noError'); + // single query error response + if (array_key_exists(self::ERROR_KEY, $responseBody) && array_key_exists(self::ERROR_DESCRIPTION_KEY, $responseBody)) { + $this->handleError($responseBody); + } + // error in batch response + if (!array_key_exists(self::RESULT_KEY, $responseBody) || (!is_array($responseBody[self::RESULT_KEY]))) { return; } + + if (array_key_exists(self::RESULT_ERROR_KEY, $responseBody[self::RESULT_KEY])) { + foreach ($responseBody[self::RESULT_KEY][self::RESULT_ERROR_KEY] as $cmdId => $errorData) { + $this->handleError($errorData, $cmdId); + } + } + } + + /** + * @throws MethodNotFoundException + * @throws QueryLimitExceededException + * @throws BaseException + */ + private function handleError(array $responseBody, ?string $batchCommandId = null): void + { $errorCode = strtolower(trim((string)$responseBody[self::ERROR_KEY])); $errorDescription = strtolower(trim((string)$responseBody[self::ERROR_DESCRIPTION_KEY])); + $this->logger->debug( - 'handle.errorCode', + 'handle.errorInformation', [ 'errorCode' => $errorCode, + 'errorDescription' => $errorDescription, ] ); + + $batchErrorPrefix = ''; + if ($batchCommandId !== null) { + $batchErrorPrefix = sprintf(' batch command id: %s', $batchCommandId); + } + switch ($errorCode) { case 'query_limit_exceeded': - throw new QueryLimitExceededException('query limit exceeded - too many requests'); + throw new QueryLimitExceededException(sprintf('query limit exceeded - too many requests %s', $batchErrorPrefix)); case 'error_method_not_found': - throw new MethodNotFoundException('api method not found'); + throw new MethodNotFoundException(sprintf('api method not found %s %s', $errorDescription, $batchErrorPrefix)); + case 'operation_time_limit': + throw new OperationTimeLimitExceededException(sprintf('operation time limit exceeded %s %s', $errorDescription, $batchErrorPrefix)); default: - throw new BaseException(sprintf('%s - %s', $errorCode, $errorDescription)); + throw new BaseException(sprintf('%s - %s %s', $errorCode, $errorDescription, $batchErrorPrefix)); } + // switch (strtoupper(trim($apiResponse['error']))) { +// case 'EXPIRED_TOKEN': +// throw new Bitrix24TokenIsExpiredException($errorMsg); +// case 'WRONG_CLIENT': +// case 'ERROR_OAUTH': +// $this->log->error($errorMsg, $this->getErrorContext()); +// throw new Bitrix24WrongClientException($errorMsg); +// case 'ERROR_METHOD_NOT_FOUND': +// $this->log->error($errorMsg, $this->getErrorContext()); +// throw new Bitrix24MethodNotFoundException($errorMsg); +// case 'INVALID_TOKEN': +// case 'INVALID_GRANT': +// $this->log->error($errorMsg, $this->getErrorContext()); +// throw new Bitrix24TokenIsInvalidException($errorMsg); + +// case 'PAYMENT_REQUIRED': +// $this->log->error($errorMsg, $this->getErrorContext()); +// throw new Bitrix24PaymentRequiredException($errorMsg); +// case 'NO_AUTH_FOUND': +// $this->log->error($errorMsg, $this->getErrorContext()); +// throw new Bitrix24PortalRenamedException($errorMsg); +// case 'INSUFFICIENT_SCOPE': +// $this->log->error($errorMsg, $this->getErrorContext()); +// throw new Bitrix24InsufficientScope($errorMsg); +// default: +// $this->log->error($errorMsg, $this->getErrorContext()); +// throw new Bitrix24ApiException($errorMsg); } } \ No newline at end of file diff --git a/src/Core/Batch.php b/src/Core/Batch.php index 10224162..8bb1fa43 100644 --- a/src/Core/Batch.php +++ b/src/Core/Batch.php @@ -6,7 +6,7 @@ use Bitrix24\SDK\Core\Commands\Command; use Bitrix24\SDK\Core\Commands\CommandCollection; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Contracts\CoreInterface; use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; @@ -23,7 +23,7 @@ * * @package Bitrix24\SDK\Core */ -class Batch implements BatchInterface +class Batch implements BatchOperationsInterface { private CoreInterface $core; private LoggerInterface $logger; @@ -34,7 +34,7 @@ class Batch implements BatchInterface /** * Batch constructor. * - * @param CoreInterface $core + * @param CoreInterface $core * @param LoggerInterface $log */ public function __construct(CoreInterface $core, LoggerInterface $log) @@ -62,7 +62,7 @@ protected function clearCommands(): void /** * Add entity items with batch call * - * @param string $apiMethod + * @param string $apiMethod * @param array $entityItems * * @return Generator|ResponseData[] @@ -73,7 +73,7 @@ public function addEntityItems(string $apiMethod, array $entityItems): Generator $this->logger->debug( 'addEntityItems.start', [ - 'apiMethod' => $apiMethod, + 'apiMethod' => $apiMethod, 'entityItems' => $entityItems, ] ); @@ -105,7 +105,7 @@ public function addEntityItems(string $apiMethod, array $entityItems): Generator /** * Delete entity items with batch call * - * @param string $apiMethod + * @param string $apiMethod * @param array $entityItemId * * @return Generator|ResponseData[] @@ -116,7 +116,7 @@ public function deleteEntityItems(string $apiMethod, array $entityItemId): Gener $this->logger->debug( 'deleteEntityItems.start', [ - 'apiMethod' => $apiMethod, + 'apiMethod' => $apiMethod, 'entityItems' => $entityItemId, ] ); @@ -173,7 +173,7 @@ public function deleteEntityItems(string $apiMethod, array $entityItemId): Gener * 'params' => [] // optional fields * ] * - * @param string $apiMethod + * @param string $apiMethod * @param array> $entityItems * * @return Generator|ResponseData[] @@ -184,7 +184,7 @@ public function updateEntityItems(string $apiMethod, array $entityItems): Genera $this->logger->debug( 'updateEntityItems.start', [ - 'apiMethod' => $apiMethod, + 'apiMethod' => $apiMethod, 'entityItems' => $entityItems, ] ); @@ -207,11 +207,14 @@ public function updateEntityItems(string $apiMethod, array $entityItems): Genera ); } - $this->registerCommand($apiMethod, [ - 'id' => $entityItemId, - 'fields' => $entityItem['fields'], - 'params' => $entityItem['params'], - ]); + $cmdArguments = [ + 'id' => $entityItemId, + 'fields' => $entityItem['fields'] + ]; + if (array_key_exists('params', $entityItem)) { + $cmdArguments['params'] = $entityItem['params']; + } + $this->registerCommand($apiMethod, $cmdArguments); } foreach ($this->getTraversable(true) as $cnt => $updatedItemResult) { @@ -244,24 +247,25 @@ public function updateEntityItems(string $apiMethod, array $entityItems): Genera /** * Register api command to command collection for batch calls * - * @param string $apiMethod + * @param string $apiMethod * @param array $parameters - * @param string|null $commandName - * @param callable|null $callback not implemented + * @param string|null $commandName + * @param callable|null $callback not implemented * * @throws \Exception */ protected function registerCommand( - string $apiMethod, - array $parameters = [], - ?string $commandName = null, + string $apiMethod, + array $parameters = [], + ?string $commandName = null, callable $callback = null - ): void { + ): void + { $this->logger->debug( 'registerCommand.start', [ - 'apiMethod' => $apiMethod, - 'parameters' => $parameters, + 'apiMethod' => $apiMethod, + 'parameters' => $parameters, 'commandName' => $commandName, ] ); @@ -317,11 +321,11 @@ protected function getReverseOrder(array $order): array /** * Get traversable list without count elements * - * @param string $apiMethod + * @param string $apiMethod * @param array $order - * @param array $filter - * @param array $select - * @param int|null $limit + * @param array $filter + * @param array $select + * @param int|null $limit * * @return \Generator * @throws \Bitrix24\SDK\Core\Exceptions\BaseException @@ -333,19 +337,22 @@ protected function getReverseOrder(array $order): array */ public function getTraversableList( string $apiMethod, - array $order, - array $filter, - array $select, - ?int $limit = null - ): Generator { + array $order, + array $filter, + array $select, + ?int $limit = null, + ?array $additionalParameters = null + ): Generator + { $this->logger->debug( 'getTraversableList.start', [ 'apiMethod' => $apiMethod, - 'order' => $order, - 'filter' => $filter, - 'select' => $select, - 'limit' => $limit, + 'order' => $order, + 'filter' => $filter, + 'select' => $select, + 'limit' => $limit, + 'additionalParameters' => $additionalParameters, ] ); @@ -376,15 +383,28 @@ public function getTraversableList( // todo проверили, что если есть limit, то он >1 // todo проверили, что в фильтре нет поля ID, т.к. мы с ним будем работать - $firstResultPage = $this->core->call( - $apiMethod, - [ - 'order' => $order, - 'filter' => $filter, - 'select' => $select, - 'start' => 0, - ] - ); + $params = [ + 'order' => $order, + 'filter' => $filter, + 'select' => $select, + 'start' => 0, + ]; + + // data structures for crm.items.* is little different =\ + $isCrmItemsInBatch = false; + if ($additionalParameters !== null) { + if (array_key_exists('entityTypeId', $additionalParameters)) { + $isCrmItemsInBatch = true; + } + $params = array_merge($params, $additionalParameters); + } + + if ($isCrmItemsInBatch) { + $keyId = 'id'; + } else { + $keyId = 'ID'; + } + $firstResultPage = $this->core->call($apiMethod, $params); $totalElementsCount = $firstResultPage->getResponseData()->getPagination()->getTotal(); $this->logger->debug('getTraversableList.totalElementsCount', [ 'totalElementsCount' => $totalElementsCount, @@ -407,31 +427,46 @@ public function getTraversableList( // filtered elements count more than one result page(50 elements) // return first page $lastElementIdInFirstPage = null; - foreach ($firstResultPage->getResponseData()->getResult() as $cnt => $listElement) { - $elementsCounter++; - $lastElementIdInFirstPage = (int)$listElement['ID']; - if ($limit !== null && $elementsCounter > $limit) { - return; + if ($isCrmItemsInBatch) { + foreach ($firstResultPage->getResponseData()->getResult()['items'] as $cnt => $listElement) { + $elementsCounter++; + $lastElementIdInFirstPage = (int)$listElement[$keyId]; + if ($limit !== null && $elementsCounter > $limit) { + return; + } + yield $listElement; + } + } else { + foreach ($firstResultPage->getResponseData()->getResult() as $cnt => $listElement) { + $elementsCounter++; + $lastElementIdInFirstPage = (int)$listElement[$keyId]; + if ($limit !== null && $elementsCounter > $limit) { + return; + } + yield $listElement; } - yield $listElement; } $this->clearCommands(); - if (!in_array('ID', $select, true)) { - $select[] = 'ID'; + if (!in_array($keyId, $select, true)) { + $select[] = $keyId; } - // getLastElementId in filtered result - $lastResultPage = $this->core->call( - $apiMethod, - [ - 'order' => $this->getReverseOrder($order), - 'filter' => $filter, - 'select' => $select, - 'start' => 0, - ] - ); - $lastElementId = (int)$lastResultPage->getResponseData()->getResult()[0]['ID']; + $params = [ + 'order' => $this->getReverseOrder($order), + 'filter' => $filter, + 'select' => $select, + 'start' => 0, + ]; + if ($additionalParameters !== null) { + $params = array_merge($params, $additionalParameters); + } + $lastResultPage = $this->core->call($apiMethod, $params); + if ($isCrmItemsInBatch) { + $lastElementId = (int)$lastResultPage->getResponseData()->getResult()['items'][0][$keyId]; + } else { + $lastElementId = (int)$lastResultPage->getResponseData()->getResult()[0][$keyId]; + } // reverse order if you need if ($lastElementIdInFirstPage > $lastElementId) { $tmp = $lastElementIdInFirstPage; @@ -440,7 +475,7 @@ public function getTraversableList( } $this->logger->debug('getTraversableList.lastElementsId', [ 'lastElementIdInFirstPage' => $lastElementIdInFirstPage, - 'lastElementId' => $lastElementId, + 'lastElementId' => $lastElementId, ]); // register commands with updated filter @@ -448,9 +483,9 @@ public function getTraversableList( $lastElementIdInFirstPage++; for ($startId = $lastElementIdInFirstPage; $startId <= $lastElementId; $startId += self::MAX_ELEMENTS_IN_PAGE) { $this->logger->debug('registerCommand.item', [ - 'startId' => $startId, + 'startId' => $startId, 'lastElementId' => $lastElementId, - 'delta' => $lastElementId - $startId, + 'delta' => $lastElementId - $startId, ]); $delta = $lastElementId - $startId; @@ -465,15 +500,17 @@ public function getTraversableList( $isLastPage = true; } - $this->registerCommand( - $apiMethod, - [ - 'order' => [], - 'filter' => $this->updateFilterForBatch($startId, $lastElementIdInPage, $isLastPage, $filter), - 'select' => $select, - 'start' => -1, - ] - ); + $params = [ + 'order' => [], + 'filter' => $this->updateFilterForBatch($keyId, $startId, $lastElementIdInPage, $isLastPage, $filter), + 'select' => $select, + 'start' => -1, + ]; + if ($additionalParameters !== null) { + $params = array_merge($params, $additionalParameters); + } + + $this->registerCommand($apiMethod, $params); } $this->logger->debug( 'getTraversableList.commandsRegistered', @@ -492,44 +529,58 @@ public function getTraversableList( 'getTraversableList.batchResultItem', [ 'batchCommandItemNumber' => $queryCnt, - 'nextItem' => $queryResultData->getPagination()->getNextItem(), - 'durationTime' => $queryResultData->getTime()->getDuration(), + 'nextItem' => $queryResultData->getPagination()->getNextItem(), + 'durationTime' => $queryResultData->getTime()->getDuration(), ] ); + // iterate items in batch query result - foreach ($queryResultData->getResult() as $cnt => $listElement) { - $elementsCounter++; - if ($limit !== null && $elementsCounter > $limit) { - return; + if ($isCrmItemsInBatch) { + foreach ($queryResultData->getResult()['items'] as $cnt => $listElement) { + $elementsCounter++; + if ($limit !== null && $elementsCounter > $limit) { + return; + } + yield $listElement; + } + } else { + foreach ($queryResultData->getResult() as $cnt => $listElement) { + $elementsCounter++; + if ($limit !== null && $elementsCounter > $limit) { + return; + } + yield $listElement; } - yield $listElement; } + } $this->logger->debug('getTraversableList.finish'); } /** - * @param int $startElementId - * @param int $lastElementId - * @param bool $isLastPage + * @param string $keyId + * @param int $startElementId + * @param int $lastElementId + * @param bool $isLastPage * @param array $oldFilter * * @return array */ - protected function updateFilterForBatch(int $startElementId, int $lastElementId, bool $isLastPage, array $oldFilter): array + protected function updateFilterForBatch(string $keyId, int $startElementId, int $lastElementId, bool $isLastPage, array $oldFilter): array { $this->logger->debug('updateFilterForBatch.start', [ 'startElementId' => $startElementId, - 'lastElementId' => $lastElementId, - 'isLastPage' => $isLastPage, - 'oldFilter' => $oldFilter, + 'lastElementId' => $lastElementId, + 'isLastPage' => $isLastPage, + 'oldFilter' => $oldFilter, + 'key' => $keyId, ]); $filter = array_merge( $oldFilter, [ - '>=ID' => $startElementId, - $isLastPage ? '<=ID' : ' $lastElementId, + sprintf('>=%s', $keyId) => $startElementId, + $isLastPage ? sprintf('<=%s', $keyId) : sprintf('<%s', $keyId) => $lastElementId, ] ); $this->logger->debug('updateFilterForBatch.finish', [ @@ -544,11 +595,11 @@ protected function updateFilterForBatch(int $startElementId, int $lastElementId, * * work with start item position and elements count * - * @param string $apiMethod + * @param string $apiMethod * @param array $order - * @param array $filter - * @param array $select - * @param int|null $limit + * @param array $filter + * @param array $select + * @param int|null $limit * * @return Generator * @throws BaseException @@ -561,19 +612,20 @@ protected function updateFilterForBatch(int $startElementId, int $lastElementId, */ public function getTraversableListWithCount( string $apiMethod, - array $order, - array $filter, - array $select, - ?int $limit = null - ): Generator { + array $order, + array $filter, + array $select, + ?int $limit = null + ): Generator + { $this->logger->debug( 'getTraversableListWithCount.start', [ 'apiMethod' => $apiMethod, - 'order' => $order, - 'filter' => $filter, - 'select' => $select, - 'limit' => $limit, + 'order' => $order, + 'filter' => $filter, + 'select' => $select, + 'limit' => $limit, ] ); $this->clearCommands(); @@ -582,10 +634,10 @@ public function getTraversableListWithCount( $firstResult = $this->core->call( $apiMethod, [ - 'order' => $order, + 'order' => $order, 'filter' => $filter, 'select' => $select, - 'start' => 0, + 'start' => 0, ] ); @@ -595,7 +647,7 @@ public function getTraversableListWithCount( $this->logger->debug( 'getTraversableListWithCount.calculateCommandsRange', [ - 'nextItem' => $nextItem, + 'nextItem' => $nextItem, 'totalItems' => $total, ] ); @@ -606,10 +658,10 @@ public function getTraversableListWithCount( $this->registerCommand( $apiMethod, [ - 'order' => $order, + 'order' => $order, 'filter' => $filter, 'select' => $select, - 'start' => $startItem, + 'start' => $startItem, ] ); if ($limit !== null && $limit < $startItem) { @@ -621,10 +673,10 @@ public function getTraversableListWithCount( $this->registerCommand( $apiMethod, [ - 'order' => $order, + 'order' => $order, 'filter' => $filter, 'select' => $select, - 'start' => 0, + 'start' => 0, ] ); } @@ -632,7 +684,7 @@ public function getTraversableListWithCount( $this->logger->debug( 'getTraversableListWithCount.commandsRegistered', [ - 'commandsCount' => $this->commands->count(), + 'commandsCount' => $this->commands->count(), 'totalItemsToSelect' => $total, ] ); @@ -647,8 +699,8 @@ public function getTraversableListWithCount( 'getTraversableListWithCount.batchResultItem', [ 'batchCommandItemNumber' => $queryCnt, - 'nextItem' => $queryResultData->getPagination()->getNextItem(), - 'durationTime' => $queryResultData->getTime()->getDuration(), + 'nextItem' => $queryResultData->getPagination()->getNextItem(), + 'durationTime' => $queryResultData->getTime()->getDuration(), ] ); // iterate items in batch query result @@ -692,8 +744,8 @@ protected function getTraversable(bool $isHaltOnError): Generator $this->logger->debug( 'getTraversable.batchResultItem.processStart', [ - 'batchItemNumber' => $batchItem, - 'batchApiCommand' => $batchResult->getApiCommand()->getApiMethod(), + 'batchItemNumber' => $batchItem, + 'batchApiCommand' => $batchResult->getApiCommand()->getApiMethod(), 'batchApiCommandUuid' => $batchResult->getApiCommand()->getUuid()->toString(), ] ); @@ -765,7 +817,7 @@ private function getTraversableBatchResults(bool $isHaltOnError): Generator 'getTraversableBatchResults.batchQuery', [ 'batchQueryNumber' => $batchQueryCounter, - 'queriesCount' => count($batchQuery), + 'queriesCount' => count($batchQuery), ] ); // batch call diff --git a/src/Core/BulkItemsReader/BulkItemsReaderBuilder.php b/src/Core/BulkItemsReader/BulkItemsReaderBuilder.php index 32e99a0e..0c0fa8ea 100644 --- a/src/Core/BulkItemsReader/BulkItemsReaderBuilder.php +++ b/src/Core/BulkItemsReader/BulkItemsReaderBuilder.php @@ -5,7 +5,7 @@ namespace Bitrix24\SDK\Core\BulkItemsReader; use Bitrix24\SDK\Core\BulkItemsReader\ReadStrategies\FilterWithBatchWithoutCountOrder; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Contracts\BulkItemsReaderInterface; use Bitrix24\SDK\Core\Contracts\CoreInterface; use Psr\Log\LoggerInterface; @@ -13,16 +13,16 @@ class BulkItemsReaderBuilder { protected CoreInterface $core; - protected BatchInterface $batch; + protected BatchOperationsInterface $batch; protected LoggerInterface $logger; protected BulkItemsReaderInterface $readStrategy; /** - * @param \Bitrix24\SDK\Core\Contracts\CoreInterface $core - * @param \Bitrix24\SDK\Core\Contracts\BatchInterface $batch - * @param \Psr\Log\LoggerInterface $logger + * @param \Bitrix24\SDK\Core\Contracts\CoreInterface $core + * @param \Bitrix24\SDK\Core\Contracts\BatchOperationsInterface $batch + * @param \Psr\Log\LoggerInterface $logger */ - public function __construct(CoreInterface $core, BatchInterface $batch, LoggerInterface $logger) + public function __construct(CoreInterface $core, BatchOperationsInterface $batch, LoggerInterface $logger) { $this->core = $core; $this->batch = $batch; diff --git a/src/Core/BulkItemsReader/ReadStrategies/FilterWithBatchWithoutCountOrder.php b/src/Core/BulkItemsReader/ReadStrategies/FilterWithBatchWithoutCountOrder.php index ef58f410..b8eed12d 100644 --- a/src/Core/BulkItemsReader/ReadStrategies/FilterWithBatchWithoutCountOrder.php +++ b/src/Core/BulkItemsReader/ReadStrategies/FilterWithBatchWithoutCountOrder.php @@ -4,21 +4,21 @@ namespace Bitrix24\SDK\Core\BulkItemsReader\ReadStrategies; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Contracts\BulkItemsReaderInterface; use Generator; use Psr\Log\LoggerInterface; class FilterWithBatchWithoutCountOrder implements BulkItemsReaderInterface { - private BatchInterface $batch; + private BatchOperationsInterface $batch; private LoggerInterface $log; /** - * @param \Bitrix24\SDK\Core\Contracts\BatchInterface $batch - * @param \Psr\Log\LoggerInterface $log + * @param \Bitrix24\SDK\Core\Contracts\BatchOperationsInterface $batch + * @param \Psr\Log\LoggerInterface $log */ - public function __construct(BatchInterface $batch, LoggerInterface $log) + public function __construct(BatchOperationsInterface $batch, LoggerInterface $log) { $this->batch = $batch; $this->log = $log; diff --git a/src/Core/Contracts/BatchInterface.php b/src/Core/Contracts/BatchOperationsInterface.php similarity index 90% rename from src/Core/Contracts/BatchInterface.php rename to src/Core/Contracts/BatchOperationsInterface.php index b6f0744e..f6e5b55c 100644 --- a/src/Core/Contracts/BatchInterface.php +++ b/src/Core/Contracts/BatchOperationsInterface.php @@ -9,21 +9,21 @@ use Generator; /** - * Interface BatchInterface + * Interface BatchOperationsInterface * * @package Bitrix24\SDK\Core\Contracts */ -interface BatchInterface +interface BatchOperationsInterface { /** * Batch wrapper for *.list methods without counting elements on every api-call * - * @param string $apiMethod + * @param string $apiMethod * @param array $order * @param array $filter * @param array $select - * @param int|null $limit - * + * @param int|null $limit + * @param array|null $additionalParameters * @return Generator * @throws BaseException */ @@ -32,7 +32,8 @@ public function getTraversableList( array $order, array $filter, array $select, - ?int $limit = null + ?int $limit = null, + ?array $additionalParameters = null ): Generator; /** diff --git a/src/Core/Core.php b/src/Core/Core.php index 688bbe26..73511395 100644 --- a/src/Core/Core.php +++ b/src/Core/Core.php @@ -7,6 +7,7 @@ use Bitrix24\SDK\Core\Commands\Command; use Bitrix24\SDK\Core\Contracts\ApiClientInterface; use Bitrix24\SDK\Core\Contracts\CoreInterface; +use Bitrix24\SDK\Core\Exceptions\AuthForbiddenException; use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Exceptions\TransportException; use Bitrix24\SDK\Core\Response\Response; @@ -32,17 +33,18 @@ class Core implements CoreInterface /** * Main constructor. * - * @param ApiClientInterface $apiClient - * @param ApiLevelErrorHandler $apiLevelErrorHandler + * @param ApiClientInterface $apiClient + * @param ApiLevelErrorHandler $apiLevelErrorHandler * @param EventDispatcherInterface $eventDispatcher - * @param LoggerInterface $logger + * @param LoggerInterface $logger */ public function __construct( - ApiClientInterface $apiClient, - ApiLevelErrorHandler $apiLevelErrorHandler, + ApiClientInterface $apiClient, + ApiLevelErrorHandler $apiLevelErrorHandler, EventDispatcherInterface $eventDispatcher, - LoggerInterface $logger - ) { + LoggerInterface $logger + ) + { $this->apiClient = $apiClient; $this->apiLevelErrorHandler = $apiLevelErrorHandler; $this->eventDispatcher = $eventDispatcher; @@ -51,7 +53,7 @@ public function __construct( /** * @param string $apiMethod - * @param array $parameters + * @param array $parameters * * @return Response * @throws BaseException @@ -62,7 +64,7 @@ public function call(string $apiMethod, array $parameters = []): Response $this->logger->debug( 'call.start', [ - 'method' => $apiMethod, + 'method' => $apiMethod, 'parameters' => $parameters, ] ); @@ -80,7 +82,7 @@ public function call(string $apiMethod, array $parameters = []): Response switch ($apiCallResponse->getStatusCode()) { case StatusCodeInterface::STATUS_OK: //todo check with empty response size from server - $response = new Response($apiCallResponse, new Command($apiMethod, $parameters), $this->logger); + $response = new Response($apiCallResponse, new Command($apiMethod, $parameters), $this->apiLevelErrorHandler, $this->logger); break; case StatusCodeInterface::STATUS_FOUND: // change domain url @@ -98,9 +100,9 @@ public function call(string $apiMethod, array $parameters = []): Response $this->logger->debug( 'api call repeated to new domain url', [ - 'domainUrl' => $portalNewDomainUrlHost, + 'domainUrl' => $portalNewDomainUrlHost, 'repeatedApiMethod' => $apiMethod, - 'httpStatusCode' => $response->getHttpResponse()->getStatusCode(), + 'httpStatusCode' => $response->getHttpResponse()->getStatusCode(), ] ); // dispatch event, application listeners update domain url host in accounts repository @@ -121,10 +123,10 @@ public function call(string $apiMethod, array $parameters = []): Response $this->logger->debug( 'access token renewed', [ - 'newAccessToken' => $renewedToken->getAccessToken()->getAccessToken(), + 'newAccessToken' => $renewedToken->getAccessToken()->getAccessToken(), 'newRefreshToken' => $renewedToken->getAccessToken()->getRefreshToken(), - 'newExpires' => $renewedToken->getAccessToken()->getExpires(), - 'appStatus' => $renewedToken->getApplicationStatus(), + 'newExpires' => $renewedToken->getAccessToken()->getExpires(), + 'appStatus' => $renewedToken->getApplicationStatus(), ] ); $this->apiClient->getCredentials()->setAccessToken($renewedToken->getAccessToken()); @@ -135,7 +137,7 @@ public function call(string $apiMethod, array $parameters = []): Response 'api call repeated', [ 'repeatedApiMethod' => $apiMethod, - 'httpStatusCode' => $response->getHttpResponse()->getStatusCode(), + 'httpStatusCode' => $response->getHttpResponse()->getStatusCode(), ] ); @@ -145,6 +147,15 @@ public function call(string $apiMethod, array $parameters = []): Response throw new BaseException('UNAUTHORIZED request error'); } break; + case StatusCodeInterface::STATUS_FORBIDDEN: + $this->logger->warning( + 'bitrix24 portal authorisation forbidden', + [ + 'apiMethod' => $apiMethod, + 'b24DomainUrl' => $this->apiClient->getCredentials()->getDomainUrl(), + ] + ); + throw new AuthForbiddenException(sprintf('authorisation forbidden for portal %s ', $this->apiClient->getCredentials()->getDomainUrl())); case StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE: $body = $apiCallResponse->toArray(false); $this->logger->notice( @@ -161,7 +172,7 @@ public function call(string $apiMethod, array $parameters = []): Response 'unhandled server status', [ 'httpStatus' => $apiCallResponse->getStatusCode(), - 'body' => $body, + 'body' => $body, ] ); $this->apiLevelErrorHandler->handle($body); @@ -172,7 +183,7 @@ public function call(string $apiMethod, array $parameters = []): Response $this->logger->error( 'call.transportException', [ - 'trace' => $exception->getTrace(), + 'trace' => $exception->getTrace(), 'message' => $exception->getMessage(), ] ); @@ -185,7 +196,7 @@ public function call(string $apiMethod, array $parameters = []): Response 'call.unknownException', [ 'message' => $exception->getMessage(), - 'trace' => $exception->getTrace(), + 'trace' => $exception->getTrace(), ] ); throw new BaseException(sprintf('unknown error - %s', $exception->getMessage()), $exception->getCode(), $exception); diff --git a/src/Core/CoreBuilder.php b/src/Core/CoreBuilder.php index e188ae62..8f0d7960 100644 --- a/src/Core/CoreBuilder.php +++ b/src/Core/CoreBuilder.php @@ -9,6 +9,8 @@ use Bitrix24\SDK\Core\Credentials\Credentials; use Bitrix24\SDK\Core\Credentials\WebhookUrl; use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\DefaultRequestIdGenerator; +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\RequestIdGeneratorInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -23,12 +25,13 @@ */ class CoreBuilder { - protected ?ApiClientInterface $apiClient; - protected HttpClientInterface $httpClient; - protected EventDispatcherInterface $eventDispatcher; - protected LoggerInterface $logger; - protected ?Credentials $credentials; - protected ApiLevelErrorHandler $apiLevelErrorHandler; + private ?ApiClientInterface $apiClient; + private HttpClientInterface $httpClient; + private EventDispatcherInterface $eventDispatcher; + private LoggerInterface $logger; + private ?Credentials $credentials; + private ApiLevelErrorHandler $apiLevelErrorHandler; + private RequestIdGeneratorInterface $requestIdGenerator; /** * CoreBuilder constructor. @@ -46,6 +49,12 @@ public function __construct() $this->credentials = null; $this->apiClient = null; $this->apiLevelErrorHandler = new ApiLevelErrorHandler($this->logger); + $this->requestIdGenerator = new DefaultRequestIdGenerator(); + } + + public function withRequestIdGenerator(RequestIdGeneratorInterface $requestIdGenerator): void + { + $this->requestIdGenerator = $requestIdGenerator; } /** @@ -60,11 +69,6 @@ public function withCredentials(Credentials $credentials): self return $this; } - /** - * @param ApiClientInterface $apiClient - * - * @return $this - */ public function withApiClient(ApiClientInterface $apiClient): self { $this->apiClient = $apiClient; @@ -72,11 +76,13 @@ public function withApiClient(ApiClientInterface $apiClient): self return $this; } - /** - * @param LoggerInterface $logger - * - * @return $this - */ + public function withHttpClient(HttpClientInterface $httpClient):self + { + $this->httpClient = $httpClient; + + return $this; + } + public function withLogger(LoggerInterface $logger): self { $this->logger = $logger; @@ -84,11 +90,6 @@ public function withLogger(LoggerInterface $logger): self return $this; } - /** - * @param EventDispatcherInterface $eventDispatcher - * - * @return $this - */ public function withEventDispatcher(EventDispatcherInterface $eventDispatcher): self { $this->eventDispatcher = $eventDispatcher; @@ -97,7 +98,6 @@ public function withEventDispatcher(EventDispatcherInterface $eventDispatcher): } /** - * @return CoreInterface * @throws InvalidArgumentException */ public function build(): CoreInterface @@ -110,6 +110,7 @@ public function build(): CoreInterface $this->apiClient = new ApiClient( $this->credentials, $this->httpClient, + $this->requestIdGenerator, $this->logger ); } diff --git a/src/Core/Credentials/Scope.php b/src/Core/Credentials/Scope.php index 7a6117bb..d028e1e7 100644 --- a/src/Core/Credentials/Scope.php +++ b/src/Core/Credentials/Scope.php @@ -6,11 +6,6 @@ use Bitrix24\SDK\Core\Exceptions\UnknownScopeCodeException; -/** - * Class Scope - * - * @package Bitrix24\SDK\Core\Credentials - */ class Scope { /** @@ -18,6 +13,7 @@ class Scope */ protected array $availableScope = [ 'bizproc', + 'biconnector', 'calendar', 'call', 'cashbox', @@ -83,9 +79,14 @@ class Scope public function __construct(array $scope = []) { $scope = array_unique(array_map('strtolower', $scope)); - foreach ($scope as $item) { - if (!in_array($item, $this->availableScope, true)) { - throw new UnknownScopeCodeException(sprintf('unknown application scope code - %s', $item)); + + if (count($scope) === 1 && $scope[0] === '') { + $scope = []; + } else { + foreach ($scope as $item) { + if (!in_array($item, $this->availableScope, true)) { + throw new UnknownScopeCodeException(sprintf('unknown application scope code - %s', $item)); + } } } @@ -99,4 +100,12 @@ public function getScopeCodes(): array { return $this->currentScope; } + + /** + * @throws \Bitrix24\SDK\Core\Exceptions\UnknownScopeCodeException + */ + public static function initFromString(string $scope): self + { + return new self(str_replace(' ', '', explode(',', $scope))); + } } \ No newline at end of file diff --git a/src/Core/Exceptions/AuthForbiddenException.php b/src/Core/Exceptions/AuthForbiddenException.php new file mode 100644 index 00000000..2d444876 --- /dev/null +++ b/src/Core/Exceptions/AuthForbiddenException.php @@ -0,0 +1,9 @@ +start = $start; $this->finish = $finish; $this->duration = $duration; @@ -139,7 +140,7 @@ public static function initFromResponse(array $response): self (float)$response['finish'], (float)$response['duration'], (float)$response['processing'], - (float)$response['operating'], + array_key_exists('operating', $response) ? (float)$response['operating'] : 0, new DateTimeImmutable($response['date_start']), new DateTimeImmutable($response['date_finish']), $response['operating_reset_at'] ?? null diff --git a/src/Core/Response/Response.php b/src/Core/Response/Response.php index 75566327..d25c64a6 100644 --- a/src/Core/Response/Response.php +++ b/src/Core/Response/Response.php @@ -4,6 +4,7 @@ namespace Bitrix24\SDK\Core\Response; +use Bitrix24\SDK\Core\ApiLevelErrorHandler; use Bitrix24\SDK\Core\Commands\Command; use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Response\DTO; @@ -23,19 +24,24 @@ class Response protected ResponseInterface $httpResponse; protected ?DTO\ResponseData $responseData; protected Command $apiCommand; + protected ApiLevelErrorHandler $apiLevelErrorHandler; protected LoggerInterface $logger; /** * Response constructor. * * @param ResponseInterface $httpResponse - * @param Command $apiCommand - * @param LoggerInterface $logger + * @param Command $apiCommand + * @param ApiLevelErrorHandler $apiLevelErrorHandler + * @param LoggerInterface $logger */ - public function __construct(ResponseInterface $httpResponse, Command $apiCommand, LoggerInterface $logger) + public function __construct(ResponseInterface $httpResponse, Command $apiCommand, + ApiLevelErrorHandler $apiLevelErrorHandler, + LoggerInterface $logger) { $this->httpResponse = $httpResponse; $this->apiCommand = $apiCommand; + $this->apiLevelErrorHandler = $apiLevelErrorHandler; $this->logger = $logger; $this->responseData = null; } @@ -64,16 +70,16 @@ public function __destruct() $restTimings = null; if ($this->responseData !== null) { $restTimings = [ - 'rest_query_duration' => $this->responseData->getTime()->getDuration(), + 'rest_query_duration' => $this->responseData->getTime()->getDuration(), 'rest_query_processing' => $this->responseData->getTime()->getProcessing(), - 'rest_query_start' => $this->responseData->getTime()->getStart(), - 'rest_query_finish' => $this->responseData->getTime()->getFinish(), + 'rest_query_start' => $this->responseData->getTime()->getStart(), + 'rest_query_finish' => $this->responseData->getTime()->getFinish(), ]; } $this->logger->info('Response.TransportInfo', [ - 'restTimings' => $restTimings, + 'restTimings' => $restTimings, 'networkTimings' => (new NetworkTimingsParser($this->httpResponse->getInfo()))->toArrayWithMicroseconds(), - 'responseInfo' => (new ResponseInfoParser($this->httpResponse->getInfo()))->toArray(), + 'responseInfo' => (new ResponseInfoParser($this->httpResponse->getInfo()))->toArray(), ]); } @@ -92,8 +98,9 @@ public function getResponseData(): DTO\ResponseData $this->logger->info('getResponseData.responseBody', [ 'responseBody' => $responseResult, ]); + // try to handle api-level errors - $this->handleApiLevelErrors($responseResult); + $this->apiLevelErrorHandler->handle($responseResult); if (!is_array($responseResult['result'])) { $responseResult['result'] = [$responseResult['result']]; @@ -143,50 +150,4 @@ private function getHttpResponseContent(): ?string return $content; } - - /** - * @param array $apiResponse - */ - private function handleApiLevelErrors(array $apiResponse): void - { - $this->logger->debug('handleApiLevelErrors.start'); - - if (array_key_exists('error', $apiResponse)) { - $errorMsg = sprintf( - '%s - %s ', - $apiResponse['error'], - (array_key_exists('error_description', $apiResponse) ? $apiResponse['error_description'] : ''), - ); -// todo check api-level error codes -// -// switch (strtoupper(trim($apiResponse['error']))) { -// case 'EXPIRED_TOKEN': -// throw new Bitrix24TokenIsExpiredException($errorMsg); -// case 'WRONG_CLIENT': -// case 'ERROR_OAUTH': -// $this->log->error($errorMsg, $this->getErrorContext()); -// throw new Bitrix24WrongClientException($errorMsg); -// case 'ERROR_METHOD_NOT_FOUND': -// $this->log->error($errorMsg, $this->getErrorContext()); -// throw new Bitrix24MethodNotFoundException($errorMsg); -// case 'INVALID_TOKEN': -// case 'INVALID_GRANT': -// $this->log->error($errorMsg, $this->getErrorContext()); -// throw new Bitrix24TokenIsInvalidException($errorMsg); - -// case 'PAYMENT_REQUIRED': -// $this->log->error($errorMsg, $this->getErrorContext()); -// throw new Bitrix24PaymentRequiredException($errorMsg); -// case 'NO_AUTH_FOUND': -// $this->log->error($errorMsg, $this->getErrorContext()); -// throw new Bitrix24PortalRenamedException($errorMsg); -// case 'INSUFFICIENT_SCOPE': -// $this->log->error($errorMsg, $this->getErrorContext()); -// throw new Bitrix24InsufficientScope($errorMsg); -// default: -// $this->log->error($errorMsg, $this->getErrorContext()); -// throw new Bitrix24ApiException($errorMsg); - } - $this->logger->debug('handleApiLevelErrors.finish'); - } } \ No newline at end of file diff --git a/src/Infrastructure/HttpClient/RequestId/DefaultRequestIdGenerator.php b/src/Infrastructure/HttpClient/RequestId/DefaultRequestIdGenerator.php new file mode 100644 index 00000000..9b49016d --- /dev/null +++ b/src/Infrastructure/HttpClient/RequestId/DefaultRequestIdGenerator.php @@ -0,0 +1,55 @@ +toRfc4122(); + } + + private function findExists(): ?string + { + $candidate = null; + foreach (self::KEY_NAME_VARIANTS as $key) { + if (!empty($_SERVER[$key])) { + $candidate = $_SERVER[$key]; + break; + } + } + return $candidate; + } + + public function getRequestId(): string + { + $reqId = $this->findExists(); + if ($reqId === null) { + $reqId = $this->generate(); + } + return $reqId; + } + + public function getHeaderFieldName(): string + { + return self::DEFAULT_REQUEST_ID_HEADER_FIELD_NAME; + } +} \ No newline at end of file diff --git a/src/Infrastructure/HttpClient/RequestId/RequestIdGeneratorInterface.php b/src/Infrastructure/HttpClient/RequestId/RequestIdGeneratorInterface.php new file mode 100644 index 00000000..77ba06b2 --- /dev/null +++ b/src/Infrastructure/HttpClient/RequestId/RequestIdGeneratorInterface.php @@ -0,0 +1,14 @@ +batch = $batch; $this->log = $log; diff --git a/src/Services/AbstractServiceBuilder.php b/src/Services/AbstractServiceBuilder.php index 186b5867..36c97565 100644 --- a/src/Services/AbstractServiceBuilder.php +++ b/src/Services/AbstractServiceBuilder.php @@ -5,7 +5,7 @@ namespace Bitrix24\SDK\Services; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Contracts\BulkItemsReaderInterface; use Bitrix24\SDK\Core\Contracts\CoreInterface; use Psr\Log\LoggerInterface; @@ -18,7 +18,7 @@ abstract class AbstractServiceBuilder { protected CoreInterface $core; - protected BatchInterface $batch; + protected BatchOperationsInterface $batch; protected BulkItemsReaderInterface $bulkItemsReader; protected LoggerInterface $log; protected array $serviceCache; @@ -27,13 +27,13 @@ abstract class AbstractServiceBuilder * AbstractServiceBuilder constructor. * * @param CoreInterface $core - * @param BatchInterface $batch + * @param BatchOperationsInterface $batch * @param \Bitrix24\SDK\Core\Contracts\BulkItemsReaderInterface $bulkItemsReader * @param LoggerInterface $log */ public function __construct( CoreInterface $core, - BatchInterface $batch, + BatchOperationsInterface $batch, BulkItemsReaderInterface $bulkItemsReader, LoggerInterface $log ) { diff --git a/src/Services/CRM/CRMServiceBuilder.php b/src/Services/CRM/CRMServiceBuilder.php index 7bd84b03..49678877 100644 --- a/src/Services/CRM/CRMServiceBuilder.php +++ b/src/Services/CRM/CRMServiceBuilder.php @@ -5,22 +5,9 @@ namespace Bitrix24\SDK\Services\CRM; use Bitrix24\SDK\Services\AbstractServiceBuilder; -use Bitrix24\SDK\Services\CRM\Contact; -use Bitrix24\SDK\Services\CRM\Deal; -use Bitrix24\SDK\Services\CRM\Product; -use Bitrix24\SDK\Services\CRM\Settings; - -/** - * Class CRMServiceBuilder - * - * @package Bitrix24\SDK\Services\CRM - */ class CRMServiceBuilder extends AbstractServiceBuilder { - /** - * @return Settings\Service\Settings - */ public function settings(): Settings\Service\Settings { if (!isset($this->serviceCache[__METHOD__])) { @@ -30,9 +17,6 @@ public function settings(): Settings\Service\Settings return $this->serviceCache[__METHOD__]; } - /** - * @return Deal\Service\DealContact - */ public function dealContact(): Deal\Service\DealContact { if (!isset($this->serviceCache[__METHOD__])) { @@ -42,9 +26,6 @@ public function dealContact(): Deal\Service\DealContact return $this->serviceCache[__METHOD__]; } - /** - * @return Deal\Service\DealCategory - */ public function dealCategory(): Deal\Service\DealCategory { if (!isset($this->serviceCache[__METHOD__])) { @@ -54,9 +35,6 @@ public function dealCategory(): Deal\Service\DealCategory return $this->serviceCache[__METHOD__]; } - /** - * @return Deal\Service\Deal - */ public function deal(): Deal\Service\Deal { if (!isset($this->serviceCache[__METHOD__])) { @@ -70,9 +48,6 @@ public function deal(): Deal\Service\Deal return $this->serviceCache[__METHOD__]; } - /** - * @return \Bitrix24\SDK\Services\CRM\Deal\Service\DealUserfield - */ public function dealUserfield(): Deal\Service\DealUserfield { if (!isset($this->serviceCache[__METHOD__])) { @@ -85,9 +60,6 @@ public function dealUserfield(): Deal\Service\DealUserfield return $this->serviceCache[__METHOD__]; } - /** - * @return Contact\Service\Contact - */ public function contact(): Contact\Service\Contact { if (!isset($this->serviceCache[__METHOD__])) { @@ -101,9 +73,6 @@ public function contact(): Contact\Service\Contact return $this->serviceCache[__METHOD__]; } - /** - * @return \Bitrix24\SDK\Services\CRM\Contact\Service\ContactUserfield - */ public function contactUserfield(): Contact\Service\ContactUserfield { if (!isset($this->serviceCache[__METHOD__])) { @@ -128,9 +97,6 @@ public function dealProductRows(): Deal\Service\DealProductRows return $this->serviceCache[__METHOD__]; } - /** - * @return Deal\Service\DealCategoryStage - */ public function dealCategoryStage(): Deal\Service\DealCategoryStage { if (!isset($this->serviceCache[__METHOD__])) { @@ -140,9 +106,6 @@ public function dealCategoryStage(): Deal\Service\DealCategoryStage return $this->serviceCache[__METHOD__]; } - /** - * @return Product\Service\Product - */ public function product(): Product\Service\Product { if (!isset($this->serviceCache[__METHOD__])) { @@ -156,9 +119,6 @@ public function product(): Product\Service\Product return $this->serviceCache[__METHOD__]; } - /** - * @return Userfield\Service\Userfield - */ public function userfield(): Userfield\Service\Userfield { if (!isset($this->serviceCache[__METHOD__])) { @@ -171,9 +131,6 @@ public function userfield(): Userfield\Service\Userfield return $this->serviceCache[__METHOD__]; } - /** - * @return Lead\Service\Lead - */ public function lead(): Lead\Service\Lead { if (!isset($this->serviceCache[__METHOD__])) { @@ -187,9 +144,6 @@ public function lead(): Lead\Service\Lead return $this->serviceCache[__METHOD__]; } - /** - * @return Activity\Service\Activity - */ public function activity(): Activity\Service\Activity { if (!isset($this->serviceCache[__METHOD__])) { @@ -203,9 +157,6 @@ public function activity(): Activity\Service\Activity return $this->serviceCache[__METHOD__]; } - /** - * @return Activity\ActivityFetcherBuilder - */ public function activityFetcher(): Activity\ActivityFetcherBuilder { if (!isset($this->serviceCache[__METHOD__])) { @@ -219,4 +170,29 @@ public function activityFetcher(): Activity\ActivityFetcherBuilder return $this->serviceCache[__METHOD__]; } + + public function item(): Item\Service\Item + { + if (!isset($this->serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new Item\Service\Item( + new Item\Service\Batch($this->batch, $this->log), + $this->core, + $this->log + ); + } + + return $this->serviceCache[__METHOD__]; + } + + public function duplicate(): Duplicates\Service\Duplicate + { + if (!isset($this->serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new Duplicates\Service\Duplicate( + $this->core, + $this->log + ); + } + + return $this->serviceCache[__METHOD__]; + } } \ No newline at end of file diff --git a/src/Services/CRM/Common/Result/AbstractCrmItem.php b/src/Services/CRM/Common/Result/AbstractCrmItem.php index 0737315f..f7d9c321 100644 --- a/src/Services/CRM/Common/Result/AbstractCrmItem.php +++ b/src/Services/CRM/Common/Result/AbstractCrmItem.php @@ -5,67 +5,173 @@ namespace Bitrix24\SDK\Services\CRM\Common\Result; use Bitrix24\SDK\Core\Result\AbstractItem; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\Email; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\InstantMessenger; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\Phone; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\PhoneValueType; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\Website; use Bitrix24\SDK\Services\CRM\Userfield\Exceptions\UserfieldNotFoundException; use DateTimeImmutable; +use Money\Currency; +use Money\Money; class AbstractCrmItem extends AbstractItem { - private const CRM_USERFIELD_PREFIX = 'UF_CRM_'; + private const string CRM_USERFIELD_PREFIX = 'UF_CRM_'; + + /** + * @var Currency + */ + private Currency $currency; + + public function __construct(array $data, Currency $currency = null) + { + parent::__construct($data); + if ($currency !== null) { + $this->currency = $currency; + } + } /** * @param int|string $offset * - * @return bool|\DateTimeImmutable|int|mixed|null + * @return bool|DateTimeImmutable|int|mixed|null */ + public function __get($offset) { // todo унести в отдельный класс и покрыть тестами + // учитывать требования + // - поддержка пользовательских полей с пользовательскими типами + // - поддержка пользовательских полей со встроенными типами + // - расширяемость для пользовательских полей в клиентском коде + // - хранение связи поле-тип в аннотациях? + // приведение полей к реальным типам данных для основных сущностей CRM switch ($offset) { case 'ID': case 'ASSIGNED_BY_ID': case 'CREATED_BY_ID': case 'MODIFY_BY_ID': + case 'createdBy': + case 'updatedBy': + case 'movedBy': + case 'begindate': + case 'closedate': + case 'opportunity': + case 'opportunityAccount': + case 'taxValueAccount': + case 'taxValue': // deal case 'LEAD_ID': case 'CONTACT_ID': case 'QUOTE_ID': + // productRow + case 'OWNER_ID': + // DealCategoryItem + case 'SORT': + case 'id': + case 'categoryId': + case 'webformId': + case 'assignedById': + case 'contactId': + case 'lastActivityBy': if ($this->data[$offset] !== '' && $this->data[$offset] !== null) { return (int)$this->data[$offset]; } return null; case 'COMPANY_ID': + case 'companyId': + case 'mycompanyId': if ($this->data[$offset] !== '' && $this->data[$offset] !== null && $this->data[$offset] !== '0') { return (int)$this->data[$offset]; } - return null; - // contact case 'EXPORT': case 'HAS_PHONE': case 'HAS_EMAIL': case 'HAS_IMOL': case 'OPENED': - // deal + case 'opened': case 'IS_MANUAL_OPPORTUNITY': + case 'isManualOpportunity': case 'CLOSED': case 'IS_NEW': + case 'IS_LOCKED': case 'IS_RECURRING': case 'IS_RETURN_CUSTOMER': case 'IS_REPEATED_APPROACH': return $this->data[$offset] === 'Y'; case 'DATE_CREATE': + case 'CREATED_DATE': case 'DATE_MODIFY': case 'BIRTHDATE': case 'BEGINDATE': case 'CLOSEDATE': + case 'createdTime': + case 'updatedTime': + case 'movedTime': + case 'lastActivityTime': if ($this->data[$offset] !== '') { return DateTimeImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]); } return null; + // deal + case 'PRICE_EXCLUSIVE': + case 'PRICE_NETTO': + case 'PRICE_BRUTTO': + case 'PRICE': + if ($this->data[$offset] !== '' && $this->data[$offset] !== null) { + $var = $this->data[$offset] * 100; + return new Money((string)$var, new Currency($this->currency->getCode())); + } + return null; + case 'PHONE': + if (!$this->isKeyExists($offset)) { + return []; + } + + $items = []; + foreach ($this->data[$offset] as $item) { + $items[] = new Phone($item); + } + return $items; + case 'EMAIL': + if (!$this->isKeyExists($offset)) { + return []; + } + + $items = []; + foreach ($this->data[$offset] as $item) { + $items[] = new Email($item); + } + return $items; + case 'WEB': + if (!$this->isKeyExists($offset)) { + return []; + } + + $items = []; + foreach ($this->data[$offset] as $item) { + $items[] = new Website($item); + } + return $items; + case 'IM': + if (!$this->isKeyExists($offset)) { + return []; + } + + $items = []; + foreach ($this->data[$offset] as $item) { + $items[] = new InstantMessenger($item); + } + return $items; + case 'currencyId': + case 'accountCurrencyId': + return new Currency($this->data[$offset]); default: return $this->data[$offset] ?? null; } @@ -77,11 +183,13 @@ public function __get($offset) * @param string $fieldName * * @return mixed|null - * @throws \Bitrix24\SDK\Services\CRM\Userfield\Exceptions\UserfieldNotFoundException + * @throws UserfieldNotFoundException */ protected function getKeyWithUserfieldByFieldName(string $fieldName) { - $fieldName = self::CRM_USERFIELD_PREFIX . $fieldName; + if (!str_starts_with($fieldName, self::CRM_USERFIELD_PREFIX)) { + $fieldName = self::CRM_USERFIELD_PREFIX . $fieldName; + } if (!$this->isKeyExists($fieldName)) { throw new UserfieldNotFoundException(sprintf('crm userfield not found by field name %s', $fieldName)); } diff --git a/src/Services/CRM/Common/Result/SystemFields/Types/Email.php b/src/Services/CRM/Common/Result/SystemFields/Types/Email.php new file mode 100644 index 00000000..603cdae9 --- /dev/null +++ b/src/Services/CRM/Common/Result/SystemFields/Types/Email.php @@ -0,0 +1,25 @@ + $this->data[$offset], + 'ID' => (int)$this->data['ID'], + 'VALUE_TYPE' => EmailValueType::from($this->data['VALUE_TYPE']), + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/CRM/Common/Result/SystemFields/Types/EmailValueType.php b/src/Services/CRM/Common/Result/SystemFields/Types/EmailValueType.php new file mode 100644 index 00000000..5f3bc5b2 --- /dev/null +++ b/src/Services/CRM/Common/Result/SystemFields/Types/EmailValueType.php @@ -0,0 +1,13 @@ + $this->data[$offset], + 'ID' => (int)$this->data['ID'], + 'VALUE_TYPE' => EmailValueType::from($this->data['VALUE_TYPE']), + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/CRM/Common/Result/SystemFields/Types/InstantMessengerValueType.php b/src/Services/CRM/Common/Result/SystemFields/Types/InstantMessengerValueType.php new file mode 100644 index 00000000..87b5c223 --- /dev/null +++ b/src/Services/CRM/Common/Result/SystemFields/Types/InstantMessengerValueType.php @@ -0,0 +1,22 @@ + $this->data[$offset], + 'ID' => (int)$this->data['ID'], + 'VALUE_TYPE' => PhoneValueType::from($this->data['VALUE_TYPE']), + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/CRM/Common/Result/SystemFields/Types/PhoneValueType.php b/src/Services/CRM/Common/Result/SystemFields/Types/PhoneValueType.php new file mode 100644 index 00000000..8d9636b4 --- /dev/null +++ b/src/Services/CRM/Common/Result/SystemFields/Types/PhoneValueType.php @@ -0,0 +1,16 @@ + $this->data[$offset], + 'ID' => (int)$this->data['ID'], + 'VALUE_TYPE' => EmailValueType::from($this->data['VALUE_TYPE']), + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/CRM/Common/Result/SystemFields/Types/WebsiteValueType.php b/src/Services/CRM/Common/Result/SystemFields/Types/WebsiteValueType.php new file mode 100644 index 00000000..dd9f322d --- /dev/null +++ b/src/Services/CRM/Common/Result/SystemFields/Types/WebsiteValueType.php @@ -0,0 +1,16 @@ + * @throws BaseException @@ -210,6 +211,26 @@ public function add(array $contacts): Generator } } + /** + * Batch update contacts + * + * Update elements in array with structure + * element_id => [ // contact id + * 'fields' => [], // contact fields to update + * 'params' => [] + * ] + * + * @param array $entityItems + * @return Generator + * @throws BaseException + */ + public function update(array $entityItems): Generator + { + foreach ($this->batch->updateEntityItems('crm.contact.update', $entityItems) as $key => $item) { + yield $key => new UpdatedItemBatchResult($item); + } + } + /** * Batch delete contact items * diff --git a/src/Services/CRM/Contact/Service/Contact.php b/src/Services/CRM/Contact/Service/Contact.php index 211d6b6f..6776dac6 100644 --- a/src/Services/CRM/Contact/Service/Contact.php +++ b/src/Services/CRM/Contact/Service/Contact.php @@ -104,7 +104,7 @@ public function __construct(Batch $batch, CoreInterface $core, LoggerInterface $ public function add(array $fields, array $params = ['REGISTER_SONET_EVENT' => 'N']): AddedItemResult { return new AddedItemResult( - $result = $this->core->call( + $this->core->call( 'crm.contact.add', [ 'fields' => $fields, diff --git a/src/Services/CRM/Deal/DealStageSemanticId.php b/src/Services/CRM/Deal/DealStageSemanticId.php new file mode 100644 index 00000000..e591b956 --- /dev/null +++ b/src/Services/CRM/Deal/DealStageSemanticId.php @@ -0,0 +1,18 @@ +currency = $currency; + } + /** * @return DealProductRowItemResult[] * @throws BaseException @@ -22,8 +32,14 @@ class DealProductRowItemsResult extends AbstractResult public function getProductRows(): array { $res = []; - foreach ($this->getCoreResponse()->getResponseData()->getResult() as $productRow) { - $res[] = new DealProductRowItemResult($productRow); + if(!empty($this->getCoreResponse()->getResponseData()->getResult()['result']['rows'])) { + foreach ($this->getCoreResponse()->getResponseData()->getResult()['result']['rows'] as $productRow) { + $res[] = new DealProductRowItemResult($productRow, $this->currency); + } + } else { + foreach ($this->getCoreResponse()->getResponseData()->getResult() as $productRow) { + $res[] = new DealProductRowItemResult($productRow, $this->currency); + } } return $res; diff --git a/src/Services/CRM/Deal/Service/Batch.php b/src/Services/CRM/Deal/Service/Batch.php index e5351495..6be17a0a 100644 --- a/src/Services/CRM/Deal/Service/Batch.php +++ b/src/Services/CRM/Deal/Service/Batch.php @@ -4,7 +4,7 @@ namespace Bitrix24\SDK\Services\CRM\Deal\Service; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Result\AddedItemBatchResult; use Bitrix24\SDK\Core\Result\DeletedItemBatchResult; @@ -20,16 +20,16 @@ */ class Batch { - protected BatchInterface $batch; + protected BatchOperationsInterface $batch; protected LoggerInterface $log; /** * Batch constructor. * - * @param BatchInterface $batch - * @param LoggerInterface $log + * @param BatchOperationsInterface $batch + * @param LoggerInterface $log */ - public function __construct(BatchInterface $batch, LoggerInterface $log) + public function __construct(BatchOperationsInterface $batch, LoggerInterface $log) { $this->batch = $batch; $this->log = $log; diff --git a/src/Services/CRM/Deal/Service/DealProductRows.php b/src/Services/CRM/Deal/Service/DealProductRows.php index 3704bd20..42cacb1d 100644 --- a/src/Services/CRM/Deal/Service/DealProductRows.php +++ b/src/Services/CRM/Deal/Service/DealProductRows.php @@ -9,6 +9,8 @@ use Bitrix24\SDK\Core\Result\UpdatedItemResult; use Bitrix24\SDK\Services\AbstractService; use Bitrix24\SDK\Services\CRM\Deal\Result\DealProductRowItemsResult; +use Bitrix24\SDK\Services\CRM\Deal\Result\DealResult; +use Money\Currency; /** * Class DealProductRows @@ -23,23 +25,37 @@ class DealProductRows extends AbstractService * @link https://training.bitrix24.com/rest_help/crm/deals/crm_deal_productrows_get.php * * @param int $dealId - * - * @return DealProductRowItemsResult - * @throws BaseException - * @throws TransportException + * @param \Money\Currency|null $currency + * @return \Bitrix24\SDK\Services\CRM\Deal\Result\DealProductRowItemsResult + * @throws \Bitrix24\SDK\Core\Exceptions\BaseException + * @throws \Bitrix24\SDK\Core\Exceptions\TransportException */ - public function get(int $dealId): DealProductRowItemsResult + public function get(int $dealId, Currency $currency = null): DealProductRowItemsResult { + if ($currency === null) { + $res = $this->core->call('batch', [ + 'halt' => 0, + 'cmd' => [ + 'deal' => sprintf('crm.deal.get?ID=%s', $dealId), + 'rows' => sprintf('crm.deal.productrows.get?ID=%s', $dealId) + ], + ]); + $data = $res->getResponseData()->getResult(); + $currency = new Currency($data['result']['deal']['CURRENCY_ID']); + return new DealProductRowItemsResult($res,$currency); + } return new DealProductRowItemsResult( $this->core->call( 'crm.deal.productrows.get', [ 'id' => $dealId, ] - ) + ), + $currency ); } + /** * Creates or updates product entries inside the specified deal. * @@ -78,7 +94,7 @@ public function set(int $dealId, array $productRows): UpdatedItemResult $this->core->call( 'crm.deal.productrows.set', [ - 'id' => $dealId, + 'id' => $dealId, 'rows' => $productRows, ] ) diff --git a/src/Services/CRM/Duplicates/Result/DuplicateResult.php b/src/Services/CRM/Duplicates/Result/DuplicateResult.php new file mode 100644 index 00000000..dffe44ad --- /dev/null +++ b/src/Services/CRM/Duplicates/Result/DuplicateResult.php @@ -0,0 +1,50 @@ +getCoreResponse()->getResponseData()->getResult())) { + return false; + } + + if (count($this->getCoreResponse()->getResponseData()->getResult()['CONTACT']) > 1) { + return true; + } + + return false; + } + + public function hasOneContact(): bool + { + if (!array_key_exists('CONTACT', $this->getCoreResponse()->getResponseData()->getResult())) { + return false; + } + + if (count($this->getCoreResponse()->getResponseData()->getResult()['CONTACT']) === 1) { + return true; + } + + return false; + } + + /** + * @return array + * @throws BaseException + */ + public function getContactsId(): array + { + if (!array_key_exists('CONTACT', $this->getCoreResponse()->getResponseData()->getResult())) { + return []; + } + + return $this->getCoreResponse()->getResponseData()->getResult()['CONTACT']; + } +} \ No newline at end of file diff --git a/src/Services/CRM/Duplicates/Service/Duplicate.php b/src/Services/CRM/Duplicates/Service/Duplicate.php new file mode 100644 index 00000000..9b0b122d --- /dev/null +++ b/src/Services/CRM/Duplicates/Service/Duplicate.php @@ -0,0 +1,47 @@ + $phones + * @param EntityType|null $entityType + * @return DuplicateResult + * @throws BaseException + * @throws TransportException + */ + public function findByPhone(array $phones, ?EntityType $entityType = null): mixed + { + return new DuplicateResult($this->core->call('crm.duplicate.findbycomm', + [ + 'type' => 'PHONE', + 'values' => $phones, + 'entity_type' => $entityType?->value + ])); + } + + /** + * @param array $emails + * @param EntityType|null $entityType + * @return DuplicateResult + * @throws BaseException + * @throws TransportException + */ + public function findByEmail(array $emails, ?EntityType $entityType = null): DuplicateResult + { + return new DuplicateResult($this->core->call('crm.duplicate.findbycomm', + [ + 'type' => 'EMAIL', + 'values' => $emails, + 'entity_type' => $entityType?->value + ])); + } +} \ No newline at end of file diff --git a/src/Services/CRM/Duplicates/Service/EntityType.php b/src/Services/CRM/Duplicates/Service/EntityType.php new file mode 100644 index 00000000..3e5583ea --- /dev/null +++ b/src/Services/CRM/Duplicates/Service/EntityType.php @@ -0,0 +1,12 @@ +getCoreResponse()->getResponseData()->getResult()['item']); + } +} \ No newline at end of file diff --git a/src/Services/CRM/Item/Result/ItemsResult.php b/src/Services/CRM/Item/Result/ItemsResult.php new file mode 100644 index 00000000..978ffc0d --- /dev/null +++ b/src/Services/CRM/Item/Result/ItemsResult.php @@ -0,0 +1,25 @@ +getCoreResponse()->getResponseData()->getResult() as $item) { + $items[] = new ItemItemResult($item); + } + + return $items; + } +} \ No newline at end of file diff --git a/src/Services/CRM/Item/Service/Batch.php b/src/Services/CRM/Item/Service/Batch.php new file mode 100644 index 00000000..14ff222b --- /dev/null +++ b/src/Services/CRM/Item/Service/Batch.php @@ -0,0 +1,68 @@ +batch = $batch; + $this->log = $log; + } + + /** + * Batch list method for crm items + * + * @return Generator + * @throws BaseException + */ + public function list(int $entityTypeId, array $order, array $filter, array $select, ?int $limit = null): Generator + { + $this->log->debug( + 'batchList', + [ + 'entityTypeId' => $entityTypeId, + 'order' => $order, + 'filter' => $filter, + 'select' => $select, + 'limit' => $limit, + ] + ); + foreach ($this->batch->getTraversableList('crm.item.list', $order, $filter, $select, $limit, ['entityTypeId' => $entityTypeId]) as $key => $value) { + yield $key => new ItemItemResult($value); + } + } + + /** + * Batch adding crm items + * + * @return Generator|ItemItemResult[] + * + * @throws BaseException + */ + public function add(int $entityTypeId, array $items): Generator + { + $rawItems = []; + foreach ($items as $item) { + $rawItems[] = [ + 'entityTypeId' => $entityTypeId, + 'fields' => $item, + ]; + } + foreach ($this->batch->addEntityItems('crm.item.add', $rawItems) as $key => $item) { + yield $key => new ItemItemResult($item->getResult()['item']); + } + } +} \ No newline at end of file diff --git a/src/Services/CRM/Item/Service/Item.php b/src/Services/CRM/Item/Service/Item.php new file mode 100644 index 00000000..f080641e --- /dev/null +++ b/src/Services/CRM/Item/Service/Item.php @@ -0,0 +1,158 @@ +batch = $batch; + } + + /** + * Method creates new SPA item with entityTypeId. + * + * @link https://training.bitrix24.com/rest_help/crm/dynamic/methodscrmitem/crm_item_add.php + * + * + * @param int $entityTypeId + * @param array $fields + * @return ItemResult + * @throws BaseException + * @throws TransportException + */ + public function add(int $entityTypeId, array $fields): ItemResult + { + return new ItemResult( + $this->core->call( + 'crm.item.add', + [ + 'entityTypeId' => $entityTypeId, + 'fields' => $fields, + ] + ) + ); + } + + /** + * Deletes item with id for SPA with entityTypeId. + * + * @link https://training.bitrix24.com/rest_help/crm/dynamic/methodscrmitem/crm_item_delete.php + * + * @param int $entityTypeId + * @param int $id + * + * @return DeletedItemResult + * @throws BaseException + * @throws TransportException + */ + public function delete(int $entityTypeId, int $id): DeletedItemResult + { + return new DeletedItemResult( + $this->core->call( + 'crm.item.delete', ['entityTypeId' => $entityTypeId, 'id' => $id] + ) + ); + } + + /** + * Returns the fields data with entityTypeId. + * + * @link https://training.bitrix24.com/rest_help/crm/dynamic/methodscrmitem/crm_item_fields.php + * + * @param int $entityTypeId + * @return FieldsResult + * @throws BaseException + * @throws TransportException + */ + public function fields(int $entityTypeId): FieldsResult + { + return new FieldsResult($this->core->call('crm.item.fields', ['entityTypeId' => $entityTypeId])); + } + + /** + * Returns item data with id for SPA with entityTypeId. + * + * @link https://training.bitrix24.com/rest_help/crm/dynamic/methodscrmitem/crm_item_get.php + * + * @throws BaseException + * @throws TransportException + */ + public function get(int $entityTypeId, int $id): ItemResult + { + return new ItemResult($this->core->call('crm.item.get', ['entityTypeId' => $entityTypeId, 'id' => $id])); + } + + /** + * Returns array with SPA items with entityTypeId + * + * @link https://training.bitrix24.com/rest_help/crm/dynamic/methodscrmitem/crm_item_list.php + * + * @throws BaseException + * @throws TransportException + */ + public function list(int $entityTypeId, array $order, array $filter, array $select, int $startItem = 0): ItemsResult + { + return new ItemsResult( + $this->core->call( + 'crm.item.list', + [ + 'entityTypeId' => $entityTypeId, + 'order' => $order, + 'filter' => $filter, + 'select' => $select, + 'start' => $startItem, + ] + ) + ); + } + + /** + * Updates the specified (existing) item. + * + * @link https://training.bitrix24.com/rest_help/crm/dynamic/methodscrmitem/crm_item_update.php + * + * @throws BaseException + * @throws TransportException + */ + public function update(int $entityTypeId, int $id, array $fields): UpdatedItemResult + { + return new UpdatedItemResult( + $this->core->call( + 'crm.item.update', + [ + 'entityTypeId' => $entityTypeId, + 'id' => $id, + 'fields' => $fields, + ] + ) + ); + } + + /** + * Count by filter + * + * @throws BaseException + * @throws TransportException + */ + public function countByFilter(int $entityTypeId, array $filter = []): int + { + return $this->list($entityTypeId, [], $filter, ['id'], 1)->getCoreResponse()->getResponseData()->getPagination()->getTotal(); + } +} \ No newline at end of file diff --git a/src/Services/CRM/Lead/Result/LeadItemResult.php b/src/Services/CRM/Lead/Result/LeadItemResult.php index ab26d91c..3a2b300c 100644 --- a/src/Services/CRM/Lead/Result/LeadItemResult.php +++ b/src/Services/CRM/Lead/Result/LeadItemResult.php @@ -5,6 +5,10 @@ namespace Bitrix24\SDK\Services\CRM\Lead\Result; use Bitrix24\SDK\Services\CRM\Common\Result\AbstractCrmItem; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\Email; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\InstantMessenger; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\Phone; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\Website; use DateTimeInterface; /** @@ -60,10 +64,10 @@ * @property-read string $UTM_CAMPAIGN * @property-read string $UTM_CONTENT * @property-read string $UTM_TERM - * @property-read string $PHONE - * @property-read string $EMAIL - * @property-read string $WEB - * @property-read string $IM + * @property-read Phone[] $PHONE + * @property-read Email[] $EMAIL + * @property-read Website[] $WEB + * @property-read InstantMessenger[] $IM * @property-read string $LINK */ class LeadItemResult extends AbstractCrmItem diff --git a/src/Services/CRM/Lead/Service/Batch.php b/src/Services/CRM/Lead/Service/Batch.php index 934457c5..e34bdbbb 100644 --- a/src/Services/CRM/Lead/Service/Batch.php +++ b/src/Services/CRM/Lead/Service/Batch.php @@ -4,7 +4,7 @@ namespace Bitrix24\SDK\Services\CRM\Lead\Service; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Result\AddedItemBatchResult; use Bitrix24\SDK\Core\Result\DeletedItemBatchResult; @@ -19,16 +19,16 @@ */ class Batch { - protected BatchInterface $batch; + protected BatchOperationsInterface $batch; protected LoggerInterface $log; /** * Batch constructor. * - * @param BatchInterface $batch - * @param LoggerInterface $log + * @param BatchOperationsInterface $batch + * @param LoggerInterface $log */ - public function __construct(BatchInterface $batch, LoggerInterface $log) + public function __construct(BatchOperationsInterface $batch, LoggerInterface $log) { $this->batch = $batch; $this->log = $log; diff --git a/src/Services/Catalog/Catalog/Result/CatalogItemResult.php b/src/Services/Catalog/Catalog/Result/CatalogItemResult.php new file mode 100644 index 00000000..7a406f88 --- /dev/null +++ b/src/Services/Catalog/Catalog/Result/CatalogItemResult.php @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000..44c9799d --- /dev/null +++ b/src/Services/Catalog/Catalog/Result/CatalogsResult.php @@ -0,0 +1,26 @@ +getCoreResponse()->getResponseData()->getResult()['catalogs'] as $product) { + $res[] = new ProductItemResult($product); + } + + 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 new file mode 100644 index 00000000..b09c7e61 --- /dev/null +++ b/src/Services/Catalog/Catalog/Service/Catalog.php @@ -0,0 +1,64 @@ +core->call('catalog.catalog.get', ['id' => $catalogId])); + } + + /** + * The method gets field value of commercial catalog product by ID. + * + * @see https://training.bitrix24.com/rest_help/catalog/catalog/catalog_catalog_list.php + * @param array $select + * @param array $filter + * @param array $order + * @param int $start + * @return CatalogsResult + * @throws BaseException + * @throws TransportException + */ + public function list(array $select, array $filter, array $order, int $start): CatalogsResult + { + return new CatalogsResult($this->core->call('catalog.catalog.list', [ + 'select' => $select, + 'filter' => $filter, + 'order' => $order, + 'start' => $start + ])); + } + + /** + * Retrieves the fields for the catalog. + * + * @return FieldsResult Returns an instance of FieldsResult. + * @throws BaseException Throws a BaseException if there is an error in the core call. + * @throws TransportException Throws a TransportException if there is an error in the transport process. + * @see https://training.bitrix24.com/rest_help/catalog/catalog/catalog_catalog_getfields.php + */ + 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 new file mode 100644 index 00000000..32347b8c --- /dev/null +++ b/src/Services/Catalog/CatalogServiceBuilder.php @@ -0,0 +1,36 @@ +serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new Catalog\Product\Service\Product( + new Catalog\Product\Service\Batch($this->batch, $this->log), + $this->core, + $this->log + ); + } + + return $this->serviceCache[__METHOD__]; + } + + public function catalog(): Catalog\Catalog\Service\Catalog + { + if (!isset($this->serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new Catalog\Catalog\Service\Catalog( + $this->core, + $this->log + ); + } + + return $this->serviceCache[__METHOD__]; + } +} \ No newline at end of file diff --git a/src/Services/Catalog/Common/ProductType.php b/src/Services/Catalog/Common/ProductType.php new file mode 100644 index 00000000..0972c1a2 --- /dev/null +++ b/src/Services/Catalog/Common/ProductType.php @@ -0,0 +1,14 @@ +currency = $currency; + } + } + + /** + * @param int|string $offset + * + * @return bool|DateTimeImmutable|int|mixed|null + */ + + public function __get($offset) + { + switch ($offset) { + case 'active': + case 'available': + case 'bundle': + return $this->data[$offset] === 'Y'; + case 'barcodeMulti': + case 'canBuyZero': + if ($this->data[$offset] !== null) { + return $this->data[$offset] === 'Y'; + } + return null; + case 'code': + case 'detailText': + case 'detailTextType': + case 'name': + case 'previewText': + case 'previewTextType': + case 'xmlId': + return (string)$this->data[$offset]; + case 'createdBy': + case 'iblockId': + case 'iblockSectionId': + case 'id': + case 'modifiedBy': + case 'sort': + case 'height': + case 'length': + if ($this->data[$offset] !== '' && $this->data[$offset] !== null) { + return (int)$this->data[$offset]; + } + break; + case 'dateActiveFrom': + case 'dateActiveTo': + case 'dateCreate': + case 'timestampX': + if ($this->data[$offset] !== '') { + return DateTimeImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]); + } + + return null; + case 'type': + return ProductType::from($this->data[$offset]); + } + + return $this->data[$offset] ?? null; + } + + /** + * get userfield by field name + * + * @param string $fieldName + * + * @return mixed|null + * @throws UserfieldNotFoundException + */ + protected function getKeyWithUserfieldByFieldName(string $fieldName) + { + if (!str_starts_with($fieldName, self::CRM_USERFIELD_PREFIX)) { + $fieldName = self::CRM_USERFIELD_PREFIX . $fieldName; + } + if (!$this->isKeyExists($fieldName)) { + throw new UserfieldNotFoundException(sprintf('crm userfield not found by field name %s', $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 new file mode 100644 index 00000000..07349bb3 --- /dev/null +++ b/src/Services/Catalog/Product/Result/ProductItemResult.php @@ -0,0 +1,46 @@ +getCoreResponse()->getResponseData()->getResult())) { + // fix for catalog.product.add + return new ProductItemResult($this->getCoreResponse()->getResponseData()->getResult()['element']); + } + 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 new file mode 100644 index 00000000..9f80be4e --- /dev/null +++ b/src/Services/Catalog/Product/Result/ProductsResult.php @@ -0,0 +1,25 @@ +getCoreResponse()->getResponseData()->getResult()['products'] as $product) { + $res[] = new ProductItemResult($product); + } + + 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 new file mode 100644 index 00000000..1bd42ef6 --- /dev/null +++ b/src/Services/Catalog/Product/Service/Batch.php @@ -0,0 +1,22 @@ +batch = $batch; + } + + /** + * The method gets field value of commercial catalog product by ID. + * + * @see https://training.bitrix24.com/rest_help/catalog/product/catalog_product_get.php + * @throws TransportException + * @throws BaseException + */ + public function get(int $productId): ProductResult + { + return new ProductResult($this->core->call('catalog.product.get', ['id' => $productId])); + } + + /** + * The method adds a commercial catalog product. + * + * @see https://training.bitrix24.com/rest_help/catalog/product/catalog_product_add.php + * @param array $productFields + * @return ProductResult + * @throws BaseException + * @throws TransportException + */ + public function add(array $productFields): ProductResult + { + return new ProductResult($this->core->call('catalog.product.add', [ + 'fields' => $productFields + ] + )); + } + + /** + * The method deletes commercial catalog product. + * + * @see https://training.bitrix24.com/rest_help/catalog/product/catalog_product_delete.php + * @param int $productId + * @return DeletedItemResult + * @throws BaseException + * @throws TransportException + */ + public function delete(int $productId): DeletedItemResult + { + return new DeletedItemResult($this->core->call('catalog.product.delete', ['id' => $productId])); + } + + /** + * The method gets list of commercial catalog products by filter. + * + * @see https://training.bitrix24.com/rest_help/catalog/product/catalog_product_list.php + * @throws TransportException + * @throws BaseException + */ + public function list(array $select, array $filter, array $order, int $start): ProductsResult + { + return new ProductsResult($this->core->call('catalog.product.list', [ + 'select' => $select, + 'filter' => $filter, + 'order' => $order, + 'start' => $start + ])); + } + + /** + * The method returns commercial catalog product fields by filter. + * @see https://training.bitrix24.com/rest_help/catalog/product/catalog_product_getfieldsbyfilter.php + * + * @param int $iblockId + * @param ProductType $productType + * @param array|null $additionalFilter + * @return FieldsResult + * @throws BaseException + * @throws TransportException + */ + public function fieldsByFilter(int $iblockId, ProductType $productType, ?array $additionalFilter = null): FieldsResult + { + $filter = [ + 'iblockId' => $iblockId, + 'productType' => $productType->value + ]; + if ($additionalFilter !== null) { + $filter = array_merge($filter, $additionalFilter); + } + + return new FieldsResult($this->core->call('catalog.product.getFieldsByFilter', ['filter' => $filter])); + } +} \ No newline at end of file diff --git a/src/Services/ServiceBuilder.php b/src/Services/ServiceBuilder.php index ecc0cac0..2f7b1ca3 100644 --- a/src/Services/ServiceBuilder.php +++ b/src/Services/ServiceBuilder.php @@ -4,19 +4,16 @@ namespace Bitrix24\SDK\Services; +use Bitrix24\SDK\Services\Catalog\CatalogServiceBuilder; use Bitrix24\SDK\Services\CRM\CRMServiceBuilder; use Bitrix24\SDK\Services\IM\IMServiceBuilder; use Bitrix24\SDK\Services\IMOpenLines\IMOpenLinesServiceBuilder; use Bitrix24\SDK\Services\Main\MainServiceBuilder; use Bitrix24\SDK\Services\Telephony\TelephonyServiceBuilder; +use Bitrix24\SDK\Services\User\UserServiceBuilder; use Bitrix24\SDK\Services\UserConsent\UserConsentServiceBuilder; use Bitrix24\SDK\Services\Placement\PlacementServiceBuilder; -/** - * Class ServiceBuilder - * - * @package Bitrix24\SDK\Services - */ class ServiceBuilder extends AbstractServiceBuilder { /** @@ -68,7 +65,7 @@ public function getMainScope(): MainServiceBuilder } /** - * @return \Bitrix24\SDK\Services\UserConsent\UserConsentServiceBuilder + * @return UserConsentServiceBuilder */ public function getUserConsentScope(): UserConsentServiceBuilder { @@ -79,6 +76,18 @@ public function getUserConsentScope(): UserConsentServiceBuilder return $this->serviceCache[__METHOD__]; } + /** + * @return UserServiceBuilder + */ + public function getUserScope(): UserServiceBuilder + { + if (!isset($this->serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new UserServiceBuilder($this->core, $this->batch, $this->bulkItemsReader, $this->log); + } + + return $this->serviceCache[__METHOD__]; + } + /** * @return PlacementServiceBuilder */ @@ -91,6 +100,15 @@ public function getPlacementScope(): PlacementServiceBuilder return $this->serviceCache[__METHOD__]; } + public function getCatalogScope(): CatalogServiceBuilder + { + if (!isset($this->serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new CatalogServiceBuilder($this->core, $this->batch, $this->bulkItemsReader, $this->log); + } + + return $this->serviceCache[__METHOD__]; + } + /** * @return TelephonyServiceBuilder */ diff --git a/src/Services/ServiceBuilderFactory.php b/src/Services/ServiceBuilderFactory.php new file mode 100644 index 00000000..5bef3253 --- /dev/null +++ b/src/Services/ServiceBuilderFactory.php @@ -0,0 +1,123 @@ +eventDispatcher = $eventDispatcher; + $this->log = $log; + } + + /** + * Init service builder from application account + * + * @param ApplicationProfile $applicationProfile + * @param Bitrix24AccountInterface $bitrix24Account + * + * @return \Bitrix24\SDK\Services\ServiceBuilder + * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + */ + public function initFromAccount(ApplicationProfile $applicationProfile, Bitrix24AccountInterface $bitrix24Account): ServiceBuilder + { + return $this->getServiceBuilder( + Credentials::createFromOAuth( + AccessToken::initFromArray( + [ + 'access_token' => $bitrix24Account->getAccessToken(), + 'refresh_token' => $bitrix24Account->getRefreshToken(), + 'expires' => $bitrix24Account->getExpires(), + ] + ), + $applicationProfile, + $bitrix24Account->getDomainUrl() + ) + ); + } + + /** + * Init service builder from request + * + * @param \Bitrix24\SDK\Core\Credentials\ApplicationProfile $applicationProfile + * @param \Bitrix24\SDK\Core\Credentials\AccessToken $accessToken + * @param string $bitrix24DomainUrl + * + * @return \Bitrix24\SDK\Services\ServiceBuilder + * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + */ + public function initFromRequest( + ApplicationProfile $applicationProfile, + AccessToken $accessToken, + string $bitrix24DomainUrl + ): ServiceBuilder { + return $this->getServiceBuilder( + Credentials::createFromOAuth( + $accessToken, + $applicationProfile, + $bitrix24DomainUrl + ) + ); + } + + /** + * Init service builder from webhook + * + * @param string $webhookUrl + * + * @return \Bitrix24\SDK\Services\ServiceBuilder + * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + */ + public function initFromWebhook(string $webhookUrl): ServiceBuilder + { + return $this->getServiceBuilder(Credentials::createFromWebhook(new WebhookUrl($webhookUrl))); + } + + /** + * @param \Bitrix24\SDK\Core\Credentials\Credentials $credentials + * + * @return \Bitrix24\SDK\Services\ServiceBuilder + * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + */ + private function getServiceBuilder(Credentials $credentials): ServiceBuilder + { + $core = (new CoreBuilder()) + ->withEventDispatcher($this->eventDispatcher) + ->withLogger($this->log) + ->withCredentials($credentials) + ->build(); + $batch = new Batch($core, $this->log); + + return new ServiceBuilder( + $core, + $batch, + (new BulkItemsReaderBuilder( + $core, + $batch, + $this->log + ))->build(), + $this->log + ); + } + +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/Auth.php b/src/Services/Telephony/Requests/Events/Auth.php new file mode 100644 index 00000000..a0a3dadd --- /dev/null +++ b/src/Services/Telephony/Requests/Events/Auth.php @@ -0,0 +1,41 @@ + Scope::initFromString((string)$this->data[$offset]), + 'status' => ApplicationStatus::initFromString((string)$this->data[$offset]), + 'user_id', 'expires_in', 'expires' => (int)$this->data[$offset], + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnExternalCallStart.php b/src/Services/Telephony/Requests/Events/OnExternalCallStart.php deleted file mode 100644 index 6361a3c1..00000000 --- a/src/Services/Telephony/Requests/Events/OnExternalCallStart.php +++ /dev/null @@ -1,95 +0,0 @@ -eventPayload['data']['USER_ID']; - } - - /** - * @return string Outbound call ID. - */ - public function getPhoneNumber(): string - { - return $this->eventPayload['data']['PHONE_NUMBER']; - } - - /** - * @return string - */ - public function getPhoneNumberInternational(): string - { - return $this->eventPayload['data']['PHONE_NUMBER_INTERNATIONAL']; - } - - /** - * @return \Bitrix24\SDK\Services\Telephony\Common\CrmEntityType - * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException - */ - public function getCrmEntityType(): CrmEntityType - { - return CrmEntityType::initByCode($this->eventPayload['data']['CRM_ENTITY_TYPE']); - } - - /** - * @return int The CRM object ID, which type is specified in CRM_ENTITY_TYPE. - */ - public function getCrmEntityId(): int - { - return (int)$this->eventPayload['data']['CRM_ENTITY_ID']; - } - - /** - * @return int Call list ID, if the call is made from the call list. - */ - public function getCallListId(): int - { - return (int)$this->eventPayload['data']['CALL_LIST_ID']; - } - - /** - * @return string External line number, via which the the call is requested. - */ - public function getLineNumber(): string - { - return $this->eventPayload['data']['LINE_NUMBER']; - } - - /** - * @return string Call ID from the telephony.externalcall.register method. - */ - public function getCallId(): string - { - return $this->eventPayload['data']['CALL_ID']; - } - - /** - * @return string - */ - public function getExtension(): string - { - return $this->eventPayload['data']['EXTENSION']; - } - - /** - * @return bool Defines call as initiated from the mobile app. - */ - public function isMobile(): bool - { - return !($this->eventPayload['data']['IS_MOBILE'] === '0'); - } -} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnExternalCallStart/CallData.php b/src/Services/Telephony/Requests/Events/OnExternalCallStart/CallData.php new file mode 100644 index 00000000..6a876d0d --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnExternalCallStart/CallData.php @@ -0,0 +1,42 @@ + (int)$this->data[$offset] !== 0, + 'CALL_TYPE' => CallType::initByTypeCode((int)$this->data[$offset]), + 'CRM_ENTITY_TYPE' => (string)$this->data[$offset], + 'REST_APP_ID', 'CALL_LIST_ID', 'CRM_CREATED_LEAD', 'CRM_ENTITY_ID', 'USER_ID' => (int)$this->data[$offset], + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnExternalCallStart/OnExternalCallStart.php b/src/Services/Telephony/Requests/Events/OnExternalCallStart/OnExternalCallStart.php new file mode 100644 index 00000000..a15f1318 --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnExternalCallStart/OnExternalCallStart.php @@ -0,0 +1,30 @@ +eventPayload['data']); + } + + /** + * @return \Bitrix24\SDK\Services\Telephony\Requests\Events\Auth + */ + public function getAuth(): Auth + { + return new Auth($this->eventPayload['auth']); + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd.php deleted file mode 100644 index ff0229f4..00000000 --- a/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd.php +++ /dev/null @@ -1,114 +0,0 @@ -eventPayload['data']['CALL_ID']; - } - - /** - * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException - */ - public function getCallType(): CallType - { - return CallType::initByTypeCode((int)$this->eventPayload['data']['CALL_TYPE']); - } - - /** - * @return string Number used by the subscriber to make a call (if call type is: 2 – Inbound) or number called by the operator (if call type is: 1 – Outbound). - */ - public function getPhoneNumber(): string - { - return $this->eventPayload['data']['PHONE_NUMBER']; - } - - /** - * @return string Number receiving the call (if call type is: 2 – Inbound) or number from which the call was made (if call type is: 1 – Outbound). - */ - public function getPortalNumber(): string - { - return $this->eventPayload['data']['PORTAL_NUMBER']; - } - - /** - * @return int Responding operator ID (if call type is: 2 – Inbound) or identifier of the calling operator (if call type is: 1 – Outbound). - */ - public function getPortalUserId(): int - { - return (int)$this->eventPayload['data']['PORTAL_USER_ID']; - } - - /** - * @return int Call duration. - */ - public function getCallDuration(): int - { - return (int)$this->eventPayload['data']['CALL_DURATION']; - } - - /** - * @return \DateTimeImmutable Date in ISO format. - */ - public function getCallStartDate(): DateTimeImmutable - { - return DateTimeImmutable::createFromFormat(DATE_ATOM, $this->eventPayload['data']['CALL_START_DATE']); - } - - /** - * @return \Money\Money Call cost. - */ - public function getCost(): Money - { - if ($this->eventPayload['COST'] === '') { - return new Money(0, new Currency($this->eventPayload['data']['COST_CURRENCY'])); - } - - return (new DecimalMoneyParser(new ISOCurrencies()))->parse( - $this->eventPayload['data']['COST'], - $this->eventPayload['data']['COST_CURRENCY'] - ); - } - - /** - * @return int Call code (See Call Code Table). - */ - public function getCallFailedCode(): int - { - return (int)$this->eventPayload['data']['CALL_FAILED_CODE']; - } - - /** - * @return string Call code textual description (Latin letters). - */ - public function getCallFailedReason(): string - { - return $this->eventPayload['data']['CALL_FAILED_REASON']; - } - - /** - * @return int - */ - public function getCrmActivityId(): int - { - return (int)$this->eventPayload['data']['CRM_ACTIVITY_ID']; - } -} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd/CallData.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd/CallData.php new file mode 100644 index 00000000..b8ddfbd0 --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd/CallData.php @@ -0,0 +1,68 @@ +data[$offset]; + case 'CALL_START_DATE': + return new \DateTimeImmutable((string)$this->data[$offset]); + case 'CALL_TYPE': + return CallType::initByTypeCode((int)$this->data[$offset]); + case 'CALL_DURATION': + case 'CALL_FAILED_CODE': + case 'CRM_ACTIVITY_ID': + case 'PORTAL_USER_ID': + return (int)$this->data[$offset]; + case 'COST_CURRENCY': + return new Currency($this->data[$offset]); + case 'COST': + if ($this->data[$offset] === null) { + return new Money(0, new Currency($this->data['COST_CURRENCY'])); + } + + return (new DecimalMoneyParser(new ISOCurrencies()))->parse( + $this->data[$offset], + new Currency($this->data['COST_CURRENCY']) + ); + default: + return parent::__get($offset); + } + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd/OnVoximplantCallEnd.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd/OnVoximplantCallEnd.php new file mode 100644 index 00000000..1b0697ad --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnVoximplantCallEnd/OnVoximplantCallEnd.php @@ -0,0 +1,35 @@ +eventPayload['auth']); + } + + /** + * @return \Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallEnd\CallData + */ + public function getCallData(): CallData + { + return new CallData($this->eventPayload['data']); + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallInit.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallInit.php deleted file mode 100644 index 753f2d67..00000000 --- a/src/Services/Telephony/Requests/Events/OnVoximplantCallInit.php +++ /dev/null @@ -1,54 +0,0 @@ -eventPayload['data']['CALL_ID']; - } - - /** - * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException - */ - public function getCallType(): CallType - { - return CallType::initByTypeCode((int)$this->eventPayload['data']['CALL_TYPE']); - } - - /** - * @return string Line ID (numeric for leased PBX, regXXX for cloud hosted PBX, and sipXXX for office PBX). - */ - public function getAccountSearchId(): string - { - return $this->eventPayload['data']['ACCOUNT_SEARCH_ID']; - } - - /** - * @return string Number called by the operator (if call type is: 1 – Outbound) or number called by the subscriber (if call type is: 2 – Inbound). - */ - public function getPhoneNumber(): string - { - return $this->eventPayload['data']['PHONE_NUMBER']; - } - - /** - * @return string Line identifier (if call type is: 1 – Outbound) or telephone number used to make a call to the portal (if call type is: 2 – Inbound). - */ - public function getCallerId(): string - { - return $this->eventPayload['data']['CALLER_ID']; - } -} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallInit/CallData.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallInit/CallData.php new file mode 100644 index 00000000..383db9bb --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnVoximplantCallInit/CallData.php @@ -0,0 +1,35 @@ + CallType::initByTypeCode((int)$this->data[$offset]), + 'REST_APP_ID' => (int)$this->data[$offset], + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallInit/OnVoximplantCallInit.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallInit/OnVoximplantCallInit.php new file mode 100644 index 00000000..2e0c2889 --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnVoximplantCallInit/OnVoximplantCallInit.php @@ -0,0 +1,32 @@ +eventPayload['auth']); + } + + /** + * @return \Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallInit\CallData + */ + public function getCallData(): CallData + { + return new CallData($this->eventPayload['data']); + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallStart.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallStart.php deleted file mode 100644 index 11df4201..00000000 --- a/src/Services/Telephony/Requests/Events/OnVoximplantCallStart.php +++ /dev/null @@ -1,29 +0,0 @@ -eventPayload['data']['CALL_ID']; - } - - /** - * @return int Identifier of the user who responded the call. - */ - public function getUserId(): int - { - return (int)$this->eventPayload['data']['USER_ID']; - } -} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallStart/CallData.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallStart/CallData.php new file mode 100644 index 00000000..86a08f01 --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnVoximplantCallStart/CallData.php @@ -0,0 +1,28 @@ + (int)$this->data[$offset], + default => parent::__get($offset), + }; + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Requests/Events/OnVoximplantCallStart/OnVoximplantCallStart.php b/src/Services/Telephony/Requests/Events/OnVoximplantCallStart/OnVoximplantCallStart.php new file mode 100644 index 00000000..6138ac4f --- /dev/null +++ b/src/Services/Telephony/Requests/Events/OnVoximplantCallStart/OnVoximplantCallStart.php @@ -0,0 +1,29 @@ +eventPayload['auth']); + } + /** + * @return \Bitrix24\SDK\Services\Telephony\Requests\Events\OnVoximplantCallStart\CallData + */ + public function getCallData(): CallData + { + return new CallData($this->eventPayload['data']); + } +} \ No newline at end of file diff --git a/src/Services/Telephony/Result/ExternalCallRegisterItemResult.php b/src/Services/Telephony/Result/ExternalCallRegisterItemResult.php index ba8a09c2..9b4796d5 100644 --- a/src/Services/Telephony/Result/ExternalCallRegisterItemResult.php +++ b/src/Services/Telephony/Result/ExternalCallRegisterItemResult.php @@ -16,14 +16,25 @@ use Bitrix24\SDK\Core\Result\AbstractItem; /** + * If registration of the call was unsuccessful, the LEAD_CREATION_ERROR field will contain the error message. + * * @property-read string $CALL_ID - * @property-read int $CRM_CREATED_LEAD - * @property-read int $CRM_ENTITY_ID + * @property-read ?int $CRM_CREATED_LEAD + * @property-read ?int $CRM_ENTITY_ID * @property-read string $CRM_ENTITY_TYPE * @property-read array $CRM_CREATED_ENTITIES * @property-read string $LEAD_CREATION_ERROR */ class ExternalCallRegisterItemResult extends AbstractItem { - + /** + * @return bool + */ + public function isError(): bool + { + if (!$this->isKeyExists('LEAD_CREATION_ERROR')) { + return false; + } + return $this->data['LEAD_CREATION_ERROR'] !== '' && $this->data['LEAD_CREATION_ERROR'] !== null; + } } \ No newline at end of file diff --git a/src/Services/User/Result/UserItemResult.php b/src/Services/User/Result/UserItemResult.php new file mode 100644 index 00000000..e8ad00e0 --- /dev/null +++ b/src/Services/User/Result/UserItemResult.php @@ -0,0 +1,60 @@ +data[$offset]; + case 'LAST_LOGIN': + case 'DATE_REGISTER': + case 'UF_EMPLOYMENT_DATE': + if ($this->data[$offset] !== '') { + return DateTimeImmutable::createFromFormat(DATE_ATOM, $this->data[$offset]); + } + break; + case 'IS_ONLINE': + return $this->data[$offset] === 'Y'; + } + + return $this->data[$offset] ?? null; + } +} \ No newline at end of file diff --git a/src/Services/User/Result/UserResult.php b/src/Services/User/Result/UserResult.php new file mode 100644 index 00000000..01e0ee4b --- /dev/null +++ b/src/Services/User/Result/UserResult.php @@ -0,0 +1,16 @@ +getCoreResponse()->getResponseData()->getResult()); + } +} \ No newline at end of file diff --git a/src/Services/User/Result/UsersResult.php b/src/Services/User/Result/UsersResult.php new file mode 100644 index 00000000..a4fff780 --- /dev/null +++ b/src/Services/User/Result/UsersResult.php @@ -0,0 +1,27 @@ +getCoreResponse()->getResponseData()->getResult() as $item) { + $res[] = new UserItemResult($item); + } + + return $res; + } +} \ No newline at end of file diff --git a/src/Services/User/Service/User.php b/src/Services/User/Service/User.php new file mode 100644 index 00000000..80228317 --- /dev/null +++ b/src/Services/User/Service/User.php @@ -0,0 +1,120 @@ +core->call('user.fields')); + } + + /** + * Get current user + * @return UserResult + * @throws BaseException + * @throws TransportException + * @link https://training.bitrix24.com/rest_help/users/user_current.php + */ + public function current(): UserResult + { + return new UserResult($this->core->call('user.current')); + } + + /** + * Invites a user. Available only for users with invitation permissions, usually an administrator. Sends a standard account invitation to the user on success. + * + * @param array $fields = ['ID','XML_ID','ACTIVE','NAME','LAST_NAME','SECOND_NAME','TITLE','EMAIL','PERSONAL_PHONE','WORK_PHONE','WORK_POSITION','WORK_COMPANY','IS_ONLINE','TIME_ZONE','TIMESTAMP_X','TIME_ZONE_OFFSET','DATE_REGISTER','LAST_ACTIVITY_DATE','PERSONAL_PROFESSION','PERSONAL_GENDER','PERSONAL_BIRTHDAY','PERSONAL_PHOTO','PERSONAL_FAX','PERSONAL_MOBILE','PERSONAL_PAGER','PERSONAL_STREET','PERSONAL_MAILBOX','PERSONAL_CITY','PERSONAL_STATE','PERSONAL_ZIP','PERSONAL_COUNTRY','PERSONAL_NOTES','WORK_DEPARTMENT','WORK_WWW','WORK_FAX','WORK_PAGER','WORK_STREET','WORK_MAILBOX','WORK_CITY','WORK_STATE','WORK_ZIP','WORK_COUNTRY','WORK_PROFILE','WORK_LOGO','WORK_NOTES','UF_DEPARTMENT','UF_DISTRICT','UF_SKYPE','UF_SKYPE_LINK','UF_ZOOM','UF_TWITTER','UF_FACEBOOK','UF_LINKEDIN','UF_XING','UF_WEB_SITES','UF_PHONE_INNER','UF_EMPLOYMENT_DATE','UF_TIMEMAN','UF_SKILLS','UF_INTERESTS','USER_TYPE'] + * @param string $messageText + * @return AddedItemResult + * @throws BaseException + * @throws TransportException + * @link https://training.bitrix24.com/rest_help/users/user_add.php + */ + public function add(array $fields, string $messageText = ''): AddedItemResult + { + if (!array_key_exists('EXTRANET', $fields)) { + throw new InvalidArgumentException(sprintf('field EXTRANET is required')); + } + + return new AddedItemResult($this->core->call( + 'user.add', + array_merge( + $fields, + [ + 'MESSAGE_TEXT' => $messageText + ] + ) + )); + } + + /** + * @param array $order + * @param array $filter = ['ID','XML_ID','ACTIVE','NAME','LAST_NAME','SECOND_NAME','TITLE','EMAIL','PERSONAL_PHONE','WORK_PHONE','WORK_POSITION','WORK_COMPANY','IS_ONLINE','TIME_ZONE','TIMESTAMP_X','TIME_ZONE_OFFSET','DATE_REGISTER','LAST_ACTIVITY_DATE','PERSONAL_PROFESSION','PERSONAL_GENDER','PERSONAL_BIRTHDAY','PERSONAL_PHOTO','PERSONAL_FAX','PERSONAL_MOBILE','PERSONAL_PAGER','PERSONAL_STREET','PERSONAL_MAILBOX','PERSONAL_CITY','PERSONAL_STATE','PERSONAL_ZIP','PERSONAL_COUNTRY','PERSONAL_NOTES','WORK_DEPARTMENT','WORK_WWW','WORK_FAX','WORK_PAGER','WORK_STREET','WORK_MAILBOX','WORK_CITY','WORK_STATE','WORK_ZIP','WORK_COUNTRY','WORK_PROFILE','WORK_LOGO','WORK_NOTES','UF_DEPARTMENT','UF_DISTRICT','UF_SKYPE','UF_SKYPE_LINK','UF_ZOOM','UF_TWITTER','UF_FACEBOOK','UF_LINKEDIN','UF_XING','UF_WEB_SITES','UF_PHONE_INNER','UF_EMPLOYMENT_DATE','UF_TIMEMAN','UF_SKILLS','UF_INTERESTS','USER_TYPE'] + * @param bool $isAdminMode + * @return UsersResult + * @throws BaseException + * @throws TransportException + */ + public function get(array $order, array $filter, bool $isAdminMode = false): UsersResult + { + return new UsersResult($this->core->call('user.get', [ + 'sort' => array_keys($order)[0], + 'order' => array_values($order)[0], + 'filter' => $filter, + 'ADMIN_MODE' => $isAdminMode ? 'true' : 'false' + ])); + } + + /** + * Updates user information. Available only for users with invitation permissions. + * @param int $userId + * @param array $fields + * @return UpdatedItemResult + * @throws BaseException + * @throws TransportException + * @link https://training.bitrix24.com/rest_help/users/user_update.php + */ + public function update(int $userId, array $fields): UpdatedItemResult + { + return new UpdatedItemResult($this->core->call('user.update', array_merge( + $fields, + [ + 'ID' => $userId + ] + ))); + } + + /** + * This method is used to retrieve list of users with expedited personal data search (name, last name, middle name, name of department, position). Works in two modes: Quick mode, via Fulltext Index and slower mode via right LIKE (support is determined automatically). + * + * @param array $filterFields + * @return UsersResult + * @throws BaseException + * @throws TransportException + * @link https://training.bitrix24.com/rest_help/users/user_search.php + */ + public function search(array $filterFields): UsersResult + { + return new UsersResult($this->core->call('user.search', $filterFields)); + } +} \ No newline at end of file diff --git a/src/Services/User/UserServiceBuilder.php b/src/Services/User/UserServiceBuilder.php new file mode 100644 index 00000000..d6977687 --- /dev/null +++ b/src/Services/User/UserServiceBuilder.php @@ -0,0 +1,25 @@ +serviceCache[__METHOD__])) { + $this->serviceCache[__METHOD__] = new User($this->core, $this->log); + } + + return $this->serviceCache[__METHOD__]; + } +} \ No newline at end of file diff --git a/tests/Builders/Services/CRM/PhoneCollectionBuilder.php b/tests/Builders/Services/CRM/PhoneCollectionBuilder.php new file mode 100644 index 00000000..2dbe6819 --- /dev/null +++ b/tests/Builders/Services/CRM/PhoneCollectionBuilder.php @@ -0,0 +1,92 @@ +phones = []; + $this->phoneNumberBuilder = new PhoneNumberBuilder(); + } + + /** + * @throws Exception + */ + public function withDuplicatePhones(int $duplicatesCount = 1): self + { + $duplicatePhone = $this->phoneNumberBuilder->build(); + + $duplicates = []; + for ($i = 0; $i <= $duplicatesCount; $i++) { + $duplicates[] = new Phone([ + 'ID' => time() + random_int(1, 1000000), + 'VALUE' => $duplicatePhone, + 'VALUE_TYPE' => PhoneValueType::work->value + ]); + } + + $this->phones = array_merge( + [ + new Phone([ + 'ID' => time() + random_int(1, 1000000), + 'VALUE' => $this->phoneNumberBuilder->build(), + 'VALUE_TYPE' => PhoneValueType::work->value + ]) + ], + $duplicates + ); + + return $this; + } + + public function withPhoneLength(int $phoneLength): self + { + $this->phones = [ + new Phone([ + 'ID' => time() + random_int(1, 1000000), + 'VALUE' => $this->phoneNumberBuilder->withLength($phoneLength)->build(), + 'VALUE_TYPE' => PhoneValueType::work->value + ]), + new Phone([ + 'ID' => time() + random_int(1, 1000000), + 'VALUE' => $this->phoneNumberBuilder->withLength(7)->build(), + 'VALUE_TYPE' => PhoneValueType::work->value + ]) + ]; + + return $this; + } + + /** + * @throws Exception + */ + public function build(): array + { + return $this->phones; + } + + public function buildNewPhonesCommand(): array + { + $res = []; + foreach ($this->phones as $phone) { + $res[] = [ + 'VALUE' => $phone->VALUE, + 'VALUE_TYPE' => $phone->VALUE_TYPE->value + ]; + } + return $res; + } +} \ No newline at end of file diff --git a/tests/Builders/Services/CRM/PhoneNumberBuilder.php b/tests/Builders/Services/CRM/PhoneNumberBuilder.php new file mode 100644 index 00000000..bcb14a96 --- /dev/null +++ b/tests/Builders/Services/CRM/PhoneNumberBuilder.php @@ -0,0 +1,31 @@ +length = 7; + } + + public function withLength(int $length): self + { + $this->length = $length; + return $this; + } + + /** + * @throws Exception + */ + public function build(): string + { + return '+7' . substr((string)time(), 2, $this->length) . substr((string)random_int(1000, PHP_INT_MAX), 0, 3); + } +} \ No newline at end of file diff --git a/tests/Integration/Core/CoreTest.php b/tests/Integration/Core/CoreTest.php index 5b590193..f6b6c004 100644 --- a/tests/Integration/Core/CoreTest.php +++ b/tests/Integration/Core/CoreTest.php @@ -5,6 +5,12 @@ namespace Bitrix24\SDK\Tests\Integration\Core; use Bitrix24\SDK\Core\Contracts\CoreInterface; +use Bitrix24\SDK\Core\CoreBuilder; +use Bitrix24\SDK\Core\Credentials\AccessToken; +use Bitrix24\SDK\Core\Credentials\ApplicationProfile; +use Bitrix24\SDK\Core\Credentials\Credentials; +use Bitrix24\SDK\Core\Credentials\Scope; +use Bitrix24\SDK\Core\Exceptions\AuthForbiddenException; use Bitrix24\SDK\Core\Exceptions\MethodNotFoundException; use Bitrix24\SDK\Tests\Integration\Fabric; use PHPUnit\Framework\TestCase; @@ -28,6 +34,19 @@ public function testCallExistingApiMethod(): void $this->assertIsArray($response->getResponseData()->getResult()); } + public function testConnectToNonExistsBitrix24PortalInCloud():void + { + $core = (new CoreBuilder()) + ->withCredentials(Credentials::createFromOAuth( + new AccessToken('non-exists-access-token','refresh-token', 3600), + new ApplicationProfile('non-exists-client-id', 'non-exists-client-secret', new Scope([])), + 'non-exists-domain.bitrix24.com' + )) + ->build(); + $this->expectException(AuthForbiddenException::class); + $core->call('app.info'); + } + /** * @return void * @throws \Bitrix24\SDK\Core\Exceptions\BaseException diff --git a/tests/Integration/Services/CRM/Contact/Service/ContactBatchTest.php b/tests/Integration/Services/CRM/Contact/Service/ContactBatchTest.php new file mode 100644 index 00000000..fafd8cb5 --- /dev/null +++ b/tests/Integration/Services/CRM/Contact/Service/ContactBatchTest.php @@ -0,0 +1,122 @@ +contactService->add(['NAME' => 'test contact']); + $cnt = 0; + + foreach ($this->contactService->batch->list([], ['>ID' => '1'], ['ID', 'NAME'], 1) as $item) { + $cnt++; + } + self::assertGreaterThanOrEqual(1, $cnt); + } + + /** + * @throws BaseException + * @throws TransportException + * @covers \Bitrix24\SDK\Services\CRM\Contact\Service\Batch::add() + */ + public function testBatchAdd(): void + { + $contacts = []; + for ($i = 1; $i < 60; $i++) { + $contacts[] = ['NAME' => 'name-' . $i]; + } + $cnt = 0; + foreach ($this->contactService->batch->add($contacts) as $item) { + $cnt++; + } + + self::assertEquals(count($contacts), $cnt); + } + + /** + * @return void + * @throws BaseException + * @covers \Bitrix24\SDK\Services\CRM\Contact\Service\Batch::update() + */ + public function testBatchUpdate(): void + { + // add contacts + $contacts = []; + for ($i = 1; $i <= self::TEST_SEGMENT_ELEMENTS_COUNT; $i++) { + $contacts[] = [ + 'NAME' => 'name-' . time(), + 'SECOND_NAME' => 'second_name-' . time(), + 'LAST_NAME' => 'last_name-' . time(), + 'PHONE' => [ + [ + 'VALUE' => (new PhoneNumberBuilder())->build(), + 'VALUE_TYPE' => 'WORK' + ] + ] + ]; + } + $cnt = 0; + $contactId = []; + foreach ($this->contactService->batch->add($contacts) as $item) { + $cnt++; + $contactId[] = $item->getId(); + } + self::assertEquals(count($contacts), $cnt); + + // generate update data + $updateContactData = []; + foreach ($contactId as $id) { + $updateContactData[$id] = [ + 'fields' => [ + 'NAME' => 'name-' . $id . '-updated' + ], + ]; + } + + // update contacts in batch mode + $cnt = 0; + foreach ($this->contactService->batch->update($updateContactData) as $item) { + $cnt++; + $this->assertTrue($item->isSuccess()); + } + self::assertEquals(count($contacts), $cnt); + + // delete contacts + $cnt = 0; + foreach ($this->contactService->batch->delete($contactId) as $item) { + $cnt++; + $this->assertTrue($item->isSuccess()); + } + self::assertEquals(count($contacts), $cnt); + } + + public function setUp(): void + { + $this->contactService = Fabric::getServiceBuilder()->getCRMScope()->contact(); + } +} diff --git a/tests/Integration/Services/CRM/Contact/Service/ContactTest.php b/tests/Integration/Services/CRM/Contact/Service/ContactTest.php index 1be0b5e4..058555a1 100644 --- a/tests/Integration/Services/CRM/Contact/Service/ContactTest.php +++ b/tests/Integration/Services/CRM/Contact/Service/ContactTest.php @@ -6,9 +6,15 @@ use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Exceptions\TransportException; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\EmailValueType; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\InstantMessengerValueType; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\PhoneValueType; +use Bitrix24\SDK\Services\CRM\Common\Result\SystemFields\Types\WebsiteValueType; use Bitrix24\SDK\Services\CRM\Contact\Service\Contact; use Bitrix24\SDK\Tests\Integration\Fabric; use PHPUnit\Framework\TestCase; +use Bitrix24\SDK\Core; +use Faker; /** * Class ContactTest @@ -18,6 +24,7 @@ class ContactTest extends TestCase { protected Contact $contactService; + protected Faker\Generator $faker; /** * @throws BaseException @@ -87,35 +94,6 @@ public function testUpdate(): void self::assertEquals($newName, $this->contactService->get($contact->getId())->contact()->NAME); } - /** - * @throws BaseException - * @throws TransportException - */ - public function testBatchList(): void - { - $this->contactService->add(['NAME' => 'test contact']); - $cnt = 0; - - foreach ($this->contactService->batch->list([], ['>ID' => '1'], ['ID', 'NAME'], 1) as $item) { - $cnt++; - } - self::assertGreaterThanOrEqual(1, $cnt); - } - - public function testBatchAdd(): void - { - $contacts = []; - for ($i = 1; $i < 60; $i++) { - $contacts[] = ['NAME' => 'name-' . $i]; - } - $cnt = 0; - foreach ($this->contactService->batch->add($contacts) as $item) { - $cnt++; - } - - self::assertEquals(count($contacts), $cnt); - } - /** * @throws \Bitrix24\SDK\Core\Exceptions\TransportException * @throws \Bitrix24\SDK\Core\Exceptions\BaseException @@ -139,8 +117,101 @@ public function testCountByFilter(): void $this->assertEquals($totalBefore + $newContactsCount, $totalAfter); } + /** + * @return void + * @covers Contact::get + * @covers Contact::add + * @throws Core\Exceptions\TransportException + * @throws Core\Exceptions\BaseException + */ + public function testGetEmail(): void + { + $email = $this->faker->email(); + $this->assertEquals($email, + $this->contactService->get( + $this->contactService->add([ + 'NAME' => $this->faker->name(), + 'EMAIL' => [ + [ + 'VALUE' => $email, + 'VALUE_TYPE' => EmailValueType::work->name, + ] + ], + ])->getId())->contact()->EMAIL[0]->VALUE); + } + + /** + * @return void + * @covers Contact::get + * @covers Contact::add + * @throws Core\Exceptions\TransportException + * @throws Core\Exceptions\BaseException + */ + public function testGetPhone(): void + { + $phone = $this->faker->e164PhoneNumber(); + $this->assertEquals($phone, + $this->contactService->get( + $this->contactService->add([ + 'NAME' => $this->faker->name(), + 'PHONE' => [ + [ + 'VALUE' => $phone, + 'VALUE_TYPE' => PhoneValueType::work->name, + ] + ], + ])->getId())->contact()->PHONE[0]->VALUE); + } + + /** + * @return void + * @covers Contact::get + * @covers Contact::add + * @throws Core\Exceptions\TransportException + * @throws Core\Exceptions\BaseException + */ + public function testGetInstantMessenger(): void + { + $phone = $this->faker->e164PhoneNumber(); + $this->assertEquals($phone, + $this->contactService->get( + $this->contactService->add([ + 'NAME' => $this->faker->name(), + 'IM' => [ + [ + 'VALUE' => $phone, + 'VALUE_TYPE' => InstantMessengerValueType::telegram->name, + ] + ], + ])->getId())->contact()->IM[0]->VALUE); + } + + /** + * @return void + * @covers Contact::get + * @covers Contact::add + * @throws Core\Exceptions\TransportException + * @throws Core\Exceptions\BaseException + */ + public function testGetWebsite(): void + { + $url = $this->faker->url(); + $this->assertEquals($url, + $this->contactService->get( + $this->contactService->add([ + 'NAME' => $this->faker->name(), + 'WEB' => [ + [ + 'VALUE' => $url, + 'VALUE_TYPE' => WebsiteValueType::work, + ] + ], + ])->getId())->contact()->WEB[0]->VALUE); + } + public function setUp(): void { $this->contactService = Fabric::getServiceBuilder()->getCRMScope()->contact(); + $this->faker = Faker\Factory::create(); } } \ No newline at end of file diff --git a/tests/Integration/Services/CRM/Deal/Service/DealProductRowsTest.php b/tests/Integration/Services/CRM/Deal/Service/DealProductRowsTest.php index 0b45ad41..2c286da0 100644 --- a/tests/Integration/Services/CRM/Deal/Service/DealProductRowsTest.php +++ b/tests/Integration/Services/CRM/Deal/Service/DealProductRowsTest.php @@ -9,6 +9,10 @@ use Bitrix24\SDK\Services\CRM\Deal\Service\Deal; use Bitrix24\SDK\Services\CRM\Deal\Service\DealProductRows; use Bitrix24\SDK\Tests\Integration\Fabric; +use Money\Currencies\ISOCurrencies; +use Money\Currency; +use Money\Formatter\DecimalMoneyFormatter; +use Money\Money; use PHPUnit\Framework\TestCase; /** @@ -28,46 +32,67 @@ class DealProductRowsTest extends TestCase */ public function testSet(): void { + + $callCosts = new Money(1050, new Currency('USD')); + $currencies = new ISOCurrencies(); + + $moneyFormatter = new DecimalMoneyFormatter($currencies); $newDealId = $this->dealService->add(['TITLE' => 'test deal'])->getId(); - $this::assertCount(0, $this->dealProductRowsService->get($newDealId)->getProductRows()); + $this::assertCount(5, $this->dealProductRowsService->get($newDealId)->getProductRows()); $this::assertTrue( $this->dealProductRowsService->set( $newDealId, [ [ - 'PRODUCT_NAME' => 'qqqq', + 'PRODUCT_NAME' => 'wine', + 'PRICE' => $moneyFormatter->format($callCosts), ], ] )->isSuccess() ); $this::assertCount(1, $this->dealProductRowsService->get($newDealId)->getProductRows()); + + } /** * @throws BaseException * @throws TransportException - * @covers \Bitrix24\SDK\Services\CRM\Deal\Service\DealProductRows::get */ public function testGet(): void { - $newDealId = $this->dealService->add(['TITLE' => 'test deal'])->getId(); - $this::assertCount(0, $this->dealProductRowsService->get($newDealId)->getProductRows()); + $callCosts = new Money(1050, new Currency('USD')); + $currencies = new ISOCurrencies(); + + $moneyFormatter = new DecimalMoneyFormatter($currencies); + $newDealId = $this->dealService->add(['TITLE' => 'test deal', 'CURRENCY_ID' => $callCosts->getCurrency()->getCode()])->getId(); $this::assertTrue( $this->dealProductRowsService->set( $newDealId, [ [ - 'PRODUCT_NAME' => 'qqqq', + 'PRODUCT_NAME' => 'wine', + 'PRICE' => $moneyFormatter->format($callCosts), ], ] )->isSuccess() ); - $this::assertCount(1, $this->dealProductRowsService->get($newDealId)->getProductRows()); + $currency = $callCosts->getCurrency(); + + $resultWithoutAvailableCurrency = $this->dealProductRowsService->get($newDealId); + $resultWithAvailableCurrency = $this->dealProductRowsService->get($newDealId, $currency); + foreach ($resultWithoutAvailableCurrency->getProductRows() as $productRow) { + $this::assertEquals($callCosts, $productRow->PRICE); + } + foreach ($resultWithAvailableCurrency->getProductRows() as $productRow) { + $this::assertEquals($callCosts, $productRow->PRICE); + } } public function setUp(): void { $this->dealService = Fabric::getServiceBuilder()->getCRMScope()->deal(); $this->dealProductRowsService = Fabric::getServiceBuilder()->getCRMScope()->dealProductRows(); + $this->core = Fabric::getCore(); } } \ No newline at end of file diff --git a/tests/Integration/Services/CRM/Duplicates/Service/DuplicateTest.php b/tests/Integration/Services/CRM/Duplicates/Service/DuplicateTest.php new file mode 100644 index 00000000..d2efa86f --- /dev/null +++ b/tests/Integration/Services/CRM/Duplicates/Service/DuplicateTest.php @@ -0,0 +1,80 @@ +duplicate->findByEmail([sprintf('%s@gmail.com', time())]); + $this->assertFalse($res->hasDuplicateContacts()); + $this->assertFalse($res->hasOneContact()); + $this->assertCount(0, $res->getContactsId()); + } + + /** + * @return void + * @throws BaseException + * @throws TransportException + * @covers \Bitrix24\SDK\Services\CRM\Duplicates\Service\Duplicate::findByEmail + */ + public function testDuplicatesByEmailOneItemFound(): void + { + $email = sprintf('%s@gmail.com', time()); + $b24ContactId = $this->contactService->add([ + 'NAME' => 'Test', + 'LAST_NAME' => 'Test', + 'EMAIL' => [ + [ + 'VALUE' => $email, + 'TYPE' => 'WORK' + ] + ] + ])->getId(); + + $res = $this->duplicate->findByEmail([$email]); + $this->assertFalse($res->hasDuplicateContacts()); + $this->assertTrue($res->hasOneContact()); + $this->assertCount(1, $res->getContactsId()); + } + + /** + * @return void + * @throws BaseException + * @throws TransportException + * @covers \Bitrix24\SDK\Services\CRM\Duplicates\Service\Duplicate::findByPhone + */ + public function testDuplicatesByPhoneNotFound(): void + { + $res = $this->duplicate->findByPhone([sprintf('+1%s', time())]); + $this->assertFalse($res->hasDuplicateContacts()); + $this->assertFalse($res->hasOneContact()); + $this->assertCount(0, $res->getContactsId()); + } + + + public function setUp(): void + { + $this->contactService = Fabric::getServiceBuilder()->getCRMScope()->contact(); + $this->duplicate = Fabric::getServiceBuilder()->getCRMScope()->duplicate(); + + } +} \ No newline at end of file diff --git a/tests/Integration/Services/Catalog/Catalog/Service/CatalogTest.php b/tests/Integration/Services/Catalog/Catalog/Service/CatalogTest.php new file mode 100644 index 00000000..dc106b5b --- /dev/null +++ b/tests/Integration/Services/Catalog/Catalog/Service/CatalogTest.php @@ -0,0 +1,62 @@ +assertIsArray($this->service->fields()->getFieldsDescription()); + } + + /** + * Test the List method. + * + * @return void + * @throws BaseException if there is a base exception occurred + * @throws TransportException if there is a transport exception occurred + * @covers \Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog::list + */ + public function testList(): void + { + $this->assertGreaterThan(1, $this->service->list([], [], [], 1)->getCatalogs()[0]->id); + } + + /** + * Retrieves a catalog using the `get` method and asserts that the retrieved catalog's ID matches the original catalog's ID. + * + * @return void + * @throws BaseException if there is a general exception. + * @throws TransportException if there is an exception during transport. + * @covers \Bitrix24\SDK\Services\Catalog\Catalog\Service\Catalog::get + */ + public function testGet(): void + { + $catalog = $this->service->list([], [], [], 1)->getCatalogs()[0]; + $this->assertEquals($catalog->id, $this->service->get($catalog->id)->catalog()->id); + } + + public function setUp(): void + { + $this->service = Fabric::getServiceBuilder()->getCatalogScope()->catalog(); + } +} \ No newline at end of file diff --git a/tests/Integration/Services/Catalog/Product/Service/ProductTest.php b/tests/Integration/Services/Catalog/Product/Service/ProductTest.php new file mode 100644 index 00000000..5628390e --- /dev/null +++ b/tests/Integration/Services/Catalog/Product/Service/ProductTest.php @@ -0,0 +1,159 @@ +catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + $this->assertIsArray($this->productService->fieldsByFilter( + $iblockId, + ProductType::simple + )->getFieldsDescription()); + } + + /** + * Adds a new product to the system and asserts that the product was added successfully. + * + * @return void + * @throws BaseException If there is a base exception thrown during the product addition process. + * @throws TransportException If there is a transport exception thrown during the product addition process. + * @covers Product::add() + */ + public function testAdd(): void + { + $iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + $fields = [ + 'iblockId' => $iblockId, + 'name' => sprintf('test product name %s', time()), + '' + ]; + $result = $this->productService->add($fields); + $this->assertEquals($fields['name'], $result->product()->name); + $this->productService->delete($result->product()->id); + } + + /** + * Retrieves a product from the system and asserts that the correct product was retrieved. + * + * @return void + * @throws BaseException If there is a base exception thrown during the product retrieval process. + * @throws TransportException If there is a transport exception thrown during the product retrieval process. + * @covers Product::get() + */ + public function testGet(): void + { + $iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + $fields = [ + 'iblockId' => $iblockId, + 'name' => sprintf('test product name %s', time()), + ]; + $result = $this->productService->add($fields); + $productGet = $this->productService->get($result->product()->id); + $this->assertEquals($result->product()->id, $productGet->product()->id); + $this->productService->delete($productGet->product()->id); + } + + /** + * Deletes a product from the system and asserts that the product was deleted successfully. + * + * @return void + * @throws BaseException If there is a base exception thrown during the product deletion process. + * @throws TransportException If there is a transport exception thrown during the product deletion process. + * @covers Product::delete() + * @testdox test Product::delete + */ + public function testDelete(): void + { + $iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + $fields = [ + 'iblockId' => $iblockId, + 'name' => sprintf('test product name %s', time()), + ]; + $result = $this->productService->add($fields); + $productGet = $this->productService->get($result->product()->id); + $this->assertEquals($result->product()->id, $productGet->product()->id); + $this->productService->delete($productGet->product()->id); + + $filteredProducts = $this->productService->list( + [ + 'id', + 'iblockId' + ], + [ + 'id' => $productGet->product()->id, + 'iblockId' => $iblockId + ], + [ + 'id' => 'asc' + ], + 1 + ); + $this->assertCount(0, $filteredProducts->getProducts()); + } + + /** + * Retrieves a list of products that match the specified filter criteria and asserts that the expected number of products is returned. + * + * @return void + * @throws BaseException If there is a base exception thrown during the process of listing products. + * @throws TransportException If there is a transport exception thrown during the process of listing products. + * @covers Product::list() + */ + public function testList():void + { + $iblockId = $this->catalogService->list([], [], [], 1)->getCatalogs()[0]->iblockId; + $fields = [ + 'iblockId' => $iblockId, + 'name' => sprintf('test product name %s', time()), + ]; + $result = $this->productService->add($fields); + $productGet = $this->productService->get($result->product()->id); + $this->assertEquals($result->product()->id, $productGet->product()->id); + $filteredProducts = $this->productService->list( + [ + 'id', + 'iblockId' + ], + [ + 'id' => $productGet->product()->id, + 'iblockId' => $iblockId + ], + [ + 'id' => 'asc' + ], + 1 + ); + $this->assertCount(1, $filteredProducts->getProducts()); + } + + public function setUp(): void + { + $this->productService = Fabric::getServiceBuilder()->getCatalogScope()->product(); + $this->catalogService = Fabric::getServiceBuilder()->getCatalogScope()->catalog(); + } +} \ No newline at end of file diff --git a/tests/Integration/Services/Telephony/Service/CallTest.php b/tests/Integration/Services/Telephony/Service/CallTest.php index 1993a385..a3d5e266 100644 --- a/tests/Integration/Services/Telephony/Service/CallTest.php +++ b/tests/Integration/Services/Telephony/Service/CallTest.php @@ -116,7 +116,7 @@ public function testAttachTranscription(): void 'ADD_TO_CHAT' => 1 ])->getExternalCallFinish(); - self::assertGreaterThan(1, + self::assertGreaterThanOrEqual(1, $this->callService->attachTranscription( $registerCallResult->CALL_ID, $callCosts, diff --git a/tests/Integration/Services/User/Service/UserTest.php b/tests/Integration/Services/User/Service/UserTest.php new file mode 100644 index 00000000..e8f0ab26 --- /dev/null +++ b/tests/Integration/Services/User/Service/UserTest.php @@ -0,0 +1,132 @@ +userService->search([ + 'NAME' => 'test', + ]); + $this->assertGreaterThanOrEqual(1, $users->getCoreResponse()->getResponseData()->getPagination()->getTotal()); + } + + /** + * @return void + * @throws BaseException + * @throws TransportException + * @covers \Bitrix24\SDK\Services\User\Service\User::get + * @testdox test get users list with internal phone + */ + public function testGetWithInternalPhone(): void + { + $this->assertGreaterThanOrEqual( + 1, + $this->userService->get(['ID' => 'ASC'], [ + '!UF_PHONE_INNER' => null + ], true)->getCoreResponse()->getResponseData()->getPagination()->getTotal() + ); + } + + /** + * @covers \Bitrix24\SDK\Services\User\Service\User::get + * @testdox test get users list + * @return void + * @throws BaseException + * @throws TransportException + */ + public function testGet(): void + { + $this->assertGreaterThanOrEqual( + 1, + $this->userService->get(['ID' => 'ASC'], [], true)->getCoreResponse()->getResponseData()->getPagination()->getTotal() + ); + } + + public function testUpdate(): void + { + $newUser = [ + 'NAME' => 'Test', + 'EMAIL' => sprintf('%s.test@test.com', time()), + 'EXTRANET' => 'N', + 'UF_DEPARTMENT' => [1] + ]; + $userId = $this->userService->add($newUser)->getId(); + $this->assertTrue($this->userService->update($userId, ['NAME' => 'Test2'])->isSuccess()); + $this->assertEquals( + 'Test2', + $this->userService->get(['ID' => 'ASC'], [ + 'ID' => $userId + ])->getUsers()[0]->NAME + ); + } + + /** + * @covers \Bitrix24\SDK\Services\User\Service\User::add + * @testdox test add user + * @return void + * @throws BaseException + * @throws TransportException + */ + public function testAdd(): void + { + $newUser = [ + 'NAME' => 'Test', + 'EMAIL' => sprintf('%s.test@test.com', time()), + 'EXTRANET' => 'N', + 'UF_DEPARTMENT' => [1] + ]; + $userId = $this->userService->add($newUser)->getId(); + $this->assertGreaterThanOrEqual(1, $userId); + } + + /** + * @covers \Bitrix24\SDK\Services\User\Service\User::current + * @testdox test get current user + * @return void + * @throws BaseException + * @throws TransportException + */ + public function testUserCurrent(): void + { + $this->assertInstanceOf(UserItemResult::class, $this->userService->current()->user()); + } + + /** + * @covers \Bitrix24\SDK\Services\User\Service\User::fields + * @testdox test get user fields + * @return void + * @throws BaseException + * @throws TransportException + */ + public function testGetUserFields(): void + { + $this->assertIsArray($this->userService->fields()->getFieldsDescription()); + } + + public function setUp(): void + { + $this->userService = Fabric::getServiceBuilder()->getUserScope()->user(); + } +} \ No newline at end of file diff --git a/tests/Temp/OperatingTimingTest.php b/tests/Temp/OperatingTimingTest.php new file mode 100644 index 00000000..50ffbb87 --- /dev/null +++ b/tests/Temp/OperatingTimingTest.php @@ -0,0 +1,153 @@ +getContactsUpdateCommand(15000); +// +// +// //todo считать количество контактов для обновления и считать количество контактов которые обновили, если не совпало, то падаем с ошибкой +// +// // обновляем контакты в батч-режиме +// $cnt = 0; +// foreach ($this->contactService->batch->update($contactsToUpdate) as $b24ContactId => $contactUpdateResult) { +// $cnt++; +// +// $debugOperatingLog = sprintf( +// 'cnt %s |id %s |operating %s |cur_time %s |op_reset_at %s → %s', +// $cnt, +// $b24ContactId, +// $contactUpdateResult->getResponseData()->getTime()->getOperating(), +// $contactUpdateResult->getResponseData()->getTime()->getDateFinish()->format('Y-m-d H:i:s'), +// $contactUpdateResult->getResponseData()->getTime()->getOperatingResetAt(), +// (new DateTime)->setTimestamp($contactUpdateResult->getResponseData()->getTime()->getOperatingResetAt())->format('Y-m-d H:i:s') +// ) . PHP_EOL; +// file_put_contents('operating.log', $debugOperatingLog); +// } +// +// $this->assertEquals( +// count($contactsToUpdate), +// $cnt, +// sprintf('updated contacts count %s not equal to expected %s cmd items', $cnt, count($contactsToUpdate)) +// ); + + // шаг 1 - выброс корректного исключения, что мол упали из за блокировки метода + // проблемы: - можно потерять часть данных при обновлении, т.к. мы не знаем, какие контакты в клиентском коде обновились, а какие нет или знаем? + +// todo уточнение, по возможности возвращать в исключении остаток данных, которые не успели обновиться + +//[2023-04-15T14:17:57.881428+00:00] integration-test.INFO: getResponseData.responseBody {"responseBody": +//{"result": +//{ +// "result":[], +// "result_error": +// { +// "592dcd1e-cd14-410f-bab5-76b3ede717dd": +// { +// "error":"OPERATION_TIME_LIMIT", +// "error_description":"Method is blocked due to operation time limit." +// } +// }, +// "result_total":[], +// "result_next":[], +// "result_time":[]}, +// "time":{ +// "start":1681568278.071253, +// "finish":1681568278.097257, +// "duration":0.02600383758544922, +// "processing":0.0005891323089599609, +// "date_start":"2023-04-15T17:17:58+03:00", +// "date_finish":"2023-04-15T17:17:58+03:00", +// "operating_reset_at":1681568878, +// "operating":0 +// } +//} +//} {"file":"/Users/mesilov/work/msk03-dev/loyalty/bitrix24-php-sdk/src/Core/Response/Response.php","line":92,"class":"Bitrix24\\SDK\\Core\\Response\\Response","function":"getResponseData","memory_usage":"36 MB"} +//[2023-04-15T14:17:57.881514+00:00] integration-test.DEBUG: handleApiLevelErrors.start [] {"file":"/Users/mesilov/work/msk03-dev/loyalty/bitrix24-php-sdk/src/Core/Response/Response.php","line":152,"class":"Bitrix24\\SDK\\Core\\Response\\Response","function":"handleApiLevelErrors","memory_usage":"36 MB"} +//[2023-04-15T14:17:57.881645+00:00] integration-test.DEBUG: handleApiLevelErrors.finish [] {"file":"/Users/mesilov/work/msk03-dev/loyalty/bitrix24-php-sdk/src/Core/Response/Response.php","line":190,"class":"Bitrix24\\SDK\\Core\\Response\\Response","function":"handleApiLevelErrors","memory_usage":"36 MB"} +//[2023-04-15T14:17:57.881795+00:00] integration-test.DEBUG: getResponseData.parseResponse.finish [] +//[2023-04-15T14:37:47.371152+00:00] integration-test.INFO: getResponseData.responseBody {"responseBody":{"result":{"result":[],"result_error":{"f26b4ebc-3a82-4fe6-8d26-595d6eaf029b":{"error":"OPERATION_TIME_LIMIT","error_description":"Method is blocked due to operation time limit."}},"result_total":[],"result_next":[],"result_time":[]},"time":{"start":1681569467.49515,"finish":1681569467.519364,"duration":0.02421402931213379,"processing":0.0005979537963867188,"date_start":"2023-04-15T17:37:47+03:00","date_finish":"2023-04-15T17:37:47+03:00","operating_reset_at":1681570067,"operating":0}}} {"file":"/Users/mesilov/work/msk03-dev/loyalty/bitrix24-php-sdk/src/Core/Response/Response.php","line":92,"class":"Bitrix24\\SDK\\Core\\Response\\Response","function":"getResponseData","memory_usage":"36 MB"} +//[2023-04-15T14:37:47.371279+00:00] integration-test.DEBUG: handleApiLevelErrors.start [] {"file":"/Users/mesilov/work/msk03-dev/loyalty/bitrix24-php-sdk/src/Core/Response/Response.php","line":152,"class":"Bitrix24\\SDK\\Core\\Response\\Response","function":"handleApiLevelErrors","memory_usage":"36 MB"} + + + // шаг 2 - сделать отдельные стратегии с логикой для батча и придумать, как может быть + // - 2.1 ожидание разблокировки метода без завершения работы батча, т.е. скрипт будет висеть 10 минут, потом попробует продолжить работу, такое можно делать толкьо осознавая последсвия + // - 2.2 выброс события \ вызов обработчика за N секунд до блокировки метода, т.е делегируем логику обработки в клиентский код + + + } + + /** + * Get contacts for update command + * + * @param int $contactsToUpdateCount + * @return array + * @throws \Bitrix24\SDK\Core\Exceptions\BaseException + */ + private function getContactsUpdateCommand(int $contactsToUpdateCount): array + { + $filter = ['>ID' => '1']; + + $contactsCount = $this->contactService->countByFilter($filter); + if ($contactsCount < $contactsToUpdateCount) { + throw new RuntimeException(sprintf('Not enough contacts for test. Need %s, but have %s', $contactsToUpdateCount, $contactsCount)); + } + + $contactsToUpdate = []; + foreach ($this->contactService->batch->list([], $filter, ['ID', 'COMMENTS'], $contactsToUpdateCount) as $b24Contact) { + $contactsToUpdate[$b24Contact->ID] = [ + 'fields' => [ + 'COMMENTS' => $b24Contact->COMMENTS . time() . PHP_EOL, + ], + 'params' => [], + ]; + } + return $contactsToUpdate; + } + + public function setUp(): void + { + $this->batch = Fabric::getBatchService(); + $this->contactService = Fabric::getServiceBuilder()->getCRMScope()->contact(); + } +} \ No newline at end of file diff --git a/tests/Unit/Application/ApplicationStatusTest.php b/tests/Unit/Application/ApplicationStatusTest.php index cdf70fb6..d768b0e4 100644 --- a/tests/Unit/Application/ApplicationStatusTest.php +++ b/tests/Unit/Application/ApplicationStatusTest.php @@ -19,7 +19,7 @@ class ApplicationStatusTest extends TestCase * @dataProvider statusDataProvider * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException */ - public function testGetStatusCode(string $shortCode, string $longCode) + public function testGetStatusCode(string $shortCode, string $longCode): void { $this->assertEquals( $longCode, @@ -36,6 +36,16 @@ public function testInvalidStatusCode(): void new ApplicationStatus('foo'); } + /** + * @return void + * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + * @covers \Bitrix24\SDK\Application\ApplicationStatus::initFromString + */ + public function testInitFromString(): void + { + $this->assertTrue(ApplicationStatus::initFromString('F')->isFree()); + } + /** * @return \Generator */ diff --git a/tests/Unit/Core/ApiLevelErrorHandlerTest.php b/tests/Unit/Core/ApiLevelErrorHandlerTest.php new file mode 100644 index 00000000..f8a96fee --- /dev/null +++ b/tests/Unit/Core/ApiLevelErrorHandlerTest.php @@ -0,0 +1,51 @@ +expectException(OperationTimeLimitExceededException::class); + + $response = [ + 'result' => [ + 'result' => [], + 'result_error' => [ + "592dcd1e-cd14-410f-bab5-76b3ede717dd" => [ + 'error' => 'OPERATION_TIME_LIMIT', + 'error_description' => 'Method is blocked due to operation time limit.' + ] + ] + ], + ]; + + $this->apiLevelErrorHandler->handle($response); + } + + public function setUp(): void + { + $this->apiLevelErrorHandler = new ApiLevelErrorHandler(new NullLogger()); + } +} diff --git a/tests/Unit/Core/Credentials/ApplicationProfileTest.php b/tests/Unit/Core/Credentials/ApplicationProfileTest.php index a66a38ce..17956aac 100644 --- a/tests/Unit/Core/Credentials/ApplicationProfileTest.php +++ b/tests/Unit/Core/Credentials/ApplicationProfileTest.php @@ -13,11 +13,11 @@ class ApplicationProfileTest extends TestCase { /** * - * @param array $arr + * @param array $arr * @param string|null $expectedException * * @return void - * @throws \Bitrix24\SDK\Core\Exceptions\InvalidArgumentException + * @throws InvalidArgumentException * @dataProvider arrayDataProvider */ public function testFromArray(array $arr, ?string $expectedException): void @@ -35,35 +35,43 @@ public function arrayDataProvider(): Generator { yield 'valid' => [ [ - 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => '1', + 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => '1', 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET' => '2', - 'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'user', + 'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'user', ], null, ]; yield 'without client id' => [ [ - '' => '1', + '' => '1', 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET' => '2', - 'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'user', + 'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'user', ], InvalidArgumentException::class, ]; yield 'without client secret' => [ [ 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => '1', - '' => '2', - 'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'user', + '' => '2', + 'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => 'user', ], InvalidArgumentException::class, ]; yield 'without client application scope' => [ [ - 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => '1', + 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => '1', 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET' => '2', - '' => 'user', + '' => 'user', ], InvalidArgumentException::class, ]; + yield 'with empty scope' => [ + [ + 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_ID' => '1', + 'BITRIX24_PHP_SDK_APPLICATION_CLIENT_SECRET' => '2', + 'BITRIX24_PHP_SDK_APPLICATION_SCOPE' => '', + ], + null + ]; } } diff --git a/tests/Unit/Core/Credentials/ScopeTest.php b/tests/Unit/Core/Credentials/ScopeTest.php index 5ffd952d..e974fd36 100644 --- a/tests/Unit/Core/Credentials/ScopeTest.php +++ b/tests/Unit/Core/Credentials/ScopeTest.php @@ -73,6 +73,15 @@ public function testUnknownScope(): void $scope = new Scope(['fooo']); } + /** + * @throws UnknownScopeCodeException + */ + public function testEmptyScope(): void + { + $scope = new Scope(['']); + $this->assertEquals([], $scope->getScopeCodes()); + } + /** * @throws UnknownScopeCodeException */ @@ -82,4 +91,16 @@ public function testWrongScopeCode(): void $this->assertEquals(['crm', 'call', 'im'], $scope->getScopeCodes()); } + + /** + * @return void + * @throws \Bitrix24\SDK\Core\Exceptions\UnknownScopeCodeException + * @covers \Bitrix24\SDK\Core\Credentials\Scope::initFromString + * @testdox Test init Scope from string + */ + public function testInitFromString(): void + { + $scope = Scope::initFromString('crm,telephony,call,user_basic,placement,im,imopenlines'); + $this->assertEquals(['crm', 'telephony', 'call', 'user_basic', 'placement', 'im', 'imopenlines'], $scope->getScopeCodes()); + } } diff --git a/tests/Unit/Infrastructure/HttpClient/RequestId/DefaultRequestIdGeneratorTest.php b/tests/Unit/Infrastructure/HttpClient/RequestId/DefaultRequestIdGeneratorTest.php new file mode 100644 index 00000000..fab333b8 --- /dev/null +++ b/tests/Unit/Infrastructure/HttpClient/RequestId/DefaultRequestIdGeneratorTest.php @@ -0,0 +1,44 @@ +assertEquals($requestId, $gen->getRequestId()); + unset($_SERVER[$requestIdKey]); + } + + public function requestIdKeyDataProvider(): Generator + { + yield 'REQUEST_ID' => [ + 'REQUEST_ID', + Uuid::v7()->toRfc4122() + ]; + yield 'HTTP_X_REQUEST_ID' => [ + 'HTTP_X_REQUEST_ID', + Uuid::v7()->toRfc4122() + ]; + yield 'UNIQUE_ID' => [ + 'UNIQUE_ID', + Uuid::v7()->toRfc4122() + ]; + } +} diff --git a/tests/Unit/Stubs/NullBatch.php b/tests/Unit/Stubs/NullBatch.php index 14ddbb6f..7781fa6f 100644 --- a/tests/Unit/Stubs/NullBatch.php +++ b/tests/Unit/Stubs/NullBatch.php @@ -4,7 +4,7 @@ namespace Bitrix24\SDK\Tests\Unit\Stubs; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Exceptions\BaseException; use Bitrix24\SDK\Core\Response\DTO\ResponseData; use Generator; @@ -14,13 +14,19 @@ * * @package Bitrix24\SDK\Tests\Unit\Stubs */ -class NullBatch implements BatchInterface +class NullBatch implements BatchOperationsInterface { /** + * @param string $apiMethod + * @param array $order + * @param array $filter + * @param array $select + * @param int|null $limit + * @param array|null $additionalParameters * @inheritDoc */ - public function getTraversableList(string $apiMethod, array $order, array $filter, array $select, ?int $limit = null): Generator + public function getTraversableList(string $apiMethod, array $order, array $filter, array $select, ?int $limit = null, ?array $additionalParameters = null): Generator { yield []; } diff --git a/tests/Unit/Stubs/NullCore.php b/tests/Unit/Stubs/NullCore.php index 493da934..13d85e00 100644 --- a/tests/Unit/Stubs/NullCore.php +++ b/tests/Unit/Stubs/NullCore.php @@ -5,37 +5,38 @@ namespace Bitrix24\SDK\Tests\Unit\Stubs; use Bitrix24\SDK\Core\ApiClient; +use Bitrix24\SDK\Core\ApiLevelErrorHandler; use Bitrix24\SDK\Core\Commands\Command; use Bitrix24\SDK\Core\Contracts\ApiClientInterface; use Bitrix24\SDK\Core\Contracts\CoreInterface; use Bitrix24\SDK\Core\Credentials\Credentials; use Bitrix24\SDK\Core\Credentials\WebhookUrl; use Bitrix24\SDK\Core\Response\Response; +use Bitrix24\SDK\Infrastructure\HttpClient\RequestId\DefaultRequestIdGenerator; use Psr\Log\NullLogger; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; -/** - * Class NullCore - * - * @package Bitrix24\SDK\Tests\Unit\Stubs - */ class NullCore implements CoreInterface { /** * @param string $apiMethod - * @param array $parameters + * @param array $parameters * * @return Response * @throws \Exception */ public function call(string $apiMethod, array $parameters = []): Response { - return new Response(new MockResponse(''), new Command('', []), new NullLogger()); + return new Response(new MockResponse(''), new Command('', []), new ApiLevelErrorHandler(new NullLogger()), new NullLogger()); } public function getApiClient(): ApiClientInterface { - return new ApiClient(Credentials::createFromWebhook(new WebhookUrl('')), new MockHttpClient(), new NullLogger()); + return new ApiClient( + Credentials::createFromWebhook(new WebhookUrl('')), + new MockHttpClient(), + new DefaultRequestIdGenerator(), + new NullLogger()); } } \ No newline at end of file diff --git a/tools/Commands/CopyPropertyValues.php b/tools/Commands/CopyPropertyValues.php new file mode 100644 index 00000000..d4cd9d8c --- /dev/null +++ b/tools/Commands/CopyPropertyValues.php @@ -0,0 +1,296 @@ +logger = $logger; + parent::__construct(); + } + + /** + * @param OutputInterface $output + * @param Contact $service + * @param array $updateCmd + * @return void + * @throws BaseException + */ + public function updateItems(OutputInterface $output, Contact $service, array $updateCmd): void + { + $progressBar = new ProgressBar($output, count($updateCmd)); + $progressBar->start(); + + foreach ($service->batch->update($updateCmd) as $item) { + $this->logger->debug('updateItems', [ + 'isUpdated' => $item->isSuccess() === true ? 'Y' : 'N', + ]); + $progressBar->advance(); + } + + $progressBar->finish(); + } + + + public function createUpdateCommand(array $data, string $b24SourceProp, string $b24DestinationProp): array + { + $updateCmd = []; + foreach ($data as $id => $item) { + $updateCmd[$id] = [ + 'fields' => [ + $b24DestinationProp =>$item[$b24SourceProp], + ] + ]; + } + + return $updateCmd; + } + + /** + * @param OutputInterface $output + * @param Contact $service + * @param array $filter + * @param string $b24SourceProp + * @param string $b24DestinationProp + * @return array + * @throws BaseException + * @throws \Bitrix24\SDK\Core\Exceptions\TransportException + * @throws \Bitrix24\SDK\Services\CRM\Userfield\Exceptions\UserfieldNotFoundException + */ + public function readDataFromProperties(OutputInterface $output, Contact $service, array $filter, string $b24SourceProp, string $b24DestinationProp): array + { + $progressBar = new ProgressBar($output, $service->countByFilter($filter)); + $progressBar->start(); + + $data = []; + foreach ($service->batch->list([], $filter, ['ID', $b24SourceProp, $b24DestinationProp]) as $id => $item) { + $data[$item->ID] = [ + $b24SourceProp => (string)$item->getUserfieldByFieldName($b24SourceProp), + $b24DestinationProp => (string)$item->getUserfieldByFieldName($b24DestinationProp), + ]; + $progressBar->advance(); + } + $progressBar->finish(); + + return $data; + } + + protected function configure(): void + { + $this + ->setDescription('copy property values from one property to another') + ->setHelp('copy property values from one property to another in same portal') + ->addOption( + self::WEBHOOK_URL, + null, + InputOption::VALUE_REQUIRED, + 'bitrix24 incoming webhook', + '' + ) + ->addOption( + self::SOURCE_PROPERTY, + null, + InputOption::VALUE_REQUIRED, + 'source property id', + + ) + ->addOption( + self::DESTINATION_PROPERTY, + null, + InputOption::VALUE_REQUIRED, + 'destination property id', + ) + ->addOption( + self::ENTITY_TYPE_PROPERTY, + null, + InputOption::VALUE_REQUIRED, + 'entity type: contact, company, lead, deal', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->logger->debug('CopyPropertyValues.start'); + + $b24Webhook = (string)$input->getOption(self::WEBHOOK_URL); + $b24EntityType = (string)$input->getOption(self::ENTITY_TYPE_PROPERTY); + $b24SourceProp = (string)$input->getOption(self::SOURCE_PROPERTY); + $b24DestinationProp = (string)$input->getOption(self::DESTINATION_PROPERTY); + + $io = new SymfonyStyle($input, $output); + $output->writeln( + [ + 'Copy property values from one property to another', + '========================', + sprintf('webhook url: %s', $b24Webhook), + sprintf('entity type: %s', $b24EntityType), + sprintf('source property: %s', $b24SourceProp), + sprintf('destination property: %s', $b24DestinationProp), + ] + ); + + try { + if ($b24Webhook === '') { + throw new InvalidArgumentException('webhook url is empty'); + } + if ($b24EntityType === '') { + throw new InvalidArgumentException('entity_type is empty'); + } + if ($b24SourceProp === '') { + throw new InvalidArgumentException('source property is empty'); + } + if ($b24DestinationProp === '') { + throw new InvalidArgumentException('destination property is empty'); + } + $sb = (new ServiceBuilderFactory(new EventDispatcher(), $this->logger))->initFromWebhook($b24Webhook); + if (!in_array($b24EntityType, $this->supportedEntityTypes, true)) { + throw new InvalidArgumentException(sprintf('entity type %s is not supported', $b24EntityType)); + } + + $service = null; + switch ($b24EntityType) { + case 'contact': + $fields = $sb->getCRMScope()->contact()->fields(); + $service = $sb->getCRMScope()->contact(); + break; + case 'company': + case 'lead': + case 'deal': + default: + throw new InvalidArgumentException(sprintf('unsupported entity type %s', $b24EntityType)); + } + + if (!array_key_exists($b24SourceProp, $fields->getFieldsDescription())) { + throw new InvalidArgumentException(sprintf('source property «%s» is not found in entity %s', $b24SourceProp, $b24EntityType)); + } + if (!array_key_exists($b24DestinationProp, $fields->getFieldsDescription())) { + throw new InvalidArgumentException(sprintf('destination property «%s» is not found in entity %s', $b24DestinationProp, $b24EntityType)); + } + + // количество элементов c заполненным полем источником + // количество элементов с заполненным полем назначения + // количество элементов у которых заполнено ОБА поля + $totalElementsCnt = $service->countByFilter(); + $elementsWithFilledSourceProp = $service->countByFilter([sprintf('!%s', $b24SourceProp) => null]); + $elementsWithoutFilledSourceProp = $service->countByFilter([sprintf('%s', $b24SourceProp) => null]); + $elementsWithFilledDestinationProp = $service->countByFilter([sprintf('!%s', $b24DestinationProp) => null]); + $elementsWithoutFilledDestinationProp = $service->countByFilter([sprintf('%s', $b24DestinationProp) => null]); + + $io->info( + [ + '', + sprintf('total elements count: %s ', $totalElementsCnt), + sprintf('elements with filled source property: %s ', $elementsWithFilledSourceProp), + sprintf('elements without filled source property: %s ', $elementsWithoutFilledSourceProp), + sprintf('elements with filled destination property: %s ', $elementsWithFilledDestinationProp), + sprintf('elements without filled destination property: %s ', $elementsWithoutFilledDestinationProp) + ] + ); + $io->info('read data from bitrix24...'); + // read data from source and destinations properties + $dataFromProperties = $this->readDataFromProperties($output, $service, [ + sprintf('!%s', $b24SourceProp) => '' + ], $b24SourceProp, $b24DestinationProp); + + // exclude items with filled destination property + $dataToCopy = []; + $conflictData = []; + foreach ($dataFromProperties as $id => $item) { + // pass items with copied values + if ($item[$b24SourceProp] === $item[$b24DestinationProp]) { + continue; + } + + // filter conflict items + if($item[$b24DestinationProp] !== '') { + $conflictData[$id] = $item; + } + // filter items to copy + if($item[$b24DestinationProp] === '') { + $dataToCopy[$id] = $item; + } + } + $io->newLine(); + $io->warning([ + '', + sprintf('conflict items count: %s', count($conflictData)), + sprintf('problem id: %s', implode(', ', array_keys($conflictData))), + ]); + + $io->info([ + '', + sprintf('items to copy count: %s', count($dataToCopy)) + ]); + + // build update command + $updateCmd = $this->createUpdateCommand($dataToCopy, $b24SourceProp, $b24DestinationProp); + + // update items + $this->updateItems($output, $service, $updateCmd); + + $io->success('all items updated'); + } catch (BaseException $exception) { + $io = new SymfonyStyle($input, $output); + $io->caution(sprintf('error message: %s', $exception->getMessage())); + $io->text( + $exception->getTraceAsString() + ); + } catch (\Throwable $exception) { + $io = new SymfonyStyle($input, $output); + $io->caution('unknown error'); + $io->text( + [ + sprintf('%s', $exception->getMessage()), + ] + ); + } + $this->logger->debug('CopyPropertyValues.finish'); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/tools/DemoDataGenerators/CRM/Contacts/GenerateContactsCommand.php b/tools/Commands/GenerateContactsCommand.php similarity index 90% rename from tools/DemoDataGenerators/CRM/Contacts/GenerateContactsCommand.php rename to tools/Commands/GenerateContactsCommand.php index baaa96b4..26880314 100644 --- a/tools/DemoDataGenerators/CRM/Contacts/GenerateContactsCommand.php +++ b/tools/Commands/GenerateContactsCommand.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace Bitrix24\SDK\Tools\DemoDataGenerators\CRM\Contacts; +namespace Bitrix24\SDK\Tools\Commands; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Bitrix24\SDK\Core\Batch; use Bitrix24\SDK\Core\BulkItemsReader\BulkItemsReaderBuilder; use Bitrix24\SDK\Core\CoreBuilder; @@ -13,27 +15,23 @@ use Bitrix24\SDK\Services\ServiceBuilder; use InvalidArgumentException; use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -/** - * Class GenerateContactsCommand - * - * @package Bitrix24\SDK\Tools\DemoDataGenerators\CRM\Contacts - */ +#[AsCommand( + name: 'b24:generate:contacts', + description: 'generate demo-data contacts in CRM', + hidden: false +)] class GenerateContactsCommand extends Command { /** * @var LoggerInterface */ protected LoggerInterface $logger; - /** - * @var string - */ - protected static $defaultName = 'generate:contacts'; protected const CONTACTS_COUNT = 'count'; protected const WEBHOOK_URL = 'webhook'; @@ -76,7 +74,7 @@ protected function configure(): void } /** - * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output * * @return void @@ -90,7 +88,7 @@ protected function interact(InputInterface $input, OutputInterface $output): voi } /** - * @param InputInterface $input + * @param InputInterface $input * @param OutputInterface $output * * @return int @@ -159,7 +157,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); } $timeEnd = microtime(true); - $io->writeln(sprintf('batch query duration: %s seconds', round($timeEnd - $timeStart, 2)) . PHP_EOL . PHP_EOL); + $io->writeln(GenerateContactsCommand . phpsprintf('batch query duration: %s seconds', round($timeEnd - $timeStart, 2)) . PHP_EOL . PHP_EOL); $io->success('contacts added'); } catch (BaseException $exception) { $io = new SymfonyStyle($input, $output); @@ -194,13 +192,13 @@ protected function generateContacts(int $contactsCount): array $contacts = []; for ($i = 0; $i < $contactsCount; $i++) { $contacts[] = [ - 'NAME' => sprintf('name_%s', $i), - 'LAST_NAME' => sprintf('last_%s', $i), + 'NAME' => sprintf('name_%s', $i), + 'LAST_NAME' => sprintf('last_%s', $i), 'SECOND_NAME' => sprintf('second_%s', $i), - 'PHONE' => [ + 'PHONE' => [ ['VALUE' => sprintf('+7978%s', random_int(1000000, 9999999)), 'VALUE_TYPE' => 'MOBILE'], ], - 'EMAIL' => [ + 'EMAIL' => [ ['VALUE' => sprintf('test-%s@gmail.com', random_int(1000000, 9999999)), 'VALUE_TYPE' => 'WORK'], ], ]; diff --git a/tools/PerformanceBenchmarks/ListCommand.php b/tools/Commands/PerformanceBenchmarks/ListCommand.php similarity index 97% rename from tools/PerformanceBenchmarks/ListCommand.php rename to tools/Commands/PerformanceBenchmarks/ListCommand.php index 1a5d06f7..88ed9fc0 100644 --- a/tools/PerformanceBenchmarks/ListCommand.php +++ b/tools/Commands/PerformanceBenchmarks/ListCommand.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Bitrix24\SDK\Tools\PerformanceBenchmarks; +namespace Bitrix24\SDK\Tools\Commands\PerformanceBenchmarks; use Bitrix24\SDK\Core\Batch; -use Bitrix24\SDK\Core\Contracts\BatchInterface; +use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface; use Bitrix24\SDK\Core\Contracts\CoreInterface; use Bitrix24\SDK\Core\CoreBuilder; use Bitrix24\SDK\Core\Credentials\Credentials; @@ -24,21 +24,19 @@ use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Component\Console\Attribute\AsCommand; -/** - * Class ListCommand - * - * @package Bitrix24\SDK\Tools\PerformanceBenchmarks - */ + +#[AsCommand( + name: 'b24:benchmark:list', + description: 'performance benchmark for *.list method', + hidden: false +)] class ListCommand extends Command { protected LoggerInterface $logger; protected CoreInterface $core; - protected BatchInterface $batch; - /** - * @var string - */ - protected static $defaultName = 'benchmark:list'; + protected BatchOperationsInterface $batch; protected const TIME_PRECISION = 4; protected const SELECT_FIELDS_MODE = 'fields'; protected const ELEMENTS_COUNT = 'count'; diff --git a/tools/ShowFieldsDescriptionCommand.php b/tools/Commands/ShowFieldsDescriptionCommand.php similarity index 88% rename from tools/ShowFieldsDescriptionCommand.php rename to tools/Commands/ShowFieldsDescriptionCommand.php index af6254c9..c24d8733 100644 --- a/tools/ShowFieldsDescriptionCommand.php +++ b/tools/Commands/ShowFieldsDescriptionCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Bitrix24\SDK\Tools; +namespace Bitrix24\SDK\Tools\Commands; use Bitrix24\SDK\Core\Contracts\CoreInterface; use Bitrix24\SDK\Core\CoreBuilder; @@ -18,20 +18,18 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Console\Attribute\AsCommand; -/** - * Class ShowFieldsDescriptionCommand - * - * @package Bitrix24\SDK\Tools\PerformanceBenchmarks - */ + +#[AsCommand( + name: 'b24:util:show-fields-description', + description: 'show entity fields description with table or phpDoc output format', + hidden: false +)] class ShowFieldsDescriptionCommand extends Command { protected LoggerInterface $logger; protected CoreInterface $core; - /** - * @var string - */ - protected static $defaultName = 'util:show-fields-description'; protected const WEBHOOK_URL = 'webhook'; protected const OUTPUT_FORMAT = 'output-format'; @@ -71,7 +69,7 @@ protected function configure(): void } /** - * @param InputInterface $input + * @param InputInterface $input * @param OutputInterface $output * * @return int @@ -160,7 +158,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * @param OutputInterface $output - * @param Response $fields + * @param Response $fields * * @throws BaseException */ @@ -175,7 +173,7 @@ private function showFieldsAsPhpDocFunctionSelectSuggest(OutputInterface $output /** * @param OutputInterface $output - * @param Response $fields + * @param Response $fields * * @throws BaseException */ @@ -183,16 +181,15 @@ private function showFieldsAsPhpDocFunctionProperty(OutputInterface $output, Res { $fieldsList = ['*', '* @param array{']; foreach ($fields->getResponseData()->getResult() as $fieldCode => $fieldDescription) { - switch (strtolower($fieldDescription['type'])) { - case 'integer': - $phpDocType = 'int'; - break; - case 'datetime': - $phpDocType = 'string'; - break; - default: - $phpDocType = 'string'; + if (is_array($fieldDescription)) { + $phpDocType = match (strtolower($fieldDescription['type'])) { + 'integer' => 'int', + default => 'string', + }; + } else { + $phpDocType = 'mixed'; } + $fieldsList[] = sprintf('* %s?: %s,', $fieldCode, $phpDocType); } $fieldsList[] = '* } $fields'; @@ -202,7 +199,7 @@ private function showFieldsAsPhpDocFunctionProperty(OutputInterface $output, Res /** * @param OutputInterface $output - * @param Response $fields + * @param Response $fields * * @throws BaseException */ @@ -210,15 +207,14 @@ private function showFieldsAsPhpDocClassHeader(OutputInterface $output, Response { $fieldsList = ['/**', '*']; foreach ($fields->getResponseData()->getResult() as $fieldCode => $fieldDescription) { - switch (strtolower($fieldDescription['type'])) { - case 'integer': - $phpDocType = 'int'; - break; - case 'datetime': - $phpDocType = 'string'; - break; - default: - $phpDocType = 'string'; + + if (is_array($fieldDescription)) { + $phpDocType = match (strtolower($fieldDescription['type'])) { + 'integer' => 'int', + default => 'string', + }; + } else { + $phpDocType = 'mixed'; } $fieldsList[] = sprintf('* @property-read %s $%s', $phpDocType, $fieldCode); } @@ -228,7 +224,7 @@ private function showFieldsAsPhpDocClassHeader(OutputInterface $output, Response /** * @param OutputInterface $output - * @param Response $fields + * @param Response $fields * * @throws BaseException */