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

Compare commits

...

64 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
87 changed files with 4119 additions and 1307 deletions

View file

@ -14,27 +14,33 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.3', '7.4', '8.0', '8.1', '8.2']
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@v2
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@v2
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@v2
uses: codecov/codecov-action@v4
with:
verbose: true

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the workspace
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Run PHPCS
uses: chekalsky/phpcs-action@v1
phpmd:
@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the workspace
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Run PHPMD
uses: GeneaLabs/action-reviewdog-phpmd@1.0.0
with:
@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the workspace
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Run PHPStan
uses: docker://oskarstark/phpstan-ga:1.8.0
with:

View file

@ -19,7 +19,7 @@ jobs:
pages_threshold: major_outage
- name: Check out code into the workspace
if: success() && ${{ github.ref != 'refs/heads/master' }}
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Setup PHP 8.3
if: ${{ github.ref != 'refs/heads/master' }}
uses: shivammathur/setup-php@v2
@ -27,7 +27,7 @@ jobs:
php-version: "8.3"
- name: Cache phpDocumentor
id: cache-phpdocumentor
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: phpDocumentor.phar
key: phpdocumentor

View file

@ -16,26 +16,26 @@
}
],
"require": {
"php": ">=7.3.0",
"php": ">=7.3",
"ext-json": "*",
"psr/log": "^1|^2|^3",
"psr/http-client": "^1.0",
"psr/http-message": "^1.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-message-implementation": "^1.0",
"php-http/client-implementation": "^1.0",
"php-http/message-factory": "^1.0",
"php-http/discovery": "^1.13",
"doctrine/annotations": "^1.13|^2.0",
"liip/serializer": "2.2.*",
"liip/serializer": "2.2.* || 2.6.*",
"php-http/httplug": "^2.2",
"civicrm/composer-compile-plugin": "^0.18.0",
"symfony/console": "^4.0|^5.0|^6.0",
"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"
"symfony/cache": ">=v3.1.0",
"psr/http-factory": "^1.1"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.5",
@ -67,7 +67,7 @@
"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.2 && ./vendor/bin/phpcs -p tests --runtime-set testVersion 7.3-8.2 --warning-severity=0",
"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",

View file

@ -10,4 +10,8 @@
<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>

View file

@ -44,4 +44,6 @@
</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

View file

@ -1,4 +1,6 @@
parameters:
excludePaths:
- src/Component/Serializer/ArraySupportDecorator.php
ignoreErrors:
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Builder\\\\ClientBuilder\\:\\:buildHandlersChain\\(\\) through static\\:\\:\\.$#"
@ -210,36 +212,6 @@ parameters:
count: 2
path: src/Component/Serializer/ArraySupportDecorator.php
-
message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\DeserializerGenerator\\:\\:FILENAME_PREFIX through static\\:\\:\\.$#"
count: 1
path: src/Component/Serializer/Generator/DeserializerGenerator.php
-
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
count: 1
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Cannot call method getClassName\\(\\) on mixed\\.$#"
count: 1
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Cannot call method getGroups\\(\\) on mixed\\.$#"
count: 3
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Cannot call method getVersions\\(\\) on mixed\\.$#"
count: 1
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Unsafe access to private constant RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Generator\\\\SerializerGenerator\\:\\:FILENAME_PREFIX through static\\:\\:\\.$#"
count: 1
path: src/Component/Serializer/Generator/SerializerGenerator.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\ModelsChecksumGenerator\\:\\:getStoredChecksums\\(\\) should return array\\<string, string\\> but returns mixed\\.$#"
count: 1
@ -250,41 +222,6 @@ parameters:
count: 4
path: src/Component/Serializer/ModelsChecksumGenerator.php
-
message: "#^Cannot cast mixed to float\\.$#"
count: 1
path: src/Component/Serializer/Parser/BaseJMSParser.php
-
message: "#^Cannot cast mixed to int\\.$#"
count: 1
path: src/Component/Serializer/Parser/BaseJMSParser.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\BaseJMSParser\\:\\:parse\\(\\) should return array but returns mixed\\.$#"
count: 1
path: src/Component/Serializer/Parser/BaseJMSParser.php
-
message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
count: 2
path: src/Component/Serializer/Parser/BaseJMSParser.php
-
message: "#^Class RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSLexer extends generic class Doctrine\\\\Common\\\\Lexer\\\\AbstractLexer but does not specify its types\\: T, V$#"
count: 1
path: src/Component/Serializer/Parser/JMSLexer.php
-
message: "#^Method RetailCrm\\\\Api\\\\Component\\\\Serializer\\\\Parser\\\\JMSLexer\\:\\:parse\\(\\) should return int\\|string\\|null but returns int\\|string\\|UnitEnum\\|null\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSLexer.php
-
message: "#^Parameter \\#1 \\$object of function get_class expects object, mixed given\\.$#"
count: 2
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Parameter \\#2 \\$string2 of function strncmp expects string, class\\-string\\|false given\\.$#"
count: 1
@ -295,36 +232,6 @@ parameters:
count: 1
path: src/Component/Serializer/Parser/JMSParser.php
-
message: "#^Parameter \\#1 \\$className of class Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeClass constructor expects string, mixed given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Parameter \\#1 \\$className of static method Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeDateTime\\:\\:fromDateTimeClass\\(\\) expects string, mixed given\\.$#"
count: 2
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Parameter \\#1 \\$typeName of class Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypePrimitive constructor expects string, mixed given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Parameter \\#1 \\$typeName of static method Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypeDateTime\\:\\:isTypeDateTime\\(\\) expects string, mixed given\\.$#"
count: 2
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Parameter \\#1 \\$typeName of static method Liip\\\\MetadataParser\\\\Metadata\\\\PropertyTypePrimitive\\:\\:isTypePrimitive\\(\\) expects string, mixed given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#"
count: 1
path: src/Component/Serializer/Parser/JMSTypeParser.php
-
message: "#^Unsafe call to private method RetailCrm\\\\Api\\\\Component\\\\Transformer\\\\DateTimeTransformer\\:\\:createFromFormat\\(\\) through static\\:\\:\\.$#"
count: 3

View file

@ -1,4 +1,5 @@
includes:
- phpstan-baseline-serializer.neon
- phpstan-baseline.neon # TODO: This should be removed eventually.
parameters:

View file

@ -18,6 +18,7 @@ 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;
@ -67,6 +68,9 @@ class Client
/** @var \RetailCrm\Api\ResourceGroup\CustomFields */
public $customFields;
/** @var \RetailCrm\Api\ResourceGroup\CustomerInteraction */
public $customerInteraction;
/** @var \RetailCrm\Api\ResourceGroup\Customers */
public $customers;
@ -187,6 +191,14 @@ class Client
$eventDispatcher,
$logger
);
$this->customerInteraction = new CustomerInteraction(
$url,
$httpClient,
$requestTransformer,
$responseTransformer,
$eventDispatcher,
$logger
);
$this->customers = new Customers(
$url,
$httpClient,

View file

@ -13,6 +13,7 @@ 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
@ -82,7 +83,15 @@ class GenerateModelsCommand extends AbstractModelsProcessorCommand
$output->writeln('');
}
$generator->generate();
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.</>',

View file

@ -11,12 +11,12 @@ 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\Utils;
use RetailCrm\Api\Component\Serializer\Generator\DeserializerGenerator;
use RetailCrm\Api\Component\Serializer\Generator\SerializerGenerator;
use RetailCrm\Api\Component\Serializer\ModelsChecksumGenerator;
@ -177,6 +177,7 @@ class ModelsGenerator
$configurationArray['classes'][$class] = [];
}
PropertyCollection::useIdenticalNamingStrategy();
$configuration = GeneratorConfiguration::createFomArray($configurationArray);
$parsers = [new JMSParser(new AnnotationReader())];
$builder = new Builder(new Parser($parsers), new RecursionChecker(null, []));

View file

@ -16,212 +16,424 @@ use Liip\Serializer\Exception\UnsupportedFormatException;
use Liip\Serializer\SerializerInterface;
use Pnz\JsonException\Json;
/**
* Class ArraySupportDecorator
*
* @category ArraySupportDecorator
* @package RetailCrm\Api\Component\Serializer
*/
class ArraySupportDecorator implements SerializerInterface
{
/** @var \Liip\Serializer\SerializerInterface */
private $serializer;
if (PHP_VERSION_ID >= 80000) {
/**
* ArraySupportDecorator constructor.
* Class ArraySupportDecorator
*
* @param \Liip\Serializer\SerializerInterface $serializer
* @category ArraySupportDecorator
* @package RetailCrm\Api\Component\Serializer
*/
public function __construct(SerializerInterface $serializer)
class ArraySupportDecorator implements SerializerInterface
{
$this->serializer = $serializer;
}
/** @var \Liip\Serializer\SerializerInterface */
private $serializer;
/**
* @inheritDoc
* @throws \JsonException
*/
public function serialize($data, string $format, ?Context $context = null): string
{
if ('json' !== $format) {
throw new UnsupportedFormatException('Liip serializer only supports JSON for now');
/**
* ArraySupportDecorator constructor.
*
* @param \Liip\Serializer\SerializerInterface $serializer
*/
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
if (is_array($data)) {
try {
return Json::encode($this->encodeArray($data, $context), JSON_UNESCAPED_SLASHES);
} catch (JsonException $exception) {
throw new Exception(
sprintf(
'Failed to JSON encode data for %s. This is not supposed to happen.',
// @phpstan-ignore-next-line
is_object($data) ? get_class($data) : gettype($data)
),
0,
$exception
);
}
}
return $this->serializer->serialize($data, $format, $context);
}
/**
* @inheritDoc
*/
public function deserialize(string $data, string $type, string $format, ?Context $context = null)
{
if ('json' !== $format) {
throw new UnsupportedFormatException('Liip serializer only supports JSON for now');
}
if (static::isArrayType($type)) {
try {
$array = Json::decode($data, true);
} catch (JsonException $exception) {
throw new Exception('Failed to JSON decode data. This is not supposed to happen.', 0, $exception);
/**
* @inheritDoc
* @throws \JsonException
*/
public function serialize($data, string $format, ?Context $context = null): string
{
if ('json' !== $format) {
throw new UnsupportedFormatException('Liip serializer only supports JSON for now');
}
return $this->serializer->fromArray($this->decodeArray($array, $type, $context), $type, $context);
}
return $this->serializer->deserialize($data, $type, $format, $context);
}
/**
* @inheritDoc
*
* @return array<int|string, mixed>
*/
public function toArray($data, ?Context $context = null): array
{
if (is_array($data)) {
return $this->encodeArray($data, $context);
}
return $this->serializer->toArray($data, $context);
}
/**
* @inheritDoc
*
* @param array<int|string, mixed> $data
*
* @return array<int|string, mixed>|object
*/
public function fromArray(array $data, string $type, ?Context $context = null)
{
if (static::isArrayType($type)) {
return $this->decodeArray($data, $type, $context);
}
return $this->serializer->fromArray($data, $type, $context);
}
/**
* Encodes array of objects into simple multidimensional array.
*
* @param mixed[] $data
* @param \Liip\Serializer\Context|null $context
*
* @return mixed[]
* @throws \Liip\Serializer\Exception\Exception
* @throws \Liip\Serializer\Exception\UnsupportedTypeException
*/
private function encodeArray(array $data, ?Context $context = null): array
{
$result = [];
foreach ($data as $key => $value) {
switch (gettype($value)) {
case 'array':
$result[$key] = $this->encodeArray($value, $context);
break;
case 'object':
$result[$key] = $this->serializer->toArray($value, $context);
break;
default:
$result[$key] = $value;
break;
if (is_array($data)) {
try {
return Json::encode($this->encodeArray($data, $context), JSON_UNESCAPED_SLASHES);
} catch (JsonException $exception) {
throw new Exception(
sprintf(
'Failed to JSON encode data for %s. This is not supposed to happen.',
// @phpstan-ignore-next-line
is_object($data) ? get_class($data) : gettype($data)
),
0,
$exception
);
}
}
return $this->serializer->serialize($data, $format, $context);
}
return $result;
}
/**
* @inheritDoc
*/
public function deserialize(string $data, string $type, string $format, ?Context $context = null): mixed
{
if ('json' !== $format) {
throw new UnsupportedFormatException('Liip serializer only supports JSON for now');
}
/**
* Decodes array of arrays to array of objects.
*
* @param mixed[] $data
* @param string $type
* @param \Liip\Serializer\Context|null $context
*
* @return array<int|string, mixed>
* @throws \Liip\Serializer\Exception\Exception
* @throws \Liip\Serializer\Exception\UnsupportedTypeException
*/
private function decodeArray(array $data, string $type, ?Context $context = null): array
{
$result = [];
$subtype = static::getArrayValueType($type);
if (class_exists($subtype)) {
foreach ($data as $key => $item) {
if (is_array($item)) {
$result[$key] = $this->decodeArray($item, $subtype, $context);
continue;
if (static::isArrayType($type)) {
try {
$array = Json::decode($data, true);
} catch (JsonException $exception) {
throw new Exception('Failed to JSON decode data. This is not supposed to happen.', 0, $exception);
}
$result[$key] = $item;
return $this->serializer->fromArray($this->decodeArray($array, $type, $context), $type, $context);
}
return $this->serializer->deserialize($data, $type, $format, $context);
}
/**
* @inheritDoc
*
* @return array<int|string, mixed>
*/
public function toArray($data, ?Context $context = null): array
{
if (is_array($data)) {
return $this->encodeArray($data, $context);
}
return $this->serializer->toArray($data, $context);
}
/**
* @inheritDoc
*
* @param array<int|string, mixed> $data
*
* @return array<int|string, mixed>|object
*/
public function fromArray(array $data, string $type, ?Context $context = null): mixed
{
if (static::isArrayType($type)) {
return $this->decodeArray($data, $type, $context);
}
return $this->serializer->fromArray($data, $type, $context);
}
/**
* Encodes array of objects into simple multidimensional array.
*
* @param mixed[] $data
* @param \Liip\Serializer\Context|null $context
*
* @return mixed[]
* @throws \Liip\Serializer\Exception\Exception
* @throws \Liip\Serializer\Exception\UnsupportedTypeException
*/
private function encodeArray(array $data, ?Context $context = null): array
{
$result = [];
foreach ($data as $key => $value) {
switch (gettype($value)) {
case 'array':
$result[$key] = $this->encodeArray($value, $context);
break;
case 'object':
$result[$key] = $this->serializer->toArray($value, $context);
break;
default:
$result[$key] = $value;
break;
}
}
return $result;
}
return $data;
}
/**
* Decodes array of arrays to array of objects.
*
* @param mixed[] $data
* @param string $type
* @param \Liip\Serializer\Context|null $context
*
* @return array<int|string, mixed>
* @throws \Liip\Serializer\Exception\Exception
* @throws \Liip\Serializer\Exception\UnsupportedTypeException
*/
private function decodeArray(array $data, string $type, ?Context $context = null): array
{
$result = [];
$subtype = static::getArrayValueType($type);
/**
* Returns true if provided type is an array.
*
* @param string $type
*
* @return bool
*/
private static function isArrayType(string $type): bool
{
return false !== strpos($type, 'array');
}
if (class_exists($subtype)) {
foreach ($data as $key => $item) {
if (is_array($item)) {
$result[$key] = $this->decodeArray($item, $subtype, $context);
continue;
}
/**
* Returns array value type from types like 'array<string|int, Class\Name>' or 'array<string>'.
*
* @param string $type
*
* @return string
*/
private static function getArrayValueType(string $type): string
{
$matches = [];
$result[$key] = $item;
}
preg_match_all(
'/array(\s+)?\<([\w\|\\\\]+)\s+\,\s+([\w\|\\\\]+)\>/m',
$type,
$matches,
PREG_SET_ORDER,
0
);
return $result;
}
if (count($matches) > 0) {
return $matches[count($matches) - 1];
return $data;
}
preg_match_all('/array(\s+)?\<([\w\|\\\\]+)\>/m', $type, $matches, PREG_SET_ORDER, 0);
if (count($matches) > 0) {
return $matches[count($matches) - 1];
/**
* Returns true if provided type is an array.
*
* @param string $type
*
* @return bool
*/
private static function isArrayType(string $type): bool
{
return false !== strpos($type, 'array');
}
return 'mixed';
/**
* Returns array value type from types like 'array<string|int, Class\Name>' or 'array<string>'.
*
* @param string $type
*
* @return string
*/
private static function getArrayValueType(string $type): string
{
$matches = [];
preg_match_all(
'/array(\s+)?\<([\w\|\\\\]+)\s+\,\s+([\w\|\\\\]+)\>/m',
$type,
$matches,
PREG_SET_ORDER,
0
);
if (count($matches) > 0) {
return $matches[count($matches) - 1];
}
preg_match_all('/array(\s+)?\<([\w\|\\\\]+)\>/m', $type, $matches, PREG_SET_ORDER, 0);
if (count($matches) > 0) {
return $matches[count($matches) - 1];
}
return 'mixed';
}
}
} else {
/**
* Class ArraySupportDecorator
*
* @category ArraySupportDecorator
* @package RetailCrm\Api\Component\Serializer
*/
class ArraySupportDecorator implements SerializerInterface
{
/** @var \Liip\Serializer\SerializerInterface */
private $serializer;
/**
* ArraySupportDecorator constructor.
*
* @param \Liip\Serializer\SerializerInterface $serializer
*/
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
/**
* @inheritDoc
* @throws \JsonException
*/
public function serialize($data, string $format, ?Context $context = null): string
{
if ('json' !== $format) {
throw new UnsupportedFormatException('Liip serializer only supports JSON for now');
}
if (is_array($data)) {
try {
return Json::encode($this->encodeArray($data, $context), JSON_UNESCAPED_SLASHES);
} catch (JsonException $exception) {
throw new Exception(
sprintf(
'Failed to JSON encode data for %s. This is not supposed to happen.',
// @phpstan-ignore-next-line
is_object($data) ? get_class($data) : gettype($data)
),
0,
$exception
);
}
}
return $this->serializer->serialize($data, $format, $context);
}
/**
* @inheritDoc
*/
public function deserialize(string $data, string $type, string $format, ?Context $context = null)
{
if ('json' !== $format) {
throw new UnsupportedFormatException('Liip serializer only supports JSON for now');
}
if (static::isArrayType($type)) {
try {
$array = Json::decode($data, true);
} catch (JsonException $exception) {
throw new Exception('Failed to JSON decode data. This is not supposed to happen.', 0, $exception);
}
return $this->serializer->fromArray($this->decodeArray($array, $type, $context), $type, $context);
}
return $this->serializer->deserialize($data, $type, $format, $context);
}
/**
* @inheritDoc
*
* @return array<int|string, mixed>
*/
public function toArray($data, ?Context $context = null): array
{
if (is_array($data)) {
return $this->encodeArray($data, $context);
}
return $this->serializer->toArray($data, $context);
}
/**
* @inheritDoc
*
* @param array<int|string, mixed> $data
*
* @return array<int|string, mixed>|object
*/
public function fromArray(array $data, string $type, ?Context $context = null)
{
if (static::isArrayType($type)) {
return $this->decodeArray($data, $type, $context);
}
return $this->serializer->fromArray($data, $type, $context);
}
/**
* Encodes array of objects into simple multidimensional array.
*
* @param mixed[] $data
* @param \Liip\Serializer\Context|null $context
*
* @return mixed[]
* @throws \Liip\Serializer\Exception\Exception
* @throws \Liip\Serializer\Exception\UnsupportedTypeException
*/
private function encodeArray(array $data, ?Context $context = null): array
{
$result = [];
foreach ($data as $key => $value) {
switch (gettype($value)) {
case 'array':
$result[$key] = $this->encodeArray($value, $context);
break;
case 'object':
$result[$key] = $this->serializer->toArray($value, $context);
break;
default:
$result[$key] = $value;
break;
}
}
return $result;
}
/**
* Decodes array of arrays to array of objects.
*
* @param mixed[] $data
* @param string $type
* @param \Liip\Serializer\Context|null $context
*
* @return array<int|string, mixed>
* @throws \Liip\Serializer\Exception\Exception
* @throws \Liip\Serializer\Exception\UnsupportedTypeException
*/
private function decodeArray(array $data, string $type, ?Context $context = null): array
{
$result = [];
$subtype = static::getArrayValueType($type);
if (class_exists($subtype)) {
foreach ($data as $key => $item) {
if (is_array($item)) {
$result[$key] = $this->decodeArray($item, $subtype, $context);
continue;
}
$result[$key] = $item;
}
return $result;
}
return $data;
}
/**
* Returns true if provided type is an array.
*
* @param string $type
*
* @return bool
*/
private static function isArrayType(string $type): bool
{
return false !== strpos($type, 'array');
}
/**
* Returns array value type from types like 'array<string|int, Class\Name>' or 'array<string>'.
*
* @param string $type
*
* @return string
*/
private static function getArrayValueType(string $type): string
{
$matches = [];
preg_match_all(
'/array(\s+)?\<([\w\|\\\\]+)\s+\,\s+([\w\|\\\\]+)\>/m',
$type,
$matches,
PREG_SET_ORDER,
0
);
if (count($matches) > 0) {
return $matches[count($matches) - 1];
}
preg_match_all('/array(\s+)?\<([\w\|\\\\]+)\>/m', $type, $matches, PREG_SET_ORDER, 0);
if (count($matches) > 0) {
return $matches[count($matches) - 1];
}
return 'mixed';
}
}
}

View file

@ -1,11 +1,6 @@
<?php
/**
* PHP version 7.3
*
* @category DeserializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
*/
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Generator;
@ -18,6 +13,8 @@ use Liip\MetadataParser\Metadata\PropertyTypeDateTime;
use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
use Liip\MetadataParser\Reducer\TakeBestReducer;
use Liip\Serializer\Configuration\ClassToGenerate;
use Liip\Serializer\Configuration\GeneratorConfiguration;
use Liip\Serializer\Path\ArrayPath;
use Liip\Serializer\Path\ModelPath;
use Liip\Serializer\Template\Deserialization;
@ -26,129 +23,75 @@ use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed;
use RetailCrm\Api\Interfaces\Orders\CustomerInterface;
use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem;
/**
* Class DeserializerGenerator
*
* @category DeserializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
* @license https://github.com/liip/serializer/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/serializer
* @internal
*
* @SuppressWarnings(PHPMD)
*/
class DeserializerGenerator
final class DeserializerGenerator
{
private const FILENAME_PREFIX = 'deserialize';
/**
* @var Deserialization
*/
private $templating;
/**
* @var \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization
*/
private $customTemplating;
/**
* @var Filesystem
*/
/** @var \Symfony\Component\Filesystem\Filesystem */
private $filesystem;
/**
* @var Builder
*/
/** @var \Liip\Serializer\Configuration\GeneratorConfiguration */
private $configuration;
/** @var \Liip\Serializer\Template\Deserialization */
private $templating;
/** @var \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization */
private $customTemplating;
/** @var string */
private $cacheDirectory;
/** @var \Liip\MetadataParser\Builder */
private $metadataBuilder;
/**
* This is a list of fqn classnames
*
* I.e.
*
* [
* Product::class,
* ];
*
* @var string[]
*/
private $classesToGenerate;
/**
* @var string
*/
private $cacheDirectory;
/**
* @param \Liip\Serializer\Template\Deserialization $templating
* @param \RetailCrm\Api\Component\Serializer\Template\CustomDeserialization $customTemplating
* @param string[] $classesToGenerate
* @param string $cacheDirectory
* @param list<class-string> $classesToGenerate This is a list of FQCN classnames
*/
public function __construct(
Deserialization $templating,
CustomDeserialization $customTemplating,
array $classesToGenerate,
string $cacheDirectory
string $cacheDirectory,
GeneratorConfiguration $configuration = null
) {
$this->cacheDirectory = $cacheDirectory;
$this->templating = $templating;
$this->customTemplating = $customTemplating;
$this->classesToGenerate = $classesToGenerate;
$this->cacheDirectory = $cacheDirectory;
$this->filesystem = new Filesystem();
$this->configuration = $this->createGeneratorConfiguration($configuration, $classesToGenerate);
}
/**
* @param string $className
*
* @return string
*/
public static function buildDeserializerFunctionName(string $className): string
{
return static::FILENAME_PREFIX . '_' . str_replace('\\', '_', $className);
return self::FILENAME_PREFIX.'_'.str_replace('\\', '_', $className);
}
/**
* @param \Liip\MetadataParser\Builder $metadataBuilder
*
* @throws \Exception
*/
public function generate(Builder $metadataBuilder): void
{
$this->metadataBuilder = $metadataBuilder;
$this->filesystem->mkdir($this->cacheDirectory);
foreach ($this->classesToGenerate as $className) {
/** @var ClassToGenerate $classToGenerate */
foreach ($this->configuration as $classToGenerate) {
// we do not use the oldest version reducer here and hope for the best
// otherwise we end up with generated property names for accessor methods
$classMetadata = $metadataBuilder->build($className, [
$classMetadata = $metadataBuilder->build($classToGenerate->getClassName(), [
new TakeBestReducer(),
]);
$this->writeFile($classMetadata);
}
}
/**
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata
*
* @throws \Exception
*/
private function writeFile(ClassMetadata $classMetadata): void
{
if (count($classMetadata->getConstructorParameters())) {
throw new RuntimeException(sprintf(
'We currently do not support deserializing when the root class has a non-empty constructor. Class %s',
$classMetadata->getClassName()
));
if (\count($classMetadata->getConstructorParameters())) {
throw new \Exception(sprintf('We currently do not support deserializing when the root class has a non-empty constructor. Class %s', $classMetadata->getClassName()));
}
$functionName = static::buildDeserializerFunctionName($classMetadata->getClassName());
$functionName = self::buildDeserializerFunctionName($classMetadata->getClassName());
$arrayPath = new ArrayPath('jsonData');
$code = $this->templating->renderFunction(
@ -162,13 +105,7 @@ class DeserializerGenerator
}
/**
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
* @param array<string, positive-int> $stack
*/
private function generateCodeForClass(
ClassMetadata $classMetadata,
@ -179,9 +116,9 @@ class DeserializerGenerator
$stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1;
$constructorArgumentNames = [];
$overwrittenNames = [];
$initCode = '';
$code = '';
foreach ($classMetadata->getProperties() as $propertyMetadata) {
$propertyArrayPath = $arrayPath->withFieldName($propertyMetadata->getSerializedName());
@ -189,6 +126,9 @@ class DeserializerGenerator
$argument = $classMetadata->getConstructorParameter($propertyMetadata->getName());
$default = var_export($argument->isRequired() ? null : $argument->getDefaultValue(), true);
$tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]);
if (\array_key_exists($propertyMetadata->getName(), $constructorArgumentNames)) {
$overwrittenNames[$propertyMetadata->getName()] = true;
}
$constructorArgumentNames[$propertyMetadata->getName()] = (string) $tempVariable;
$initCode .= $this->templating->renderArgument(
@ -206,26 +146,21 @@ class DeserializerGenerator
}
$constructorArguments = [];
foreach ($classMetadata->getConstructorParameters() as $definition) {
if (array_key_exists($definition->getName(), $constructorArgumentNames)) {
if (\array_key_exists($definition->getName(), $constructorArgumentNames)) {
$constructorArguments[] = $constructorArgumentNames[$definition->getName()];
continue;
}
if ($definition->isRequired()) {
throw new RuntimeException(sprintf(
'Unknown constructor argument "%s" in "%s(%s)"',
$definition->getName(),
$classMetadata->getClassName(),
implode(', ', array_keys($constructorArgumentNames))
));
$msg = sprintf('Unknown constructor argument "%s". Class %s only has properties that tell how to handle %s.', $definition->getName(), $classMetadata->getClassName(), implode(', ', array_keys($constructorArgumentNames)));
if ($overwrittenNames) {
$msg .= sprintf(' Multiple definitions for fields %s seen - the last one overwrites previous ones.', implode(', ', array_keys($overwrittenNames)));
}
throw new \Exception($msg);
}
$constructorArguments[] = var_export($definition->getDefaultValue(), true);
}
if (count($constructorArgumentNames) > 0) {
if (\count($constructorArgumentNames) > 0) {
$code .= $this->templating->renderUnset(array_values($constructorArgumentNames));
}
@ -233,13 +168,7 @@ class DeserializerGenerator
return $this->generateCustomerInterface($classMetadata, $arrayPath, $modelPath, $initCode, $stack);
}
return $this->templating->renderClass(
(string) $modelPath,
$classMetadata->getClassName(),
$constructorArguments,
$code,
$initCode
);
return $this->templating->renderClass((string) $modelPath, $classMetadata->getClassName(), $constructorArguments, $code, $initCode);
}
/**
@ -282,13 +211,7 @@ class DeserializerGenerator
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
* @param array<string, positive-int> $stack
*/
private function generateCodeForProperty(
PropertyMetadata $propertyMetadata,
@ -300,16 +223,16 @@ class DeserializerGenerator
return '';
}
if (Recursion::hasMaxDepthReached($propertyMetadata, $stack)) {
return '';
}
if ($propertyMetadata->getAccessor()->hasSetterMethod()) {
$tempVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]);
$code = $this->generateCodeForField($propertyMetadata, $arrayPath, $tempVariable, $stack);
$code .= $this->templating->renderConditional(
(string) $tempVariable,
$this->templating->renderSetter(
(string) $modelPath,
(string) $propertyMetadata->getAccessor()->getSetterMethod(),
(string) $tempVariable
)
$this->templating->renderSetter((string) $modelPath, $propertyMetadata->getAccessor()->getSetterMethod(), (string) $tempVariable)
);
$code .= $this->templating->renderUnset([(string) $tempVariable]);
@ -322,13 +245,7 @@ class DeserializerGenerator
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
* @param array<string, positive-int> $stack
*/
private function generateCodeForField(
PropertyMetadata $propertyMetadata,
@ -342,14 +259,17 @@ class DeserializerGenerator
);
}
private function isArrayTraversable(PropertyTypeArray $array): bool
{
if (method_exists($array, 'isCollection')) {
return $array->isCollection();
}
return $array->isTraversable();
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPropertyPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
* @param array<string, positive-int> $stack
*/
private function generateInnerCodeForFieldType(
PropertyMetadata $propertyMetadata,
@ -359,64 +279,50 @@ class DeserializerGenerator
): string {
$type = $propertyMetadata->getType();
if ($type instanceof PropertyTypeArray) {
if ($type->getSubType() instanceof PropertyTypePrimitive) {
// for arrays of scalars, copy the field even when its an empty array
return $this->templating->renderAssignJsonDataToField((string) $modelPropertyPath, (string) $arrayPath);
}
// either array or hashmap with second param the type of values
// the index works the same whether its numeric or hashmap
return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack);
}
switch ($type) {
case $type instanceof PropertyTypeArray:
if ($this->isArrayTraversable($type)) {
return $this->generateCodeForArrayCollection($propertyMetadata, $type, $arrayPath, $modelPropertyPath, $stack);
}
return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack);
case $type instanceof PropertyTypeDateTime:
if (null !== $type->getZone()) {
throw new RuntimeException('Timezone support is not implemented');
if (method_exists($type, 'getDeserializeFormat')) {
$format = $type->getDeserializeFormat();
if (null !== $format) {
return $this->templating->renderAssignDateTimeFromFormat($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath, $format, $type->getZone());
}
return $this->templating->renderAssignDateTimeToField($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath);
}
$format = $type->getDeserializeFormat() ?: $type->getFormat();
if (null !== $format) {
return $this->templating->renderAssignDateTimeFromFormat(
$type->isImmutable(),
(string) $modelPropertyPath,
(string) $arrayPath,
$format
);
$formats = $type->getDeserializeFormats() ?: (\is_string($type->getFormat()) ? [$type->getFormat()] : $type->getFormat());
if (null !== $formats) {
return $this->templating->renderAssignDateTimeFromFormat($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath, $formats, $type->getZone());
}
return $this->templating->renderAssignDateTimeToField(
$type->isImmutable(),
(string) $modelPropertyPath,
(string) $arrayPath
);
return $this->templating->renderAssignDateTimeToField($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath);
case $type instanceof PropertyTypePrimitive && 'float' === $type->getTypeName():
return $this->templating->renderAssignJsonDataToFieldWithCasting(
(string) $modelPropertyPath,
(string) $arrayPath,
'float'
);
return $this->templating->renderAssignJsonDataToFieldWithCasting((string) $modelPropertyPath, (string) $arrayPath, 'float');
case $type instanceof PropertyTypePrimitive:
case $type instanceof PropertyTypeUnknown:
case $type instanceof PropertyTypeMixed:
return $this->templating->renderAssignJsonDataToField((string) $modelPropertyPath, (string) $arrayPath);
case $type instanceof PropertyTypeClass:
return $this->generateCodeForClass($type->getClassMetadata(), $arrayPath, $modelPropertyPath, $stack);
default:
throw new RuntimeException('Unexpected type ' . get_class($type) . ' at ' . $modelPropertyPath);
throw new \Exception('Unexpected type '. get_class($type) .' at '.$modelPropertyPath);
}
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyTypeArray $type
* @param \Liip\Serializer\Path\ArrayPath $arrayPath
* @param \Liip\Serializer\Path\ModelPath $modelPath
* @param mixed[] $stack
*
* @return string
* @throws \Exception
* @param array<string, positive-int> $stack
*/
private function generateCodeForArray(
PropertyTypeArray $type,
@ -424,6 +330,11 @@ class DeserializerGenerator
ModelPath $modelPath,
array $stack
): string {
if ($type->getSubType() instanceof PropertyTypePrimitive) {
// for arrays of scalars, copy the field even when its an empty array
return $this->templating->renderAssignJsonDataToField((string) $modelPath, (string) $arrayPath);
}
$index = ModelPath::indexVariable((string) $arrayPath);
$arrayPropertyPath = $arrayPath->withVariable((string) $index);
$modelPropertyPath = $modelPath->withArray((string) $index);
@ -433,22 +344,16 @@ class DeserializerGenerator
case $subType instanceof PropertyTypeArray:
$innerCode = $this->generateCodeForArray($subType, $arrayPropertyPath, $modelPropertyPath, $stack);
break;
case $subType instanceof PropertyTypeClass:
$innerCode = $this->generateCodeForClass(
$subType->getClassMetadata(),
$arrayPropertyPath,
$modelPropertyPath,
$stack
);
$innerCode = $this->generateCodeForClass($subType->getClassMetadata(), $arrayPropertyPath, $modelPropertyPath, $stack);
break;
case $subType instanceof PropertyTypeUnknown:
$innerCode = $this->templating->renderAssignJsonDataToField(
$modelPropertyPath,
$arrayPropertyPath
);
break;
return $this->templating->renderAssignJsonDataToField((string) $modelPath, (string) $arrayPath);
default:
throw new RuntimeException('Unexpected array subtype ' . get_class($subType));
throw new \Exception('Unexpected array subtype '. get_class($subType));
}
if ('' === $innerCode) {
@ -460,4 +365,42 @@ class DeserializerGenerator
return $code;
}
/**
* @param array<string, positive-int> $stack
*/
private function generateCodeForArrayCollection(
PropertyMetadata $propertyMetadata,
PropertyTypeArray $type,
ArrayPath $arrayPath,
ModelPath $modelPath,
array $stack
): string {
$tmpVariable = ModelPath::tempVariable([(string) $modelPath, $propertyMetadata->getName()]);
$innerCode = $this->generateCodeForArray($type, $arrayPath, $tmpVariable, $stack);
if ('' === $innerCode) {
return '';
}
return $innerCode.$this->templating->renderArrayCollection((string) $modelPath, (string) $tmpVariable);
}
/**
* @param list<class-string> $classesToGenerate
*/
private function createGeneratorConfiguration(
?GeneratorConfiguration $configuration,
array $classesToGenerate
): GeneratorConfiguration {
if (null === $configuration) {
$configuration = new GeneratorConfiguration([], []);
}
foreach ($classesToGenerate as $className) {
$configuration->addClassToGenerate(new ClassToGenerate($configuration, $className));
}
return $configuration;
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Generator;
use Liip\MetadataParser\Metadata\PropertyMetadata;
use Liip\MetadataParser\Metadata\PropertyTypeArray;
use Liip\MetadataParser\Metadata\PropertyTypeClass;
abstract class Recursion
{
/**
* @param array<string, positive-int> $stack
*/
public static function check(string $className, array $stack, string $modelPath): bool
{
if (\array_key_exists($className, $stack) && $stack[$className] > 1) {
throw new \Exception(sprintf('recursion for %s at %s', key($stack), $modelPath));
}
return false;
}
/**
* @param array<string, positive-int> $stack
*/
public static function hasMaxDepthReached(PropertyMetadata $propertyMetadata, array $stack): bool
{
if (null === $propertyMetadata->getMaxDepth()) {
return false;
}
$className = self::getClassNameFromProperty($propertyMetadata);
if (null === $className) {
return false;
}
$classStackCount = $stack[$className] ?? 0;
return $classStackCount > $propertyMetadata->getMaxDepth();
}
private static function getClassNameFromProperty(PropertyMetadata $propertyMetadata): ?string
{
$type = $propertyMetadata->getType();
if ($type instanceof PropertyTypeArray) {
$type = $type->getLeafType();
}
if (!($type instanceof PropertyTypeClass)) {
return null;
}
return $type->getClassName();
}
}

View file

@ -1,17 +1,9 @@
<?php
/**
* PHP version 7.3
*
* @category SerializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
*/
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Generator;
use DateTime;
use Liip\MetadataParser\Builder;
use Liip\MetadataParser\Metadata\ClassMetadata;
use Liip\MetadataParser\Metadata\PropertyMetadata;
@ -27,140 +19,99 @@ use Liip\MetadataParser\Reducer\TakeBestReducer;
use Liip\MetadataParser\Reducer\VersionReducer;
use Liip\Serializer\Configuration\GeneratorConfiguration;
use Liip\Serializer\Template\Serialization;
use RetailCrm\Api\Component\Serializer\Template\CustomSerialization;
use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed;
use RetailCrm\Api\Interfaces\Orders\CustomerInterface;
use RetailCrm\Api\Model\Entity\Customers\Customer;
use RetailCrm\Api\Model\Entity\Customers\CustomerTag;
use RetailCrm\Api\Model\Entity\CustomersCorporate\CustomerCorporate;
use RetailCrm\Api\Component\Serializer\Template\CustomSerialization;
use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed;
use RetailCrm\Api\Model\Entity\CustomersCorporate\SerializedRelationAbstractCustomer;
use RetailCrm\Api\Model\Entity\Orders\SerializedRelationCustomer;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem;
/**
* Class SerializerGenerator
*
* @category SerializerGenerator
* @package RetailCrm\Api\Component\Serializer\Generator
* @license https://github.com/liip/serializer/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/serializer
* @internal
*
* @SuppressWarnings(PHPMD)
*/
class SerializerGenerator
final class SerializerGenerator
{
private const FILENAME_PREFIX = 'serialize';
/**
* @var Serialization
*/
private $templating;
/**
* @var \RetailCrm\Api\Component\Serializer\Template\CustomSerialization
*/
private $customTemplating;
/**
* @var Builder
*/
private $metadataBuilder;
/**
* @var GeneratorConfiguration<mixed>
*/
private $configuration;
/**
* @var string
*/
private $cacheDirectory;
/**
* @var Filesystem
*/
/** @var \Symfony\Component\Filesystem\Filesystem */
private $filesystem;
/**
* SerializerGenerator constructor.
*
* @param \Liip\Serializer\Template\Serialization $templating
* @param \RetailCrm\Api\Component\Serializer\Template\CustomSerialization $customTemplating
* @param \Liip\Serializer\Configuration\GeneratorConfiguration<mixed> $configuration
* @param string $cacheDirectory
*/
/** @var \Liip\Serializer\Template\Serialization */
private $templating;
/** @var \Liip\Serializer\Configuration\GeneratorConfiguration */
private $configuration;
/** @var string */
private $cacheDirectory;
/** @var \RetailCrm\Api\Component\Serializer\Template\CustomSerialization */
private $customTemplating;
/** @var \Liip\MetadataParser\Builder */
private $metadataBuilder;
public function __construct(
Serialization $templating,
CustomSerialization $customTemplating,
GeneratorConfiguration $configuration,
string $cacheDirectory
) {
$this->templating = $templating;
$this->cacheDirectory = $cacheDirectory;
$this->configuration = $configuration;
$this->templating = $templating;
$this->customTemplating = $customTemplating;
$this->configuration = $configuration;
$this->cacheDirectory = $cacheDirectory;
$this->filesystem = new Filesystem();
}
/**
* @param string $className
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
*
* @return string
* @param list<string> $serializerGroups
*/
public static function buildSerializerFunctionName(
string $className,
?string $apiVersion,
array $serializerGroups
): string {
$functionName = static::FILENAME_PREFIX . '_' . $className;
if (count($serializerGroups)) {
$functionName .= '_' . implode('_', $serializerGroups);
public static function buildSerializerFunctionName(string $className, ?string $apiVersion, array $serializerGroups): string
{
$functionName = self::FILENAME_PREFIX.'_'.$className;
if (\count($serializerGroups)) {
$functionName .= '_'.implode('_', $serializerGroups);
}
if (null !== $apiVersion) {
$functionName .= '_' . $apiVersion;
$functionName .= '_'.$apiVersion;
}
return (string) preg_replace('/[^a-zA-Z0-9_]/', '_', $functionName);
return preg_replace('/[^a-zA-Z0-9_]/', '_', $functionName);
}
/**
* @param \Liip\MetadataParser\Builder $metadataBuilder
*/
public function generate(Builder $metadataBuilder): void
{
$this->metadataBuilder = $metadataBuilder;
$this->filesystem->mkdir($this->cacheDirectory);
foreach ($this->configuration as $classToGenerate) {
foreach ($classToGenerate as $groupCombination) {
$className = $classToGenerate->getClassName();
foreach ($groupCombination->getVersions() as $version) {
$groups = $groupCombination->getGroups();
if ('' === $version) {
$metadata = $metadataBuilder->build($className, [
new PreferredReducer(),
new TakeBestReducer(),
]);
$this->writeFile($className, null, $groupCombination->getGroups(), $metadata);
if ([] === $groups) {
$metadata = $metadataBuilder->build($className, [
new PreferredReducer(),
new TakeBestReducer(),
]);
$this->writeFile($className, null, [], $metadata);
} else {
$metadata = $metadataBuilder->build($className, [
new GroupReducer($groups),
new PreferredReducer(),
new TakeBestReducer(),
]);
$this->writeFile($className, null, $groups, $metadata);
}
} else {
$metadata = $metadataBuilder->build($className, [
new VersionReducer($version),
new GroupReducer($groupCombination->getGroups()),
new GroupReducer($groups),
new TakeBestReducer(),
]);
$this->writeFile($className, $version, $groupCombination->getGroups(), $metadata);
$this->writeFile($className, $version, $groups, $metadata);
}
}
}
@ -168,10 +119,7 @@ class SerializerGenerator
}
/**
* @param string $className
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata
* @param list<string> $serializerGroups
*/
private function writeFile(
string $className,
@ -179,8 +127,7 @@ class SerializerGenerator
array $serializerGroups,
ClassMetadata $classMetadata
): void {
sort($serializerGroups);
$functionName = static::buildSerializerFunctionName($className, $apiVersion, $serializerGroups);
$functionName = self::buildSerializerFunctionName($className, $apiVersion, $serializerGroups);
$code = $this->templating->renderFunction(
$functionName,
@ -192,15 +139,8 @@ class SerializerGenerator
}
/**
* @param \Liip\MetadataParser\Metadata\ClassMetadata $classMetadata
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
* @param string $arrayPath
* @param string $modelPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
* @param list<string> $serializerGroups
* @param array<string, positive-int> $stack
*/
private function generateCodeForClass(
ClassMetadata $classMetadata,
@ -226,17 +166,10 @@ class SerializerGenerator
}
$stack[$classMetadata->getClassName()] = ($stack[$classMetadata->getClassName()] ?? 0) + 1;
$code = '';
$code = '';
foreach ($classMetadata->getProperties() as $propertyMetadata) {
$code .= $this->generateCodeForField(
$propertyMetadata,
$apiVersion,
$serializerGroups,
$arrayPath,
$modelPath,
$stack
);
$code .= $this->generateCodeForField($propertyMetadata, $apiVersion, $serializerGroups, $arrayPath, $modelPath, $stack);
}
return $this->templating->renderClass($arrayPath, $code);
@ -334,15 +267,8 @@ class SerializerGenerator
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyMetadata $propertyMetadata
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
* @param string $arrayPath
* @param string $modelPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
* @param list<string> $serializerGroups
* @param array<string, positive-int> $stack
*/
private function generateCodeForField(
PropertyMetadata $propertyMetadata,
@ -352,61 +278,34 @@ class SerializerGenerator
string $modelPath,
array $stack
): string {
$modelPropertyPath = $modelPath . '->' . $propertyMetadata->getName();
$fieldPath = $arrayPath . '["' . $propertyMetadata->getSerializedName() . '"]';
if (Recursion::hasMaxDepthReached($propertyMetadata, $stack)) {
return '';
}
$modelPropertyPath = $modelPath.'->'.$propertyMetadata->getName();
$fieldPath = $arrayPath.'["'.$propertyMetadata->getSerializedName().'"]';
if ($propertyMetadata->getAccessor()->hasGetterMethod()) {
$tempVariable = str_replace(['->', '[', ']', '$'], '', $modelPath) . ucfirst($propertyMetadata->getName());
$tempVariable = str_replace(['->', '[', ']', '$'], '', $modelPath).ucfirst($propertyMetadata->getName());
return $this->templating->renderConditional(
$this->templating->renderTempVariable(
$tempVariable,
$this->templating->renderGetter(
$modelPath,
(string) $propertyMetadata->getAccessor()->getGetterMethod()
)
),
$this->generateCodeForFieldType(
$propertyMetadata->getType(),
$apiVersion,
$serializerGroups,
$fieldPath,
'$' . $tempVariable,
$stack
)
$this->templating->renderTempVariable($tempVariable, $this->templating->renderGetter($modelPath, $propertyMetadata->getAccessor()->getGetterMethod())),
$this->generateCodeForFieldType($propertyMetadata->getType(), $apiVersion, $serializerGroups, $fieldPath, '$'.$tempVariable, $stack)
);
}
if (!$propertyMetadata->isPublic()) {
throw new RuntimeException(sprintf(
'Property %s is not public and no getter has been defined. Stack %s',
$modelPropertyPath,
var_export($stack, true)
));
throw new \Exception(sprintf('Property %s is not public and no getter has been defined. Stack %s', $modelPropertyPath, var_export($stack, true)));
}
return $this->templating->renderConditional(
$modelPropertyPath,
$this->generateCodeForFieldType(
$propertyMetadata->getType(),
$apiVersion,
$serializerGroups,
$fieldPath,
$modelPropertyPath,
$stack
)
$this->generateCodeForFieldType($propertyMetadata->getType(), $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack)
);
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyType $type
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
* @param string $fieldPath
* @param string $modelPropertyPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
* @param list<string> $serializerGroups
* @param array<string, positive-int> $stack
*/
private function generateCodeForFieldType(
PropertyType $type,
@ -416,31 +315,9 @@ class SerializerGenerator
string $modelPropertyPath,
array $stack
): string {
if ($type instanceof PropertyTypeArray) {
if ($type->getSubType() instanceof PropertyTypePrimitive) {
// for arrays of scalars, copy the field even when its an empty array
return $this->templating->renderAssign($fieldPath, $modelPropertyPath);
}
// either array or hashmap with second param the type of values
// the index works the same whether its numeric or hashmap
return $this->generateCodeForArray(
$type,
$apiVersion,
$serializerGroups,
$fieldPath,
$modelPropertyPath,
$stack
);
}
switch ($type) {
case $type instanceof PropertyTypeDateTime:
if (null !== $type->getZone()) {
throw new \RuntimeException('Timezone support is not implemented');
}
$dateFormat = $type->getFormat() ?: DateTime::ATOM;
$dateFormat = $type->getFormat() ?: \DateTimeInterface::ISO8601;
return $this->templating->renderAssign(
$fieldPath,
@ -454,30 +331,19 @@ class SerializerGenerator
return $this->templating->renderAssign($fieldPath, $modelPropertyPath);
case $type instanceof PropertyTypeClass:
return $this->generateCodeForClass(
$type->getClassMetadata(),
$apiVersion,
$serializerGroups,
$fieldPath,
$modelPropertyPath,
$stack
);
return $this->generateCodeForClass($type->getClassMetadata(), $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack);
case $type instanceof PropertyTypeArray:
return $this->generateCodeForArray($type, $apiVersion, $serializerGroups, $fieldPath, $modelPropertyPath, $stack);
default:
throw new RuntimeException('Unexpected type ' . \get_class($type) . ' at ' . $modelPropertyPath);
throw new \Exception('Unexpected type '. get_class($type) .' at '.$modelPropertyPath);
}
}
/**
* @param \Liip\MetadataParser\Metadata\PropertyTypeArray $type
* @param string|null $apiVersion
* @param array<mixed> $serializerGroups
* @param string $arrayPath
* @param string $modelPath
* @param array<mixed> $stack
*
* @return string
* @throws \Exception
* @param list<string> $serializerGroups
* @param array<string, positive-int> $stack
*/
private function generateCodeForArray(
PropertyTypeArray $type,
@ -487,35 +353,25 @@ class SerializerGenerator
string $modelPath,
array $stack
): string {
$index = '$index' . \mb_strlen($arrayPath);
$index = '$index'.mb_strlen($arrayPath);
$subType = $type->getSubType();
switch ($subType) {
case $subType instanceof PropertyTypeArray:
$innerCode = $this->generateCodeForArray(
$subType,
$apiVersion,
$serializerGroups,
$arrayPath . '[' . $index . ']',
$modelPath . '[' . $index . ']',
$stack
);
break;
case $subType instanceof PropertyTypeClass:
$innerCode = $this->generateCodeForClass(
$subType->getClassMetadata(),
$apiVersion,
$serializerGroups,
$arrayPath . '[' . $index . ']',
$modelPath . '[' . $index . ']',
$stack
);
break;
case $subType instanceof PropertyTypePrimitive:
case $subType instanceof PropertyTypeArray && self::isArrayForPrimitive($subType):
case $subType instanceof PropertyTypeUnknown:
$innerCode = $this->templating->renderAssign($arrayPath, $modelPath);
return $this->templating->renderArrayAssign($arrayPath, $modelPath);
case $subType instanceof PropertyTypeArray:
$innerCode = $this->generateCodeForArray($subType, $apiVersion, $serializerGroups, $arrayPath.'['.$index.']', $modelPath.'['.$index.']', $stack);
break;
case $subType instanceof PropertyTypeClass:
$innerCode = $this->generateCodeForClass($subType->getClassMetadata(), $apiVersion, $serializerGroups, $arrayPath.'['.$index.']', $modelPath.'['.$index.']', $stack);
break;
default:
throw new RuntimeException('Unexpected array subtype ' . get_class($subType));
throw new \Exception('Unexpected array subtype '. get_class($subType));
}
if ('' === $innerCode) {
@ -532,4 +388,16 @@ class SerializerGenerator
return $this->templating->renderLoopArray($arrayPath, $modelPath, $index, $innerCode);
}
private static function isArrayForPrimitive(PropertyTypeArray $type): bool
{
do {
$type = $type->getSubType();
if ($type instanceof PropertyTypePrimitive) {
return true;
}
} while ($type instanceof PropertyTypeArray);
return false;
}
}

View file

@ -1,197 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category BaseJMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser;
use ReflectionClass;
use RetailCrm\Api\Component\Serializer\Exception\SyntaxError;
/**
* Class Parser
*
* @package RetailCrm\Api\Component\Serializer\Parser
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @internal
*/
final class BaseJMSParser
{
/**
* @var \RetailCrm\Api\Component\Serializer\Parser\JMSLexer
*/
private $lexer;
/**
* @var bool
*/
private $root = true;
/**
* @param string $string
*
* @return array|mixed[]
*/
public function parse(string $string): array
{
$this->lexer = new JMSLexer();
$this->lexer->setInput($string);
$this->lexer->moveNext();
return $this->visit();
}
/**
* @return mixed
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function visit()
{
$this->lexer->moveNext();
if (!$this->lexer->token) {
throw new SyntaxError(
'Syntax error, unexpected end of stream'
);
}
if (JMSLexer::T_FLOAT === $this->lexer->token['type']) {
return (float)$this->lexer->token['value'];
} elseif (JMSLexer::T_INTEGER === $this->lexer->token['type']) {
return (int)$this->lexer->token['value'];
} elseif (JMSLexer::T_NULL === $this->lexer->token['type']) {
return null;
} elseif (JMSLexer::T_STRING === $this->lexer->token['type']) {
return $this->lexer->token['value'];
} elseif (JMSLexer::T_IDENTIFIER === $this->lexer->token['type']) {
if ($this->lexer->isNextToken(JMSLexer::T_TYPE_START)) {
return $this->visitCompoundType();
} elseif ($this->lexer->isNextToken(JMSLexer::T_ARRAY_START)) {
return $this->visitArrayType();
}
return $this->visitSimpleType();
} elseif (!$this->root && JMSLexer::T_ARRAY_START === $this->lexer->token['type']) {
return $this->visitArrayType();
}
throw new SyntaxError(sprintf(
'Syntax error, unexpected "%s" (%s)',
$this->lexer->token['value'],
// @phpstan-ignore-next-line
$this->getConstant($this->lexer->token['type'])
));
}
/**
* @return mixed[]
*/
private function visitSimpleType(): array
{
$value = $this->lexer->token['value']; // @phpstan-ignore-line
return ['name' => $value, 'params' => []];
}
/**
* @return array<string, mixed>
*/
private function visitCompoundType(): array
{
$this->root = false;
$name = $this->lexer->token['value']; // @phpstan-ignore-line
$this->match(JMSLexer::T_TYPE_START);
$params = [];
if (!$this->lexer->isNextToken(JMSLexer::T_TYPE_END)) {
while (true) {
$params[] = $this->visit();
if ($this->lexer->isNextToken(JMSLexer::T_TYPE_END)) {
break;
}
$this->match(JMSLexer::T_COMMA);
}
}
$this->match(JMSLexer::T_TYPE_END);
return [
'name' => $name,
'params' => $params,
];
}
/**
* @return array<int, mixed>
*/
private function visitArrayType(): array
{
/*
* Here we should call $this->match(JMSLexer::T_ARRAY_START); to make it clean
* but the token has already been consumed by moveNext() in visit()
*/
$params = [];
if (!$this->lexer->isNextToken(JMSLexer::T_ARRAY_END)) {
while (true) {
$params[] = $this->visit();
if ($this->lexer->isNextToken(JMSLexer::T_ARRAY_END)) {
break;
}
$this->match(JMSLexer::T_COMMA);
}
}
$this->match(JMSLexer::T_ARRAY_END);
return $params;
}
/**
* @param int $token
*/
private function match(int $token): void
{
if (!$this->lexer->lookahead) {
throw new SyntaxError(
sprintf('Syntax error, unexpected end of stream, expected %s', $this->getConstant($token))
);
}
if ($this->lexer->lookahead['type'] === $token) {
$this->lexer->moveNext();
return;
}
throw new SyntaxError(sprintf(
'Syntax error, unexpected "%s" (%s), expected was %s',
$this->lexer->lookahead['value'],
$this->getConstant($this->lexer->lookahead['type']), // @phpstan-ignore-line
$this->getConstant($token)
));
}
/**
* @param int $value
*
* @return string
*/
private function getConstant(int $value): string
{
$oClass = new ReflectionClass(JMSLexer::class);
return (string) array_search($value, $oClass->getConstants());
}
}

View file

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Exception;
/**
* Base exception for the Serializer.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface Exception extends \Throwable
{
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Exception\Exception as BaseException;
interface Exception extends BaseException
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception;
final class InvalidNode extends \LogicException implements Exception
{
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception;
final class SyntaxError extends \RuntimeException implements Exception
{
}

View file

@ -1,24 +1,16 @@
<?php
/**
* PHP version 7.3
*
* @category BaseJMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser;
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type;
use Doctrine\Common\Lexer\AbstractLexer;
use RetailCrm\Api\Component\Serializer\Exception\SyntaxError;
use Throwable;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception\SyntaxError;
/**
* @internal
*/
final class JMSLexer extends AbstractLexer
final class Lexer extends AbstractLexer
{
public const T_UNKNOWN = 0;
public const T_INTEGER = 1;
@ -32,23 +24,15 @@ final class JMSLexer extends AbstractLexer
public const T_IDENTIFIER = 9;
public const T_NULL = 10;
/**
* @param string $type
*
* @return int|string|null
*/
public function parse(string $type)
{
try {
return $this->getType($type);
} catch (Throwable $e) {
} catch (\Throwable $e) {
throw new SyntaxError($e->getMessage(), 0, $e);
}
}
/**
* @return string[]
*/
protected function getCatchablePatterns(): array
{
return [
@ -63,18 +47,15 @@ final class JMSLexer extends AbstractLexer
];
}
/**
* @return string[]
*/
protected function getNonCatchablePatterns(): array
{
return ['\s+'];
}
/**
* {{@inheritDoc}}
* {@inheritDoc}
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @return int|string|null
*/
protected function getType(&$value)
{

View file

@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception\SyntaxError;
/**
* @internal
*/
final class Parser implements ParserInterface
{
/** @var \RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Lexer */
private $lexer;
/** @var \RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Token|null */
private $token = null;
/** @var string */
private $input;
/** @var bool */
private $root = true;
public function parse(string $type): array
{
$this->input = $type;
$this->lexer = new Lexer();
$this->lexer->setInput($type);
$this->lexer->moveNext();
return $this->visit();
}
/**
* @return mixed
*/
private function visit(bool $fetchingParam = false)
{
$this->lexer->moveNext();
if (!$this->lexer->token) {
throw new SyntaxError(
'Syntax error, unexpected end of stream',
);
}
if (is_array($this->lexer->token)) {
$this->token = Token::fromArray($this->lexer->token);
} else {
$this->token = Token::fromObject($this->lexer->token);
}
/*
* There is a difference in the behavior of the tokenizer depending on the version of the lexer. This affects
* how the lexer interprets date formats for compound date types. In our case it has a huge impact because
* we're using `DateTime<'Y-m-d H:i:s'>` in the API. Newer lexer versions will treat the whole format
* (`Y-m-d H:i:s`) as a token of string type. Older lexer versions will treat each symbol of the format as
* a single token, and this will inevitably break type parsing during model compilation (it will say something
* about unexpected token).
*
* The condition below is used to work around this, albeit in a very hacky way. The token it tries to detect is
* actually a single quote ('). After detecting this token and determining that this time parser has been
* called recursively for a compound type, it will try to determine the parameter length and extract it as
* a single token. In other words, we're trying to mimic newer lexer behavior while using an older version.
*
* This implementation is very limited, as it may fail abruptly with more complex compound types, also it
* may fail to extract the type correctly if it has different whitespace symbols / multiple whitespace symbols.
* It was tested only with the compound date type mentioned above.
*
* A special fallback parser would be a better solution, but it would take much more time and effort to create.
*/
if ("" === $this->token->value && $fetchingParam) {
$len = 0;
$this->lexer->moveNext();
while (true) {
if (is_array($this->lexer->token)) {
$this->token = Token::fromArray($this->lexer->token);
} else {
$this->token = Token::fromObject($this->lexer->token);
}
if ("" === $this->token->value) {
$len++;
break;
}
$len += strlen($this->token->value);
$this->lexer->moveNext();
}
return substr($this->input, 9, $len + substr_count($this->input, ' '));
}
if (Lexer::T_FLOAT === $this->token->type) {
return floatval($this->token->value);
} elseif (Lexer::T_INTEGER === $this->token->type) {
return intval($this->token->value);
} elseif (Lexer::T_NULL === $this->token->type) {
return null;
} elseif (Lexer::T_STRING === $this->token->type) {
return $this->token->value;
} elseif (Lexer::T_IDENTIFIER === $this->token->type) {
if ($this->lexer->isNextToken(Lexer::T_TYPE_START)) {
return $this->visitCompoundType();
} elseif ($this->lexer->isNextToken(Lexer::T_ARRAY_START)) {
return $this->visitArrayType();
}
return $this->visitSimpleType();
} elseif (!$this->root && Lexer::T_ARRAY_START === $this->token->type) {
return $this->visitArrayType();
}
throw new SyntaxError(sprintf(
'Syntax error, unexpected "%s" (%s)',
$this->token->value,
$this->getConstant($this->token->type),
));
}
/**
* @return string|mixed[]
*/
private function visitSimpleType()
{
$value = $this->token->value;
return ['name' => $value, 'params' => []];
}
private function visitCompoundType(): array
{
$this->root = false;
$name = $this->token->value;
$this->match(Lexer::T_TYPE_START);
$params = [];
if (!$this->lexer->isNextToken(Lexer::T_TYPE_END)) {
while (true) {
$params[] = $this->visit(true);
if ($this->lexer->isNextToken(Lexer::T_TYPE_END)) {
break;
}
$this->match(Lexer::T_COMMA);
}
}
$this->match(Lexer::T_TYPE_END);
return [
'name' => $name,
'params' => $params,
];
}
private function visitArrayType(): array
{
/*
* Here we should call $this->match(Lexer::T_ARRAY_START); to make it clean
* but the token has already been consumed by moveNext() in visit()
*/
$params = [];
if (!$this->lexer->isNextToken(Lexer::T_ARRAY_END)) {
while (true) {
$params[] = $this->visit();
if ($this->lexer->isNextToken(Lexer::T_ARRAY_END)) {
break;
}
$this->match(Lexer::T_COMMA);
}
}
$this->match(Lexer::T_ARRAY_END);
return $params;
}
private function match(int $token): void
{
if (!$this->lexer->lookahead) {
throw new SyntaxError(
sprintf('Syntax error, unexpected end of stream, expected %s', $this->getConstant($token)),
);
}
if (is_array($this->lexer->lookahead)) {
$lookahead = Token::fromArray($this->lexer->lookahead);
} else {
$lookahead = Token::fromObject($this->lexer->lookahead);
}
if ($lookahead->type === $token) {
$this->lexer->moveNext();
return;
}
throw new SyntaxError(sprintf(
'Syntax error, unexpected "%s" (%s), expected was %s',
$lookahead->value,
$this->getConstant($lookahead->type),
$this->getConstant($token),
));
}
private function getConstant(int $value): string
{
$oClass = new \ReflectionClass(Lexer::class);
return array_search($value, $oClass->getConstants());
}
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type;
interface ParserInterface
{
public function parse(string $type): array;
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type;
use function in_array;
/**
* @template T of string|int
* @template V of string|int
*/
final class Token
{
public static function fromArray(array $source): Token
{
return new self($source['value'] ?? '', $source['type'] ?? '', $source['position'] ?? '');
}
public static function fromObject(object $source): Token
{
return new self($source->value, $source->type, $source->position);
}
/**
* The string value of the token in the input string
*
* @readonly
* @var string|int
*/
public $value;
/**
* The type of the token (identifier, numeric, string, input parameter, none)
*
* @readonly
* @var T|null
*/
public $type;
/**
* The position of the token in the input string
*
* @var int
*
* @readonly
*/
public $position;
/**
* @param string|int $value
* @param string|int $type
*/
public function __construct($value, $type, int $position)
{
$this->value = $value;
$this->type = $type;
$this->position = $position;
}
/** @param T ...$types */
public function isA(...$types): bool
{
return in_array($this->type, $types, true);
}
}

View file

@ -1,21 +1,18 @@
<?php
/**
* PHP version 7.3
*
* @category JMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser;
use Doctrine\Common\Annotations\AnnotationException;
use Doctrine\Common\Annotations\Reader;
use Liip\MetadataParser\ModelParser\ModelParserInterface;
use RetailCrm\Api\Component\Serializer\Annotation\Accessor;
use RetailCrm\Api\Component\Serializer\Annotation\AccessorOrder;
use RetailCrm\Api\Component\Serializer\Annotation\Exclude;
use RetailCrm\Api\Component\Serializer\Annotation\ExclusionPolicy;
use RetailCrm\Api\Component\Serializer\Annotation\Groups;
use RetailCrm\Api\Component\Serializer\Annotation\MaxDepth;
use RetailCrm\Api\Component\Serializer\Annotation\PostDeserialize;
use RetailCrm\Api\Component\Serializer\Annotation\SerializedName;
use RetailCrm\Api\Component\Serializer\Annotation\Since;
@ -27,54 +24,32 @@ use Liip\MetadataParser\Exception\ParseException;
use Liip\MetadataParser\Metadata\PropertyAccessor;
use Liip\MetadataParser\Metadata\PropertyType;
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
use Liip\MetadataParser\ModelParser\ModelParserInterface;
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyCollection;
use Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata;
use Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata;
use Liip\MetadataParser\TypeParser\PhpTypeParser;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionProperty;
use UnexpectedValueException;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Exception\SyntaxError;
/**
* Class JMSParser
* Parse JMSSerializer annotations.
*
* Run this parser *after* the PHPDoc parser as JMS annotations are more precise.
*
* @category JMSParser
* @package RetailCrm\Api\Component\Serializer\Parser
* @license https://github.com/liip/metadata-parser/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/metadata-parser
* @internal
*
* @SuppressWarnings(PHPMD)
*/
class JMSParser implements ModelParserInterface
{
private const ACCESS_ORDER_CUSTOM = 'custom';
/**
* @var Reader
*/
/** @var \Doctrine\Common\Annotations\Reader */
private $annotationsReader;
/**
* @var PhpTypeParser
*/
/** @var \Liip\MetadataParser\TypeParser\PhpTypeParser */
private $phpTypeParser;
/**
* @var JMSTypeParser
*/
private $jmsTypeParser;
/** @var \RetailCrm\Api\Component\Serializer\Parser\JMSTypeParser */
protected $jmsTypeParser;
/**
* JMSParser constructor.
*
* @param \Doctrine\Common\Annotations\Reader $annotationsReader
*/
public function __construct(Reader $annotationsReader)
{
$this->annotationsReader = $annotationsReader;
@ -82,124 +57,99 @@ class JMSParser implements ModelParserInterface
$this->jmsTypeParser = new JMSTypeParser();
}
/**
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*/
public function parse(RawClassMetadata $classMetadata): void
{
try {
$refClass = new ReflectionClass($classMetadata->getClassName()); // @phpstan-ignore-line
} catch (ReflectionException $exception) {
throw ParseException::classNotFound($classMetadata->getClassName(), $exception);
$reflClass = new \ReflectionClass($classMetadata->getClassName());
} catch (\ReflectionException $e) {
throw ParseException::classNotFound($classMetadata->getClassName(), $e);
}
$this->parseProperties($refClass, $classMetadata);
$this->parseMethods($refClass, $classMetadata);
$this->parseClass($refClass, $classMetadata);
try {
$this->parseProperties($reflClass, $classMetadata);
$this->parseMethods($reflClass, $classMetadata);
$this->parseClass($reflClass, $classMetadata);
} catch (SyntaxError $exception) {
throw new ParseException($exception->getMessage(), $exception->getCode(), $exception);
}
}
/**
* @param \ReflectionClass $refClass
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*
* @phpstan-ignore-next-line
*/
private function parseProperties(ReflectionClass $refClass, RawClassMetadata $classMetadata): void
private function parseProperties(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
{
if ($refParentClass = $refClass->getParentClass()) {
$this->parseProperties($refParentClass, $classMetadata);
if ($reflParentClass = $reflClass->getParentClass()) {
$this->parseProperties($reflParentClass, $classMetadata);
}
foreach ($refClass->getProperties() as $refProperty) {
foreach ($reflClass->getProperties() as $reflProperty) {
try {
$annotations = $this->annotationsReader->getPropertyAnnotations($refProperty);
} catch (AnnotationException $exception) {
throw ParseException::propertyError((string) $classMetadata, $refProperty->getName(), $exception);
$annotations = $this->annotationsReader->getPropertyAnnotations($reflProperty);
} catch (AnnotationException $e) {
throw ParseException::propertyError((string) $classMetadata, $reflProperty->getName(), $e);
}
$property = $this->getProperty($classMetadata, $refProperty, $annotations);
$property = $this->getProperty($classMetadata, $reflProperty, $annotations);
$this->parsePropertyAnnotations($classMetadata, $property, $annotations);
}
}
/**
* @param \ReflectionClass $refClass
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*
* @phpstan-ignore-next-line
*/
private function parseMethods(ReflectionClass $refClass, RawClassMetadata $classMetadata): void
private function parseMethods(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
{
if ($refParentClass = $refClass->getParentClass()) {
$this->parseMethods($refParentClass, $classMetadata);
if ($reflParentClass = $reflClass->getParentClass()) {
$this->parseMethods($reflParentClass, $classMetadata);
}
foreach ($refClass->getMethods() as $refMethod) {
if (false === $refMethod->getDocComment()) {
continue;
}
foreach ($reflClass->getMethods() as $reflMethod) {
try {
$annotations = $this->annotationsReader->getMethodAnnotations($refMethod);
} catch (AnnotationException $exception) {
throw ParseException::propertyError((string) $classMetadata, $refMethod->getName(), $exception);
$annotations = $this->annotationsReader->getMethodAnnotations($reflMethod);
} catch (AnnotationException $e) {
throw ParseException::propertyError((string) $classMetadata, $reflMethod->getName(), $e);
}
if ($this->isVirtualProperty($annotations)) {
if (!$refMethod->isPublic()) {
throw ParseException::nonPublicMethod((string) $classMetadata, $refMethod->getName());
if (!$reflMethod->isPublic()) {
throw ParseException::nonPublicMethod((string) $classMetadata, $reflMethod->getName());
}
$methodName = $this->getMethodName($annotations, $refMethod);
$methodName = $this->getMethodName($annotations, $reflMethod);
$name = $this->getSerializedName($annotations) ?: $methodName;
$property = new PropertyVariationMetadata($methodName, true, true);
$classMetadata->addPropertyVariation($name, $property);
$property->setType($this->getReturnType($property, $refMethod, $refClass));
$property->setAccessor(new PropertyAccessor($refMethod->getName(), null));
$property->setType($this->getReturnType($property, $reflMethod, $reflClass));
$property->setAccessor(new PropertyAccessor($reflMethod->getName(), null));
$this->parsePropertyAnnotations($classMetadata, $property, $annotations);
}
if ($this->isPostDeserializeMethod($annotations)) {
if (!$refMethod->isPublic()) {
throw ParseException::nonPublicMethod((string) $classMetadata, $refMethod->getName());
if (!$reflMethod->isPublic()) {
throw ParseException::nonPublicMethod((string) $classMetadata, $reflMethod->getName());
}
$classMetadata->addPostDeserializeMethod($refMethod->getName());
$classMetadata->addPostDeserializeMethod($reflMethod->getName());
}
}
}
/**
* @param \ReflectionClass $refClass
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*
* @phpstan-ignore-next-line
*/
private function parseClass(ReflectionClass $refClass, RawClassMetadata $classMetadata): void
private function parseClass(\ReflectionClass $reflClass, RawClassMetadata $classMetadata): void
{
try {
$annotations = $this->gatherClassAnnotations($refClass);
$annotations = $this->gatherClassAnnotations($reflClass);
} catch (AnnotationException $e) {
throw ParseException::classError($refClass->getName(), $e);
throw ParseException::classError($reflClass->getName(), $e);
}
foreach ($annotations as $annotation) {
switch (true) {
case $annotation instanceof AccessorOrder:
if (self::ACCESS_ORDER_CUSTOM !== $annotation->order) {
throw ParseException::unsupportedClassAnnotation(
(string) $classMetadata,
'AccessorOrder::' . $annotation->order
);
throw ParseException::unsupportedClassAnnotation((string) $classMetadata, 'AccessorOrder::'.$annotation->order);
}
// usort is not stable for the same result. we want to preserve order of
// the fields that are not explicitly mentioned
// usort is not stable for the same result. we want to preserve order of the fields that are not explicitly mentioned
$order = [];
$init = count($annotation->custom);
$init = \count($annotation->custom);
foreach ($classMetadata->getPropertyCollections() as $property) {
$position = $property->getPosition($annotation->custom);
if (null === $position) {
@ -208,21 +158,17 @@ class JMSParser implements ModelParserInterface
$order[$property->getSerializedName()] = $position;
}
$classMetadata->sortProperties(static function (
PropertyCollection $propA,
PropertyCollection $propB
) use ($order): int {
$classMetadata->sortProperties(static function (PropertyCollection $propA, PropertyCollection $propB) use ($order): int {
return $order[$propA->getSerializedName()] <=> $order[$propB->getSerializedName()];
});
break;
case $annotation instanceof ExclusionPolicy:
if (ExclusionPolicy::NONE !== $annotation->policy) {
throw ParseException::unsupportedClassAnnotation(
(string) $classMetadata,
'ExclusionPolicy::' . $annotation->policy
);
throw ParseException::unsupportedClassAnnotation((string) $classMetadata, 'ExclusionPolicy::'.$annotation->policy);
}
break;
default:
if (
0 === strncmp(
@ -232,10 +178,7 @@ class JMSParser implements ModelParserInterface
)
) {
// if there are annotations we can safely ignore, we need to explicitly ignore them
throw ParseException::unsupportedClassAnnotation(
(string) $classMetadata,
get_class($annotation)
);
throw ParseException::unsupportedClassAnnotation((string) $classMetadata, \get_class($annotation));
}
}
}
@ -244,51 +187,36 @@ class JMSParser implements ModelParserInterface
/**
* Find the annotations we care about by looking through all ancestors of $reflectionClass.
*
* @param \ReflectionClass $reflectionClass
*
* @return object[] Hashmap of annotation class => annotation object
* @throws \Doctrine\Common\Annotations\AnnotationException
*
* @phpstan-ignore-next-line
* @throws AnnotationException
*/
private function gatherClassAnnotations(ReflectionClass $reflectionClass): array
private function gatherClassAnnotations(\ReflectionClass $reflectionClass): array
{
$map = [];
if ($parent = $reflectionClass->getParentClass()) {
$map = $this->gatherClassAnnotations($parent);
}
$annotations = $this->annotationsReader->getClassAnnotations($reflectionClass);
foreach ($annotations as $annotation) {
$map[get_class($annotation)] = $annotation;
$map[\get_class($annotation)] = $annotation;
}
return $map;
}
/**
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
* @param \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata $property
* @param array<mixed> $annotations
*/
private function parsePropertyAnnotations(
RawClassMetadata $classMetadata,
PropertyVariationMetadata $property,
array $annotations
): void {
private function parsePropertyAnnotations(RawClassMetadata $classMetadata, PropertyVariationMetadata $property, array $annotations): void
{
foreach ($annotations as $annotation) {
switch (true) {
case $annotation instanceof Type:
if (null === $annotation->name) {
throw ParseException::propertyTypeNameNull((string) $classMetadata, (string) $property);
}
try {
$type = $this->jmsTypeParser->parse($annotation->name);
} catch (InvalidTypeException $exception) {
throw ParseException::propertyTypeError(
(string) $classMetadata,
(string) $property,
$exception
);
} catch (InvalidTypeException $e) {
throw ParseException::propertyTypeError((string) $classMetadata, (string) $property, $e);
}
if ($property->getType() instanceof PropertyTypeUnknown) {
@ -296,52 +224,49 @@ class JMSParser implements ModelParserInterface
} else {
try {
$property->setType($property->getType()->merge($type));
} catch (UnexpectedValueException $exception) {
throw ParseException::propertyTypeConflict(
(string) $classMetadata,
(string) $property,
(string) $property->getType(),
(string) $type,
$exception
);
} catch (\UnexpectedValueException $e) {
throw ParseException::propertyTypeConflict((string) $classMetadata, (string) $property, (string) $property->getType(), (string) $type, $e);
}
}
break;
case $annotation instanceof Exclude:
if (null !== $annotation->if) {
throw ParseException::unsupportedPropertyAnnotation(
(string) $classMetadata,
(string) $property,
'Exclude::if'
);
throw ParseException::unsupportedPropertyAnnotation((string) $classMetadata, (string) $property, 'Exclude::if');
}
$classMetadata->removePropertyVariation((string) $property);
break;
case $annotation instanceof Groups:
$property->setGroups($annotation->groups);
break;
case $annotation instanceof Accessor:
$property->setAccessor(new PropertyAccessor($annotation->getter, $annotation->setter));
break;
case $annotation instanceof Since:
$property->setVersionRange($property->getVersionRange()->withSince($annotation->version));
break;
case $annotation instanceof Until:
$property->setVersionRange($property->getVersionRange()->withUntil($annotation->version));
break;
case $annotation instanceof SerializedName:
// we handle this separately
case $annotation instanceof MaxDepth:
$property->setMaxDepth($annotation->depth);
break;
case $annotation instanceof VirtualProperty:
// we handle this separately
case $annotation instanceof SerializedName:
// we handle this separately
break;
default:
if (0 === strncmp('JMS\Serializer\\', get_class($annotation), mb_strlen('JMS\Serializer\\'))) {
if (0 === strncmp('JMS\Serializer\\', \get_class($annotation), mb_strlen('JMS\Serializer\\'))) {
// if there are annotations we can safely ignore, we need to explicitly ignore them
throw ParseException::unsupportedPropertyAnnotation(
(string) $classMetadata,
(string) $property,
get_class($annotation)
);
throw ParseException::unsupportedPropertyAnnotation((string) $classMetadata, (string) $property, \get_class($annotation));
}
break;
}
@ -352,96 +277,38 @@ class JMSParser implements ModelParserInterface
* Returns the property metadata for the specified property.
*
* If the property already exists on the class metadata this is returned.
* If the property has a serialized name that overrides the name of an existing property,
* it will be renamed and merged.
*
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
* @param \ReflectionProperty $refProperty
* @param array<mixed> $annotations
*
* @return \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata
* @throws \ReflectionException
* If the property has a serialized name that overrides the name of an existing property, it will be renamed and merged.
*/
private function getProperty(
RawClassMetadata $classMetadata,
ReflectionProperty $refProperty,
array $annotations
): PropertyVariationMetadata {
$defaultName = PropertyCollection::serializedName($refProperty->getName());
private function getProperty(RawClassMetadata $classMetadata, \ReflectionProperty $reflProperty, array $annotations): PropertyVariationMetadata
{
$defaultName = PropertyCollection::serializedName($reflProperty->getName());
$name = $this->getSerializedName($annotations) ?: $defaultName;
if ($classMetadata->hasPropertyVariation($refProperty->getName())) {
$property = $classMetadata->getPropertyVariation($refProperty->getName());
if ($classMetadata->hasPropertyVariation($reflProperty->getName())) {
$property = $classMetadata->getPropertyVariation($reflProperty->getName());
if ($defaultName !== $name && $classMetadata->hasPropertyCollection($defaultName)) {
$classMetadata->removePropertyVariation($defaultName);
$this->addPropertyVariation($defaultName, $name, $property, $classMetadata);
$classMetadata->renameProperty($defaultName, $name);
}
} else {
$property = PropertyVariationMetadata::fromReflection($refProperty);
$this->addPropertyVariation($defaultName, $name, $property, $classMetadata);
$property = PropertyVariationMetadata::fromReflection($reflProperty);
$classMetadata->addPropertyVariation($name, $property);
}
return $property;
}
/**
* This workaround helps to avoid unnecessary camelCase to snake_case conversion while
* using default property metadata classes. This allows us to produce code we expect
* without rewriting the whole metadata parsing library.
*
* @param string $defaultName
* @param string $name
* @param \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata $property
* @param \Liip\MetadataParser\ModelParser\RawMetadata\RawClassMetadata $classMetadata
*
* @throws \ReflectionException
*/
private function addPropertyVariation(
string $defaultName,
string $name,
PropertyVariationMetadata $property,
RawClassMetadata $classMetadata
): void {
if ($classMetadata->hasPropertyCollection($defaultName)) {
$prop = $classMetadata->getPropertyCollection($defaultName);
} else {
$prop = new PropertyCollection($name);
$classMetadata->addPropertyCollection($prop);
}
$propName = new ReflectionProperty(get_class($prop), 'serializedName');
$propName->setAccessible(true);
$propName->setValue($prop, $name);
$prop->addVariation($property);
}
/**
* @param \Liip\MetadataParser\ModelParser\RawMetadata\PropertyVariationMetadata $property
* @param \ReflectionMethod $refMethod
* @param \ReflectionClass $refClass
*
* @return \Liip\MetadataParser\Metadata\PropertyType
*
* @phpstan-ignore-next-line
*/
private function getReturnType(
PropertyVariationMetadata $property,
ReflectionMethod $refMethod,
ReflectionClass $refClass
): PropertyType {
private function getReturnType(PropertyVariationMetadata $property, \ReflectionMethod $reflMethod, \ReflectionClass $reflClass): PropertyType
{
$type = new PropertyTypeUnknown(true);
$refType = $refMethod->getReturnType();
if (null !== $refType) {
$type = $this->phpTypeParser->parseReflectionType($refType);
$reflType = $reflMethod->getReturnType();
if (null !== $reflType) {
$type = $this->phpTypeParser->parseReflectionType($reflType);
}
try {
$docBlockType = $this->getReturnTypeOfMethod($refMethod, $refClass);
} catch (InvalidTypeException $exception) {
throw ParseException::propertyTypeError($refClass->getName(), (string) $property, $exception);
$docBlockType = $this->getReturnTypeOfMethod($reflMethod);
} catch (InvalidTypeException $e) {
throw ParseException::propertyTypeError($reflClass->getName(), (string) $property, $e);
}
if (null === $docBlockType) {
@ -450,47 +317,27 @@ class JMSParser implements ModelParserInterface
try {
return $type->merge($docBlockType);
} catch (UnexpectedValueException $exception) {
throw ParseException::propertyTypeConflict(
$refClass->getName(),
(string) $property,
(string) $type,
(string) $docBlockType,
$exception
);
} catch (\UnexpectedValueException $e) {
throw ParseException::propertyTypeConflict($reflClass->getName(), (string) $property, (string) $type, (string) $docBlockType, $e);
}
}
/**
* @param \ReflectionMethod $refMethod
* @param \ReflectionClass $refClass
*
* @return \Liip\MetadataParser\Metadata\PropertyType|null
*
* @phpstan-ignore-next-line
*/
private function getReturnTypeOfMethod(ReflectionMethod $refMethod, ReflectionClass $refClass): ?PropertyType
private function getReturnTypeOfMethod(\ReflectionMethod $reflMethod): ?PropertyType
{
$docComment = $refMethod->getDocComment();
$docComment = $reflMethod->getDocComment();
if (false === $docComment) {
return null;
}
foreach (explode("\n", $docComment) as $line) {
if (1 === preg_match('/@return ([^ ]+)/', $line, $matches)) {
return $this->phpTypeParser->parseAnnotationType($matches[1], $refClass);
return $this->phpTypeParser->parseAnnotationType($matches[1], $reflMethod->getDeclaringClass());
}
}
return null;
}
/**
* @param array<mixed> $annotations
*
* @return string|null
*/
private function getSerializedName(array $annotations): ?string
{
foreach ($annotations as $annotation) {
@ -502,11 +349,6 @@ class JMSParser implements ModelParserInterface
return null;
}
/**
* @param array<mixed> $annotations
*
* @return bool
*/
private function isVirtualProperty(array $annotations): bool
{
foreach ($annotations as $annotation) {
@ -518,11 +360,6 @@ class JMSParser implements ModelParserInterface
return false;
}
/**
* @param array<mixed> $annotations
*
* @return bool
*/
private function isPostDeserializeMethod(array $annotations): bool
{
foreach ($annotations as $annotation) {
@ -534,16 +371,9 @@ class JMSParser implements ModelParserInterface
return false;
}
/**
* @param array<mixed> $annotations
* @param \ReflectionMethod $refMethod
*
* @return string
*/
private function getMethodName(array $annotations, ReflectionMethod $refMethod): string
private function getMethodName(array $annotations, \ReflectionMethod $reflMethod): string
{
$name = $refMethod->getName();
$name = $reflMethod->getName();
foreach ($annotations as $annotation) {
if ($annotation instanceof VirtualProperty && null !== $annotation->name) {
$name = $annotation->name;

View file

@ -1,59 +1,42 @@
<?php
/**
* PHP version 7.3
*
* @category JMSTypeParser
* @package RetailCrm\Api\Component\Serializer\Parser
*/
declare(strict_types=1);
namespace RetailCrm\Api\Component\Serializer\Parser;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Liip\MetadataParser\Exception\InvalidTypeException;
use Liip\MetadataParser\Metadata\AbstractPropertyType;
use Liip\MetadataParser\Metadata\DateTimeOptions;
use Liip\MetadataParser\Metadata\PropertyType;
use Liip\MetadataParser\Metadata\PropertyTypeArray;
use Liip\MetadataParser\Metadata\PropertyTypeClass;
use Liip\MetadataParser\Metadata\PropertyTypeDateTime;
use Liip\MetadataParser\Metadata\PropertyTypeIterable;
use Liip\MetadataParser\Metadata\PropertyTypeArray;
use Liip\MetadataParser\Metadata\PropertyTypePrimitive;
use Liip\MetadataParser\Metadata\PropertyTypeUnknown;
use RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Parser;
use RetailCrm\Api\Component\Serializer\Type\PropertyTypeMixed;
/**
* Class JMSTypeParser
*
* @category JMSTypeParser
* @package RetailCrm\Api\Component\Serializer\Parser
* @license https://github.com/liip/metadata-parser/blob/master/LICENSE MIT License
* @author Liip <https://github.com/liip>
* @author Pavel Kovalenko
* @see https://github.com/liip/metadata-parser
* @internal
*
* @SuppressWarnings(PHPMD)
*/
class JMSTypeParser
final class JMSTypeParser
{
private const TYPE_ARRAY = 'array';
private const TYPE_ARRAY_COLLECTION = 'ArrayCollection';
private const TYPE_DATETIME_INTERFACE = 'DateTimeInterface';
/**
* @var \RetailCrm\Api\Component\Serializer\Parser\BaseJMSParser
*/
/** @var \RetailCrm\Api\Component\Serializer\Parser\JMSCore\Type\Parser */
private $jmsTypeParser;
/**
* JMSTypeParser constructor.
*/
private $useArrayDateFormat;
public function __construct()
{
$this->jmsTypeParser = new BaseJMSParser();
$this->jmsTypeParser = new Parser();
$this->useArrayDateFormat = null === (new \ReflectionClass(DateTimeOptions::class))
->getConstructor()->getParameters()[2]->getType();
}
/**
* @param string $rawType
*
* @return \Liip\MetadataParser\Metadata\PropertyType
*/
public function parse(string $rawType): PropertyType
{
if ('' === $rawType) {
@ -63,12 +46,6 @@ class JMSTypeParser
return $this->parseType($this->jmsTypeParser->parse($rawType));
}
/**
* @param array<mixed> $typeInfo
* @param bool $isSubType
*
* @return \Liip\MetadataParser\Metadata\PropertyType
*/
private function parseType(array $typeInfo, bool $isSubType = false): PropertyType
{
$typeInfo = array_merge(
@ -84,13 +61,12 @@ class JMSTypeParser
if (0 === \count($typeInfo['params'])) {
if (self::TYPE_ARRAY === $typeInfo['name']) {
return new PropertyTypeArray(new PropertyTypeUnknown(false), false, $nullable);
return self::iterableType(new PropertyTypeUnknown(false), false, $nullable);
}
if (PropertyTypePrimitive::isTypePrimitive($typeInfo['name'])) {
return new PropertyTypePrimitive($typeInfo['name'], $nullable);
}
if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name'])) {
return PropertyTypeDateTime::fromDateTimeClass($typeInfo['name'], $nullable);
}
@ -102,41 +78,62 @@ class JMSTypeParser
return new PropertyTypeClass($typeInfo['name'], $nullable);
}
if (self::TYPE_ARRAY === $typeInfo['name']) {
$collectionClass = $this->getCollectionClass($typeInfo['name']);
if (self::TYPE_ARRAY === $typeInfo['name'] || $collectionClass) {
if (1 === \count($typeInfo['params'])) {
return new PropertyTypeArray(
$this->parseType($typeInfo['params'][0], true),
false,
$nullable
);
return self::iterableType($this->parseType($typeInfo['params'][0], true), false, $nullable, $collectionClass);
}
if (2 === \count($typeInfo['params'])) {
return new PropertyTypeArray(
$this->parseType($typeInfo['params'][1], true),
true,
$nullable
);
return self::iterableType($this->parseType($typeInfo['params'][1], true), true, $nullable, $collectionClass);
}
throw new InvalidTypeException(sprintf(
'JMS property type array can\'t have more than 2 parameters (%s)',
var_export($typeInfo, true)
));
throw new InvalidTypeException(sprintf('JMS property type array can\'t have more than 2 parameters (%s)', var_export($typeInfo, true)));
}
if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name'])) {
if (PropertyTypeDateTime::isTypeDateTime($typeInfo['name']) || (self::TYPE_DATETIME_INTERFACE === $typeInfo['name'])) {
// the case of datetime without params is already handled above, we know we have params
$serializeFormat = $typeInfo['params'][0] ?: null;
// {@link \JMS\Serializer\Handler\DateHandler} of jms/serializer defaults to using the serialization format as a deserialization format if none was supplied...
$deserializeFormats = ($typeInfo['params'][2] ?? null) ?: $serializeFormat;
// ... and always converts single strings to arrays
$deserializeFormats = \is_string($deserializeFormats) ? [$deserializeFormats] : $deserializeFormats;
// Jms defaults to DateTime when given DateTimeInterface despite the documentation saying DateTimeImmutable, {@see \JMS\Serializer\Handler\DateHandler} in jms/serializer
$className = (self::TYPE_DATETIME_INTERFACE === $typeInfo['name']) ? \DateTime::class : $typeInfo['name'];
return PropertyTypeDateTime::fromDateTimeClass(
$typeInfo['name'],
$className,
$nullable,
new DateTimeOptions(
$typeInfo['params'][0] ?: null,
$serializeFormat,
($typeInfo['params'][1] ?? null) ?: null,
($typeInfo['params'][2] ?? null) ?: null
$this->useArrayDateFormat ? $deserializeFormats : $deserializeFormats[0],
)
);
}
throw new InvalidTypeException(sprintf('Unknown JMS property found (%s)', var_export($typeInfo, true)));
}
private function getCollectionClass(string $name): ?string
{
switch ($name) {
case self::TYPE_ARRAY_COLLECTION:
return ArrayCollection::class;
default:
return is_a($name, Collection::class, true) ? $name : null;
}
}
private static function iterableType(
PropertyType $subType,
bool $hashmap,
bool $nullable,
?string $collectionClass = null
): AbstractPropertyType {
if (class_exists('Liip\MetadataParser\Metadata\PropertyTypeIterable')) {
return new PropertyTypeIterable($subType, $hashmap, $nullable, $collectionClass);
}
return new PropertyTypeArray($subType, $hashmap, $nullable, $collectionClass !== null);
}
}

22
src/Enum/BySite.php Normal file
View file

@ -0,0 +1,22 @@
<?php
/**
* PHP version 7.3
*
* @category BySite
* @package RetailCrm\Api\Enum
*/
namespace RetailCrm\Api\Enum;
/**
* Class BySite
*
* @category BySite
* @package RetailCrm\Api\Enum
*/
final class BySite
{
public const ID = 'id';
public const CODE = 'code';
}

View file

@ -285,6 +285,7 @@ class ClientFactory implements ClientFactoryInterface, EventDispatcherAwareInter
->setStreamFactory($this->streamFactory)
->setRequestFactory($this->requestFactory)
->setUriFactory($this->uriFactory)
->setEventDispatcher($this->eventDispatcher)
->appendRequestHandlers($this->requestHandlers)
->appendResponseHandlers($this->responseHandlers)
->build();

View file

@ -0,0 +1,86 @@
<?php
/**
* PHP version 7.3
*
* @category Cart
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
namespace RetailCrm\Api\Model\Entity\CustomerInteraction;
use DateTime;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class Cart
*
* @category Cart
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
class Cart
{
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("currency")
*/
public $currency;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("externalId")
*/
public $externalId;
/**
* @var DateTime
*
* @JMS\Type("DateTime<'Y-m-d H:i:sP'>")
* @JMS\SerializedName("droppedAt")
*/
public $droppedAt;
/**
* @var DateTime
*
* @JMS\Type("DateTime<'Y-m-d H:i:sP'>")
* @JMS\SerializedName("clearedAt")
*/
public $clearedAt;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("link")
*/
public $link;
/**
* @var \RetailCrm\Api\Model\Entity\CustomerInteraction\CartCustomer
*
* @JMS\Type("RetailCrm\Api\Model\Entity\CustomerInteraction\CartCustomer")
* @JMS\SerializedName("customer")
*/
public $customer;
/**
* @var \RetailCrm\Api\Model\Entity\CustomerInteraction\CartItem[]
*
* @JMS\Type("array<RetailCrm\Api\Model\Entity\CustomerInteraction\CartItem>")
* @JMS\SerializedName("items")
*/
public $items;
/**
* @var \RetailCrm\Api\Model\Entity\CustomerInteraction\CartOrder
*
* @JMS\Type("RetailCrm\Api\Model\Entity\CustomerInteraction\CartOrder")
* @JMS\SerializedName("order")
*/
public $order;
}

View file

@ -0,0 +1,61 @@
<?php
/**
* PHP version 7.3
*
* @category CartCustomer
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
namespace RetailCrm\Api\Model\Entity\CustomerInteraction;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class CartCustomer
*
* @category CartCustomer
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
class CartCustomer
{
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("id")
*/
public $id;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("externalId")
*/
public $externalId;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("browserId")
*/
public $browserId;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("site")
*/
public $site;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("gaClientId")
*/
public $gaClientId;
}

View file

@ -0,0 +1,53 @@
<?php
/**
* PHP version 7.3
*
* @category CartItem
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
namespace RetailCrm\Api\Model\Entity\CustomerInteraction;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class CartItem
*
* @category CartItem
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
class CartItem
{
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("id")
*/
public $id;
/**
* @var \RetailCrm\Api\Model\Entity\Orders\Items\Offer
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Orders\Items\Offer")
* @JMS\SerializedName("offer")
*/
public $offer;
/**
* @var float
*
* @JMS\Type("float")
* @JMS\SerializedName("quantity")
*/
public $quantity;
/**
* @var float
*
* @JMS\Type("float")
* @JMS\SerializedName("price")
*/
public $price;
}

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.3
*
* @category CartOrder
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
namespace RetailCrm\Api\Model\Entity\CustomerInteraction;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class CartOrder
*
* @category CartOrder
* @package RetailCrm\Api\Model\Entity\CustomerInteraction
*/
class CartOrder
{
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("id")
*/
public $id;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("externalId")
*/
public $externalId;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("number")
*/
public $number;
}

View file

@ -369,6 +369,14 @@ class Customer implements CustomerInterface
*/
public $subscribed;
/**
* @var \RetailCrm\Api\Model\Entity\Customers\CustomerSubscription[]
*
* @JMS\Type("array<RetailCrm\Api\Model\Entity\Customers\CustomerSubscription>")
* @JMS\SerializedName("customerSubscriptions")
*/
public $customerSubscriptions;
/**
* @var \RetailCrm\Api\Model\Entity\Customers\MGCustomer[]
*

View file

@ -0,0 +1,46 @@
<?php
/**
* PHP version 7.3
*
* @category CustomerSubscription
* @package RetailCrm\Api\Model\Entity\Customers
*/
namespace RetailCrm\Api\Model\Entity\Customers;
use DateTime;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class CustomerSubscription
*
* @category CustomerSubscription
* @package RetailCrm\Api\Model\Entity\Customers
*/
class CustomerSubscription
{
/**
* @var \RetailCrm\Api\Model\Entity\Customers\SubscriptionCategory
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Customers\SubscriptionCategory")
* @JMS\SerializedName("subscription")
*/
public $subscription;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("subscribed")
*/
public $subscribed;
/**
* @var DateTime
*
* @JMS\Type("DateTime<'Y-m-d H:i:s'>")
* @JMS\SerializedName("changedAt")
*/
public $changedAt;
}

View file

@ -0,0 +1,53 @@
<?php
/**
* PHP version 7.3
*
* @category Subscription
* @package RetailCrm\Api\Model\Entity\Customers
*/
namespace RetailCrm\Api\Model\Entity\Customers;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class Subscription
*
* @category Subscription
* @package RetailCrm\Api\Model\Entity\Customers
*/
class Subscription
{
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("channel")
*/
public $channel;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("subscription")
*/
public $subscription;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("active")
*/
public $active;
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("messageId")
*/
public $messageId;
}

View file

@ -0,0 +1,77 @@
<?php
/**
* PHP version 7.3
*
* @category SubscriptionCategory
* @package RetailCrm\Api\Model\Entity\Customers
*/
namespace RetailCrm\Api\Model\Entity\Customers;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class SubscriptionCategory
*
* @category SubscriptionCategory
* @package RetailCrm\Api\Model\Entity\Customers
*/
class SubscriptionCategory
{
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("id")
*/
public $id;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("channel")
*/
public $channel;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("name")
*/
public $name;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("code")
*/
public $code;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("active")
*/
public $active;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("autoSubscribe")
*/
public $autoSubscribe;
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("ordering")
*/
public $ordering;
}

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.3
*
* @category EmbedJsConfiguration
* @package RetailCrm\Api\Model\Entity\Integration\EmbedJs
*/
namespace RetailCrm\Api\Model\Entity\Integration\EmbedJs;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class EmbedJsConfiguration
*
* @category EmbedJsConfiguration
* @package RetailCrm\Api\Model\Entity\Integration\EmbedJs
*/
class EmbedJsConfiguration
{
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("entrypoint")
*/
public $entrypoint;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("stylesheet")
*/
public $stylesheet;
/**
* @var string[]
*
* @JMS\Type("array<string>")
* @JMS\SerializedName("targets")
*/
public $targets;
}

View file

@ -74,4 +74,12 @@ class Integrations
* @JMS\SerializedName("mgBot")
*/
public $mgBot;
/**
* @var \RetailCrm\Api\Model\Entity\Integration\EmbedJs\EmbedJsConfiguration
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Integration\EmbedJs\EmbedJsConfiguration")
* @JMS\SerializedName("embedJs")
*/
public $embedJs;
}

View file

@ -43,6 +43,14 @@ class ApiUpdateInvoiceRequest
*/
public $amount;
/**
* @var float
*
* @JMS\Type("float")
* @JMS\SerializedName("discountAmount")
*/
public $discountAmount;
/**
* @var string
*

View file

@ -16,6 +16,8 @@ use RetailCrm\Api\Component\Serializer\Annotation as JMS;
*
* @category Currency
* @package RetailCrm\Api\Model\Entity\References
*
* @SuppressWarnings(PHPMD.LongVariable)
*/
class Currency
{

View file

@ -97,7 +97,7 @@ class LegalEntity
/**
* @var DateTime
*
* @JMS\Type("DateTime<'Y-m-d H:i:s'>")
* @JMS\Type("DateTime<'Y-m-d'>")
* @JMS\SerializedName("certificateDate")
*/
public $certificateDate;

View file

@ -91,6 +91,14 @@ class Store
*/
public $phone;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("contact")
*/
public $contact;
/**
* @var string
*

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.3
*
* @category Feature
* @package RetailCrm\Api\Model\Entity\Settings
*/
namespace RetailCrm\Api\Model\Entity\Settings;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class ChannelSetting
*
* @category ChannelSetting
* @package RetailCrm\Api\Model\Entity\Settings
*/
class ChannelSetting
{
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("site")
*/
public $site;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("order_type")
*/
public $orderType;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("order_method")
*/
public $orderMethod;
}

View file

@ -0,0 +1,37 @@
<?php
/**
* PHP version 7.3
*
* @category Feature
* @package RetailCrm\Api\Model\Entity\Settings
*/
namespace RetailCrm\Api\Model\Entity\Settings;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class MgOrderCreationSettings
*
* @category MgOrderCreationSettings
* @package RetailCrm\Api\Model\Entity\Settings
*/
class MgOrderCreationSettings
{
/**
* @var \RetailCrm\Api\Model\Entity\Settings\ChannelSetting
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Settings\ChannelSetting")
* @JMS\SerializedName("default")
*/
public $default;
/**
* @var array<int, \RetailCrm\Api\Model\Entity\Settings\ChannelSetting>
*
* @JMS\Type("array<int, RetailCrm\Api\Model\Entity\Settings\ChannelSetting>")
* @JMS\SerializedName("channels")
*/
public $channels;
}

View file

@ -0,0 +1,29 @@
<?php
/**
* PHP version 7.3
*
* @category Feature
* @package RetailCrm\Api\Model\Entity\Settings
*/
namespace RetailCrm\Api\Model\Entity\Settings;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class MgSettings
*
* @category MgSettings
* @package RetailCrm\Api\Model\Entity\Settings
*/
class MgSettings
{
/**
* @var \RetailCrm\Api\Model\Entity\Settings\MgOrderCreationSettings
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Settings\MgOrderCreationSettings")
* @JMS\SerializedName("order_creation")
*/
public $orderCreation;
}

View file

@ -58,4 +58,12 @@ class Settings
* @JMS\SerializedName("non_working_days")
*/
public $nonWorkingDays;
/**
* @var \RetailCrm\Api\Model\Entity\Settings\MgSettings
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Settings\MgSettings")
* @JMS\SerializedName("mg")
*/
public $mgSettings;
}

View file

@ -0,0 +1,190 @@
<?php
/**
* PHP version 7.3
*
* @category OfferProduct
* @package RetailCrm\Api\Model\Entity\Store
*/
namespace RetailCrm\Api\Model\Entity\Store;
use DateTime;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class Product
*
* @category OfferProduct
* @package RetailCrm\Api\Model\Entity\Store
*/
class OfferProduct
{
/**
* @var float
*
* @JMS\Type("float")
* @JMS\SerializedName("minPrice")
*/
public $minPrice;
/**
* @var float
*
* @JMS\Type("float")
* @JMS\SerializedName("maxPrice")
*/
public $maxPrice;
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("id")
*/
public $id;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("article")
*/
public $article;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("name")
*/
public $name;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("url")
*/
public $url;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("imageUrl")
*/
public $imageUrl;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("description")
*/
public $description;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("popular")
*/
public $popular;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("stock")
*/
public $stock;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("novelty")
*/
public $novelty;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("recommended")
*/
public $recommended;
/**
* @var array<string, mixed>
*
* @JMS\Type("array")
* @JMS\SerializedName("properties")
*/
public $properties;
/**
* @var \RetailCrm\Api\Model\Entity\FixExternalRow[]
*
* @JMS\Type("array<RetailCrm\Api\Model\Entity\FixExternalRow>")
* @JMS\SerializedName("groups")
*/
public $groups;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("externalId")
*/
public $externalId;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("manufacturer")
*/
public $manufacturer;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("active")
*/
public $active;
/**
* @var float
*
* @JMS\Type("float")
* @JMS\SerializedName("quantity")
*/
public $quantity;
/**
* @var bool
*
* @JMS\Type("bool")
* @JMS\SerializedName("markable")
*/
public $markable;
/**
* @var DateTime
*
* @JMS\Type("DateTime<'Y-m-d H:i:s'>")
* @JMS\SerializedName("updatedAt")
*/
public $updatedAt;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("type")
*/
public $type;
}

View file

@ -36,6 +36,14 @@ class Product
*/
public $maxPrice;
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("catalogId")
*/
public $catalogId;
/**
* @var int
*

View file

@ -66,4 +66,12 @@ class ProductGroup
* @JMS\SerializedName("active")
*/
public $active;
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("lvl")
*/
public $lvl;
}

View file

@ -170,4 +170,12 @@ class ProductOffer
* @JMS\SerializedName("barcode")
*/
public $barcode;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("site")
*/
public $site;
}

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.3
*
* @category ProductPropertyValue
* @package RetailCrm\Api\Model\Entity\Store
*/
namespace RetailCrm\Api\Model\Entity\Store;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class ProductPropertyValue
*
* @category ProductPropertyValue
* @package RetailCrm\Api\Model\Entity\Store
*/
class ProductPropertyValue
{
/**
* @var \RetailCrm\Api\Model\Entity\Store\ProductProperty
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Store\ProductProperty")
* @JMS\SerializedName("property")
*/
public $property;
/**
* @var string
*
* @JMS\Type("string")
* @JMS\SerializedName("value")
*/
public $value;
/**
* @var int
*
* @JMS\Type("int")
* @JMS\SerializedName("offersCount")
*/
public $offersCount;
}

View file

@ -0,0 +1,34 @@
<?php
/**
* PHP version 7.3
*
* @category StoreOffer
* @package RetailCrm\Api\Model\Entity\Store
*/
namespace RetailCrm\Api\Model\Entity\Store;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
/**
* Class StoreOffer
*
* @category StoreOffer
* @package RetailCrm\Api\Model\Entity\Store
*/
class StoreOffer extends ProductOffer
{
/**
* @JMS\Exclude()
*/
public $price;
/**
* @var \RetailCrm\Api\Model\Entity\Store\Product
*
* @JMS\Type("RetailCrm\Api\Model\Entity\Store\Product")
* @JMS\SerializedName("product")
*/
public $product;
}

View file

@ -494,4 +494,12 @@ class CustomerFilter
* @Form\SerializedName("attachedTags")
*/
public $attachedTags;
/**
* @var \RetailCrm\Api\Model\Filter\Customers\CustomerSubscriptionFilter[]
*
* @Form\Type("array<RetailCrm\Api\Model\Filter\Customers\CustomerSubscriptionFilter>")
* @Form\SerializedName("subscriptions")
*/
public $subscriptions;
}

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.3
*
* @category CustomerSubscriptionFilter
* @package RetailCrm\Api\Model\Filter\Customers
*/
namespace RetailCrm\Api\Model\Filter\Customers;
use RetailCrm\Api\Component\FormData\Mapping as Form;
/**
* Class CustomerSubscriptionFilter
*
* @category CustomerSubscriptionFilter
* @package RetailCrm\Api\Model\Filter\Customers
*/
class CustomerSubscriptionFilter
{
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("channel")
*/
public $channel;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("subscription")
*/
public $subscription;
/**
* @var bool
*
* @Form\Type("bool")
* @Form\SerializedName("subscribed")
*/
public $subscribed;
}

View file

@ -357,4 +357,12 @@ class CustomerCorporateFilter
* @Form\SerializedName("companyName")
*/
public $companyName;
/**
* @var array<string, mixed>
*
* @Form\Type("array")
* @Form\SerializedName("customFields")
*/
public $customFields;
}

View file

@ -0,0 +1,142 @@
<?php
/**
* PHP version 7.3
*
* @category OfferFilterType
* @package RetailCrm\Api\Model\Filter\Store
*/
namespace RetailCrm\Api\Model\Filter\Store;
use DateTime;
use RetailCrm\Api\Component\FormData\Mapping as Form;
/**
* Class OfferFilterType
*
* @category OfferFilterType
* @package RetailCrm\Api\Model\Filter\Store
*/
class OfferFilterType
{
/**
* @var int[]
*
* @Form\Type("int[]")
* @Form\SerializedName("ids")
*/
public $ids;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("name")
*/
public $name;
/**
* @var string
*
* @Form\Type("string[]")
* @Form\SerializedName("externalIds")
*/
public $externalIds;
/**
* @var string
*
* @Form\Type("string[]")
* @Form\SerializedName("xmlIds")
*/
public $xmlIds;
/**
* @var string[]
*
* @Form\Type("string[]")
* @Form\SerializedName("properties")
*/
public $properties;
/**
* @var string[]
*
* @Form\Type("string[]")
* @Form\SerializedName("sites")
*/
public $sites;
/**
* @var int[]
*
* @Form\Type("int[]")
* @Form\SerializedName("catalogs")
*/
public $catalogs;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("active")
*/
public $active;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("sinceId")
*/
public $sinceId;
/**
* @var int[]
*
* @Form\Type("int[]")
* @Form\SerializedName("groups")
*/
public $groups;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("priceType")
*/
public $priceType;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("minPrice")
*/
public $minPrice;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("maxPrice")
*/
public $maxPrice;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("minQuantity")
*/
public $minQuantity;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("maxQuantity")
*/
public $maxQuantity;
}

View file

@ -37,81 +37,81 @@ class ProductFilterType
public $name;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("minPrice")
*/
public $minPrice;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("maxPrice")
*/
public $maxPrice;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("minPurchasePrice")
*/
public $minPurchasePrice;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("maxPurchasePrice")
*/
public $maxPurchasePrice;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("minQuantity")
*/
public $minQuantity;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("maxQuantity")
*/
public $maxQuantity;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("popular")
*/
public $popular;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("stock")
*/
public $stock;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("novelty")
*/
public $novelty;
/**
* @var string
* @var int
*
* @Form\Type("string")
* @Form\Type("int")
* @Form\SerializedName("recommended")
*/
public $recommended;
@ -124,6 +124,14 @@ class ProductFilterType
*/
public $url;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("urlLike")
*/
public $urlLike;
/**
* @var string
*
@ -165,9 +173,9 @@ class ProductFilterType
public $offerIds;
/**
* @var string
* @var string[]
*
* @Form\Type("string")
* @Form\Type("string[]")
* @Form\SerializedName("properties")
*/
public $properties;

View file

@ -58,4 +58,20 @@ class ProductGroupFilterType
* @Form\SerializedName("catalogs")
*/
public $catalogs;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("minLevel")
*/
public $minLevel;
/**
* @var int
*
* @Form\Type("int")
* @Form\SerializedName("maxLevel")
*/
public $maxLevel;
}

View file

@ -66,4 +66,12 @@ class ProductPropertiesFilterType
* @Form\SerializedName("catalogs")
*/
public $catalogs;
/**
* @var int[]
*
* @Form\Type("int[]")
* @Form\SerializedName("groups")
*/
public $groups;
}

View file

@ -0,0 +1,45 @@
<?php
/**
* PHP version 7.3
*
* @category ProductPropertyValuesFilterType
* @package RetailCrm\Api\Model\Filter\Store
*/
namespace RetailCrm\Api\Model\Filter\Store;
use RetailCrm\Api\Component\FormData\Mapping as Form;
/**
* Class ProductPropertyValuesFilterType
*
* @category ProductPropertyValuesFilterType
* @package RetailCrm\Api\Model\Filter\Store
*/
class ProductPropertyValuesFilterType
{
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("propertyName")
*/
public $propertyName;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("propertyCode")
*/
public $propertyCode;
/**
* @var int[]
*
* @Form\Type("int[]")
* @Form\SerializedName("groups")
*/
public $groups;
}

View file

@ -0,0 +1,55 @@
<?php
/**
* PHP version 7.3
*
* @category ByAndSiteByRequest
* @package RetailCrm\Api\Model\Request
*/
namespace RetailCrm\Api\Model\Request;
use RetailCrm\Api\Component\FormData\Mapping as Form;
use RetailCrm\Api\Interfaces\RequestInterface;
/**
* Class ByAndSiteByRequest
*
* @category ByAndSiteByRequest
* @package RetailCrm\Api\Model\Request
*/
class ByAndSiteByRequest implements RequestInterface
{
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("by")
*/
public $by;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("siteBy")
*/
public $siteBy;
/**
* BySiteRequest constructor.
*
* @param string $by
* @param string $siteBy
*/
public function __construct(string $by = '', string $siteBy = '')
{
if ('' !== $by) {
$this->by = $by;
}
if ('' !== $siteBy) {
$this->siteBy = $siteBy;
}
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* PHP version 7.3
*
* @category CustomerInteractionCartClearRequest
* @package RetailCrm\Api\Model\Request\CustomerInteraction
*/
namespace RetailCrm\Api\Model\Request\CustomerInteraction;
use RetailCrm\Api\Component\FormData\Mapping as Form;
use RetailCrm\Api\Interfaces\RequestInterface;
/**
* Class CustomerInteractionCartClearRequest
*
* @category CustomerInteractionCartClearRequest
* @package RetailCrm\Api\Model\Request\CustomerInteraction
*/
class CustomerInteractionCartClearRequest implements RequestInterface
{
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("siteBy")
*/
public $siteBy;
/**
* @var \RetailCrm\Api\Model\Entity\CustomerInteraction\Cart
*
* @Form\Type("RetailCrm\Api\Model\Entity\CustomerInteraction\Cart")
* @Form\SerializedName("cart")
* @Form\JsonField()
*/
public $cart;
}

View file

@ -0,0 +1,20 @@
<?php
/**
* PHP version 7.3
*
* @category CustomerInteractionCartSetRequest
* @package RetailCrm\Api\Model\Request\CustomerInteraction
*/
namespace RetailCrm\Api\Model\Request\CustomerInteraction;
/**
* Class CustomerInteractionCartSetRequest
*
* @category CustomerInteractionCartSetRequest
* @package RetailCrm\Api\Model\Request\CustomerInteraction
*/
class CustomerInteractionCartSetRequest extends CustomerInteractionCartClearRequest
{
}

View file

@ -0,0 +1,47 @@
<?php
/**
* PHP version 7.3
*
* @category CustomersSubscriptionsRequest
* @package RetailCrm\Api\Model\Request\Customers
*/
namespace RetailCrm\Api\Model\Request\Customers;
use RetailCrm\Api\Component\FormData\Mapping as Form;
use RetailCrm\Api\Interfaces\RequestInterface;
/**
* Class CustomersSubscriptionsRequest
*
* @category CustomersSubscriptionsRequest
* @package RetailCrm\Api\Model\Request\Customers
*/
class CustomersSubscriptionsRequest implements RequestInterface
{
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("by")
*/
public $by;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("site")
*/
public $site;
/**
* @var \RetailCrm\Api\Model\Entity\Customers\Subscription[]
*
* @Form\Type("RetailCrm\Api\Model\Entity\Customers\Subscription[]")
* @Form\SerializedName("subscriptions")
* @Form\JsonField()
*/
public $subscriptions;
}

View file

@ -0,0 +1,39 @@
<?php
/**
* PHP version 7.3
*
* @category LoyaltyBonusChargeRequest
* @package RetailCrm\Api\Model\Request\Loyalty
*/
namespace RetailCrm\Api\Model\Request\Loyalty;
use DateTime;
use RetailCrm\Api\Component\FormData\Mapping as Form;
use RetailCrm\Api\Interfaces\RequestInterface;
/**
* Class LoyaltyBonusChargeRequest
*
* @category LoyaltyBonusChargeRequest
* @package RetailCrm\Api\Model\Request\Loyalty
*/
class LoyaltyBonusChargeRequest implements RequestInterface
{
/**
* @var float
*
* @Form\Type("float")
* @Form\SerializedName("amount")
*/
public $amount;
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("comment")
*/
public $comment;
}

View file

@ -0,0 +1,33 @@
<?php
/**
* PHP version 7.3
*
* @category OffersRequest
* @package RetailCrm\Api\Model\Request\Store
*/
namespace RetailCrm\Api\Model\Request\Store;
use RetailCrm\Api\Component\FormData\Mapping as Form;
use RetailCrm\Api\Interfaces\RequestInterface;
use RetailCrm\Api\Model\Request\Traits\PageLimitTrait;
/**
* Class OffersRequest
*
* @category OffersRequest
* @package RetailCrm\Api\Model\Request\Store
*/
class OffersRequest implements RequestInterface
{
use PageLimitTrait;
/**
* @var \RetailCrm\Api\Model\Filter\Store\OfferFilterType
*
* @Form\Type("RetailCrm\Api\Model\Filter\Store\OfferFilterType")
* @Form\SerializedName("filter")
*/
public $filter;
}

View file

@ -23,6 +23,14 @@ class ProductPropertiesRequest implements RequestInterface
{
use PageLimitTrait;
/**
* @var ProductPropertiesSort
*
* @Form\Type("ProductPropertiesSort")
* @Form\SerializedName("sort")
*/
public $sort;
/**
* @var \RetailCrm\Api\Model\Filter\Store\ProductPropertiesFilterType
*

View file

@ -0,0 +1,31 @@
<?php
/**
* PHP version 7.3
*
* @category ProductPropertiesRequest
* @package RetailCrm\Api\Model\Request\Store
*/
namespace RetailCrm\Api\Model\Request\Store;
use RetailCrm\Api\Component\FormData\Mapping as Form;
use RetailCrm\Api\Interfaces\RequestInterface;
use RetailCrm\Api\Model\Request\Traits\PageLimitTrait;
/**
* Class ProductPropertiesSort
*
* @category ProductPropertiesRequest
* @package RetailCrm\Api\Model\Request\Store
*/
class ProductPropertiesSort implements RequestInterface
{
/**
* @var string
*
* @Form\Type("string")
* @Form\SerializedName("offersCount")
*/
public $offersCount;
}

View file

@ -0,0 +1,33 @@
<?php
/**
* PHP version 7.3
*
* @category ProductPropertyValuesRequest
* @package RetailCrm\Api\Model\Request\Store
*/
namespace RetailCrm\Api\Model\Request\Store;
use RetailCrm\Api\Component\FormData\Mapping as Form;
use RetailCrm\Api\Interfaces\RequestInterface;
use RetailCrm\Api\Model\Request\Traits\PageLimitTrait;
/**
* Class ProductPropertyValuesRequest
*
* @category ProductPropertyValuesRequest
* @package RetailCrm\Api\Model\Request\Store
*/
class ProductPropertyValuesRequest implements RequestInterface
{
use PageLimitTrait;
/**
* @var \RetailCrm\Api\Model\Filter\Store\ProductPropertyValuesFilterType
*
* @Form\Type("RetailCrm\Api\Model\Filter\Store\ProductPropertyValuesFilterType")
* @Form\SerializedName("filter")
*/
public $filter;
}

View file

@ -0,0 +1,30 @@
<?php
/**
* PHP version 7.3
*
* @category CustomerInteractionCartGetResponse
* @package RetailCrm\Api\Model\Response\CustomerInteraction
*/
namespace RetailCrm\Api\Model\Response\CustomerInteraction;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
use RetailCrm\Api\Model\Response\SuccessResponse;
/**
* Class CustomerInteractionCartGetResponse
*
* @category CustomerInteractionCartGetResponse
* @package RetailCrm\Api\Model\Response\CustomerInteraction
*/
class CustomerInteractionCartGetResponse extends SuccessResponse
{
/**
* @var \RetailCrm\Api\Model\Entity\CustomerInteraction\Cart
*
* @JMS\Type("RetailCrm\Api\Model\Entity\CustomerInteraction\Cart")
* @JMS\SerializedName("cart")
*/
public $cart;
}

View file

@ -0,0 +1,30 @@
<?php
/**
* PHP version 7.3
*
* @category OffersResponse
* @package RetailCrm\Api\Model\Response\Store
*/
namespace RetailCrm\Api\Model\Response\Store;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
use RetailCrm\Api\Model\Response\AbstractPaginatedResponse;
/**
* Class OffersResponse
*
* @category OffersResponse
* @package RetailCrm\Api\Model\Response\Store
*/
class OffersResponse extends AbstractPaginatedResponse
{
/**
* @var \RetailCrm\Api\Model\Entity\Store\StoreOffer[]
*
* @JMS\Type("array<RetailCrm\Api\Model\Entity\Store\StoreOffer>")
* @JMS\SerializedName("offers")
*/
public $offers;
}

View file

@ -0,0 +1,31 @@
<?php
/**
* PHP version 7.3
*
* @category ProductPropertyValuesResponse
* @package RetailCrm\Api\Model\Response\Store
*/
namespace RetailCrm\Api\Model\Response\Store;
use RetailCrm\Api\Component\Serializer\Annotation as JMS;
use RetailCrm\Api\Model\Response\AbstractPaginatedResponse;
/**
* Class ProductPropertyValuesResponse
*
* @category ProductPropertyValuesResponse
* @package RetailCrm\Api\Model\Response\Store
* @SuppressWarnings(PHPMD.LongVariable)
*/
class ProductPropertyValuesResponse extends AbstractPaginatedResponse
{
/**
* @var \RetailCrm\Api\Model\Entity\Store\ProductPropertyValue[]
*
* @JMS\Type("array<RetailCrm\Api\Model\Entity\Store\ProductPropertyValue>")
* @JMS\SerializedName("productPropertyValues")
*/
public $productPropertyValues;
}

View file

@ -0,0 +1,238 @@
<?php
/**
* PHP version 7.3
*
* @category CustomerInteraction
* @package RetailCrm\Api\ResourceGroup
*/
namespace RetailCrm\Api\ResourceGroup;
use RetailCrm\Api\Enum\RequestMethod;
use RetailCrm\Api\Model\Request\ByAndSiteByRequest;
use RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartClearRequest;
use RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartSetRequest;
use RetailCrm\Api\Model\Response\CustomerInteraction\CustomerInteractionCartGetResponse;
use RetailCrm\Api\Model\Response\SuccessResponse;
/**
* Class CustomerInteraction
*
* @category CustomerInteraction
* @package RetailCrm\Api\ResourceGroup
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CustomerInteraction extends AbstractApiResourceGroup
{
/**
* Makes POST "/api/v5/customer-interaction/{site}/cart/clear" request.
*
* Example:
* ```php
* use RetailCrm\Api\Enum\BySite;
* use RetailCrm\Api\Factory\SimpleClientFactory;
* use RetailCrm\Api\Interfaces\ApiExceptionInterface;
* use RetailCrm\Api\Model\Entity\CustomerInteraction\Cart;
* use RetailCrm\Api\Model\Entity\CustomerInteraction\CartCustomer;
* use RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartClearRequest;
*
* $client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
*
* $cart = new Cart();
* $cart->clearedAt = new DateTime('now');
* $cart->customer = new CartCustomer();
* $cart->customer->id = 4770;
* $cart->customer->browserId = 'browserId';
* $cart->customer->gaClientId = 'gaClientId';
*
* $request = new CustomerInteractionCartClearRequest();
* $request->cart = $cart;
* $request->siteBy = BySite::CODE;
*
* try {
* $response = $client->customerInteraction->cartClear('testSite', $request);
* } 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;
* }
* ```
*
* @param \RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartClearRequest $request
* @param string|int $site
*
* @return \RetailCrm\Api\Model\Response\SuccessResponse
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @throws \RetailCrm\Api\Interfaces\ClientExceptionInterface
* @throws \RetailCrm\Api\Exception\Api\AccountDoesNotExistException
* @throws \RetailCrm\Api\Exception\Api\ApiErrorException
* @throws \RetailCrm\Api\Exception\Api\MissingCredentialsException
* @throws \RetailCrm\Api\Exception\Api\MissingParameterException
* @throws \RetailCrm\Api\Exception\Api\ValidationException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Exception\Client\HttpClientException
*/
public function cartClear(CustomerInteractionCartClearRequest $request, $site): SuccessResponse
{
/** @var SuccessResponse $response */
$response = $this->sendRequest(
RequestMethod::POST,
'customer-interaction/' . $site . '/cart/clear',
$request,
SuccessResponse::class
);
return $response;
}
/**
* Makes POST "/api/v5/customer-interaction/{site}/cart/set" request.
*
* Example:
* ```php
* use RetailCrm\Api\Enum\BySite;
* use RetailCrm\Api\Factory\SimpleClientFactory;
* use RetailCrm\Api\Interfaces\ApiExceptionInterface;
* use RetailCrm\Api\Model\Entity\CustomerInteraction\Cart;
* use RetailCrm\Api\Model\Entity\CustomerInteraction\CartCustomer;
* use RetailCrm\Api\Model\Entity\CustomerInteraction\CartItem;
* use RetailCrm\Api\Model\Entity\Orders\Items\Offer;
* use RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartSetRequest;
*
* $client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
*
* $customer = new CartCustomer();
* $customer->id = 4770;
*
* $cartItem = new CartItem();
* $cartItem->price = 24.99;
* $cartItem->quantity = 2;
* $cartItem->offer = new Offer();
* $cartItem->offer->externalId = '47701234567890';
*
* $cart = new Cart();
* $cart->items[] = $cartItem;
* $cart->customer = $customer;
*
* $request = new CustomerInteractionCartSetRequest();
* $request->cart = $cart;
* $request->siteBy = BySite::CODE;
*
* try {
* $response = $client->customerInteraction->cartSet('testSite', $request);
* } 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;
* }
* ```
*
* @param \RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartSetRequest $request
* @param string|int $site
*
* @return \RetailCrm\Api\Model\Response\SuccessResponse
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @throws \RetailCrm\Api\Interfaces\ClientExceptionInterface
* @throws \RetailCrm\Api\Exception\Api\AccountDoesNotExistException
* @throws \RetailCrm\Api\Exception\Api\ApiErrorException
* @throws \RetailCrm\Api\Exception\Api\MissingCredentialsException
* @throws \RetailCrm\Api\Exception\Api\MissingParameterException
* @throws \RetailCrm\Api\Exception\Api\ValidationException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Exception\Client\HttpClientException
*/
public function cartSet(CustomerInteractionCartSetRequest $request, $site): SuccessResponse
{
/** @var SuccessResponse $response */
$response = $this->sendRequest(
RequestMethod::POST,
'customer-interaction/' . $site . '/cart/set',
$request,
SuccessResponse::class
);
return $response;
}
/**
* Makes GET "/api/v5/customer-interaction/{site}/cart/{customerId}" request.
*
* Example:
* ```php
* use RetailCrm\Api\Enum\ByIdentifier;
* use RetailCrm\Api\Enum\BySite;
* use RetailCrm\Api\Factory\SimpleClientFactory;
* use RetailCrm\Api\Interfaces\ApiExceptionInterface;
* use RetailCrm\Api\Model\Request\ByAndSiteByRequest;
*
* $client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
*
* try {
* $response = $client->customerInteraction->cartGet(
* 'testSite',
* 4200,
* new ByAndSiteByRequest(ByIdentifier::ID, BySite::CODE)
* );
* } 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 'Cart: ' . print_r($response->cart, true);
* ```
*
* @param string|int $site
* @param string|int $customerId
* @param \RetailCrm\Api\Model\Request\ByAndSiteByRequest|null $request
*
* @return \RetailCrm\Api\Model\Response\CustomerInteraction\CustomerInteractionCartGetResponse
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @throws \RetailCrm\Api\Interfaces\ClientExceptionInterface
* @throws \RetailCrm\Api\Exception\Api\AccountDoesNotExistException
* @throws \RetailCrm\Api\Exception\Api\ApiErrorException
* @throws \RetailCrm\Api\Exception\Api\MissingCredentialsException
* @throws \RetailCrm\Api\Exception\Api\MissingParameterException
* @throws \RetailCrm\Api\Exception\Api\ValidationException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Exception\Client\HttpClientException
*/
public function cartGet(
$site,
$customerId,
?ByAndSiteByRequest $request = null
): CustomerInteractionCartGetResponse {
/** @var CustomerInteractionCartGetResponse $response */
$response = $this->sendRequest(
RequestMethod::GET,
'customer-interaction/' . $site . '/cart/' . $customerId,
$request,
CustomerInteractionCartGetResponse::class
);
return $response;
}
}

View file

@ -19,6 +19,7 @@ use RetailCrm\Api\Model\Request\Customers\CustomersHistoryRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersNotesCreateRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersNotesRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersSubscriptionsRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersUploadRequest;
use RetailCrm\Api\Model\Response\Customers\CustomerNotesResponse;
use RetailCrm\Api\Model\Response\Customers\CustomersEditResponse;
@ -782,4 +783,70 @@ class Customers extends AbstractApiResourceGroup
);
return $response;
}
/**
* Makes POST "/api/v5/customers/{externalId}/subscriptions" request.
*
* Example:
* ```php
* use RetailCrm\Api\Enum\ByIdentifier;
* use RetailCrm\Api\Factory\SimpleClientFactory;
* use RetailCrm\Api\Interfaces\ApiExceptionInterface;
* use RetailCrm\Api\Model\Entity\Customers\Subscription;
* use RetailCrm\Api\Model\Request\Customers\CustomersSubscriptionsRequest;
*
* $client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
*
* $subscription = new Subscription();
* $subscription->channel = 'waba';
* $subscription->subscription = 'category';
* $subscription->active = false;
* $subscription->messageId = 123;
* $request = new CustomersSubscriptionsRequest();
* $request->by = ByIdentifier::ID;
* $request->site = 'aliexpress';
* $request->subscriptions = [$subscription];
*
* try {
* $response = $client->customers->subscriptions(4770, $request);
* } 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;
* }
* ```
*
* @param int|string $identifier
* @param \RetailCrm\Api\Model\Request\Customers\CustomersSubscriptionsRequest $request
*
* @return \RetailCrm\Api\Model\Response\SuccessResponse
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @throws \RetailCrm\Api\Interfaces\ClientExceptionInterface
* @throws \RetailCrm\Api\Exception\Api\AccountDoesNotExistException
* @throws \RetailCrm\Api\Exception\Api\ApiErrorException
* @throws \RetailCrm\Api\Exception\Api\MissingCredentialsException
* @throws \RetailCrm\Api\Exception\Api\MissingParameterException
* @throws \RetailCrm\Api\Exception\Api\ValidationException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Exception\Client\HttpClientException
*/
public function subscriptions($identifier, CustomersSubscriptionsRequest $request): SuccessResponse
{
/** @var SuccessResponse $response */
$response = $this->sendRequest(
RequestMethod::POST,
'customers/' . $identifier . '/subscriptions',
$request,
SuccessResponse::class
);
return $response;
}
}

View file

@ -16,6 +16,7 @@ use RetailCrm\Api\Model\Request\Loyalty\LoyaltiesRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyAccountCreateRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyAccountEditRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyAccountsRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusChargeRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusCreditRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusOperationsRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyCalculateRequest;
@ -30,6 +31,7 @@ use RetailCrm\Api\Model\Response\Loyalty\LoyaltyBonusCreditResponse;
use RetailCrm\Api\Model\Response\Loyalty\LoyaltyBonusOperationsResponse;
use RetailCrm\Api\Model\Response\Loyalty\LoyaltyCalculateResponse;
use RetailCrm\Api\Model\Response\Loyalty\LoyaltyResponse;
use RetailCrm\Api\Model\Response\SuccessResponse;
/**
* Class Loyalty
@ -163,6 +165,66 @@ class Loyalty extends AbstractApiResourceGroup
return $response;
}
/**
* Makes POST "/api/v5/loyalty/account/{id}/bonus/charge" request.
*
* Example:
* ```php
* use RetailCrm\Api\Factory\SimpleClientFactory;
* use RetailCrm\Api\Interfaces\ApiExceptionInterface;
* use RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusChargeRequest;
*
* $client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
*
* $request = new LoyaltyBonusChargeRequest();
* $request->amount = 100;
* $request->comment = 'Payment for the goods.';
*
* try {
* $response = $client->loyalty->accountBonusCharge(159, $request);
* } 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 'Status: ' . var_export($response->success, true);
* ```
*
* @param int $id
* @param \RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusChargeRequest $request
*
* @return \RetailCrm\Api\Model\Response\Loyalty\LoyaltyBonusCreditResponse
* @throws \RetailCrm\Api\Exception\Api\AccountDoesNotExistException
* @throws \RetailCrm\Api\Exception\Api\ApiErrorException
* @throws \RetailCrm\Api\Exception\Api\MissingCredentialsException
* @throws \RetailCrm\Api\Exception\Api\MissingParameterException
* @throws \RetailCrm\Api\Exception\Api\ValidationException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Exception\Client\HttpClientException
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @throws \RetailCrm\Api\Interfaces\ClientExceptionInterface
*/
public function accountBonusCharge(int $id, LoyaltyBonusChargeRequest $request): SuccessResponse
{
/** @var LoyaltyBonusCreditResponse $response */
$response = $this->sendRequest(
RequestMethod::POST,
'loyalty/account/' . $id . '/bonus/charge',
$request,
SuccessResponse::class
);
return $response;
}
/**
* Makes POST "/api/v5/loyalty/account/{id}/bonus/credit" request.
*

View file

@ -12,21 +12,25 @@ namespace RetailCrm\Api\ResourceGroup;
use RetailCrm\Api\Enum\RequestMethod;
use RetailCrm\Api\Model\Request\Store\InventoriesRequest;
use RetailCrm\Api\Model\Request\Store\InventoriesUploadRequest;
use RetailCrm\Api\Model\Request\Store\OffersRequest;
use RetailCrm\Api\Model\Request\Store\PricesUploadRequest;
use RetailCrm\Api\Model\Request\Store\ProductBatchEditRequest;
use RetailCrm\Api\Model\Request\Store\ProductGroupsCreateRequest;
use RetailCrm\Api\Model\Request\Store\ProductGroupsEditRequest;
use RetailCrm\Api\Model\Request\Store\ProductGroupsRequest;
use RetailCrm\Api\Model\Request\Store\ProductPropertiesRequest;
use RetailCrm\Api\Model\Request\Store\ProductPropertyValuesRequest;
use RetailCrm\Api\Model\Request\Store\ProductsBatchCreateRequest;
use RetailCrm\Api\Model\Request\Store\ProductsRequest;
use RetailCrm\Api\Model\Response\IdResponse;
use RetailCrm\Api\Model\Response\Store\InventoriesResponse;
use RetailCrm\Api\Model\Response\Store\InventoriesUploadResponse;
use RetailCrm\Api\Model\Response\Store\OffersResponse;
use RetailCrm\Api\Model\Response\Store\PricesUploadResponse;
use RetailCrm\Api\Model\Response\Store\ProductBatchEditResponse;
use RetailCrm\Api\Model\Response\Store\ProductGroupsResponse;
use RetailCrm\Api\Model\Response\Store\ProductPropertiesResponse;
use RetailCrm\Api\Model\Response\Store\ProductPropertyValuesResponse;
use RetailCrm\Api\Model\Response\Store\ProductsBatchCreateResponse;
use RetailCrm\Api\Model\Response\Store\ProductsResponse;
@ -690,4 +694,126 @@ class Store extends AbstractApiResourceGroup
return $response;
}
/**
* Makes GET "/api/v5/store/offers" request.
*
* Example:
* ```php
* use RetailCrm\Api\Enum\NumericBoolean;
* use RetailCrm\Api\Factory\SimpleClientFactory;
* use RetailCrm\Api\Interfaces\ApiExceptionInterface;
* use RetailCrm\Api\Model\Filter\Store\OfferFilterType;
* use RetailCrm\Api\Model\Request\Store\OffersRequest;
*
* $client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
*
* $request = new OffersRequest();
* $request->filter = new OfferFilterType();
* $request->filter->active = NumericBoolean::TRUE;
* $request->filter->name = 'Test Offer';
*
* try {
* $response = $client->store->offers($request);
* } 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 'Offers: ' . print_r($response->offers, true);
* ```
*
* @param \RetailCrm\Api\Model\Request\Store\OffersRequest|null $request
*
* @return \RetailCrm\Api\Model\Response\Store\OffersResponse
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @throws \RetailCrm\Api\Interfaces\ClientExceptionInterface
* @throws \RetailCrm\Api\Exception\Api\AccountDoesNotExistException
* @throws \RetailCrm\Api\Exception\Api\ApiErrorException
* @throws \RetailCrm\Api\Exception\Api\MissingCredentialsException
* @throws \RetailCrm\Api\Exception\Api\MissingParameterException
* @throws \RetailCrm\Api\Exception\Api\ValidationException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Exception\Client\HttpClientException
*/
public function offers(?OffersRequest $request = null): OffersResponse
{
/** @var OffersResponse $response */
$response = $this->sendRequest(
RequestMethod::GET,
'store/offers',
$request,
OffersResponse::class
);
return $response;
}
/**
* Makes GET "/api/v5/store/products/properties/values" request.
*
* Example:
* ```php
* use RetailCrm\Api\Factory\SimpleClientFactory;
* use RetailCrm\Api\Interfaces\ApiExceptionInterface;
* use RetailCrm\Api\Model\Filter\Store\ProductPropertiesFilterType;
* use RetailCrm\Api\Model\Request\Store\ProductPropertiesRequest;
*
* $client = SimpleClientFactory::createClient('https://test.retailcrm.pro', 'apiKey');
*
* $request = new ProductPropertyValuesRequest();
* $request->filter = new ProductPropertyValuesFilterType();
* $request->filter->propertyCode = 'property_code';
*
* try {
* $response = $client->store->productsPropertyValues($request);
* } 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 'Product property values: ' . print_r($response->productPropertyValues, true);
* ```
*
* @param \RetailCrm\Api\Model\Request\Store\ProductPropertyValuesRequest|null $request
*
* @return \RetailCrm\Api\Model\Response\Store\ProductPropertyValuesResponse
* @throws \RetailCrm\Api\Interfaces\ApiExceptionInterface
* @throws \RetailCrm\Api\Interfaces\ClientExceptionInterface
* @throws \RetailCrm\Api\Exception\Api\AccountDoesNotExistException
* @throws \RetailCrm\Api\Exception\Api\ApiErrorException
* @throws \RetailCrm\Api\Exception\Api\MissingCredentialsException
* @throws \RetailCrm\Api\Exception\Api\MissingParameterException
* @throws \RetailCrm\Api\Exception\Api\ValidationException
* @throws \RetailCrm\Api\Exception\Client\HandlerException
* @throws \RetailCrm\Api\Exception\Client\HttpClientException
*/
public function productsPropertyValues(?ProductPropertyValuesRequest $request = null): ProductPropertyValuesResponse
{
/** @var ProductPropertyValuesResponse $response */
$response = $this->sendRequest(
RequestMethod::GET,
'store/products/properties/values',
$request,
ProductPropertyValuesResponse::class
);
return $response;
}
}

View file

@ -27,7 +27,7 @@ trait EventDispatcherAwareTrait
*
* @param \Psr\EventDispatcher\EventDispatcherInterface|null $eventDispatcher
*
* @return object
* @return static
*/
public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): object
{

View file

@ -18,7 +18,10 @@ if (!is_file($autoloadFile = __DIR__ . '/../vendor/autoload.php')) {
$loader = require $autoloadFile;
$loader->add('RetailCrm\\TestUtils', __DIR__ . '/tests/utils');
$loader->add('RetailCrm\\Tests', __DIR__ . '/src');
AnnotationRegistry::registerLoader('class_exists');
if (method_exists(AnnotationRegistry::class, 'registerLoader')) {
AnnotationRegistry::registerLoader('class_exists');
}
if (file_exists(__DIR__ . '/../.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');

View file

@ -11,6 +11,7 @@ namespace RetailCrm\Tests\Model\Callback\Entity\Integration;
use PHPUnit\Framework\TestCase;
use RetailCrm\Api\Factory\SerializerFactory;
use RetailCrm\Api\Model\Entity\Settings\MgSettings;
use RetailCrm\Api\Model\Entity\Settings\NonWorkingDay;
use RetailCrm\Api\Model\Entity\Settings\Settings as SystemSettings;
use RetailCrm\Api\Model\Callback\Entity\Integration\Settings;
@ -50,6 +51,27 @@ class SettingsTest extends TestCase
'start_date' => '05.29',
'end_date' => '06.29'
]
],
'mg' => [
'order_creation' => [
'default' => [
'site' => 'site',
'order_type' => 'type',
'order_method' => 'method',
],
'channels' => [
1111 => [
'site' => 'site_1',
'order_type' => 'type_1',
'order_method' => 'method_1',
],
2222 => [
'site' => 'site_2',
'order_type' => 'type_2',
'order_method' => 'method_2',
],
]
]
]
],
],
@ -61,5 +83,6 @@ class SettingsTest extends TestCase
self::assertInstanceOf(Value::class, $settings->settings->defaultCurrency);
self::assertInstanceOf(WorkTime::class, $settings->settings->workTimes[0]);
self::assertInstanceOf(NonWorkingDay::class, $settings->settings->nonWorkingDays[0]);
self::assertInstanceOf(MgSettings::class, $settings->settings->mgSettings);
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace RetailCrm\Tests\ResourceGroup;
use DateTime;
use RetailCrm\Api\Enum\ByIdentifier;
use RetailCrm\Api\Enum\BySite;
use RetailCrm\Api\Enum\RequestMethod;
use RetailCrm\Api\Model\Entity\CustomerInteraction\Cart;
use RetailCrm\Api\Model\Entity\CustomerInteraction\CartCustomer;
use RetailCrm\Api\Model\Entity\CustomerInteraction\CartItem;
use RetailCrm\Api\Model\Entity\Orders\Items\Offer;
use RetailCrm\Api\Model\Request\ByAndSiteByRequest;
use RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartClearRequest;
use RetailCrm\Api\Model\Request\CustomerInteraction\CustomerInteractionCartSetRequest;
use RetailCrm\TestUtils\Factory\TestClientFactory;
use RetailCrm\TestUtils\TestCase\AbstractApiResourceGroupTestCase;
class CustomerInteractionTest extends AbstractApiResourceGroupTestCase
{
public static function testCustomerInteractionCartClear(): void
{
$json = <<<'EOF'
{
"success": true
}
EOF;
$cart = new Cart();
$cart->clearedAt = new DateTime('now');
$cart->customer = new CartCustomer();
$cart->customer->id = 4770;
$cart->customer->browserId = 'browserId';
$cart->customer->gaClientId = 'gaClientId';
$request = new CustomerInteractionCartClearRequest();
$request->cart = $cart;
$request->siteBy = BySite::CODE;
$mock = static::createApiMockBuilder('customer-interaction/testSite/cart/clear');
$mock->matchMethod(RequestMethod::POST)
->matchPath('/api/v5/customer-interaction/testSite/cart/clear')
->matchBody(static::encodeForm($request))
->reply(200)
->withBody($json);
$client = TestClientFactory::createClient($mock->getClient());
$response = $client->customerInteraction->cartClear($request, 'testSite');
self::assertModelEqualsToResponse($json, $response);
}
public static function testCustomerInteractionCartSet(): void
{
$json = <<<'EOF'
{
"success": true
}
EOF;
$customer = new CartCustomer();
$customer->id = 4770;
$cartItem = new CartItem();
$cartItem->price = 24.99;
$cartItem->quantity = 2;
$cartItem->offer = new Offer();
$cartItem->offer->externalId = '47701234567890';
$cart = new Cart();
$cart->items[] = $cartItem;
$cart->customer = $customer;
$request = new CustomerInteractionCartSetRequest();
$request->cart = $cart;
$request->siteBy = BySite::CODE;
$mock = static::createApiMockBuilder('customer-interaction/testSite/cart/set');
$mock->matchMethod(RequestMethod::POST)
->matchPath('/api/v5/customer-interaction/testSite/cart/set')
->matchBody(static::encodeForm($request))
->reply(200)
->withBody($json);
$client = TestClientFactory::createClient($mock->getClient());
$response = $client->customerInteraction->cartSet($request, 'testSite');
self::assertModelEqualsToResponse($json, $response);
}
public static function testCustomerInteractionCartGet(): void
{
$json = <<<'EOF'
{
"success": true,
"cart": {
"externalId": "12345678901234",
"droppedAt": "2024-05-08 15:10:30-05:00",
"link": "https:/shop.myshopify.com/recover?key=1234",
"items": [
{
"id": "1",
"offer": {
"displayName": "Some sample product",
"id": 11,
"externalId": "01234567890123",
"name": "Some sample product",
"vatRate": "20.00",
"properties": [],
"unit": {
"code": "pc",
"name": "Piece",
"sym": "pc"
}
},
"quantity": 2,
"price": 24.99
}
],
"currency": "EUR"
}
}
EOF;
$request = new ByAndSiteByRequest(ByIdentifier::ID, BySite::CODE);
$mock = static::createApiMockBuilder('customer-interaction/testSite/cart/4770');
$mock->matchMethod(RequestMethod::GET)
->matchPath('/api/v5/customer-interaction/testSite/cart/4770')
->matchQuery(static::encodeFormArray($request))
->reply(200)
->withBody($json);
$client = TestClientFactory::createClient($mock->getClient());
$response = $client->customerInteraction->cartGet('testSite', 4770, $request);
self::assertModelEqualsToResponse($json, $response);
}
}

View file

@ -22,6 +22,7 @@ use RetailCrm\Api\Model\Entity\Customers\CustomerNote;
use RetailCrm\Api\Model\Entity\Customers\CustomerPhone;
use RetailCrm\Api\Model\Entity\Customers\CustomerTag;
use RetailCrm\Api\Model\Entity\Customers\SerializedCustomerReference;
use RetailCrm\Api\Model\Entity\Customers\Subscription;
use RetailCrm\Api\Model\Entity\FixExternalRow;
use RetailCrm\Api\Model\Filter\Customers\CustomerFilter;
use RetailCrm\Api\Model\Filter\Customers\CustomerHistoryFilter;
@ -35,6 +36,7 @@ use RetailCrm\Api\Model\Request\Customers\CustomersHistoryRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersNotesCreateRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersNotesRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersSubscriptionsRequest;
use RetailCrm\Api\Model\Request\Customers\CustomersUploadRequest;
use RetailCrm\TestUtils\Factory\TestClientFactory;
use RetailCrm\TestUtils\TestCase\AbstractApiResourceGroupTestCase;
@ -178,6 +180,33 @@ class CustomersTest extends AbstractApiResourceGroupTestCase
"number": "89229112322"
}
],
"customerSubscriptions": [
{
"subscription": {
"id": 2,
"channel": "email",
"name": "Без тематики",
"code": "default_marketing",
"active": true,
"autoSubscribe": true,
"ordering": 1
},
"subscribed": true
},
{
"subscription": {
"id": 4,
"channel": "waba",
"name": "Без тематики",
"code": "default_marketing",
"active": true,
"autoSubscribe": true,
"ordering": 1
},
"subscribed": false,
"changedAt": "2024-12-17 11:50:43"
}
],
"mgCustomers": [
{
"id": 1,
@ -2668,6 +2697,33 @@ EOF;
"number": "+79094055044"
}
],
"customerSubscriptions": [
{
"subscription": {
"id": 2,
"channel": "email",
"name": "Без тематики",
"code": "default_marketing",
"active": true,
"autoSubscribe": true,
"ordering": 1
},
"subscribed": true
},
{
"subscription": {
"id": 4,
"channel": "waba",
"name": "Без тематики",
"code": "default_marketing",
"active": true,
"autoSubscribe": true,
"ordering": 1
},
"subscribed": false,
"changedAt": "2024-12-17 11:50:43"
}
],
"mgCustomers": [
{
"id": 1,
@ -2773,4 +2829,35 @@ EOF;
self::assertModelEqualsToResponse($json, $response);
}
public function testCustomersSubscriptions(): void
{
$json = <<<'EOF'
{
"success": true
}
EOF;
$subscription = new Subscription();
$subscription->channel = 'waba';
$subscription->subscription = 'category';
$subscription->active = false;
$subscription->messageId = 123;
$request = new CustomersSubscriptionsRequest();
$request->by = ByIdentifier::ID;
$request->site = 'aliexpress';
$request->subscriptions = [$subscription];
$mock = static::createApiMockBuilder('customers/4770/subscriptions');
$mock->matchMethod(RequestMethod::POST)
->matchBody(static::encodeForm($request))
->reply(200)
->withBody($json);
$client = TestClientFactory::createClient($mock->getClient());
$response = $client->customers->subscriptions(4770, $request);
self::assertModelEqualsToResponse($json, $response);
}
}

View file

@ -38,6 +38,7 @@ use RetailCrm\Api\Model\Request\Loyalty\LoyaltiesRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyAccountCreateRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyAccountEditRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyAccountsRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusChargeRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusCreditRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyBonusOperationsRequest;
use RetailCrm\Api\Model\Request\Loyalty\LoyaltyCalculateRequest;
@ -121,6 +122,29 @@ EOF;
self::assertModelEqualsToResponse($json, $response);
}
public function testAccountBonusCharge(): void
{
$json = <<<EOF
{
"success": true
}
EOF;
$request = new LoyaltyBonusChargeRequest();
$request->amount = 100;
$request->comment = 'Monthly membership bonuses.';
$mock = static::createApiMockBuilder('loyalty/account/159/bonus/charge');
$mock->matchMethod(RequestMethod::POST)
->reply(200)
->withBody($json);
$client = TestClientFactory::createClient($mock->getClient());
$response = $client->loyalty->accountBonusCharge(159, $request);
self::assertModelEqualsToResponse($json, $response);
}
public function testAccountBonusCredit(): void
{
$activationDate = new DateTime();

View file

@ -99,6 +99,7 @@ EOF;
$invoiceRequest->paymentId = 'd6458333-fff3-4fd0-9b23-4e6344451f8e';
$invoiceRequest->invoiceUuid = '5b1b2e9d-b7f1-48f4-acb9-4bfce04b30cf';
$invoiceRequest->invoiceUrl = 'https://example.com/newUrl';
$invoiceRequest->discountAmount = 20.0;
$request = new PaymentUpdateInvoiceRequest($invoiceRequest);

View file

@ -2671,7 +2671,8 @@ EOF;
"legalName": "ИП Бороздина Татьяна Александровна",
"INN": "773719480254",
"code": "test",
"countryIso": "RU"
"countryIso": "RU",
"certificateDate": "2020-01-01"
},
"countryIso": "RU",
"currency": "RUB",
@ -2702,7 +2703,8 @@ EOF;
"legalName": "ИП Бороздина Татьяна Александровна",
"INN": "773719480154",
"code": "Vog_Gallery",
"countryIso": "RU"
"countryIso": "RU",
"certificateDate": "2020-01-01"
},
"countryIso": "RU",
"currency": "RUB"

View file

@ -54,7 +54,28 @@ class SettingsTest extends AbstractApiResourceGroupTestCase
"lunch_start_time": "12:30",
"lunch_end_time": "13:00"
}
]
],
"mg": {
"order_creation": {
"default": {
"site": "site",
"order_type": "type",
"order_method": "method"
},
"channels": {
"1111": {
"site": "site_1",
"order_type": "type_1",
"order_method": "method_1"
},
"2222": {
"site": "site_2",
"order_type": "type_2",
"order_method": "method_2"
}
}
}
}
}
}
EOF;

View file

@ -22,17 +22,22 @@ use RetailCrm\Api\Model\Entity\Store\ProductEditGroupInput;
use RetailCrm\Api\Model\Entity\Store\ProductEditInput;
use RetailCrm\Api\Model\Entity\Store\SerializedProductGroup;
use RetailCrm\Api\Model\Filter\Store\InventoryFilterType;
use RetailCrm\Api\Model\Filter\Store\OfferFilterType;
use RetailCrm\Api\Model\Filter\Store\ProductFilterType;
use RetailCrm\Api\Model\Filter\Store\ProductGroupFilterType;
use RetailCrm\Api\Model\Filter\Store\ProductPropertiesFilterType;
use RetailCrm\Api\Model\Filter\Store\ProductPropertyValuesFilterType;
use RetailCrm\Api\Model\Request\Store\InventoriesRequest;
use RetailCrm\Api\Model\Request\Store\InventoriesUploadRequest;
use RetailCrm\Api\Model\Request\Store\OffersRequest;
use RetailCrm\Api\Model\Request\Store\PricesUploadRequest;
use RetailCrm\Api\Model\Request\Store\ProductBatchEditRequest;
use RetailCrm\Api\Model\Request\Store\ProductGroupsCreateRequest;
use RetailCrm\Api\Model\Request\Store\ProductGroupsEditRequest;
use RetailCrm\Api\Model\Request\Store\ProductGroupsRequest;
use RetailCrm\Api\Model\Request\Store\ProductPropertiesRequest;
use RetailCrm\Api\Model\Request\Store\ProductPropertiesSort;
use RetailCrm\Api\Model\Request\Store\ProductPropertyValuesRequest;
use RetailCrm\Api\Model\Request\Store\ProductsBatchCreateRequest;
use RetailCrm\Api\Model\Request\Store\ProductsRequest;
use RetailCrm\TestUtils\Factory\TestClientFactory;
@ -446,7 +451,7 @@ EOF;
$request->filter = new ProductFilterType();
$request->filter->active = NumericBoolean::TRUE;
$request->filter->priceType = 'base';
$request->filter->maxPrice = '10000';
$request->filter->maxPrice = 10000;
$request->filter->name = 'Test Product';
$mock = static::createApiMockBuilder('store/products');
@ -548,7 +553,7 @@ EOF;
$request->filter = new ProductFilterType();
$request->filter->active = NumericBoolean::TRUE;
$request->filter->priceType = 'base';
$request->filter->maxPrice = '10000';
$request->filter->maxPrice = 10000;
$request->filter->name = 'Test Product';
$request->filter->sinceId = 828272;
$request->filter->sinceUpdatedAt = DateTimeTransformer::create('2020-01-01 00:00:00');
@ -616,10 +621,14 @@ EOF;
EOF;
$request = new ProductPropertiesRequest();
$request->sort = new ProductPropertiesSort();
$request->sort->offersCount = 'desc';
$request->filter = new ProductPropertiesFilterType();
$request->filter->sites = ['moysklad', 'aliexpress'];
$request->filter->visible = NumericBoolean::TRUE;
$request->filter->variative = NumericBoolean::TRUE;
$request->filter->groups = [3676, 3679, 3680, 3724];
$mock = static::createApiMockBuilder('store/products/properties');
$mock->matchMethod(RequestMethod::GET)
@ -778,4 +787,186 @@ EOF;
self::assertModelEqualsToResponse($json, $response);
}
public function testProductPropertyValues(): void
{
$json = <<<'EOF'
{
"success": true,
"pagination": {
"limit": 20,
"totalCount": 1,
"currentPage": 1,
"totalPageCount": 1
},
"productPropertyValues": [
{
"property": {
"sites": [
"e-mapper",
"sendpulse",
"glavpunkt",
"retailcrm-services-peshkariki",
"vk-com",
"moysklad",
"eftestshop-ru"
],
"groups": [
{
"id": 3676,
"name": "warehouseRoot"
},
{
"id": 3679,
"name": "Входящая в группу"
},
{
"id": 3680,
"name": "test"
},
{
"id": 3724,
"name": "Услуги"
}
],
"code": "code",
"name": "Код",
"isNumeric": false,
"visible": true,
"variative": true
},
"value": "testValue",
"offersCount": 10
}
]
}
EOF;
$request = new ProductPropertyValuesRequest();
$request->filter = new ProductPropertyValuesFilterType();
$request->filter->propertyName = "testName";
$request->filter->propertyCode = "testCode";
$request->filter->groups = [1, 2, 3];
$mock = static::createApiMockBuilder('store/products/properties/values');
$mock->matchMethod(RequestMethod::GET)
->matchQuery(self::encodeFormArray($request))
->reply(200)
->withBody($json);
$client = TestClientFactory::createClient($mock->getClient());
$response = $client->store->productsPropertyValues($request);
self::assertModelEqualsToResponse($json, $response);
}
public function testOffers(): void
{
$json = <<<'EOF'
{
"success": true,
"pagination": {
"limit": 20,
"totalCount": 1,
"currentPage": 1,
"totalPageCount": 1
},
"offers": [
{
"name": "Test Offer",
"images": [
"https://example.com/image.jpg"
],
"id": 1941833,
"externalId": "38311",
"xmlId": "38311",
"article": "38311",
"prices": [
{
"priceType": "base",
"price": 624,
"ordering": 991
}
],
"purchasePrice": 272.64,
"vatRate": "none",
"properties": {
"ves": "33",
"brend": "Test",
"image": "https://example.com/image.jpg",
"ves_g": "33",
"artikul": "38311"
},
"quantity": 0.0,
"weight": 33.0,
"active": true,
"unit": {
"code": "pc",
"name": "Штука",
"sym": "шт."
},
"product": {
"minPrice": 624,
"maxPrice": 624,
"id": 828272,
"article": "38311",
"type": "product",
"name": "Test Product",
"url": "https://example.com",
"imageUrl": "https://example.com/image.jpg",
"description": "Test Description",
"groups": [
{
"id": 4050,
"externalId": "368"
},
{
"id": 4154,
"externalId": "391"
},
{
"id": 4279,
"externalId": "394"
}
],
"externalId": "38311",
"manufacturer": "Test",
"active": true,
"quantity": 0,
"markable": false
}
}
]
}
EOF;
$request = new OffersRequest();
$request->filter = new OfferFilterType();
$request->filter->active = NumericBoolean::TRUE;
$request->filter->minPrice = 1;
$request->filter->maxPrice = 120000;
$request->filter->sites = ['main'];
$request->filter->maxQuantity = 5;
$request->filter->catalogs = [2];
$mock = static::createApiMockBuilder('store/offers');
$mock->matchMethod(RequestMethod::GET)
->matchQuery([
'filter' => [
'sites' => ['main'],
'catalogs' => ['2'],
'active' => '1',
'minPrice' => '1',
'maxPrice' => '120000',
'maxQuantity' => '5',
]
])
->reply(200)
->withBody($json);
$client = TestClientFactory::createClient($mock->getClient());
$response = $client->store->offers($request);
self::assertModelEqualsToResponse($json, $response);
}
}