1
0
Fork 0
mirror of synced 2025-04-04 06:13:41 +03:00

Compare commits

...

362 commits

Author SHA1 Message Date
fc78e5897b
Merge pull request #221 from opheugene/offer-site
Added site field to ProductOffer
2025-03-20 16:41:04 +03:00
Opheugene
0a862dd86b Added site field to ProductOffer 2025-03-19 17:51:25 +01:00
5a3547894a
Merge pull request #219 from Neur0toxine/fix-cache-action
fix github workflows (cache action)
2025-03-04 20:00:39 +03:00
1d05f3efeb fix github workflows (cache action) 2025-03-04 19:52:24 +03:00
831ed64871
Merge pull request #218 from Neur0toxine/fix-github-workflows
fix github workflows (checkout action)
2025-03-04 19:48:31 +03:00
502a0c8641 fix github workflows (checkout action) 2025-03-04 19:45:09 +03:00
4a99094294
Merge pull request #217 from curse89/master
Add calling setEventDispatcher method in ClientFactory
2025-03-04 19:42:56 +03:00
Сергей Кривич
c379815f76 Upd GH actions config && add calling setEventDispatcher method in CLientFactory 2025-03-04 19:19:35 +03:00
27e9e8eaa5
Merge pull request #216 from retailcrm/integration-embed-js
Add embedJs field to integrations/edit method
2025-01-14 09:23:27 +03:00
Ilyas Salikhov
c21a89c761 Add embedJs field to integrations/edit method 2025-01-13 22:28:31 +03:00
068a9d0e0b
Add customerSubscriptions to Customer entity 2024-12-20 16:34:11 +03:00
Alex Komarichev
1881dd3499 Add customerSubscriptions to Customer entity. Add subscriptions filter to CustomerFilter filter. 2024-12-18 14:42:35 +03:00
Uryvskiy Dima
47edbda927
Add discount amount for update payment request
Add discount amount for update payment request
2024-12-10 15:07:42 +03:00
ellynoize
91c3ca8ce5 add discount in test 2024-12-10 11:46:19 +03:00
ellynoize
6d05a1af3f add for update payment request 2024-12-09 19:43:10 +06:00
Andrey Belikin
16cdc6ce49
Add contact to store reference model (#213) 2024-10-18 10:08:15 +03:00
cdca2b6d6d
Support for urlLike filter in GET /api/v5/store/products 2024-09-25 11:30:20 +03:00
Vlasov
ecaac435cc Added support for urlLike filter in GET /api/v5/store/products 2024-09-24 17:59:01 +03:00
GrishaginEvgeny
e3cc485873
add customFields in CustomerCorporateFilter.php (#210)
Co-authored-by: Евгений Гришагин <grishagin@retailcrm.ru>
2024-09-10 15:00:32 +03:00
4880ed9930
Resolves #206: /api/v5/loyalty/account/{id}/bonus/charge method support 2024-09-04 13:37:32 +03:00
7b393e961b Resolves #206: /api/v5/loyalty/account/{id}/bonus/charge method support 2024-09-04 13:19:20 +03:00
3a3d00aeb8
update version constraint for symfony/console 2024-09-04 13:04:28 +03:00
60624dad9b
Add support for new filters of GET /api/v5/store/offers method 2024-09-04 12:58:19 +03:00
121dcebb32
Replace abandoned php-http/message-factory with psr/http-factory 2024-09-04 12:57:30 +03:00
Viacheslav Akulov
80396691dd
Update composer.json 2024-09-04 10:29:51 +03:00
Viacheslav Akulov
3d5e3e92e6
Update composer.json 2024-09-04 10:27:30 +03:00
RenCurs
6130b265fe
Add support for new filters of GET /api/v5/store/offers method (#205)
* Add support for new filters of GET /api/v5/store/offers method

- filter[min/maxPrice]
- filter[priceType]
- filter[min/maxQuantity]
- filter[catalogs][]

---------

Co-authored-by: Kirill Sukhorukov <suhorukov@retailcrm.ru>
2024-08-27 09:33:04 +03:00
Kirill Sukhorukov
da8ce13736 Add support for new filters of GET /api/v5/store/offers method
- filter[min/maxPrice]
- filter[priceType]
- filter[min/maxQuantity]
2024-08-22 17:15:24 +03:00
Kirill
1c6ebd819f
Add support for GET /api/v5/store/offers method (#203)
Co-authored-by: Kirill Sukhorukov <suhorukov@retailcrm.ru>
2024-08-19 16:01:35 +03:00
Nikolay Parshakov
a7d071af28 Replace abandoned php-http/message-factory to psr/http-factory 2024-08-12 07:26:15 +03:00
544d16186f
Add support for GET "/api/v5/store/products/properties/values" method 2024-08-09 12:46:31 +03:00
Kirill Sukhorukov
dc9e0a5b39 Add support for GET /api/v5/properties/values method 2024-08-07 17:50:13 +03:00
5b04bd9d8a
Support for filtering product properties by groups and sorting by usage in offers 2024-08-06 12:38:45 +03:00
Kirill Sukhorukov
ca1650d8d9 Add filter by ProductGroups and sort by popular for ProductProperties 2024-08-06 10:08:30 +03:00
54ce958c76
Add customer-interaction 2024-08-01 15:14:15 +03:00
Alex Komarichev
be71f22c65 Change params, add matchPath to test 2024-08-01 12:26:50 +03:00
Alex Komarichev
a7cffe45da Add customer-interaction 2024-07-31 18:24:22 +03:00
8b677de616
Fixed ProductFilter types 2024-07-19 12:06:13 +03:00
Opheugene
0ef461e504 fixed array of string 2024-07-18 18:24:03 +02:00
Opheugene
7d65407c7c fixes for tests 2024-07-18 18:20:25 +02:00
Opheugene
ccbff3aba3 Fixed ProductFilter types 2024-07-18 17:33:02 +02:00
71a3a66724
Add support for MG settings 2024-07-09 11:18:34 +03:00
Kirill Sukhorukov
36bf34b511 Add support for MG settings 2024-07-09 11:13:53 +03:00
ca955cd49f
Support for ProductGroup level in filter and response 2024-07-09 09:44:39 +03:00
Kirill Sukhorukov
a2bac7dcd0 Support for ProductGroup level in filter and response 2024-07-09 09:25:01 +03:00
bc5d044282
Customer subscriptions support 2024-06-20 18:05:04 +03:00
Opheugene
0298a1ad58 new fields 2024-06-20 16:47:02 +02:00
Opheugene
b7ae093cc4 Support for working with customer subscriptions 2024-06-20 14:03:20 +02:00
f04a352126
fixed LegalEntity certificateDate format 2024-04-17 15:39:21 +03:00
Владимир Колчин
e3877587bb fixed LegalEntity certificateDate format 2024-04-16 10:40:35 +03:00
fda9208de2
Compatibility with newer Symfony skeleton versions (with Doctrine 3.x) 2024-04-02 11:50:22 +03:00
5bb006588b add explanation for DateTime lexer behavior backport 2024-03-12 13:05:57 +03:00
759c1ee5a8 restore php 7.3 support 2024-03-12 09:59:53 +03:00
18f5b8c196 update phpcs command & composer-compile-plugin 2024-03-12 09:30:21 +03:00
312dfeffd7 better php 7.4 support 2024-03-11 20:45:58 +03:00
7a4d755f83 update phpstan baseline 2024-03-11 20:36:12 +03:00
f53f545c22 WIP: php 7.4 support 2024-03-11 20:31:56 +03:00
8ced4dc8a1 update README.md 2024-03-11 18:39:46 +03:00
49844e50d6 update php test matrix 2024-03-11 18:39:25 +03:00
0d747fa1e7 cq tools fixes 2024-03-11 18:37:34 +03:00
232c22a557 fix customer tags support 2024-03-11 18:24:44 +03:00
4de6a3b798 fix contracts support 2024-03-11 18:18:01 +03:00
82f110d0d0 update liip/serializer 2024-03-11 18:09:10 +03:00
curse89
7cf7cbf467
add allolwed version of psr/http-message (#189)
Co-authored-by: Сергей Кривич <krivich@retailcrm.ru>
2024-03-11 13:00:53 +03:00
Uryvskiy Dima
3927344dde
Add new method for get task comments (#188) 2024-02-08 12:52:58 +03:00
72636563ff
added type property to Product and ProductBatchModel 2024-01-18 16:08:45 +03:00
angelina pehova
37e9ca0110 type fix in Store and StoreTest 2024-01-18 10:20:28 +03:00
af8a79379b
Add history method for tasks 2024-01-17 16:32:18 +03:00
Uryvskiy Dima
a2d7035655 Add description 2024-01-17 00:44:40 +03:00
Uryvskiy Dima
0fce4a49db Fix tests 2024-01-17 00:38:29 +03:00
Uryvskiy Dima
f37434af17 Add test 2024-01-16 18:16:36 +03:00
angelina pehova
2310b3b825 added type var to Product and ProductBatchModel 2024-01-16 14:46:33 +03:00
Uryvskiy Dima
f926b59e36 Add history method for task 2024-01-16 14:00:43 +03:00
fe18dce66a
update resource group list in docs 2024-01-09 12:00:39 +03:00
3054974756
Add feature check method 2023-12-20 15:53:22 +03:00
Alex Komarichev
a7bb610a0a Fix example 2023-12-20 15:48:45 +03:00
725209296c
Add visitsUpload method 2023-12-20 12:53:28 +03:00
Alex Komarichev
a682ce606a Some refactor 2023-12-19 15:55:23 +03:00
Alex Komarichev
5223d0e9f6 Add feature/check method 2023-12-19 15:27:34 +03:00
Artem Osipov
2d0ed4c149 Добавлен метод загрузки визитов веб-аналитики 2023-12-14 19:25:46 +03:00
454a5fac26
Merge pull request #183 from Neur0toxine/phpdocumentor-fix
fix phpDocumentor pipeline & pin phar version
2023-11-27 15:37:16 +03:00
c10f2d3d47 fix phpDocumentor pipeline & pin phar version 2023-11-27 15:32:27 +03:00
Opheugene
37069e8cdd
Updated doctrine/annotations version (#180) 2023-11-15 16:10:03 +03:00
curse89
ef7d5cba25
Fix bonus details response data 2023-10-12 16:57:34 +03:00
b497e37c8e
API client updates 2023-10-03 11:09:27 +03:00
19f2b3dcd1 fix for phpstan 2023-10-03 10:02:39 +03:00
974b0cc8ef update ubuntu version for phpstan 2023-10-03 10:00:01 +03:00
Danila
b6afd33906 Add functionality from recent updates to the library 2023-10-03 10:00:01 +03:00
Danila
2b02c0e116 Add functionality from recent updates to the library 2023-10-03 10:00:00 +03:00
Vladimir Kolchin
6515e39144
Added currency methods + properties (#174)
Added currency methods + properties
2023-08-02 08:32:24 +03:00
curse89
a68705055c
Fix costs upload request model (#175) 2023-08-01 10:00:53 +03:00
Andrey
bb5a205cd1
Merge pull request #173 from oxy-coach/master
Fixed files/upload issue
2023-07-26 10:21:04 +03:00
Владимир Колчин
8476f8946e Fixed files/upload issue 2023-07-24 17:57:42 +03:00
fa0e8a7075
Added non_working_days field to settings. Added attachedTag field to customer 2023-06-27 11:33:07 +03:00
a87630a72b
Добавлены методы загрузки данных веб-аналитики 2023-06-27 11:32:40 +03:00
Vitaly Bormotov
06e34486ac Добавлены методы загрузки данных веб-аналитики 2023-06-27 09:35:39 +05:00
Max Baranikov
aa3c99fa6d Added attachedTag field to customer 2023-06-23 16:30:08 +04:00
Max Baranikov
86d280ba47 Added non_working_days field to settings. 2023-06-23 13:35:40 +04:00
curse89
a42ddcc337
Add field 'sites' to delivery and payment types (#167)
Add field sites to delivery and payment types
2023-05-23 10:50:33 +03:00
azgalot
847fbd0bd0
Merge pull request #166 from ilyavlasoff/add_multiselect_custom_field_support
Added multi dictionary custom field support
2023-05-05 14:23:35 +03:00
Vlasov
47741acd28 Added multi dictionary support 2023-05-03 12:01:58 +03:00
Andrey
bc820d0a69
Merge pull request #164 from AndreyMuriy/master
Add itemDeclaredValues field to deliveryData
2023-04-27 16:36:04 +03:00
Andrey Muriy
12d8381b80 Add itemDeclaredValues field to deliveryData 2023-04-27 16:05:24 +03:00
azgalot
5e2c947f27
Merge pull request #160 from oxy-coach/master
add notifications/send method support
2023-03-31 15:03:35 +03:00
Vladimir Kolchin
b0080ff23c add notifications/send method support 2023-03-31 14:09:58 +03:00
curse89
186b44b0d4
PHP 8.2 support 2023-02-27 16:08:34 +03:00
f75a653a7d
update documentation & update compile:prompt command 2022-11-18 12:17:42 +03:00
c47b6d2bb4 update documentation & update compile:prompt command 2022-11-17 12:21:47 +03:00
9a56566a7f
remove mgCustomerIds filter from /api/v5/customers request 2022-11-08 13:47:48 +03:00
ccd3c07872
add new methods & fix wrong URL bug 2022-11-08 13:47:32 +03:00
d960d43053 remove mgCustomerIds filter from /api/v5/customers request 2022-11-07 17:28:25 +03:00
Кривич Сергей
bbb9af3ac0 Update api-client 2022-10-28 18:01:44 +03:00
040cb49d29
fix for incorrect version removal from URL 2022-10-28 09:56:00 +03:00
Кривич Сергей
66690ac5dd Исправлена очистка версии api 2022-10-21 13:52:52 +03:00
Кривич Сергей
1c68383869 Update .gitignore 2022-10-21 13:50:39 +03:00
fc62d95dbb
/api/v5/loyalty/bonus/operations method 2022-08-09 16:47:41 +03:00
df9d1b6e6f update method docblock 2022-08-09 14:38:03 +03:00
3aa3e65bbe /api/v5/loyalty/bonus/operations method 2022-08-09 14:32:45 +03:00
937b0e4b76
fix certificateDate field format 2022-06-21 13:29:39 +03:00
dd419cb1ef fix certificateDate field format 2022-06-21 13:24:14 +03:00
5b69fc77cf
add ordering field to site definition 2022-05-24 12:55:10 +03:00
36524fc85b add ordering field to site definition 2022-05-24 11:28:08 +03:00
Alex Lushpai
48ed7e168a
addTags and removeTags fields 2022-04-27 10:07:42 +03:00
c1719bf84a addTags and removeTags fields 2022-04-25 11:30:43 +03:00
a5ed9af862
composer 2.3 compatibility, pock update, fixes (#146) 2022-04-06 14:25:28 +03:00
Andrey
13caebdbc2
Add update-scopes method to integration group (#142)
Co-authored-by: Andrey Muriy <andrey.muriy@gmail.com>
2022-04-01 17:26:14 +03:00
RenCurs
0b6cbb6794
Add new field in model for callback of calculate delivery (#145) 2022-03-31 14:48:34 +03:00
RenCurs
c6950a2103
Fix data type of shipment point id for /api/v5/integration-modules/{code}/edit 2022-03-18 15:53:27 +03:00
RenCurs
e206a69d4a
Add models for simple connection callbacks
* Add models for simple connection callbacks
* Add verifier request and refactor models
2022-03-18 10:47:19 +03:00
Alex Lushpai
c796400ccb
Add system-info method to api group 2022-03-17 10:33:40 +03:00
Andrey Muriy
d71a74edcc Add system-info method to api group 2022-03-16 16:45:45 +03:00
123a59fdf8
php 8.1 support (#138)
* fix #136 php 8.1 support
* update test matrix & phpstan
* fix for the phpstan error that should work
* lock serializer version to prevent unexpected updates
2022-02-17 13:50:36 +03:00
Andrey
2ada519c10
Add isError and isPreprocessing fields to delivery status (#140)
* Add `isError` and `isPreprocessing` fields to delivery status
* Add test of the status entity
* Add test for integration editing

Co-authored-by: Andrey Muriy <andrey.muriy@gmail.com>
2022-02-09 12:33:06 +03:00
7124c816b5
Fix for loyaltyAccount property in the response
* fix for loyaltyAccount property in the response
* fix negligible phpstan warning
* fix for the phpstan error that should work
2022-02-08 16:09:44 +03:00
Alex Lushpai
f16cd5f6e7
Fix shipment and delivery date for delivery callback get 2021-12-17 16:07:23 +03:00
Ruslan Efanov
972cd2ce68 fix shipment and delivery date for delivery callback get 2021-12-17 15:22:58 +03:00
5c35b3e3be
Symfony 6.0 compatibility (#134) 2021-12-16 17:06:14 +03:00
Alex Lushpai
8b136b075b
Аdd priceWithDiscount to billing info 2021-12-14 15:18:36 +03:00
RenCurs
6a8f9ec589
Fix data of store in "RequestSave" (#132) 2021-12-14 15:18:01 +03:00
795ae316ff add priceWithDiscount to billing info 2021-12-14 15:10:41 +03:00
e76bb59970
Easiest way to disable the compiler prompt was not mentioned in the documentation (#131) 2021-12-13 10:46:08 +03:00
Сергей Чазов
8c05f269fa
deliveryType and billingInfo for IntegrationModuleEditInfo (#130) 2021-12-13 10:44:39 +03:00
75fd10d40e
Сustom methods support 2021-11-25 11:02:42 +03:00
Сергей Чазов
f0a1adfb3d
Add product batch edit request (#127) 2021-11-22 17:06:00 +03:00
Alex Lushpai
649b60b392
Сorrections for two namespaces & visible and variative fields 2021-11-16 12:42:24 +03:00
Alex Lushpai
f472ca1fde
page and limit request fields for /api/v5/store/products/properties 2021-11-16 12:34:20 +03:00
46954b290c visible & variative params for product properties and filter 2021-11-11 14:33:46 +03:00
fdc01456d2 corrections for two namespaces 2021-11-11 10:24:26 +03:00
5a38c078c7 page and limit request fields for /api/v5/store/products/properties 2021-11-11 10:03:23 +03:00
Alex Lushpai
f2d9be5d4a
Add sites[][isDemo] field for /api/v5/reference/sites method 2021-11-08 12:08:40 +03:00
0b6ed08d49 add sites[][isDemo] field for /api/v5/reference/sites method 2021-11-08 11:38:26 +03:00
Alex Lushpai
c8be9c7891
Add scopes in credentials method 2021-10-26 13:34:46 +03:00
Akolzin Dmitry
a17a816198 add scopes in credentials method 2021-10-26 13:02:54 +03:00
Сергей Чазов
0ada1cd922
Add new loyalty methods (#121)
* add supporting /api/v5/loyalty/account/{}/bonus/{0}/details method
* repair example for getBonusAccountDetails
* add catcher ApiExceptionInterfaces for example for getBonusAccountDetails method
2021-10-15 13:42:17 +03:00
bce2bfaf4c
update pock to the latest version (#120) 2021-09-28 11:21:10 +03:00
tishmaria90
9c6029fa0d
add applyRound and loyaltyLevelId params (#119) 2021-09-28 11:20:48 +03:00
Alex Lushpai
fa93c2bc8d
Merge pull request #118 from Neur0toxine/fix-customer-birthday
closes #117 - fix for customer[birthday] date format
2021-09-03 12:38:29 +03:00
ed6f672cda closes #117 - fix for customer[birthday] date format 2021-09-03 12:00:01 +03:00
Alex Lushpai
cefb9fabcf
Merge pull request #116 from Neur0toxine/fix-custom-fields-create-example
fix for displayArea for custom-fields/create request
2021-08-30 11:55:20 +03:00
822058c241 fix for displayArea for custom-fields/create request 2021-08-30 11:37:47 +03:00
Alex Lushpai
1a5c972842
Merge pull request #115 from Neur0toxine/fix-custom-field-annotation
fix for `CustomFieldsCreateRequest` annotations
2021-08-30 09:50:10 +03:00
b78088d794 fix for CustomFieldsCreateRequest annotations 2021-08-30 09:43:47 +03:00
Alex Lushpai
fa85db6b7f
Replace deprecated bash uploader with the codecov action 2021-08-25 15:50:20 +03:00
Alex Lushpai
2282f2bb4c
Payment create model 2021-08-25 15:47:32 +03:00
f97fa8366f replace deprecated bash uploader with the codecov action 2021-08-25 15:40:43 +03:00
fe3dd0134a payment create model 2021-08-25 15:34:01 +03:00
Alex Lushpai
3d666332c3
PHP 8 support 2021-08-19 12:39:33 +03:00
a250b9d28e PHP 8 support - closes #109 2021-08-19 12:14:24 +03:00
7d3ae80fe7
Support for /api/v5/loyalty/loyalties/{id} and `/api/v5/loyalty/account/{id}
closes #108
2021-08-17 18:33:15 +03:00
Alex Lushpai
cfa975db17
/api/v5/orders/loyalty/cancel-bonus-operations support 2021-07-21 14:46:37 +03:00
153bc6344e /api/v5/orders/loyalty/cancel-bonus-operations support 2021-07-21 14:37:45 +03:00
d7c36b2cc7
fix incorrect composer.json patching (#106) 2021-07-20 16:37:00 +03:00
Alex Lushpai
1bdf8be34c
correct DTO for settings statuses list in the delivery module configuration 2021-07-15 12:36:15 +03:00
d0ebd84cc8 correct DTO for settings statuses list in the delivery module configuration 2021-07-15 10:35:33 +03:00
Alex Lushpai
7040640b57
Extend of TariffListResponse and ShipmentPointListResponse 2021-06-11 11:22:28 +03:00
Alex Lushpai
b17385ffb6
Correct doctrine/annotations version constraint 2021-06-10 18:38:46 +03:00
8899cd6ee2 fix for phpmd 2021-06-10 14:57:24 +03:00
b2bd3ddfe1 correct doctrine/annotations version constraint 2021-06-10 14:51:20 +03:00
Ruslan Efanov
0e2867c9d9 Add extends for class TariffListResponse and ShipmentPoinListResponse 2021-06-09 09:38:19 +03:00
962ff0b1da
New API client 2021-06-02 17:00:32 +03:00
azgalot
6ed0d7eb0d
fixed throwing NotFoundException (#98) 2020-12-29 14:28:36 +03:00
Alex Lushpai
1109578208
NotFoundException for deleted accounts 2020-12-29 13:46:07 +03:00
Артаханов Андрей
db549ab357 added throwing NotFoundException 2020-12-29 13:31:04 +03:00
Alex Lushpai
0a0bb0e461
Update product name, cleanup annotations (#96) 2020-12-15 13:32:52 +03:00
Akolzin Dmitry
d77cb53f10
XDebug instead pcov (#95) 2020-11-25 20:53:56 +03:00
Akolzin Dmitry
f8ba3b1503
secrets (#94) 2020-11-24 16:24:26 +03:00
Alex Lushpai
063fe1f921
Update README.md 2020-11-24 14:24:29 +03:00
Akolzin Dmitry
b66801d7d2
Move CI to GitHub actions (#93) 2020-11-24 14:23:44 +03:00
Alex Lushpai
d566d2b344
Updated fileEdit method 2020-09-24 14:02:25 +03:00
Akolzin Dmitry
2672fcdebf updated method 2020-09-24 13:55:04 +03:00
serega-kasyanow
e68a23885e
Create invoice method (#89)
Co-authored-by: seregakasyanow <serega.kasyanow@yandex.ru>
2020-09-11 16:23:24 +03:00
Yura
5d0e20581c
updated method TelephonyCallEvent (#88) 2020-09-09 12:51:31 +03:00
Akolzin Dmitry
88cb6526b0
Additional request options 2020-08-21 10:24:55 +03:00
Alex Lushpai
e3b452a0c2
Settings method 2020-07-14 11:24:01 +03:00
d9128fa5d1 settings method 2020-07-14 10:07:01 +03:00
Alex Lushpai
963eb19bfd
Units methods 2020-07-03 18:00:26 +03:00
Akolzin Dmitry
4073580394 units methods 2020-07-03 09:58:31 +03:00
Alex Lushpai
30dfcc7c68
Merge pull request #81 from Neur0toxine/master
[fix] fileDownload and fileUpload method works properly now
2020-02-12 14:25:27 +03:00
8ebcdaeebc fix & add tests 2020-02-12 14:07:27 +03:00
9bbf277d92 fix for fileUpload & fileDownload 2020-02-11 10:57:17 +03:00
Ilyas Salikhov
5371a9750e
Merge pull request #79 from moriony/master
Add orders merge technique
2020-01-20 19:35:13 +03:00
Vitaliy Chesnokov
0f05f84d04
Add orders merge technique 2020-01-20 18:32:25 +03:00
Alex Lushpai
73b5b8e1df
Merge pull request #78 from azgalot/master
Поддержка корпоративных клиентов и их компаний при работе с польз. полями
2019-12-17 14:15:57 +03:00
Андрей Артаханов
170e0b8f34 Добавлена поддержка копроративных клиентов и их компаний при работе с пользовательскими полями 2019-12-16 20:08:57 +03:00
Ilyas Salikhov
29fd360e4f
Merge pull request #76 from Neur0toxine/master
[fix] Fill site code for /telephony/call/event and /telephony/call/upload
2019-12-09 10:39:47 +03:00
8cd0837aa0 [fix] Fill site code for /telephony/call/event and /telephony/call/upload 2019-12-09 09:15:57 +03:00
Ilyas Salikhov
2ab21e1ea6
Merge pull request #74 from Neur0toxine/master
Fix invalid condition in fileEdit
2019-11-25 13:37:43 +03:00
b4eccc73af Fixes #73 2019-11-25 11:51:33 +03:00
Alex Lushpai
96f345c657
Merge pull request #72 from Neur0toxine/master
Feature: Methods for corporate clients
2019-10-15 14:57:00 +03:00
df0177ac5e Feature: Methods for corporate clients 2019-10-15 13:13:25 +03:00
Alex Lushpai
8069c0fcbe
Merge pull request #71 from gwinn/master
Files methods
2019-08-30 14:20:23 +03:00
Alex Lushpai
8084401dc6 files methods 2019-08-30 14:10:52 +03:00
Alex Lushpai
6714723dc2
Merge pull request #69 from strelcov/api-payments
Добавлены методы для интеграционных платежей
2019-07-11 14:36:21 +03:00
Alexandr Streltsov
934d3ba751 Add methods for intagration payments 2019-07-11 14:17:26 +03:00
Alex Lushpai
275721aea7
Update Telephony.php 2019-05-30 09:29:51 +03:00
Alex Lushpai
ae1c1fa360
Merge pull request #65 from gwinn/master
add getters for response & raw response
2019-04-23 14:23:42 +03:00
Alex Lushpai
9ba0785e73 update phpdoc 2019-04-23 14:19:41 +03:00
Alex Lushpai
73d7f71c99 add getters for response & raw response 2019-02-28 18:27:27 +03:00
Alex Lushpai
0951e3cfb3
Update README (#63) 2018-03-20 21:28:14 +03:00
Alex Lushpai
2e4e24f507
Fix CostsDelete method (#62)
* change phpunit call to prevent of using global configuration
* fix CostsDelete method
2018-03-06 14:01:00 +03:00
Alex Lushpai
1826dd57d6
Add LimitException (#61)
Add LimitException class to determine if the query limit is exceeded
2018-02-27 12:00:09 +03:00
Alex Lushpai
5d1760c245
Merge pull request #60 from gwinn/master
update api docs requirements
2018-02-21 12:20:12 +03:00
Alex Lushpai
372029e1a2 update api docs requirements 2018-02-21 11:07:39 +03:00
Alex Lushpai
4d59f37980
Merge pull request #58 from gwinn/master
Products properties method
2018-02-21 10:06:36 +03:00
Alex Lushpai
d1a73d7acd Products properties 2018-02-21 09:42:40 +03:00
Alex Lushpai
251c21300a
Merge pull request #57 from gwinn/master
Minor code improvements, move test data to env
2018-02-20 18:01:53 +03:00
Alex Lushpai
6e48a1d222 minor improvements, move test data to env 2018-02-20 17:51:01 +03:00
Alex Lushpai
5e890c9324
Merge pull request #56 from gwinn/master
delivery shipments
2018-01-10 11:39:54 +03:00
Alex Lushpai
d61ab837e8 delivery shipments 2018-01-10 11:37:06 +03:00
Alex Lushpai
6947dfe08c couriers, legal entities and costs references methods 2017-12-20 16:58:23 +03:00
Alex Lushpai
403d04d3c4 costs; upgrade phpunit 2017-12-20 16:35:26 +03:00
Alex Lushpai
440c7fcd8c
Marketplace methods (#51) 2017-11-17 15:02:54 +03:00
Alex Lushpai
c6c5f737bd Merge pull request #50 from devalex86/api-group-methods
Added availlableVersions() and credentials()
2017-09-27 17:34:40 +03:00
Alexander Kulinich
ada3de8770 added availlableVersions() and credentials() 2017-09-27 16:59:56 +03:00
Alex Lushpai
95c24bed4a Merge pull request #49 from BringerPro/master
Fix customFieldsEdit url
2017-09-23 21:31:34 +03:00
Dmitry Mamontov
93c204f322 Fix customFieldsEdit url 2017-09-23 19:02:05 +03:00
Alex Lushpai
aa79e774f1 Merge pull request #48 from vstaheev/paymentCreate
add site attribute in ordersPaymentCreate method
2017-08-18 10:53:54 +03:00
Vladimir Stakheev
25f9010793 add site attribute in ordersPaymentCreate method 2017-08-18 11:16:21 +05:00
Alex Lushpai
3dd663e7a6 Update CustomFields.php 2017-07-28 10:11:47 +03:00
Alex Lushpai
9aab02c189 Payment delete & custom dictionary creation fix (#46) 2017-07-24 15:12:13 +03:00
Alex Lushpai
4f57734e92 Minor bugfixes (#42) 2017-06-22 16:42:42 +03:00
Alex Lushpai
a18767bddc Update to API version 5 (#40)
* combine methods for orders & customer
* update status method for users
* custom fields|dictionaries
* task methods
* segments
* product groups list methods,
* orders payments
* multi-version
* customers notes methods
2017-06-22 00:55:08 +03:00
Ilyas Salikhov
b701fa232c Merge pull request #36 from profitlanding/patch-1
Fixed typing mistake
2017-06-12 05:25:32 +03:00
GreenRobot
40fd0a2f70 Fixed typing mistake 2017-06-12 05:23:48 +07:00
Alex Lushpai
ce2dab2c03 Update README.md 2017-04-27 14:30:14 +03:00
Alex Lushpai
c9384ff54b Update README.ru.md 2017-04-27 14:29:58 +03:00
Gula Andrij
d9bc74c9d4 Update composer.json (#32) 2017-03-07 12:23:20 +03:00
Vitaly Bormotov
cf814c5aae Fix isset($response->field) syntax (#35) 2017-03-07 12:20:27 +03:00
Alex Lushpai
8c583de05b marketplace api method (#31)
* marketplace integration
* update tests
2016-11-28 14:38:03 +03:00
Alex Lushpai
0fad2158be Merge pull request #30 from gwinn/master
fix for delivery tracking
2016-10-30 23:17:14 +04:00
Alex Lushpai
afd681201a fix for delivery tracking 2016-10-30 22:16:11 +03:00
Alex Lushpai
2c77999fcc Merge pull request #29 from cyradin/master
fix storeSettingsEdit
2016-09-29 01:56:49 +03:00
cyradin
7a60213a7e fix storeSettingsEdit 2016-09-22 19:10:16 +03:00
Alex Lushpai
11bae943f2 Merge pull request #28 from gwinn/master
fix controller
2016-09-21 12:57:31 +03:00
Alex Lushpai
5c4413a97c fix controller 2016-09-21 12:56:43 +03:00
Alex Lushpai
5be40d1d09 Merge pull request #27 from gwinn/master
prices methods: get, edit, upload
2016-09-21 00:12:23 +03:00
Alex Lushpai
0168ce754a prices methods: get, edit, upload 2016-09-21 00:09:24 +03:00
Alex Lushpai
64c24fab33 Merge pull request #25 from gwinn/master
minor fixes
2016-08-10 00:39:28 +03:00
Alex Lushpai
1f662a020a fix apigen config 2016-08-10 00:35:46 +03:00
Alex Lushpai
c79fe5b0ab Merge branch 'master' of github.com:gwinn/api-client-php 2016-08-02 18:26:13 +00:00
Alex Lushpai
e4870f1e29 fix setting path for delivery 2016-08-02 18:25:35 +00:00
Alex Lushpai
82df6d5c4c Merge pull request #24 from dmamontov/master
fix settings
2016-08-02 20:00:26 +03:00
Dmitry Mamontov
51b53cfcc1 fix settings
Проверьте внимательно методы , это не единственные ошибки
2016-08-02 16:54:50 +03:00
Alex Lushpai
b1e7dbc52c store settings url fix (#23)
* improve retry

* store settings api url fix
2016-07-28 10:48:15 +03:00
Alex Lushpai
c0c7ac6a7e store settings api url fix 2016-07-28 10:47:24 +03:00
Alex Lushpai
c1c0f5af8c improve retry (#22) 2016-07-26 15:59:07 +04:00
Alex Lushpai
f2d51b017e improve retry 2016-07-26 14:57:13 +03:00
Alex Lushpai
3dc1fbb7a9 Merge pull request #21 from gwinn/master
make retry local, remove redundant error types
2016-07-26 15:42:16 +04:00
Alex Lushpai
c5ddf84813 improve retry 2016-07-26 14:40:31 +03:00
Alex Lushpai
b6b49d5430 improve retry 2016-07-26 14:38:18 +03:00
Alex Lushpai
49f253c228 Merge pull request #20 from gwinn/master
merge v4 to master
2016-07-22 02:37:53 +04:00
Alex Lushpai
8233a670bc merge v4 to master 2016-07-22 01:34:37 +03:00
Alex Lushpai
edf300dfad remove phpunit from vendor 2016-07-22 01:08:09 +03:00
Alex Lushpai
15b5a2b736 remove phpunit from vendor 2016-07-22 01:04:38 +03:00
Alex Lushpai
acf29c8c1b update tests 2016-07-22 01:01:47 +03:00
gwinn
ddf46fd73d update tests 2016-07-21 16:37:41 +03:00
Alex Lushpai
ffaa661384 clean phpunit.xml 2016-07-20 01:35:08 +03:00
Alex Lushpai
de70ffd2bd first alpha 2016-07-11 14:22:06 +03:00
Alex Lushpai
a9b60cfd62 update readme 2016-06-06 17:54:08 +03:00
Alex Lushpai
a6e5afa165 v4 initial commit, users, stores & telephony method updated 2016-06-06 17:53:05 +03:00
Alex Lushpai
d26f609e9f travis update, packs test update 2016-04-14 23:11:51 +03:00
Alex Lushpai
546010b337 travis update 2016-04-14 22:53:59 +03:00
Alex Lushpai
4ccf30d588 travis update 2016-04-14 22:44:50 +03:00
Alex Lushpai
494a4d3432 travis update 2016-04-14 22:40:33 +03:00
Alex Lushpai
338452a169 travis update 2016-04-14 22:28:55 +03:00
Alex Lushpai
0c213eac0e travis 2016-04-14 22:22:18 +03:00
Alex Lushpai
a65fc7ff94 Merge pull request #14 from gwinn/master
paths & mail
2016-04-10 14:58:10 +04:00
Alex Lushpai
347d620959 paths & mail 2016-04-10 13:57:22 +03:00
Alex Lushpai
4606062af8 Merge pull request #13 from gwinn/master
telephony setting method, test & psr compability comments
2016-04-10 01:47:19 +04:00
Alex Lushpai
838b850d48 telephony setting method, test & psr compability comments 2016-04-10 00:46:08 +03:00
Alex Lushpai
118644be43 Update README.md 2016-03-14 23:06:57 +03:00
Alex Lushpai
7f9e8cbb56 Update composer.json 2016-03-14 20:44:21 +03:00
Alex Lushpai
1c695eeda7 Merge pull request #11 from gwinn/master
Packs, telephony, test & docs
2016-03-14 20:34:13 +03:00
Alex Lushpai
1387fbca38 fix readme 2016-03-12 02:03:50 +03:00
Alex Lushpai
4881a416be fix require section 2016-03-12 02:01:43 +03:00
Alex Lushpai
ecb26bc1ff PSR-2, refactoring, tests 2016-03-12 01:54:33 +03:00
Alex Lushpai
31b1d9d0f1 prepare multilanguage readme 2016-03-09 02:34:13 +03:00
Alex Lushpai
9d3e2817fe PSR 2016-03-09 02:31:29 +03:00
Alex Lushpai
9fdb2f0ada packs tests 2016-01-09 15:24:12 +03:00
Alex Lushpai
0b512fa031 packs & telephony methods 2016-01-09 15:23:50 +03:00
Alex Lushpai
5d37588ab2 .gitignore for IDE 2016-01-09 12:02:19 +03:00
Alex Lushpai
04520e7993 merge upstream 2015-12-23 10:26:23 +03:00
Alex Lushpai
700b87fba0 get retry protected 2015-12-23 10:23:10 +03:00
Alex Lushpai
aba730295d Update ApiClient.php
Опечатка в storeInventoriesUpload
2015-12-23 10:12:52 +03:00
Ilyas Salikhov
3a3d0c2658 Merge pull request #9 from linniksa/patch-provide-full-args-to-http-build-query
Provide values for all arguments in http_build_query
2015-07-30 14:45:05 +03:00
Sergey Linnik
e09fc4a58f Provide values for all arguments in http_build_query
Without that http_build_query depend on system settings and may fail
(drupal 6 in default config)
2015-07-30 13:47:15 +03:00
Alex Lushpai
6ee6452748 рекурсивная отправка запроса в случае timeout, количество попыток - 3 2015-06-15 17:50:31 +03:00
Alex Lushpai
1cb4074470 stores get & stores edit 2015-06-15 16:47:47 +03:00
Alex Lushpai
741ee9bb1a stores get & stores edit 2015-06-15 16:40:39 +03:00
Ilyas Salikhov
2098ad3bf2 Merge pull request #6 from gwinn/master
Методы `/orders/packs/history`, `/stores/inventories`, `/stores/inventories/upload`.
2015-05-20 11:41:17 +03:00
Alex Lushpai
67e0f4746e Update Client.php 2015-05-20 11:31:07 +03:00
Alex Lushpai
de6dbdd87f psr2 & rollback for timeout handle 2015-05-08 18:03:05 +03:00
Alex Lushpai
7ec35c2ad8 minor fixes 2015-05-04 18:56:30 +03:00
Alex Lushpai
d1b26b1663 minor fixes 2015-05-04 18:54:57 +03:00
Alex Lushpai
c7e7ed92cf packs & inventories, improve curl connection timeout handler 2015-05-04 18:47:14 +03:00
Alex Lushpai
03466cf0ad Curl retry, curl timeouts & verify options 2015-04-30 17:35:57 +03:00
Alex Lushpai
b4f3c0efb2 Create LICENSE 2015-02-02 16:21:11 +03:00
Ilyas Salikhov
33f34b991e Added sites methods, Added site parameter to order/customer methods 2014-11-27 11:23:44 +03:00
Ilyas Salikhov
1b5af03644 Fix CRM domain 2014-11-17 00:12:48 +03:00
Ilyas Salikhov
1319ff063d Update README.md 2014-11-13 13:22:26 +03:00
Ilyas Salikhov
4eb7a73887 Link to documentation 2014-11-13 13:04:11 +03:00
Ilyas Salikhov
a084f0f183 Moved docs to gh-pages branch 2014-11-13 12:58:05 +03:00
Ilyas Salikhov
695b0555a1 Remove private and protected methods from docs 2014-11-13 09:39:01 +03:00
Ilyas Salikhov
4e78faa228 Add method ordersStatuses 2014-11-13 09:37:53 +03:00
Alex Lushpai
60db8448d7 add apigen doc, minor changes in code for phpcs approval 2014-11-10 03:12:50 +03:00
Ilyas Salikhov
a217a9786e Update README.md 2014-11-07 13:05:40 +03:00
Ilyas Salikhov
91955b3ee4 Update README.md 2014-11-07 11:34:54 +03:00
Ilyas Salikhov
19b483dacb Translate README to russian. 2014-11-07 11:31:33 +03:00
Ilyas Salikhov
8c029959e5 Merge branch 'master' of https://github.com/retailcrm/api-client-php 2014-11-07 11:25:11 +03:00
Ilyas Salikhov
931e11fbfa Separate doc in english 2014-11-07 11:24:50 +03:00
Ilyas Salikhov
0a82ae223d Update README.md 2014-11-06 19:15:51 +03:00
Ilyas Salikhov
cf536edc2c Correct install instructions 2014-11-06 17:24:44 +03:00
Ilyas Salikhov
652938ea89 Merge branch 'v3' 2014-11-06 17:20:07 +03:00
Ilyas Salikhov
65eab8845b Added skipMyChanges in ordersHistory 2014-11-06 17:19:20 +03:00
Ilyas Salikhov
3c2b25982d Merge pull request #3 from retailcrm/v3
New client for API v3
2014-11-06 16:50:23 +03:00
Ilyas Salikhov
ca2b480a26 Added reference and statistic methods 2014-11-06 16:39:55 +03:00
Ilyas Salikhov
de8514fe14 Removed docs 2014-11-06 14:10:48 +03:00
Ilyas Salikhov
5a81516922 Example of the docs 2014-11-06 14:05:32 +03:00
Ilyas Salikhov
a0f583406c Complete orders and customers methods 2014-11-06 13:48:07 +03:00
Ilyas Salikhov
5b1e5651e7 Fix examples 2014-11-06 11:31:47 +03:00
Ilyas Salikhov
a1aa7da86d Add ordersList method 2014-11-06 03:50:15 +03:00
Ilyas Salikhov
be29e86d73 fix in README 2014-11-06 03:20:32 +03:00
Ilyas Salikhov
8b59f079ef Added ordersHistory. Fix test 2014-11-06 03:18:39 +03:00
Ilyas Salikhov
5a9c0bdb7b Added example of usage 2014-11-06 02:55:09 +03:00
Ilyas Salikhov
921dbda756 New version of PHP client for API v3. Not all methods implements 2014-11-06 02:44:52 +03:00
Alex Lushpai
5effd0cd18 cleanup unused code 2014-10-03 19:30:18 +04:00
Ilyas Salikhov
e5b07ed701 Merge pull request #2 from linniksa/patch-add-datetime-support
Add support to DateTime dates
2014-09-22 09:45:27 +04:00
tikijian
b00bbdae91 add page support to orders method 2014-09-19 15:21:26 +04:00
Sergey Linnik
9eefd8d8ba Add support to DateTime dates 2014-09-18 21:38:52 +04:00
tikijian
e15b87fd0f Merge branch 'master' of https://github.com/intarocrm/rest-api-client 2014-09-18 17:49:48 +04:00
tikijian
6260236e09 quickfix 2014-09-18 17:49:31 +04:00
Alex Lushpai
74d41119ed Update README.md 2014-09-18 16:42:24 +04:00
tikijian
88864dcafe orders method 2014-09-18 16:19:36 +04:00
tikijian
225937f12e add_gitignore 2014-09-18 15:08:48 +04:00
Grisha Pomadchin
a53565c8fd customers method fix 2014-07-11 17:19:46 +04:00
Grisha Pomadchin
e5215e7fcb Merge branch 'master' of github.com:intarocrm/rest-api-client 2014-06-20 17:56:36 +04:00
Grisha Pomadchin
c7ab1125c3 v 1.3.0 api v3 2014-06-20 17:56:13 +04:00
Ilyas Salikhov
7345e7f6cd Update RestApi.php 2014-06-17 07:32:13 +04:00
Ilyas Salikhov
3fcfa535de Update README.md 2014-04-21 14:56:52 +04:00
Ilyas Salikhov
b54350ff2f Fix warning of open_basedir 2014-04-13 13:58:37 +04:00
Grisha Pomadchin
a5e7b5253d +method fix name 2014-04-03 13:52:08 +04:00
Grisha Pomadchin
66139a5e8e customers Find method 2014-04-03 13:47:51 +04:00
876 changed files with 88546 additions and 642 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{less,css,yml,json}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

6
.env.dist Normal file
View file

@ -0,0 +1,6 @@
# This file can be used change api url and api key in tests. Useful when you want to use real networking for test case.
# Follow these steps if you want to use real networking for your test case:
# - Replace credentials below with your own.
# - Use `php-http/curl-client` as an argument for `TestClientFactory::createClient`.
API_URL=https://test.retailcrm.pro/
API_KEY=testkey

46
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: CI
on:
push:
branches:
- '**'
tags-ignore:
- '*.*'
pull_request:
jobs:
test:
name: "PHPUnit"
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
steps:
- name: Check out code into the workspace
uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: pcov
- name: Composer cache
uses: actions/cache@v4
with:
path: ${{ env.HOME }}/.composer/cache
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install -o
- name: Configure matchers
uses: mheap/phpunit-matcher-action@v1
- name: Run tests
run: composer run-script phpunit-ci
- name: Coverage
uses: codecov/codecov-action@v4
with:
verbose: true

42
.github/workflows/code_quality.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: "Code Quality Check"
on:
pull_request:
paths:
- "**.php"
- "phpcs.xml"
- ".github/workflows/code_quality.yml"
jobs:
phpcs:
name: "PHP CodeSniffer"
runs-on: ubuntu-latest
steps:
- name: Check out code into the workspace
uses: actions/checkout@v4
- name: Run PHPCS
uses: chekalsky/phpcs-action@v1
phpmd:
name: "PHP MessDetector"
runs-on: ubuntu-latest
steps:
- name: Check out code into the workspace
uses: actions/checkout@v4
- name: Run PHPMD
uses: GeneaLabs/action-reviewdog-phpmd@1.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
level: 'warning'
reporter: github-pr-check
standard: './phpmd.xml'
target_directory: 'src'
phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- name: Check out code into the workspace
uses: actions/checkout@v4
- name: Run PHPStan
uses: docker://oskarstark/phpstan-ga:1.8.0
with:
args: analyse src -c phpstan.neon --memory-limit=1G --no-progress

47
.github/workflows/documentation.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: phpDocumentor
on:
push:
branches:
- 'master'
tags:
- 'v*'
jobs:
documentation:
name: "phpDocumentor"
runs-on: ubuntu-latest
steps:
- name: Check GitHub Pages status
if: ${{ github.ref != 'refs/heads/master' }}
uses: crazy-max/ghaction-github-status@v2
with:
pages_threshold: major_outage
- name: Check out code into the workspace
if: success() && ${{ github.ref != 'refs/heads/master' }}
uses: actions/checkout@v4
- name: Setup PHP 8.3
if: ${{ github.ref != 'refs/heads/master' }}
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- name: Cache phpDocumentor
id: cache-phpdocumentor
uses: actions/cache@v4
with:
path: phpDocumentor.phar
key: phpdocumentor
- name: Download latest phpDocumentor
if: steps.cache-phpdocumentor.outputs.cache-hit != 'true'
run: curl -O -L https://github.com/phpDocumentor/phpDocumentor/releases/download/v3.4.3/phpDocumentor.phar
- name: Generate documentation
if: ${{ github.ref != 'refs/heads/master' }}
run: php phpDocumentor.phar
- name: Deploy documentation to GitHub Pages
if: ${{ github.ref != 'refs/heads/master' }}
uses: crazy-max/ghaction-github-pages@v2
with:
target_branch: gh-pages
build_dir: docs/build/html
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# Composer files.
/vendor
composer.lock
composer.phar
# Code Quality tools artifacts.
coverage.xml
test-report.xml
phpunit.xml
.php_cs.cache
.phpunit.result.cache
# phpDocumentor files.
.phpdoc
docs
phpDocumentor.phar
# Ignore autogenerated code.
models/*.php
models/checksum.json
# Different environment-related files.
.idea
.DS_Store
.settings
.buildpath
.project
.swp
/nbproject
.env
.docker
docker*
Makefile

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-2020 RetailDriver LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

309
README.md
View file

@ -1,55 +1,286 @@
IntaroCRM REST API client
=================
[![Build Status](https://github.com/retailcrm/api-client-php/workflows/CI/badge.svg)](https://github.com/retailcrm/api-client-php/actions)
[![Coverage](https://img.shields.io/codecov/c/gh/retailcrm/api-client-php/master.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/retailcrm/api-client-php)
[![Latest stable](https://img.shields.io/packagist/v/retailcrm/api-client-php.svg)](https://packagist.org/packages/retailcrm/api-client-php)
[![PHP from Packagist](https://img.shields.io/packagist/php-v/retailcrm/api-client-php.svg?logo=php&logoColor=white)](https://packagist.org/packages/retailcrm/api-client-php)
PHP Client for [IntaroCRM REST API](http://docs.intarocrm.ru/rest-api/).
Requirements
------------
# RetailCRM API PHP client
* PHP version 5.3 and above
* PHP-extension CURL
This is the PHP RetailCRM API client. This library allows using of the actual API version.
You can find more info in the [documentation](doc/index.md).
Installation
------------
# Table of contents
Add IntaroCRM REST API client in your composer.json:
* [Requirements](#requirements)
* [Installation](#installation)
* [Usage](#usage)
* [Examples](#examples)
* [Notes](#notes)
* [Documentation](doc/index.md)
```js
{
"require": {
"intarocrm/rest-api-client": "1.2.*"
}
}
## Requirements
* PHP 7.3 and above
* PHP's cURL support
* PHP's JSON support
* Any HTTP client compatible with PSR-18 (covered by the installation instructions).
* Any HTTP factories implementation compatible with PSR-17 (covered by the installation instructions).
* Any HTTP messages implementation compatible with PSR-7 (covered by the installation instructions).
* Other dependencies listed in the `composer.json` (covered by the installation instructions)
## Installation
Follow those steps to install the library:
1. Download and install [Composer](https://getcomposer.org/download/) package manager.
2. Install the library from the Packagist by executing this command:
```bash
composer require retailcrm/api-client-php:"~6.0"
```
Usage
------------
### Create API clent class
``` php
$crmApiClient = new \IntaroCrm\RestApi(
'https://demo.intarocrm.ru',
'T9DMPvuNt7FQJMszHUdG8Fkt6xHsqngH'
);
During the installation you will see this message. Press `'y'` when you do:
```sh
civicrm/composer-compile-plugin contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins
Do you trust "civicrm/composer-compile-plugin" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]
```
Constructor arguments are:
After that, you may see a message which will look like this:
```sh
The following packages have new compilation tasks:
- retailcrm/api-client-php has 1 task
1. Your IntaroCRM acount URL-address
2. Your site API Token
Allow these packages to compile? ([y]es, [a]lways, [n]o, [l]ist, [h]elp)
```
### Example: get order types list
Choose `[a]lways` by typing `a` and pressing Enter.
``` php
**Note:** You should choose `'y'` and `[a]lways` if your application is using CI/CD pipeline because the interactive terminal is not available
in that environment which will result in failure during the dependencies installation.
If you chose something else during the installation and API client doesn't work properly - please follow [these instructions](doc/compilation_prompt.md#ive-chosen-something-else-now-api-client-doesnt-work) to fix the problem.
3. Include the autoloader if it's not included, or you didn't use Composer before.
```php
require 'path/to/vendor/autoload.php';
```
Replace `path/to/vendor/autoload.php` with the correct path to Composer's `autoload.php`.
**Note:** API client uses `php-http/curl-client` and `nyholm/psr7` as a PSR-18, PSR-17 and PSR-7 implementation.
You can replace those implementations during installation by installing this library with the implementation of your choice, like this:
```sh
composer require symfony/http-client guzzlehttp/psr7 retailcrm/api-client-php:"~6.0"
```
More information about that can be found in the [documentation](doc/customization/different_psr_implementations.md).
## Usage
Firstly, you should initialize the Client. The easiest way to do this is to use the `SimpleClientFactory`:
```php
$client = \RetailCrm\Api\Factory\SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
```
The client is separated into several resource groups, all of which are accessible through the Client's public properties.
You can call API methods from those groups like this:
```php
$client->api->credentials();
```
For example, you can retrieve the customers list:
```php
$client = \RetailCrm\Api\Factory\SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
$response = $client->customers->list();
```
Or the orders list:
```php
$client = \RetailCrm\Api\Factory\SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
$response = $client->orders->list();
```
To handle errors you must use two types of exceptions:
* `RetailCrm\Api\Interfaces\ClientExceptionInterface` for the network or other runtime errors.
* `RetailCrm\Api\Interfaces\ApiExceptionInterface` for the errors from the API.
An example of error handling can be found in the next section of this document.
Each resource group is responsible for the corresponding API section. For example, `costs` resource group provide methods
for costs manipulation and `loyalty` resource group allows interacting with loyalty programs, accounts, bonuses, etc.
Use annotations to determine which DTOs you need for sending the requests. If annotations are not provided by your IDE - you
probably should configure them. It'll ease your work with this (and any other) library a lot.
More information about the usage including examples can be found in the [documentation](doc/usage/usage.md).
## Examples
Listing orders:
```php
<?php
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
try {
$orderTypes = $crmApiClient->orderTypesList();
}
catch (\IntaroCrm\Exception\CurlException $e) {
//$logger->addError('orderTypesList: connection error');
}
catch (\IntaroCrm\Exception\ApiException $e) {
//$logger->addError('orderTypesList: ' . $e->getMessage());
$response = $client->orders->list();
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiExceptionInterface and ClientExceptionInterface instance implements __toString() method.
exit(-1);
}
foreach ($response->orders as $order) {
printf("Order ID: %d\n", $order->id);
printf("First name: %s\n", $order->firstName);
printf("Last name: %s\n", $order->lastName);
printf("Patronymic: %s\n", $order->patronymic);
printf("Phone #1: %s\n", $order->phone);
printf("Phone #2: %s\n", $order->additionalPhone);
printf("E-Mail: %s\n", $order->email);
if ($order->customer instanceof CustomerCorporate) {
echo "Customer type: corporate\n";
} else {
echo "Customer type: individual\n";
}
foreach ($order->items as $item) {
echo PHP_EOL;
printf("Product name: %s\n", $item->productName);
printf("Quantity: %d\n", $item->quantity);
printf("Initial price: %f\n", $item->initialPrice);
}
echo PHP_EOL;
printf("Discount: %f\n", $order->discountManualAmount);
printf("Total: %f\n", $order->totalSumm);
echo PHP_EOL;
}
```
Fetching a specific order by it's ID:
```php
<?php
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Enum\ByIdentifier;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Request\BySiteRequest;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
try {
$response = $client->orders->get(1234, new BySiteRequest(ByIdentifier::ID, 'site'));
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiExceptionInterface instance should implement __toString() method.
exit(-1);
}
echo 'Order: ' . print_r($response->order, true);
```
Creating a new customer:
```php
<?php
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Request\Customers\CustomersCreateRequest;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
$request = new CustomersCreateRequest();
$request->customer = new Customer();
$request->site = 'aliexpress';
$request->customer->email = 'john.doe@example.com';
$request->customer->firstName = 'John';
$request->customer->lastName = 'Doe';
try {
$response = $client->customers->create($request);
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiExceptionInterface instance should implement __toString() method.
exit(-1);
}
echo 'Customer ID: ' . $response->id;
```
Creating a task for the user with a specific email:
```php
<?php
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Entity\Tasks\Task;use RetailCrm\Api\Model\Filter\Users\ApiUserFilter;
use RetailCrm\Api\Model\Request\Tasks\TasksCreateRequest;
use RetailCrm\Api\Model\Request\Users\UsersRequest;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
$usersRequest = new UsersRequest();
$usersRequest->filter = new ApiUserFilter();
$usersRequest->filter->email = 'john.doe@example.com';
try {
$usersResponse = $client->users->list($usersRequest);
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiExceptionInterface instance should implement __toString() method.
exit(-1);
}
if (0 === count($usersResponse->users)) {
echo 'User is not found.';
exit(-1);
}
$tasksRequest = new TasksCreateRequest();
$tasksRequest->task = new Task();
$tasksRequest->task->performerId = $usersResponse->users[0]->id;
$tasksRequest->task->text = 'Do something!';
$tasksRequest->site = 'site';
try {
$tasksResponse = $client->tasks->create($tasksRequest);
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiExceptionInterface instance should implement __toString() method.
exit(-1);
}
echo 'Created task with ID: ' . $tasksResponse->id;
```
The error handling in the examples above is good enough for real production usage.
You can safely assume that `ApiExceptionInterface` is an error from the API, and `ClientExceptionInterface` is a client error
(e.g. network error or any runtime error, use `HttpClientException` to catch only PSR-18 client errors).
However, you can implement more complex error handling if you want.
Also, both `ApiExceptionInterface` and `ClientExceptionInterface` implements `__toString()`. This means that you can just
convert those exceptions to string and put the results into logs without any special treatment for the exception data.
More examples can be found in the [documentation](doc/usage/examples/index.md).
You can use a PSR-14 compatible event dispatcher to receive events from the client. See [documentation](doc/index.md) for details.
## Notes
This library uses HTTPlug abstractions. Visit [official documentation](http://docs.php-http.org/en/latest/httplug/users.html) to learn more about it.

49
bin/retailcrm-client Executable file
View file

@ -0,0 +1,49 @@
#!/usr/bin/env php
<?php
namespace RetailCrm\Api;
use ReflectionClass;
use RetailCrm\Api\Component\ComposerLocator;
use RetailCrm\Api\Component\PhpFilesIterator;
use Symfony\Component\Console\Application;
if (!in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
}
set_time_limit(0);
require __DIR__ . '/../src/Component/ComposerLocator.php';
$composerAutoloader = ComposerLocator::findAutoloader();
if ('' === $composerAutoloader) {
echo 'Cannot find autoload.php. Please install dependencies first.' . PHP_EOL;
exit(-1);
}
require $composerAutoloader;
if (!class_exists('Symfony\Component\Console\Application')) {
echo 'Cannot find Symfony\Component\Console\Application class. Please install dependencies first.';
exit(-1);
}
$application = new Application();
$commands = new PhpFilesIterator(implode(DIRECTORY_SEPARATOR, [dirname(__DIR__), 'src', 'Command']));
foreach ($commands as $command) {
if (!array_key_exists('fqn', $command)) {
continue;
}
$commandFqn = $command['fqn'];
if (class_exists($commandFqn) && !(new ReflectionClass($commandFqn))->isAbstract()) {
$application->add(new $commandFqn());
}
}
$application->setName('RetailCRM API Client Management Tool');
$application->run();

View file

@ -1,29 +1,121 @@
{
"name": "intarocrm/rest-api-client",
"description": "PHP Client for IntaroCRM REST API",
"name": "retailcrm/api-client-php",
"description": "PHP client for RetailCRM API",
"type": "library",
"keywords": ["api", "Intaro CRM", "rest"],
"homepage": "http://www.intarocrm.ru/",
"keywords": [
"API",
"RetailCRM",
"REST"
],
"homepage": "http://www.retailcrm.pro/",
"license": "MIT",
"authors": [
{
"name": "Kruglov Kirill",
"email": "kruglov@intaro.ru",
"role": "Developer"
"name": "RetailCRM",
"email": "support@retailcrm.pro"
}
],
"require": {
"php": ">=5.2.0",
"ext-curl": "*"
"php": ">=7.3",
"ext-json": "*",
"psr/log": "^1|^2|^3",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-message-implementation": "^1.0",
"php-http/client-implementation": "^1.0",
"php-http/discovery": "^1.13",
"doctrine/annotations": "^1.13|^2.0",
"liip/serializer": "2.2.* || 2.6.*",
"php-http/httplug": "^2.2",
"civicrm/composer-compile-plugin": "^0.20",
"symfony/console": "^4.0|^5.0|^6.0|^7.0",
"psr/event-dispatcher": "^1.0",
"neur0toxine/psr.http-client-implementation.php-http-curl": "*",
"neur0toxine/psr.http-factory-implementation.nyholm": "*",
"neur0toxine/psr.http-message-implementation.nyholm": "*",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"symfony/cache": ">=v3.1.0",
"psr/http-factory": "^1.1"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.5",
"phpmd/phpmd": "^2.10",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "1.9.14",
"vlucas/phpdotenv": "^5.3",
"phpunit/phpunit": "^9.5",
"php-http/curl-client": "^2.2",
"nyholm/psr7": "^1.3",
"league/event": "^3.0",
"league/container": "^3.3",
"neur0toxine/pock": "^0.11"
},
"suggest": {
"ext-curl": "Most HTTP clients need ext-curl to work properly.",
"php-http/client-implementation": "PSR-18 compatible client should be available to use this library.",
"psr/http-message-implementation": "PSR-7 compatible HTTP message implementation should be available to use to use this library.",
"psr/http-factory-implementation": "PSR-17 compatible factories should be available to use this library.",
"symfony/event-dispatcher": "PSR-14 compatible event dispatcher.",
"league/event": "PSR-14 compatible event dispatcher.",
"nyholm/psr7": "This is recommended PSR-7 and PSR-17 implementation.",
"php-http/curl-client": "Simplest PSR-18 client implementation.",
"symfony/http-client": "One of the most popular HTTP clients. Has PSR-18 compatible adapter.",
"psr/log-implementation": "You can use log implementation for debug purposes."
},
"scripts": {
"phpunit": "./vendor/bin/phpunit -c phpunit.xml.dist --coverage-text",
"phpunit-ci": "@php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude=\"~vendor~\" ./vendor/bin/phpunit --teamcity -c phpunit.xml.dist",
"phpmd": "./vendor/bin/phpmd src text ./phpmd.xml",
"phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.3-8.3 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.3-8.3 --warning-severity=0",
"phpstan": "./vendor/bin/phpstan analyse -c phpstan.neon src --memory-limit=-1",
"phpstan-dockerized-ci": "docker run --rm -it -w=/app -v ${PWD}:/app oskarstark/phpstan-ga:1.0.1 analyse src -c phpstan.neon --memory-limit=1G --no-progress",
"lint:fix": "./vendor/bin/phpcbf src",
"lint": [
"@phpcs",
"@phpmd",
"@phpstan"
],
"verify": [
"@lint",
"@phpunit"
],
"models": "@php bin/retailcrm-client models:generate --all"
},
"support": {
"email": "support@intarocrm.ru"
"email": "support@retailcrm.pro"
},
"autoload": {
"psr-0": { "IntaroCrm\\": "lib/" }
"psr-4": {
"RetailCrm\\Api\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"RetailCrm\\TestUtils\\": "tests/utils/",
"RetailCrm\\Tests\\": "tests/src/"
}
},
"bin": [
"bin/retailcrm-client"
],
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "6.x-dev"
},
"compile": [
{
"run": "@composer run-script models"
}
]
},
"config": {
"bin-dir": "vendor/bin",
"process-timeout": 600,
"allow-plugins": {
"civicrm/composer-compile-plugin": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"php-http/discovery": true
}
}
}
}

109
doc/compilation_prompt.md Normal file
View file

@ -0,0 +1,109 @@
## Dealing with `civicrm/composer-compile-plugin` prompts
During installation you will see this prompt:
```sh
civicrm/composer-compile-plugin contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins
Do you trust "civicrm/composer-compile-plugin" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]
```
And after almost any Composer operation you will see this prompt:
```sh
The following packages have new compilation tasks:
- retailcrm/api-client-php has 1 task
Allow these packages to compile? ([y]es, [a]lways, [n]o, [l]ist, [h]elp)
```
That's because the API client utilizes code generation to speed up the serialization and deserialization of the requests. However,
these prompts may be annoying and sometimes can even break the application lifecycle pipeline (in the CI/CD environment). We can't just
disable it for everyone [because of security concerns](https://github.com/composer/composer/issues/1193). But you can disable it for your project.
There are three ways of disabling this prompt:
1. During the installation.
2. Automated way.
3. Manual way.
### Disable compilation prompts during the installation
Press `'y'` when you see this message:
```sh
civicrm/composer-compile-plugin contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins
Do you trust "civicrm/composer-compile-plugin" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]
```
And when you see this prompt, press `'a'`:
```sh
The following packages have new compilation tasks:
- retailcrm/api-client-php has 1 task
Allow these packages to compile? ([y]es, [a]lways, [n]o, [l]ist, [h]elp)
```
That's it. Code generation is now enabled.
### I've chosen something else, now API client doesn't work!
That happens. We provide special CLI utility which will automatically configure your `composer.json` to enable code generation.
Just run this command inside your project after API client installation:
```sh
./vendor/bin/retailcrm-client compiler:prompt
```
You should see this message after running the command:
```sh
✓ Done, code generation has been enabled.
```
You may also want to run code generation manually once. It can be achieved by running this command:
```sh
composer compile --all
```
**Note:** `retailcrm-client` should be in your binary directory. By default it is set to `vendor/bin`. You can check `config.bin-dir`
value in your `composer.json` and update paths in the commands above accordingly.
**Note (2):** `compiler:prompt` command has `--revert` flag. You can use it if you want to disable automatic code generation for some reason.
### Disabling compilation prompts manually
It is possible to replicate the same actions manually. First, you will need to enable compiler plugin. Add the plugin
to the `config.allow-plugins` segment of your `composer.json` file:
```json
"allow-plugins": {
"civicrm/composer-compile-plugin": true
}
```
After that add these params into the `extra` segment of your `composer.json`:
```json
"compile-mode": "whitelist",
"compile-whitelist": ["retailcrm/api-client-php"]
```
Your `composer.json` file should look like this:
```json
{
"name": "author/some-project",
"description": "Description of the project.",
"type": "project",
"license": "MIT",
"require": {
"php": ">=7.3.0",
"symfony/http-client": "^5.2",
"nyholm/psr7": "^1.4",
"retailcrm/api-client-php": "~6.0"
},
"config": {
"allow-plugins": {
"civicrm/composer-compile-plugin": true
}
},
"extra": {
"compile-mode": "whitelist",
"compile-whitelist": ["retailcrm/api-client-php"]
}
}
```
Voilà! You won't see the annoying prompt again.

View file

@ -0,0 +1,20 @@
## Customization
* [Controlling HTTP abstraction layer](different_psr_implementations.md)
* [Customizing request and response processing](pipelines/implementing_a_handler.md)
+ [Using a predefined handler](pipelines/using_a_predefined_handler.md)
+ [Built-in handlers](pipelines/using_a_predefined_handler.md#built-in-handlers)
+ [Modifying the default pipeline](pipelines/using_a_predefined_handler.md#modifying-the-default-pipeline)
+ [Constructing the pipeline from scratch](pipelines/using_a_predefined_handler.md#constructing-the-pipeline-from-scratch)
+ [Implementing a handler](pipelines/implementing_a_handler.md)
* [Implementing custom API methods](implementing_custom_api_methods.md)
Both `ClientFactory` and `ClientBuilder` provide the necessary functionality to replace PSR dependencies with any other compatible implementation.
By default, those dependencies will be detected via service discovery. But service discovery supports a limited amount of implementation.
If your implementation is not supported - the client won't work unless you provide the necessary dependencies manually.
Another case would be testing. You can provide special HTTP client implementation which will return mocked responses instead of making
real requests.
The Client uses [chain of responsibility](https://refactoring.guru/design-patterns/chain-of-responsibility) pattern to process requests.
Each request and response is being processed by the pipeline of handlers. Every handler can apply some mutation to the request or response
and can pass it to the next handler. In fact, API authentication is made possible by using a special handler which appends API key to every request.

View file

@ -0,0 +1,69 @@
## Controlling HTTP abstraction layer
You can replace default PSR dependencies in the client while using `ClientFactory` or `ClientBuilder`. It can be useful for tests
or if you want to use a specific implementation that is not supported by the service discovery.
Both `ClientFactory` and `ClientBuilder` provide those methods:
```php
/**
* Set your PSR-18 HTTP client.
*
* Service discovery will be used if no client has been provided.
*
* @param \Psr\Http\Client\ClientInterface $httpClient
*/
public function setHttpClient(\Psr\Http\Client\ClientInterface $httpClient);
/**
* Sets PSR-17 compatible stream factory. You can skip this step if you want to use service discovery.
*
* @param \Psr\Http\Message\StreamFactoryInterface|null $streamFactory
*/
public function setStreamFactory(?\Psr\Http\Message\StreamFactoryInterface $streamFactory);
/**
* Sets PSR-17 compatible request factory. You can skip this step if you want to use service discovery.
*
* @param \Psr\Http\Message\RequestFactoryInterface|null $requestFactory
*/
public function setRequestFactory(?\Psr\Http\Message\RequestFactoryInterface $requestFactory);
/**
* Sets PSR-17 compatible URI factory. You can skip this step if you want to use service discovery.
*
* @param \Psr\Http\Message\UriFactoryInterface|null $uriFactory
*/
public function setUriFactory(?\Psr\Http\Message\UriFactoryInterface $uriFactory);
```
They can be used to specify PSR dependencies like this:
```php
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
$factory = new \RetailCrm\Api\Factory\ClientFactory();
$factory->setHttpClient(new \Http\Client\Curl\Client())
->setRequestFactory($psr17Factory)
->setStreamFactory($psr17Factory)
->setUriFactory($psr17Factory);
$client = $factory->createClient('https://test.retailcrm.pro', 'apiKey');
```
Or like this:
```php
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
$builder = new \RetailCrm\Api\Builder\ClientBuilder();
$client = $builder
->setApiUrl('https://test.retailcrm.pro')
->setAuthenticatorHandler(new \RetailCrm\Api\Handler\Request\HeaderAuthenticatorHandler('apiKey'))
->setHttpClient(new \Http\Client\Curl\Client())
->setRequestFactory($psr17Factory)
->setStreamFactory($psr17Factory)
->setUriFactory($psr17Factory)
->build();
```
By replacing the HTTP client in the test environment you can easily mock requests and responses via libraries like
[`neur0toxine/pock`](https://packagist.org/packages/neur0toxine/pock) or [`php-http/mock-client`](https://packagist.org/packages/php-http/mock-client).

View file

@ -0,0 +1,28 @@
# Custom API methods with DTO
This example demonstrates how you can use your custom serializer with custom DTOs to implement API methods.
## How to run the project
1. Open `app.php` and change credentials and the site to your data.
2. Run these commands:
```sh
composer install
php app.php
```
You will see something like this:
```sh
Created customer using custom methods. ID: 5633
```
This means that the project works as expected.
## Navigation
- [`app.php`](app.php) - entrypoint, calls the custom method and outputs the response data.
- [`src/Component/Adapter/SymfonyToLiipAdapter.php`](src/Component/Adapter/SymfonyToLiipAdapter.php) - adapter for using `symfony/serializer` inside `FormEncoder` component.
- [`src/Component/CustomApiMethod.php`](src/Component/CustomApiMethod.php) - `CustomApiMethod` that uses `SerializerInterface` from `liip/serializer` and `FormEncoder`. This component will handle marshaling.
- [`src/Dto`](src/Dto) - data models used in the project.
- [`src/Factory/SerializerFactory.php`](src/Factory/SerializerFactory.php) - builds `symfony/serializer`'s serializer and wraps it into the `SymfonyToLiipAdapter`.
- [`src/Factory/ClientFactory.php`](src/Factory/ClientFactory.php) - custom client factory that register the custom API method.

View file

@ -0,0 +1,29 @@
<?php
use RetailCrm\Api\Builder\FormEncoderBuilder;
use RetailCrm\Examples\CustomMethodsDto\Dto\Customer;
use RetailCrm\Examples\CustomMethodsDto\Dto\Request\CustomersCreateRequest;
use RetailCrm\Examples\CustomMethodsDto\Factory\ClientFactory;
use RetailCrm\Examples\CustomMethodsDto\Factory\SerializerFactory;
require __DIR__ . '/vendor/autoload.php';
// Three lines below will be usually called during DI container building or hidden by other means.
$serializer = SerializerFactory::create();
$encoder = (new FormEncoderBuilder())->setSerializer($serializer)->build();
$clientFactory = (new ClientFactory())->setCustomEncoder($encoder);
// Replace API URL and API key with your data.
$client = $clientFactory->createClient('https://test.simla.com', 'apiKey');
$request = new CustomersCreateRequest();
$request->customer = new Customer();
$request->customer->firstName = 'Tester';
$request->customer->lastName = 'User';
$request->customer->patronymic = 'Patronymic';
$request->site = 'site'; // Replace site with your data.
/** @var \Retailcrm\Examples\CustomMethodsDto\Dto\Response\CustomersCreateResponse $response */
$response = $client->customMethods->createCustomer($request);
echo 'Created customer using custom methods. ID: ' . $response->id;

View file

@ -0,0 +1,16 @@
{
"name": "retailcrm/custom-api-methods-with-dto-example",
"description": "This project demonstrates DTO usage with the custom methods.",
"type": "project",
"require": {
"retailcrm/api-client-php": "^6",
"symfony/serializer": "^5.3",
"symfony/property-access": "^5.3"
},
"license": "MIT",
"autoload": {
"psr-4": {
"RetailCrm\\Examples\\CustomMethodsDto\\": "src/"
}
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* PHP version 7.3
*
* @category SymfonyToLiipAdapter
* @package Retailcrm\Examples\CustomMethodsDto\Component\Adapter
*/
namespace Retailcrm\Examples\CustomMethodsDto\Component\Adapter;
use Liip\Serializer\Context;
use Liip\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Serializer as SymfonySerializer;
/**
* Class SymfonyToLiipAdapter
*
* @category SymfonyToLiipAdapter
* @package Retailcrm\Examples\CustomMethodsDto\Component\Adapter
*/
class SymfonyToLiipAdapter implements SerializerInterface
{
/** @var SymfonySerializer */
private $serializer;
public function __construct(SymfonySerializer $serializer)
{
$this->serializer = $serializer;
}
/**
* @inheritDoc
*/
public function serialize($data, string $format, Context $context = null): string
{
return $this->serializer->serialize($data, $format);
}
/**
* @inheritDoc
*/
public function deserialize(string $data, string $type, string $format, Context $context = null)
{
return $this->serializer->deserialize($data, $type, $format);
}
/**
* @inheritDoc
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
*/
public function toArray($data, Context $context = null): array
{
return $this->serializer->normalize($data);
}
/**
* @inheritDoc
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
*/
public function fromArray(array $data, string $type, Context $context = null)
{
return $this->serializer->denormalize($data, $type);
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* PHP version 7.3
*
* @category CustomApiMethod
* @package Retailcrm\Examples\CustomMethodsDto\Component
*/
namespace Retailcrm\Examples\CustomMethodsDto\Component;
use RetailCrm\Api\Exception\Client\HandlerException;
use RetailCrm\Api\Interfaces\FormEncoderInterface;
use RetailCrm\Api\Interfaces\RequestSenderInterface;
/**
* Class CustomApiMethod
*
* @category CustomApiMethod
* @package Retailcrm\Examples\CustomMethodsDto\Component
*/
class CustomApiMethod extends \RetailCrm\Api\Component\CustomApiMethod
{
/** @var string */
private $responseFqn;
/** @var FormEncoderInterface */
private $encoder;
public function __construct(string $method, string $route, string $responseFqn, FormEncoderInterface $encoder)
{
parent::__construct($method, $route);
$this->responseFqn = $responseFqn;
$this->encoder = $encoder;
}
/**
* Sends the request, returns the response.
*
* @param \RetailCrm\Api\Interfaces\RequestSenderInterface $sender
* @param array<int|string, mixed>|object $data
*
* @return object
* @throws \RetailCrm\Api\Exception\ApiException
* @throws \RetailCrm\Api\Exception\ClientException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
*/
public function __invoke(RequestSenderInterface $sender, $data = [])
{
if (is_object($data)) {
$data = $this->encoder->encodeArray($data);
}
$result = parent::__invoke($sender, $data);
try {
return $this->encoder->getSerializer()->fromArray($result, $this->responseFqn);
} catch (\Throwable $throwable) {
throw new HandlerException(
'Cannot deserialize body: ' . $throwable->getMessage(),
0,
$throwable
);
}
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* PHP version 7.3
*
* @category Customer
* @package Retailcrm\Examples\CustomMethodsDto\Dto
*/
namespace Retailcrm\Examples\CustomMethodsDto\Dto;
/**
* Class Customer
*
* @category Customer
* @package Retailcrm\Examples\CustomMethodsDto\Dto
*/
class Customer
{
/** @var string */
public $firstName;
/** @var string */
public $lastName;
/** @var string */
public $patronymic;
/** @var string */
public $email;
}

View file

@ -0,0 +1,38 @@
<?php
/**
* PHP version 7.3
*
* @category CustomersCreateRequest
* @package Retailcrm\Examples\CustomMethodsDto\Dto\Request
*/
namespace Retailcrm\Examples\CustomMethodsDto\Dto\Request;
use RetailCrm\Api\Component\FormData\Mapping as Form;
/**
* Class CustomersCreateRequest
*
* @category CustomersCreateRequest
* @package Retailcrm\Examples\CustomMethodsDto\Dto\Request
*/
class CustomersCreateRequest
{
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("site")
*/
public $site;
/**
* @var \Retailcrm\Examples\CustomMethodsDto\Dto\Customer
*
* @Form\Type("Retailcrm\Examples\CustomMethodsDto\Dto\Customer")
* @Form\SerializedName("customer")
* @Form\JsonField()
*/
public $customer;
}

View file

@ -0,0 +1,22 @@
<?php
/**
* PHP version 7.3
*
* @category CustomersCreateResponse
* @package Retailcrm\Examples\CustomMethodsDto\Dto\Response
*/
namespace Retailcrm\Examples\CustomMethodsDto\Dto\Response;
/**
* Class CustomersCreateResponse
*
* @category CustomersCreateResponse
* @package Retailcrm\Examples\CustomMethodsDto\Dto\Response
*/
class CustomersCreateResponse
{
/** @var int */
public $id;
}

View file

@ -0,0 +1,55 @@
<?php
/**
* PHP version 7.3
*
* @category ClientFactory
* @package RetailCrm\Examples\CustomMethodsDto\Factory
*/
namespace RetailCrm\Examples\CustomMethodsDto\Factory;
use RetailCrm\Api\Client;
use RetailCrm\Api\Enum\RequestMethod;
use RetailCrm\Api\Factory\ClientFactory as Base;
use RetailCrm\Api\Interfaces\FormEncoderInterface;
use RetailCrm\Examples\CustomMethodsDto\Component\CustomApiMethod;
use RetailCrm\Examples\CustomMethodsDto\Dto\Response\CustomersCreateResponse;
/**
* Class ClientFactory
*
* @category ClientFactory
* @package RetailCrm\Examples\CustomMethodsDto\Factory
*/
class ClientFactory extends Base
{
/** @var \RetailCrm\Api\Interfaces\FormEncoderInterface */
private $customEncoder;
public function setCustomEncoder(FormEncoderInterface $customEncoder): ClientFactory
{
$this->customEncoder = $customEncoder;
return $this;
}
public function createClient(string $apiUrl, string $apiKey): Client
{
$client = parent::createClient($apiUrl, $apiKey);
$client->customMethods->register(
'createCustomer',
$this->method(
RequestMethod::POST,
'customers/create',
CustomersCreateResponse::class
)
);
return $client;
}
private function method(string $method, string $route, string $responseFqn): CustomApiMethod
{
return new CustomApiMethod($method, $route, $responseFqn, $this->customEncoder);
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* PHP version 7.3
*
* @category SerializerFactory
* @package Retailcrm\Examples\CustomMethodsDto\Factory
*/
namespace Retailcrm\Examples\CustomMethodsDto\Factory;
use Liip\Serializer\SerializerInterface;
use RetailCrm\Examples\CustomMethodsDto\Component\Adapter\SymfonyToLiipAdapter;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
/**
* Class SerializerFactory
*
* @category SerializerFactory
* @package Retailcrm\Examples\CustomMethodsDto\Factory
*/
class SerializerFactory
{
public static function create(): SerializerInterface
{
return new SymfonyToLiipAdapter(new Serializer([new ObjectNormalizer()], [new JsonEncoder()]));
}
}

View file

@ -0,0 +1,106 @@
## Implementing custom API methods
You can use this feature if you need to group multiple API calls, or return something custom - not the API response as-is,
or, for example, to use new API methods without waiting for the implementation. For all of those cases, we provide a special
resource group in the client that will make it easy to implement a custom API method.
The main limitation of this mechanism is the lack of DTO. You will be limited to arrays inside custom methods. It is
_possible_ to use custom DTO's for the custom methods, but it is a little tricky - check the bottom of this page for the example.
Let's imagine that we have a special method with this route: `/api/v5/dialogs`. This method returns a list of the dialogs present in the system.
We cannot use it directly because it is not implemented in the client. However, we can implement it by ourselves using custom methods.
It will look like this:
```php
use RetailCrm\Api\Component\CustomApiMethod;
use RetailCrm\Api\Enum\RequestMethod;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
$client = SimpleClientFactory::createClient('https://test.simla.io', 'key');
$client->customMethods->register('dialogs', new CustomApiMethod(RequestMethod::GET, 'dialogs'));
try {
$dialogs = $client->customMethods->call('dialogs');
} catch (ApiExceptionInterface $exception) {
echo sprintf(
'Error from RetailCRM API (status code: %d): %s',
$exception->getStatusCode(),
$exception->getMessage()
);
if (count($exception->getErrorResponse()->errors) > 0) {
echo PHP_EOL . 'Errors: ' . implode(', ', $exception->getErrorResponse()->errors);
}
return;
}
echo 'Dialogs: ' . print_r($dialogs['dialogs'], true);
```
First, we need to register the custom method in the client. To do this we must give our method a name and an implementation.
The name will be used to call the method later using `call()` and the implementation is just a callable with the three
parameters: `RequestSenderInterface`, data array, and the context. Let's take a look at all three.
1. `RequestSenderInterface` is implemented by the `RequestSender` and contains three methods: `send`, `route` and `host`.
`send` is used to send the request, `route` will append the base URL to your method name, and the latter, `host`, will return
hostname from the base URL.
2. The data array contains any data that has been provided to the callable during the method call. It will be encoded as
a query string and stored either as the URL params or as the form body (query string is used for GET and DELETE requests).
3. The context contains any information provided to the callable during the method call. It is not checked by the client.
However, it is always must be of array type.
As you can see, we're using `CustomApiMethod` as a callable in the example. The `CustomApiMethod` is a simple invokable
wrapper that can be used to simplify the registration of the new methods. The boilerplate code in the previous example is
already implemented inside the `CustomApiMethod` component.
It is easier to understand how the callable should work and how the `CustomApiMethod` works by looking at the registration example.
It works exactly like the previous example, but without the `CustomApiMethod` component:
```php
use RetailCrm\Api\Enum\RequestMethod;
use RetailCrm\Api\Interfaces\RequestSenderInterface;
$client->customMethods->register('dialogs', function (RequestSenderInterface $sender, $data, array $context) {
return $sender->send(RequestMethod::GET, $sender->route('dialogs'), $data);
});
```
The data here is not defined as array because it can be anything you like. You can use any serializer to serialize the data
and deserialize the response. `CustomApiMethod` only supports array out-of-box, but your handlers can utilize any
data types.
The base URL inside the client is always represented as a URL with the version suffix. It should be always kept in mind
while using the `route` method:
```php
// This code is being executed inside the custom method callable.
// Base URL is http://test.simla.io/api/v5
$settingsRoute = $sender->route('settings');
$host = $sender->host();
echo $settingsRoute; // prints "https://test.simla.io/api/v5/settings" - the slash is inserted by the route() method.
echo $host; // prints "test.simla.io"
```
Any registered custom method can be called using the `__call` magic method:
```php
// Both calls below works the same.
$client->customMethods->call('dialogs', ['param' => 'value'], ['contextParam' => 'contextValue']);
$client->customMethods->dialogs(['param' => 'value'], ['contextParam' => 'contextValue']);
```
The last notable difference from the regular methods lies inside events. You won't be able to get request or response models
from the events, but you can extract the response array using the `getResponseArray()` method.
## Using DTOs with the custom methods
You can use DTOs with the custom methods. To do that you just need to serialize the request models inside the method callback
before sending the request and deserialize the response before returning it. We already made a great example project that
demonstrates the DTO usage alongside custom methods.
[**DTOs with the custom methods - sample project**](examples/custom-api-methods-with-dto)

View file

@ -0,0 +1,23 @@
## Implementing a handler
You can implement your own handler using the `RetailCrm\Api\Interfaces\HandlerInterface`.
`RetailCrm\Api\Handler\AbstractHandler` provides boilerplate code for the chain of responsibility.
`AbstractHandler::next` method will call next handler in the chain. You can safely use `return parent::next()` in your code
while using `AbstractHandler`.
Most of the information about how handlers operate can be found in the [chain of responsibility](https://refactoring.guru/design-patterns/chain-of-responsibility)
pattern explanation. There are some specific details about handlers in the client. Client will pass desired dependencies to
the handler if handler implements one of those interfaces:
* Any request handler
+ `RetailCrm\Api\Interfaces\PsrFactoriesAwareInterface` will inject PSR-17 factories into the handler.
* Any response handler
+ `RetailCrm\Api\Interfaces\SerializerAwareInterface` will inject a `liip/serializer` instance into the handler.
+ `RetailCrm\Api\Interfaces\ApiExceptionFactoryAwareInterface` will inject a `Liip\Serializer\SerializerInterface` instance into the handler.
+ `RetailCrm\Api\Interfaces\EventDispatcherAwareInterface` will inject a `Psr\EventDispatcher\EventDispatcherInterface` instance into the handler.
All of those interfaces above have the corresponding implementation traits in the `RetailCrm\Api\Traits` namespace.
For example, `RetailCrm\Api\Interfaces\EventDispatcherAwareInterface` implementation can be found in the
`RetailCrm\Api\Traits\EventDispatcherAwareTrait` trait.
Handlers can be used in the both chains just like callback handlers (see ["using a predefined hander"](using_a_predefined_handler.md)).

View file

@ -0,0 +1,292 @@
## Using a predefined handler
* [Built-in handlers](#built-in-handlers)
* [Modifying the default pipeline](#modifying-the-default-pipeline)
* [Constructing the pipeline from scratch](#constructing-the-pipeline-from-scratch)
### Built-in handlers
You can use a predefined handler to modify the process of request or response processing. Most of the predefined handlers are
already in use by the client, except for the `CallbackRequestHandler` and `CallbackResponseHandler`.
Those are handlers that are present in the client and can be used for request processing:
* `CallbackRequestHandler` - processes a request using provided callback.
* `GetParameterAuthenticatorHandler` - appends a GET `apiKey` parameter with the API key.
* `HeaderAuthenticatorHandler` - appends `X-Api-Key` header with the API key.
* `PsrRequestHandler` - constructs base PSR-7 request with method and URI.
* `RequestDataHandler` - fills base PSR-7 request with the data from the DTO.
Request handlers are using `RetailCrm\Api\Model\RequestData` DTO to share state between handlers.
Those are handlers that can be used for response processing:
* `AccountNotFoundHandler` - detects when RetailCRM with specified API URL is not exist and throws specified exception.
* `CallbackResponseHandler` - processes a response using provided callback.
* `ErrorResponseHandler` - detects an error response, throws correct exception.
* `FilesDownloadResponseHandler` - handles `/api/v5/files/download` response.
* `UnmarshalResponseHandler` - unmarshals response body into response DTO.
Response handlers are using `RetailCrm\Api\Model\ResponseData` DTO to share state between handlers.
There are two built-in handlers which are most useful to users who want to modify the processing pipelines: `CallbackRequestHandler` and
`CallbackResponseHandler`. Both handlers use provided callbacks to modify a request or response. You can initialize those
with the examples below:
```php
use RetailCrm\Api\Handler\Request\CallbackRequestHandler;
use RetailCrm\Api\Model\RequestData;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
$handler = new CallbackRequestHandler(
static function (
RequestData $requestData,
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
UriFactoryInterface $uriFactory
) {
if (null !== $requestData->request) {
$requestData->request = $requestData->request->withHeader('X-Rlimit-Token', 'example_token');
}
}
);
```
```php
use RetailCrm\Api\Exception\Api\MissingParameterException;
use RetailCrm\Api\Factory\ApiExceptionFactory;
use RetailCrm\Api\Handler\Response\CallbackResponseHandler;
use RetailCrm\Api\Model\Response\Api\Credentials;
use RetailCrm\Api\Model\Response\ErrorResponse;
use RetailCrm\Api\Model\ResponseData;
use Liip\Serializer\SerializerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
$handler = new CallbackResponseHandler(
static function (
ResponseData $data,
SerializerInterface $serializer,
EventDispatcherInterface $eventDispatcher,
ApiExceptionFactory $apiExceptionFactory
) {
if (
$data->responseModel instanceof Credentials &&
!in_array('/api/customers/create', $data->responseModel->credentials)
) {
$data->responseModel = new ErrorResponse();
$data->responseModel->errorMsg = 'Parameter "/api/customers/create" is missing';
throw new MissingParameterException($data->responseModel, 400);
}
}
);
```
Both handlers above can be appended to request and response pipelines. Let's move on to the next part to learn how
you can append handlers to the chain using `ClientFactory` or `ClientBuilder`.
### Modifying the default pipeline
Two limitations apply to default pipeline modifying:
1. Chain ordering cannot be changed. Your handler will always be at the end of the default chain.
1. Callback handlers will always call the next handler. There is no way to stop the next handlers in the chain.
However, you can construct your own pipeline which won't be bound by the first limitation. And the second limitation can be
circumvented by [implementing your own handler](implementing_a_handler.md).
Both `ClientFactory` and `ClientBuilder` allow you to append your own handlers to the processing pipeline. They provide
those methods:
```php
/**
* Appends an additional request handler into the request processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface $handler
*/
public function appendRequestHandler(HandlerInterface $handler);
/**
* Appends an additional handler into the response processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface $handler
*/
public function appendResponseHandler(HandlerInterface $handler);
/**
* Appends an additional request handlers into the request processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface[] $handlers
*/
public function appendRequestHandlers(array $handlers);
/**
* Appends an additional response handlers into the response processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface[] $handlers
*/
public function appendResponseHandlers(array $handlers);
```
You can use those methods to pass the handler into desired chain. Take a look at the examples. This code will initialize the client
with provided `CallbackRequestHandler`. Instantiation will be done via `ClientFactory`:
```php
use RetailCrm\Api\Factory\ClientFactory;
use RetailCrm\Api\Handler\Request\CallbackRequestHandler;
use RetailCrm\Api\Model\RequestData;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
$handler = new CallbackRequestHandler(
static function (
RequestData $requestData,
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
UriFactoryInterface $uriFactory
) {
if (null !== $requestData->request) {
$requestData->request = $requestData->request->withHeader('X-Rlimit-Token', 'example_token');
}
}
);
$factory = new ClientFactory();
$client = $factory->appendRequestHandler($handler)->createClient('https://test.retailcrm.pro', 'apiKey');
```
And this code will instantiate a client using `ClientBuilder`. But this time we're modifying response pipeline:
```php
use RetailCrm\Api\Builder\ClientBuilder;
use RetailCrm\Api\Builder\FormEncoderBuilder;
use RetailCrm\Api\Exception\Api\MissingParameterException;
use RetailCrm\Api\Factory\ApiExceptionFactory;
use RetailCrm\Api\Handler\Request\HeaderAuthenticatorHandler;
use RetailCrm\Api\Handler\Response\CallbackResponseHandler;
use RetailCrm\Api\Model\Response\Api\Credentials;
use RetailCrm\Api\Model\Response\ErrorResponse;
use RetailCrm\Api\Model\ResponseData;
use Liip\Serializer\SerializerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
$handler = new CallbackResponseHandler(
static function (
ResponseData $data,
SerializerInterface $serializer,
EventDispatcherInterface $eventDispatcher,
ApiExceptionFactory $apiExceptionFactory
) {
if (
$data->responseModel instanceof Credentials &&
!in_array('/api/customers/create', $data->responseModel->credentials)
) {
$data->responseModel = new ErrorResponse();
$data->responseModel->errorMsg = 'Parameter "/api/customers/create" is missing';
throw new MissingParameterException($data->responseModel, 400);
}
}
);
$formEncoder = (new FormEncoderBuilder())->setCacheDir('cache')->build();
$builder = new ClientBuilder();
$client = $builder->setApiUrl('https://test.retailcrm.pro')
->setAuthenticatorHandler(new HeaderAuthenticatorHandler('apiKey'))
->setFormEncoder($formEncoder)
->appendRequestHandler($handler)
->build();
```
That's how you can modify request and response processing chains. However, you also can use the default pipeline factory
to pass desired handlers into the chain. Default request and response pipelines are constructed by those static factories:
```php
RetailCrm\Api\Factory\RequestPipelineFactory::createDefaultPipeline()
RetailCrm\Api\Factory\ResponsePipelineFactory::createDefaultPipeline()
```
Look at the example below to learn how to use those static factories. In this example we will modify both the request
and response pipelines:
```php
use Liip\Serializer\SerializerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use RetailCrm\Api\Builder\ClientBuilder;
use RetailCrm\Api\Builder\FormEncoderBuilder;
use RetailCrm\Api\Component\Transformer\RequestTransformer;
use RetailCrm\Api\Component\Transformer\ResponseTransformer;
use RetailCrm\Api\Exception\Api\MissingParameterException;
use RetailCrm\Api\Factory\ApiExceptionFactory;
use RetailCrm\Api\Factory\RequestPipelineFactory;
use RetailCrm\Api\Factory\ResponsePipelineFactory;
use RetailCrm\Api\Handler\Request\CallbackRequestHandler;
use RetailCrm\Api\Handler\Request\HeaderAuthenticatorHandler;
use RetailCrm\Api\Handler\Response\CallbackResponseHandler;
use RetailCrm\Api\Model\RequestData;
use RetailCrm\Api\Model\Response\Api\Credentials;
use RetailCrm\Api\Model\Response\ErrorResponse;
use RetailCrm\Api\Model\ResponseData;
$requestHandler = new CallbackRequestHandler(
static function (
RequestData $requestData,
RequestFactoryInterface $requestFactory,
StreamFactoryInterface $streamFactory,
UriFactoryInterface $uriFactory
) {
if (null !== $requestData->request) {
$requestData->request = $requestData->request->withHeader('X-Rlimit-Token', 'example_token');
}
}
);
$responseHandler = new CallbackResponseHandler(
static function (
ResponseData $data,
SerializerInterface $serializer,
EventDispatcherInterface $eventDispatcher,
ApiExceptionFactory $apiExceptionFactory
) {
if (
$data->responseModel instanceof Credentials &&
!in_array('/api/customers/create', $data->responseModel->credentials)
) {
$data->responseModel = new ErrorResponse();
$data->responseModel->errorMsg = 'Parameter "/api/customers/create" is missing';
throw new MissingParameterException($data->responseModel, 400);
}
}
);
$builder = new ClientBuilder();
$formEncoder = (new FormEncoderBuilder())->setCacheDir('cache')->build();
$client = $builder->setApiUrl('https://test.retailcrm.pro')
->setAuthenticatorHandler(new HeaderAuthenticatorHandler('apiKey'))
->setRequestTransformer(new RequestTransformer(
RequestPipelineFactory::createDefaultPipeline(
$formEncoder,
null, // PSR factories will be found by the service discovery.
null,
null,
$requestHandler
)
))
->setResponseTransformer(new ResponseTransformer(
ResponsePipelineFactory::createDefaultPipeline(
$formEncoder->getSerializer(),
new ApiExceptionFactory(),
null, // No EventDispatcherInterface was provided
$responseHandler
)
))->build();
```
### Constructing the pipeline from scratch
Both `RequestTransformer` and `ResponseTransformer` accept the `HandlerInterface` instance as the first argument. You can
look into `RequestPipelineFactory` and `ResponsePipelineFactory` source code to learn how to build your own pipeline from
scratch. This is, however, is discouraged.

32
doc/index.md Normal file
View file

@ -0,0 +1,32 @@
# Documentation
* [Dealing with `civicrm/composer-compile-plugin` prompts](compilation_prompt.md)
+ [Disable compilation prompts during the installation](compilation_prompt.md#disable-compilation-prompts-during-the-installation)
+ [I've chosen something else, now API client doesn't work!](compilation_prompt.md#ive-chosen-something-else-now-api-client-doesnt-work)
+ [Disabling compilation prompts manually](compilation_prompt.md#disabling-compilation-prompts-manually)
* [Client structure](structure.md)
+ [Design principles](structure.md#design-principles)
+ [Resource groups](structure.md#resource-groups)
* [Usage](usage/usage.md)
+ [Instantiation](usage/instantiation.md)
+ [Sending a request](usage/sending_a_request.md)
+ [Choosing correct resource group, DTOs, and method](usage/sending_a_request.md#choosing-correct-resource-group-dtos-and-method)
+ [Sending a request](usage/sending_a_request.md#sending-a-request)
+ [Dealing with exceptions](usage/sending_a_request.md#dealing-with-exceptions)
+ [Error handling](usage/error_handling.md)
+ [Examples](usage/examples)
+ [How to create an order](usage/examples/create_order.md)
+ [How to receive the list of orders](usage/examples/fetch_orders.md)
+ [How to handle all Client's exceptions](usage/examples/complete_error_handling_example.md)
+ [Event handling](usage/event_handing.md)
* [Customization](customization/customization.md)
+ [Controlling HTTP abstraction layer](customization/different_psr_implementations.md)
+ [Customizing request and response processing](customization/pipelines/implementing_a_handler.md)
+ [Using a predefined handler](customization/pipelines/using_a_predefined_handler.md)
+ [Built-in handlers](customization/pipelines/using_a_predefined_handler.md#built-in-handlers)
+ [Modifying the default pipeline](customization/pipelines/using_a_predefined_handler.md#modifying-the-default-pipeline)
+ [Constructing the pipeline from scratch](customization/pipelines/using_a_predefined_handler.md#constructing-the-pipeline-from-scratch)
+ [Implementing a handler](customization/pipelines/implementing_a_handler.md)
+ [Implementing custom API methods](customization/implementing_custom_api_methods.md)
* [Troubleshooting](troubleshooting.md)
* [PHPDoc](https://retailcrm.github.io/api-client-php/)

48
doc/structure.md Normal file
View file

@ -0,0 +1,48 @@
## Client structure
The client is separated into several resource groups, all of which are accessible through the Client's public properties. Each resource group is responsible for the corresponding API section. It is much easier to understand how the Client works by learning its design principles.
### Design principles
The general principles of the Client design are:
1. The Client itself doesn't contain any API methods implementations.
1. ResourceGroups implements all API methods.
1. Every method can accept from zero to several arguments.
1. URI path arguments (not the query string ones) are always passed as separate method arguments.
1. Query parameters are always passed in the form of the Request model. The same principle applies to the POST form data.
1. If the Request contains only one parameter - it'll probably accept it through the constructor. This doesn't apply to the filter requests, which don't have constructors at all.
1. All DTO's you'll ever need can be found in the `RetailCrm\Api\Model` namespace.
1. Every method has an example of the usage in its DocBlock. Yes, all of them. You can learn how to use any of the methods just by looking at the DocBlock help tooltip provided by your IDE. If it's not provided by your IDE - you'll need to look up how to configure it. It'll ease your work with this (and any other) library a lot.
### Resource groups
The resource groups list into which client is separated:
* `api`
* `costs`
* `customFields`
* `customers`
* `customersCorporate`
* `delivery`
* `features`
* `files`
* `integration`
* `loyalty`
* `notifications`
* `orders`
* `packs`
* `payments`
* `references`
* `segments`
* `settings`
* `store`
* `tasks`
* `telephony`
* `users`
* `verification`
* `statistics`
* `webAnalytics`
There is also a special `customMethods` group that is used for custom API methods. Each group except this one implements corresponding API documentation block:
* [English](https://docs.retailcrm.pro/Developers/API/APIVersions/APIv5)
* [Русский](https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5)

59
doc/troubleshooting.md Normal file
View file

@ -0,0 +1,59 @@
### I've got error that looks like this: `Cannot deserialize body: Type "RetailCrm\Api\Model\Response\Api\ApiVersionsResponse" is not known.`
Run this command to fix the problem:
```sh
composer compile --all
```
The details about that error can be found in the [documentation](compilation_prompt.md).
### I've got "No Message Factories" error.
This means that PSR-17 implementation you have is not supported by the service discovery, therefore, API client cannot find proper factories.
In order to use your own PSR-17 implementation you need to use `ClientBuilder`. Also, you can just install supported implementation which is
much easier:
```sh
composer require nyholm/psr7
```
### I've got `Http\Discovery\Exception\DiscoveryFailedException` or any other error with message like "`Could not find resource using any discovery strategy`".
That's because you don't have any supported PSR-18, PSR-7 or PSR-17 implementation available. This usually happens if you do have any implementation for those
standards, but it's not supported by service discovery. You can fix this easily by installing supported implementations. We recommend using `symfony/http-client`
and `nyholm/psr7`. Install those using this command:
```sh
composer require nyholm/psr7 symfony/http-client
```
Alternatively, you can use `ClientBuilder` which allows you to pass your own HTTP client and message & URI factories.
### There are too many available exceptions! How do I catch them all?
Every exception in the library implements either `ApiExceptionInterface` or `ClientExceptionInterface`. First will be thrown in case of any
errors from the API, and the second will be thrown in case of any problems with the client or network itself. Concrete exception types are meant
to be used to determine what exactly gone wrong while sending a request.
Also, you can use PSR-14 compatible event dispatcher to handle some exceptions globally. The Client will send `FailureRequestEvent` in case of any exceptions.
You can call `FailureRequestEvent::suppressThrow()` to prevent client from throwing an exception.
### I can't test my app in the CI because Composer fails while installing dependencies.
That's because you should explicitly allow code generation for the library. Otherwise, Composer will ask you to run compilation task at runtime
which is not possible in the CI since it lacks the support for interactive input.
You can allow code generation tasks without interactive approval. Just add parameters from the JSON below to
the `"extra"` key of your project's `composer.json` file.
```json
{
"compile-mode": "whitelist",
"compile-whitelist": ["retailcrm/api-client-php"]
}
```
### How can I choose proper DTO class for my request?
Request DTO's can be found in the `RetailCrm\Api\Model\Request` namespace. They are separated by the API resource groups.
For example, requests for interaction with customer entities can be found in the `RetailCrm\Api\Model\Request\Customers` namespace
and requests for operations with the orders can be found in the `RetailCrm\Api\Model\Request\Orders` namespace. Every request method
defines it's input and output types. Also, you can choose correct type for child entities by looking at type annotations.

View file

@ -0,0 +1,52 @@
## Error handling
The API Client exceptions are hierarchical. It means that groups of exceptions can be caught using more generic exception types.
Let's take a look at the exceptions hierarchy:
- `ApiException` is an abstraction and will never be thrown by itself.
- `AccessDeniedException` is thrown if the `Access denied.` error string is received from the API.
- `AccountDoesNotExistException` is thrown if the RetailCRM account with a specified API URL does not exist.
- `ApiErrorException` is thrown in case of any generic API error which is not recognized as a more specific type.
- `InvalidCredentialsException` is thrown if the provided API key is not correct.
- `MissingCredentialsException` is thrown if no API key was provided (this can happen if you initialize the Client without an authenticator).
- `MissingParameterException` is thrown if `Parameter '*' is missing` error is returned from the API (`*` is any parameter name).
- `ValidationException` is thrown if request validation is failed on the server-side (incorrect request data).
- `ClientException` is an abstraction and will never be thrown by itself.
- `BuilderException` is thrown by any builder if something goes wrong.
- `HandlerException` is thrown by the handlers in the request / response processing chain if they encountered an error.
- `HttpClientException` is thrown if underlying HTTP client throws an exception.
`ApiException` implements `ApiExceptionInterface` and `ClientException` implements `ClientExceptionInterface`.
It means that you can catch any exception in the `ApiException` leaf using `ApiExceptionInterface` as an exception type.
Also, you can catch any exception in the `ClientException` leaf using the `ClientExceptionInterface` type.
`ClientExceptionInterface` and `ApiExceptionInterface` implements `__toString()`. The first one will return a string with an error
message and stack trace and the second one will print out the error message, stack trace, and error response data.
Let's take a look at the example:
```php
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
try {
$apiVersions = $client->api->apiVersions();
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception;
return;
}
echo 'Available API versions: ' . implode(', ', $apiVersions->versions);
```
All Client exceptions can be found in the `RetailCrm\Api\Exception` namespace. `ApiException` children reside in the `RetailCrm\Api\Exception\Api` namespace,
and `ClientException` children reside at the `RetailCrm\Api\Exception\Client` namespace.
It is recommended to look at the [complete error handling example](examples/complete_error_handling_example.md).
The most useful case would be the error message generation for the client in your app, which can look like this:
- `InvalidCredentialsException` - show the `Invalid API key` message.
- `AccountDoesNotExistException` - show the `Incorrect RetailCRM URL` or `This RetailCRM instance does not exist` message.
- `AccessDeniedException` - show the `Please enable the /api/v5/... method for this key` message.
- and so on.

105
doc/usage/event_handing.md Normal file
View file

@ -0,0 +1,105 @@
## Events
You can use a PSR-14 compatible event dispatcher to receive events from the client.
It may be useful if you want to process certain events with the same logic without duplicating calls to such code.
These events are provided by the library:
- `RetailCrm\Api\Event\SuccessRequestEvent` will be dispatched if the request was successful.
- `RetailCrm\Api\Event\FailureRequestEvent` will be dispatched if the request was not successful. It won't be dispatched if the request cannot be formed at all.
Event API documentation can be found here:
* [`FailureRequestEvent`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Event-FailureRequestEvent.html)
* [`SuccessRequestEvent`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Event-SuccessRequestEvent.html)
Here is an example of event handling with Symfony. We're using an empty Symfony 5.x project with annotations routing. `IndexController`
outputs `/api/credentials` response without any changes. We'll handle all exceptions inside this event listener which will allow us
to call the method without any `try...catch` blocks.
**config/services.yml**
```yaml
services:
# ClientFactory definition.
RetailCrm\Api\Interfaces\ClientFactoryInterface:
class: 'RetailCrm\Api\Factory\ClientFactory'
calls:
- setCacheDir: ['%kernel.cache_dir%']
- setEventDispatcher: ['@event_dispatcher']
# Event listener definition. This listener will suppress exceptions and log them.
App\EventListener\FailureRequestListener:
tags:
- { name: kernel.event_listener, event: RetailCrm\Api\Event\FailureRequestEvent, method: onRequestFailure }
```
**src/EventListener/FailureRequestListener.php**
```php
<?php
namespace App\EventListener;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Log\LoggerInterface;
use RetailCrm\Api\Event\FailureRequestEvent;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
class FailureRequestListener
{
/** @var \Psr\Log\LoggerInterface */
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function onRequestFailure(FailureRequestEvent $event): void
{
$exception = $event->getException();
if ($exception instanceof ApiExceptionInterface) {
$event->suppressThrow();
$this->logger->error(sprintf(
'CRM URL "%s", API error: %s',
$event->getApiUrl(),
(string) $exception
));
}
if ($exception instanceof ClientExceptionInterface) {
$event->suppressThrow();
$this->logger->error(sprintf(
'CRM URL "%s", client error: %s, trace: %s',
$event->getApiUrl(),
$exception->getMessage(),
$exception->getTraceAsString()
));
}
}
}
```
**src/Controller/IndexController.php**
```php
<?php
namespace App\Controller;
use RetailCrm\Api\Interfaces\ClientFactoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class IndexController extends AbstractController
{
/**
* @Route("/", name="index")
*/
public function index(ClientFactoryInterface $clientFactory): Response
{
$client = $clientFactory->createClient('https://test3487687.retailcrm.pro', 'key');
$credentials = $client->api->credentials();
// Will print out empty model because https://test3487687.retailcrm.pro account does not exist.
return $this->json($credentials);
}
}
```

View file

@ -0,0 +1,115 @@
Here is a complete example of the Client usage with error handling. It fetches the filtered orders data and prints it out.
**Note:** it's not recommended using this example to process all API client errors in your application. It will clutter your code too much.
There are other options for exception processing. You can [match more generic exceptions in the hierarchy](../error_handling.md) or use
[events](../event_handing.md) to process exceptions.
```php
<?php
use Psr\Http\Client\ClientExceptionInterface as PsrClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use RetailCrm\Api\Exception\Api\AccessDeniedException;
use RetailCrm\Api\Exception\Api\AccountDoesNotExistException;
use RetailCrm\Api\Exception\Api\ApiErrorException;
use RetailCrm\Api\Exception\Api\InvalidCredentialsException;
use RetailCrm\Api\Exception\Api\MissingCredentialsException;
use RetailCrm\Api\Exception\Api\MissingParameterException;
use RetailCrm\Api\Exception\Api\ValidationException;
use RetailCrm\Api\Exception\Client\HandlerException;
use RetailCrm\Api\Exception\Client\HttpClientException;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
use RetailCrm\Api\Model\Filter\Orders\OrderFilter;
use RetailCrm\Api\Model\Request\Orders\OrdersRequest;
use Throwable;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
$request = new OrdersRequest();
$request->filter = new OrderFilter();
$request->filter->ids = [7141];
try {
$response = $client->orders->list($request);
} catch (HandlerException $exception) {
echo 'Error while trying to prepare request: ' . $exception->getMessage();
exit(-1);
} catch (HttpClientException $exception) {
$prefix = 'Unknown error';
if ($exception->getPrevious() instanceof NetworkExceptionInterface) {
$prefix = 'Network error';
}
if ($exception->getPrevious() instanceof RequestExceptionInterface) {
$prefix = 'Invalid request';
}
if ($exception->getPrevious() instanceof PsrClientExceptionInterface) {
$prefix = 'HTTP client exception';
}
echo $prefix . ': ' . $exception->getMessage();
exit(-1);
} catch (
InvalidCredentialsException |
AccessDeniedException |
AccountDoesNotExistException |
MissingCredentialsException |
MissingParameterException $exception
) {
echo $exception->getMessage();
exit(-1);
} catch (ValidationException $exception) {
echo 'Errors in fields:' . PHP_EOL;
foreach ($exception->getErrorResponse()->errors as $field => $error) {
printf(" - %s: %s\n", $field, $error);
}
exit(-1);
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiException and ClientException implements __toString() method
exit(-1);
} catch (Throwable $throwable) {
echo 'Unknown runtime exception: ' . $throwable->getMessage() . PHP_EOL;
echo $throwable->getTraceAsString();
exit(-1);
}
foreach ($response->orders as $order) {
printf("Order ID: %d\n", $order->id);
printf("First name: %s\n", $order->firstName);
printf("Last name: %s\n", $order->lastName);
printf("Patronymic: %s\n", $order->patronymic);
printf("Phone #1: %s\n", $order->phone);
printf("Phone #2: %s\n", $order->additionalPhone);
printf("E-Mail: %s\n", $order->email);
if ($order->customer instanceof CustomerCorporate) {
echo "Customer type: corporate\n";
} else {
echo "Customer type: individual\n";
}
foreach ($order->items as $item) {
echo PHP_EOL;
printf("Product name: %s\n", $item->productName);
printf("Quantity: %d\n", $item->quantity);
printf("Initial price: %f\n", $item->initialPrice);
}
echo PHP_EOL;
printf("Discount: %f\n", $order->discountManualAmount);
printf("Total: %f\n", $order->totalSumm);
echo PHP_EOL;
}
```

View file

@ -0,0 +1,253 @@
<?php
/**
* PHP version 7.3
*
* @category PaginatedRequestIterator
*/
use RetailCrm\Api\Client;
use RetailCrm\Api\Exception\Client\HandlerException;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Interfaces\RequestInterface;
use RetailCrm\Api\Model\Response\AbstractPaginatedResponse;
/**
* Class PaginatedRequestIterator
*
* @category PaginatedRequestIterator
* @template T
* @implements Iterator<int|string, T>
*/
class PaginatedRequestIterator implements Iterator
{
/** @var \RetailCrm\Api\Client */
private $client;
/** @var array<int|string, mixed> */
private $items = [];
/** @var int */
private $position = 0;
/** @var int */
private $seek = 0;
/** @var int */
private $currentPage = 1;
/** @var bool */
private $fetchFailed = false;
/** @var \Throwable|null */
private $error;
/** @var bool */
private $finalized = false;
/** @var \RetailCrm\Api\Interfaces\RequestInterface */
private $request;
/** @var callable */
private $nextPageFunc;
/** @var callable */
private $sendResponseFunc;
/** @var callable */
private $extractBatchFunc;
/**
* PaginatedRequestIterator constructor.
*
* @param \RetailCrm\Api\Client $client
*/
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* This request will be used in every iteration.
*
* @param \RetailCrm\Api\Interfaces\RequestInterface $request
*
* @return self
*/
public function setRequest(RequestInterface $request): self
{
$this->request = $request;
return $this;
}
/**
* This function should modify provided request which will be used in the next iteration.
*
* These params will be supplied in this exact order:
* - The request instance.
* - The response instance.
* - Next page number.
*
* @param callable $nextPageFunc
*
* @return self
*/
public function setNextPageFunc(callable $nextPageFunc): self
{
$this->nextPageFunc = $nextPageFunc;
return $this;
}
/**
* This function should send the request and return the response.
*
* These params will be supplied in this exact order:
* - The Client instance.
* - The request instance.
*
* @param callable $sendResponseFunc
*
* @return self
*/
public function setSendResponseFunc(callable $sendResponseFunc): self
{
$this->sendResponseFunc = $sendResponseFunc;
return $this;
}
/**
* This function should extract the batch of objects from the response.
*
* These params will be supplied in this exact order:
* - The response instance.
*
* @param callable $extractBatchFunc
*
* @return PaginatedRequestIterator
*/
public function setExtractBatchFunc(callable $extractBatchFunc): PaginatedRequestIterator
{
$this->extractBatchFunc = $extractBatchFunc;
return $this;
}
/**
* @inheritDoc
* @return mixed
*/
public function current()
{
return $this->items[$this->getPosition()];
}
/**
* @inheritDoc
*/
public function next(): void
{
++$this->position;
}
/**
* @inheritDoc
*/
public function key(): int
{
return $this->position;
}
/**
* @inheritDoc
*/
public function valid(): bool
{
if (empty($this->items) || !array_key_exists($this->getPosition(), $this->items)) {
$this->fetchChunk();
}
return isset($this->items[$this->getPosition()]);
}
/**
* @inheritDoc
*/
public function rewind(): void
{
$this->items = [];
$this->position = 0;
$this->seek = 0;
$this->currentPage = 1;
$this->error = null;
}
/**
* @return bool
*/
public function isFetchFailed(): bool
{
return $this->fetchFailed;
}
/**
* @return \Throwable|null
*/
public function getError(): ?Throwable
{
return $this->error;
}
/**
* @return int
*/
private function getPosition(): int
{
return $this->position - $this->seek;
}
/**
* fetchChunk obtains chunk of data
*/
private function fetchChunk(): void
{
if ($this->finalized || $this->fetchFailed) {
return;
}
try {
if ($this->currentPage > 1) {
time_nanosleep(0, 100000000);
}
$response = call_user_func($this->sendResponseFunc, $this->client, $this->request);
if ($response instanceof AbstractPaginatedResponse) {
$batch = call_user_func($this->extractBatchFunc, $response);
if (empty($batch)) {
$this->items = [];
$this->fetchFailed = true;
$this->error = new HandlerException('Received an empty response');
} else {
$this->seek += count($this->items);
$this->items = array_values($batch);
if ($this->currentPage < $response->pagination->totalPageCount) {
call_user_func($this->nextPageFunc, $this->request, $response, ++$this->currentPage);
} else {
$this->finalized = true;
}
}
}
} catch (Throwable $exception) {
if ($exception instanceof ApiExceptionInterface && 503 === $exception->getStatusCode()) {
time_nanosleep(1, 0);
$this->fetchChunk();
return;
}
$this->fetchFailed = true;
$this->error = $exception;
}
}
}

View file

@ -0,0 +1,39 @@
<?php
use RetailCrm\Api\Client;
use RetailCrm\Api\Enum\PaginationLimit;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Filter\Customers\CustomerHistoryFilter;
use RetailCrm\Api\Model\Request\Customers\CustomersHistoryRequest;
use RetailCrm\Api\Model\Response\Customers\CustomersHistoryResponse;
require __DIR__ . '/../../../../vendor/autoload.php';
require __DIR__ . '/PaginatedRequestIterator.php';
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro/', 'apiKey');
$request = new CustomersHistoryRequest();
$request->filter = new CustomerHistoryFilter();
$request->filter->startDate = (new DateTime())->sub(new DateInterval('P7D')); // 7 days
$request->limit = PaginationLimit::LIMIT_100;
$paginator = (new PaginatedRequestIterator($client))
->setRequest($request)
->setNextPageFunc(static function (CustomersHistoryRequest $request, CustomersHistoryResponse $response, int $page) {
$request->filter->startDate = null;
$request->filter->sinceId = end($response->history)->id;
})
->setExtractBatchFunc(static function (CustomersHistoryResponse $response) {
return $response->history;
})
->setSendResponseFunc(static function (Client $client, CustomersHistoryRequest $request) {
return $client->customers->history($request);
});
/** @var \RetailCrm\Api\Model\Entity\Customers\Customer $customer */
foreach ($paginator as $historyEntry) {
print_r($historyEntry);
}
if ($paginator->isFetchFailed()) {
echo $paginator->getError();
}

View file

@ -0,0 +1,47 @@
<?php
use RetailCrm\Api\Client;
use RetailCrm\Api\Enum\PaginationLimit;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Entity\Customers\CustomerPhone;
use RetailCrm\Api\Model\Request\Customers\CustomersRequest;
use RetailCrm\Api\Model\Response\Customers\CustomersResponse;
require __DIR__ . '/../../../../vendor/autoload.php';
require __DIR__ . '/PaginatedRequestIterator.php';
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro/', 'apiKey');
$request = new CustomersRequest();
$request->limit = PaginationLimit::LIMIT_100;
$request->page = 1;
$paginator = (new PaginatedRequestIterator($client))
->setRequest($request)
->setNextPageFunc(static function (CustomersRequest $request, CustomersResponse $response, int $page) {
$request->page = $page;
})
->setExtractBatchFunc(static function (CustomersResponse $response) {
return $response->customers;
})
->setSendResponseFunc(static function (Client $client, CustomersRequest $request) {
return $client->customers->list($request);
});
/** @var \RetailCrm\Api\Model\Entity\Customers\Customer $customer */
foreach ($paginator as $customer) {
printf(
'%d: %s %s %s <%s> (%s) %s',
$customer->id,
$customer->firstName,
$customer->lastName,
$customer->patronymic,
implode(', ', array_map(static function (CustomerPhone $phone) {
return $phone->number;
}, $customer->phones)),
$customer->email, PHP_EOL
);
}
if ($paginator->isFetchFailed()) {
echo $paginator->getError();
}

View file

@ -0,0 +1,8 @@
Here are some complex examples of pagination handling. In this case, we're using a special component that handles the pagination for us.
As a result, we can just iterate over batches of the data using `foreach`.
More than that, this component automatically handles rate limit exceptions.
* [Fetching customers](example_customers_list.php)
* [Fetching customers history](example_customers_history.php)
* [Pagination component (`PaginatedRequestIterator`)](PaginatedRequestIterator.php)

View file

@ -0,0 +1,104 @@
That's how you can create a new order:
```php
<?php
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Enum\CountryCodeIso3166;
use RetailCrm\Api\Enum\Customers\CustomerType;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Model\Entity\Orders\Delivery\OrderDeliveryAddress;
use RetailCrm\Api\Model\Entity\Orders\Delivery\SerializedOrderDelivery;
use RetailCrm\Api\Model\Entity\Orders\Items\Offer;
use RetailCrm\Api\Model\Entity\Orders\Items\OrderProduct;
use RetailCrm\Api\Model\Entity\Orders\Items\PriceType;
use RetailCrm\Api\Model\Entity\Orders\Items\Unit;
use RetailCrm\Api\Model\Entity\Orders\Order;
use RetailCrm\Api\Model\Entity\Orders\Payment;
use RetailCrm\Api\Model\Entity\Orders\SerializedRelationCustomer;
use RetailCrm\Api\Model\Request\Orders\OrdersCreateRequest;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
$request = new OrdersCreateRequest();
$order = new Order();
$payment = new Payment();
$delivery = new SerializedOrderDelivery();
$deliveryAddress = new OrderDeliveryAddress();
$offer = new Offer();
$item = new OrderProduct();
$payment->type = 'bank-card';
$payment->status = 'paid';
$payment->amount = 1000;
$payment->paidAt = new DateTime();
$deliveryAddress->index = '344001';
$deliveryAddress->countryIso = CountryCodeIso3166::RUSSIAN_FEDERATION;
$deliveryAddress->region = 'Region';
$deliveryAddress->city = 'City';
$deliveryAddress->street = 'Street';
$deliveryAddress->building = '10';
$delivery->address = $deliveryAddress;
$delivery->cost = 0;
$delivery->netCost = 0;
$offer->name = 'Offer №1445123';
$offer->displayName = 'Offer №1445123';
$offer->xmlId = 'tGunLo27jlPGmbA8BrHxY2';
$offer->article = '14451445-14451445';
$offer->unit = new Unit('796', 'Piece', 'pcs');
$item->offer = $offer;
$item->priceType = new PriceType('base');
$item->quantity = 1;
$item->purchasePrice = 60;
$order->delivery = $delivery;
$order->items = [$item];
$order->payments = [$payment];
$order->orderType = 'test';
$order->orderMethod = 'phone';
$order->countryIso = CountryCodeIso3166::RUSSIAN_FEDERATION;
$order->firstName = 'Test';
$order->lastName = 'User';
$order->patronymic = 'Patronymic';
$order->phone = '89003005069';
$order->email = 'testuser12345678901@example.com';
$order->managerId = 28;
$order->customer = SerializedRelationCustomer::withIdAndType(
4924,
CustomerType::CUSTOMER
);
$order->status = 'assembling';
$order->statusComment = 'Assembling order';
$order->weight = 1000;
$order->shipmentStore = 'main12';
$order->shipmentDate = (new DateTime())->add(new DateInterval('P7D'));
$order->shipped = false;
$order->customFields = [
"galka" => false,
"test_number" => 0,
"otpravit_dozakaz" => false,
];
$request->order = $order;
$request->site = 'moysklad';
try {
$response = $client->orders->create($request);
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiExceptionInterface instance should implement __toString() method.
exit(-1);
}
printf(
'Created order id = %d with the following data: %s',
$response->id,
print_r($response->order, true)
);
```

View file

@ -0,0 +1,50 @@
That's how you can fetch the orders list:
```php
<?php
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
try {
$response = $client->orders->list();
} catch (ApiExceptionInterface | ClientExceptionInterface $exception) {
echo $exception; // Every ApiExceptionInterface instance should implement __toString() method.
exit(-1);
}
foreach ($response->orders as $order) {
printf("Order ID: %d\n", $order->id);
printf("First name: %s\n", $order->firstName);
printf("Last name: %s\n", $order->lastName);
printf("Patronymic: %s\n", $order->patronymic);
printf("Phone #1: %s\n", $order->phone);
printf("Phone #2: %s\n", $order->additionalPhone);
printf("E-Mail: %s\n", $order->email);
if ($order->customer instanceof CustomerCorporate) {
echo "Customer type: corporate\n";
} else {
echo "Customer type: individual\n";
}
foreach ($order->items as $item) {
echo PHP_EOL;
printf("Product name: %s\n", $item->productName);
printf("Quantity: %d\n", $item->quantity);
printf("Initial price: %f\n", $item->initialPrice);
}
echo PHP_EOL;
printf("Discount: %f\n", $order->discountManualAmount);
printf("Total: %f\n", $order->totalSumm);
echo PHP_EOL;
}
```

View file

@ -0,0 +1,7 @@
## Examples
* [How to create an order](create_order.md)
* [How to receive the list of orders](fetch_orders.md)
* [How to handle all Client's exceptions](complete_error_handling_example.md)
* [Fetching orders history](orders_history.md)
* [Complex pagination example](complex_pagination/index.md)

View file

@ -0,0 +1,34 @@
In this example we will fetch all history entries for 1 month from the API.
```php
use RetailCrm\Api\Enum\PaginationLimit;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
use RetailCrm\Api\Model\Filter\Orders\OrderHistoryFilterV4Type;
use RetailCrm\Api\Model\Request\Orders\OrdersHistoryRequest;
$history = [];
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
$request = new OrdersHistoryRequest();
$request->filter = new OrderHistoryFilterV4Type();
$request->limit = PaginationLimit::LIMIT_100;
$request->filter->startDate = (new DateTime())->sub(new DateInterval('P1M')); // History for 1 month by default.
do {
time_nanosleep(0, 100000000); // 10 requests per second
try {
$response = $client->orders->history($request);
} catch (ClientExceptionInterface | ApiExceptionInterface $exception) {
echo $exception;
break;
}
$history = array_merge($history, $response->history);
$request->filter->startDate = null;
$request->filter->sinceId = end($response->history)->id;
} while ($response->pagination->currentPage < $response->pagination->totalPageCount);
// Here you can process all history entries in the `$history` variable.
```

255
doc/usage/instantiation.md Normal file
View file

@ -0,0 +1,255 @@
## Instantiation
There are several ways to instantiate the Client. Let's take look at them: from the simplest one to the most complex.
* [Instantiation via `SimpleClientFactory`](#instantiation-via-simpleclientfactory)
* [Instantiation via `ClientFactory`](#instantiation-via-clientfactory)
+ [Simple example](#simple-example)
+ [Abstract example](#abstract-example)
+ [Real-world example (Symfony)](#real-world-example-symfony)
* [Instantiation via `ClientBuilder`](#instantiation-via-clientbuilder)
### Instantiation via `SimpleClientFactory`
It's the easiest way to instantiate an API client. You can use this method if you just want to use API client without
thinking much about integration with the app or framework.
Example:
```php
use RetailCrm\Api\Factory\SimpleClientFactory;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'key');
```
That's it. You can use `$client` to make requests. However, even this simple method allows you to pass the `CacheItemPoolInterface` instance
(`symfony/cache` is being used by default) or path to the cache directory. The cache is being used by the client to store
annotations which omits the necessity to parse them each time you send a request.
Example with the `CacheItemPoolInterface` instance (we're using Redis as cache here):
```php
use Symfony\Component\Cache\Adapter\RedisAdapter;
use RetailCrm\Api\Enum\CacheDirectories;
use RetailCrm\Api\Factory\SimpleClientFactory;
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$client = SimpleClientFactory::createWithCache(
'https://test.retailcrm.pro',
'key',
new RedisAdapter($redis, CacheDirectories::DIR_NAME)
);
```
`CacheDirectories::DIR_NAME` is optional here. You can use any other namespace you want. By default, `CacheDirectories::DIR_NAME`
will be used as a namespace by default if the cache instance was created by the client automatically.
It's easier to use `SimpleClientFactory::createWithCacheDir` if you just want to specify your directory for cache instead of
default temporary directory:
```php
use RetailCrm\Api\Factory\SimpleClientFactory;
$client = SimpleClientFactory::createWithCacheDir('https://test.retailcrm.pro', 'key', __DIR__ . '/cache');
```
You can find more details about `SimpleClientFactory` in the documentation:
* [`SimpleClientFactory`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Factory-SimpleClientFactory.html)
### Instantiation via `ClientFactory`
The main difference between `SimpleClientFactory` and `ClientFactory` is the fact that the second factory is stateful.
It allows you to better integrate this client with your application. Consider the following: you may just set all global client
dependencies into the `ClientFactory` during the dependency container instantiation, and use the factory after that
to instantiate any number of clients you want.
#### Simple example
Here's an example of `ClientFactory` usage without any integration with the framework or the app:
```php
use RetailCrm\Api\Factory\ClientFactory;
use League\Event\EventDispatcher;
$eventDispatcher = new EventDispatcher();
$factory = new ClientFactory();
$factory
->setCacheDir('/tmp/retailcrm_cache')
->setEventDispatcher($eventDispatcher);
$client = $factory->createClient('https://test.retailcrm.pro', 'key');
```
Doesn't look that impressive, right? That's because `ClientFactory` is not supposed to be used just like that.
It won't give you any benefits in comparison to `SimpleClientFactory` with such usage.
Let's take a look at the more complex example. We'll use `league/container` here to demonstrate how you can integrate
`ClientFactory` with the DI you're using.
#### Abstract example
Let's assume that you have a specific service that should instantiate a Client for some internal purposes.
Here it is:
```php
namespace App\Services;
use RetailCrm\Api\Interfaces\ClientFactoryInterface;
use Throwable;
class ClientFactoryDependentService
{
/** @var ClientFactoryInterface */
private $clientFactory;
public function __construct(ClientFactoryInterface $clientFactory)
{
$this->clientFactory = $clientFactory;
}
public function isApiAccessible(string $apiUrl, string $apiKey): bool
{
try {
$client = $this->clientFactory->createClient($apiUrl, $apiKey);
$client->api->apiVersions();
return true;
} catch (Throwable $throwable) {
return false;
}
}
}
```
And you have a controller which returns a JSON response with the API availability flag. It requires the service above.
```php
namespace App\Controller;
use Some\Framework\Specific\Annotations\Route;
use Some\Framework\Specific\Controller;
use Some\Framework\Specific\Response;
class HealthCheckController extends Controller
{
/** @var ClientFactoryDependentService */
private $service;
public function __construct(ClientFactoryDependentService $service)
{
$this->service = $service;
}
/**
* @Route("/healthcheck")
*/
public function healthCheckAction(): Response
{
return $this->responseJson([
'is_api_accessible' => $this->service->isApiAccessible($_GET['apiUrl'], $_GET['apiKey'])
]);
}
}
```
During the container configuration you configure the container to instantiate proper `ClientFactory` once. After that
you can use the factory in any service you want. Here's an example with the `league/container`:
```php
use RetailCrm\Api\Factory\ClientFactory;
use RetailCrm\Api\Interfaces\ClientFactoryInterface;
use League\Container\Container;
use League\Event\EventDispatcher;
use Psr\Cache\CacheItemPoolInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use App\Services\ClientFactoryDependentService;
use App\Controller\HealthCheckController;
$container = new Container();
$container->add(CacheItemPoolInterface::class, new FilesystemAdapter('test_app'));
$container->add(EventDispatcherInterface::class, EventDispatcher::class);
$container->add(ClientFactoryInterface::class, ClientFactory::class)
->addMethodCalls([
'setCache' => [CacheItemPoolInterface::class],
'setEventDispatcher' => [EventDispatcherInterface::class],
]);
$container->add(ClientFactoryDependentService::class)->addArgument(ClientFactoryInterface::class);
$container->add(HealthCheckController::class)->addArgument(ClientFactoryDependentService::class);
```
That's it! Now your controller is using the underlying service to form a response. The service is using `ClientFactory`
which is being instantiated only once by the container. `ClientFactory` can be injected into other services as well, and it
will be the same instance.
#### Real-world example (Symfony)
You can use this exact service definition to instantiate a `ClientFactory` instance:
```yaml
RetailCrm\Api\Interfaces\ClientFactoryInterface:
class: 'RetailCrm\Api\Factory\ClientFactory'
calls:
- setCacheDir: ['%kernel.cache_dir%']
- setEventDispatcher: ['@event_dispatcher']
```
That's it. Now you can autowire `ClientFactoryInterface` in your services. It also allows you to mock the factory itself
in the tests using `services_test.yml`.
You can find more details about `ClientFactory` in the documentation:
* [`ClientFactory`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Factory-ClientFactory.html)
### Instantiation via `ClientBuilder`
`ClientBuilder` is the most powerful, and most complex method of client instantiation. It allows you to replace any client's
external dependencies with your own implementations. Here's an example of `ClientBuilder` usage:
```php
use Http\Client\Curl\Client as CurlClient;
use League\Event\EventDispatcher;
use Nyholm\Psr7\Factory\Psr17Factory;
use RetailCrm\Api\Builder\ClientBuilder;
use RetailCrm\Api\Builder\FormEncoderBuilder;
use RetailCrm\Api\Component\Transformer\RequestTransformer;
use RetailCrm\Api\Component\Transformer\ResponseTransformer;
use RetailCrm\Api\Factory\ApiExceptionFactory;
use RetailCrm\Api\Factory\RequestPipelineFactory;
use RetailCrm\Api\Factory\ResponsePipelineFactory;
use RetailCrm\Api\Handler\Request\HeaderAuthenticatorHandler;
$eventDispatcher = new EventDispatcher();
$psr17Factory = new Psr17Factory();
$httpClient = new CurlClient();
$builder = new ClientBuilder();
$formEncoder = (new FormEncoderBuilder())->setCacheDir('cache')->build();
$client = $builder->setApiUrl('https://test.retailcrm.pro')
->setAuthenticatorHandler(new HeaderAuthenticatorHandler('apiKey'))
->setFormEncoder($formEncoder)
->setHttpClient($httpClient)
->setRequestTransformer(new RequestTransformer(
RequestPipelineFactory::createDefaultPipeline(
$formEncoder,
$psr17Factory, // PSR-17 UriFactoryInterface
$psr17Factory, // PSR-17 RequestFactoryInterface
$psr17Factory // PSR-17 StreamFactoryInterface
)
))
->setResponseTransformer(new ResponseTransformer(
ResponsePipelineFactory::createDefaultPipeline(
$formEncoder->getSerializer(),
new ApiExceptionFactory(),
$eventDispatcher
)
))->build();
```
You can find more details about this in the documentation:
* [`ClientBuilder`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Builder-ClientBuilder.html)
* [`FormEncoderBuilder`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Builder-FormEncoderBuilder.html)
* [`RequestPipelineFactory`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Factory-RequestPipelineFactory.html)
* [`ResponsePipelineFactory`](https://retailcrm.github.io/api-client-php/classes/RetailCrm-Api-Factory-ResponsePipelineFactory.html)

View file

@ -0,0 +1,144 @@
## Sending a request
You need three things to send a request:
1. [Resource group, DTOs, and method](#choosing-correct-resource-group-dtos-and-method)
2. [Send a request](#sending-a-request)
3. [Deal with exceptions](#dealing-with-exceptions)
### Choosing correct resource group, DTOs, and method
First, take a look at the API itself:
* [English](https://docs.retailcrm.pro/Developers/API/APIVersions/APIv5)
* [Русский](https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5)
Choose a method you want to use. Which one is yours depend on the task you want to perform.
Then take look at the API again. It consists of several blocks, each block is responsible for a specific set of features.
The Client itself is also separated to those blocks or resource groups (full list of them can be found [here](../structure.md#resource-groups)).
Each resource group corresponds to an equal block in the documentation. For example. `customersCorporate` resource group implements methods
in the _Corporate customers_ API block (рус. _Корпоративные клиенты_).
Just look at the method you want to use and pick a proper resource group. That's it. Let's move on to the DTOs.
Choosing proper DTOs is also easy. Each client method defines expected parameter types. If the method doesn't have any
parameters, then the API method is also doesn't require any parameters and can be just called. The most noteworthy would be
`/api/api-versions` and `/api/credentials` methods. They can be called from the Client instance like that:
```php
use RetailCrm\Api\Factory\SimpleClientFactory;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'key');
$client->api->apiVersions();
$client->api->credentials();
```
The majority of the methods will require some parameters. Usually it's a request DTO, some path parameters (like customer id)
or both. For example, `/api/v5/customers/create` method requires request DTO, `/api/v5/customers/{externalId}` requires
customer ID and `/api/v5/customers/{externalId}/edit` requires both.
The corresponding methods on the client instance are:
| API method | Client instance method |
| ------------------------------------- | ---------------------------- |
| `/api/v5/customers/create` | `$client->customers->create` |
| `/api/v5/customers/{externalId}` | `$client->customers->get` |
| `/api/v5/customers/{externalId}/edit` | `$client->customers->edit` |
Look at the method definitions (they have also shown by the IDE):
```php
public function create(CustomersCreateRequest $request): IdResponse;
public function get($identifier, ?BySiteRequest $request = null): CustomersGetResponse;
public function edit($identifier, CustomersEditRequest $request): CustomersEditResponse;
```
Just use types from the type hints and everything will work. The DTO's fields also have type declarations but in the form
of the `@var` annotation tags. That's how you can choose the correct type for the DTO fields.
### Sending a request
Using information from the previous article you can easily construct a request.
This request will create a new customer.
```php
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Request\Customers\CustomersCreateRequest;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'key');
$request = new CustomersCreateRequest();
$request->customer = new Customer();
$request->customer->email = 'test@example.com';
$request->site = 'site';
$response = $client->customers->create($request);
```
This one will fetch specific customer from the API by the ID and site.
```php
use RetailCrm\Api\Enum\ByIdentifier;use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Request\BySiteRequest;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'key');
$response = $client->customers->get(1, new BySiteRequest(ByIdentifier::ID, 'site'));
echo $response->customer->email;
```
And this one will edit specific customer:
```php
use RetailCrm\Api\Enum\ByIdentifier;
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Request\Customers\CustomersEditRequest;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'key');
$request = new CustomersEditRequest();
$request->customer = new Customer();
$request->customer->email = 'test@example.com';
$request->site = 'site';
$request->by = ByIdentifier::ID;
$response = $client->customers->edit(1, $request);
echo "Edited customer ID: " . $response->id;
```
### Dealing with exceptions
What will happen if an API error is returned for the one of requests above? Or the network is not working at all?
An exception will happen.
There are two main exception types:
* `RetailCrm\Api\Interfaces\ApiExceptionInterface` is thrown if API returned an error.
* `RetailCrm\Api\Interfaces\ClientExceptionInterface` is thrown if the request cannot be sent due to an internal error.
You can use those like this:
```php
use RetailCrm\Api\Factory\SimpleClientFactory;
use RetailCrm\Api\Interfaces\ApiExceptionInterface;
use RetailCrm\Api\Interfaces\ClientExceptionInterface;
$client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'key');
try {
$response = $client->customers->list();
} catch (ApiExceptionInterface $exception) {
echo $exception;
} catch (ClientExceptionInterface $exception) {
echo 'Client error: ' . $exception->getMessage();
}
if (isset($response)) {
echo "Customers: " . print_r($response->customers, true);
}
```
More information about exceptions can be found on the [error handling](error_handling.md) page.

15
doc/usage/usage.md Normal file
View file

@ -0,0 +1,15 @@
## Usage
* [Instantiation](instantiation.md)
* [Sending a request](sending_a_request.md)
* [Choosing correct resource group, DTOs, and method](sending_a_request.md#choosing-correct-resource-group-dtos-and-method)
* [Sending a request](sending_a_request.md#sending-a-request)
* [Dealing with exceptions](sending_a_request.md#dealing-with-exceptions)
* [Error handling](error_handling.md)
* [Examples](examples)
+ [How to create an order](examples/create_order.md)
+ [How to receive the list of orders](examples/fetch_orders.md)
+ [How to handle all Client's exceptions](examples/complete_error_handling_example.md)
+ [Fetching orders history](examples/orders_history.md)
+ [Complex pagination example](examples/complex_pagination/index.md)
* [Event handling](event_handing.md)

View file

@ -1,6 +0,0 @@
<?php
namespace IntaroCrm\Exception;
class ApiException extends \Exception
{
}

View file

@ -1,6 +0,0 @@
<?php
namespace IntaroCrm\Exception;
class CurlException extends \Exception
{
}

View file

@ -1,578 +0,0 @@
<?php
namespace IntaroCrm;
class RestApi
{
protected $apiUrl;
protected $apiKey;
protected $apiVersion = '2';
protected $generatedAt;
protected $parameters;
/**
* @param string $crmUrl - адрес CRM
* @param string $apiKey - ключ для работы с api
*/
public function __construct($crmUrl, $apiKey)
{
$this->apiUrl = $crmUrl.'/api/v'.$this->apiVersion.'/';
$this->apiKey = $apiKey;
$this->parameters = array('apiKey' => $this->apiKey);
}
/* Методы для работы с заказами */
/**
* Получение заказа по id
*
* @param string $id - идентификатор заказа
* @param string $by - поиск заказа по id или externalId
* @return array - информация о заказе
*/
public function orderGet($id, $by = 'externalId')
{
$url = $this->apiUrl.'orders/'.$id;
if ($by != 'externalId')
$this->parameters['by'] = $by;
$result = $this->curlRequest($url);
return $result;
}
/**
* Создание заказа
*
* @param array $order- информация о заказе
* @return array
*/
public function orderCreate($order)
{
$dataJson = json_encode($order);
$this->parameters['order'] = $dataJson;
$url = $this->apiUrl.'orders/create';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Изменение заказа
*
* @param array $order- информация о заказе
* @return array
*/
public function orderEdit($order)
{
$dataJson = json_encode($order);
$this->parameters['order'] = $dataJson;
$url = $this->apiUrl.'orders/'.$order['externalId'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Пакетная загрузка заказов
*
* @param array $orders - массив заказов
* @return array
*/
public function orderUpload($orders)
{
$dataJson = json_encode($orders);
$this->parameters['orders'] = $dataJson;
$url = $this->apiUrl.'orders/upload';
$result = $this->curlRequest($url, 'POST');
if (is_null($result) && isset($result['uploadedOrders']))
return $result['uploadedOrders'];
return $result;
}
/**
* Обновление externalId у заказов с переданными id
*
* @param array $orders- массив, содержащий id и externalId заказа
* @return array
*/
public function orderFixExternalIds($order)
{
$dataJson = json_encode($order);
$this->parameters['orders'] = $dataJson;
$url = $this->apiUrl.'orders/fix-external-ids';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Удаление заказа
*
* @param string $id - идентификатор заказа
* @param string $by - поиск заказа по id или externalId
* @return array
*/
/*
public function orderDelete($id, $by = 'externalId')
{
$url = $this->apiUrl.'orders/'.$id.'/delete';
if ($by != 'externalId')
$this->parameters['by'] = $by;
$result = $this->curlRequest($url, 'POST');
return $result;
}*/
/**
* Получение последних измененных заказов
*
* @param DateTime $startDate - начальная дата выборки
* @param DateTime $endDate - конечная дата
* @param int $limit - ограничение на размер выборки
* @param int $offset - сдвиг
* @return array - массив заказов
*/
public function orderHistory($startDate = null, $endDate = null, $limit = 100, $offset = 0)
{
$url = $this->apiUrl.'orders/history';
$this->parameters['startDate'] = $startDate;
$this->parameters['endDate'] = $endDate;
$this->parameters['limit'] = $limit;
$this->parameters['offset'] = $offset;
$result = $this->curlRequest($url);
return $result;
}
/* Методы для работы с клиентами */
/**
* Получение клиента по id
*
* @param string $id - идентификатор
* @param string $by - поиск заказа по id или externalId
* @return array - информация о клиенте
*/
public function customerGet($id, $by = 'externalId')
{
$url = $this->apiUrl.'customers/'.$id;
if ($by != 'externalId')
$this->parameters['by'] = $by;
$result = $this->curlRequest($url);
return $result;
}
/**
* Создание клиента
*
* @param array $customer - информация о клиенте
* @return array
*/
public function customerCreate($customer)
{
$dataJson = json_encode($customer);
$this->parameters['customer'] = $dataJson;
$url = $this->apiUrl.'customers/create';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Редактирование клиента
*
* @param array $customer - информация о клиенте
* @return array
*/
public function customerEdit($customer)
{
$dataJson = json_encode($customer);
$this->parameters['customer'] = $dataJson;
$url = $this->apiUrl.'customers/'.$customer['externalId'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Пакетная загрузка клиентов
*
* @param array $customers - массив клиентов
* @return array
*/
public function customerUpload($customers)
{
$dataJson = json_encode($customers);
$this->parameters['customers'] = $dataJson;
$url = $this->apiUrl.'customers/upload';
$result = $this->curlRequest($url, 'POST');
if (is_null($result) && isset($result['uploaded']))
return $result['uploaded'];
return $result;
}
/**
* Обновление externalId у клиентов с переданными id
*
* @param array $customers- массив, содержащий id и externalId заказа
* @return array
*/
public function customerFixExternalIds($customers)
{
$dataJson = json_encode($customers);
$this->parameters['customers'] = $dataJson;
$url = $this->apiUrl.'customers/fix-external-ids';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Удаление клиента
*
* @param string $id - идентификатор
* @param string $by - поиск заказа по id или externalId
* @return array
*/
/*
public function customerDelete($id, $by = 'externalId')
{
$url = $this->apiUrl.'customers/'.$id.'/delete';
if ($by != 'externalId')
$this->parameters['by'] = $by;
$result = $this->curlRequest($url, 'POST');
return $result;
}*/
/**
* Получение списка заказов клиента
*
* @param string $id - идентификатор клиента
* @param string $by - поиск заказа по id или externalId
* @param DateTime $startDate - начальная дата выборки
* @param DateTime $endDate - конечная дата
* @param int $limit - ограничение на размер выборки
* @param int $offset - сдвиг
* @return array - массив заказов
*/
public function customerOrdersList($id, $startDate = null, $endDate = null,
$limit = 100, $offset = 0, $by = 'externalId')
{
$url = $this->apiUrl.'customers/'.$id.'/orders';
if ($by != 'externalId')
$this->parameters['by'] = $by;
$this->parameters['startDate'] = $startDate;
$this->parameters['endDate'] = $endDate;
$this->parameters['limit'] = $limit;
$this->parameters['offset'] = $offset;
$result = $this->curlRequest($url);
return $result;
}
/* Методы для работы со справочниками */
/**
* Получение списка типов доставки
*
* @return array - массив типов доставки
*/
public function deliveryTypesList()
{
$url = $this->apiUrl.'reference/delivery-types';
$result = $this->curlRequest($url);
return $result;
}
/**
* Редактирование типа доставки
*
* @param array $deliveryType - информация о типе доставки
* @return array
*/
public function deliveryTypeEdit($deliveryType)
{
$dataJson = json_encode($deliveryType);
$this->parameters['deliveryType'] = $dataJson;
$url = $this->apiUrl.'reference/delivery-types/'.$deliveryType['code'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Получение списка служб доставки
*
* @return array - массив типов доставки
*/
public function deliveryServicesList()
{
$url = $this->apiUrl.'reference/delivery-services';
$result = $this->curlRequest($url);
return $result;
}
/**
* Редактирование службы доставки
*
* @param array $deliveryService - информация о типе доставки
* @return array
*/
public function deliveryServiceEdit($deliveryService)
{
$dataJson = json_encode($deliveryService);
$this->parameters['deliveryService'] = $dataJson;
$url = $this->apiUrl.'reference/delivery-services/'.$deliveryService['code'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Получение списка типов оплаты
*
* @return array - массив типов оплаты
*/
public function paymentTypesList()
{
$url = $this->apiUrl.'reference/payment-types';
$result = $this->curlRequest($url);
return $result;
}
/**
* Редактирование типа оплаты
*
* @param array $paymentType - информация о типе оплаты
* @return array
*/
public function paymentTypesEdit($paymentType)
{
$dataJson = json_encode($paymentType);
$this->parameters['paymentType'] = $dataJson;
$url = $this->apiUrl.'reference/payment-types/'.$paymentType['code'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Получение списка статусов оплаты
*
* @return array - массив статусов оплаты
*/
public function paymentStatusesList()
{
$url = $this->apiUrl.'reference/payment-statuses';
$result = $this->curlRequest($url);
return $result;
}
/**
* Редактирование статуса оплаты
*
* @param array $paymentStatus - информация о статусе оплаты
* @return array
*/
public function paymentStatusesEdit($paymentStatus)
{
$dataJson = json_encode($paymentStatus);
$this->parameters['paymentStatus'] = $dataJson;
$url = $this->apiUrl.'reference/payment-statuses/'.$paymentStatus['code'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Получение списка типов заказа
*
* @return array - массив типов заказа
*/
public function orderTypesList()
{
$url = $this->apiUrl.'reference/order-types';
$result = $this->curlRequest($url);
return $result;
}
/**
* Редактирование типа заказа
*
* @param array $orderType - информация о типе заказа
* @return array
*/
public function orderTypesEdit($orderType)
{
$dataJson = json_encode($orderType);
$this->parameters['orderType'] = $dataJson;
$url = $this->apiUrl.'reference/order-types/'.$orderType['code'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Получение списка способов оформления заказа
*
* @return array - массив способов оформления заказа
*/
public function orderMethodsList()
{
$url = $this->apiUrl.'reference/order-methods';
$result = $this->curlRequest($url);
return $result;
}
/**
* Редактирование способа оформления заказа
*
* @param array $orderMethod - информация о способе оформления заказа
* @return array
*/
public function orderMethodsEdit($orderMethod)
{
$dataJson = json_encode($orderMethod);
$this->parameters['orderMethod'] = $dataJson;
$url = $this->apiUrl.'reference/order-methods/'.$orderMethod['code'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Получение списка статусов заказа
*
* @return array - массив статусов заказа
*/
public function orderStatusesList()
{
$url = $this->apiUrl.'reference/statuses';
$result = $this->curlRequest($url);
return $result;
}
/**
* Редактирование статуса заказа
*
* @param array $status - информация о статусе заказа
* @return array
*/
public function orderStatusEdit($status)
{
$dataJson = json_encode($status);
$this->parameters['status'] = $dataJson;
$url = $this->apiUrl.'reference/statuses/'.$status['code'].'/edit';
$result = $this->curlRequest($url, 'POST');
return $result;
}
/**
* Получение списка групп статусов заказа
*
* @return array - массив групп статусов заказа
*/
public function orderStatusGroupsList()
{
$url = $this->apiUrl.'reference/status-groups';
$result = $this->curlRequest($url);
return $result;
}
/**
* Обновление статистики
*
* @return array - статус вып обновления
*/
public function statisticUpdate()
{
$url = $this->apiUrl.'statistic/update';
$result = $this->curlRequest($url);
return $result;
}
/**
* @return \DateTime
*/
public function getGeneratedAt() {
return $this->generatedAt;
}
protected function getErrorMessage($response)
{
$str = '';
if (isset($response['message']))
$str = $response['message'];
elseif (isset($response[0]['message']))
$str = $response[0]['message'];
elseif (isset($response['error']) && isset($response['error']['message']))
$str = $response['error']['message'];
elseif (isset($response['errorMsg']))
$str = $response['errorMsg'];
if (isset($response['errors']) && sizeof($response['errors'])) {
foreach ($response['errors'] as $error)
$str .= '. ' . $error;
}
if (!strlen($str))
return 'Application Error';
return $str;
}
protected function curlRequest($url, $method = 'GET', $format = 'json')
{
if ($method == 'GET' && !is_null($this->parameters))
$url .= '?'.http_build_query($this->parameters);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FAILONERROR, FALSE);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);// allow redirects
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return into a variable
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // times out after 30s
if ($method == 'POST')
{
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->parameters);
}
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
unset($this->parameters);
/* Сброс массива с параметрами */
$this->parameters = array('apiKey' => $this->apiKey);
$errno = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errno)
throw new Exception\CurlException($error, $errno);
$result = json_decode($response, true);
if ($statusCode >= 400 || isset($result['success']) && $result['success'] === false) {
throw new Exception\ApiException($this->getErrorMessage($result), $statusCode);
}
if (isset($result['generatedAt'])) {
$this->generatedAt = new \DateTime($result['generatedAt']);
unset($result['generatedAt']);
}
unset($result['success']);
if (count($result) == 0)
return true;
return reset($result);
}
}

0
models/.gitkeep Normal file
View file

17
phpcs.xml.dist Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<arg name="basepath" value="."/>
<arg name="cache" value=".php_cs.cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<rule ref="PSR12"/>
<file>src/</file>
<file>tests/</file>
<exclude-pattern>src/Component/Serializer/Generator/*</exclude-pattern>
<exclude-pattern>src/Component/Serializer/Parser/*</exclude-pattern>
<exclude-pattern>src/Component/Serializer/ArraySupportDecorator.php</exclude-pattern>
</ruleset>

21
phpdoc.dist.xml Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<phpdocumentor
configVersion="3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.phpdoc.org"
xsi:noNamespaceSchemaLocation="https://docs.phpdoc.org/latest/phpdoc.xsd"
>
<title>RetailCRM API Client</title>
<paths>
<output>docs/build/html</output>
<cache>docs/build/cache</cache>
</paths>
<version number="latest">
<api>
<visibility>public</visibility>
<source dsn=".">
<path>src</path>
</source>
</api>
</version>
</phpdocumentor>

49
phpmd.xml Normal file
View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="Ruleset"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>Ruleset</description>
<rule ref="rulesets/controversial.xml" />
<rule ref="rulesets/unusedcode.xml" />
<rule ref="rulesets/design.xml">
<exclude name="CouplingBetweenObjects" />
</rule>
<rule ref="rulesets/cleancode.xml">
<exclude name="StaticAccess" />
</rule>
<rule ref="rulesets/codesize.xml">
<exclude name="TooManyPublicMethods" />
<exclude name="TooManyFields" />
</rule>
<rule ref="rulesets/naming.xml">
<exclude name="ShortVariable" />
</rule>
<rule ref="rulesets/naming.xml/ShortVariable">
<properties>
<property name="minimum" value="2" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyPublicMethods">
<properties>
<property name="maxmethods" value="20" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyFields">
<properties>
<property name="maxfields" value="30" />
</properties>
</rule>
<rule ref="rulesets/design.xml/CouplingBetweenObjects">
<properties>
<property name="maximum" value="15" />
</properties>
</rule>
<exclude-pattern>tests/*</exclude-pattern>
<exclude-pattern>src/Component/Serializer/Generator/*</exclude-pattern>
<exclude-pattern>src/Component/Serializer/Parser/*</exclude-pattern>
</ruleset>

View file

@ -0,0 +1,246 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$config of static method Liip\\\\Serializer\\\\Configuration\\\\GeneratorConfiguration\\:\\:createFomArray\\(\\) expects array\\{default_group_combinations\\?\\: array\\<int, array\\<int, string\\>\\>\\|null, default_versions\\?\\: array\\<int, string\\>\\|null, classes\\?\\: array\\<class\\-string, array\\<string, mixed\\>\\>\\|null, options\\?\\: array\\<string, mixed\\>\\}, array\\{default_group_combinations\\: array\\{\\}, default_versions\\: array\\{\\}, classes\\: non\\-empty\\-array\\<string, array\\{\\}\\>\\} given\\.$#"
count: 1
path: src/Component/ModelsGenerator.php
-
message: "#^Parameter \\#3 \\$classesToGenerate of class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\DeserializerGenerator constructor expects array\\<int, class\\-string\\>, array\\<string\\> given\\.$#"
count: 1
path: src/Component/ModelsGenerator.php
-
message: "#^Parameter \\#2 \\$method of method Liip\\\\Serializer\\\\Template\\\\Deserialization\\:\\:renderSetter\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: src/Component/Serializer/Generator/DeserializerGenerator.php
-
message: "#^Parameter \\#4 \\$stack of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\DeserializerGenerator\\:\\:generateCodeForClass\\(\\) expects array\\<string, int\\<1, max\\>\\>, array given\\.$#"
count: 2
path: src/Component/Serializer/Generator/DeserializerGenerator.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\SerializerGenerator\\:\\:buildSerializerFunctionName\\(\\) should return string but returns string\\|null\\.$#"
count: 1
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Parameter \\#2 \\$method of method Liip\\\\Serializer\\\\Template\\\\Serialization\\:\\:renderGetter\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Parameter \\#3 \\$serializerGroups of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\SerializerGenerator\\:\\:generateCodeForClass\\(\\) expects array\\<int, string\\>, array given\\.$#"
count: 4
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Lexer extends generic class Doctrine\\\\Common\\\\Lexer\\\\AbstractLexer but does not specify its types\\: T, V$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Lexer\\:\\:getType\\(\\) has parameter \\$value with no type specified\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Lexer\\:\\:parse\\(\\) has no return type specified\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php
-
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, float\\|int\\|string given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php
-
message: "#^Parameter \\#1 \\$haystack of function strpos expects string, float\\|int\\|string given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Lexer.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) should return string but returns string\\|false\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:parse\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:parse\\(\\) should return array but returns mixed\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:visitArrayType\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:visitCompoundType\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:visitSimpleType\\(\\) never returns string so it can be removed from the return type\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\ParserInterface\\:\\:parse\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/ParserInterface.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:gatherClassAnnotations\\(\\) has parameter \\$reflectionClass with generic class ReflectionClass but does not specify its types\\: T$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getMethodName\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getProperty\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getReturnType\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:getSerializedName\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:isPostDeserializeMethod\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:isVirtualProperty\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parseClass\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parseMethods\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parseProperties\\(\\) has parameter \\$reflClass with generic class ReflectionClass but does not specify its types\\: T$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSParser\\:\\:parsePropertyAnnotations\\(\\) has parameter \\$annotations with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\<T of object\\>\\|T of object, string given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection not found\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Class Doctrine\\\\Common\\\\Collections\\\\Collection not found\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSTypeParser\\:\\:parseType\\(\\) has parameter \\$typeInfo with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Parameter \\#4 \\$traversableClass of class Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeIterable constructor expects class\\-string\\<Traversable\\>\\|null, string\\|null given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Cannot access property \\$value on RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\|null\\.$#"
count: 2
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Parameter \\#1 \\$string of function strlen expects string, int\\|string given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\<min, 0\\>\\|int\\<4, 8\\>\\|int\\<11, max\\>\\|string\\|null given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Parameter \\#1 \\$value of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:getConstant\\(\\) expects int, int\\|string\\|null given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Parser\\:\\:\\$token with generic class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token does not specify its types\\: T, V$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Parser.php
-
message: "#^Access to an undefined property object\\:\\:\\$position\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Token.php
-
message: "#^Access to an undefined property object\\:\\:\\$type\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Token.php
-
message: "#^Access to an undefined property object\\:\\:\\$value\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Token.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\:\\:fromArray\\(\\) has parameter \\$source with no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Token.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\:\\:fromArray\\(\\) return type with generic class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token does not specify its types\\: T, V$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Token.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\:\\:fromObject\\(\\) return type with generic class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token does not specify its types\\: T, V$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Token.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSCore\\\\Type\\\\Token\\<T of int\\|string,V of int\\|string\\>\\:\\:\\$type \\(\\(T of int\\|string\\)\\|null\\) does not accept int\\|string\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSCore/Type/Token.php
-
message: "#^Cannot call method getParameters\\(\\) on ReflectionMethod\\|null\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSTypeParser\\:\\:\\$useArrayDateFormat has no type specified\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php

329
phpstan-baseline.neon Normal file
View file

@ -0,0 +1,329 @@
parameters:
excludePaths:
- src/Component/Serializer/ArraySupportDecorator.php
ignoreErrors:
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Builder\\\\ClientBuilder\\:\\:buildHandlersChain\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Builder/ClientBuilder.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Builder\\\\FilesystemCacheBuilder\\:\\:getCacheDirPath\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Builder/FilesystemCacheBuilder.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Client\\:\\:getBaseUrl\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Client.php
-
message: "#^Cannot assign new offset to array\\<string\\>\\|string\\.$#"
count: 1
path: src/Command/CompilerPromptCommand.php
-
message: "#^Parameter \\#1 \\$array of function array_filter expects array, string given\\.$#"
count: 1
path: src/Command/CompilerPromptCommand.php
-
message: "#^Parameter \\#2 \\$haystack of function in_array expects array, array\\<string\\>\\|string given\\.$#"
count: 1
path: src/Command/CompilerPromptCommand.php
-
message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Command\\\\CompilerPromptCommand\\:\\:COMPILER_PLUGIN through static\\:\\:\\.$#"
count: 3
path: src/Command/CompilerPromptCommand.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Command\\\\CompilerPromptCommand\\:\\:activateAutoCompiler\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Command/CompilerPromptCommand.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Command\\\\CompilerPromptCommand\\:\\:activatePlugin\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Command/CompilerPromptCommand.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Command\\\\CompilerPromptCommand\\:\\:deactivateAutoCompiler\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Command/CompilerPromptCommand.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\ComposerLocator\\:\\:getBaseDirectory\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Component/ComposerLocator.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\ComposerLocator\\:\\:getPackageComposerJson\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/ComposerLocator.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\FilesIteratorChecksumGenerator\\:\\:\\$fileNameAccessor \\(callable\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
path: src/Component/FilesIteratorChecksumGenerator.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\FilesIteratorChecksumGenerator\\:\\:\\$hashFunc \\(callable\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
path: src/Component/FilesIteratorChecksumGenerator.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\FilesIteratorChecksumGenerator\\:\\:\\$keyTransformer \\(callable\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
path: src/Component/FilesIteratorChecksumGenerator.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\FormData\\\\FormEncoder\\:\\:processPostSerialize\\(\\) should return array but returns mixed\\.$#"
count: 1
path: src/Component/FormData/FormEncoder.php
-
message: "#^Parameter \\#1 \\$object of function get_class expects object, mixed given\\.$#"
count: 1
path: src/Component/FormData/FormEncoder.php
-
message: "#^Parameter \\#1 \\$object of method ReflectionMethod\\:\\:invokeArgs\\(\\) expects object\\|null, mixed given\\.$#"
count: 1
path: src/Component/FormData/FormEncoder.php
-
message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\<object\\>\\|object, class\\-string\\|false given\\.$#"
count: 1
path: src/Component/FormData/FormEncoder.php
-
message: "#^Parameter \\#1 \\$object of function get_class expects object, mixed given\\.$#"
count: 1
path: src/Component/FormData/Strategy/Encode/EntityStrategy.php
-
message: "#^Parameter \\#1 \\$object of method ReflectionProperty\\:\\:getValue\\(\\) expects object\\|null, mixed given\\.$#"
count: 1
path: src/Component/FormData/Strategy/Encode/EntityStrategy.php
-
message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\<object\\>\\|object, class\\-string\\|false given\\.$#"
count: 1
path: src/Component/FormData/Strategy/Encode/EntityStrategy.php
-
message: "#^Cannot cast mixed to float\\.$#"
count: 1
path: src/Component/FormData/Strategy/Encode/SimpleTypeStrategy.php
-
message: "#^Cannot cast mixed to int\\.$#"
count: 1
path: src/Component/FormData/Strategy/Encode/SimpleTypeStrategy.php
-
message: "#^Unsafe access to private property RetailCrm\\\\Api\\\\Component\\\\FormData\\\\Strategy\\\\Encode\\\\TypedArrayStrategy\\:\\:\\$innerTypesMatcher through static\\:\\:\\.$#"
count: 1
path: src/Component/FormData/Strategy/Encode/TypedArrayStrategy.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\FormData\\\\Strategy\\\\Encode\\\\TypedArrayStrategy\\:\\:getInnerTypes\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/FormData/Strategy/Encode/TypedArrayStrategy.php
-
message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Component\\\\FormData\\\\Strategy\\\\StrategyFactory\\:\\:TYPED_MATCHER through static\\:\\:\\.$#"
count: 2
path: src/Component/FormData/Strategy/StrategyFactory.php
-
message: "#^Unsafe access to private property RetailCrm\\\\Api\\\\Component\\\\FormData\\\\Strategy\\\\StrategyFactory\\:\\:\\$simpleTypes through static\\:\\:\\.$#"
count: 1
path: src/Component/FormData/Strategy/StrategyFactory.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\FormData\\\\Strategy\\\\StrategyFactory\\:\\:getArrayInnerTypes\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/FormData/Strategy/StrategyFactory.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\FormData\\\\Strategy\\\\StrategyFactory\\:\\:getDateTimeFormat\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/FormData/Strategy/StrategyFactory.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\FormData\\\\Strategy\\\\StrategyFactory\\:\\:isDateTime\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/FormData/Strategy/StrategyFactory.php
-
message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Component\\\\ModelsGenerator\\:\\:IGNORED_NAMESPACES through static\\:\\:\\.$#"
count: 1
path: src/Component/ModelsGenerator.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\ModelsGenerator\\:\\:createDir\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/ModelsGenerator.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\ModelsGenerator\\:\\:isNamespaceIgnored\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/ModelsGenerator.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\PhpFilesIterator\\:\\:\\$parent \\(Iterator\\<int\\|string, array\\|string\\>\\) does not accept RegexIterator\\<mixed, mixed, Traversable\\<TKey, TValue\\>\\>\\.$#"
count: 1
path: src/Component/PhpFilesIterator.php
-
message: "#^Property RetailCrm\\\\Api\\\\Component\\\\PhpFilesIterator\\:\\:\\$parent type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Component/PhpFilesIterator.php
-
message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Component\\\\PhpFilesIterator\\:\\:NAMESPACE_MATCHER through static\\:\\:\\.$#"
count: 1
path: src/Component/PhpFilesIterator.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ArraySupportDecorator\\:\\:fromArray\\(\\) should return array\\<int\\|string, mixed\\>\\|object but returns mixed\\.$#"
count: 1
path: src/Component/Serializer/ArraySupportDecorator.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ArraySupportDecorator\\:\\:getArrayValueType\\(\\) should return string but returns array\\<string\\>\\.$#"
count: 2
path: src/Component/Serializer/ArraySupportDecorator.php
-
message: "#^Parameter \\#1 \\$data of method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ArraySupportDecorator\\:\\:decodeArray\\(\\) expects array, mixed given\\.$#"
count: 1
path: src/Component/Serializer/ArraySupportDecorator.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ArraySupportDecorator\\:\\:getArrayValueType\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Component/Serializer/ArraySupportDecorator.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ArraySupportDecorator\\:\\:isArrayType\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Component/Serializer/ArraySupportDecorator.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ModelsChecksumGenerator\\:\\:getStoredChecksums\\(\\) should return array\\<string, string\\> but returns mixed\\.$#"
count: 1
path: src/Component/Serializer/ModelsChecksumGenerator.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ModelsChecksumGenerator\\:\\:getChecksumFileName\\(\\) through static\\:\\:\\.$#"
count: 4
path: src/Component/Serializer/ModelsChecksumGenerator.php
-
message: "#^Parameter \\#2 \\$string2 of function strncmp expects string, class\\-string\\|false given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Parameter \\#3 \\$annotation of static method Liip\\\\MetadataParser\\\\Exception\\\\ParseException\\:\\:unsupportedPropertyAnnotation\\(\\) expects string, class\\-string\\|false given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\Transformer\\\\DateTimeTransformer\\:\\:createFromFormat\\(\\) through static\\:\\:\\.$#"
count: 3
path: src/Component/Transformer/DateTimeTransformer.php
-
message: "#^Method RetailCrm\\\\Api\\\\Event\\\\AbstractRequestEvent\\:\\:getApiKey\\(\\) should return string but returns array\\|string\\.$#"
count: 1
path: src/Event/AbstractRequestEvent.php
-
message: "#^Property RetailCrm\\\\Api\\\\Event\\\\AbstractRequestEvent\\:\\:\\$apiKey \\(string\\) does not accept array\\|string\\.$#"
count: 1
path: src/Event/AbstractRequestEvent.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Exception\\\\ApiException\\:\\:getErrorMessage\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Exception/ApiException.php
-
message: "#^Property RetailCrm\\\\Api\\\\Model\\\\Response\\\\ErrorResponse\\:\\:\\$errorMsg \\(string\\) on left side of \\?\\? is not nullable\\.$#"
count: 2
path: src/Factory/ApiExceptionFactory.php
-
message: "#^Property RetailCrm\\\\Api\\\\Model\\\\Response\\\\ErrorResponse\\:\\:\\$errors \\(array\\<string\\>\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
path: src/Factory/ApiExceptionFactory.php
-
message: "#^Unsafe access to private property RetailCrm\\\\Api\\\\Handler\\\\Request\\\\PsrRequestHandler\\:\\:\\$methodsWithBody through static\\:\\:\\.$#"
count: 1
path: src/Handler/Request/PsrRequestHandler.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Handler\\\\Request\\\\RequestDataHandler\\:\\:queryShouldBeUsed\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Handler/Request/RequestDataHandler.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Handler\\\\Request\\\\RequestDataHandler\\:\\:throwEncodeException\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Handler/Request/RequestDataHandler.php
-
message: "#^Method RetailCrm\\\\Api\\\\Handler\\\\Response\\\\AbstractResponseHandler\\:\\:unmarshalBody\\(\\) should return RetailCrm\\\\Api\\\\Interfaces\\\\ResponseInterface but returns mixed\\.$#"
count: 1
path: src/Handler/Response/AbstractResponseHandler.php
-
message: "#^Method RetailCrm\\\\Api\\\\Handler\\\\Response\\\\AbstractResponseHandler\\:\\:unmarshalBody\\(\\) should return RetailCrm\\\\Api\\\\Interfaces\\\\ResponseInterface but returns object\\.$#"
count: 1
path: src/Handler/Response/AbstractResponseHandler.php
-
message: "#^Method RetailCrm\\\\Api\\\\Handler\\\\Response\\\\AbstractResponseHandler\\:\\:unmarshalBodyArray\\(\\) should return array\\<int\\|string, mixed\\> but returns mixed\\.$#"
count: 1
path: src/Handler/Response/AbstractResponseHandler.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Handler\\\\Response\\\\AbstractResponseHandler\\:\\:throwUnmarshalError\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Handler/Response/AbstractResponseHandler.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Handler\\\\Response\\\\AccountNotFoundHandler\\:\\:isContentType\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Handler/Response/AccountNotFoundHandler.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Handler\\\\Response\\\\FilesDownloadResponseHandler\\:\\:fileNameFromDisposition\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Handler/Response/FilesDownloadResponseHandler.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Handler\\\\Response\\\\FilesDownloadResponseHandler\\:\\:isFileRequest\\(\\) through static\\:\\:\\.$#"
count: 1
path: src/Handler/Response/FilesDownloadResponseHandler.php
-
message: "#^Property RetailCrm\\\\Api\\\\Model\\\\ResponseData\\:\\:\\$responseArray \\(array\\<int\\|string, mixed\\>\\) on left side of \\?\\? is not nullable\\.$#"
count: 1
path: src/Handler/Response/UnmarshalResponseHandler.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Model\\\\Entity\\\\Delivery\\\\TimeInterval\\:\\:createTime\\(\\) through static\\:\\:\\.$#"
count: 2
path: src/Model/Entity/Delivery/TimeInterval.php
-
message: "#^Method RetailCrm\\\\Api\\\\ResourceGroup\\\\AbstractApiResourceGroup\\:\\:sendRequest\\(\\) should return RetailCrm\\\\Api\\\\Interfaces\\\\ResponseInterface but returns object\\.$#"
count: 1
path: src/ResourceGroup/AbstractApiResourceGroup.php

9
phpstan.neon Normal file
View file

@ -0,0 +1,9 @@
includes:
- phpstan-baseline-serializer.neon
- phpstan-baseline.neon # TODO: This should be removed eventually.
parameters:
level: max
paths:
- src
- tests

42
phpunit.xml.dist Normal file
View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
backupGlobals="false"
colors="false"
bootstrap="tests/bootstrap.php"
backupStaticAttributes="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="false"
convertWarningsToExceptions="false"
processIsolation="true"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
stopOnRisky="false"
>
<coverage>
<include>
<directory>src</directory>
<directory>dev</directory>
</include>
<report>
<clover outputFile="coverage.xml"/>
</report>
</coverage>
<testsuites>
<testsuite name="Project Test Suite">
<file>tests/src/Command/ClearModelsCommandTest.php</file>
<file>tests/src/Command/GenerateModelsCommandTest.php</file>
<file>tests/src/Command/VerifyModelsCommandTest.php</file>
<directory>tests/src</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="test-report.xml"/>
</logging>
<php>
<ini name="memory_limit" value="4G" />
</php>
</phpunit>

View file

@ -0,0 +1,492 @@
<?php
/**
* PHP version 7.3
*
* @category ClientBuilder
* @package RetailCrm\Api\Builder
*/
namespace RetailCrm\Api\Builder;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Discovery\Psr18ClientDiscovery;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Log\LoggerInterface;
use RetailCrm\Api\Client;
use RetailCrm\Api\Component\Transformer\RequestTransformer;
use RetailCrm\Api\Component\Transformer\ResponseTransformer;
use RetailCrm\Api\Exception\Client\BuilderException;
use RetailCrm\Api\Factory\ApiExceptionFactory;
use RetailCrm\Api\Factory\RequestPipelineFactory;
use RetailCrm\Api\Factory\ResponsePipelineFactory;
use RetailCrm\Api\Interfaces\ApiExceptionFactoryAwareInterface;
use RetailCrm\Api\Interfaces\BuilderInterface;
use RetailCrm\Api\Interfaces\EventDispatcherAwareInterface;
use RetailCrm\Api\Interfaces\FormEncoderInterface;
use RetailCrm\Api\Interfaces\HandlerInterface;
use RetailCrm\Api\Interfaces\PsrFactoriesAwareInterface;
use RetailCrm\Api\Interfaces\RequestTransformerInterface;
use RetailCrm\Api\Interfaces\ResponseTransformerInterface;
use RetailCrm\Api\Interfaces\SerializerAwareInterface;
use RetailCrm\Api\Traits\EventDispatcherAwareTrait;
/**
* Class ClientBuilder
*
* @category ClientBuilder
* @package RetailCrm\Api\Builder
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
class ClientBuilder implements BuilderInterface, EventDispatcherAwareInterface
{
use EventDispatcherAwareTrait;
/** @var string */
private $apiUrl;
/** @var HandlerInterface|null */
private $authenticator;
/** @var ClientInterface|null */
private $httpClient;
/** @var \Psr\Log\LoggerInterface|null */
private $debugLogger;
/** @var RequestTransformerInterface|null */
private $requestTransformer;
/** @var \RetailCrm\Api\Interfaces\ResponseTransformerInterface|null */
protected $responseTransformer;
/** @var \RetailCrm\Api\Interfaces\FormEncoderInterface|null */
private $formEncoder;
/** @var \Psr\Http\Message\StreamFactoryInterface|null */
private $streamFactory;
/** @var \Psr\Http\Message\RequestFactoryInterface|null */
private $requestFactory;
/** @var \Psr\Http\Message\UriFactoryInterface|null */
private $uriFactory;
/** @var \RetailCrm\Api\Factory\ApiExceptionFactory|null */
private $apiExceptionFactory;
/** @var \RetailCrm\Api\Interfaces\HandlerInterface[] */
private $requestHandlers = [];
/** @var \RetailCrm\Api\Interfaces\HandlerInterface[] */
private $responseHandlers = [];
/**
* API URL. Looks like this: "https://test.retailcrm.pro/"
*
* @param string $apiUrl
*
* @return ClientBuilder
*/
public function setApiUrl(string $apiUrl): ClientBuilder
{
$this->apiUrl = $apiUrl;
return $this;
}
/**
* Request authenticator to append into request transformer pipeline.
*
* Do not use it if you already added a proper authenticator in the pipeline manually.
* You can use this method to drop authenticator from client builder (use null).
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface|null $authenticator
*
* @return ClientBuilder
*/
public function setAuthenticatorHandler(?HandlerInterface $authenticator): ClientBuilder
{
$this->authenticator = $authenticator;
return $this;
}
/**
* Set your PSR-18 HTTP client.
*
* Service discovery will be used if no client has been provided.
*
* @param \Psr\Http\Client\ClientInterface|null $httpClient
*
* @return ClientBuilder
*/
public function setHttpClient(?ClientInterface $httpClient): ClientBuilder
{
$this->httpClient = $httpClient;
return $this;
}
/**
* Set debug logger.
*
* The provided logger will be used to record all requests and responses.
* This feature consumes a lot of resources and shouldn't be used in production.
*
* @param \Psr\Log\LoggerInterface|null $debugLogger
*
* @return ClientBuilder
*/
public function setDebugLogger(?LoggerInterface $debugLogger): ClientBuilder
{
$this->debugLogger = $debugLogger;
return $this;
}
/**
* Set request transformer into API client.
*
* You can use this method to set your request transformer which will execute the pipeline.
* The default request transformer doesn't do anything besides calling provided chain of handlers.
*
* @param \RetailCrm\Api\Interfaces\RequestTransformerInterface|null $requestTransformer
*
* @return ClientBuilder
*/
public function setRequestTransformer(?RequestTransformerInterface $requestTransformer): ClientBuilder
{
$this->requestTransformer = $requestTransformer;
return $this;
}
/**
* Set response transformer into API client.
*
* You can use this method to set your response transformer which will execute the pipeline.
* The default response transformer doesn't do anything besides calling provided chain of handlers.
* The serializer instance for the request pipeline can be inferred from the provided FormEncoder instance.
*
* @param \RetailCrm\Api\Interfaces\ResponseTransformerInterface|null $responseTransformer
*
* @return ClientBuilder
*/
public function setResponseTransformer(?ResponseTransformerInterface $responseTransformer): ClientBuilder
{
$this->responseTransformer = $responseTransformer;
return $this;
}
/**
* Set form encoder into API client.
*
* Form encoder is a vital part of the API client. Its purpose is to transform provided request models
* into form-data. The result will be used as a query or POST body (depends on request type).
*
* @param \RetailCrm\Api\Interfaces\FormEncoderInterface $formEncoder
*
* @return ClientBuilder
*/
public function setFormEncoder(FormEncoderInterface $formEncoder): ClientBuilder
{
$this->formEncoder = $formEncoder;
return $this;
}
/**
* Sets PSR-17 compatible stream factory. You can skip this step if you want to use service discovery.
*
* @param \Psr\Http\Message\StreamFactoryInterface|null $streamFactory
*
* @return ClientBuilder
*/
public function setStreamFactory(?StreamFactoryInterface $streamFactory): ClientBuilder
{
$this->streamFactory = $streamFactory;
return $this;
}
/**
* Sets PSR-17 compatible request factory. You can skip this step if you want to use service discovery.
*
* @param \Psr\Http\Message\RequestFactoryInterface|null $requestFactory
*
* @return ClientBuilder
*/
public function setRequestFactory(?RequestFactoryInterface $requestFactory): ClientBuilder
{
$this->requestFactory = $requestFactory;
return $this;
}
/**
* Sets PSR-17 compatible URI factory. You can skip this step if you want to use service discovery.
*
* @param \Psr\Http\Message\UriFactoryInterface|null $uriFactory
*
* @return ClientBuilder
*/
public function setUriFactory(?UriFactoryInterface $uriFactory): ClientBuilder
{
$this->uriFactory = $uriFactory;
return $this;
}
/**
* Appends an additional request handler into the request processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface $handler
*
* @return ClientBuilder
*/
public function appendRequestHandler(HandlerInterface $handler): ClientBuilder
{
$this->requestHandlers[] = $handler;
return $this;
}
/**
* Appends an additional response handler into the response processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface $handler
*
* @return ClientBuilder
*/
public function appendResponseHandler(HandlerInterface $handler): ClientBuilder
{
$this->responseHandlers[] = $handler;
return $this;
}
/**
* Appends additional request handlers into the request processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface[] $handlers
*
* @return ClientBuilder
*/
public function appendRequestHandlers(array $handlers): ClientBuilder
{
foreach ($handlers as $handler) {
$this->appendRequestHandler($handler);
}
return $this;
}
/**
* Appends additional response handlers into the response processing chain.
*
* @param \RetailCrm\Api\Interfaces\HandlerInterface[] $handlers
*
* @return ClientBuilder
*/
public function appendResponseHandlers(array $handlers): ClientBuilder
{
foreach ($handlers as $handler) {
$this->appendResponseHandler($handler);
}
return $this;
}
/**
* Builds client with provided dependencies.
*
* @inheritDoc
*/
public function build(): Client
{
$this->validateBuilder();
if (
null !== $this->authenticator &&
null !== $this->requestTransformer &&
null !== $this->requestTransformer->getHandler()
) {
$this->requestTransformer->getHandler()->append($this->authenticator);
}
if (null === $this->requestTransformer) {
$this->requestTransformer = $this->buildRequestTransformer();
}
if (null === $this->responseTransformer) {
$this->responseTransformer = $this->buildResponseTransformer();
}
$this->appendAdditionalRequestHandlers();
$this->appendAdditionalResponseHandlers();
return new Client(
$this->apiUrl,
$this->httpClient ?: Psr18ClientDiscovery::find(),
$this->requestTransformer, // @phpstan-ignore-line
$this->responseTransformer, // @phpstan-ignore-line
$this->streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(),
$this->eventDispatcher,
$this->debugLogger
);
}
/**
* Check if builder is ready to build a Client instance.
*
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*/
private function validateBuilder(): void
{
if (empty($this->apiUrl)) {
throw new BuilderException('apiUrl must not be empty', ['apiUrl']);
}
if (empty($this->authenticator) && empty($this->requestTransformer)) {
throw new BuilderException(
'Authenticator or RequestTransformer must be present',
['authenticator', 'requestTransformer']
);
}
}
/**
* Appends additional request handlers into the request and response processor chain (if needed).
*
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*/
private function appendAdditionalRequestHandlers(): void
{
if (
null !== $this->requestTransformer &&
null !== $this->requestTransformer->getHandler() &&
count($this->requestHandlers) > 0
) {
foreach ($this->requestHandlers as $handler) {
if ($handler instanceof PsrFactoriesAwareInterface) {
$handler->setRequestFactory($this->requestFactory ?: Psr17FactoryDiscovery::findRequestFactory());
$handler->setStreamFactory($this->streamFactory ?: Psr17FactoryDiscovery::findStreamFactory());
$handler->setUriFactory($this->uriFactory ?: Psr17FactoryDiscovery::findUriFactory());
}
}
$this->requestTransformer->getHandler()->append(static::buildHandlersChain($this->requestHandlers));
}
}
/**
* Appends additional response handlers into the request and response processor chain (if needed).
*
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function appendAdditionalResponseHandlers(): void
{
if (
null !== $this->responseTransformer &&
null !== $this->responseTransformer->getHandler() &&
count($this->responseHandlers) > 0
) {
foreach ($this->responseHandlers as $handler) {
if ($handler instanceof SerializerAwareInterface && null !== $this->formEncoder) {
$handler->setSerializer($this->formEncoder->getSerializer());
}
if ($handler instanceof ApiExceptionFactoryAwareInterface && null !== $this->apiExceptionFactory) {
$handler->setApiExceptionFactory($this->apiExceptionFactory);
}
if ($handler instanceof EventDispatcherAwareInterface) {
$handler->setEventDispatcher($this->eventDispatcher);
}
}
$this->responseTransformer->getHandler()->append(static::buildHandlersChain($this->responseHandlers));
}
}
/**
* Builds RequestTransformer with default pipeline and authenticator.
*
* @return \RetailCrm\Api\Component\Transformer\RequestTransformer
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*/
private function buildRequestTransformer(): RequestTransformer
{
if (null === $this->formEncoder) {
throw new BuilderException(
"You must provide a FormEncoder instance in order to delegate " .
"RequestTransformer instantiation to the ClientBuilder."
);
}
if (null === $this->authenticator) {
throw new BuilderException(
"You must provide an authenticator handler instance in order to delegate " .
"RequestTransformer instantiation to the ClientBuilder."
);
}
return new RequestTransformer(
RequestPipelineFactory::createDefaultPipeline(
$this->formEncoder,
$this->uriFactory ?: Psr17FactoryDiscovery::findUriFactory(),
$this->requestFactory ?: Psr17FactoryDiscovery::findRequestFactory(),
$this->streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(),
$this->authenticator
)
);
}
/**
* Builds ResponseTransformer.
*
* @return \RetailCrm\Api\Component\Transformer\ResponseTransformer
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*/
private function buildResponseTransformer(): ResponseTransformer
{
if (null === $this->formEncoder) {
throw new BuilderException(
"You must provide a FormEncoder instance in order to delegate " .
"ResponseTransformer instantiation to the ClientBuilder."
);
}
if (null === $this->apiExceptionFactory) {
$this->apiExceptionFactory = new ApiExceptionFactory();
}
return new ResponseTransformer(ResponsePipelineFactory::createDefaultPipeline(
$this->formEncoder->getSerializer(),
$this->apiExceptionFactory,
$this->eventDispatcher
));
}
/**
* Connect all handlers in the array into chain, return first handler.
*
* @param HandlerInterface[] $handlers
*
* @return \RetailCrm\Api\Interfaces\HandlerInterface
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*/
private static function buildHandlersChain(array $handlers): HandlerInterface
{
if (empty($handlers)) {
throw new BuilderException('Supplied handlers chain must contain at least one handler');
}
if (1 === count($handlers)) {
return $handlers[0];
}
for ($i = 0, $iMax = count($handlers) - 1; $i < $iMax; $i++) {
$handlers[$i]->setNext($handlers[$i + 1]);
}
return $handlers[0];
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* PHP version 7.3
*
* @category FilesystemCacheBuilder
* @package RetailCrm\Api\Builder
*/
namespace RetailCrm\Api\Builder;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use RetailCrm\Api\Enum\CacheDirectories;
use RetailCrm\Api\Exception\Client\BuilderException;
use RetailCrm\Api\Interfaces\BuilderInterface;
/**
* Class FilesystemCacheBuilder
*
* @category FilesystemCacheBuilder
* @package RetailCrm\Api\Builder
*/
class FilesystemCacheBuilder implements BuilderInterface
{
/** @var string */
private $cacheDir;
/**
* @param string $cacheDir
*
* @return FilesystemCacheBuilder
*/
public function setCacheDir(string $cacheDir): FilesystemCacheBuilder
{
$this->cacheDir = $cacheDir;
return $this;
}
/**
* Builds and returns filesystem cache.
*
* @return CacheItemPoolInterface
* @inheritDoc
*/
public function build(): CacheItemPoolInterface
{
if (empty($this->cacheDir)) {
return new FilesystemAdapter(CacheDirectories::DIR_NAME);
}
$cacheDir = static::getCacheDirPath($this->cacheDir);
$this->createDir($cacheDir);
return new FilesystemAdapter('', 0, $cacheDir);
}
/**
* @param string $dir
*
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*/
private function createDir(string $dir): void
{
if (is_dir($dir)) {
return;
}
if (false === mkdir($dir, 0777, true) && false === is_dir($dir)) {
throw new BuilderException(sprintf('Could not create directory "%s".', $dir));
}
}
/**
* Returns target cache dir for cache.
*
* @param string $cacheDir
*
* @return string
*/
private static function getCacheDirPath(string $cacheDir): string
{
if ('' !== $cacheDir && DIRECTORY_SEPARATOR !== $cacheDir[strlen($cacheDir) - 1]) {
$cacheDir .= DIRECTORY_SEPARATOR;
}
return $cacheDir . CacheDirectories::MAIN_DIR . DIRECTORY_SEPARATOR;
}
}

View file

@ -0,0 +1,148 @@
<?php
/**
* PHP version 7.3
*
* @category FormEncoderBuilder
* @package RetailCrm\Api\Builder
*/
namespace RetailCrm\Api\Builder;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\PsrCachedReader;
use Liip\Serializer\SerializerInterface;
use Psr\Cache\CacheItemPoolInterface;
use RetailCrm\Api\Component\FormData\FormEncoder;
use RetailCrm\Api\Factory\SerializerFactory;
use RetailCrm\Api\Interfaces\BuilderInterface;
use RetailCrm\Api\Interfaces\FormEncoderInterface;
/**
* Class FormEncoderBuilder
*
* @category FormEncoderBuilder
* @package RetailCrm\Api\Builder
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class FormEncoderBuilder implements BuilderInterface
{
/** @var CacheItemPoolInterface|null */
private $cache;
/** @var \Doctrine\Common\Annotations\Reader */
private $annotationReader;
/** @var \RetailCrm\Api\Builder\FilesystemCacheBuilder */
private $fsCacheBuilder;
/** @var \Liip\Serializer\SerializerInterface */
private $serializer;
/**
* FormEncoderBuilder constructor.
*/
public function __construct()
{
$this->fsCacheBuilder = new FilesystemCacheBuilder();
}
/**
* Sets cache directory.
*
* This directory will be used by FormEncoder component and underlying serializer to store annotations cache.
* Annotations parsing consumes a lot of resources, which is the reason why you should cache results.
*
* @param string $cacheDir
*
* @return FormEncoderBuilder
*/
public function setCacheDir(string $cacheDir): FormEncoderBuilder
{
$this->fsCacheBuilder->setCacheDir($cacheDir);
return $this;
}
/**
* Sets cache implementation.
*
* This cache implementation will be used by FormEncoder component and underlying serializer to store
* annotations cache. Any cache from `symfony/cache` should work just fine.
*
* @param \Psr\Cache\CacheItemPoolInterface $cache
*
* @return FormEncoderBuilder
*/
public function setCache(CacheItemPoolInterface $cache): FormEncoderBuilder
{
$this->cache = $cache;
return $this;
}
/**
* Sets serializer implementation.
*
* This serializer implementation will be used by FormEncoder component.
*
* @param \Liip\Serializer\SerializerInterface $serializer
*
* @return FormEncoderBuilder
*/
public function setSerializer(SerializerInterface $serializer): FormEncoderBuilder
{
$this->serializer = $serializer;
return $this;
}
/**
* Builds FormEncoder.
*
* **Note:** Cache won't be set into provided serializer instance. It only works for instance built by
* this builder. You must manually inject the proper cache into the custom JMS Serializer instance.
*
* @inheritDoc
*/
public function build(): FormEncoderInterface
{
$this->buildCache();
$this->buildAnnotationReader();
$this->buildSerializer();
return new FormEncoder($this->serializer, $this->annotationReader);
}
/**
* Builds cache if needed.
*
* @throws \RetailCrm\Api\Exception\Client\BuilderException
*/
private function buildCache(): void
{
if (null === $this->cache) {
$this->cache = $this->fsCacheBuilder->build();
}
}
/**
* Builds annotation reader.
*/
private function buildAnnotationReader(): void
{
$this->annotationReader = new AnnotationReader();
if (null !== $this->cache) {
$this->annotationReader = new PsrCachedReader(new AnnotationReader(), $this->cache);
}
}
/**
* Builds serializer.
*/
private function buildSerializer(): void
{
if (null === $this->serializer) {
$this->serializer = SerializerFactory::create();
}
}
}

403
src/Client.php Normal file
View file

@ -0,0 +1,403 @@
<?php
/**
* PHP version 7.3
*
* @category Client
* @package RetailCrm\Api
*/
namespace RetailCrm\Api;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
use RetailCrm\Api\Component\Utils;
use RetailCrm\Api\Interfaces\RequestTransformerInterface;
use RetailCrm\Api\Interfaces\ResponseTransformerInterface;
use RetailCrm\Api\ResourceGroup\Api;
use RetailCrm\Api\ResourceGroup\Costs;
use RetailCrm\Api\ResourceGroup\CustomerInteraction;
use RetailCrm\Api\ResourceGroup\Customers;
use RetailCrm\Api\ResourceGroup\CustomersCorporate;
use RetailCrm\Api\ResourceGroup\CustomFields;
use RetailCrm\Api\ResourceGroup\CustomMethods;
use RetailCrm\Api\ResourceGroup\Delivery;
use RetailCrm\Api\ResourceGroup\Features;
use RetailCrm\Api\ResourceGroup\Files;
use RetailCrm\Api\ResourceGroup\Integration;
use RetailCrm\Api\ResourceGroup\Inventories;
use RetailCrm\Api\ResourceGroup\Loyalty;
use RetailCrm\Api\ResourceGroup\Notifications;
use RetailCrm\Api\ResourceGroup\Orders;
use RetailCrm\Api\ResourceGroup\Packs;
use RetailCrm\Api\ResourceGroup\Payments;
use RetailCrm\Api\ResourceGroup\References;
use RetailCrm\Api\ResourceGroup\Segments;
use RetailCrm\Api\ResourceGroup\Settings;
use RetailCrm\Api\ResourceGroup\Statistics;
use RetailCrm\Api\ResourceGroup\Store;
use RetailCrm\Api\ResourceGroup\Tasks;
use RetailCrm\Api\ResourceGroup\Telephony;
use RetailCrm\Api\ResourceGroup\Users;
use RetailCrm\Api\ResourceGroup\Verification;
use RetailCrm\Api\ResourceGroup\WebAnalytics;
/**
* Class Client
*
* Do not instantiate API client directly! Use `ClientFactory`, `SimpleClientFactory` or `ClientBuilder`.
*
* @category Client
* @package RetailCrm\Api
* @see \RetailCrm\Api\Factory\ClientFactory
* @see \RetailCrm\Api\Factory\SimpleClientFactory
* @see \RetailCrm\Api\Builder\ClientBuilder
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Client
{
/** @var \RetailCrm\Api\ResourceGroup\Api */
public $api;
/** @var \RetailCrm\Api\ResourceGroup\Costs */
public $costs;
/** @var \RetailCrm\Api\ResourceGroup\CustomFields */
public $customFields;
/** @var \RetailCrm\Api\ResourceGroup\CustomerInteraction */
public $customerInteraction;
/** @var \RetailCrm\Api\ResourceGroup\Customers */
public $customers;
/** @var \RetailCrm\Api\ResourceGroup\CustomersCorporate */
public $customersCorporate;
/** @var \RetailCrm\Api\ResourceGroup\Delivery */
public $delivery;
/** @var \RetailCrm\Api\ResourceGroup\Features */
public $features;
/** @var \RetailCrm\Api\ResourceGroup\Files */
public $files;
/** @var \RetailCrm\Api\ResourceGroup\Integration */
public $integration;
/** @var \RetailCrm\Api\ResourceGroup\Loyalty */
public $loyalty;
/** @var \RetailCrm\Api\ResourceGroup\Notifications */
public $notifications;
/** @var \RetailCrm\Api\ResourceGroup\Orders */
public $orders;
/** @var \RetailCrm\Api\ResourceGroup\Packs */
public $packs;
/** @var \RetailCrm\Api\ResourceGroup\Payments */
public $payments;
/** @var \RetailCrm\Api\ResourceGroup\References */
public $references;
/** @var \RetailCrm\Api\ResourceGroup\Segments */
public $segments;
/** @var \RetailCrm\Api\ResourceGroup\Settings */
public $settings;
/** @var \RetailCrm\Api\ResourceGroup\Store */
public $store;
/** @var \RetailCrm\Api\ResourceGroup\Tasks */
public $tasks;
/** @var \RetailCrm\Api\ResourceGroup\Telephony */
public $telephony;
/** @var \RetailCrm\Api\ResourceGroup\Users */
public $users;
/** @var \RetailCrm\Api\ResourceGroup\Verification */
public $verification;
/** @var \RetailCrm\Api\ResourceGroup\Statistics */
public $statistics;
/** @var \RetailCrm\Api\ResourceGroup\CustomMethods */
public $customMethods;
/** @var \RetailCrm\Api\ResourceGroup\WebAnalytics */
public $webAnalytics;
/** @var StreamFactoryInterface */
private $streamFactory;
/**
* Client constructor.
*
* @param string $apiUrl
* @param \Psr\Http\Client\ClientInterface $httpClient
* @param \RetailCrm\Api\Interfaces\RequestTransformerInterface $requestTransformer
* @param \RetailCrm\Api\Interfaces\ResponseTransformerInterface $responseTransformer
* @param \Psr\Http\Message\StreamFactoryInterface $streamFactory
* @param \Psr\EventDispatcher\EventDispatcherInterface|null $eventDispatcher
* @param \Psr\Log\LoggerInterface|null $logger
*
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @todo Maybe initialize children groups using different method?
*/
public function __construct(
string $apiUrl,
ClientInterface $httpClient,
RequestTransformerInterface $requestTransformer,
ResponseTransformerInterface $responseTransformer,
StreamFactoryInterface $streamFactory,
?EventDispatcherInterface $eventDispatcher = null,
?LoggerInterface $logger = null
) {
$url = static::getBaseUrl($apiUrl);
$this->streamFactory = $streamFactory;
$this->api = new Api(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->costs = new Costs(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->customFields = new CustomFields(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->customerInteraction = new CustomerInteraction(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->customers = new Customers(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->customersCorporate = new CustomersCorporate(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->delivery = new Delivery(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->features = new Features(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->files = new Files(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->integration = new Integration(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->loyalty = new Loyalty(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->notifications = new Notifications(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->orders = new Orders(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->packs = new Packs(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->payments = new Payments(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->references = new References(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->segments = new Segments(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->settings = new Settings(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->store = new Store(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->tasks = new Tasks(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->telephony = new Telephony(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->users = new Users(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->verification = new Verification(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->statistics = new Statistics(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->customMethods = new CustomMethods(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->webAnalytics = new WebAnalytics(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
}
/**
* Returns PSR-17 stream factory.
*
* StreamFactory can be used to create a PSR-7 StreamInterface from various sources.
*
* @return \Psr\Http\Message\StreamFactoryInterface
*/
public function getStreamFactory(): StreamFactoryInterface
{
return $this->streamFactory;
}
/**
* Parses provided URL, builds API url with version out of it.
*
* @param string $url
*
* @return string
*/
private static function getBaseUrl(string $url): string
{
return Utils::removeTrailingSlash($url) . '/api/v5';
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* PHP version 7.3
*
* @category AbstractModelsProcessorCommand
* @package RetailCrm\Api\Command
*/
namespace RetailCrm\Api\Command;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class AbstractModelsProcessorCommand
*
* @category AbstractModelsProcessorCommand
* @package RetailCrm\Api\Command
* @internal
*/
abstract class AbstractModelsProcessorCommand extends Command
{
/**
* Returns true if "-v" was provided.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return bool
*/
protected static function isVerbose(OutputInterface $output): bool
{
return $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE;
}
/**
* Create directory
*
* @param string $dir
*
* @throws RuntimeException
*/
protected static function createDir(string $dir): void
{
if (is_dir($dir)) {
return;
}
if (false === mkdir($dir, 0777, true) && false === is_dir($dir)) {
throw new RuntimeException(sprintf('Could not create directory "%s".', $dir));
}
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* PHP version 7.3
*
* @category ClearModelsCommand
* @package RetailCrm\Api\Command
*/
namespace RetailCrm\Api\Command;
use RetailCrm\Api\Component\Utils;
use RetailCrm\Api\Component\PhpFilesIterator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class ClearModelsCommand
*
* @category ClearModelsCommand
* @package RetailCrm\Api\Command
* @internal
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
class ClearModelsCommand extends AbstractModelsProcessorCommand
{
/**
* Sets description and help for a command.
*/
protected function configure(): void
{
$this->setName('models:clear')
->setDescription('Removes all generated models, leaves only empty directory behind.')
->setHelp('Use this command if you want to remove existing model cache.');
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$target = Utils::getModelsCacheDirectory();
if (!is_dir($target)) {
$output->writeln('<error>The target directory does not exist.</error>');
$output->writeln('<info>You can safely use "bin/console models:generate" to generate models.</info>');
return -1;
}
$output->writeln(sprintf('Cleaning up <fg=magenta>"%s"</>...', $target));
if (self::isVerbose($output)) {
$output->writeln('');
}
$checksumFile = implode(DIRECTORY_SEPARATOR, [$target, 'checksum.json']);
$models = new PhpFilesIterator($target);
foreach ($models as $model) {
if (file_exists($model['file'])) {
unlink($model['file']);
if (self::isVerbose($output)) {
$output->writeln(sprintf('- Removed <fg=magenta>"%s"</>', $model['file']));
}
}
}
if (file_exists($checksumFile)) {
unlink($checksumFile);
}
if (self::isVerbose($output)) {
$output->writeln('');
}
$output->writeln('<fg=black;bg=green> ✓ Done!</>');
return 0;
}
}

View file

@ -0,0 +1,206 @@
<?php
/**
* PHP version 7.3
*
* @category CompilerPromptCommand
* @package RetailCrm\Api\Command
*/
namespace RetailCrm\Api\Command;
use JsonException;
use RetailCrm\Api\Component\ComposerLocator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class CompilerPromptCommand
*
* @category CompilerPromptCommand
* @package RetailCrm\Api\Command
*/
class CompilerPromptCommand extends Command
{
private const PACKAGE_NAME = 'retailcrm/api-client-php';
private const COMPILER_PLUGIN = 'civicrm/composer-compile-plugin';
/**
* Sets description and help for a command.
*/
protected function configure(): void
{
$this->setName('compiler:prompt')
->setDescription('Enable or disable code generation during client installation & update.')
->setHelp(
'Use this command to enable or disable automatic code generation.'
)->addOption(
'revert',
'r',
InputOption::VALUE_OPTIONAL,
'You will need to run ./vendor/bin/retailcrm-client models:generate -a after each update.',
false
);
}
/**
* Execute the command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return int
*
* @SuppressWarnings(PHPMD.ElseExpression)
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$composerJson = ComposerLocator::findComposerJson();
if ('' === $composerJson) {
$output->writeln('<fg=black;bg=red> ❌ Cannot find composer.json</>');
return -1;
}
try {
$json = json_decode((string) file_get_contents($composerJson), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
$output->writeln(sprintf('<fg=black;bg=red> ❌ Invalid JSON: %s</>', $exception->getMessage()));
return -1;
}
$revert = false !== $input->getOption('revert');
if ($revert) {
static::deactivateAutoCompiler($json);
} else {
static::activateAutoCompiler($json);
static::activatePlugin($json);
}
try {
$result = json_encode($json, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (JsonException $exception) {
$output->writeln(sprintf('<fg=black;bg=red> ❌ Cannot encode JSON: %s</>', $exception->getMessage()));
return -1;
}
if (false === file_put_contents($composerJson, $result)) {
$output->writeln('<fg=black;bg=red> ❌ Cannot write to file.</>');
return -1;
}
$output->writeln(sprintf(
'<fg=black;bg=green> ✓ Done, code generation has been %s.</>',
$revert ? 'disabled' : 'enabled'
));
return 0;
}
/**
* Activate plugin in the provided composer.json
*
* @param array<string, array<string, array<string>|string>> $composerJson
*/
private static function activatePlugin(array &$composerJson): void
{
if (!array_key_exists('config', $composerJson)) {
$composerJson['config'] = [
'allow-plugins' => [
static::COMPILER_PLUGIN => true
]
];
return;
}
if (!array_key_exists('allow-plugins', $composerJson['config'])) {
$composerJson['config']['allow-plugins'] = [
static::COMPILER_PLUGIN => true
];
return;
}
$composerJson['config']['allow-plugins'][static::COMPILER_PLUGIN] = true;
}
/**
* Activate auto compiler in the provided composer.json
*
* @param array<string, array<string, array<string>|string>> $composerJson
*/
private static function activateAutoCompiler(array &$composerJson): void
{
if (!array_key_exists('extra', $composerJson)) {
$composerJson['extra'] = [
'compile-mode' => 'whitelist',
'compile-whitelist' => [self::PACKAGE_NAME]
];
return;
}
if (array_key_exists('compile-mode', $composerJson['extra'])) {
if (
'prompt' === $composerJson['extra']['compile-mode'] ||
'none' === $composerJson['extra']['compile-mode']
) {
$composerJson['extra']['compile-mode'] = 'whitelist';
}
if ('all' === $composerJson['extra']['compile-mode']) {
return;
}
}
$composerJson['extra']['compile-mode'] = 'whitelist';
if (!array_key_exists('compile-whitelist', $composerJson['extra'])) {
$composerJson['extra']['compile-whitelist'] = [self::PACKAGE_NAME];
return;
}
if (!in_array(self::PACKAGE_NAME, $composerJson['extra']['compile-whitelist'])) {
$composerJson['extra']['compile-whitelist'][] = self::PACKAGE_NAME;
}
}
/**
* Deactivate prompt in the provided composer.json
*
* @param array<string, array<string>> $composerJson
*
* @SuppressWarnings(PHPMD.ElseExpression)
*/
private static function deactivateAutoCompiler(array &$composerJson): void
{
if (!array_key_exists('extra', $composerJson)) {
return;
}
if (
array_key_exists('compile-whitelist', $composerJson['extra']) &&
null !== $composerJson['extra']['compile-whitelist']
) {
$composerJson['extra']['compile-whitelist'] = array_filter(
$composerJson['extra']['compile-whitelist'],
static function (string $item) {
return $item !== self::PACKAGE_NAME;
}
);
if (0 === count($composerJson['extra']['compile-whitelist'])) {
unset($composerJson['extra']['compile-whitelist']);
}
}
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* PHP version 7.3
*
* @category GenerateModelsCommand
* @package RetailCrm\Api\Command
*/
namespace RetailCrm\Api\Command;
use RetailCrm\Api\Component\ModelsGenerator;
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 GenerateModelsCommand
*
* @category GenerateModelsCommand
* @package RetailCrm\Api\Command
* @internal
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*
* @see There is no need to refactor generator into separate service. Its logic won't be used anywhere else.
*/
class GenerateModelsCommand extends AbstractModelsProcessorCommand
{
/**
* Sets description and help for a command.
*/
protected function configure(): void
{
$this->setName('models:generate')
->setDescription('Converts all JMS models to static (de)serialization code.')
->setHelp('Use this command after making any changes to the models.')
->addOption(
'all',
'a',
InputOption::VALUE_OPTIONAL,
'Generate cache for all models instead of changed models only.',
false
);
}
/**
* Execute the command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$verbose = static::isVerbose($output);
$generator = new ModelsGenerator(false !== $input->getOption('all'));
$output->writeln('Preparing a list of models to generate cache files...');
$output->writeln(
'<options=bold>Note:</> Request models will be omitted ' .
'because they are being handled by FormEncoder.'
);
$output->writeln('');
$generator->loadModelsList();
if ($verbose) {
foreach ($generator->getModels() as $model) {
$output->writeln(sprintf('- Added <fg=magenta>%s</>', $model));
}
}
if ($verbose && count($generator->getModels()) > 0) {
$output->writeln('');
}
if (count($generator->getModels()) === 0) {
$output->writeln('<info>No changes were found; skipping generation...</info>');
$output->writeln('');
}
try {
$generator->generate();
} catch (\Throwable $throwable) {
$styled = new SymfonyStyle($input, $output);
$styled->error($throwable->getMessage());
$styled->writeln($throwable->getTraceAsString());
return -1;
}
$output->writeln(sprintf(
'<fg=black;bg=green> ✓ Done, generated code for %d models.</>',
count($generator->getModels())
));
return 0;
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* PHP version 7.3
*
* @category VerifyModelsCommand
* @package RetailCrm\Api\Command
*/
namespace RetailCrm\Api\Command;
use RetailCrm\Api\Component\Serializer\ModelsChecksumGenerator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Class VerifyModelsCommand
*
* @category VerifyModelsCommand
* @package RetailCrm\Api\Command
* @internal
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
class VerifyModelsCommand extends AbstractModelsProcessorCommand
{
/**
* Sets description and help for a command.
*/
protected function configure(): void
{
$this->setName('models:verify')
->setDescription('Verify models cache. This command will fail if model cache is not up-to-date.')
->setHelp('Use this command if you want to check existing model cache.');
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return int
* @throws \JsonException
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
if (ModelsChecksumGenerator::verifyChecksum()) {
$io->success("Models are up to date.");
return 0;
}
$io->error("Outdated models! Run \"models:generate\" command to fix that.");
return -1;
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* PHP version 7.3
*
* @category ComposerLocator
* @package RetailCrm\Api\Component
*/
namespace RetailCrm\Api\Component;
/**
* Class ComposerLocator
*
* @category ComposerLocator
* @package RetailCrm\Api\Component
*/
class ComposerLocator
{
/**
* Locate Composer autoloader.
*
* @return string
*/
public static function findAutoloader(): string
{
$counter = 0;
$dir = static::getBaseDirectory();
for (;;) {
if (file_exists($dir . '/autoload.php')) {
return $dir . '/autoload.php';
}
if (file_exists($dir . '/vendor/autoload.php')) {
return $dir . '/vendor/autoload.php';
}
$counter++;
$dir = dirname($dir);
if (5 < $counter) {
break;
}
}
return '';
}
/**
* Locate `composer.json`.
*
* @return string
*/
public static function findComposerJson(): string
{
$counter = 0;
$dir = static::getBaseDirectory();
for (;;) {
$fileName = implode(DIRECTORY_SEPARATOR, [$dir, 'composer.json']);
if (file_exists($fileName) && static::getPackageComposerJson() !== $fileName) {
return $fileName;
}
$counter++;
$dir = dirname($dir);
if (2 < $counter) {
break;
}
}
return '';
}
/**
* @return string
*/
private static function getBaseDirectory(): string
{
$cwd = getcwd();
return false === $cwd ? (string) realpath(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..'])) : $cwd;
}
/**
* Returns full path to the composer.json of this package.
*
* @return string
*/
private static function getPackageComposerJson(): string
{
return (string) realpath(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'composer.json']));
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* PHP version 7.3
*
* @category CustomApiMethod
* @package RetailCrm\Api\Component
*/
namespace RetailCrm\Api\Component;
use RetailCrm\Api\Exception\Client\HandlerException;
use RetailCrm\Api\Interfaces\RequestSenderInterface;
/**
* Class CustomApiMethod
*
* This class can be used to implement custom methods without any hassle. It is useful if you don't want to
* do anything besides sending the request and reading the response.
*
* @see \RetailCrm\Api\ResourceGroup\CustomMethods::register() for the usage example.
*
* @category CustomApiMethod
* @package RetailCrm\Api\Component
*/
class CustomApiMethod
{
/** @var string */
protected $method;
/** @var string */
protected $route;
/** @var bool */
protected $rawRouteUri;
/**
* Instantiates new instance of the CustomApiMethod.
*
* @param string $method
* @param string $route
*/
public function __construct(string $method, string $route)
{
$this->method = $method;
$this->route = $route;
}
/**
* Use provided route as if it was full URL.
*
* @return $this
*/
public function useRouteAsUri(): self
{
$this->rawRouteUri = true;
return $this;
}
/**
* Sends the request, returns the response.
*
* @param \RetailCrm\Api\Interfaces\RequestSenderInterface $sender
* @param array<int|string, mixed>|object $data
*
* @return array<int|string, mixed>|mixed
* @throws \RetailCrm\Api\Exception\ApiException
* @throws \RetailCrm\Api\Exception\ClientException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
*/
public function __invoke(RequestSenderInterface $sender, $data = [])
{
if (!is_array($data)) {
throw new HandlerException(__CLASS__ . ' only supports array data');
}
return $sender->send($this->method, $this->rawRouteUri ? $this->route : $sender->route($this->route), $data);
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* PHP version 7.3
*
* @category FilesIteratorChecksumGenerator
* @package RetailCrm\Api\Component
*/
namespace RetailCrm\Api\Component;
use Iterator;
/**
* Class FilesIteratorChecksumGenerator
*
* @category FilesIteratorChecksumGenerator
* @package RetailCrm\Api\Component
* @internal
*/
class FilesIteratorChecksumGenerator
{
/** @var Iterator<mixed> */
private $iterator;
/** @var callable */
private $fileNameAccessor;
/** @var callable */
private $keyTransformer;
/** @var callable */
private $hashFunc;
/**
* FilesIteratorChecksumGenerator constructor.
*
* @param Iterator<mixed> $iterator
*/
public function __construct(Iterator $iterator)
{
$this->iterator = $iterator;
}
/**
* @return callable
*/
public function getFileNameAccessor(): callable
{
return $this->fileNameAccessor ?? static function ($fileName) {
return (string) $fileName;
};
}
/**
* @param callable $fileNameAccessor
*
* @return FilesIteratorChecksumGenerator
*/
public function setFileNameAccessor(callable $fileNameAccessor): FilesIteratorChecksumGenerator
{
$this->fileNameAccessor = $fileNameAccessor;
return $this;
}
/**
* @return callable
*/
public function getKeyTransformer(): callable
{
return $this->keyTransformer ?? static function ($fileName) {
return (string) $fileName;
};
}
/**
* This callable will be used to provide key for the hashes array.
*
* @param callable $keyTransformer
*
* @return FilesIteratorChecksumGenerator
*/
public function setKeyTransformer(callable $keyTransformer): FilesIteratorChecksumGenerator
{
$this->keyTransformer = $keyTransformer;
return $this;
}
/**
* @return callable
*/
public function getHashFunc(): callable
{
return $this->hashFunc ?? static function ($fileName) {
$contents = preg_replace('/\r|\n|\r\n/m', "\n", (string) file_get_contents($fileName));
return md5($contents ?? '');
};
}
/**
* This function will receive file name and should return file hash.
*
* @param callable $hashFunc
*
* @return FilesIteratorChecksumGenerator
*/
public function setHashFunc(callable $hashFunc): FilesIteratorChecksumGenerator
{
$this->hashFunc = $hashFunc;
return $this;
}
/**
* Generate hash string from the contents of a directory.
*
* @return array<string, string>
*/
public function generateHashes(): array
{
$hashes = [];
foreach ($this->iterator as $item) {
$fileName = $this->getFileNameAccessor()($item);
$hashes[(string) $this->getKeyTransformer()($item)] = $this->getHashFunc()($fileName);
}
return $hashes;
}
}

View file

@ -0,0 +1,131 @@
<?php
/**
* PHP version 7.3
*
* @category FormEncoder
* @package RetailCrm\Api\Component\FormData
*/
namespace RetailCrm\Api\Component\FormData;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\Reader;
use Liip\Serializer\SerializerInterface;
use ReflectionClass;
use ReflectionException;
use RetailCrm\Api\Component\FormData\Mapping\PostSerialize;
use RetailCrm\Api\Component\FormData\Strategy\StrategyFactory;
use RetailCrm\Api\Interfaces\FormEncoderInterface;
/**
* Class FormEncoder
*
* FormEncoder is a vital part of the library. Our API expects form-data for all requests, but some fields may contain
* JSON data (for example, `/api/v5/customers/create` method works like that). FormEncoder is our custom serializer that
* converts request instances to form-data and uses Liip serializer under the hood to fill some fields with JSON data.
*
* @see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-customers-create
*
* @category FormEncoder
* @package RetailCrm\Api\Component\FormData
*/
class FormEncoder implements FormEncoderInterface
{
/** @var \Doctrine\Common\Annotations\Reader */
private $annotationReader;
/** @var \Liip\Serializer\SerializerInterface */
private $serializer;
/**
* FormEncoder constructor.
*
* @param \Liip\Serializer\SerializerInterface $serializer
* @param \Doctrine\Common\Annotations\Reader|null $annotationReader
*/
public function __construct(SerializerInterface $serializer, ?Reader $annotationReader = null)
{
$this->serializer = $serializer;
$this->annotationReader = $annotationReader ?: new AnnotationReader();
}
/**
* Encodes provided object into a form data
*
* @param mixed $object
* @param string $type
*
* @return string
* @throws \ReflectionException
*/
public function encode($object, string $type = ''): string
{
return http_build_query($this->encodeArray($object, $type));
}
/**
* Encodes provided object into an array
*
* @param mixed $object
* @param string $type
*
* @return array<mixed>
* @throws \ReflectionException
*/
public function encodeArray($object, string $type = ''): array
{
$type = empty($type) ? gettype($object) : $type;
$result = (array) StrategyFactory::encodeStrategyByType(
$type,
$object,
$this->annotationReader,
$this->serializer
)->encode($object, null);
return $this->processPostSerialize($object, $result);
}
/**
* Returns underlying serializer instance.
*
* @return \Liip\Serializer\SerializerInterface
*/
public function getSerializer(): SerializerInterface
{
return $this->serializer;
}
/**
* Process post deserialize callback
*
* @param mixed $object
* @param mixed[] $result
*
* @return mixed[]
* @throws ReflectionException
*/
private function processPostSerialize($object, array $result): array
{
$class = get_class($object);
if (false !== $object) {
try {
$reflection = new ReflectionClass($class);
} catch (ReflectionException $e) {
return $result;
}
foreach ($reflection->getMethods() as $method) {
$postDeserialize = $this->annotationReader
->getMethodAnnotation($method, PostSerialize::class);
if ($postDeserialize instanceof PostSerialize) {
return $method->invokeArgs($object, [$result]);
}
}
}
return $result;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.3
*
* @category Accessor
* @package RetailCrm\Api\Component\FormData\Mapping
*/
namespace RetailCrm\Api\Component\FormData\Mapping;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use Doctrine\Common\Annotations\Annotation\Attribute;
use Doctrine\Common\Annotations\Annotation\Attributes;
/**
* Class Accessor
*
* @category Accessor
* @package RetailCrm\Api\Component\FormData\Mapping
*
* @Annotation
* @Attributes(
* @Attribute("getter", required=false, type="string"),
* @Attribute("setter", required=false, type="string")
* )
* @Target({"PROPERTY","ANNOTATION"})
*/
final class Accessor
{
/**
* Property getter
*
* @var string
*/
public $getter;
/**
* Property setter
*
* @var string
*/
public $setter;
}

View file

@ -0,0 +1,26 @@
<?php
/**
* PHP version 7.3
*
* @category JsonField
* @package RetailCrm\Api\Component\FormData\Mapping
*/
namespace RetailCrm\Api\Component\FormData\Mapping;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class JsonField
*
* @category JsonField
* @package RetailCrm\Api\Component\FormData\Mapping
*
* @Annotation
* @Target({"PROPERTY","ANNOTATION"})
*/
final class JsonField
{
}

View file

@ -0,0 +1,26 @@
<?php
/**
* PHP version 7.3
*
* @category NoTransform
* @package RetailCrm\Api\Component\FormData\Mapping
*/
namespace RetailCrm\Api\Component\FormData\Mapping;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class NoTransform
*
* @category NoTransform
* @package RetailCrm\Api\Component\FormData\Mapping
*
* @Annotation
* @Target({"PROPERTY","ANNOTATION"})
*/
final class NoTransform
{
}

View file

@ -0,0 +1,26 @@
<?php
/**
* PHP version 7.3
*
* @category PostDeserialize
* @package RetailCrm\Api\Component\FormData\Mapping
*/
namespace RetailCrm\Api\Component\FormData\Mapping;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class PostDeserialize
*
* @category PostDeserialize
* @package RetailCrm\Api\Component\FormData\Mapping
*
* @Annotation
* @Target({"METHOD"})
*/
final class PostDeserialize
{
}

View file

@ -0,0 +1,26 @@
<?php
/**
* PHP version 7.3
*
* @category PostSerialize
* @package RetailCrm\Api\Component\FormData\Mapping
*/
namespace RetailCrm\Api\Component\FormData\Mapping;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class PostSerialize
*
* @category PostSerialize
* @package RetailCrm\Api\Component\FormData\Mapping
*
* @Annotation
* @Target({"METHOD"})
*/
final class PostSerialize
{
}

View file

@ -0,0 +1,37 @@
<?php
/**
* PHP version 7.3
*
* @category SerializedName
* @package RetailCrm\Api\Component\FormData\Mapping
*/
namespace RetailCrm\Api\Component\FormData\Mapping;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use Doctrine\Common\Annotations\Annotation\Attribute;
use Doctrine\Common\Annotations\Annotation\Attributes;
/**
* Class SerializedName
*
* @category SerializedName
* @package RetailCrm\Api\Component\FormData\Mapping
*
* @Annotation
* @Attributes(
* @Attribute("name", required=true, type="string")
* )
* @Target({"PROPERTY","ANNOTATION"})
*/
final class SerializedName
{
/**
* Property name in result JSON
*
* @var string
*/
public $name;
}

View file

@ -0,0 +1,37 @@
<?php
/**
* PHP version 7.3
*
* @category Type
* @package RetailCrm\Api\Component\FormData\Mapping
*/
namespace RetailCrm\Api\Component\FormData\Mapping;
use Doctrine\Common\Annotations\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use Doctrine\Common\Annotations\Annotation\Attribute;
use Doctrine\Common\Annotations\Annotation\Attributes;
/**
* Class Type
*
* @category Type
* @package RetailCrm\Api\Component\FormData\Mapping
*
* @Annotation
* @Attributes(
* @Attribute("type", required=false, type="string")
* )
* @Target({"PROPERTY","ANNOTATION"})
*/
final class Type
{
/**
* Property type
*
* @var string
*/
public $type;
}

View file

@ -0,0 +1,73 @@
<?php
/**
* PHP version 7.3
*
* @category PropertyAnnotations
* @package RetailCrm\Api\Component\FormData
*/
namespace RetailCrm\Api\Component\FormData;
use RetailCrm\Api\Component\FormData\Mapping\Accessor;
use RetailCrm\Api\Component\FormData\Mapping\JsonField;
use RetailCrm\Api\Component\FormData\Mapping\SerializedName;
use RetailCrm\Api\Component\FormData\Mapping\Type;
/**
* Class PropertyAnnotations
*
* @category PropertyAnnotations
* @package RetailCrm\Api\Component\FormData
* @author RetailDriver LLC <integration@retailcrm.ru>
* @license https://retailcrm.ru Proprietary
* @link http://retailcrm.ru
* @see https://help.retailcrm.ru
*/
class PropertyAnnotations
{
/**
* @var SerializedName|null
*/
public $serializedName;
/**
* @var Accessor|null
*/
public $accessor;
/**
* @var Type|null
*/
public $type;
/**
* @var JsonField|null
*/
public $jsonField;
/**
* PropertyAnnotations constructor.
*
* @param object[] $annotations
*/
public function __construct(array $annotations = [])
{
foreach ($annotations as $annotation) {
switch (get_class($annotation)) {
case Type::class:
$this->type = $annotation;
break;
case SerializedName::class:
$this->serializedName = $annotation;
break;
case Accessor::class:
$this->accessor = $annotation;
break;
case JsonField::class:
$this->jsonField = $annotation;
break;
}
}
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* PHP version 7.3
*
* @category AbstractEncodeStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
namespace RetailCrm\Api\Component\FormData\Strategy\Encode;
use Doctrine\Common\Annotations\Reader;
use Liip\Serializer\SerializerInterface;
/**
* Class AbstractEncodeStrategy
*
* @category AbstractEncodeStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
abstract class AbstractEncodeStrategy implements EncodeStrategyInterface
{
/** @var \Doctrine\Common\Annotations\Reader */
protected $annotationReader;
/** @var string $innerType */
protected $innerType;
/** @var \Liip\Serializer\SerializerInterface */
protected $liipSerializer;
/**
* AbstractEncodeStrategy constructor.
*
* @param \Doctrine\Common\Annotations\Reader $annotationReader
* @param \Liip\Serializer\SerializerInterface $liipSerializer
*/
public function __construct(Reader $annotationReader, SerializerInterface $liipSerializer)
{
$this->annotationReader = $annotationReader;
$this->liipSerializer = $liipSerializer;
}
/**
* Sets inner type for types like array<key, value> and \DateTime<format>
*
* @param string $type
*
* @return \RetailCrm\Api\Component\FormData\Strategy\Encode\EncodeStrategyInterface
*/
public function setInnerType(string $type): EncodeStrategyInterface
{
$this->innerType = $type;
return $this;
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* PHP version 7.3
*
* @category DateTimeStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
namespace RetailCrm\Api\Component\FormData\Strategy\Encode;
use DateTime;
use RetailCrm\Api\Component\FormData\PropertyAnnotations;
/**
* Class DateTimeStrategy
*
* @category DateTimeStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
class DateTimeStrategy extends AbstractEncodeStrategy
{
/**
* @inheritDoc
*/
public function encode($value, ?PropertyAnnotations $annotations): ?string
{
if ($value instanceof DateTime) {
return $value->format($this->innerType);
}
return null;
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* PHP version 7.3
*
* @category Integration
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
* @author RetailCRM <integration@retailcrm.ru>
* @license MIT
* @link http://retailcrm.ru
* @see http://retailcrm.ru/docs
*/
namespace RetailCrm\Api\Component\FormData\Strategy\Encode;
use RetailCrm\Api\Component\FormData\PropertyAnnotations;
/**
* Interface EncodeStrategyInterface
*
* @internal
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
interface EncodeStrategyInterface
{
/**
* Serialize value
*
* @param mixed $value
* @param \RetailCrm\Api\Component\FormData\PropertyAnnotations|null $annotations
*
* @return mixed
*/
public function encode($value, ?PropertyAnnotations $annotations);
/**
* Sets inner type for types like array<key, value> and \DateTime<format>
*
* @param string $type
*
* @return \RetailCrm\Api\Component\FormData\Strategy\Encode\EncodeStrategyInterface
*/
public function setInnerType(string $type): EncodeStrategyInterface;
}

View file

@ -0,0 +1,117 @@
<?php
/**
* PHP version 7.3
*
* @category EntityStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
namespace RetailCrm\Api\Component\FormData\Strategy\Encode;
use Doctrine\Common\Annotations\AnnotationReader;
use JMS\Serializer\SerializerInterface;
use ReflectionClass;
use RetailCrm\Api\Component\FormData\Mapping\JsonField;
use RetailCrm\Api\Component\FormData\Mapping\NoTransform;
use RetailCrm\Api\Component\FormData\PropertyAnnotations;
use RetailCrm\Api\Component\FormData\Mapping\Accessor;
use RetailCrm\Api\Component\FormData\Mapping\SerializedName;
use RetailCrm\Api\Component\FormData\Mapping\Type;
use RetailCrm\Api\Component\FormData\Strategy\StrategyFactory;
/**
* Class EntityStrategy
*
* @category EntityStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
class EntityStrategy extends AbstractEncodeStrategy
{
/**
* @inheritDoc
*
* @return mixed[]|string|null
* @throws \ReflectionException
*/
public function encode($value, ?PropertyAnnotations $annotations = null)
{
if (empty($value)) {
return null;
}
if (null !== $annotations && $annotations->jsonField instanceof JsonField) {
return $this->liipSerializer->serialize($value, 'json');
}
$result = [];
$reflection = new ReflectionClass(get_class($value));
if (!$reflection->isUserDefined()) {
return (array) $value;
}
foreach ($reflection->getProperties() as $property) {
$this->encodeProperty($value, $property, $result);
}
return $result;
}
/**
* @param mixed $object
* @param \ReflectionProperty $property
* @param mixed[] $result
*
* @SuppressWarnings(PHPMD.ElseExpression)
*/
protected function encodeProperty($object, \ReflectionProperty $property, array &$result): void
{
$annotations = new PropertyAnnotations($this->annotationReader->getPropertyAnnotations($property));
if (!($annotations->serializedName instanceof SerializedName)) {
return;
}
if ($annotations->accessor instanceof Accessor && !empty($annotations->accessor->getter)) {
$value = $object->{$annotations->accessor->getter}();
} else {
$property->setAccessible(true);
$value = $property->getValue($object);
}
if ($this->isNoTransform($property)) {
$result[$annotations->serializedName->name] = $value;
} elseif ($annotations->type instanceof Type) {
$result[$annotations->serializedName->name] =
StrategyFactory::encodeStrategyByType(
$annotations->type->type,
$value,
$this->annotationReader,
$this->liipSerializer
)->encode($value, $annotations);
} else {
$result[$annotations->serializedName->name] =
StrategyFactory::encodeStrategyByType(
gettype($value),
$value,
$this->annotationReader,
$this->liipSerializer
)->encode($value, $annotations);
}
}
/**
* Returns true if NoTransform annotation was used
*
* @param \ReflectionProperty $property
*
* @return bool
*/
protected function isNoTransform(\ReflectionProperty $property): bool
{
$isNoTransform = $this->annotationReader->getPropertyAnnotation($property, NoTransform::class);
return $isNoTransform instanceof NoTransform;
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* PHP version 7.3
*
* @category SimpleTypeStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
namespace RetailCrm\Api\Component\FormData\Strategy\Encode;
use RetailCrm\Api\Component\FormData\Mapping\JsonField;
use RetailCrm\Api\Component\FormData\PropertyAnnotations;
use RetailCrm\Api\Component\FormData\Strategy\StrategyFactory;
/**
* Class SimpleTypeStrategy
*
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
class SimpleTypeStrategy extends AbstractEncodeStrategy
{
/**
* @inheritDoc
*/
public function encode($value, ?PropertyAnnotations $annotations)
{
if (null !== $annotations && $annotations->jsonField instanceof JsonField && !empty($value)) {
return $this->liipSerializer->serialize($value, 'json');
}
return $this->encodeValue($value);
}
/**
* Encode simple value.
*
* @param mixed $value
*
* @return bool|float|int|mixed[]|string|null
*/
private function encodeValue($value)
{
switch (gettype($value)) {
case 'bool':
case 'boolean':
return (bool) $value;
case 'int':
case 'integer':
return (int) $value;
case 'float':
return (float) $value;
case 'double':
return (double) $value;
case 'string':
return (string) $value;
default:
return $this->encodeDefault($value);
}
}
/**
* @param mixed $value
*
* @return mixed[]|null
*/
private function encodeDefault($value): ?array
{
if (is_iterable($value)) {
$result = [];
foreach ($value as $key => $item) {
$result[$key] = StrategyFactory::encodeStrategyByType(
gettype($item),
$item,
$this->annotationReader,
$this->liipSerializer
)->encode($item, new PropertyAnnotations());
}
return $result;
}
return null;
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* PHP version 7.3
*
* @category StreamInterfaceStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
namespace RetailCrm\Api\Component\FormData\Strategy\Encode;
use Psr\Http\Message\StreamInterface;
use RetailCrm\Api\Component\FormData\PropertyAnnotations;
/**
* Class StreamInterfaceStrategy
*
* @category StreamInterfaceStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
class StreamInterfaceStrategy extends AbstractEncodeStrategy
{
/**
* @inheritDoc
*/
public function encode($value, ?PropertyAnnotations $annotations): ?string
{
if ($value instanceof StreamInterface) {
if ($value->isSeekable()) {
$value->seek(0);
}
return $value->getContents();
}
return null;
}
}

View file

@ -0,0 +1,121 @@
<?php
/**
* PHP version 7.3
*
* @category TypedArrayStrategy
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*/
namespace RetailCrm\Api\Component\FormData\Strategy\Encode;
use Doctrine\Common\Annotations\Reader;
use Liip\Serializer\SerializerInterface;
use RetailCrm\Api\Component\FormData\Mapping\JsonField;
use RetailCrm\Api\Component\FormData\PropertyAnnotations;
use RetailCrm\Api\Component\FormData\Strategy\StrategyFactory;
/**
* Class TypedArrayStrategy
*
* @package RetailCrm\Api\Component\FormData\Strategy\Encode
*
* @SuppressWarnings(PHPMD.ElseExpression)
*/
class TypedArrayStrategy extends AbstractEncodeStrategy
{
/** @var string */
private static $innerTypesMatcher = '/^([a-z]+)\s*\,?\s*(.+?\>)/m';
/** @var \RetailCrm\Api\Component\FormData\Strategy\Encode\SimpleTypeStrategy */
private $simpleStrategy;
/**
* TypedArrayStrategy constructor.
*
* @param \Doctrine\Common\Annotations\Reader $annotationReader
* @param \Liip\Serializer\SerializerInterface $liipSerializer
*/
public function __construct(Reader $annotationReader, SerializerInterface $liipSerializer)
{
parent::__construct($annotationReader, $liipSerializer);
$this->simpleStrategy = new SimpleTypeStrategy($this->annotationReader, $this->liipSerializer);
}
/**
* @param mixed $value
* @param \RetailCrm\Api\Component\FormData\PropertyAnnotations|null $annotations
*
* @return mixed[]|mixed
*/
public function encode($value, ?PropertyAnnotations $annotations = null)
{
if (!is_array($value)) {
return $value;
}
if (strpos($this->innerType, ',') !== false) {
[$keyType, $valueType] = static::getInnerTypes($this->innerType);
if ('' === $keyType && '' === $valueType) {
$valueType = $this->innerType;
}
} else {
$valueType = $this->innerType;
}
if (null !== $annotations && $annotations->jsonField instanceof JsonField && !empty($value)) {
return $this->liipSerializer->serialize($value, 'json');
}
return $this->encodeRegularArray($value, $valueType);
}
/**
* Encode regular typed array.
*
* @param array<string|int, mixed> $value
* @param string $valueType
*
* @return array<string|int, mixed>
*/
private function encodeRegularArray(array $value, string $valueType): array
{
$result = [];
foreach (array_keys($value) as $key) {
$result[$this->simpleStrategy->encode($key, new PropertyAnnotations())]
= StrategyFactory::encodeStrategyByType(
$valueType,
$value[$key],
$this->annotationReader,
$this->liipSerializer
)->encode($value[$key], new PropertyAnnotations());
}
return $result;
}
/**
* Returns inner types for array with typed key (example: array<string, DateTime<Y m d H i s>>).
*
* @param string $innerType
*
* @return string[]
*/
private static function getInnerTypes(string $innerType): array
{
$matches = [];
preg_match_all(static::$innerTypesMatcher, $innerType, $matches, PREG_SET_ORDER, 0);
if (empty($matches)) {
return ['', ''];
}
$matches = $matches[0];
return [trim($matches[1]), trim($matches[2])];
}
}

View file

@ -0,0 +1,159 @@
<?php
/**
* PHP version 7.3
*
* @category StrategyFactory
* @package RetailCrm\Api\Component\FormData\Strategy
*/
namespace RetailCrm\Api\Component\FormData\Strategy;
use DateTime;
use Doctrine\Common\Annotations\Reader;
use Liip\Serializer\SerializerInterface;
use Psr\Http\Message\StreamInterface;
use RetailCrm\Api\Component\FormData\Strategy\Encode;
use RetailCrm\Api\Component\FormData\Strategy\Encode\EncodeStrategyInterface;
/**
* Class StrategyFactory
*
* @category StrategyFactory
* @package RetailCrm\Api\Component\FormData\Strategy
*/
class StrategyFactory
{
/** @var string */
private const TYPED_MATCHER = '/^\\\\?([a-zA-Z0-9_]+)\s*\<(.+)\>$/m';
/** @var string[] $simpleTypes */
private static $simpleTypes = [
'bool',
'boolean',
'int',
'integer',
'float',
'double',
'string',
'array'
];
/**
* Returns encode strategy for provided type
*
* @param string $dataType
* @param mixed $value
* @param \Doctrine\Common\Annotations\Reader $annotationReader
* @param \Liip\Serializer\SerializerInterface $serializer
*
* @return \RetailCrm\Api\Component\FormData\Strategy\Encode\EncodeStrategyInterface
*/
public static function encodeStrategyByType(
string $dataType,
$value,
Reader $annotationReader,
SerializerInterface $serializer
): EncodeStrategyInterface {
if (in_array($dataType, static::$simpleTypes)) {
return new Encode\SimpleTypeStrategy($annotationReader, $serializer);
}
if (static::isDateTime($dataType)) {
return (new Encode\DateTimeStrategy($annotationReader, $serializer))->setInnerType(DateTime::RFC3339);
}
$arrSubType = static::getArrayInnerTypes($dataType);
if (!empty($arrSubType)) {
return (new Encode\TypedArrayStrategy($annotationReader, $serializer))->setInnerType($arrSubType);
}
$dateTimeFormat = static::getDateTimeFormat($dataType);
if (!empty($dateTimeFormat)) {
return (new Encode\DateTimeStrategy($annotationReader, $serializer))->setInnerType($dateTimeFormat);
}
if ($value instanceof StreamInterface) {
return new Encode\StreamInterfaceStrategy($annotationReader, $serializer);
}
return new Encode\EntityStrategy($annotationReader, $serializer);
}
/**
* Returns true if provided type is DateTime
*
* @param string $dataType
*
* @return bool
*/
private static function isDateTime(string $dataType): bool
{
return strlen($dataType) > 1
&& (DateTime::class === $dataType
|| ('\\' === $dataType[0] && DateTime::class === substr($dataType, 1)));
}
/**
* Returns array inner type for arrays like array<int, \DateTime<Y-m-d\TH:i:sP>>
* For this example, "int, \DateTime<Y-m-d\TH:i:sP>" will be returned.
*
* Also works for arrays like int[].
*
* @param string $dataType
*
* @return string
*/
private static function getArrayInnerTypes(string $dataType): string
{
$matches = [];
preg_match_all(static::TYPED_MATCHER, $dataType, $matches, PREG_SET_ORDER, 0);
if (empty($matches)) {
if (strlen($dataType) > 2 && substr($dataType, -2) === '[]') {
return substr($dataType, 0, -2);
}
return '';
}
if ($matches[0][1] === 'array') {
return $matches[0][2];
}
return '';
}
/**
* Returns DateTime format. Example: \DateTime<Y-m-d\TH:i:sP>>
*
* @param string $dataType
*
* @return string
*/
private static function getDateTimeFormat(string $dataType): string
{
$matches = [];
preg_match_all(static::TYPED_MATCHER, $dataType, $matches, PREG_SET_ORDER, 0);
if (empty($matches)) {
return '';
}
if ($matches[0][1] === 'DateTime') {
$format = $matches[0][2];
if (strlen($format) > 2 && $format[0] === "'" && substr($format, -1) === "'") {
return substr($format, 1, -1);
}
return $format;
}
return '';
}
}

View file

@ -0,0 +1,236 @@
<?php
/**
* PHP version 7.3
*
* @category ModelsGenerator
* @package RetailCrm\Api\Component
*/
namespace RetailCrm\Api\Component;
use Doctrine\Common\Annotations\AnnotationReader;
use Liip\MetadataParser\Builder;
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection;
use Liip\MetadataParser\Parser;
use Liip\MetadataParser\RecursionChecker;
use Liip\Serializer\Configuration\GeneratorConfiguration;
use Liip\Serializer\Template\Deserialization;
use Liip\Serializer\Template\Serialization;
use RetailCrm\Api\Component\Serializer\Generator\DeserializerGenerator;
use RetailCrm\Api\Component\Serializer\Generator\SerializerGenerator;
use RetailCrm\Api\Component\Serializer\ModelsChecksumGenerator;
use RetailCrm\Api\Component\Serializer\Parser\JMSParser;
use RetailCrm\Api\Component\Serializer\Template\CustomDeserialization;
use RetailCrm\Api\Component\Serializer\Template\CustomSerialization;
use RetailCrm\Api\Component\Utils as DevUtils;
use RuntimeException;
/**
* Class ModelsGenerator
*
* @category ModelsGenerator
* @package RetailCrm\Api\Component
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class ModelsGenerator
{
/**
* Request models and filters are being handled by the FormEncoder component.
* They don't require caching at all. That's why we should ignore them - there is no need to
* waste inodes for the useless cache files.
*/
private const IGNORED_NAMESPACES = [
'RetailCrm\\Api\\Model\\Request',
'RetailCrm\\Api\\Model\\Filter'
];
/** @var string[] */
private $models;
/** @var array<string, string> */
private $oldChecksums;
/** @var array<string, string> */
private $newChecksums;
/** @var bool */
private $generateAll;
/**
* ModelsGenerator constructor.
*
* @param bool $generateAll
*/
public function __construct(bool $generateAll)
{
$this->generateAll = $generateAll;
}
public function generate(): void
{
$target = Utils::getModelsCacheDirectory();
$this->calculateChecksums();
if (!is_dir($target)) {
static::createDir($target);
file_put_contents(implode(DIRECTORY_SEPARATOR, [$target, '.gitkeep']), '');
}
self::generateModelCache($this->models, $target);
ModelsChecksumGenerator::saveChecksums($this->newChecksums);
}
/**
* Returns a list of the models present in the library.
*/
public function loadModelsList(): void
{
$this->models = [];
$classes = new PhpFilesIterator(DevUtils::getModelsDirectory());
foreach ($classes as $model) {
if (!array_key_exists('fqn', $model)) {
continue;
}
if (
!static::isNamespaceIgnored($model['fqn']) &&
$this->shouldGenerateForModel($model['fqn'])
) {
$this->models[] = $model['fqn'];
}
}
}
/**
* @return string[]
*/
public function getModels(): array
{
return $this->models;
}
/**
* Calculate models checksums
*/
private function calculateChecksums(): void
{
$this->newChecksums = ModelsChecksumGenerator::generateChecksums();
if (!$this->generateAll) {
$this->oldChecksums = ModelsChecksumGenerator::getStoredChecksums();
}
}
/**
* Returns true if cache for model should be generated.
*
* @param string $className
*
* @return bool
*/
private function shouldGenerateForModel(string $className): bool
{
if ($this->generateAll) {
return true;
}
$serializerFile = SerializerGenerator::buildSerializerFunctionName($className, null, []) . '.php';
$deserializerFile = DeserializerGenerator::buildDeserializerFunctionName($className) . '.php';
if (
!is_file(implode(DIRECTORY_SEPARATOR, [Utils::getModelsCacheDirectory(), $serializerFile])) ||
!is_file(implode(DIRECTORY_SEPARATOR, [Utils::getModelsCacheDirectory(), $deserializerFile]))
) {
return true;
}
return (
isset($this->oldChecksums[$className]) &&
$this->oldChecksums[$className] !== $this->newChecksums[$className]
);
}
/**
* Generate models cache.
*
* @param string[] $classes
* @param string $target
*
* @throws \Exception
*/
private static function generateModelCache(array $classes, string $target): void
{
if (empty($classes)) {
return;
}
$configurationArray = [
'default_group_combinations' => [],
'default_versions' => [],
'classes' => [],
];
foreach ($classes as $class) {
$configurationArray['classes'][$class] = [];
}
PropertyCollection::useIdenticalNamingStrategy();
$configuration = GeneratorConfiguration::createFomArray($configurationArray);
$parsers = [new JMSParser(new AnnotationReader())];
$builder = new Builder(new Parser($parsers), new RecursionChecker(null, []));
$marshalGenerator = new SerializerGenerator(
new Serialization(),
new CustomSerialization(),
$configuration,
$target
);
$unmarshalGenerator = new DeserializerGenerator(
new Deserialization(),
new CustomDeserialization(),
$classes,
$target
);
$marshalGenerator->generate($builder);
$unmarshalGenerator->generate($builder);
}
/**
* Returns true if models in provided namespace should be ignored.
*
* @param string $namespace
*
* @return bool
*/
private static function isNamespaceIgnored(string $namespace): bool
{
foreach (static::IGNORED_NAMESPACES as $ignoredNamespace) {
if (false !== strpos($namespace, $ignoredNamespace)) {
return true;
}
}
return false;
}
/**
* Create directory
*
* @param string $dir
*
* @throws RuntimeException
*/
private static function createDir(string $dir): void
{
if (is_dir($dir)) {
return;
}
if (false === mkdir($dir, 0777, true) && false === is_dir($dir)) {
throw new RuntimeException(sprintf('Could not create directory "%s".', $dir));
}
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* PHP version 7.3
*
* @category PhpFilesIterator
* @package RetailCrm\Api\Component
*/
namespace RetailCrm\Api\Component;
use Iterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveRegexIterator;
use RegexIterator;
/**
* Class PhpFilesIterator
*
* @category PhpFilesIterator
* @package RetailCrm\Api\Component
* @implements Iterator<string|int, array<string, string>>
*/
class PhpFilesIterator implements Iterator
{
private const NAMESPACE_MATCHER = '/namespace\s+((?:\\\\{1,2}\w+|\w+\\\\{1,2})(?:\w+\\\\{0,2})+)/m';
/** @var Iterator<string|int, string|array> */
private $parent;
/**
* PhpFilesIterator constructor.
*
* @param string $directory
*/
public function __construct(string $directory)
{
$this->parent = new RegexIterator(
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)),
'/^.+\.php$/i',
RecursiveRegexIterator::GET_MATCH
);
}
/**
* @inheritDoc
*
* @return array<string, string>
*/
public function current(): array
{
$matches = [];
$file = $this->parent->current();
if (is_array($file) && count($file) > 0) {
$file = $file[0];
}
if (empty($file)) {
return [];
}
$result = ['file' => $file];
preg_match(static::NAMESPACE_MATCHER, (string) file_get_contents($file), $matches);
if (count($matches) >= 2) {
$result['namespace'] = $matches[1];
$result['fqn'] = sprintf('%s\\%s', $matches[1], str_ireplace('.php', '', basename($file)));
}
return $result;
}
/**
* @inheritDoc
*/
public function next(): void
{
$this->parent->next();
}
/**
* @inheritDoc
*
* @return int|string
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->parent->key();
}
/**
* @inheritDoc
*/
public function valid(): bool
{
return $this->parent->valid();
}
/**
* @inheritDoc
*/
public function rewind(): void
{
$this->parent->rewind();
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* PHP version 7.3
*
* @category RequestSender
* @package RetailCrm\Api\Component
*/
namespace RetailCrm\Api\Component;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use RetailCrm\Api\Interfaces\RequestSenderInterface;
use RetailCrm\Api\ResourceGroup\AbstractApiResourceGroup;
use RetailCrm\Api\Traits\BaseUrlAwareTrait;
/**
* Class RequestSender
*
* @category RequestSender
* @package RetailCrm\Api\Component
*/
class RequestSender extends AbstractApiResourceGroup implements RequestSenderInterface
{
use BaseUrlAwareTrait {
route as private makeRoute;
}
/**
* @param \RetailCrm\Api\ResourceGroup\AbstractApiResourceGroup $resourceGroup
*/
public function __construct(AbstractApiResourceGroup $resourceGroup)
{
parent::__construct(
$resourceGroup->baseUrl,
$resourceGroup->httpClient,
$resourceGroup->requestTransformer,
$resourceGroup->responseTransformer,
$resourceGroup->eventDispatcher,
$resourceGroup->logger
);
}
/**
* Sends custom request to provided route with provided method and body, returns array response.
* Request will be put into GET parameters or into POST form-data (depends on method).
*
* Note: do not remove "useless" exceptions which are marked as "never thrown" by IDE.
* PSR-18's ClientInterface doesn't have them in the DocBlock, but, according to PSR-18,
* they can be thrown by clients, and therefore should be present here.
*
* @see https://www.php-fig.org/psr/psr-18/#error-handling
*
* @param string $method
* @param string $route
* @param array<int|string, mixed> $requestForm
*
* @return array<int|string, mixed>
* @throws \RetailCrm\Api\Exception\ApiException
* @throws \RetailCrm\Api\Exception\ClientException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @SuppressWarnings(PHPMD.ElseExpression)
*/
public function send(
string $method,
string $route,
array $requestForm = []
): array {
$method = strtoupper($method);
$psrRequest = $this->requestTransformer->createCustomPsrRequest($method, $route, $requestForm);
$this->logPsr7Request($psrRequest);
try {
$psrResponse = $this->httpClient->sendRequest($psrRequest);
} catch (ClientExceptionInterface | NetworkExceptionInterface $exception) {
$this->processPsr18Exception($psrRequest, $exception);
}
if (isset($psrResponse)) {
$this->logPsr7Response($psrResponse);
} else {
return [];
}
return $this->responseTransformer->createCustomResponse($this->baseUrl, $psrRequest, $psrResponse);
}
/**
* @inheritDoc
*/
public function route(string $route): string
{
return $this->makeRoute($route);
}
/**
* @inheritDoc
*/
public function host(): string
{
return (string) parse_url($this->baseUrl, PHP_URL_HOST);
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target({"CLASS", "PROPERTY"})
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class AccessType
{
/**
* @Required
* @var string
*/
public $type;
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target("PROPERTY")
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class Accessor
{
/**
* @var string
*/
public $getter;
/**
* @var string
*/
public $setter;
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* Controls the order of properties in a class.
*
* @Annotation
* @Target("CLASS")
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class AccessorOrder
{
/**
* @Required
* @var string
*/
public $order;
/**
* @var array<string>
*/
public $custom = [];
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target("CLASS")
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
class Discriminator
{
/** @var array<string> */
public $map;
/** @var string */
public $field = 'type';
/** @var bool */
public $disabled = false;
/** @var string[] */
public $groups = [];
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target({"PROPERTY", "CLASS", "METHOD", "ANNOTATION"})
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class Exclude
{
/**
* @var string
*/
public $if;
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
use RetailCrm\Api\Component\Serializer\Exception\RuntimeException;
/**
* @Annotation
* @Target("CLASS")
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class ExclusionPolicy
{
public const NONE = 'NONE';
public const ALL = 'ALL';
/**
* @var string
*/
public $policy;
/**
* ExclusionPolicy constructor.
*
* @param array<string, string> $values
*/
public function __construct(array $values)
{
$value = self::NONE;
if (array_key_exists('value', $values)) {
$value = $values['value'];
}
if (array_key_exists('policy', $values)) {
$value = $values['policy'];
}
if (!\is_string($value)) {
throw new RuntimeException('Exclusion policy value must be of string type.');
}
$value = strtoupper($value);
if (self::NONE !== $value && self::ALL !== $value) {
throw new RuntimeException('Exclusion policy must either be "ALL", or "NONE".');
}
$this->policy = $value;
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class Expose
{
/**
* @var string
*/
public $if;
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target({"PROPERTY","METHOD","ANNOTATION"})
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class Groups
{
/** @var array<string> @Required */
public $groups;
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target({"PROPERTY","METHOD","ANNOTATION"})
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class Inline
{
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target({"PROPERTY","METHOD","ANNOTATION"})
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class MaxDepth
{
/**
* @Required
* @var int
*/
public $depth;
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* This annotation can be defined on methods which are called after the
* deserialization of the object is complete.
*
* These methods do not necessarily have to be public.
*
* @Annotation
* @Target("METHOD")
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class PostDeserialize
{
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* @Annotation
* @Target("METHOD")
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class PostSerialize
{
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
/**
* This annotation can be declared on methods which should be called
* before the Serialization process.
*
* These methods do not need to be public, and should do any clean-up, or
* preparation of the object that is necessary.
*
* @Annotation
* @Target("METHOD")
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class PreSerialize
{
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Annotation;
use RetailCrm\Api\Component\Serializer\Exception\RuntimeException;
/**
* @Annotation
* @Target({"PROPERTY","METHOD", "ANNOTATION"})
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class SerializedName
{
/**
* @var string
*/
public $name;
/**
* SerializedName constructor.
*
* @param array<string, string> $values
*/
public function __construct(array $values)
{
if (!isset($values['value']) || !\is_string($values['value'])) {
throw new RuntimeException(sprintf('"value" must be a string.'));
}
$this->name = $values['value'];
}
}

Some files were not shown because too many files have changed in this diff Show more