From 69d2233ce899e76ecdce5653d4efce695349ac4c Mon Sep 17 00:00:00 2001 From: Eugene Ivanov <93479789+evgeek@users.noreply.github.com> Date: Sun, 4 Jun 2023 21:22:15 +0300 Subject: [PATCH] Record Format (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Объектный подход Record к работе с сущностями Моего Склада. * Метод massCreateUpdate(). * Метод fromUrl($url, $withParams). * Методы получения объектов HTTP запроса и ответа в RequestException. --- CHANGELOG.md | 54 +- README.md | 254 +----- UPGRADE.md | 71 ++ composer.json | 6 +- composer.lock | 767 ++++++++---------- docs/active_record.md | 349 ++++++++ docs/api_interaction.md | 20 + docs/autocomplete.gif | Bin 0 -> 201666 bytes docs/comments.png | Bin 0 -> 58663 bytes docs/exceptions.md | 45 + docs/formatters.md | 121 +++ docs/index.md | 25 + docs/query_builder.md | 285 +++++++ docs/setup.md | 43 + docs/tools.md | 110 +++ phpunit.xml.dist | 53 +- src/Api/Query.php | 86 -- src/Api/{ => Query}/AbstractBuilder.php | 50 +- src/Api/{ => Query}/Debug.php | 26 +- src/Api/Query/QueryBuilder.php | 114 +++ .../Segments/AbstractSegmentCommon.php | 4 +- .../Segments/AbstractSegmentNamed.php | 4 +- .../Segments/ById/AbstractByIdSegment.php | 21 + .../Query/Segments/ById/ByIdSegmentCommon.php | 16 + .../Segments/ById/ByIdSegmentPositioned.php | 18 + .../AbstractEndpointSegmentNamed.php | 13 + .../Query/Segments/Endpoints/AuditSegment.php | 25 + .../Endpoints/EndpointSegmentCommon.php | 25 + .../Segments/Endpoints/EntitySegment.php | 84 ++ .../Endpoints/NotificationSegment.php | 23 + .../Segments/Endpoints/ReportSegment.php | 12 + .../Methods/AbstractMethodSegmentNamed.php | 17 + .../Documents/CustomerorderSegment.php | 35 + .../Methods/Entities/AssortmentSegment.php | 32 + .../Methods/Entities/EmployeeSegment.php | 38 + .../Methods/Entities/ProductSegment.php | 38 + .../Segments/Methods/MethodSegmentCommon.php | 43 + .../Methods/Nested/AttributesSegment.php | 25 + .../Methods/Nested/MetadataSegment.php | 21 + .../Methods/Nested/PositionsSegment.php | 27 + .../Methods/Nested/SettingsSegment.php | 17 + .../Segments/Special/MassDeleteSegment.php | 27 + .../Traits/Actions/CreateTrait.php | 5 +- .../{ => Query}/Traits/Actions/DebugTrait.php | 8 +- .../Traits/Actions/DeleteTrait.php | 5 +- .../Traits/Actions/GetGeneratorTrait.php | 6 +- .../{ => Query}/Traits/Actions/GetTrait.php | 5 +- .../Traits/Actions/MassCreateUpdateTrait.php | 43 + .../Query/Traits/Actions/MassDeleteTrait.php | 35 + .../{ => Query}/Traits/Actions/SendTrait.php | 7 +- .../Traits/Actions/UpdateTrait.php | 5 +- src/Api/Query/Traits/Params/ExpandTrait.php | 35 + src/Api/Query/Traits/Params/FilterTrait.php | 41 + .../Query/Traits/Params/LimitOffsetTrait.php | 46 ++ src/Api/Query/Traits/Params/OrderTrait.php | 36 + src/Api/Query/Traits/Params/ParamTrait.php | 33 + .../{ => Query}/Traits/Params/SearchTrait.php | 11 +- .../Query/Traits/Segments/AttributesTrait.php | 27 + .../Query/Traits/Segments/ByIdCommonTrait.php | 26 + .../Traits/Segments/ByIdPositionedTrait.php | 28 + .../Query/Traits/Segments/MetadataTrait.php | 28 + .../Traits/Segments/MethodCommonTrait.php | 25 + .../Query/Traits/Segments/PositionsTrait.php | 27 + .../Query/Traits/Segments/SettingsTrait.php | 26 + src/Api/Record/AbstractConcreteRecord.php | 18 + src/Api/Record/AbstractRecord.php | 107 +++ src/Api/Record/AbstractUnknownRecord.php | 24 + .../Record/AutocompleteHelpers/Alcoholic.php | 21 + .../Record/AutocompleteHelpers/Barcode.php | 21 + src/Api/Record/AutocompleteHelpers/Image.php | 24 + .../AutocompleteHelpers/MetaCollection.php | 21 + .../Record/AutocompleteHelpers/MetaObject.php | 19 + src/Api/Record/AutocompleteHelpers/Pack.php | 22 + src/Api/Record/AutocompleteHelpers/Price.php | 20 + .../AutocompleteHelpers/PriceWithType.php | 21 + src/Api/Record/Builders/AbstractBuilder.php | 27 + src/Api/Record/Builders/CollectionBuilder.php | 77 ++ src/Api/Record/Builders/ObjectBuilder.php | 66 ++ src/Api/Record/Builders/RecordBuilder.php | 24 + .../AbstractConcreteCollection.php | 31 + .../Documents/CustomerorderCollection.php | 26 + .../Entities/AssortmentCollection.php | 28 + .../Entities/EmployeeCollection.php | 26 + .../Entities/ProductCollection.php | 26 + .../Traits/CrudCollectionTrait.php | 191 +++++ .../Traits/FillMetaCollectionTrait.php | 22 + .../Traits/IterateCollectionTrait.php | 80 ++ .../Collections/Traits/IteratorTrait.php | 40 + .../Traits/ParamsCollectionTrait.php | 169 ++++ .../Record/Collections/UnknownCollection.php | 32 + .../Record/Objects/AbstractConcreteObject.php | 51 ++ .../Objects/Documents/Customerorder.php | 76 ++ .../Record/Objects/Entities/Assortment.php | 26 + src/Api/Record/Objects/Entities/Employee.php | 56 ++ src/Api/Record/Objects/Entities/Product.php | 81 ++ .../Record/Objects/Traits/CrudObjectTrait.php | 123 +++ .../Objects/Traits/FillMetaObjectTrait.php | 27 + .../Objects/Traits/ParamsObjectTrait.php | 58 ++ .../Objects/Traits/SetIdInMetaHrefTrait.php | 60 ++ src/Api/Record/Objects/UnknownObject.php | 53 ++ src/Api/Segments/ById/AbstractById.php | 21 - src/Api/Segments/ById/ByIdCommon.php | 16 - src/Api/Segments/ById/ByIdPositioned.php | 18 - .../Endpoints/AbstractEndpointNamed.php | 13 - src/Api/Segments/Endpoints/Audit.php | 24 - src/Api/Segments/Endpoints/EndpointCommon.php | 25 - src/Api/Segments/Endpoints/Entity.php | 62 -- src/Api/Segments/Endpoints/Notification.php | 22 - src/Api/Segments/Endpoints/Report.php | 10 - .../Segments/Methods/AbstractMethodNamed.php | 17 - .../Methods/Documents/Customerorder.php | 35 - .../Segments/Methods/Entities/Assortment.php | 29 - src/Api/Segments/Methods/Entities/Product.php | 35 - src/Api/Segments/Methods/MethodCommon.php | 43 - .../Segments/Methods/Nested/Attributes.php | 25 - src/Api/Segments/Methods/Nested/Metadata.php | 21 - src/Api/Segments/Methods/Nested/Positions.php | 27 - src/Api/Segments/Special/MassDelete.php | 27 - src/Api/Traits/Actions/MassDeleteTrait.php | 27 - src/Api/Traits/Params/ExpandTrait.php | 49 -- src/Api/Traits/Params/FilterTrait.php | 123 --- src/Api/Traits/Params/LimitOffsetTrait.php | 47 -- src/Api/Traits/Params/OrderTrait.php | 54 -- src/Api/Traits/Params/ParamTrait.php | 51 -- src/Api/Traits/Segments/AttributesTrait.php | 26 - src/Api/Traits/Segments/ByIdCommonTrait.php | 25 - .../Traits/Segments/ByIdPositionedTrait.php | 27 - src/Api/Traits/Segments/MetadataTrait.php | 25 - src/Api/Traits/Segments/MethodCommonTrait.php | 24 - src/Api/Traits/Segments/PositionsTrait.php | 26 - src/Dictionaries/Document.php | 10 + src/Dictionaries/Endpoint.php | 13 + src/Dictionaries/Entity.php | 12 + src/Enums/HttpMethod.php | 17 + src/Exceptions/RequestException.php | 74 ++ src/Formatters/AbstractMultiDecoder.php | 49 +- src/Formatters/ArrayFormat.php | 6 +- src/Formatters/JsonFormatterInterface.php | 4 +- src/Formatters/RecordFormat.php | 133 +++ src/Formatters/RecordMapping.php | 126 +++ src/Formatters/StdClassFormat.php | 6 +- src/Formatters/StringFormat.php | 4 +- src/Formatters/WithMoySkladInterface.php | 12 + src/Http/ApiClient.php | 16 +- src/Http/GuzzleSenderFactory.php | 12 +- src/Meta/MetaMaker.php | 35 + src/MoySklad.php | 78 +- src/Services/CollectionHelper.php | 23 + src/Services/QueryParams.php | 188 +++++ src/Services/RecordHelper.php | 24 + src/Services/RecordMappingHelper.php | 46 ++ src/Services/Url.php | 44 +- src/Tools/Guid.php | 14 +- src/Tools/Meta.php | 74 +- .../Api/Builders/Endpoints/AuditTest.php | 16 - .../Api/Builders/Endpoints/ReportTest.php | 16 - .../Methods/Documents/CustomerorderTest.php | 16 - .../Methods/Entities/AssortmentTest.php | 16 - .../Builders/Methods/Entities/ProductTest.php | 16 - tests/Feature/Api/{ => Query}/ApiTestCase.php | 8 +- .../Builders/ById/ByIdCommonTest.php | 6 +- .../Builders/ById/ByIdPositionedTest.php | 6 +- .../Query/Builders/Endpoints/AuditTest.php | 16 + .../Builders/Endpoints/EndpointCommonTest.php | 6 +- .../Builders/Endpoints/EntityTest.php | 12 +- .../Builders/Endpoints/NotificationTest.php | 6 +- .../Query/Builders/Endpoints/ReportTest.php | 16 + .../Methods/Documents/CustomerorderTest.php | 16 + .../Methods/Entities/AssortmentTest.php | 16 + .../Builders/Methods/Entities/ProductTest.php | 16 + .../Builders/Methods/MethodCommonTest.php | 6 +- .../Methods/Nested/AttributesTest.php | 6 +- .../Builders/Methods/Nested/MetadataTest.php | 6 +- .../Builders/Methods/Nested/PositionsTest.php | 6 +- .../Builders/Methods/Special/DebugTest.php | 8 +- tests/Traits/ApiClientMockerTrait.php | 16 +- tests/Traits/MoySkladMockerTrait.php | 31 + tests/Unit/Api/{ => Query}/ApiTestCase.php | 2 +- tests/Unit/Api/{ => Query}/DebugTest.php | 11 +- tests/Unit/Api/Query/QueryBuilderTest.php | 116 +++ .../Segments/AbstractSegmentCommonTest.php | 10 +- .../Segments/AbstractSegmentNamedTest.php | 8 +- .../Query/Segments/Endpoints/EntityTest.php | 65 ++ .../Segments/Special/MassDeleteTest.php | 12 +- .../Traits/Actions/CreateTraitTest.php | 10 +- .../Traits/Actions/DebugTraitTest.php | 14 +- .../Traits/Actions/DeleteTraitTest.php | 10 +- .../Traits/Actions/GetGeneratorTraitTest.php | 12 +- .../Traits/Actions/GetTraitTest.php | 10 +- .../Actions/MassCreateUpdateTraitTest.php | 24 + .../Traits/Actions/MassDeleteTraitTest.php | 10 +- .../Traits/Actions/SendTraitTest.php | 15 +- .../Traits/Actions/UpdateTraitTest.php | 10 +- .../Traits/Params/ExpandTraitTest.php | 17 +- .../Traits/Params/FilterTraitTest.php | 35 +- .../Traits/Params/LimitOffsetTraitTest.php | 17 +- .../Traits/Params/OrderTraitTest.php | 17 +- .../Traits/Params/ParamTraitTest.php | 33 +- .../Traits/Params/SearchTraitTest.php | 17 +- .../Traits/Segments/AttributesTraitTest.php | 25 + .../Traits/Segments/ByIdCommonTraitTest.php | 24 + .../Segments/ByIdPositionedTraitTest.php | 24 + .../Traits/Segments/MetadataTraitTest.php | 25 + .../Traits/Segments/MethodCommonTraitTest.php | 24 + .../Traits/Segments/PositionsTraitTest.php | 25 + .../Traits/Segments/SettingsTraitTest.php | 25 + .../Api/{ => Query}/Traits/TraitTestCase.php | 4 +- tests/Unit/Api/QueryTest.php | 69 -- tests/Unit/Api/Record/AbstractRecordTest.php | 157 ++++ .../Record/Builders/CollectionBuilderTest.php | 77 ++ .../Api/Record/Builders/ObjectBuilderTest.php | 69 ++ .../Api/Record/Builders/RecordBuilderTest.php | 37 + .../Builders/RecordResolversTestCase.php | 37 + .../Traits/CollectionTraitCase.php | 33 + .../Traits/CrudCollectionTraitTest.php | 153 ++++ .../Traits/FillMetaCollectionTraitTest.php | 33 + .../Traits/IterateCollectionTraitTest.php | 110 +++ .../Collections/Traits/IteratorTraitTest.php | 30 + .../Traits/ParamsCollectionTraitTest.php | 221 +++++ .../Objects/AbstractConcreteObjectTest.php | 60 ++ .../Objects/Traits/CrudObjectTraitTest.php | 56 ++ .../Traits/FillMetaObjectTraitTest.php | 45 + .../Record/Objects/Traits/ObjectTraitCase.php | 33 + .../Objects/Traits/ParamsObjectTraitTest.php | 98 +++ .../Traits/SetIdInMetaHrefTraitTest.php | 72 ++ .../Api/Record/Objects/UnknownObjectTest.php | 46 ++ .../Api/Segments/Endpoints/EntityTest.php | 51 -- .../Traits/Segments/AttributesTraitTest.php | 25 - .../Traits/Segments/ByIdCommonTraitTest.php | 24 - .../Segments/ByIdPositionedTraitTest.php | 24 - .../Api/Traits/Segments/MetadataTraitTest.php | 25 - .../Traits/Segments/MethodCommonTraitTest.php | 24 - .../Traits/Segments/PositionsTraitTest.php | 25 - tests/Unit/Enums/HttpMethodTest.php | 39 + tests/Unit/Enums/QueryParamTest.php | 18 +- .../Unit/Exceptions/RequestExceptionTest.php | 142 ++++ tests/Unit/Formatters/ArrayFormatTest.php | 17 +- .../ExtendedObjects/ExtendedTestEmployee.php | 11 + .../ExtendedTestEmployeeCollection.php | 11 + .../ExtendedObjects/ExtendedTestProduct.php | 11 + .../ExtendedTestProductCollection.php | 11 + .../Unit/Formatters/MultiDecoderTestCase.php | 135 +-- tests/Unit/Formatters/RecordFormatTest.php | 204 +++++ tests/Unit/Formatters/RecordMappingTest.php | 150 ++++ tests/Unit/Formatters/StdClassFormatTest.php | 40 +- tests/Unit/Formatters/StringFormatTest.php | 11 +- tests/Unit/Http/ApiClientTest.php | 16 +- tests/Unit/Http/GuzzleSenderFactoryTest.php | 40 +- tests/Unit/Meta/MetaMakerTest.php | 41 + tests/Unit/MoySkladTest.php | 48 +- tests/Unit/Services/CollectionHelperTest.php | 41 + tests/Unit/Services/QueryParamsTest.php | 272 +++++++ tests/Unit/Services/RecordHelperTest.php | 30 + .../Unit/Services/RecordMappingHelperTest.php | 75 ++ tests/Unit/Services/UrlTest.php | 121 ++- tests/Unit/Tools/GuidTest.php | 40 +- tests/Unit/Tools/MetaTest.php | 82 +- 257 files changed, 8900 insertions(+), 2614 deletions(-) create mode 100644 docs/active_record.md create mode 100644 docs/api_interaction.md create mode 100644 docs/autocomplete.gif create mode 100644 docs/comments.png create mode 100644 docs/exceptions.md create mode 100644 docs/formatters.md create mode 100644 docs/index.md create mode 100644 docs/query_builder.md create mode 100644 docs/setup.md create mode 100644 docs/tools.md delete mode 100644 src/Api/Query.php rename src/Api/{ => Query}/AbstractBuilder.php (55%) rename src/Api/{ => Query}/Debug.php (65%) create mode 100644 src/Api/Query/QueryBuilder.php rename src/Api/{ => Query}/Segments/AbstractSegmentCommon.php (86%) rename src/Api/{ => Query}/Segments/AbstractSegmentNamed.php (81%) create mode 100644 src/Api/Query/Segments/ById/AbstractByIdSegment.php create mode 100644 src/Api/Query/Segments/ById/ByIdSegmentCommon.php create mode 100644 src/Api/Query/Segments/ById/ByIdSegmentPositioned.php create mode 100644 src/Api/Query/Segments/Endpoints/AbstractEndpointSegmentNamed.php create mode 100644 src/Api/Query/Segments/Endpoints/AuditSegment.php create mode 100644 src/Api/Query/Segments/Endpoints/EndpointSegmentCommon.php create mode 100644 src/Api/Query/Segments/Endpoints/EntitySegment.php create mode 100644 src/Api/Query/Segments/Endpoints/NotificationSegment.php create mode 100644 src/Api/Query/Segments/Endpoints/ReportSegment.php create mode 100644 src/Api/Query/Segments/Methods/AbstractMethodSegmentNamed.php create mode 100644 src/Api/Query/Segments/Methods/Documents/CustomerorderSegment.php create mode 100644 src/Api/Query/Segments/Methods/Entities/AssortmentSegment.php create mode 100644 src/Api/Query/Segments/Methods/Entities/EmployeeSegment.php create mode 100644 src/Api/Query/Segments/Methods/Entities/ProductSegment.php create mode 100644 src/Api/Query/Segments/Methods/MethodSegmentCommon.php create mode 100644 src/Api/Query/Segments/Methods/Nested/AttributesSegment.php create mode 100644 src/Api/Query/Segments/Methods/Nested/MetadataSegment.php create mode 100644 src/Api/Query/Segments/Methods/Nested/PositionsSegment.php create mode 100644 src/Api/Query/Segments/Methods/Nested/SettingsSegment.php create mode 100644 src/Api/Query/Segments/Special/MassDeleteSegment.php rename src/Api/{ => Query}/Traits/Actions/CreateTrait.php (77%) rename src/Api/{ => Query}/Traits/Actions/DebugTrait.php (51%) rename src/Api/{ => Query}/Traits/Actions/DeleteTrait.php (81%) rename src/Api/{ => Query}/Traits/Actions/GetGeneratorTrait.php (57%) rename src/Api/{ => Query}/Traits/Actions/GetTrait.php (70%) create mode 100644 src/Api/Query/Traits/Actions/MassCreateUpdateTrait.php create mode 100644 src/Api/Query/Traits/Actions/MassDeleteTrait.php rename src/Api/{ => Query}/Traits/Actions/SendTrait.php (64%) rename src/Api/{ => Query}/Traits/Actions/UpdateTrait.php (82%) create mode 100644 src/Api/Query/Traits/Params/ExpandTrait.php create mode 100644 src/Api/Query/Traits/Params/FilterTrait.php create mode 100644 src/Api/Query/Traits/Params/LimitOffsetTrait.php create mode 100644 src/Api/Query/Traits/Params/OrderTrait.php create mode 100644 src/Api/Query/Traits/Params/ParamTrait.php rename src/Api/{ => Query}/Traits/Params/SearchTrait.php (61%) create mode 100644 src/Api/Query/Traits/Segments/AttributesTrait.php create mode 100644 src/Api/Query/Traits/Segments/ByIdCommonTrait.php create mode 100644 src/Api/Query/Traits/Segments/ByIdPositionedTrait.php create mode 100644 src/Api/Query/Traits/Segments/MetadataTrait.php create mode 100644 src/Api/Query/Traits/Segments/MethodCommonTrait.php create mode 100644 src/Api/Query/Traits/Segments/PositionsTrait.php create mode 100644 src/Api/Query/Traits/Segments/SettingsTrait.php create mode 100644 src/Api/Record/AbstractConcreteRecord.php create mode 100644 src/Api/Record/AbstractRecord.php create mode 100644 src/Api/Record/AbstractUnknownRecord.php create mode 100644 src/Api/Record/AutocompleteHelpers/Alcoholic.php create mode 100644 src/Api/Record/AutocompleteHelpers/Barcode.php create mode 100644 src/Api/Record/AutocompleteHelpers/Image.php create mode 100644 src/Api/Record/AutocompleteHelpers/MetaCollection.php create mode 100644 src/Api/Record/AutocompleteHelpers/MetaObject.php create mode 100644 src/Api/Record/AutocompleteHelpers/Pack.php create mode 100644 src/Api/Record/AutocompleteHelpers/Price.php create mode 100644 src/Api/Record/AutocompleteHelpers/PriceWithType.php create mode 100644 src/Api/Record/Builders/AbstractBuilder.php create mode 100644 src/Api/Record/Builders/CollectionBuilder.php create mode 100644 src/Api/Record/Builders/ObjectBuilder.php create mode 100644 src/Api/Record/Builders/RecordBuilder.php create mode 100644 src/Api/Record/Collections/AbstractConcreteCollection.php create mode 100644 src/Api/Record/Collections/Documents/CustomerorderCollection.php create mode 100644 src/Api/Record/Collections/Entities/AssortmentCollection.php create mode 100644 src/Api/Record/Collections/Entities/EmployeeCollection.php create mode 100644 src/Api/Record/Collections/Entities/ProductCollection.php create mode 100644 src/Api/Record/Collections/Traits/CrudCollectionTrait.php create mode 100644 src/Api/Record/Collections/Traits/FillMetaCollectionTrait.php create mode 100644 src/Api/Record/Collections/Traits/IterateCollectionTrait.php create mode 100644 src/Api/Record/Collections/Traits/IteratorTrait.php create mode 100644 src/Api/Record/Collections/Traits/ParamsCollectionTrait.php create mode 100644 src/Api/Record/Collections/UnknownCollection.php create mode 100644 src/Api/Record/Objects/AbstractConcreteObject.php create mode 100644 src/Api/Record/Objects/Documents/Customerorder.php create mode 100644 src/Api/Record/Objects/Entities/Assortment.php create mode 100644 src/Api/Record/Objects/Entities/Employee.php create mode 100644 src/Api/Record/Objects/Entities/Product.php create mode 100644 src/Api/Record/Objects/Traits/CrudObjectTrait.php create mode 100644 src/Api/Record/Objects/Traits/FillMetaObjectTrait.php create mode 100644 src/Api/Record/Objects/Traits/ParamsObjectTrait.php create mode 100644 src/Api/Record/Objects/Traits/SetIdInMetaHrefTrait.php create mode 100644 src/Api/Record/Objects/UnknownObject.php delete mode 100644 src/Api/Segments/ById/AbstractById.php delete mode 100644 src/Api/Segments/ById/ByIdCommon.php delete mode 100644 src/Api/Segments/ById/ByIdPositioned.php delete mode 100644 src/Api/Segments/Endpoints/AbstractEndpointNamed.php delete mode 100644 src/Api/Segments/Endpoints/Audit.php delete mode 100644 src/Api/Segments/Endpoints/EndpointCommon.php delete mode 100644 src/Api/Segments/Endpoints/Entity.php delete mode 100644 src/Api/Segments/Endpoints/Notification.php delete mode 100644 src/Api/Segments/Endpoints/Report.php delete mode 100644 src/Api/Segments/Methods/AbstractMethodNamed.php delete mode 100644 src/Api/Segments/Methods/Documents/Customerorder.php delete mode 100644 src/Api/Segments/Methods/Entities/Assortment.php delete mode 100644 src/Api/Segments/Methods/Entities/Product.php delete mode 100644 src/Api/Segments/Methods/MethodCommon.php delete mode 100644 src/Api/Segments/Methods/Nested/Attributes.php delete mode 100644 src/Api/Segments/Methods/Nested/Metadata.php delete mode 100644 src/Api/Segments/Methods/Nested/Positions.php delete mode 100644 src/Api/Segments/Special/MassDelete.php delete mode 100644 src/Api/Traits/Actions/MassDeleteTrait.php delete mode 100644 src/Api/Traits/Params/ExpandTrait.php delete mode 100644 src/Api/Traits/Params/FilterTrait.php delete mode 100644 src/Api/Traits/Params/LimitOffsetTrait.php delete mode 100644 src/Api/Traits/Params/OrderTrait.php delete mode 100644 src/Api/Traits/Params/ParamTrait.php delete mode 100644 src/Api/Traits/Segments/AttributesTrait.php delete mode 100644 src/Api/Traits/Segments/ByIdCommonTrait.php delete mode 100644 src/Api/Traits/Segments/ByIdPositionedTrait.php delete mode 100644 src/Api/Traits/Segments/MetadataTrait.php delete mode 100644 src/Api/Traits/Segments/MethodCommonTrait.php delete mode 100644 src/Api/Traits/Segments/PositionsTrait.php create mode 100644 src/Dictionaries/Document.php create mode 100644 src/Dictionaries/Endpoint.php create mode 100644 src/Dictionaries/Entity.php create mode 100644 src/Formatters/RecordFormat.php create mode 100644 src/Formatters/RecordMapping.php create mode 100644 src/Formatters/WithMoySkladInterface.php create mode 100644 src/Meta/MetaMaker.php create mode 100644 src/Services/CollectionHelper.php create mode 100644 src/Services/QueryParams.php create mode 100644 src/Services/RecordHelper.php create mode 100644 src/Services/RecordMappingHelper.php delete mode 100644 tests/Feature/Api/Builders/Endpoints/AuditTest.php delete mode 100644 tests/Feature/Api/Builders/Endpoints/ReportTest.php delete mode 100644 tests/Feature/Api/Builders/Methods/Documents/CustomerorderTest.php delete mode 100644 tests/Feature/Api/Builders/Methods/Entities/AssortmentTest.php delete mode 100644 tests/Feature/Api/Builders/Methods/Entities/ProductTest.php rename tests/Feature/Api/{ => Query}/ApiTestCase.php (92%) rename tests/Feature/Api/{ => Query}/Builders/ById/ByIdCommonTest.php (67%) rename tests/Feature/Api/{ => Query}/Builders/ById/ByIdPositionedTest.php (74%) create mode 100644 tests/Feature/Api/Query/Builders/Endpoints/AuditTest.php rename tests/Feature/Api/{ => Query}/Builders/Endpoints/EndpointCommonTest.php (64%) rename tests/Feature/Api/{ => Query}/Builders/Endpoints/EntityTest.php (69%) rename tests/Feature/Api/{ => Query}/Builders/Endpoints/NotificationTest.php (50%) create mode 100644 tests/Feature/Api/Query/Builders/Endpoints/ReportTest.php create mode 100644 tests/Feature/Api/Query/Builders/Methods/Documents/CustomerorderTest.php create mode 100644 tests/Feature/Api/Query/Builders/Methods/Entities/AssortmentTest.php create mode 100644 tests/Feature/Api/Query/Builders/Methods/Entities/ProductTest.php rename tests/Feature/Api/{ => Query}/Builders/Methods/MethodCommonTest.php (51%) rename tests/Feature/Api/{ => Query}/Builders/Methods/Nested/AttributesTest.php (52%) rename tests/Feature/Api/{ => Query}/Builders/Methods/Nested/MetadataTest.php (51%) rename tests/Feature/Api/{ => Query}/Builders/Methods/Nested/PositionsTest.php (51%) rename tests/Feature/Api/{ => Query}/Builders/Methods/Special/DebugTest.php (89%) create mode 100644 tests/Traits/MoySkladMockerTrait.php rename tests/Unit/Api/{ => Query}/ApiTestCase.php (93%) rename tests/Unit/Api/{ => Query}/DebugTest.php (91%) create mode 100644 tests/Unit/Api/Query/QueryBuilderTest.php rename tests/Unit/Api/{ => Query}/Segments/AbstractSegmentCommonTest.php (75%) rename tests/Unit/Api/{ => Query}/Segments/AbstractSegmentNamedTest.php (67%) create mode 100644 tests/Unit/Api/Query/Segments/Endpoints/EntityTest.php rename tests/Unit/Api/{ => Query}/Segments/Special/MassDeleteTest.php (67%) rename tests/Unit/Api/{ => Query}/Traits/Actions/CreateTraitTest.php (63%) rename tests/Unit/Api/{ => Query}/Traits/Actions/DebugTraitTest.php (54%) rename tests/Unit/Api/{ => Query}/Traits/Actions/DeleteTraitTest.php (62%) rename tests/Unit/Api/{ => Query}/Traits/Actions/GetGeneratorTraitTest.php (58%) rename tests/Unit/Api/{ => Query}/Traits/Actions/GetTraitTest.php (62%) create mode 100644 tests/Unit/Api/Query/Traits/Actions/MassCreateUpdateTraitTest.php rename tests/Unit/Api/{ => Query}/Traits/Actions/MassDeleteTraitTest.php (65%) rename tests/Unit/Api/{ => Query}/Traits/Actions/SendTraitTest.php (76%) rename tests/Unit/Api/{ => Query}/Traits/Actions/UpdateTraitTest.php (63%) rename tests/Unit/Api/{ => Query}/Traits/Params/ExpandTraitTest.php (87%) rename tests/Unit/Api/{ => Query}/Traits/Params/FilterTraitTest.php (86%) rename tests/Unit/Api/{ => Query}/Traits/Params/LimitOffsetTraitTest.php (82%) rename tests/Unit/Api/{ => Query}/Traits/Params/OrderTraitTest.php (88%) rename tests/Unit/Api/{ => Query}/Traits/Params/ParamTraitTest.php (86%) rename tests/Unit/Api/{ => Query}/Traits/Params/SearchTraitTest.php (75%) create mode 100644 tests/Unit/Api/Query/Traits/Segments/AttributesTraitTest.php create mode 100644 tests/Unit/Api/Query/Traits/Segments/ByIdCommonTraitTest.php create mode 100644 tests/Unit/Api/Query/Traits/Segments/ByIdPositionedTraitTest.php create mode 100644 tests/Unit/Api/Query/Traits/Segments/MetadataTraitTest.php create mode 100644 tests/Unit/Api/Query/Traits/Segments/MethodCommonTraitTest.php create mode 100644 tests/Unit/Api/Query/Traits/Segments/PositionsTraitTest.php create mode 100644 tests/Unit/Api/Query/Traits/Segments/SettingsTraitTest.php rename tests/Unit/Api/{ => Query}/Traits/TraitTestCase.php (68%) delete mode 100644 tests/Unit/Api/QueryTest.php create mode 100644 tests/Unit/Api/Record/AbstractRecordTest.php create mode 100644 tests/Unit/Api/Record/Builders/CollectionBuilderTest.php create mode 100644 tests/Unit/Api/Record/Builders/ObjectBuilderTest.php create mode 100644 tests/Unit/Api/Record/Builders/RecordBuilderTest.php create mode 100644 tests/Unit/Api/Record/Builders/RecordResolversTestCase.php create mode 100644 tests/Unit/Api/Record/Collections/Traits/CollectionTraitCase.php create mode 100644 tests/Unit/Api/Record/Collections/Traits/CrudCollectionTraitTest.php create mode 100644 tests/Unit/Api/Record/Collections/Traits/FillMetaCollectionTraitTest.php create mode 100644 tests/Unit/Api/Record/Collections/Traits/IterateCollectionTraitTest.php create mode 100644 tests/Unit/Api/Record/Collections/Traits/IteratorTraitTest.php create mode 100644 tests/Unit/Api/Record/Collections/Traits/ParamsCollectionTraitTest.php create mode 100644 tests/Unit/Api/Record/Objects/AbstractConcreteObjectTest.php create mode 100644 tests/Unit/Api/Record/Objects/Traits/CrudObjectTraitTest.php create mode 100644 tests/Unit/Api/Record/Objects/Traits/FillMetaObjectTraitTest.php create mode 100644 tests/Unit/Api/Record/Objects/Traits/ObjectTraitCase.php create mode 100644 tests/Unit/Api/Record/Objects/Traits/ParamsObjectTraitTest.php create mode 100644 tests/Unit/Api/Record/Objects/Traits/SetIdInMetaHrefTraitTest.php create mode 100644 tests/Unit/Api/Record/Objects/UnknownObjectTest.php delete mode 100644 tests/Unit/Api/Segments/Endpoints/EntityTest.php delete mode 100644 tests/Unit/Api/Traits/Segments/AttributesTraitTest.php delete mode 100644 tests/Unit/Api/Traits/Segments/ByIdCommonTraitTest.php delete mode 100644 tests/Unit/Api/Traits/Segments/ByIdPositionedTraitTest.php delete mode 100644 tests/Unit/Api/Traits/Segments/MetadataTraitTest.php delete mode 100644 tests/Unit/Api/Traits/Segments/MethodCommonTraitTest.php delete mode 100644 tests/Unit/Api/Traits/Segments/PositionsTraitTest.php create mode 100644 tests/Unit/Enums/HttpMethodTest.php create mode 100644 tests/Unit/Exceptions/RequestExceptionTest.php create mode 100644 tests/Unit/Formatters/ExtendedObjects/ExtendedTestEmployee.php create mode 100644 tests/Unit/Formatters/ExtendedObjects/ExtendedTestEmployeeCollection.php create mode 100644 tests/Unit/Formatters/ExtendedObjects/ExtendedTestProduct.php create mode 100644 tests/Unit/Formatters/ExtendedObjects/ExtendedTestProductCollection.php create mode 100644 tests/Unit/Formatters/RecordFormatTest.php create mode 100644 tests/Unit/Formatters/RecordMappingTest.php create mode 100644 tests/Unit/Meta/MetaMakerTest.php create mode 100644 tests/Unit/Services/CollectionHelperTest.php create mode 100644 tests/Unit/Services/QueryParamsTest.php create mode 100644 tests/Unit/Services/RecordHelperTest.php create mode 100644 tests/Unit/Services/RecordMappingHelperTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6998f8..203182cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,62 @@ # Changelog -Все заметные изменения в проекте будут задокументированы в этом файле. Формат основан на [Keep a Changelog](https://keepachangelog.com/ru), и этот проект придерживается семантического версионирования ([semver](https://semver.org/lang/ru/)). +Все существенные изменения в проекте будут задокументированы в этом файле. Формат основан на [Keep a Changelog](https://keepachangelog.com/), и этот проект придерживается семантического версионирования ([semver](https://semver.org/)). + +## v0.8.0 [[Upgrade guide](/UPGRADE.md#v080-changelog)] + +### Added + +- Реализована работа с API через [Active Record объекты](/docs/active_record.md). + +```php +use Evgeek\Moysklad\Api\Record\Objects\Entities\Product; +use Evgeek\Moysklad\MoySklad; + +$ms = new MoySklad(['token']) +$product = Product::make($ms, ['name' => 'orange'])->create(); +$product->code = '1234567'; +$product->update(); +``` + +- В конструктор запросов добавлен метод `massCreateUpdate()`, позволяющий создавать и/или обновлять по нескольку объектов за раз. + +- В конструктор запросов добавлен метод `fromUrl($url, $withParams)`, позволяющий строить запросы из уже имеющегося url: + +```php +$orderUrl = 'https://online.moysklad.ru/api/remap/1.2/entity/customerorder/3aba2611-c64f-11ed-0a80-108a00230a9c'; +$orderPositions = $ms + ->query() + ->fromUrl($orderUrl) + ->positions() + ->get(); +``` + +- В ошибку запроса `Evgeek\Moysklad\Exceptions\RequestException` добавлены методы: + - `getRequest()` - возвращает PSR-7 объект HTTP запроса, если он существует. + - `getResponse()` - возвращает PSR-7 объект HTTP ответа, если он существует. + - `getContent()` - Возвращает содержимое HTTP ответа, отформатированное текущим форматтером, или `null` в случае отсутствия содержимого. + +### Changed + +- Переписана документация. +- Реорганизация namespace `Evgeek\Moysklad\Api`. +- Методы `Evgeek\Moysklad\Formatters\JsonFormatterInterface` теперь динамические. +- Аргументы в методе `Meta::state()` приведены к общей логике. +- Максимальное количество символов ответа от API по умолчанию в `Evgeek\Moysklad\Http\GuzzleSenderFactory` увеличено до 4000. + +### Removed + +- Удалён устаревший метод `filters()`. Его функциональность теперь целиком возложена на `filter()`. + +### Deprecated + +- Явная установка форматирования в хелпере `Meta`. +- `Meta::entity()`. Вместо этого метода используйте `Meta::create()`. ## v0.7.0 [[Upgrade guide](/UPGRADE.md#v070-changelog)] ### Added + - В методы для формирования query-параметров запроса, подразумеющих возможность передачи нескольких значений (`filter()`, `order()`, `expand()` и `params()`) можно передавать несколько наборов значений за раз при помощи массива массивов. Примеры есть в [README](/##параметры-запроса) и PHPDoc методов. - Полное покрытие проекта unit тестами. diff --git a/README.md b/README.md index 3b09cda2..c3d023d3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # SDK для работы с API v1.2 сервиса "Мой Склад" -Идея этой библиотеки - максимально лёгкий и гибкий SDK, позволяющий работать с [API 1.2](https://dev.moysklad.ru/doc/api/remap/1.2) и `PHP 8.1+`. +Лёгкая и универсальная библиотека, позволяющий работать с [API 1.2](https://dev.moysklad.ru/doc/api/remap/1.2) и `PHP 8.1+`. -Библиотека находится в разработке, версии до `v1.0.0` могут не обладать обратной совместимостью. Список изменений можно найти в [Changelog](CHANGELOG.md). Инструкция по обновлению для версий, не поддерживающих обратную совместимость - [Upgrade guide](UPGRADE.md). +Находится в разработке, версии до `v1.0.0` могут не обладать обратной совместимостью. Список изменений можно найти в [Changelog](CHANGELOG.md). Инструкция по обновлению для версий, не поддерживающих обратную совместимость - [Upgrade guide](UPGRADE.md). ## Установка @@ -10,244 +10,80 @@ composer require evgeek/moysklad ``` -## Настройка +## Быстрый старт ```php -//Минимум -$ms = new \Evgeek\Moysklad\MoySklad(['token']); - -//С подробностями -$ms = new \Evgeek\Moysklad\MoySklad( - credentials: ['login', 'password'], - formatter: new \Evgeek\Moysklad\Formatters\StdClassFormat(), - requestSenderFactory: new \Evgeek\Moysklad\Http\GuzzleSenderFactory(retires: 3, exceptionTruncateAt: 4000) -); -``` - -* `credentials` - массив с учётными данными. Можно использовать либо токен, либо логин/пароль. -* `formatter` - объект, преобразующий json-строку ответа от API в нужный формат, и наоборот - передаваемый payload в json-строку. Должен реализовывать `\Evgeek\Moysklad\Formatters\JsonFormatterInterface`. Встроенные форматтеры - `StdClassFormat` (по умолчанию), `ArrayFormat`, и `StringFormat`. Все встроенные форматтеры могут принимать в качестве payload `stdClass`, `array` и `string`. -* `requestSenderFactory` - фабрика, создающая объект для отправки http-запросов. По умолчанию библиотека для этих целей использует [Guzzle](https://github.com/guzzle/guzzle). В стандартный `GuzzleSenderFactory()` в качестве аргументов можно передать желаемое количество попыток повтора запроса в случае неудачи (по умолчанию 0, задержка между повторами экспоненциальна) и максимальный размер сообщения об ошибке (по умолчанию 120 символов). Фабрика и отправитель реализованы через простые `PSR-7` совместимые интерфейсы, поэтому не составит труда как просто настроить клиент Guzzle под свои предпочтения, так и реализовать собственный способ отправки. - -## Базовое использование +use Evgeek\Moysklad\Api\Record\Objects\Entities\Product; +use Evgeek\Moysklad\MoySklad; -Взаимодействовать с API можно как при помощи реализованных в SDK сущностей, так и при помощи универсальных методов, которые позволят собрать любой запрос. К примеру, запрос `GET https://online.moysklad.ru/api/remap/1.2/entity/customerorder/00001c03-5227-11e8-9ff4-315000132d57/positions?limit=2` можно построить так: +$ms = new MoySklad(['token']); -```php -$ms->query() +//Конструктор запросов +$product = $ms->query() ->entity() - ->customerorder() - ->byId('00001c03-5227-11e8-9ff4-315000132d57') - ->positions() - ->limit(2) + ->product() + ->byId('25cf41f2-b068-11ed-0a80-0e9700500d7e') ->get(); -``` -Или так: - -```php -$ms->query() - ->endpoint('entity') - ->method('customerorder') - ->byId('00001c03-5227-11e8-9ff4-315000132d57') - ->method('positions') - ->param('limit', '2') - ->send('GET'); +//Active Record +$product = Product::make($ms); +$product->id = '25cf41f2-b068-11ed-0a80-0e9700500d7e'; +$product->get(); +//Или +$product = Product::make($ms, ['id' => '25cf41f2-b068-11ed-0a80-0e9700500d7e'])->get(); ``` -Список универсальных методов: - -* `endpoint()` - входная точка api: `entity`, `report` и т д. -* `method()` - шаг вложенности url. Вложенностей может быть несколько -* `byId()` - аналогично, шаг вложенности, но предназначен для идентификаторов сущностей. От `method()` отличается только набором методов. -* `param()` - для формирования query-параметров url -* `send()` - отправляет http-запрос и возвращает его результат. Параметры `method` и `body` позволяют задать http-метод запроса и его payload. +## Документация -## Параметры запроса +* [Настройка клиента](/docs/setup.md) +* [Взаимодействие с API](/docs/api_interaction.md) +* [Форматтеры](/docs/formatters.md) +* [Вспомогательные инструменты](/docs/tools.md) +* [Обработка исключений](/docs/exceptions.md) -Для формирования параметров запроса, помимо `param()`, есть несколько более удобных методов: +## Особенности -* `limit()` - ограничение количества записей в ответе -* `offset()` - сдвиг для пагинации -* `search()` - контекстный поиск ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-kontextnyj-poisk)) -* `expand()` - разворачивание вложенных сущностей ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand)). Несколько полей можно задать при помощи нескольких вызовов метода, или передав в метод массив с названиями полей. Помните, что разворачивание работает только с limit <= 100 и до 3-го уровня вложенности (ограничение API): +Библиотека предоставляет два подхода к работе с API - конструктор запросов (Query) и объектный (Record). Подходы полностью совместимы и взаимозаменяемы. -```php -$ms->query()->entity()->product() - ->limit(100) - ->expand('owner') - ->expand('minPrice.currency') - ->expand(['group', 'images']); -``` +### Конструктор запросов ([Документация](/docs/query_builder.md)) -* `filter()` - фильтрация результатов выдачи ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter)). В метод можно передать три параметра (ключ, знак и значение), или только два (ключ и значение, в качестве знака по умолчанию будет использовано `=`). Знаком может быть строка (`'='`, `'!='` и пр.) или enum `Evgeek\Moysklad\Enums\FilterSign`. Несколько фильтров за раз можно передать как массив массивов с параметрами фильтрации: +Позволяет при помощи fluent-цепочки методов собрать абсолютно любой запрос к API Моего Склада. ```php -$product = $ms->query()->entity()->product() - ->filter('archived', false) - ->filter('name', '=~', 'apple') - ->filter([ - ['minimumBalance', '=', '0'], - ['code', FilterSign::NEQ, 123], - ]); -``` - -* `order()` - сортировка ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sortirowka-ob-ektow)). Если направление не задано, будет сортироваться по возрастанию (`asc`). Несколько сортировок можно передать или массивом массивов, или через несколько вызовов метода: - -```php -$ms->query()->entity()->product() - ->order('updated', 'asc') - ->order([ - ['code', 'desc'], - ['name'], - ]); -``` - -Нюансы: - -* И `param()`, и специализированные методы поддерживают дозапись в параметрах, где это возможно (`filter`, `expand`, `order`). В остальных параметрах ранее установленное значение перезаписывается. -* `param()`, аналогично прочим методам с дозаписью, поддерживает передачу нескольких наборов значений через массив массивов. -* `filter()` автоматом экранирует `;`. `param()` - нет. - -## Отправка запросов - -Помимо `send()`, для отправки http-запросов поддерживаются следующие методы: - -* `get()` - `GET` запрос для чтения данных -* `create()` - `POST` запрос для создания сущности -* `update()` - `PUT` запрос для обновления сущности -* `delete()` - `DELETE` запрос для удаления сущности -* `massDelete()` - `POST` запрос для массового удаления - -Тело для `POST` и `PUT` запросов можно передавать в любом поддерживаемом формате (массив, объект, json-строка). - -#### Дополнительные методы: - -* `getGenerator()` - метод для итерируемых сущностей. Возвращает генератор, перебирающий массив `rows` с текущего `offset` и до последнего элемента (с отправкой новых запросов, если это необходимо). Пагинация осуществляется с шагом `limit`. - -```php -$generator = $ms->query()->entity()->product()->limit(100)->search('orange')->getGenerator(); -foreach ($generator as $product) { - //... -} -``` - -* `debug()` - можно разместить перед любым `CRUD` методом, чтобы увидеть в подробностях, какой именно запрос будет отправлен: - -```php -$product = $ms - ->query() +$products = $ms->query() ->entity() ->product() - ->limit(1) - ->filter([ - ['archived', false], - ['name', '!=', 'tangerine'], - ]) - ->debug() + ->order('name') + ->limit(3) ->get(); -var_dump($product); -``` -```bash -object(stdClass)#28 (5) { - ["method"]=> - string(3) "GET" - ["url"]=> - string(101) "https://online.moysklad.ru/api/remap/1.2/entity/product?limit=1&filter=archived=false;name!=tangerine" - ["url_encoded"]=> - string(109) "https://online.moysklad.ru/api/remap/1.2/entity/product?limit=1&filter=archived%3Dfalse%3Bname%21%3Dtangerine" - ["headers"]=> - object(stdClass)#29 (2) { - ["Content-Type"]=> - string(16) "application/json" - ["Authorization"]=> - string(38) "Basic ###############################" - } - ["body"]=> - array(0) { - } +foreach ($products as $product) { + var_dump($product->name); } ``` -## Вспомогательные классы +### Объектный подход ([Документация](/docs/active_record.md)) -* `\Evgeek\Moysklad\Tools\Guid` - предназначен для работы с id (guid/uuid) сущностей: +Подход, основанный на концепции Active Record. Каждая сущность Моего Склада представлена отдельным классом, набор сущностей - коллекцией. Намного более лаконичный, хоть и менее универсальный, чем конструктор запросов, способ взаимодействия с API. ```php -$url = 'https://online.moysklad.ru/api/remap/1.2/entity/customerorder/00001c03-5227-11e8-9ff4-315000132d57/positions/00002107-5227-11e8-9ff4-315000132d58'; -var_dump(Guid::extractAll($url)); -var_dump(Guid::extractFirst($url)); -var_dump(Guid::extractLast($url)); +Product::collection($ms) + ->eachGenerator(function (Product $product) { + $product->name = mb_strtoupper($product->name); + $product->update(); + }); ``` -```bash -array(2) { - [0]=> - string(36) "00001c03-5227-11e8-9ff4-315000132d57" - [1]=> - string(36) "00002107-5227-11e8-9ff4-315000132d58" -} -string(36) "00001c03-5227-11e8-9ff4-315000132d57" -string(36) "00002107-5227-11e8-9ff4-315000132d58" -``` +Из прочих плюсов - возможность расширять объекты сущностей собственными методами и автоподсказки свойств для IDE с глубокой вложенностью. -* `\Evgeek\Moysklad\Tools\Meta` - помогает формировать метадату для создания новых объектов: +![autocomplete](/docs/autocomplete.gif) -```php -var_dump(Meta::organization('ec008e5b-f5ab-11e5-7a69-970f0019fa50')); -``` +### Документация -```bash -object(stdClass)#19 (3) { - ["href"]=> - string(97) "https://online.moysklad.ru/api/remap/1.2/entity/organization/ec008e5b-f5ab-11e5-7a69-970f0019fa50" - ["type"]=> - string(12) "organization" - ["mediaType"]=> - string(16) "application/json" -} -``` +Публичные методы тщательно документированы: описание, примеры кода, ссылки на документацию API. -Форматирование можно задать при помощи метода `Meta::setFormat()`. По умолчанию используется `StdClassFormat`. Учитывайте, что `Meta` и `MoySklad` используют разные форматтеры, то есть установка формата в одном классе не затронет другой. +![comment](/docs/comments.png) -Помимо небольшого набора предопределённых сущностей, можно сформировать любую мету при помощи универсального метода `Meta::create()` (и более узкого `Meta::entity()`). Примеры: +## Обратная связь -```php -Meta::setFormat(new ArrayFormat()); -$order = [ - 'name' => 'test_order', - 'organization' => ['meta' => Meta::create(['entity', 'organization', 'ec008e5b-f5ab-11e5-7a69-970f0019fa50'], 'organization')], - 'agent' => ['meta' => Meta::entity(['counterparty', '918e0c83-483c-11e7-7a69-93a700ee9dbd'], 'counterparty')], -]; -var_dump($order); -``` -```bash -array(3) { - ["name"]=> - string(10) "test_order" - ["organization"]=> - array(1) { - ["meta"]=> - array(3) { - ["href"]=> - string(97) "https://online.moysklad.ru/api/remap/1.2/entity/organization/ec008e5b-f5ab-11e5-7a69-970f0019fa50" - ["type"]=> - string(12) "organization" - ["mediaType"]=> - string(16) "application/json" - } - } - ["agent"]=> - array(1) { - ["meta"]=> - array(3) { - ["href"]=> - string(97) "https://online.moysklad.ru/api/remap/1.2/entity/counterparty/918e0c83-483c-11e7-7a69-93a700ee9dbd" - ["type"]=> - string(12) "counterparty" - ["mediaType"]=> - string(16) "application/json" - } - } -} -``` \ No newline at end of file +Буду рад видеть ваши идеи, пожелания и вопросы в [issues](https://github.com/evgeek/moysklad/issues). \ No newline at end of file diff --git a/UPGRADE.md b/UPGRADE.md index db8fb33f..bfb80d9c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,76 @@ # Upgrade guide +## v0.8.0 [[Changelog](/CHANGELOG.md#v080-upgrade-guide)] + +### Реорганизация namespace `Evgeek\Moysklad\Api`. + +Данный namespace используется fluent-цепочкой билдера запросов (`$ms->query()->...`), поэтому изменения в нём не влияют на работу библиотеки. Однако, если ваш проект явно использует этот namespace, проверьте следующее: + +До: + +- `Evgeek\Moysklad\Api\Query` +- `Evgeek\Moysklad\Api\Segments\Special\MassDelete` +- `Evgeek\Moysklad\Api\Segments\...` +- `Evgeek\Moysklad\Api\Traits\Segments\...` + +После: + +- `Evgeek\Moysklad\Api\Query\QueryBuilder` +- `Evgeek\Moysklad\Api\Query\Segments\Special\MassDelete` +- `Evgeek\Moysklad\Api\Query\Segments\...` +- `Evgeek\Moysklad\Api\Query\Traits\Segments\...` + +### Аргументы в методе `Meta::state()` приведены к общей логике. + +До: + +```php +Meta::state('25cf41f2-b068-11ed-0a80-0e9700500d7e', 'counterparty'); +``` + +После: + +```php +Meta::state('counterparty', '25cf41f2-b068-11ed-0a80-0e9700500d7e'); +``` + +### Deprecated установки форматирования в хелпере `Meta` + +До: + +```php +Meta::setFormat(new ArrayFormat()); +Meta::product('25cf41f2-b068-11ed-0a80-0e9700500d7e'); +``` + +После: + +```php +//Создание меты через основной объект MoySklad применит форматирование, заданное в MoySklad +$ms->meta()->product('25cf41f2-b068-11ed-0a80-0e9700500d7e'); + +//Альтернатива - явная передача форматтера в хелпер (по умолчанию - StdClassFormat) +Meta::product('25cf41f2-b068-11ed-0a80-0e9700500d7e', new ArrayFormat()) +``` + +### Методы `Evgeek\Moysklad\Formatters\JsonFormatterInterface` теперь динамические. + +Не требуется ничего менять, если вы не работали с форматтерами напрямую. + +До: + +```php +ArrayFormat::encode($entity); +StdClassFormat::decode($entity); +``` + +После: + +```php +(new ArrayFormat())->encode($entity); +(new StdClassFormat())->decode($entity); +``` + ## v0.7.0 [[Changelog](/CHANGELOG.md#v070-upgrade-guide)] ### Приведение метода `expand()` к общей логике. diff --git a/composer.json b/composer.json index be753156..8e044aac 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,9 @@ "guzzlehttp/guzzle": "^7.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.13", - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan": "^1.10", + "friendsofphp/php-cs-fixer": "^3.15", + "phpunit/phpunit": "^10.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 0f4bbeec..0a56187a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6b53ecf8a6ab06d93bbd96d29555c9e9", + "content-hash": "674e67e5ecb590f9883c5d8f7b36c3ba", "packages": [ { "name": "guzzlehttp/guzzle", @@ -914,77 +914,6 @@ }, "time": "2023-02-02T22:02:53+00:00" }, - { - "name": "doctrine/instantiator", - "version": "2.0.x-dev", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "d6eef505a6c46e963e54bf73bb9de43cdea70821" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d6eef505a6c46e963e54bf73bb9de43cdea70821", - "reference": "d6eef505a6c46e963e54bf73bb9de43cdea70821", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" - }, - "default-branch": true, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.x" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2023-01-04T15:42:40+00:00" - }, { "name": "doctrine/lexer", "version": "3.0.x-dev", @@ -1064,16 +993,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.14.4", + "version": "v3.15.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "1b3d9dba63d93b8a202c31e824748218781eae6b" + "reference": "d48755372a113bddb99f749e34805d83f3acfe04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/1b3d9dba63d93b8a202c31e824748218781eae6b", - "reference": "1b3d9dba63d93b8a202c31e824748218781eae6b", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/d48755372a113bddb99f749e34805d83f3acfe04", + "reference": "d48755372a113bddb99f749e34805d83f3acfe04", "shasum": "" }, "require": { @@ -1140,9 +1069,15 @@ } ], "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.14.4" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.15.1" }, "funding": [ { @@ -1150,7 +1085,7 @@ "type": "github" } ], - "time": "2023-02-09T21:49:13+00:00" + "time": "2023-03-13T23:26:30+00:00" }, { "name": "myclabs/deep-copy", @@ -1158,12 +1093,12 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "reference": "928a96f585b86224ebc78f8f09d0482cf15b04f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/928a96f585b86224ebc78f8f09d0482cf15b04f5", + "reference": "928a96f585b86224ebc78f8f09d0482cf15b04f5", "shasum": "" }, "require": { @@ -1171,11 +1106,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "default-branch": true, @@ -1202,7 +1138,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.x" }, "funding": [ { @@ -1210,7 +1146,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2023-03-08T17:24:01+00:00" }, { "name": "nikic/php-parser", @@ -1218,12 +1154,12 @@ "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + "reference": "0ffddce52d816f72d0efc4d9b02e276d3309ef01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ffddce52d816f72d0efc4d9b02e276d3309ef01", + "reference": "0ffddce52d816f72d0efc4d9b02e276d3309ef01", "shasum": "" }, "require": { @@ -1265,9 +1201,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + "source": "https://github.com/nikic/PHP-Parser/tree/4.x" }, - "time": "2023-01-16T22:05:37+00:00" + "time": "2023-03-06T22:12:36+00:00" }, { "name": "phar-io/manifest", @@ -1394,12 +1330,12 @@ "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "7da13f74db97ab5afb1d9d28df4d7c5b9bdca46f" + "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7da13f74db97ab5afb1d9d28df4d7c5b9bdca46f", - "reference": "7da13f74db97ab5afb1d9d28df4d7c5b9bdca46f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0166aef76e066f0dd2adc2799bdadfa1635711e9", + "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9", "shasum": "" }, "require": { @@ -1429,8 +1365,11 @@ "static analysis" ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.10.x" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -1446,48 +1385,49 @@ "type": "tidelift" } ], - "time": "2023-02-12T08:47:11+00:00" + "time": "2023-03-24T10:28:16+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "8083be52c6f5ba5a48eafa154e0aeefd96bda098" + "reference": "22daefe2842433aea935115282671e006bd7a7aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/8083be52c6f5ba5a48eafa154e0aeefd96bda098", - "reference": "8083be52c6f5ba5a48eafa154e0aeefd96bda098", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/22daefe2842433aea935115282671e006bd7a7aa", + "reference": "22daefe2842433aea935115282671e006bd7a7aa", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", + "nikic/php-parser": "^4.15", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-text-template": "^3.0", + "sebastian/code-unit-reverse-lookup": "^3.0", + "sebastian/complexity": "^3.0", + "sebastian/environment": "^6.0", + "sebastian/lines-of-code": "^2.0", + "sebastian/version": "^4.0", "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.1" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "10.1-dev" } }, "autoload": { @@ -1515,7 +1455,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/main" }, "funding": [ { @@ -1523,32 +1464,33 @@ "type": "github" } ], - "time": "2023-02-11T08:53:26+00:00" + "time": "2023-03-25T08:10:23+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "38b24367e1b340aa78b96d7cab042942d917bb84" + "reference": "45cdddffbcbcbe715ee290a62bc562284033db3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/38b24367e1b340aa78b96d7cab042942d917bb84", - "reference": "38b24367e1b340aa78b96d7cab042942d917bb84", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/45cdddffbcbcbe715ee290a62bc562284033db3c", + "reference": "45cdddffbcbcbe715ee290a62bc562284033db3c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1575,7 +1517,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/main" }, "funding": [ { @@ -1583,36 +1526,37 @@ "type": "github" } ], - "time": "2022-02-11T16:23:04+00:00" + "time": "2023-03-25T08:10:37+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "518b6bd48536d0d3adc4ae152526f730809d5da0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/518b6bd48536d0d3adc4ae152526f730809d5da0", + "reference": "518b6bd48536d0d3adc4ae152526f730809d5da0", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-pcntl": "*" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1638,7 +1582,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/main" }, "funding": [ { @@ -1646,32 +1591,33 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2023-03-25T08:10:47+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "427af13b5c8fb2ab07eea8d644fd51c69f634513" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/427af13b5c8fb2ab07eea8d644fd51c69f634513", + "reference": "427af13b5c8fb2ab07eea8d644fd51c69f634513", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1697,7 +1643,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/main" }, "funding": [ { @@ -1705,32 +1652,33 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2023-03-25T08:11:00+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "04cf386465fb064c7196c0b1aa602f7e69bc179d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/04cf386465fb064c7196c0b1aa602f7e69bc179d", + "reference": "04cf386465fb064c7196c0b1aa602f7e69bc179d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -1756,7 +1704,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/main" }, "funding": [ { @@ -1764,24 +1713,23 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2023-03-25T08:11:12+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "796aeeaa688d8bc7730f0e6d00ccebb380bd21cc" + "reference": "464d0777691244940d32f624a9e3e9417e12ab0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/796aeeaa688d8bc7730f0e6d00ccebb380bd21cc", - "reference": "796aeeaa688d8bc7730f0e6d00ccebb380bd21cc", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/464d0777691244940d32f624a9e3e9417e12ab0a", + "reference": "464d0777691244940d32f624a9e3e9417e12ab0a", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1791,35 +1739,35 @@ "myclabs/deep-copy": "^1.10.1", "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", - "sebastian/version": "^3.0.2" + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-invoker": "^4.0", + "phpunit/php-text-template": "^3.0", + "phpunit/php-timer": "^6.0", + "sebastian/cli-parser": "^2.0", + "sebastian/code-unit": "^2.0", + "sebastian/comparator": "^5.0", + "sebastian/diff": "^5.0", + "sebastian/environment": "^6.0", + "sebastian/exporter": "^5.0", + "sebastian/global-state": "^6.0", + "sebastian/object-enumerator": "^5.0", + "sebastian/recursion-context": "^5.0", + "sebastian/type": "^4.0", + "sebastian/version": "^4.0" }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files" }, + "default-branch": true, "bin": [ "phpunit" ], "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "10.1-dev" } }, "autoload": { @@ -1850,7 +1798,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/main" }, "funding": [ { @@ -1866,7 +1815,7 @@ "type": "tidelift" } ], - "time": "2023-02-11T08:52:39+00:00" + "time": "2023-03-25T08:03:19+00:00" }, { "name": "psr/cache", @@ -2081,28 +2030,29 @@ }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "9d5058141f2c5f251ad72cd1468a4da32e8edd22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/9d5058141f2c5f251ad72cd1468a4da32e8edd22", + "reference": "9d5058141f2c5f251ad72cd1468a4da32e8edd22", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -2125,7 +2075,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/main" }, "funding": [ { @@ -2133,32 +2084,33 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2023-03-25T08:07:21+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "69b9b2b07e0bf599c8b337692ad6e6847b60874c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/69b9b2b07e0bf599c8b337692ad6e6847b60874c", + "reference": "69b9b2b07e0bf599c8b337692ad6e6847b60874c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -2181,7 +2133,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/main" }, "funding": [ { @@ -2189,32 +2142,33 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2023-03-25T08:07:57+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "66517337595170e3d7be140b6e7c6edd08d7299b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/66517337595170e3d7be140b6e7c6edd08d7299b", + "reference": "66517337595170e3d7be140b6e7c6edd08d7299b", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2236,7 +2190,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/main" }, "funding": [ { @@ -2244,34 +2199,37 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2023-03-25T08:08:10+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1" + "reference": "1deea2c3a4cac3ecfdb50e851792c9aeae6e3f9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b247957a1c8dc81a671770f74b479c0a78a818f1", - "reference": "b247957a1c8dc81a671770f74b479c0a78a818f1", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1deea2c3a4cac3ecfdb50e851792c9aeae6e3f9b", + "reference": "1deea2c3a4cac3ecfdb50e851792c9aeae6e3f9b", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2310,7 +2268,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/main" }, "funding": [ { @@ -2318,33 +2277,34 @@ "type": "github" } ], - "time": "2022-09-14T12:46:14+00:00" + "time": "2023-03-25T08:08:23+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "42c278edd0d9b43bd0fce37a5570cb397583654f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/42c278edd0d9b43bd0fce37a5570cb397583654f", + "reference": "42c278edd0d9b43bd0fce37a5570cb397583654f", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" + "nikic/php-parser": "^4.10", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2367,7 +2327,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/main" }, "funding": [ { @@ -2375,33 +2336,34 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-03-25T08:08:35+00:00" }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "932a76019c3c6700411608a8399f1c1fe17ff8cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/932a76019c3c6700411608a8399f1c1fe17ff8cb", + "reference": "932a76019c3c6700411608a8399f1c1fe17ff8cb", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", + "phpunit/phpunit": "^10.0", "symfony/process": "^4.2 || ^5" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2433,7 +2395,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/main" }, "funding": [ { @@ -2441,35 +2404,36 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-03-25T08:08:46+00:00" }, { "name": "sebastian/environment", - "version": "5.1.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "6b148eed05f3c7a66923242a333b19653323368a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6b148eed05f3c7a66923242a333b19653323368a", + "reference": "6b148eed05f3c7a66923242a333b19653323368a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2488,7 +2452,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -2496,7 +2460,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/main" }, "funding": [ { @@ -2504,34 +2469,35 @@ "type": "github" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2023-03-25T08:08:56+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "47a1ad152a54e8090767efbbcacc14fb5f7957ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/47a1ad152a54e8090767efbbcacc14fb5f7957ef", + "reference": "47a1ad152a54e8090767efbbcacc14fb5f7957ef", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2573,7 +2539,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/main" }, "funding": [ { @@ -2581,38 +2548,36 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2023-03-25T08:09:05+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "ee4bb88dc81326d7b120139493e31faf81ad852e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ee4bb88dc81326d7b120139493e31faf81ad852e", + "reference": "ee4bb88dc81326d7b120139493e31faf81ad852e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2637,7 +2602,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/main" }, "funding": [ { @@ -2645,33 +2611,34 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-03-25T08:09:15+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "a579387619b7860db9c06886202139609869b9b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/a579387619b7860db9c06886202139609869b9b6", + "reference": "a579387619b7860db9c06886202139609869b9b6", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" + "nikic/php-parser": "^4.10", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -2694,7 +2661,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/main" }, "funding": [ { @@ -2702,34 +2670,35 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-03-25T08:09:24+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "38b1bf1b6008c8b4dbe5898ed57bc43683b161ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/38b1bf1b6008c8b4dbe5898ed57bc43683b161ff", + "reference": "38b1bf1b6008c8b4dbe5898ed57bc43683b161ff", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2751,7 +2720,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/main" }, "funding": [ { @@ -2759,32 +2729,33 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2023-03-25T08:09:36+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "503d118c4ffceb0079e4b752bd8e5755f1db30f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/503d118c4ffceb0079e4b752bd8e5755f1db30f2", + "reference": "503d118c4ffceb0079e4b752bd8e5755f1db30f2", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2806,7 +2777,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/main" }, "funding": [ { @@ -2814,32 +2786,33 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2023-03-25T08:09:45+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "646a4d7362051ca7fb3a9c74bc861822fb404936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/646a4d7362051ca7fb3a9c74bc861822fb404936", + "reference": "646a4d7362051ca7fb3a9c74bc861822fb404936", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2869,7 +2842,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/main" }, "funding": [ { @@ -2877,87 +2851,33 @@ "type": "github" } ], - "time": "2023-02-03T06:07:39+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "dev-main", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "e1157eac767e4dc4ae40dd9aab7fb4de6e56bd32" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/e1157eac767e4dc4ae40dd9aab7fb4de6e56bd32", - "reference": "e1157eac767e4dc4ae40dd9aab7fb4de6e56bd32", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/main" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-08T06:53:39+00:00" + "time": "2023-03-25T08:11:24+00:00" }, { "name": "sebastian/type", - "version": "3.2.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "reference": "47b6b8cfbb1b920be1d68c089cbbb50c69fa3cff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/47b6b8cfbb1b920be1d68c089cbbb50c69fa3cff", + "reference": "47b6b8cfbb1b920be1d68c089cbbb50c69fa3cff", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^10.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2980,7 +2900,8 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/main" }, "funding": [ { @@ -2988,29 +2909,30 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2023-03-25T08:11:54+00:00" }, { "name": "sebastian/version", - "version": "3.0.x-dev", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "4b1e2878a450aa48b6f959388b158bc739b7540b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/4b1e2878a450aa48b6f959388b158bc739b7540b", + "reference": "4b1e2878a450aa48b6f959388b158bc739b7540b", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3033,7 +2955,8 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/main" }, "funding": [ { @@ -3041,7 +2964,7 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2023-03-25T08:12:23+00:00" }, { "name": "symfony/console", @@ -3049,12 +2972,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a8a68f1cdb412a95add09c1bb9c1fa3a705837a5" + "reference": "516a2684d4545bc6dedbf162480d2a35d451b0ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a8a68f1cdb412a95add09c1bb9c1fa3a705837a5", - "reference": "a8a68f1cdb412a95add09c1bb9c1fa3a705837a5", + "url": "https://api.github.com/repos/symfony/console/zipball/516a2684d4545bc6dedbf162480d2a35d451b0ae", + "reference": "516a2684d4545bc6dedbf162480d2a35d451b0ae", "shasum": "" }, "require": { @@ -3116,7 +3039,7 @@ "homepage": "https://symfony.com", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], @@ -3137,7 +3060,7 @@ "type": "tidelift" } ], - "time": "2023-02-07T10:19:03+00:00" + "time": "2023-03-21T18:22:26+00:00" }, { "name": "symfony/event-dispatcher", @@ -3145,12 +3068,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "80c4d5226e870ee793e9dbdde20f6182b4c997f7" + "reference": "3aa523704d095e4dffe7ec8d6a8b212f793ea2ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/80c4d5226e870ee793e9dbdde20f6182b4c997f7", - "reference": "80c4d5226e870ee793e9dbdde20f6182b4c997f7", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3aa523704d095e4dffe7ec8d6a8b212f793ea2ad", + "reference": "3aa523704d095e4dffe7ec8d6a8b212f793ea2ad", "shasum": "" }, "require": { @@ -3221,7 +3144,7 @@ "type": "tidelift" } ], - "time": "2023-02-02T07:48:03+00:00" + "time": "2023-03-20T16:06:12+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -3229,12 +3152,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "0782b0b52a737a05b4383d0df35a474303cabdae" + "reference": "0ad3b6f1e4e2da5690fefe075cd53a238646d8dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0782b0b52a737a05b4383d0df35a474303cabdae", - "reference": "0782b0b52a737a05b4383d0df35a474303cabdae", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/0ad3b6f1e4e2da5690fefe075cd53a238646d8dd", + "reference": "0ad3b6f1e4e2da5690fefe075cd53a238646d8dd", "shasum": "" }, "require": { @@ -3285,7 +3208,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.2.1" }, "funding": [ { @@ -3301,7 +3224,7 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-03-01T10:32:47+00:00" }, { "name": "symfony/filesystem", @@ -3309,12 +3232,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "83ba0068dd89bcd23f6f7e1053992662e8a9c3e9" + "reference": "bf3226d895bb4cd6635ef42649ec4a5818e3bf01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/83ba0068dd89bcd23f6f7e1053992662e8a9c3e9", - "reference": "83ba0068dd89bcd23f6f7e1053992662e8a9c3e9", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/bf3226d895bb4cd6635ef42649ec4a5818e3bf01", + "reference": "bf3226d895bb4cd6635ef42649ec4a5818e3bf01", "shasum": "" }, "require": { @@ -3364,7 +3287,7 @@ "type": "tidelift" } ], - "time": "2023-02-11T13:27:49+00:00" + "time": "2023-02-14T09:04:20+00:00" }, { "name": "symfony/finder", @@ -3372,12 +3295,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d5678eb3905c8b43a093025dada98e8f6cb64219" + "reference": "f5891f0383dc22a615ebd5848e87328d1efad0be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d5678eb3905c8b43a093025dada98e8f6cb64219", - "reference": "d5678eb3905c8b43a093025dada98e8f6cb64219", + "url": "https://api.github.com/repos/symfony/finder/zipball/f5891f0383dc22a615ebd5848e87328d1efad0be", + "reference": "f5891f0383dc22a615ebd5848e87328d1efad0be", "shasum": "" }, "require": { @@ -3428,7 +3351,7 @@ "type": "tidelift" } ], - "time": "2023-02-11T13:27:49+00:00" + "time": "2023-02-14T09:04:20+00:00" }, { "name": "symfony/options-resolver", @@ -3436,12 +3359,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "2c368659436ca81328cae97f496453b1efd239bc" + "reference": "2fdfd4259397b1300da21c04ba52a673763b73c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2c368659436ca81328cae97f496453b1efd239bc", - "reference": "2c368659436ca81328cae97f496453b1efd239bc", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2fdfd4259397b1300da21c04ba52a673763b73c9", + "reference": "2fdfd4259397b1300da21c04ba52a673763b73c9", "shasum": "" }, "require": { @@ -3495,7 +3418,7 @@ "type": "tidelift" } ], - "time": "2023-02-02T07:48:03+00:00" + "time": "2023-02-14T09:04:20+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4001,12 +3924,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5e6dd40d75b48692363fe51db28cf8169b617e39" + "reference": "25e5f8662af68a432bef229d4f0939b393e8410e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5e6dd40d75b48692363fe51db28cf8169b617e39", - "reference": "5e6dd40d75b48692363fe51db28cf8169b617e39", + "url": "https://api.github.com/repos/symfony/process/zipball/25e5f8662af68a432bef229d4f0939b393e8410e", + "reference": "25e5f8662af68a432bef229d4f0939b393e8410e", "shasum": "" }, "require": { @@ -4054,7 +3977,7 @@ "type": "tidelift" } ], - "time": "2023-02-02T07:48:03+00:00" + "time": "2023-03-09T16:20:38+00:00" }, { "name": "symfony/service-contracts", @@ -4062,12 +3985,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75" + "reference": "a8c9cedf55f314f3a186041d19537303766df09a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/aac98028c69df04ee77eb69b96b86ee51fbf4b75", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", + "reference": "a8c9cedf55f314f3a186041d19537303766df09a", "shasum": "" }, "require": { @@ -4124,7 +4047,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" }, "funding": [ { @@ -4140,7 +4063,7 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-03-01T10:32:47+00:00" }, { "name": "symfony/stopwatch", @@ -4148,12 +4071,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "64bddc4466cbe143120e0ced9d7797f9d96b0e39" + "reference": "f7fa451783b748e7b5942b9afe889b8df062e935" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/64bddc4466cbe143120e0ced9d7797f9d96b0e39", - "reference": "64bddc4466cbe143120e0ced9d7797f9d96b0e39", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/f7fa451783b748e7b5942b9afe889b8df062e935", + "reference": "f7fa451783b748e7b5942b9afe889b8df062e935", "shasum": "" }, "require": { @@ -4202,7 +4125,7 @@ "type": "tidelift" } ], - "time": "2023-02-02T07:48:03+00:00" + "time": "2023-02-14T09:04:20+00:00" }, { "name": "symfony/string", @@ -4210,12 +4133,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "04d148afb950bc167d1666aa020f6cb8591576a0" + "reference": "01204629fd5c1db46e6c9181a5a2933c33bbfff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/04d148afb950bc167d1666aa020f6cb8591576a0", - "reference": "04d148afb950bc167d1666aa020f6cb8591576a0", + "url": "https://api.github.com/repos/symfony/string/zipball/01204629fd5c1db46e6c9181a5a2933c33bbfff8", + "reference": "01204629fd5c1db46e6c9181a5a2933c33bbfff8", "shasum": "" }, "require": { @@ -4288,7 +4211,7 @@ "type": "tidelift" } ], - "time": "2023-02-02T07:48:03+00:00" + "time": "2023-03-20T16:06:12+00:00" }, { "name": "theseer/tokenizer", diff --git a/docs/active_record.md b/docs/active_record.md new file mode 100644 index 00000000..320e226e --- /dev/null +++ b/docs/active_record.md @@ -0,0 +1,349 @@ +# Объектный подход (Record) + +Основан на шаблоне Active Record. Объект Record представляет собой одну запись соответствующего типа в Моём Складе, коллекция - набор объектов. + +* [Общая информация](/docs/active_record.md#общая-информация) + * [Свойства](/docs/active_record.md#свойства) + * [Приведение к простым типам](/docs/active_record.md#приведение-к-простым-типам) +* [Объекты](/docs/active_record.md#объекты) + * [Инициализация объекта](/docs/active_record.md#инициализация-объекта) + * [Параметры запроса объекта](/docs/active_record.md#параметры-запроса-объекта) + * [Методы отправки запросов объекта](/docs/active_record.md#методы-отправки-запросов-объекта) +* [Коллекции](/docs/active_record.md#коллекции) + * [Инициализация коллекции](/docs/active_record.md#инициализация-коллекции) + * [Параметры запроса коллекций](/docs/active_record.md#параметры-запроса-коллекций) + * [Методы отправки запросов коллекций](/docs/active_record.md#методы-отправки-запросов-коллекций) + * [Итерирование](/docs/active_record.md#итерирование) +* [Универсальные методы](/docs/active_record.md#универсальные-методы) +* [Расширяемость](/docs/active_record.md#расширяемость) + +## Общая информация + +### Свойства + +В классах объектов и коллекций при помощи PHPDoc реализованы подсказки имён и типов свойств для IDE. Впрочем, даже если свойство ещё не добавлено в PHPDoc, с ним всё равно можно работать. + +```php +use Evgeek\Moysklad\Api\Record\Objects\Entities\Product; +use Evgeek\Moysklad\MoySklad; + +$ms = new MoySklad(['token']); +$product = Product::make($ms, ['name' => 'orange'])->create(); +$product->unknownProperty = 123; + +var_dump($product->id, $product->name, $product->unknownProperty); +//string(36) "5444e6f7-0300-11ee-0a80-046f00689371" +//string(6) "orange" +//int(123) +``` + +Устанавливаемые свойства автоматический преобразуются в объекты, если это возможно. Данные в формате Моего Склада будут преобразованы в `Record`, остальные - в `stdClass`. + +```php +$product = Product::make($ms); +$product->owner = [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/entity/employee/f71b6eb9-a93d-11ed-0a80-0fba0011a679', + 'type' => 'employee', + ], +]; + +echo $product->owner->meta->type . PHP_EOL; +//employee +echo $product->owner->get()->name . PHP_EOL; +//Evgeniy +``` + +### Приведение к простым типам + +Любой Record можно привести к простому типу при помощи методов `toArray()`, `toString()` и `toStdClass()`. Это удобно для отладки. + +```php +$product = Product::make($ms, ['name' => 'orange']); +var_dump($product->toArray()); +``` + +## Объекты + +Объект представляет собой сущность Моего Склада. Объект обладает свойствами сущности и методами для взаимодействия с API. Свойства могут быть как простыми типами (строка, число и т.д.), так и другими объектами. + +### Инициализация объекта + +Создать объект Record можно несколькими равноценными способами. Объект создаётся пустым, только со свойствами, переданными ему при инициализации, и автоматически устанавливаемыми метаданными. Для загрузки свойств из Моего Склада используйте [методы отправки запросов](/docs/active_record.md#методы-отправки-запросов-объекта). + +```php +use Evgeek\Moysklad\Api\Record\Objects\Entities\Product; + +$product1 = Product::make($ms); +$product2 = new Product($ms); +$product3 = $ms->record()->object()->product(); +``` + +Задать свойства объекта можно как при инициализации, так и после: + +```php +$product = Product::make($ms, ['name' => 'orange']); +$product->externalCode = '1234567'; +``` + +### Параметры запроса объекта + +* `expand($field)` - разворачивание вложенных сущностей ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand)). Несколько полей можно задать при помощи нескольких вызовов метода, или передав в метод массив с названиями полей. + +```php +$product = Product::make($ms, ['id' => '66046520-f26f-11ed-0a80-0f6000692310']) + ->expand('owner') + ->expand(['group', 'images']) + ->get(); +``` + +### Методы отправки запросов объекта + +Тело запроса формируется из свойств объекта. Все методы отправки запросов обновляют объект и возвращают его же. + +* `create()` - `POST` запрос для создания сущности в Моём Складе. У сущности не должно быть id. + +```php +$product = Product::make($ms, ['name' => 'orange'])->create(); +//Или +$product = Product::make($ms); +$product->name = 'orange'; +$product->create(); + +echo $product->id; +//c297fc79-0300-11ee-0a80-0350006b23fc +``` + +* `get()` - `GET` запрос, загружающий сущность из Моего Склада. У сущности должен быть задан id. + +```php +$product = Product::make($ms, ['id' => '9aa1b41b-f2fc-11ed-0a80-0f60007ec621']) + ->get(); +``` + +* `update($content)` - `PUT` запрос для обновления сущности. Изменяемые поля можно задать как через свойства класса, так и передав в качестве параметра метода в любом удобном формате (stdClass, массив, json-строка, Record). У сущности должен быть задан id. + +```php +$product = Product::make($ms, ['id' => '9aa1b41b-f2fc-11ed-0a80-0f60007ec621']) + ->update(['name' => 'orange']); +//Или +$product = Product::make($ms); +$product->id = '9aa1b41b-f2fc-11ed-0a80-0f60007ec621'; +$product->name = 'orange'; +$product->update(); +``` + +* `delete()` - `DELETE` запрос для удаления сущности. У сущности должен быть задан id. + +```php +Product::make($ms, ['id' => '9aa1b41b-f2fc-11ed-0a80-0f60007ec621']) + ->delete(); +``` + +## Коллекции + +Коллекция представляет собой набор объектов Record. + +### Инициализация коллекции + +Инициализировать коллекцию можно несколькими равноценными способами. Коллекция создаётся пустой. Для загрузки свойств из Моего Склада используйте [методы отправки запросов](/docs/active_record.md#методы-отправки-запросов-коллекций). + +```php +use Evgeek\Moysklad\Api\Record\Collections\Entities\ProductCollection; +use Evgeek\Moysklad\Api\Record\Objects\Entities\Product; + +$products1 = Product::collection($ms); +$products2 = new ProductCollection($ms); +$products3 = $ms->record()->collection()->product(); +``` + +### Параметры запроса коллекций + +* `limit($amount)` - ограничение количества записей в ответе. +* `offset($amount)` - сдвиг для пагинации. + +```php +$products = Product::collection($ms) + ->limit(100) + ->offset(200); +``` + +* `search($text)` - контекстный поиск ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-kontextnyj-poisk)). + +```php +Product::collection($ms) + ->search('tangerine'); +``` + +* `expand($field)` - разворачивание вложенных сущностей ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand)). Несколько полей можно задать при помощи нескольких вызовов метода, или передав в метод массив с названиями полей. Помните, что разворачивание работает только с limit <= 100 и до 3-го уровня вложенности (ограничение API). + +```php +$products = Product::collection($ms) + ->limit(100) + ->expand('owner') + ->expand(['group', 'images']); +``` + +* `filter()` - фильтрация результатов выдачи ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter)). В метод можно передать три параметра (ключ, знак и значение), или только два (ключ и значение, в качестве знака по умолчанию будет использовано `=`). Знаком может быть строка (`'='`, `'!='` и пр.) или enum `Evgeek\Moysklad\Enums\FilterSign`. Несколько фильтров за раз можно передать как массив массивов с параметрами фильтрации. + +```php +$products = Product::collection($ms) + ->filter('archived', false) + ->filter('name', '=~', 'apple') + ->filter([ + ['minimumBalance', '=', '0'], + ['code', FilterSign::NEQ, 123], + ]); +``` + +* `order()` - сортировка ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sortirowka-ob-ektow)). Если направление не задано, будет сортироваться по возрастанию (`asc`). Несколько сортировок можно передать или массивом массивов, или через несколько вызовов метода. + +```php +$products = Product::collection($ms) + ->order('updated', 'asc') + ->order([ + ['code', 'desc'], + ['name'], + ]); +``` + +### Методы отправки запросов коллекций + +Тело запроса формируется из свойств коллекции. Все методы отправки запросов обновляют объект коллекции и возвращают его же. + +* `get()` - `GET` запрос, загружающий коллекцию из Моего Склада. + +```php +$products = Product::collection($ms)->get(); +``` + +* `getNext()` - `GET` запрос для загрузки следующей страницы коллекции. Если следующая страница отсутствует - вернёт `null`; +* `getPrevious()` - `GET` запрос для загрузки предыдущей страницы коллекции. Принцип работы аналогичен `getNext()`. + +```php +$products = Product::collection($ms)->get(); +$productNext = $product->getNext(); +``` + +* `massDelete($objects)` - `POST` запрос для массового удаления. В метод требуется передать массив удаляемых сущностей в любом формате. Сущность может быть как получена из Моего Склада, так и сформирована при помощи соответствующего класса либо хелпера `Meta`. Вместо ручного формирования массива удаляемых сущностей, можно передать коллекцию, содержащую удаляемые сущности (или её свойство `rows`). Однако помните - удалены будут только сущности, загруженные в коллекцию, и если все подлежащие удалению сущности не вместились в лимит - потребуется вызывать удаление несколько раз, дозагружая сущности. + +```php +$product1 = $ms->query()->entity()->product()->byId('cc181c35-f259-11ed-0a80-00e900658c8f')->get(); +$product2 = Product::make($ms, ['id' => 'd540c409-f259-11ed-0a80-00e900658e53']); +$product3 = ['meta' => Meta::product('d540c409-f259-11ed-0a80-00e900658e53')]; + +Product::collection($ms)->massDelete([$product1, $product2, $product3]); + +//Или +$oranges = Product::collection($ms)->search('orange')->get(); +Product::collection($ms)->massDelete($oranges); +``` + +* `massCreateUpdate($objects)` - `POST` запрос для массового создания и/или обновления сущностей. Аналогично `massDelete()`, в качестве параметра требуется передать набор сущностей в любом формате, или коллекцию. Сущности с заданным id будут обновлены, без - созданы. Возвращает коллекцию, содержащую обновлённые объекты сущностей. + +```php +$product1 = ['name' => 'Корнишоны']; +$product2 = Product::make($ms, [ + 'id' => 'efcddaff-f308-11ed-0a80-09ee0084c2c6', + 'name' => 'Кабачки', +]); +$product3 = [ + 'meta' => Meta::product('1a4d67b8-f309-11ed-0a80-086800825780'), + 'name' => 'Патиссоны', +]; + +$products = Product::collection($ms) + ->massCreateUpdate([$product1, $product2, $product3]); + +//Или +$updatableProducts = Product::collection($ms)->get(); +$updatableProducts->each(function (Product $product) { + $product->name = mb_strtoupper($product->name); +}); +$products = Product::collection($ms)->massCreateUpdate($updatableProducts); +``` + +### Итерирование + +Мой Склад возвращает объекты в коллекциях в свойстве `rows`, поэтому перебор объектов, загруженных в коллекцию, можно осуществить циклом - `foreach($collection->rows as $entity)`. Помимо этого, коллекции реализууют интерфейс `Iterator`, поэтому их можно итерировать напрямую - `foreach($collection as $entity)`. + +Также можно воспользоваться методом `each()`, принимающим замыкание. Указывая тип сущности в замыкании, помните, что коллекции могут содержать разные сущности - допустим, `AssortmentCollection` может содержать товары, услуги, комплекты, серии и модификации. + +```php +Product::collection($ms) + ->get() + ->each(function (Product $product) { + echo $product->name . PHP_EOL; + }); +``` + +Вышеописанные способы подходят для перебора сущностей, загруженных в коллекцию. Если в Моём Складе сущностей больше, чем помещается в лимит, и требуется перебрать их все, необходимо организовать перебор API. Это можно сделать вручную, при помощи параметров запроса `limit()` и `offset()`, или при помощи методов `getNext()` и `getPrevious()`. + +Также имеется более удобный метод, который самостоятельно пройдёт по всему API, отправляя дополнительные запросы в случае необходимости. Метод учитывает параметры, заданные в `limit()` и `offset()`. В отличие от `each()`, коллекцию не нужно заранее загружать, метод сделает это сам. + +```php +Product::collection($ms) + ->limit(100) + ->eachGenerator(function (Product $product) { + echo $product->name . PHP_EOL; + }); +``` + +В случае, если вам обработать за раз сразу много объектов (допустим, изменить в Моём Складе, или записать в БД), взаимодействие с ними по одной в `eachGenerator()` может быть не лучшим с точки зрения производительности. Рассмотрите использование метода `eachCollectionGenerator()`. Он, подобно `eachGenerator()`, перебирает весь API, но в его замыкание объекты передаются не по одному, а сразу коллекциями. + +К примеру, перевести названия всех товаров в верхний регистр с минимальным количеством запросов к API можно следующим образом: + +```php +Product::collection($ms) + ->eachCollectionGenerator(function (ProductCollection $products) use ($ms) { + $products->each(function (Product $product) { + $product->name = mb_strtoupper($product->name); + }); + Product::collection($ms)->massCreateUpdate($products); + }); +``` + +## Универсальные методы + +Позволяют взаимодействовать с API в случае отсутствия нужных методов в библиотеке. + +* `param($key, $value)` - позволяет сформировать любой параметр запроса. Несколько параметров можно передать массивом массивов. + +```php +Product::make($ms, ['id' => '66046520-f26f-11ed-0a80-0f6000692310']) + ->param('expand', 'owner') + ->param([ + ['expand', 'group'], + ['expand', 'images,images.owner'], + ]) +``` + +* `send($method)` - позволяет отправить любой HTTP запрос. В качестве тела запроса будет отправлен сам объект. + +```php +$product = Product::make($ms, ['id' => '825c1a20-f2ff-11ed-0a80-0868007fddf4']); +$product->name = 'tangerine'; +$product->send('PUT'); +``` + +## Расширяемость + +Сущности Моего Склада, пока не реализованные в библиотеке, оборачиваются в `UnknownObject::class` и `UnknownCollection::class`. Через них же можно создавать соответствующие объекты и коллекции, а также взаимодействовать с API. Для инициализации им необходим массив `$path` с сегментами url и строка `$type` с типом, из них формируется meta сущности. + +```php +$report = UnknownObject::make($ms, ['report', 'money', 'plotseries'], 'moneyplotseries') + ->param('momentFrom', '2023-01-01') + ->param('momentTo', '2024-01-01') + ->param('interval', 'month') + ->get(); + +UnknownObject::collection($ms, ['entity', 'counterparty'], 'counterparty') + ->eachGenerator(function (UnknownObject $counterparty) { + echo $counterparty->name . PHP_EOL; + }); +``` + +Также вы можете регистрировать собственные классы сущностей, или расширять базовые. Подробнее об этом рассказано в разделе [Форматтеры](/docs/formatters.md). + +| [<< Конструктор запросов (Query)](/docs/query_builder.md) | [Оглавление](/docs/index.md) | [Форматтеры >>](/docs/formatters.md) | +|:----------------------------------------------------------|:----------------------------:|-------------------------------------:| \ No newline at end of file diff --git a/docs/api_interaction.md b/docs/api_interaction.md new file mode 100644 index 00000000..51edf0f0 --- /dev/null +++ b/docs/api_interaction.md @@ -0,0 +1,20 @@ +# Взаимодействие с API + +Библиотека позволяет формировать и отправлять запросы к API двумя способами - через [Конструктор запросов Query](/docs/query_builder.md) и при помощи [Объектного подхода Record](/docs/active_record.md). Проще работать с Record (лаконичный синтаксис и абстрагированность от API). Однако, в Record пока реализованы не все методы. В свою очередь, Query позволяет построить абсолютно любой запрос. + +Подходы полностью совместимы, поэтому рекомендую использовать объектный подход в качестве основного, а конструктором запросов решать ещё не реализованные в объектах задачи. + +## Терминология + +Состав URL (https://online.moysklad.ru/api/remap/1.2/entity/customerorder?limit=1&offset=2) +* `https://online.moysklad.ru/api/remap/1.2` - базовый url +* `entity`, `customerorder` - сегменты +* `limit=1`, `offset=2` - параметры запроса + +## Документация + +* [Конструктор запросов (Query)](/docs/query_builder.md) +* [Объектный подход (Record)](/docs/active_record.md) + +| [<< Настройка клиента](/docs/setup.md) | [Оглавление](/docs/index.md) | [Конструктор запросов (Query) >>](/docs/query_builder.md) | +|:---------------------------------------|:----------------------------:|----------------------------------------------------------:| \ No newline at end of file diff --git a/docs/autocomplete.gif b/docs/autocomplete.gif new file mode 100644 index 0000000000000000000000000000000000000000..8280b96557996709f633a55c362a8ce125e7ae53 GIT binary patch literal 201666 zcmeF&^;cA1{4f3qV1^#LW9ViG=@>ecPU-G$>F!QxL|UaghZF^cp%tW*1_cE~5$5v# zet!7=aDTXez`f74&N{#Cwe~sdwa>EM~=Q z!UC7}V>NVRe~4NNhpiV!$S2N+Xen~~lyUJQxcCIP__?`61-Oh_xt!l}KSV%+8^Ohm zc*Kp+;CYBaE05a}kJ~CAA1|MvB%ibozn~z$kP5%3xPY3P;6p@25mHtNRd0lGme50l zg@ttDgrzlvrS*j6g+)}QMS|amic5$pBOVFxK6>K%$V^{M6d^7nBd%mDZlEbGkCc^1 z$_w%!#RV1Ijg(ZMD64BIYg#B9WUHuYs2F&us;j9!)=|~9R<+epGxJq*s8aLPRV&g{ z7v@t}*Vd5b*N~Ic&@$FkQ`7V?)$})d?3DaC*zAd+#gp*5ClT#hPsO!t{IxZuw9Txw zLv?kuwRCi(bPSDj{4#XXKI)|J>q_zIni}gNCG_3H4K&q_jEo+}GS(3>(a|X6kgY?hZK#r+ftFpgzFq1od+%@uZ&^pzD#tJ-#{@Gc9W^H(Tc?O5r?e_( zt0-rCS?4G(7dugx$Yht2KG!&PH(x)usCf6H7EcQ;?^tgiWrS~8m!FQ9pQE8)XrzB2 zG9aKgI5{dfry|5wH^g1+nP=X!h=^zPsL-0;@YvY!vepO(`v`aAh@_xMZ_B6cuN12q@)K=z4RrzOB1&URb zWmL80RYw@TY>2CAX{v4St4num=tVV}3O4$9Hb!Z`Nwj$L-1kj`$D6kHmPn~qpNO_B z$97Mh4o{(u7qJ~J86E8ny&cxQeZ2#=f0b;4)uCcj>)U5&cl>>%J6d~Fp&10@Ar6=4Ac7zh9W;D4~N@gDAc4;SFSGD-S> zMe_fO>_OKN`%VbwxV`g}dpaC)S z<>; zItqYuh$MK_4_otwyFJ%SO1a?LjTJ(fpo4i1m&XWIQ?YU&ECf_9AXJ7`%)X{Ulw(Bj z2~=zrkBXW;_F_biFrvQ3R)WSQR@6JEw*ZK(dRgef&pVsp+n zl2BqAd0xeTl(Hj8om#DmD2SweRb9}}dD2v6b=g<|E`M*EL(AUZ5az=2{e10GG4BJ@^=9(Dzn>xso=Sj978C5!iLXV0Q7Ll(U<-r7-PB569 z?*uhTfb|81O{F`Ji=YZOLaDg1kdIE%bd|4u>pN;LbqLsqFky0ie};IjPED@I)y6Z0 zC@;VTX?CgCy!nk@DXuoMet7|u*-8hrgt|ry!^>PlT->pCSICSIOg<83Zm{fz63mC{ zEmpaP@}{RcX=wcWGg|Ratt>=uHYydRs$0m+?|Qya453~jbAD)E$KGc!NQJwYz$E?Y zGquqx`I4_4JmLJ`6lzB`aclD@GfEAdHC9#42-uiSDl5BXAuW(_Ir=zA&6Tdg2s7Qt zAOxOIbvHq4$>�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

ZzdhpX5 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 literal 0 HcmV?d00001 diff --git a/docs/comments.png b/docs/comments.png new file mode 100644 index 0000000000000000000000000000000000000000..4004cece188b7c417c1dd290a5447b2b4fb8bd84 GIT binary patch literal 58663 zcma&N1#leAvL!587Fu92OBORTGcz+Yvqmh7Ew;rxVrFIriR~~lc0n$ z)W^#U%J|pEIi|CSy0emvRh#KhLg+|KzNtb_NX5#?tiAx9Gf zXA3*qAIcUsCLl^Kwm;~Ze@K2D80i_AelW0bGSPF=v;B}!lCtG$x&;CG0U{wRpzM}* zvI_9RolSmtA0IhAYSXATmPiO+X{nN_VpvE5A^l3v0-f4Vy3-={CDm96YWR%Q`o|*) zf3<|G0W`65fDnt{&#K68tko+sHp544YrISye8d`R)kE<#!!hn_73k{COipLX96220 zX-=k5F{!CmKLWoCLLn00dYSP<5d?lm4*!ZE`0)V)5gPCF8wNro?$7In!o+SLrv$yf z#4kR({6fT}cdHZ~CE!|No^x;NRELsEmGZD2Cs2iw7$qSZi*nSxCi!TWmJaF<32wDx zoi8eWVc&m(N!|1c+U|2qLcHk7$2u@#cDl;98wKa|>o+U@=k4Pt2naU2*4DR@Szobe zCFIB`Y_URF(QjpL8+Tb0yOa!*0RwHN6ZID(=XzPWb@vlI+k52F%K5If4=7FLoQ*9= z{KJU_dsLf1k<-NV`bOS&O_ja zChd)+EAJ!7__;NKOV6vi<`8zigT*m}ZPUF`xo3XlfwI&^8DUt^ zuw6!9q$USotsznc0ZozJ8v;S^=pa_qEuvqm<2^2tZ)NngZ*#@h3MLk1!m7kQYohCx zim37nxv8oSN{E>qYRgJn$TZ$o=k#PStFmqcJSW#EyP$Q~p6=&rXZvy_!$%gbOC*=t zb94erYGSP~;mDoPS;vC!BQ$V+zdi4Tk%G`+*2SbM;n~%R{({QmyyeGwq+bDA&)_dcHJ`Y6R_(doznv z4qlW{1??I3#Gfi9OQIp@!#+a`&cB15Gq|3= zv505@Drx%#=T7KitzYr$)68KADbiD7)ER1 z*Y^yR6mc(!^GtuE>=tZ_KNoEXDe!+QjAvPe;&tN}P@&Ow7(haEoxcm~i>Uc~pcI4+ zN`&!BlIL~#VZ7;LRPJp~ylk|TIWq|(t+BJ*+Ouqs!3LS+Jt5$dfX>g=B+U#KAvbgR z*jB4-W8nEZx{|^fTX=O^3lW=A5hE1x-c7*IFz}n{*p)V2b*#!8^73nJav{#1c5Bq` zHIn(5XJT12pNPBFxf^s3h!TI9wPO5F$)(PU01AEZ8VP}Ek1w!bI`@}%z`KiyNhYs; zf#Z$%=p}b|5eR^W;NrC|&8+v4FK*&=C=J0EWY{&AEYa_1M+38%BMOKgUamiJ&Gy^0 zbGiC6u4>;+IGOYN#Or-QAfwBjng)<1`8z7?YKYi0RI^QITwfl*mTN&HP#jG-?^b;d1@($n7Bfpqv`W)+VI;i4V(9^H@yqDj7-hXrjZyFs_iBP|VQ0ux;r}kGonqqwh2Lr2B1@9*1+90=IK^HY9x7OPqwyHR;o6 zPAKM+H55k6L0w1mn&WX<`1DWF)#Tq_T(O>bq_advwyv;gyB2QQLzZ%vKZoC&v$MAc zF{4j;R|#8PQ*NC9Zd4zSCclkW0T_V!W~yavwl(@;WZ}=zaSm z-uJJv>Y5`uItgp#cJjc2gCZmf;r{v{4fg)V^NEJ;S~W0XBgOd>uN?Ax@xjlcjBxBM zh(`68qrgJ0MF_kt7;B=`F8N2lmqCmf*1um4;EW{6)YjY^>Gk;3^S%+9xX5BsQhdN) z_tE^-KTyfQoSY%?iHJmmT>+nnC@Sm^`Ee>3@$3H;Isa@JtW1(zfSxL`BDs z9%INw{!djSX=9Ll!v=pWexssLQI(JEtzj!BF?4eF*z)0!B)B(trJD3>aleM^)wEQb zjmZVnznDF48_6lL+)}N0iI65Ry?jFEmbG-+hQKE@Hor^j8jW9ETvaqUO(KLZc*F0( zF`4NM_E`kA-XGDXK!~p&un$32SPSVmurzH>`F->@JW0QAW;d7o$}zt_jKmYIbKWu9 zp6%iFnt!9%o%3)+qqNAp>L+yl`(1(pp<|9e52BF3>sAk~=Big>iEMz0W-g+nHVra^ zb5MXy-E?##z%wTFtUMa{rb7wRo=sT(BmzDgJY)hW+nFC8A^-U2=49Gq9F9!|~u{qG^KHhnq-GvMRlnRh1H< z{qZml7#;i}+hWq?c=;F8W=mJ>`G)6lP0pX~t`9dUS0FOg8(;P`Rtiy__Py(rm$Mu9 zTWZ`NrsSHAz0q}CS_M;b0+#V9a{6(`8qRb&5$LRT<{Hu;;L$7h&IC)D93PI$Ao#p0 zlU}hfn*HWMCLRwCPpQ2i1kuO0R%g9y*iP>1W?SXilPk(1HWm}LE-C*7S?2<-O`eJh zRUzyb&b3)^F4X^E7IE+b$UmW3M41CBci{)>^z*18uCFE=y4_t&$-VIl$X&Os3COIW z>GD>_FC3Prw?DObG2L@yxDnt3kMwPuyUIIeln%kX%B&wpMn{!;s@l)9OMF<#xmScE zA5SsVdU^aag5e0f@DBBF@!}`zA>8@YpB2&$Jui@SX&y{BuQBKhkLPEg@E2LzrFle=|%8+53n(;}ws(DR|2D{lWJ3;(b-gqy)bZ@h` z-N4`AYa*G5pS}?6=Za&FEZHz%-}^VXAXgLUSN`nwEa&NVP{?b{xgR;#MTK8?eH8|K zRvlQq-=~l+^;j_S2lY&J$+Xj04fTgL->^Ak)YF?d9HyuKy*k^Q z6~o^c*W!hT!U0(od4mq>`(`B09ANluGi7=2x`ZW*Xi1`Hd%&h47SE*HRG})xV;Mx+ zwCvQcKjAs-Y@#N%~j2p-G2^eSoT_l z?X3A~b>cek#Czb-@$rH?qrcz(rF|=z!SHy_u;$cSP%3At$xhJ$W5#)qYN*cHgDidB zf;j@N%L;W|{{nA?<_|gXFzCJ_eC6F99 z1WFq2!#dh=3!PDBct!&fxbnZ!e90QD(*n+G%-r3FeKdo;h=o23jh&6y4MPb=X^w7Z zoT<3!EioPzjmx>}don}n!5w!Ln=3d1fklnPU7Wn?eq2_}r+>r#FCv0$)|vb1aQngk z>&kzkCVw^r{*#ABP6(>3ZOR?EL&*{px1_~%xMN5){BQxi`>|gUkpKK&oNe{zU=WK8 z$CA4JQxmQ?aT(M_8B~Tx4!tgFaL|$=o|n}3R%QkzU7aA|leoHZ%YyxL%KbYfKl2(>#r%eqoxx?q(H0cnBpt~5{ zyk+UwlLz)OYrO}l>@HPE0%N!L)xzUO^UroQ6@_?u8*#dpPt0F;GHNa|Hi9*$iw-zhg7!<+CIjQeB z1D;1!Fr(hQntE)^!=ze`I^UqMkp+oeE$CXtIl_NtA2Ywq-!|uptcTzT6t5lK-W}dv zA1PtD^^;iw5Y8k>n_Jp_p8!Q54`E@AN9d@lO!mdSW^z3aCvcMK0pn)*Dtihm4$w+d zqdT})k*d(253Ezz(tUzz@o@)?yQ3y(2JZg))bq@6w-n{X+qN8)G^FKWmhQgtEd+QO zMw1TI#uU&;;Qx2=KYvx_U4F9mG2h_)`TH+aKAIhQQ+R|5=tEBNuy{DVq0Q`mM%(s# zBi6T~^aW+B3rP7Y`$w6k73<=y41AxFA;{=TDu!?|R$+YzuaIx*PWK_y&n~F9k?~%q zT=>-&X9yZk>Kikwly~1DGon~)v)YFbGHa$IhXv%tHsK= zrzRxP=L!L_ z0*mXPDyH)^k&$5Zdr5FI&}-q^Sl0mV3AFwyA&=&*B6gXXLYSX}!km1yVC zTa04+gZ@1oDR``9COGXptbDxEa>;9gRWqV73B0|>(^v}WS_w3#U-=6*L>Qx&dZx1o zF_F|Kp(@P&Sov4Ro(S1lTb8Uou)I7; zpoMk@s$1~2k&{OtkMh%h_4Z<3>kn;Bak2f`T^%in=IsPFczJ1VfWUBE$0VWV>xV{c zEj<#5+CyB3$SVVSj?S>jJp;}itl%|>< z!r?iyg+x6kwJ;|qXHgPBUuv^03wBkFl}qXZ91a4zrs6;1ezhtwk;~m>rzW%dI=Sbc zo0r@5%M#Iy1~rB1GJZC&oN#$0ZuuibRK^$ekV->1)k4t@FFPR7MxTaZX0cVvZBF)Z z5gKyw9AX*9MqZ@Y_zG0B*NBlkokZqttn)tT3Myc|y<7_yOY+cSeZT(8-2G|`e?qf0 zfY08di3dw(*Q(yT(o*eqSyjZGpmM32N0+H0^zXy_`#at>&r5pD2I<|<_Ynfgd;6Ul z#xviV$WF>N-_jT|gps|s5V}2q8o}i;Fzvb5T8N(=&Qx5MKAlT+<(Y4fr_482)$&B9 z4v%n&`X?S~LN-%2AOFJehN7vd30)L!`n7Pc8v$?;%|&;?b@sccv~`!3R?=F3fGN6a zI~Sf@c&sBB)ezB&t)r5~mKt$%l`S$+(0K^bP`Nqx=7|!s;w*h=7dL!1!v9)M# zxaW_h6CO9qL3i?sE(qnItHVz7FBuE@{~a}&e&ElxepfG3Rla@ccWo};zV!&vyL7VV zj+<8h?IZw&c+lN(B*X98;;pSLUR-S0usjZ6?Jv7jlr<6<11Av993P0c`%o<1vRmH z?~0`~1u^_c+~jR2NE*-3pBkhzbVQv!e!-8kP+DR{mm_hEPMc*oI|(svNIkzf`(oBvSRK-WKiJEMnU9YKP(bUg=H;@gplIlgkxl0hN3-v-pmryz>~H{*taf#BLa}N8NwfJ+ zh9Nfly;3Umc-6n_K2_oQ`T$`C`5E*43e90>Sj})g13MGqYgJOE4kvt6Jo=ylMsE*@ z%T2V}JIgQxRK;yhp6i6a&ZZ;cWVADZ6+Pg^qD+~>##PQfuq@BKyKW&Xbs0rnQFI9? zd-rjPd|?z$roXXJ{bg>Ie4WuDi@AjovM8v7(hie}MV301tv6NKRO%p6$6Y#`Fx4-{7^?o|tb{OzKnN;o! zNB6h5HQ}zPS7o=jDZ`ALFRpxO=#&Jl;If;rAk#? zV?f3?CuL41vtUsD2TltM*2%^9O!)qZ$(PK>=ejg<)~WI|Noggpx1xp3z9-ACzJ>UTPiV~!~?)OlY>_XNxy*N zLw$CYKdkzm&XI~aO*+|lPJA}Wi4MX-m~s*xBmPVIl9g3qe)f3IDydqzJG;3)y3SCr zXL)AmQYU_<)^&druYZR`XFo>ceo|LKY;cW~^9$JZk1_BM?KLJUJo#VR>%?u~cO6pM zh9KR+F*5v($8?%p+eV<>x)VB;S)Ts6#k(?fX`2P<{FB_(?K7WYpWHW&xa6K5E5WA| z?$uXmcxZ!-=)XEAH>Cv~Ly! zLDTuy6C_behu9c_eGl%(2tdczXjdm+2aNR&lH@q0(82{Acy8P>?{Z)d6Fn^0>cJD+ z1rwM&5prlja5eI1BTxEFMqxK9rcC3KU0dPTts6?oD3+;1*D1|);exd~ST#8CN0Av% zh{0})7j~;<3mYD9Iash0Q#KN<@P*eWha;R&W4SsPe5v?_rdaX(P@xK3!vSp6T2V3v z7tMReo=$o08E-{ViLwBC0B%SFi=nX?vdI^E#0GoDK}{a_RD_!ANXaE+rNQK-4hfHo zmvky2y!D@sAYelG+pbNQ9y*QQLWx;e@xs}fG7=U%xVCI#cktYzb}2&X=+KfZBqngq z&FRcQ^89XWDZ_|IWjtZV4xKq-nFDn`1QS zEp7Ux!vkg`z=Hp(1p)7TzshhxO7kDP6(`io4|2S@*dxXF2nNmrK=goQao2Kt>{K-@ zs4#pyI=>oS*>~Ft^71MU(6Fey@56%Obk0nkziW9=6va?YYix-%=b9TqWcvwxX%(e( zIwdZV4-FU*PH&@|Q`}?i)EQq6(kMM^idDiLNw%&$e)OPhgK*PYxJpjNuIZ_4T~BYH zOm83V>=gSHpGRtAOEH|`Z0O}tCS00nAHZ(X@TQUOpDs z4I@QYl;VZ}!ry_;M{HcMcqRD(z;9p(rBI(`&xu7>**#T(!C3foJ=03#3MB*UwS_yq z7w?NDRQ@D4y7D&yLGyR6U0IH4E%6iAMUF_fqdte0c~$^9uUCVEquyndw6X^C1=2 zUm;ylrG1tyz2)UL4Q%!+gboL^@lV>kK?2a??kIRaODooyx%rFHvr6j znW=OHlTo7wzfnk%fjNNay79p7KU}`|&Ryl7_invD(Z!?1Anr4$;Q3Th$vJH>?ZOievJG`&3+~jd4jcMWYUVD(Pj#>B#47-#aA+ZYapwy6zr?PfrG(EHUo!F+jBjkGEg~9m?L4~ z@V*=F3NmH9AtMxUZj@YAq^5riiwY_y`S|g^44c=Ef#)1cLp8!vye1%MCFqjP0`Us4 zI<1KeBp?=@h|E{!{DI}Pshz?yhz7k2ZOk+7kk*UXaagkIzYx^+HGnUI(r!F{eeweT zmfhfdX9sBx7uu~fcSlkZQaMqW)aF_jxU7N?_8{t*oYpICAusa_8b%tdrCKoDZe$#o z@|2m?*#XL{H~t9z%0yoOy}aE7FIpFEiEFP5j(dyh^S9R-2Ze*_t8Dg<#1+duEt|*l2^dC0cN!f7PT9}9y8^3SN0Z&t7FTYQ4eg$7yj# z@=O-T*3$UOqVOj3MeS-sn^m^)RST=pI6U%vB1^3B1UeG7u7OLipqWRAze4xt5IJtN z2fCbEsDw;}&{gDxS)08iNgm%^7pj?&8`xtpnS`oRKt57}O!POMrv!{lDfBI2M^GkH zF@w^wc#k}PDBM|-Ke7y5?(G@gXofGC#j;9{PEG^!r^}fmNuyXk?;DG7UQ2{4=cIPy z3~w|W7U3dnblYb6f^&THfdFzGC;$Jd1^5YOC?UNCsW)is9Tu8zq|&8D;)tk9pH~SUM`__}%rM>?f z-Dz&tdf2wh8%V=4&x<5TSmaFIqA&{X?%&Ju{M}!VO34;w2IFU}&dS|Rqzo)aL8EY z#SoJBEEBB$?y1s~rg)n9j^@#c9n4WLZV~g2Sl^Sjtw*e52?|>7)rc7NZs~rZp7g2HQnB zMK3x7vHlFFaCJzF4xYO`olbSVy9OockGX31$REmNZW!D|stsC zwYBk$Oj}pLtR?QP`K2{xu!En> zKxw7rpPvR*D_%W%Dv>!DJaLS2ruCG{Uq2c2q7xtunLY1IBO)%8lc<#p5gufk;1S0@Ks~cbE z48Y3FF#3N2eQEtex?pd&^7Q#W7&AeCjR1bb2|5bw+!DP!h(i}7LKg(sZ~_f>_{paL zf!tlL(<1x2!uPXrBOt^N7ENARbw0{L_EYVu+P(*8bEWWhT(SUyLwrYfzOZi}p!w{z zE#u+w^m|w?jU5!0cMX(u>bOWu;+c(jLPLL!5d`^KL59RX?fBdzG-IIre%(}0fyXZr z;zJ#n6}ZJb03*$Gyi6Z67)Rk!{k-ki%70ShG8PNPp#Q1(VPg77k}07jt~G@obOYROC)<<( z@GEQjZZFRIW|}EM_t6#@5D(0_qad*weSvnWMLmNf+>^@}D~^!otsn>zB?yQe-an}}cj@j7a1Jfv5-DuTI*y{t?}!GR;OiJ|i@y=mjc2dB%9)l~)Bajkv+z7aPey;s(rhH2rTeye%}!)G{9 zFH{S!F59CZ(fXiOTGiTfeV!QQ26AChl&3eEZ?&KPhEd}hfh)%&Ca|yJBY;2>A@Oal zlARTpzhzncblSrCNNKB?b9w*nnV3?uTI3{2a6iQCV{Nlh-@ys#2ECe0irsZ`J74{` zA-#j!DbUoNBE9ja_yzqLd7N_Zbu5hhX}Xt<<$H)cSn`5a0zWTIhLgMlB^p~|KlTZg ztbVFi7r%iQ90EtfGaozGVM)13b5$fDW2j&?rYoCHd!V>UrM0Q!u(g9nn9etRALs^I z8RI_NFj(dp%*Jce$uGv2vtjyO6gx=EMHEv{(u#n<+*`8737ar|@-%176h%f-ydfqgM< zVvrfTwl?I-(w%%43nSPcUmninci3NjQ4ib(5p43Gd$9X#eOWW$rKL%2+9^x@)lhID zS&EBTLkVHui^c3A3zJ|=3z^5e8Lp^~}gC(qQuhv3Y&-f2qEa zOpBP`&wJaHBZHI?`HM*jnnN4DtAvNl7M)T<-y5+>r&+^q-%5(ja|-O(iCdynIhb0y z;${x+LaI@a3@i_ZMGNIY!^Q7Nz5kKw31`pG|BZwk+NUvOcHSnVprm$~nr&9|5;kkx zuVJs^D{4_72-MOQ<8-2JNlUhvOuB_XiuUP@N*8fH=(In?=1j>4qihDn@L?Xsx2J^z z@{tgyFD7b_jYNDs`KaN_<63y!^uh85Atlv&94}3`1>>)H=1K*w3rO9g-`G7aT%`Bu z9ILbsOI4iY<&Y6YSfRKYR&6Y?@GUbHQ=)ki z^+%UFk^|Fy;nZ%RB$AuNt$kk{T6uWGrCTJ&uTxZ*A>;=Mj)iQk2OsVCUCyRhP4(iy z%K{?!BNTjevs!zl?4WgJEsSy>Wg*T7)%;lVzRvRQI;NvWEP7@xIuv1fq@B0EBz@NX4>OvQW2&$llrGh&;@12>){z`7yrVZ@~pjV8*rA!YPg*yHoA?yU%Bc! z0lw2KKXt{8$uDb3v>jmOQ*pVU7jbKj98x#$L8y*U?N@kW(@(HJt{HK^vVjlR9ydAI zcI2$DUS;K%Z3hp(SbLt?&6xkpy0vMV;&HBTn4CMqYAn&8*rlYZ5Z5p32*rOZPL~W1 z;Ift6S9{T4DNpb1IVNJiC{VS8Lr&B|9OzDfICHNLSo$sB z?x6Hl(!}nGYL3LQZmsj)XHg)1b}Z3OV#_nUb){hdKnaD-{aU_l1z5P?)aiL^gJwXK zxUfgFT(;H$Xay&CyE0h`zNlAz<*_eqo|VmmFlW81OVK#FFd|GEDrs=(W|kMpGS3a- zx5x{C>G-0)=*k#|##q$d9`YovIGeZ zih++eVTA*Cl7GC$h7kn}hnte$`2IC*e3f(QtdfMB^#QM}s^}Zp`rSSNn0SeV0kb=W z#%cw@JCc&Jkc%dk_Jb!$DKw8fvI#N2JldeiExmG_l37hHW*{c!-xUL|wfcEXOp-@4 zA}!G!H_ybd^8FAJl^eFMEiu;j8IDY@%rkA9jO%Yym|zwt7`-zboIE6_MvE3NtPUhB za|gan<}Ekb<3k_c<)yv`j&+kI2^sX&x>do9$z|na{&5%qLI)6ds zgoI2&vM{(M>bJuxh(|3h@_USp!<`=FDsAI48DCMx6W@YZEWQoEb;B1vy(H_NX0l&5 zU{M_g82fG5Qoo!8{6lKKyYbAlRD2hhE8$nmTX<&5#2{Mov4^0%Y1+PQF2jNGa0+q- zLeJ)AgUSLABOtzDiLSRP9B}ZOC3kFgJl5Voa(*uJ+G@lJMul76RN(2G$f<1;6uY0Z zPDsWED5{7Hy~*gj#8LaMsx+%te1V8OSb=Z-V~_MsEeo|?l0;+EvC28A7_sCcIX(Ii!UCVKJJI@+&OIGvq2&ANGoF@;ipFgavnc?9Lve(14B2}eC@_6) zI&mw$?8ad#7(oh+H|8(F@w%Te9BS*Vlj%_?IdA5dNUn0&7J04-sAPaA%F9HSM+a4H zNyMoM&B4uAun>haeIL_%la*V~1qp~Ez@z}{k$>W@&n=cz1EG&O1+#2!*TLfqUXxgS z?l2n|yq-9~WlS|GnYQqF^>r^80xKjgeIm6wH3G(WnR}>#O!nk{;S>O1Vu^kXSoN~G z{BjbT?D4iU`tj>ET%(R{X%)Qm4;}O^e@{F5}DUA5(JS%PTAbi3Zu- zd5j!OB4LA_mQFQ4x-*v+8>9$HrewmPN6_fL5xp|EJgKHN=WhrQEoK=hz>b7D6nD;? zt&hTFVkCV@#3rRo`;B>Q7_-=Ecv($Z7gUFsbJME*`*TFP zUos+ffE% z=d+_&Z^F;XHrTfgIW2v42b=Q7u~SZB;))nl1=muBO3W?!D24N+C{#^iVhUtOCP(|0 zXeYioAXQ4#sDH$)qP!_HXc|Uz5E+<6pS!zBJ=1I;qKeC8Ju!#azt^166$3*u`^PJ7 zsXx8Rh8>n^0E=jG#rVi+Uu%D|$=VW^L)F|CVSDz;CxFJ=b*XHh zc%4UgZrt23sLo6TVLf>oG&uB>Q}Bh46%)4UjAQzj@_ch2Rok8l*pBxQDk2GKoBfieGIm(x-BH9=MSNp>CTcb^ zUZ3=&St||SdCpQzs=6S%>q|d<{OOb2oo#@4qan+@&nb*Hi+dc)*LJV2zABpvME6C_ z^2a67(nfRM`jA!dTcau8kBWBV^Qgg6?ROj7zQ~%YUq+*+ZR=(ERS!|i$*7Y`@q6~G ztBF4+N3s%qhR@E#JZ_e}y;NPnq|O_o<4G@Y-2FOQ)uy!iiz6=z#j|I=Q^ORnX~8vX zn?28QGP3qw)x4vNCGG7WY(n5IZ-1-yV&Pp_>Kr*XbbIl1U{ExNCKQPM*{hP=C03qr>kKKw(VfvJ`P;qn-O_k-I01lA8BEsX(K z12IIV_GmvTEn+fz*JplFvX(jilZ@^!_7;W5LphdrB$9aGwXFH3dW+n8?`wfr4-UQ@ zP$@MME(&j7Vy#(4n&^muV!y2fBFJ|WHoXQ*vzjhfJ*?m?IYsETuZq`k2Ywb(eYN>W z=O4tMcwK>JtS<^41wTLD@c^nERVB}Bd&1&NY?FqzmK58a&j+ku)=5i46V^^qza_5G z$3}X91Na(a>9`dICGM2Zv}}R=ke^(W{r&$;88rQ`l)<>zG49Nc&E_tcR%m*A*ltP3 zFq;!_I~4uul7e;0kl=m$>Fj=%?W%zgzeH{neM8?hrE4t_tMJZm)5hqS06{MaDcGdt z+3VzN|7X_&oMzD7P3z zCh@SVY5QEB14#)Ln$mW6f*Kx(X4}jX%JP`W6gj~NvHvDs&&hvKsiUM7>#mq!@lXW2 z9mN^g?6dP28XL%92zoIv)Vg}?M-(_ln%r=Q_emzw?rF8~eZ#Ki&P<_ugxtZj>&P=G z#EuK$BMnBCzs23!RvdrSFBstS(ap$tO2lFK{x`WwmSMHo#d9{xVH6A#OM)*(3!baD>YQ&o_aCN<% z>1NYkX&r(S8+2(6*AkiB&pI5E zJaLQI!hxl?SCz(Y59nonIjyA_ge*`}dqk>G^S4ax>sxflX*Lst0!5~9n~=TOL`K-e z-=aJ!E86S=&R!trSk(`Su=P2pjGK%146N(%N-jkvzg({8Z%?$v;goN%J*OjI$x1PJ zYQ!&%UD@+x6)HCbUwMc?sjPCvCTot%pkYIz;%m6VrLOls?;@T!Q3=rCQ+m(N4YK7o zacE-)A_};H#lQ zdCKPViBVVdd0AVt`qj+o-HqSlJ=(s+?cA=wVgfMbd-XDhJ+larU_yIxQAJtXZH?b;9aN7h{R4VH7ON0PV8oGkvpF|%Pu<{ zJuDm;J*@Xc6TPw)+VvFL$M%?Q2Q-Bn1NH%?aBA2Yo_^9+go!9|E)A2%7ob4F)#DH^~3$tB)dl{Q+7#-``O{#9+5?V&HV~ zgL?XEFN;I{*>(XFTVABJ+}92YhY8EOO53n+{uv8@MN*fCBa$$C0fm{EZs~m}u59)c z1lLW>Mx0q;(BZ}Oc2A^xAi*6GZ6Ym2Ow5*)8T)lGmBKnh@ddl<{kd^ce~l5Aqi}jG zB#rmtW~n1=Og)7>zsH(!h)d~8`UB{E`BcNEnUgs%r{!}&WuXvrp;5TMcA@m$<7W)y zn}h3IE2(G?`g$#JNHR%8k--F!Z9i6I=&@i!F01SVxDpw9*u@>JzWH?8syX+3&TM#Q+U>IB+eOy+F!wtz*?1;gqEE&cI=aSX-|hv-eS&poGZ#BKwxP%1R>}iY z?5=Ou*(<$(1q%lELAdEGr(Wo+Lm$o*fPJMmT%uS3@{)7qIx@OhoB(W(j7Z z7%D6*igF&?jJ(3`9}@!M3EHIKp@;}KR$J{}NPHZF?S2ts<6fs#mv4_3JvrK*r(dR~ zw;T6Tz^#{6J7?9#&8~LXS*??Xqi{t^wb=H(%zTAWV3om9@{VPvjV~E_l=17reJc+? z^0M;QPSq2JMs~;IvLxw?7BVX_2PD#mmZ0{>lJ{(o<@p;-7QGrDfxn;oSFS=z&}DLn z{Im6d;XFwnldpYq@MmLX+bywe58Olxd@hXPP8DXdg%le?V6Zd?46I>>-MLBh6gmSU zD$+@DXE1B#IfHpf=p4`?UkVYWGn`3=x3Zt>xes-{0uc=w00px(-jFu7@r{Mj-74U* zERhlh^`MNK21d4Ud_(rzP(J5D-+^H0(!)I_iKE8Xd*x^$qp5r*+x7KNDr8V zD{>oY6HyP<5I5@(hp<#NY~p~_-^woqCG_uVZ;iD)1^t-kMp6(&x%%=_ISy{*HEt0% zV-xHTN5ZQ%^4ebMA4f;MhyMa1{F?`_NWwn$;CgnsxZL?(Ds7y<>cP)mpNFAR6_%Fc=HTdJ zpoGU(NCO;7*%LYt*w+jUJ2Es+ZHwfUBX^aslGibN?22gw&O-cWGe&YB`iu`AaSEvl z2HC2D&-zK2uhPx2<p9rJg(7-3H=E} zYt>)xGS00(@=bZ)O#ewy5cCKCU-nIHwK61e=(ZM4W)w8f#IZN?nvi zFUO>HU+xzZS+u3ByrenlwW{7&L@j%%2bD_@ewEQOf#&#u%NNQ}bl-561c2My>r{f`zKV8MU`csmn6w9%A53w6|JK~ZB$F@q1P|Nkzs`?)@G ze~5qi7p(0-2s)g|jRnlMZ~-GrL9XLdcuP&{xTxJ{oSFY${0W(SFGRv>LL@b-)2gI^ zY45`+{!6dd+LNNJEOI9L@|k?5GF&~exT?UO4Gzrb{&93&Ln&LM`j9mrji(35XM_73 zIz4uOA5I`~K~JzKq31#~9cj~gt;~Cx0E_0m$*DS8wl(@Hv%M2NG9C)>?OTUowsxp# zI{2n%Y_fm;xcujC?FG{Y#rBcJBn;^A2%mSE(NSY+Xs7Yzk=(8SCYJ#*wK0Vw>LXB4 zmze%`2-Wc`58B2{o-5{he(-XePTr4oBkpv6-EDl`KH}%QFJ6ZptcZU|sCXnH>zMRt zIT&-(%LZlMj@$G5_?owvn!|2r`@8CF=n%9BN~qx1KV8OuOkn#XWm-;cTfoi^50l&P z$I{u3U+>?%?N(jP&Pp>f&JJCd`%ix|Z3vGMLZuS(L&Z6baf)&7E6+Y}i`x@B$NYxA zdn@fjnkrhlUCt_YJuOyBl|bJ5Z6PK?YH@3)f$NhB!;!p`Bpy-ENQ?iLvV3}$Ob~aV zLB0B0Of?PBv&UrUi8xpW%Rt8LF2!X;JhDIYc1qDJ!oQ}$L2m1S@6}ToLbJkQdrZk% zTt>KkF(6EEso&S|)%kuDr0kI$cuJ9ZeWpP8^TxX$Qc)bjX|9xviwC}DPRPmvbYE6C ze-zsB2~Bq;A>73N=vTqJLl#$5bAZ>)pooWy@1-9KXK0iAaZ<+h+ZObH)dHY}OPprs z?Vx}cgdHej;(Mh^M@04QnKFGkhF!Yd`D$M(qa83W=YsM zLzgA&eCcbp2#u@lI*)U2jB9E4^}U5TQ1*wF;Ww^G(=cMPSZYes#N?b=M6YE!P|n4} z(N>((j0fqupzCL@tot=$iY9fAyvU;Z?VPq0zdAa+9@HNl8#V~*OBzE%0%(DABf1a|= z(uR5p!NW)t@tIoGKAZkD?dYAHvbu4r1ws$<8DNwJ$8iNL_#6XUzYdUj1{VQar)~|1@2#QCn|MS2RrjDA!E95`DWiAIYLR^A7dW z=2V6g&y-9Ruw`$_j$8gLyP^137;U%A*wz$>pgA&sN)R5uO|)kEwF!qKzEy@Q=?_Yj zgyYn-TdKd%K-1bDmNs8}-?1tK8xcgwoY;1{U=$A3$A*}W({Irk2i)9$h>0ceSUg<`)idJj@d|M;wkUU*S;g&s{&V*#ztoA=_GRg-1tC7Z5YvHvy}hU5f_x z+YgNU0kPY#`v@YcOA5(o>pf}q=xWcAg{yyFeAS5LyIUR$LXq%SVG0xhiOFX3qlPh! z9{+>2w~CG<%CZF|i!8RlVz8K*S(aomTFlJM7N(e)nHemLnVFfHnOP~AbX9fFRKM=^ z=B=5pT(L4TBJ#$)2lm-#Uy39K*37#-cAZSK;x^U*BvKpY!P^fdf3BAJ4;q_0N&|F~ z4`m$L}so!%nNUrWu|56qr~s? z_DdJ?4z4}SG^G;Zp{`nczkXn|lSM9L&&FJM|DI864Q{X3i_jPsWw^$Tl(iV?*ef$3 zW;bV%Bah#CnTxH2Poy z2*D1sJv1|<(U?^4pyrOoYY0H3qTXEV5Kl+qUeWs)(DejeN+qU9cSqVD+slD?8M{1% zaB*Zf>38cnh0^hs(K6ZDPlRRl^x??LT@gpH-bV*!OYiGX@WjD#!l%@4X&+0j@WsjV z7mIS5+H|@2B%1=Z#E#FR2jZB|cMKe69I{6JwVF$uRY|V&b@}-*Ep}fO&rIEb$Q;Kr zy+ayxerI=THVtgK`uZnno(ChCOTZUIjYLQczBdHTB-Y!7WAfJuCTN5dILDkIK3hpyuZGTHY+Z4-#tO1uRxEA<+rzF@FPy8l~!Gn z@o>Y13UVW(%E|~awWR2&RD=4LEh2W322ts?g%;NpQ_5RdPyYPs92aRnkgaMMmG%gb zOn}61@#sLp%z64;lshPrjC3R_>0z9dqD+eLt+X#Kr=?pc9h!4&?IdR)eo zQbu!P?dgkF1DVn3kG10yr{qj@(E#=Q`rHfxWr+H;m5o4BkGn``ZTzv;M+DCENGA(0 zB<_~eKY_t0#J-WA4JxLzbbn+cHKmnaoDk8lVl>yoD2IJpfulogJhm$pByqKno3X~? z4m~z&3G?uFe*Bn6(SM4N3;Q+#SBYrsR7zcPujG6mgN+&~y|^_LDXJ=$3B)F9k3Zn& zQ7>X7{dB}@c8>>m`>K3sH=>>VA$c%oukzP|6o$L?)E zJGv;zFEh#7N5*v&nO`&o1y;Ge5|S49gcr3?g$YZv?-|#j*v=i-t8;1KPVB#T!K*G_ z(v?4e9E~PF#zQTCQ@L|n8HOrE|9-YSi#|zsF9NCDnsmgr)I@`GK5|rrdDeY2#@$r? zp_}F@Fetwf1^}|8!y+Sn)_q9p2XWF(FL&Lq5~!qTSP^Znin+Ps;Xs^=exeoysFyKWvX zql8Oi*Ws{w>o3l=>;*NsOpuZl_@A^)*r(vUzTX+EL(@H>WvG_L_NYNiIrOj|!E+NSjsLI}1OLHisg9W`YvzH{esm;0 zly#2l_R39vbmT5jUk_|rp$c1m9fvcaL$&1fmN#-mE7dFFTS>zvljfmMk=EQH329=Gl70{u6JaDD}!{jWB z3!&m_Oi`?v^gfU3jTBIFr=<-XcNH_y)MsXsa(<3qBLRC$1GWH`hJVD{QB&KC;`9I4l8I;Tc3+&|{k;wM86%MioB}jde@a(EXn6{X4R%)4L0@42iV16u_{g6LpUI~?8}9Qjn2A3E z@^Y}^#lsPbR~ML&@X;|iA6yZ_)RHuLri+Z zo&CcBjl4d*D6Wdji*2f^`Ch^O1?(F4W~spv;$eU$q0wE3eWjGMK6!}kbh*Wnhy2V* z3Eg2^Md}MrqwihhM-Kx>2`5magzhDQrQct7HbGJSsWzLL#o#D_CDHW5`|A!|0L1;=?jfB`K~F#}`LzeuL9w zuUFqJZ(XXR0aJ0!9_qL+SP%cu@@0PxPL*G%=`kv5De|XDV{g);bg7@u4l^4_sJ z_wcw$qe+#eKFiLs%<9x4X?#A`obzZ`e8v*?Iv^mZ{U*_)h*9l6AQ{1VbllusAC(*I z3sX`hAn|KNC@ZbwbG%J)Ef5t`%xQwn1Gce6mBnL4YAl`<4`1x5tO#S=))QimErXoc z7@Vdi*!Xl847%@s@TRPkS^)Nika6QM%K-)KqyW*$)tg)ks)Z40}Em- zIAAzVWF#Qa$ZWA`MN06(2kFQjs1d!XY zu}vDWUGrl218U+(Sray^)C2|9;7fEpn(YO`0$?T(SJj;_`cpSSw-2RfL?@nxCbZ zLjR%j59q9`n2;)qs8HF-^ZeIJ_DJQ){aUYM{tX5*tYpa(y7YjV{%J74@ zc=U`G*~OF&B!>7uHyjmg1bUOxH89@Tu^AnKn2q}e*j#6MD)Ltct!r@ZQ4kCTj6V-5 zjrGsiRxVlP1&A*lH28TI%kVb6Vg@MA5v4%?K#pG3V6*n|lN{?XLrMm9f&pQ7PXJzn9j0Wk7$H03MjM(f$Vn*SPI%KS$+} zS;Xp^DwsP58So?0VL0$taLl*QJ-fnA1OzcFTB7QkRx@!3=!sD&%3a*4G6Ks(1zESA zSJRDk(C!P#pjd%r>eoBgvOhuO7LcpkSsYDIck1o#AtBm zflDh7G?_bw6vtldSJS=?yVM%~Q2lrUwb|5m6MUAR`AlZ*b?AioTW8j5@dz&xx{LD4 zh%wVng08t`^wF}BA5R4_P9~KagCGc%)}6uH7%GF$K9rrp=RfmlxPRr-j&=|vV*FX} zgMkR;#lGPZ1~+tuCzvVXhN3o*+)f)&M7N~Z7Oo4 z@TtbIT79O~J)>@ix4(`e&TS*#!qGl?6=KfW>>Je)} zt==h8(!R>=bH9p{k_FNSjV*pGqpU|1=0ODILU$}OmopmALKElx1qzO`psK=;LC|E~ z=Aqz5-u(_^URR}!FlYDD+iCy8vr>{@8m0o%1x9CVd{-cbO8&hHT}G)+vbXfqiJR3f zL0+dZ>Mj>MW*=*3H}_(X&RwZXN7!7Qf+Y@3;kOfli<4&o;S}u3;tP^R)N58+>!`|4 z_1V$A5=v5yX>e^t1#q`IQ*3B4ekU0SC#7U~W!~GrlYc5?qL(vq`1qB!2QddC zj19FE?|NPp^kyi0K&dVzRe*fCB;ezh_o9&4VqxoS_NSqmy$dkN+Kxf(sO?5xR;SA7 zn|we~&H)NC=#ncH#QrH4PnJ%o?+YgngY`~G$?@!#bw*Ot2lPIawr6vX;-opVGH z@jxj_;6qVoBSS`sF?mjd!n%?kwP>7tz@Q5Yqy!jY$E~RW0Zc1g_--03^z%VFPnub> zPl=f#=$Ge)tW>S^P6Dma*DkEyy|!fabe`>8t!Js42wL#C0LYX9Zt~u0y&fOUg_|0#*{*y)*Ar3cl5WS~JI*M_2L!`(bmckwSA4$52D;*! zD^meulRkM>ed@{gH|i;#ruS*W=Bcd?b^8U{9|6gu@KW9j;FEgfp9LFV{i6vFCQ&3## zJJYj^*SQ|}Ueb#Oxg9JeYcOBV6ViTV%HPgpcR4vs^SmF0HSy-gum~j7_v}HFb0QcR zit7j?v>hE#_Dm8cT%6iwM2(FRO7IeN!uva5mu%B+Snym~Q01nEv>TJaU>fX`F4sB^ z1-3fzU*vN8bfzxutmI-a%8L_rd!A5yD_bv`9E=QHVRp3lY z6_PmKViK0rTa|otv}^TnKvP@C8U+Zhw6${{_5ADux$KRu3mR>{n5H=OcLV`G0Vf+;PLW z+_p%`v{&Bt4=DJ_3{XdJl9&w*SWU!e8Yc<2hw3r3rfYwhto~%quNaE5!MQ|t*z3d3 zTlnyc#M|!dpzeniXA1xIyzgYwcF&aT-jn*#Q@$sobP<%JLa;5DjUG7^?am-NEcV># z5qhPZzgNq{{NyZ}6d5;Mq-$%KqDXczw3vSNx@h!_kluW;WKWy?1Kg!?LOSSA=txp+q;TAA zccn68XC&Jc^Ee@(_OUOap(5UDpb9Libvk!2Del*Esr&gd4jZLop~nl2owQMuEyW3w?MC6`LcqF9aK%O9TDL z@BUJ8LMLgeOSFXh_xbZBEn$*DgQ5te^c#IQ=p=JUPzOBPOjcyLS?AI%Ky9I+s zhrA7P`2Nu3V5+~z%5GB#O{+<;PUom@I@X@3Q&#+y6GUR|brh>OVu-V!w~#Xndx(dD&z zv3o+%%B4~cu3};h2+R;%RgyPK~3}SF33(tn)>xy!))gp8W8*hFVn2DQb zp8chK2enQ&8eFT(KOZWa5J{=gEmol>-3xSMXMBUnmup4Z^?LKIQ&?qV3Q-WdskT)E zE!z5Rp)llwf;Ae|Gh)%!cev;dJqRWP><;fsUr)F_jw@PwV&M_{4vgaM8vB&b*4OXu zT&A@^aUZ5e3L^+5{$19jMV*FpL|rx-ScbHj#N-BxTh!=y;V>GK+3V5b+YfbwlV%Cl zy6#)XW8ZRdkTF#;f2M{hJd6o!p4w|`4QsDWlwAdT<8#((UJfDRPGg*euNB;p8Z$AG zh6pSUH?LkcXJ{Xq9}s7rC5hG2W@UFVaOAc}K-ikmxsHzi9xr}Qy(o{6{qb0}aK6(%q)t!T+o#v-+dd9;4Jjh>`5L>Ms2#(dIQ0*Q>U z2*b^LX`@u;Y^Vm#y0DKx3pw8;+i%YMRn|y!7%Z9U5_DR;-jOX61S>S~Tna`MmJ6gk zugP$!r!Lo zY|pK~ODlpm!q=wJkfP74e#ZHOo*_68<>V}H)cex*O?#bFxVJrv)qR!6KFAH|328_y zr6QAe+!vhCZ)4c}C>~t6iYl)46VkEal30^NuNupKK6_vKa4S}!CWfB|xA_#u86PPc zL5$h>87C_(NhIMbo-9;zBfn49ambTU?`O86YVXBMHIBLv$aNVEQbF0#UDZ`n6q^|P zde7cLV;4Imhjt|jfGG39!O0|9*#_Y9qah^BkVkxTR|!segsni!$f=6h9X z{0e&i;fVYpLodQ?AOj&h(SdgwgKmM>n}fq{xmmVA=!2B5JP%3uu?jpY(j7SAIn~q+ zqM5+$EPElNgX?S3^3V_j$8noSZr=3zJVbmiY+~-BsUA+fsleV=rU$Sr<4?#TM^Mwp zWj=l4A3LGUb*D)7x|4lQM*8Q6200ahEtf!hP&jLtT3$4GFSgb%O3+PhiH`N9?aer=FxPo^R49A+9s)mcv!Q3`?N zFQ^3l|SGek5-u^B_T>HIZ}&bHeqC;LvOE6IkgU@9UdhkV~!|E;HuEh z5IMb1U*kiPFxi=%4UI-1p*ANk$1Ivsc1RZm3{p|~jqTaMO(Yl+-aB3%Tln@tiN`^T zV@Z=oj9lSsUE1c^edtUekLgKy(=&}s-EfP;g?;~ibMJ+geZ8L7#FJQPX~M!p9uX5` zXMko18!HGGN%H~{Ot>?W_i!H`%p0AON!So4NKoX|n z#r-fdta@y$F+{G!Y%1PiPW$=++e^;PGuUYa`BV!$E#2k*8Irq|a{&7I*cvOhZ3IF5 zN6nP|!|KuLX@1qDXs6|#e8=PDTT=9J1U4p8u_*AKhP970cfCzAQmXfOiZN~Mal|y? z0bT3)C5r4%16PWQ6fDOos#={pJ5{WHpAoIWJuS`GMX7a)fhlPfoAt5RzO5<9h$u@qPeG=_)IjtA4_I2ok^Cy@CY{B}+DJ{1s;pDXGt0rP+cKhw{ z-o7^@v}gE}UmvTrcygMPiURLnaizD;8fTTyy*2nsp5{a+E_g!f+^Htp+S0x(|Ij^k zM)I@^YiT;JSmq3e)i*Jkmlb=Xi^{0PS__JdlWJ7UBIu2bqjwS-UcbR}+%Na^s&Tpt z)^O&*r$aj6d$Tl2Ls!zINlZp^Xqg$MgU|VpWqig1a_8Tux^`bFOe9_a`MMXUgMaj0 zV!bBy_o+S|e$B#N>tO)$@7%NODq$E>5)SKi`M?)T!Fa&xw(0op;R%X$A3GT^;_&|V z7X$h82r{1rF51e4?z*g<)a>7VXI6!GJ>F3mt;k?;Tww(XMlHPvKBKN0B+nKw6XX-; zKsfVp#l6ltJes$&%*==jqG<` zsaGFH@}|zUW!FmZbm_{?@g2Z8UY?utzR9m1AhT^o<0j93cXw}@G%hPE6PJ@J%*g@6 z!ePUESB&8OEJx23^@HKeNWw`#hU{<&aI)d6WAhVs>p!>v&d_dO$$7sansx$Jdc3kZ z&z9~7|FH1HML)1+d6!t<2B%$K(lFgzQG$TCznK^cR#hy_|4Z2HzvfIke8`5ZCt8nz z<6D+GZlGM`f5WV&XtjzZGXqW#1Jg`&Xvc=>NuXn_=y_mn>9V4D~%;38h zw@v;J83y1K$)hwz*64v;M@@fcXE1v+^Y`lb&|PJxN&YiJt3Zd)p`KRJU42^%Un=fp zyVUpE0}d4~clK4?6tTx#WKON^Z-1kD5alAhI30F;;*dhFnX}{YOCyj)wCzm%-$MTV z(fEJm4D+|j>o|zTn+NMXNKSzLDx;cXHo|*Mk!4Dhc(tU)1Z=c?CLWDuulLVlzw5W! zF85_gZcb@}CQnlLYtNycXDD^ghX|I(mN?A73?2zLeYQT0=1mR@n+KZyD-7w04T4i~+WlZ^)@#dzEOSuZrcuJQrF! z4aK{FrJK7sj}JsYJj`u3`@{J=C_tbm?cUP#VN3F^+79Rq{vYQP1Bm~bPrMJywWM`E zUy!lrqfm43Kg-FoQw0o65&?i37|It1B8m~0&X)sZDC}I(gp~;UD;w=3+(Y$(pNp&-^Z`xmCu9G)HZYqO{}*)Q+?>eMyddOlg1%2tk?VI77RUa zM9C~hel}Qd^z9V6a?PGlbaog_@qDuK0Kb{fN`sIRx-2ca5=0bRd6^<9j(M~qaN|<; zRNEsthsKmq?KpVrA$UpTJlMB2e7n)0?riJzsY=1&rEqb(YN5PRNs7zfh43lRW8|dV z_<)9?W{peg9COU=R`s21m_g-xxem`ojI3dRsp}LUsuAtq?yPw%@;`c>yq)RO^CLKQ z308S*Bz!R=+*nyn)VUUwX6UmHXNqYbb0%DByP9HKXSn^o6IBPC0(pQdQQX z|0)CH^mTq;NI958+wcaJUE20=Z9jGv(nF48>-oYNah%O({X%d)lT+LGHw{W!eH#9$ z0p+uS0A`x#@oy;=AzGxe5%&~c0JT0UR-EH7Te9Me1!-T|8l5I!wN^FDU=vEM%@_(B z1D48@8nh7qvdUrE;*Cevd8eVjU z**i}!ErpIB_c1a0NI6amLLQHjZ@WLP4J##y!B2T=j$wS{>b=u$*;~C|!m_rb~8@2)p(ja!*Le*>9Jl2=rQe zpEi)&8;}9#GELD~Zdee@qgs#MlQdPcs~e##v|Vn8k)gEeY@tPHgg-G_BlmSM@v>~B zzS&Elj4gMBTdn)_8}umyl2|fv97MmJYKUhiMy@$WwnW9EC2VSv_pQm@?uXY^>B`!8 zem%dXnD#z~w=gwgTDSP#gQACB=Cn9JRqu(itS^f2f>3kk9>uh5*fv+*_yS*N^?#=luCCfgHZ`C?kN@VB13G)T+Wz*IgG-4FCAKA|xY5SQ zcvcgysb|!1>7*XEKhH%i*IIlLT#;uq9jLxHpofwvkE?U<*`$yCFf%zLm!sKPI zV!HpDz`SpKjjKu-Xr>qCIfk4&p#5iR(E*UW1&%I>0UuDZclMvy5t7s!rqHLDoEa zn_?8Dc+HyP|RA^rJhU^F&GdtLGRAvo)chsW`$!ZA&Y z&~eXUNOb!L^Uq5D0l*uO$w&&X`I%>_ts;X%)=kbsZk3qbw3YMv8duBKQ7`qLwrU^x zi{kg%LCPu4xj;A32| zaOrS`h~6PVTC3|YWjw-dPRbHlj@mo|Ez)F?p8UpsQezSvGM)OHh>`gG^CL(&@} znKpb9Nad8cRy4iogb$A^;gUWR7{ZF(LMCRqj+lB+I|8Rgp?8O0FjPw0FIo#vr z{%aRP!`>k(%S|c-Na%2p8QifF zy!dcX9QhFw8XH_K4>Hq$I$j^QeKH~Dq?@%iHK93zlW^qPOHV_vc1@W1!wGrj8%d%V zCpLlG^;3vD7u=M_UBVPou=x#P$9*m&Z41%a&)?+yGi0=$b{OpimPOwExHmed95{62 zx_!VgWs{K}P&bwdu zQmAoudPi?#w$r3Y;(LAlPq*W5#Wb0EtojKrX1H#!N9BCSp-y$&DUJJ=DaLVj_YLwc z2aK5MYzYA#5?DJ$wI;i?L%$~^e@jhqe*pNa`C*{suVX$NN)Zs(!_{m^Kfz;h?% z9%R5^QIi;=e2m|QNv->T{c6+fB# z%>*4=2+kpbHoLu_ysUP2*~*KsKve~O>AiR84!#Yc^T)lM7c+BC;Cds$1+Y(l@VCH5 z=Fj_%s!C6R5cZ2qEVo}NB>yNkUww{T9P@C=;jwjD~FS-d%UFtTZTq=y>6SX_IOHtieLNtokx zv1YP6nhd5?HU7&1y45d!j%JWj@E_Lgy2c(-dm~Oa71d`exnoCnyy=pVY=JgJmNJ#t za5ymPGnlqwMZY-g?J>bRzx<1Ct9m*xkLr$5aIiy$=dnyJ%F0ZeBW0Ek98g42aOirU zY)-a-%17>bw^v>N2~E4d^G!9Mw^v?%zx;R5w>ABmBlwNeMk>m1dco7d9d79nBrKI1 zub!Aa$-YRiL1QHxUl9;AloEGX{p}uFtSs}}4`xSuc9fd(|jfbT$X zcoB{6t+zh%n9<;?Y)mwd()G%@zaKO_ zHoh}*rR)d4Z?_=4YlSxd>LbdN2GP7t;VX`3=+I;(Oz%&v< zz_xzSBhzfXJpLsAJNyQF+uyO!)N3qIWhnRyXkBne`2=n`8rwVX2d(<>9(J4PxM(Bf zO)<3z9zVUbw%7+DS&s^ZG-n@-Vw&rAW~@C?uN=zdz0F|GO$-^d%=-n9_&5!Gi+Pps z?JpjUl~(TvDlQ^JF@_3O{C-g>2EF$$bTH#!X>=1JOfF1s;z8T!6q*);;pk ztDWXQLBtaus{t#VoZ7}9o*(os2i5~Nm3vF7_yN*0Oq$Z-z6EGbFvMMdAJR0f#czC> zUe@y!dT|v`&4h2%vf1BQuU4O9;EOppuSxOU!=um6Y-@9iE63m|8>u62c%#7N!H>i< zbIm=rC<-oJK>6pK_x6g`)|}_+Ogzai-r^v?TOCI{Bz;k|CH)){eDf(|bz|_QdvM1K z33$Dh1j2>x8$^=!_Tx~oMU_VI7U*U3?q6imSi6h{BH)7+^WR>+;uyqD+2|Aw6mC12 zVaXhH&Dg_4l1dHLM6fPNl1aFH(1XWCoY^Ph$iIJzn0$M8`KNPpTeF+N32$Cb?TK@~ z9S%VxVdV|#Tl(U}07wR%)-Ll8$A0Y@{d#22JNyh+@h+yD{4dN(#D1ORb9CGi#NgQX z`(~DeHCHx_5j+6M+Gqi2+a~n|ey`4GP_BJ^4QkuB@^CH$QazXJ)?DH~3P=5qeoBtp zd&b_BDq#)(Bjhmf!SRD`5L5>6-dG(xEryuX&AGnc%)vfM^obcw+o-gDC zpZjm18L?5Z4br(Kl$yQL{@`DjuXL1dT&vix!UTLZ(y{=n8?#4F|)q1J=R3Kp@ z?k_h6#2w0VvVekle|pJ}+98@P;khNaHCeGu_~tIGe*F#4}h$^ZBA@-M-n zKC7aBs%Y*ALRy9Q-EKiM0}M2h!d3vRgg6D;NWh*U9q4hIKs0S%@f5UT7HdCFH$I%a zSHcu7UpGyioy6B=QnjEKI8@0!qlL3qY#vn;6#6Jszi$!Wkl+oCBa8=5Dy?)ggbu>+ zt#r8(kHpx;N~K3mbgv$9XRs${OX51BHqSJKPj;6i3CeE(LZJL-r7aTb<;MoVgJF~1 z+@J91ZjFCr^W7z6pbpVWH!a{g7MdC~dI$ErbiCl`Ka;GD?*qs?=xmC9AE9QJS}r$G z>YyJ9Ux$q9)}ZnborPvkZZO(|OQBhExt)=N`YHq#t>-iNe+DqSGR+DTdV7LwRY%s{ z?q)twU~CGOwZY#TD&4cz=#D6`la7ne8dycg z!e)ywKBen^={m z)hqJ>F=d~(2Y_EDgqY*~N7m?iC2>q9yszY50Z{8hVmfj( z)1?cHHNd!Bcy zgL^$d!#P0U9moXgvv_sL99e9YQz-ZSU0TBJBXrWEEM1+we=e|AAcag~ISp4cYuLQ( z!T``29RDVlX6Ueqz45k-*djz;d$7DL ze*cb2V4!cI!|zxZ-}Z5c+7d}s@ieW%e%LVxliV^DoOH9`y<+Q7XhIZjN_KNT46QU~ zv=8c_z2Q=h%C#0(t1!W1Z*8sJVXJbndV@ow6}`BZh5G)Kiw1Iq_6o{&F}B$oLqTNv zoH)0?B-@8L8dsYQWe~OiPr8tz+LeLq#JklSQA_ zQ}oqzULC{w{XLlzU-61{qQ`>yd(-sOR2=KCZsH22IqZ zWgciAKD{n9ikMCqOz)h(@}6P=_|&2lqrTCL<~8J`EOft6{ni=Y`*uRX1zr}FIJ5sr z>wc|iwtp=F5+V5_(&3l2KucsCjjLLRJv;Ux4ke4MiHRr!2ftZUResLt%lbr!D!2HK zC8poEUw>zG&7%C(bvio33yLNMPHN3N)X$2`+w1v>Rv1I=PFJ-BXM9q(C)G_}{>2m7 zT93yqhMEA{V>1E@E15+>0`7PTaT0;WR~-hACIK%~54Oft2yNZ`_nyTJjfRv$O&wBt zt=y_@4AbbWoq^saYjFP2O@WVVOBtxPYg;I9#kfjPhXVN*!rwDGCb)R@c8fKpZ!nqy zaaQ^(LD|gqe~Q0hOLJO?feN4Xgk9=hk*9A0#I6=t)0epyLU3}r3FQ5h`xLZ=xf=( zPdIP=(b?%tc-PRcou3T&wjK--6& zM`@Wm!hl^nXTf@VfyzKjRn1?<2L>3p2Bq&0?CZTa*|MGOgo+hkq1qOk(S=Hs3p)j_ zG*BS7fq!{WJ)EI*tttkqk4Tuo??wK<%LrifQ(@z4C8*nlYBPi;_RQR=c|NsTRya>B zA(WdMvE;sEWCj^I6p74e%&JCQr(%d=snhk+%y01x* zVIm)q?XQO*w`rxcL1_%Kk5zQ=y&?WtMLc){Y6D$Q34d(8M>ane*$5fED^vaWF1H-q zcp@Jifp_*ZH>y{&n-vZKT=Jn8Dz107=~I>K8`~SESvkvRvZzgjx?M(eQNH2n1d*on>6tC%R8}1HxRH?_kfAAA=zE zs1wy?6Yi7uvIF6jGIaj*L)}z~d*G#G$M~p?jg0>AfedZf`9aTNwq33Q1SxS*m8qBx z#)PKW5+Kj%iY5;f)*Sy=0_CSiM_u7Yy?-(&XOC>li`sIN?|j1spZe~-$VN6fLXXby zP}6Pfa@Gg$ecPjMvo8)zB%u}FTnX>{=ha85`6x;)2tV7@34aM7gk-v5Jc#`VA^ulQ z#9v?{z2+Zm$NvpY{xh<#?^GWxxqF)eGbvH#9A*5bDhWI&*o_8tUp#$}4;9ZI_dA26 z-JB;Mg9WvJqE_uql6N!DDaXhYQ`DLqPA)YwhuclVL4E&{MY;3mjjyPnYL}yRA?Bnq zsqhOkMupM*2;NR2Q^jR8dUwV6s#=cj-dp42jrDAMGp@T0;l3?^N5pUix`9EahjT#Y zZ%mDx4Pp($#U(Dm>PNpLYP&Nm@&nO2KThojxIf|;frSM=S{A7b=i7U!@!A6LxlEj@eV zfWZfB@-Du&JIzZB<~wLZ!0Dq1!&YH(ksWZI4;`taeJZ6oO4lZV>+XPP1RspK+8nKV zF0;62v_J~X!V%lpt5!maSzd{H0~N%Fy&?sfbg}QYg;)OTO0wg|*I}8oS~}zV@R-e7 zD+4}AdjDgD5k&4uKC;&1;Q@D~u;X^}e5jtTIouj}UZO1awA)E2ETP%UuvqHZ4n(iV zIZ6vFgD6SBe?|LnSL@+DH+LDwC&=2oU;Urr6D3kw6&9rQ^+zqF|De`?N_SKO4e+l% zQ2xO~OhLOlpG`~bax@TA8_*7T3gZzr{W&gsTj}WOeX5zqVp?r>?c$x~9MMVe`mx+P zd&K^mr5pHB_UW?^P*!2BG%>GUGtOEA_eZ5l-qLlStifRDtn7+}X;JIsT?W_T8{=VH zwux-E_blJB)66n=O{SX7U=%)sM#VfknL;_4q*mV#(ta-(OfX1!9B;S zSN_id^3K<5#38`B_Qn&H9mV{VY_R#kQTrEg0N>(hMun_omxKAAD$U-o)2}Z1bF+mKd=a zkEVB(UhPmnb6JB_OEmQ6GmwZj0ahHZG+I7Y_U0Qapyymk3=;MhH3WIqyvh!dHncLQ z#3kwMSL~keH9|jXK%JGlUwgg4LO|RP4`&ie|3EeW~ ziVVqjl^8Xe+zSJf36{H{r4ZOKZKv31!5Occ_#pc^r}WNp;mK0I1V;@zE><9eMDA)6 zw5*xX=0qGG_Tw&SwtL^It|x{qg;8O;j&gD(h@2&G^l|hei9LX|XPRheQ!)rv>1po?X`_%=4!MUv}K=4%rM|FWN;sRv1|d1$JEV2%5x;Vs6V^sZ(?hS5mziv zuP1cHgAC8G>Yt>U{cuao6P`f|@YgvpS;47#tnq*?#Mk%gmlM8(EKYHy4HKPT&dBV% zbt*bLA_}X0w=h8+GP}yMs%K=6`-HWMidR~IXaJ8Bns(;o!KIhKnC<2@Q+)FK>8_rO z!*-Xq>>TdyC4gtl-6G8yRijFQMRL%$^MDwta8G#D(B!xIzUN22NDC=f@zhtMhc{0Dn`@{3bW*~Ffll=?JJ3=Dii!V^qf^31Q9S<77lxq+< zBd?{MA!h^q@AJvU!PrMjuS~WNfADdnTE98XC(g3%0kk3$m1N}fK(nzZRXPP7^eG}p z>h@RS{lERJ^$Cn|OC=RsYlc;}6T+d2{SLm4E(KEKe4xTsV7UXE;~??&rm* zEI&nzQFGsXd9_v~-Lt0%+ETGL%##jYkj#8^^wmrZX9Yc}tZ@JOgho>W`&6+gpw{z| zpMY)h_Q3MezS{gsR)Kt-BkKC)RLo3zy?ZvA!XUG!38aST$rZ;t2HF2zTI(pIb2y0X zS%Dy1b+i-&$wbuD1s<1e*X9SBm2>=I-FqLT0BQ0&Ikvy05fRZ2c_guka$&+vI2Mi! z|0yI&pj-glt{SoH@;2u!?Wk_Wkyz>m;3;sdRYhbi%=LFHJ-EX#A_v-S2N~(?a^qv{_`J5%ncSX6d%t zU(*DZYP5wR;$|DJ$FphCb58Exu)o-Bma7vnH^oPP+(BzgBAjNqMd6>8J-?aCAh=wt z_O_vSI{O&v;7X)-cGs>FY%i?pZn2)zZq$TZ@ntsxw55Z|LNmPNsy$@T0Gx|a5{&BU zjd2^!VI1#D(yCWN4QHCZGmOh{OX_XT$mR2&Hh|;K8ugG10{34Z_!q`51uTWC_(vGt z_c=9AR64SY9qm4f=Wz6JxL;ykmhsu!=pUCxogq?ev$7AP2Ab_e6^!i7z+>9qzE^2ITb`)9LcDV4(S)- z;b-$(qV(6KkTSc)*`h1nTOh}fd6>-+Wn%cA`(i|dNRMB#zZM&92J7;ui?5G!)B+v^ zn{wnW@XmzM!7SsrQ%@%?zgGhfTC8lOZYkuViu|2G^Y?(&(x}g<2V`Fei)MAU4a{&o z7A1t*@IZ@4sY3Sh^VN^?1VA|sy_1YKiWbMAtV#T^UVQ5PB5ZK?A9mdDx2D#05Q$R<+@4IFC z$cm}1zJ8sg+&HM3YG&Zz4^Lo|!7HM*A=+{6Z@m;@gCi+xcjPcAxO!1m=H;=Px}C%67uL#rc;fIqP57X=MhpM$F&5v+E%2ZsGcY*+hOs;s*8A7+$X z(vabCg8NNbb@rY5bsTixnRwWsChs4g8hy*zz+Txfsm#2x#@&|d2->`Xk}=;c#JSTO ztGe1=_{sR<%bGw_9w_)v*AWkL9eb|h_*nZ)6coWNK}A6HZAfVPdFhTD%Z>I8j@fBx z1~Js=^Ns$O6nZ%RU%`I=7jJJFT*sDVjoOyQ%w$OxGc#MvWUE!1-tgnTx(@krYM*^luk>Ai39UR!lzw&@)0`- zCel9*po^dR=*`T`wyhPlv}C@S!#q5^T;0a|Vjl|;6zmI((zxK6Y^=#CeC3MC>>`=y z-B2ID04plCK1KnkyRGzlSi&nNiKrjo6vCaG0W4pM{K?vGnvg(UBt!o9_Lk7)OHWqX z%e8(VTUpRQd=Us&8=T+o=y6D6f{wGnCdB8uG~p#LCb4o$A^0sWls2rNV4HZhK9INP zw(j|xmyRv6DJ~AB@6qNZz=j`(am*z9PKBSAHd<9@Npd*pl^SdNrKt7w?)N>!ICi65jCx`*Ze3uE=zPqezAEK))-^5_=~5F>z9sjP$n1a_PwaLYEnN z_aI{OaL9Qdd5Iyfl=dz3Re4nLNCfghEs@upZB1OY&w}COL^ca?G?DcnbBg?8%%FX3@_FZmtNsUt`9{`79 zgt_F-XYIk|jH zL*k6957&pPklK|{=|0IYr2J2A zOINK^yTr}gNflKo@!)-YgjhjCTXKT8L->|>ta;w6Pu%m$S5FhE49Ugb0){-zaS6k2 z>n}G>%N0YG&&%4*l_k58e$LlNdCwr7sJsH}-6uCL$U7g&iuZ*%d5HLLcF_~UXZ+|l zSrOI41f8#BIF!e1wS!)%3*OoRGjOC1FQPTm`V{6C-N2nzc&JrioUhwd&Xkh=d@ZT^ zc~`yXyG`)rMVg&1S~Puacx@>G2_!)J?>wZ%-+M?>KGra|3)^Eua8bS=A>HrYMdTb~k#R_`HP6r}+Ss4x#S@52ETw93{O(Lu#_guBjfRnW zUWz)4NS`3)JyHdcm8Z{B{k73Hneoemj z^rndt*U#-fuX9nfdo-YeXjIKhPRf?|s`&wtgfMG=Iw)56u~lr;D%tQwTHcVZ0tpda z0>3bHDt_V&$cornE}3C_q41jKA(%k z22IwRvwabfK1Uqj`eQ8Drj1irhQvgN?yv)eZ}ko5EWM45#O_@L0!|vCkvfK*Gwl2% zS`DViA4NL@)LWFB|d#~ayLrC+pU#? z&&@A?P3{s@reVv9_I33Lzosg98Wl2hcx4o}>a;5;cz#$aox&Wp*zSBmGe7&rjcwm( z#{HbjD$bhVS;@=$2dtaM!y_V_-b}_U2L9IhO(x-)&E#@n&Fzw^8dk-$qC=DrCItC$ zqz->=QGj9=Y3)@>(nB?`WAl_xuSK-HDLzbW60}a;A9q!XJKP|17S@#>-=8Y z(UV*(Qi`x|UE~5YL3%fSIP~}|_sE?y+~MxrKV81u_0XDXyLSub@%!tCmG)-#zHw%rc$u6aV}2mVQd z_jAnO7pXH)QXK`=;d!l5onJp}rh&4`jo1DqU*=yNR3pA!H7dkX-fzW~qI zQ2UL1=fQ5w<=mqCDjlr|aE5QNt}iz;EF+{IfNt?CZb!hA6QIhL8+`^BvM_w5IEg&( zu*OX8UvN##Vvd0Z&Y&E^9t;OR$?3$Thp8x*x)K@{Xp@IiFCO#E{jr7hZT&b$Y5?YG zeje(PmjBe-8_0LQ#+SGw=H{tQTNH&kMJR;{{GEQ0-Kc|DjD)enJA#v!4hz2D{B_rG zC)Holl#A01S3LzMQs%5M=Ne8-76Y-IASX8ViOHjfx+#b32M(U>kJhkfpf(}f&tLz_ zZ{akQM`A{UtkTpnaD3+u3OXpV^0HLnRz+Pwmja|@tk@oF>eekq54J!{ykzD4eLb?| zigXVP{xj`EQoC)3=Q-F`S4#!Bh80-MrN2D=BWBynan@DsLevz44Rz~+Frr5D6#ibp zeGl04fX(fdp4Eqy242B{4@lDUdlmUE8GDgBudy3G)55l^lgjA7X~x%jB)5rdUXL)J zk6#Vy@=tZ|N<$Gg)O#|TQaX_a_#gch=J}r7cwJzc!5>HP0;+ZV#5on=1WB{(r$QU%r&ms*ri;O4o z|H*BIBvb|>3G(a!P>V3}CrQmh<@QuTjANqa=6zal$uagxA_Jqeky>Ro6;)4-uU$%V z#G;QHNKYTfIxMp&M|=K&RFm+cFu1x%+;L=O1IeyQT)9h%c#q@GQ0tm~X^ zR^p&{SU?3Sq2U@D&DOWNn#S<5ID8OH& zqJ2%uM=pFnPf^pzKhnX<`2Ly*t8QU_JEQXl+`vv5&{6}`1-vWfE>p6%U%2-#24fiK zw%a>|pi8#PNCL|qW5*eJdq1HhZZgi!Um_Pns3gC=V=;Bs{K|5EfK={&c>SFesPW|r z^MBAW*)+{c$^aw>zu1h7m&q{~W*LXkeSiLF&rzTYO=Zp@pU6Fu9m&PwsaARPVeY2C z3x#PLa~)A*5r$yMQs3ECY+@qYDcjuKTz)~}k1U|bqMSl}D?Y%lJULIf#yeWGa`PRD zczfd5+E0kRALR(f;Bz&*k52$)x#u zcfJk9i8+5E&sZjgRo*!rr&=FO_l*xbriL&F&Hxlhbx8~!pn zeRKKO`_zN`^`u8w!d~9k9X;8r|A6f#y>Bc3R)hM3tZ7>Z=dawJ7Wy33Vh5M*&{`K* zE`l9?Cx48qccVVkDstVH>vIIBHyr6er+^n4p7>)<&}?2yovR*+ZM5tW zG=5oJ|L6Th#yh2{@9>RT@Y8pj1;sXd6RVpsqSeEH>a>eKdnxS%gcMxthU6JX>kRL# zXN|}6H;^-w>s)d3&z0YEnC(#38L@MV2D> ziej%)QRR$TY#!4T?Wk;I^@HFP&gr$auVTT0WEUWf6oE z$0ipuSYFEmu$ixo8=&DRnZf;D{?}ILKcGgy8aPnFgM;-zd`Fg zGus)izH9zKF)v=z;FHlteq7?(`Eep(KoyG6m>s(T$%XlE;n<7T9ng(%>_-hvI7chE zg(=KZIFYH9Hju1Ejy8aV{Cl2VofDX_I2K9eQI48Zz(I@k|Oz&NmsHAHXcd* zSQ+5?z4-8?uf{KM&+cUt4!QUQLpbH!@p~zF0I2ulz$(pkFM*b0R6BK8_DWA+`(*xumjnG zdx1Sg`*;@hU*iz~&cDde#AG7TcaPE_buW1W(g*(`IWA5}OK8R`?EKZW$wsaMss7bX z`5)^aR64rzXBS5@>75aF0-p z_^_8fh4qKF1|Kg-F(%vwD}5*Pk1BadkGY;v-rBx1$-dB6VbV8<0(VUiB0`72D#!Rwjxd7lE1eMws(06NA8$qdEE+TVgpjUF&md(RmhPX)IZ zGAI7t=%>yvLC1N~h6*WS!Q-qrU1$WMie$?rm@spbp(soV{6RD{DWvsp*y+ICK>{HO zB3nYKp)sk#-8KG8=M2pM6s4pa-l5LeDthiX@s&&!G0FqpZbo2_C*(V2SXk-1FuxC` zgc3B#XUWgI@uHCMGB71l(@-<-EYOx#vWzxTlS|r3W@5+3J6|DLa3 zgncl+jki6jHFV~?Vd7V5yKYdCUtag;JWSjW`kU1zbEu4Nt_eeVOBdEzEL^Aww2Ycn zsHv?bXz|_4F}NYlLVV@9{HFx_!r1t~mr^_|q44Ja5?LSKQGi{o1@TEv^ax4QKZRty z&WJJN^ofI0#YlpMh2L_oTf4xCMK! zO-F1ykE2IDdaC~llk#m%WGxW;WBj=S+F#EP*rwrsD%o-Me_|Ot@_*28Wxr&pG1ni` z2Erdq|2G)xpV;fY8dOS>gne%YIGZ2pw0J0-VB+eZv4{mQQ3kCqt4HUUl!wX>T>@!z zw7QT$ORrOIo<_oWio8i71TRiuGU9Kb{EwSb!z7!j&pc zssM1eiJE?jeZelbN+r&-7T$VNva~iWPC!lGG^Z(?O&DUjBY!?Jh3EQCoC3eXR*h@a zbMuc}04~-KWm8gK3VdSxRlm%>|9u3KNsH{eq_jEHZADjLe^YW1Cm7-5WH=2Th%&xsEqn_WQ>;G1%C3nA3@V=lQnEKjy`NEUo|HNM~|88b-%4P#rqwDnN zmQ(XnTnwXu)Aprl?^4CD^PR5Q_G?F{trO?1I1ftzDy9?uJH^!M-zugNgSK&x-Ar6w zWcQ^M6;8EkxHLRr576>nZJ&w=&KDfoM~9B`Khh+n-%EpOvHxOtkGgg?z%N?$-7FQg zszT3?Y1s(#G%elr%2*dt6j(K2a(!u!Ev&#%m<**zT6Jl2Q;j0h}P9tVQ|A?yw2 zZUKwuacB;MS>RuEJ!f*ecL1*{K~)mr=?eFG5$b5A2KNcxA0PQUv#i$+RX7n&Pmx|M zPmK{CREXd;_YXmjqaQ&z94I8y6qDMoKsO-2?RwIDcBLw3giPfUhXOcwA$NdMPD#J| zvY$T<;KNR?gmXR*=p%q-$HumXoUD{&{+(im%jN^bGKLSs+IKph4V=wkGLO$LY=W)V zPmTUXNh7`lVYl%nu6%#9J>B3k<#go?tL~lWQfM{=7KvsLRYod-?`F*QT-6bJU zr%u{l(CFQ&Q@WQ6zlg-4usehz>OV6#Y$D1S{lq20^D53soZ`d-{@61fz@Ak^e2aQ{ z%;I#`Ss(rHak-`^SIU?34a$u9KUor^qW>=fbboB(-v#deZyyh7RU2BqWA`)>f*Y0& zwkJ^Y!~9{mV9W&vEDYh;;AU|Jg?+)Y`*-1R4l$^^J&eE4@^zgIV=X%usy+m6PLk3@ zM09Z#*g+E-Gml6^o_|s7R=Z#1!VpoDmquON2res&d`v@EYH(*T;p&)D+p9KqfLPlf z|2of0P3*a8rl<8R1k%U@urP}RQSsOL z)499X=I^{}5ESgp!^=Z;@>7JM<7qnm8GORLg?a2J}iH^Sc+N4a7c(SD}n z`WW7s)*nVxxp5>4DKDrVUz;KFed=3pC~W9X zHAlh??zF8A|HvHKM)81Twd+#y>e#lPx1hY4ZP2;56~hf!QEbW(EssgPLh-$t?n3p0 z)u%G({amq>+2iems%7_wEfmBBDTDjEs<#)v?w0#jrMSB=Vv3C3!*ZM*J)3XrWqwYU z6dYR4QR^`Kv0?(SE|>hwYXNYUPs5U!@7(1$-jwjA_X94{3=3GV1miH(?Hg}}vaJ(=4ZSG@3b*$cm_!iVPSwxJXp*4n*Y`&w_vS5$xaiU)s=Urn!x-md(a zs)wtV=1Psn2M@Npdr}(nH(t_xZZ$*_bYY3XheET6C845ajGt6Sv_sCYUX{4B_vrZ} z0wGHP0;9|~W$Q+b3xU0yo<8pD%`V%cGNIzVpJkX*n#Da{Zo$iYN^4@{eqnn2^=0O z=T^U!kI&ZWc(RW?23|dBG_sC0zDU~C_LI5pODRi33n0IHqqtq?R$aueOOnP6}&ylB1w?va+A~Ai>w#Eu${%TGqn|{QIWxpYdXpq)Qc;hCz75)>$;mpQEgR3?RQ+om1iW(tRGuH@c!wfhX2^+@~!fsk^SPv z4JdTiR)6D2GunH6)W2o|s(1XzHf(eTiw-1R63x{{~@ZC$B<=Bt=ge$Es^Y)Y+ z6G$DiZMos#l3pq#wjc-v3!OW)o7?E}`Mv`~lCCkrp&U)prtc!!U(N|PaE+?W zl56-!bnXB;%Oq-!{BhkBekE$jmtfARW7LEy`--vtTQp4AG$IOxCpFwkyAm4H>szPz zd#@lr;~P|y8Gt27;t*3}*92}TG01=eS)fLL2Rh2?0h z6yXER`o&alZT!YF(Jr4Gy4JzgK9Q=Crkg^tEU3;{e0ViBPhT?(>dkI&W-~ePEp!dt72MxOnl#$K zVo7lawU$M!+$alfcv0D*l6ys*YT4JN>XtXatm?5SWP|3WSk8E6JbLccheE#gdwPK` z+t<1@r@0rAa%Re#C7ifjH<`AJFSFhPX6=b}0kOSsDO(!YOJh7oin1ipA zI*lHweIT3crb|5QzTbvI#eok)t)UH`y@w5S(lg-$`yvYMSO@pdbgQUue!(II6aplq zXT-*z*;V2{Kez9Z7%0o5p&E$m>oeRU1tWo8V*#JVJk@TkS=l;pUvY&|OEi_e(`_<) zd!QDlTFdHm68mAzdo-lY%k_nJmE#87iMVI_B$=Z4h@??f?3nv*Mp||8)at1xKGX~JY)RJ zx=^EiC0m%o_uNPIl<6*2)Qb33$IZ-K^HpZWygAr1BHrGc#lzV)!(g0A?o-)=psdU& zPng!m7|aAwagVJKYPwTOEa@2`tKUzWr_D*BFvf5DO3iWR#l;Sw4qQPVsC-o0Zxov zqkuR4fB_d{u6G3(+1a1w-)W;r%aY>o86O8EyM&d$C_GN^v6dwj5cbEjLdDT`))FT( zrG?k4;O=MmK0{;C$eHbHKx+#oyyCx$&Vg&YnjAy(OqDI4>&;D8`%s)+3b!Q2I0ca@ zM<H^%&Jov;`}@(Ct8C@!@wZa_aT=#B{+Xv~RX-G~^^+T??%g`coEDBZ0DPD>G( zFTb+$0aZ|%`_L^}v}4acOh2*Ow1*6T>eso4T|HamQai!HtpQVdyjBN<6z0#P(1JxY z|Blmdm6@#qS*Q6eb(zKG%9gV1hZa>k-GQamjRXaR)dA!&8N6AOpV52vb6->_*|}xn zbs=LEVw!&AaGde2uy`RLEO@waRV2JY23ki)cJo;cx8#y*b>IsZXb=wJ@eGB=`YR1D zTN#qOTyE@Rt0^4VuRIj@_fN(kuAz@Inn21Kv0ku(ul`neKEnYLys6OWTWgnxpmCm~ z`Mhg#A*CZHMz-%=QR2W~ybxYB_ckZMCYa-R%kiy*@}g??EbXUnkxuK8yV=+vmWrE| zIlaaDeyeS(cNnt6`a>xCqBZc=;EY-iQ@u*(lts5ZDh7~Nn2glOs9j92d1aL0;q~|s zbfm?z6xFc3;L^-1F|1HhJ$*-sPSbz~7cMF%W%uCCT*sYtH$H4v3`q8mKRG!GK0TZI zWOu}J{jfPfOyFQ|Uu;9A}edU~c&5!raefx6B(zquHRniuU(Ds&*&_mhDsXi7CK#S9R988wCqQb8SG z1)0Z#Ya}^hxfYqT=njkP(JG z1`&MStN__sug~B$Ib!s=)UKRnSIaAlIzH0!qOAe1*m~d(I@#!J6cJtq zlv^qec^(t0s_l$;3AD_!NL}o&6}|4WIKSl@glmnX>O64C-M)t9iLAMRm|H1cM&wGA zo=1nDsK##ZJ6IyG5-v1Fuy1Y;6>snvp_|KXmE;$(g~JpiD{Zi{!h}x=NnO}o z%57x(=naI9bP5$!-B2xzWa^3y^UUU2JFM zBjw0Q&}fWHzz82ZD6HqnVlJYa*#l#>qTg6;gn9ZU>|f+vQ^_CI($n@b?&dxk3IZcF zd|&2prIv~#^y4(Y;9BvLpEh67Bzu3iO5B<+n%$dmY761{#^nAa?!&jXxMy@ox5c#i zu~J<;To~D;jJ(F~<{gE}?u_RNX?8-em5!PkpK645@^l|6#6r|TY3nV{vPi+cfe)=w zV>a7rG8~w^FuJh`^P{i|FR)pM|H&M5U(oo|FzTxp(IZx-#PomoR~;{}5LH zTjs!`50CwK%%NNrQfcLbN?jdY&zXIM7_Ska?L%^Y`oBD%(GwYKf6byMsuED2^CYex;mF-0a5XIQ9K4G zM+c@0PVtSlXpu)~gtrEyBGyP&k1V~JBX_T1#jU3&8?oQ(a$G;TLnc9)hP-!Xa~6so%8mgA^}fT7PC#^)CY~ z2^`En;i`4;1a{wK7a={7&)KL6+pCX>T??OHeM-IerZ_vzRm&)TT|0eLKoSH2YTxIy ztNoCaI2?9OO3|pZU{=oi%cEX5a5@V`!_J{;U2ss&G3!VzXCc zCeDE(5q3K>Xqu5hBP(ghtVU-8eS_EMF+&Pf4#lscjVL3zvlZphHv4J@)S`E0d~`hF zIFDG|v5s`B9CB&UWc^>fnH4biawHkhJW^8fV7|+7BeyL#Zt>x2l1%Z{FqhfUaV?r}?_>A{DC;vHYibim zV2eePsolf*jgq44wHBR9GsK4HW;^_!uk;`@5!LUl+U{pfLHap4_f%H+{I92y!&F=$ z5pr1eo^Z<+REMnZbeL~$cmqU5WG_o$z_W6))n9s-98U^GB@9Sx>eXzr=ALB0JC`~G zn)%MJ22bsysYBfJI@`V#MLLJe?tFQ-hxG(E_ZO>y`@?Et*t~R0JTj6$7a!Km7=Pce z*mj>#)A8jRlgU!FrqOkJ=FvAGAf_g|9A8RAk!%{vuCI-?Ui)%4FTcXv=F!jM1g9uQ zU=rz&)fyuU_Df1k6^~Aeq6;QF0QL9xy(n{c8HSg!S&r6FxX{D5X_f9!`!7;Y@LbQ{ z&mE$}=5h+NyiK~{p?XJHg>FwKFSl0`y+r}2hu`O;VA5NnHdN*6Ytzm*iwPrxd}~`P zefWM%NjOxcZDvn$+JQ&?$b@9Xh5wyW%-^52f#sTuaIFbvh6UfVDLYb>Ta3We@bcXa z%p@+%xJQ*8G-6kNS#+dfGYYIhIT3~6g^2~bxY8)fFpKz0_@0Ak3$^?HA6WtdDc3nA zW??(x`iRYkR=pZB?wSZub3t}X%n$w+sV)t3f3cx_mv#%EgSYzinX37XFLt}!t6?ic zLGrLyaW1G<4-Q>yrf}+PNKK@S-3Xy)F3_SxzDG<7ud=D=a1@nOyV%7*@cx?BRVonk`JY& ze#owQ>||@h&C3+y@57Evj3gm-Wa_@!&-i+DXB4HBOdSO($KZq9PaMs)YlPqr@Lb3p zd*tNA_yX5f2E!$Z^V@&mOG~58=42VoQt}+LG0Gs;*Pl5wo78Qzh3X;r_=H>WK_}AT zxO)_C*4n!~l;(ct49r5m_!6fFYxjl)G+2C;P^Jla3g)A^CYFp(PU0aEV2AR}~-Wb0mEbzYiSnpApP& zV!`mqkk6azD@fD4f3{aL8srNLWjpt@*D<70J?^H2J=zjm8hdV3qVMQnyj4Qq!_-EP#A5E;2bGdN0c6Nq;UXxHf6yR%R zT9Vpur+hm>y5P27x*JAVjnt7};<~Pz8XJ?_Al5&2?sy!Y`M6cMy~hb7`RxOTM$M0g zE1&ZhJg$y)40G6x-7Apq2+JJWyw|=6J~V-)Eyh(nffYopzlF`4pI=bua3%#GyK>Pw zvAzZVN@pWj>ynH`pM-2?6~S$w6^qu6{cf%0{rKCa1pH&zI%rI8c#1q?LT0Y$3^gHb z;PmXQtI%c2bDJT{i!o*BM_kU=d23?s=D0V$iCg>?C3uy<^ zw74y}!&GZVa+D=O%cW~1x5L3oq54wFvJ%zQ(pcQb;X(bQ zx>1>j25(s3dl@0WeE%rWp_A}&{P^^)@H^EDWQEfV6;zr-Te442MKJqRzP>Soei0So-O@G$^b+Np z^+rcVROM+JNscg6mfg>zI32yK1fMMvF%#d1Vky11X*rOlY^4WgSYsVBckU=KB)F15j$UAP`W7unf;X$B3VP zvLx^1Q|+%R@_sk<%FgKyCK4;YM{NHqUzjLHoFvwa&>u4NuiY6z2t<^>b_)aH@JasK zm4?OU5&vs91%t`;$6vclw11r#IBNK9a8uyVSx|=ResuX8zUJP`Yo4AwN&NXwC8n!~ zHZ+Wz95HK~jJ3$LDZ~8s#hQ}klhZ_8YlCqm`1=Lt=ja!#4MDlfMq7q(3uYM8iT()?6%|A~{LE$d zqG6y7^Lk4L2PTamRz>ebyKsI9wnl#CM9q8Bc+EsDf?r`|u{R--ngl6LZ`{Slo&HrV za3V6gH)q85&&nV@)cDAI{3oW{m?~s!Y5cQ?dwibnYcEW`UN(j5^$NiB*du3hO$Mi7 z7U0SJ-X!bj8q&4seyXJ?q=;#RMJWBHDO|0?96UVy zh2uAezf2LwJWn-v0w}ejoc<0ZPpdDKG3%oyr`ij#Rb;P4D;4(a`sqVnXQmMOa!B(` zW6Aw+v3h&@8Tah`fgZM)Q(wX!ncCq| zN^YYKbUb}GAG|H=VeS%5&MSDOK!eC+%0`VSUQg=!?_pOmD;9Wxwa3H_lnxkeLcr!( zeRqB8>~7BXGkdUaH^jdHcU~TYYvRJ|Wf2@$2-dVCAA-%G9@M@zX2QIn6d^jHHsEob zK(V2kASHj)n6Pc46crrGabJTlSB0DQXu%a~8lt~MD(4@K(MDCS4~gxmakN6%uhAEx zCCzbP=bn_39w{)t`9@VkNt(X1!-?Hun|$?xQRzoouG1Q{D-_&>6pDfcmu(_-$L*ki z)hk(_#rHiB(aveJ&=>u%YZ`vi7bLfJnMwS3s)5Cb3l)nM2KwYBByUSFo(`c8d8^lN zd2mnqcnN8nn1}+OUrbLOch?xKA@(8g>VP(~DJdZj>Z?f-4cJQhf*X0Lusv=rXZn&$ zsw8ji*Kk7~=TE%vA7mkkYzWai-X+O8B8P$TCYiz0J4zwddc)|Q3C?0N>1i85>Y z=0P5g?CVoZANRidP=y(ZQT8?Og}Y)@3ENRE7KVk)PspzQd94#ax_TnN z0AW}{e7Vn>)|%#o7N;8mzvq4F zg}vg~VvEg~%RE-N0u6$euQ!vvfXg7le?=QFRSxk*9LRrSoh^NmM=c74&qu&NG38G* zfDV)T;%NU)?^tLt6g9x->eEH&`=A=TBc?*(&%>SHtC(aBH?ndd4MIpKVmo;fvN5s= zcakOk3_G!Zu)5eL6?Ks?{Pb*<0jCp4lpg*pGuJ@y$7dMHPz&!i1g64iJR@rL#z|1O z$CDWWo1K#7X4mlb)5-;$)m~DzMe}{SU&kU`k+1BX?J-LAjLr7yExi57nsC~L!BTVa zZuE8qCDFwzVkr5^tPTP8su<6?9ghotl#@HX@oFGQNsk2zXE7T8fxz|1SvP~*oKrzIJ0LSGZmR#- z6%uO3d9m*<4~85I?TQ=Vm-|h8p>yP=!0#maqlk>-~HHlVu6+-+&t!Z@{8TpNtZH=zmg2;pxw*{J z)DZXwf9!$p!b<%?JnsPM+Q;aeA?`o0MeNEC%P#*<^Q3=p#=k<2|4SIg9u*l%l`)L1 z;HJ|CK@xmCRm|2^c(0~x#gl^WZY=IB2qa3W8F(Z)M?e?5$p`W!YtLGGP;+A>U3m+3(XN^bdXr`vaQll^(7824A=)8@Aut&o;3= zx(lh&lr>EgT!i)s;fNJe3{RU=UYMk;NWX>Xc&7<7@I5(gpnV;rg0U_4iSi5fCSaW_ z5Too@xMFGUe3C=+JTloiyN5UhrJvli;1lh~O>8>CAdOt`Jrk{iaBbI%PEdz#J}2#D zB2&GJP^-1qMSREti@@6Iavsm@7|l+3F&)AR07BaBmTXh=o0_I5{O+&I=D8)qBn}f} zCrL}@Bj)7Zx0vjOr)gar>a6jxV&~;AavU%CSh5Zgc{pP0pC2zeiIA_hHLqCKX|wuW zU(n&GmD?qRx2R_2d5yav?(CHojIMvdH-!`)p$<6#1A+b7zHI5I@%ybHM2=O|u34U) z2SRfr{@Za<#Qim&;1R8Np$I7O{Dhi5gl@>l39ft{kchf2ozj~>`I;@?NYhs`d$sD2 z`bu=g=;BP5W%6HzXmI6+Z7bgt+mxvl;yG0AtObL;!w-Gp(;*3ZxT?c##V&`*CC~cw zJ{dHfEi-{oM})_nd2so|%ZjnC*`!Bc+YM$ftok(?QTP2spz;##o0CFVoVFJqi9K(l zvACRhO46RTcX%$LBV0N}98E_>6lnU|UnEuojVNRxo$Gw>cWZ%$*Cqmty=kF7=XU`b zTyakx>|0CcA1kLtLWIdf3w_A!PrYbX{2h+@H@A&~qVQ!JQl21pyvw8RaryhdJL>AiqBSLTvYa`0$P4lUL{Q^ zMgT9{FZviQdd~Owlzes~xhxtF^4^5xSk5GKtbTiZB(2C0ovY|kymQkuLWko1ZnwoL zh?)AeEm#%CA@AxPo#5O@(bY_)Dz#^s%f{76Bplt?CrIu%D`?!` zQ~bdT9dfJ56%K}iS=Z=A9V(-JZj6Wk=YW%M^KMi5-rwNuNA@M&)^^lc4w_npHm_|m z*f^lc{_vB@iXc$xNK2@o$(Z_hq@6=4d{LhKhG2dMUE*StsEww)WVcep%~dH*Z9^m~ z!;;w2-eLtqBJ!6DY81iw*CO#K@CE;fqBcuuPnh5803c5x3T^;k=*dq2?P2hhLF&+U)XrV+-)|7Xf?m9w@^sf#=-~z)tgo`3;UyH77Fv3>+$su z*&v<1uYWAtDQ^zGzu1SqM#xk04oq*Eh&gy9w97-8;MEyGUm82d9r(Gbac=zp;eAC9 zt!+v?<%ThD^F=W_mH}MSj`rvr63Z=%j66Z6vM>X$SSqVuXHD*UO;!BBISez^E+U*B ziDx6aLS<|H60W1(MB)>tU&fg1_Vxcn(H=q=St-_?N=82wsYCjj&K}4r7xBAo@cQeX zp(^t)!r|P*pCdYwwW84)kPbb~F{gg9c#`rdHj^)R^;)_oP<${lOFV4}L#=Q`*FVRL zC^Nt%n_amDgIHPRICH~x*~mh*JQ1sSz_ow&z;ZvE755pR=bYwRN1xH6-UGEI2M;nL zGXKIk%2}YvDYV9hb;#I`q_kIS(StV%HI6!48OXU3xHO-yp;0wlXG4h7J;p{j+Zb7S zlAZ1%GFOeykGP)&mnxwRceohTUUz-^JaFU@vYcmMUa)$)`Qi_`=u-;T>WPP&bRV}L zf=tSQ^%<>I{V#<0`RnCZ@Vl*1v(L9(e4}bewcAMt{skP8Qa@Xx^*D}gF^a$A4&;72soXaB zkWh`K_V~GBJ$$MTv8Se5kXCI9KVk%uAzqTD^8@(Z_NWz!`;k+`mUnr_La3(cmx>NV znZ-p_gdg&3AkEDWtEQwQKP%IF#Z6G(vCQ`A^mQ%^oNR_4^c;TeW4pDbPQ>qzN*A*+ z`{=_Nmh6}D^$-`J3Xs*^IdD%}Hl2nwiuSG4RlWyLRWVRE27ww3J%`Uk{_6%#5?kTw zH{wR&b$rV1R&M~Yliro3Ja(LvFU{=LVg0hxz7WU0>v$8UucY(A!4*ng;ki#OClmT+ zte=yqqrYw;4Ee$4#BJ+W^Nk6b_u~e|^L1xzqxG)S|I^lahqKlGe;ifR3bor>RjaKT zd$&UE@o3FbBxbGJyVMS%X06sLYR}qRtkR;W71Sn3>{#I!eSY8H@A^L1lj}PFBxhas zN$zuApVvG0J>!FVpBBMn-q$1%FrIC%%K1p#phoJax*Pb0W`Iz?iAtD2M?Q^d=#s*) zmGO<}FFePtx~lDELX0dJei0TXF3DP(_bETvQ}oYVq6aGOId;RJ2dtcAzKjBqL?o`_ z@FW_G&)>T6Et?W#EIC^nL|n{};MEw<-ui)zdz~lF8UW^Y2wh88>wAq97b8gvoA7Y z`VH&ps3^S+X8N{bDNt50_{n?FvegE9$ec#NxJQ|!J&NhOV5G)>_(h|jNt2GcEGyxo zb+5;r0fF_B(nSsm1Llm6fhS^eQbLaXb#uj0-o$ib%e88wCV1 z$hv#fS|NRvc%g*=w<$v1P~>D~)7hATe4-oVF;fMd_*sjfN~TXrM;IhKV*X=WtO+fy z2tx(@sXjJsmd@{OSP7wfE$-CyW?PVgU!4Ezdr=HJ+tb|!e>CVuyv3|U=s!s5TUzul z1rF{X0?Xyib^AJ+?~kL8#=R)P&4`8=o_HbW2e;#S!Utp3J2K;DntWj4jKz>4HZ{a!(3KOJIx%6XB|24kKQ4a~$pm7uTawOiQ-X8W8Ud=ef;Fj ztpvNwTe<(wR7fJw-=qDEsAoN8CVOOKnls#N$!#}xVaifEXJ*>QAWUF+g94TfN+iv=II?9Io9(ZMvY z`K2Rq<;(LOlNJ;C&}55QLk72`GRWN1=oFfBpM&qM+s=$bE*0V=p5ty89f6dl<3>8o zu>3{>lenDMEG^)r+Yyr?1zQ_#%n~s@$WAC-&Wq9~dyf^q3n+emc%vgi-#7yK=qax+ z3EEWDaECatg0N+9WS^9}IE>D=yLygzLuJzf7TF>_ILt0z0l zqd|8@9FR5;XwPwv_=oZI0G7*)qNZB_;968i2Hf|<@1j}gM3Vq+nBa^U^ShdG$}7CN zJ&CrXyU!d-!6uSlZgP^~4_x7Q1?z~y4Yh}?3mwjkH6IYkO6beITN427(VCvWpE^2J z7n7#geg>HQJ$=XsL)@ozzG>`ahO86bq<|duRDCX%reg}QzcU0v6@8QXHg!q&qG56& z&f}8P68}by6cw_+-uSQa*Y#L7$&PvqUHIkjI-~#cD?v-YTiCvJ#)~*KO%_V9v?!A! zZ|KNJXgD6%=Vh<(e}2f_slcXzc<#c$UoZ})BHc0!{x>fEWH&28%%EyRi~FOXX1_#g zY6{KUa$-@cPS@4HevjNzCSU5du=K>Iq}$HnpNV*6Sl5)G6CmDH5}Kax6FP&uH5K$4 z->ZyY(OF8}&786@$}ko#=S}=}j(^`^%CJ?QyY^}_ageeu(Z6Y06*sgrNeLkDh%(pb3q`5sU?v z=7eP?DmM+s_ly+5@jDDrEWXrLm%tM*e_lyT>0SE8R{0)byaU|CQEaGWS&(t>XHMq4 zY6%NlDKUF4-;aMI8 z!t>>(q0$1`;xFN^&KKTbF6c&KvLLm#ck7)W_Uyzw??+C^3YTzXe5~Gljmd4lqN#0` zSO3xdN?}Tez4u6}!rpBqA(ZdotcsctQylOy>sHEY=Dw?NNzXO}l!NcoyUWntehr zUt_l1l5*^FQD{b9qcJ|GfM%Q7G4Mp@-0w$WX)o;P=Wd`tQTW#MqCV`h{THo?limeu zXWAs_Ld9#X`ZWAY7q;W7YZ>?Z-?2Cuk^$WNy>>hp>+o9cbTT%^Y5GQhd|6 zZVa8p6IWY&_l8AU=325* z?AUkZx6lbZw)QUR7kiC(z^Y}XK2Z7>ZpcF1u5i0}ZeTEM-0E6fu(D?H$8|KOSlSOU z7_CQV)^wKTmgqc5+eLWbwp~!U-tZ>^q2cJbGx2u#+B6|#dIK{HDj3(#i8DA_qmnp9%4Q1|V*u9rS6eN@Bb;Zo@h5taRd{ahOYr(q3L;4NBVW^;SRF+?q zirxWa_H&Yjz_&Hl<@G*tW@%U~wCA0+~6t3lTty`Wx zF?y-glU;9NY{uApY1{+`9-%>|vRk{Q=l9CPCU|hSx*fRf1#$S@480RGq70kEdbO7( z?a=1Ww$*)xv+v3^{#J9j~;e!%SI#@ zm37=k;0i=ShYxeawao1XU7Y<%*PpMDLpB!PH0du+*O3`4O@?CR;+VXC8(zupt*j1L zx6|b%Cz*dbrOzqnu_a1r#oyM?b(U9uY;G}2RB53B=7mwG&DeFP7SHkCwSxyY9ok(? zU_lE3VWCbo#~7pI#esEDfgJN3Z`cqe*Q{`!!&ub>q{%o#H`IJ_sW>UnwYl0-$#nqT z+sj+JEv*D;QxWE#Ql>RA2}(~Sv&I98q#|@&$xP5Zj&Y zfF4;W`3Xq}#>zbp(l-ou+Zw*6B;AwEx17#%Y8|~SPCY-nr{b^e%x?kKn*R6Kng@Dp z?zzqG-^S!IfQ?}_N2R_*Cqc-;?(K0xh=^$nSNs_nPjr1AZ<>eKswh>!mFVr9z)}}7 zYeUu|0T`WqH}n|n*(R3jw&$HkkB9&k22T;63xb~v!CvJ8QkHo5vOX)?GdXO3(qlv! zi)0bNCT!a%H4gY|W$5mSN|2tZx_jB!me7)-dsbziZ6BI2cHb~m<3%~sM zWeY2DkRI8Jal5T3YZ&kJI6y=p9bp4Ju2%u1$VJjonkIG%SAHi1vrO~}E~dupXmq3S zY`;XeL*IT4nd)R1jd*sXwT`aY=`KPJ{wmyrXju#zeVy-vJE~2_LYf-SzdPk@`x5ZbB!#@}kCW%Tqhr-Snb{j)r)6gbCh#gb3N;S8J`# zmW^Sqr%qR18mb}W=Lw?l73ay9nDnO}UP>M?puw4DK#F&+;4*C_2NLp}+Og32lLq7RcJ6i7WT2yCEb>s&Hm%2}`Mi-5u%=gFx4 zUq5JfKEb2)tEju2EtUJoM>Mvs+r$nzI8|r|X2aW#x}ynl8Q*h`>em>pbf` zw|Qqw@kwsWUn&l*;*Ago!w&yt>Tx7wUr`Sg4Q(MbA1K>9X_YhbK7~&}{hJd!vH^8-E z;)rTbg%5QE$UP<1+5ffNj|=tUo>|08h#%>P>Q@o<)ZmG>KR$=|mhRy0LL5`dG*#X5 z3SKfWXKXLz+z1;WwXdMe`l+AxOeBA($U$__6p7U|c0u=1Kfdgf{Fo@Cci*?`y>GBAFllOY@B zb|QEDjE5XtcMv~#bYwm?`3?dEu>O({M(2FwNsnS~eK$(gALvlmu~S@50Rps>-}Op3 zh!O~yhuRf1huKu0dYokZ)A$k3*7QqwT6;JU61%Pg&=Mqn5p z=F0w;qm?l|!|*3nYfV(;)?3^oTAbV&4_-y<@tt4=wn6H&#`LHL8vTh(!xVcf&Bo%j zSkMz&7Wg~v{>DO5Z2bb4*rfWBbq16-&Bj(j?2;sFpCf!;T6D5Yv#Ry7MtgO(1iwR|Fro=<{fq?mD{am*?4 zPnap8<->f6p6IgyF)Ozvw8reu30xEhmqYRgTXM0Fp`Xt>%?>F_aaptWUjC?cJj8$? z3hcNQ5=_z@D%J|`N_040%n%f`!Fu3$1gVF94%2Wj=k*DsX@73=vKmy5S*@dF`6-mV zN40sZ8a5<%eb5#C6<_)ERKA9WosbCk>4+jCsyqlO73BC&BOJuKW6FyloKEMz*=f?M z$%MDAn=4^9IE{b$eeng1ghe%QOTP5nkQVPh37hx@^D@54G4gZE#N(yjnN-d z+6Md>G3RhTFNFIbD8qy;K}YX0tj E08@aZr~m)} literal 0 HcmV?d00001 diff --git a/docs/exceptions.md b/docs/exceptions.md new file mode 100644 index 00000000..96705aa0 --- /dev/null +++ b/docs/exceptions.md @@ -0,0 +1,45 @@ +# Обработка исключений + +Исключения, генерируемые библиотекой. + +* `InvalidArgumentException` - при передаче в методы некорректных параметров. +* `UnexpectedValueException` - при получении от API Моего Склада нестандартных ответов. В нормальной работе возникать не должно. Если вы с ним столкнулись - опишите, пожалуйста, в [issues](https://github.com/evgeek/moysklad/issues) обстоятельства. +* `Evgeek\Moysklad\Exceptions\RequestException` - обёртка для исключений HTTP запросов, генерируемых [Request Sender](/docs/setup.md#requestsenderfactory). Исходное исключение можно получить при помощи `getPrevious()`. + +```php +try { + $ms->query()->entity()->product()->method('new')->send('PUT'); +} catch (RequestException $e) { + $previous = $e->getPrevious(); + echo $previous->getMessage(); +} +``` + +При использовании стандартной для библиотеки `GuzzleSenderFactory`, ответы с HTTP-кодами, отличными от 2xx и 3xx, выбрасывают исключения. Получить объекты запроса и ответа, а также тело ответа можно при помощи методов исключения Guzzle (см. [документацию](https://docs.guzzlephp.org/en/stable/quickstart.html)). Помните, что тело ответа отдаётся как stream, поэтому получить из него контент можно только один раз. + +```php +try { + $ms->query()->entity()->product()->method('new')->send('PUT'); +} catch (RequestException $e) { + /** @var \GuzzleHttp\Exception\ClientException $previous */ + $previous = $e->getPrevious(); + $request = $previous->getRequest(); + $response = $previous->getResponse(); + $content = $response->getBody()->getContents(); +} +``` + +Аналогичного эффекта можно добиться, использовав методы исключения библиотеки. Контент в этом случае форматируется установленным форматтером и кэшируется (можно запросить многократно). + +```php +try { + $ms->query()->entity()->product()->method('new')->send('PUT'); +} catch (RequestException $e) { + $request = $e->getRequest(); + $response = $e->getResponse(); + $content = $e->getContent(); +} +``` + +| [<< Вспомогательные инструменты](/docs/tools.md) | [Оглавление](/docs/index.md) | - | +|:-------------------------------------------------|:----------------------------:|--:| \ No newline at end of file diff --git a/docs/formatters.md b/docs/formatters.md new file mode 100644 index 00000000..1e541529 --- /dev/null +++ b/docs/formatters.md @@ -0,0 +1,121 @@ +# Форматтеры + +Классы, определяющие формат данных, возвращаемых библиотекой. + +* [Общая информация](/docs/formatters.md#общая-информация) +* [Простые типы](/docs/formatters.md#простые-типы) +* [Record](/docs/formatters.md#record) + * [Конфигурирование классов](/docs/formatters.md#конфигурирование-классов) + * [Совместимость подходов](/docs/formatters.md#совместимость-подходов) + +## Общая информация + +Форматирование применяется к ответам API, возвращаемым [Конструктором запросов](/docs/query_builder.md) и данным, возвращаемые вспомогательными методами (Meta, Debug и т. д.). На объекты и коллекции [Record](/docs/active_record.md) установленный форматтер не влияет, их можно привести к простому типу методами `toArray()`, `toStdClass()` и `toString()`. + +Нужный форматтер устанавливается при [инициализации клиента](/docs/setup.md). Все форматтеры реализуют интерфейс `JsonFormatterInterface` с методами `encode` (преобразовать json-строку в нужный формат) и `decode` (обратное преобразование). При необходимости, можно использовать их напрямую. Каждый встроенный форматтер умеют декодировать данные в любом из встроенных форматов. + +```php +$product = Product::make($ms, ['name' => 'orange']); +$currentFormatter = $ms->getFormatter(); +$productString = $currentFormatter->decode($product); +$productArray = (new ArrayFormat())->encode($productString); +``` + +## Простые типы + +* `StdClassFormat` - Форматтер по умолчанию. Преобразует данные в объект `stdClass`. +* `ArrayFormat` - Преобразует данные в ассоциативный массив. +* `StringFormat` - Преобразует данные в строку. + +## Record + +`RecordFormat` - форматтер, преобразующий данные в формат `Record` ([Документация](/docs/active_record.md)). Данные, не являющиеся сущностями Моего Склада, преобразуются в `stdClass`. + +Ответы от API преобразуются в объекты на всех уровнях вложенности, что позволяет работать с методами вложенных объектов. + +```php +Customerorder::collection($ms) + ->limit(100) + ->expand('positions.assortment') + ->eachGenerator(function (Customerorder $customerorder) { + echo $customerorder->id . PHP_EOL; + + $customerorder->positions + ->each(function ($position) { + echo $position->assortment->name . PHP_EOL; + }); + }); +``` + +### Конфигурирование классов + +`RecordMapping` - конфиг сопоставления объектов Моего Склада с PHP классами. С его помощью можно регистрировать отсутствующие в библиотеке сущности и коллекции, или переопределять текущие. + +#### Регистрация + +Для регистрации классов используются методы `setObject()` и `setCollection()`. В качестве входных аргументов оба принимают строку с именем класса (или массив строк для массового назначения). Объекты должны наследоваться от `AbstractConcreteObject`, коллекции - от `AbstractConcreteCollection`. И те, и те должны содержать константы `PATH` - массив с сегментами пути url сущности и `TYPE` - значение поля type из meta сущности. Для автокомплита IDE требуется наполнить PHPDoc класса, за примерами можно обратиться к реализованным в библиотеке классам. + +```php +use Evgeek\Moysklad\Api\Record\Collections\AbstractConcreteCollection; +use Evgeek\Moysklad\Api\Record\Objects\AbstractConcreteObject; +use Evgeek\Moysklad\Formatters\RecordMapping; + +/** + * @property string $id + */ +class Contract extends AbstractConcreteObject +{ + public const PATH = ['entity', 'contract']; + public const TYPE = 'contract'; +} +class ContractCollection extends AbstractConcreteCollection +{ + public const PATH = ['entity', 'contract']; + public const TYPE = 'contract'; +} + +$mapping = new RecordMapping(); +$mapping + ->setObject(Contract::class) + ->setCollection(ContractCollection::class); + +$ms = new MoySklad(['token'], new RecordFormat($mapping)); + +Contract::collection($ms) + ->eachGenerator(function (Contract $contract) { + echo $contract->id . PHP_EOL; + }); +``` + +#### Переопределение + +Помимо регистрации новых объектов и коллекций, может быть удобно расширять имеющиеся - допустим, чтобы дополнить их нужными методами. Достичь этого можно аналогично, перерегистрировав имеющийся класс. + +```php +class ExtendedProduct extends Product +{ + public function printCodeWithName(): void + { + echo $this->code . ' | ' . $this->name . PHP_EOL; + } +} + +$mapping = (new RecordMapping()) + ->setObject(ExtendedProduct::class); + +$ms = new MoySklad(['token'], new RecordFormat($mapping)); + +Product::collection($ms) + ->eachGenerator(function (ExtendedProduct $product) { + $product->printCodeWithName(); + }); +``` + +### Совместимость подходов + +Подходы Active Record и Конструктор запросов полностью совместимы между собой. Можно как получать Record через конструктор запросов, установив соответствующий форматтер, так и передавать их в качестве параметров, вне зависимости от установленного форматтера. Аналогично, методы Record, требующие payload, могут принимать его в любом формате, а сами объекты Record могут быть приведены к простому типу при помощи вышеописанных методов. + +Иными словами, можно пользоваться обоими подходами одновременно, работая с данными в любом удобном формате, не задумываясь о их преобразованиях - библиотека сделает всё сама. + +| [<< Объектный подход (Record)](/docs/active_record.md) | [Оглавление](/docs/index.md) | [Вспомогательные инструменты >>](/docs/tools.md) | +|:-------------------------------------------------------|:----------------------------:|-------------------------------------------------:| \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..1108299f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,25 @@ +# Оглавление + +* [Настройка клиента](/docs/setup.md) +* [Взаимодействие с API](/docs/api_interaction.md) + * [Конструктор запросов (Query)](/docs/query_builder.md) + * [Базовое использование](/docs/query_builder.md#базовое-использование) + * [Методы формирования url](/docs/query_builder.md#методы-формирования-url) + * [Методы отправки запросов](/docs/query_builder.md#методы-отправки-запросов) + * [Универсальные методы](/docs/query_builder.md#универсальные-методы) + * [Итерация результатов](/docs/query_builder.md#итерация-результатов) + * [Отладка](/docs/query_builder.md#отладка) + * [Объектный подход (Record)](/docs/active_record.md) + * [Общая информация](/docs/active_record.md#общая-информация) + * [Объекты](/docs/active_record.md#объекты) + * [Коллекции](/docs/active_record.md#коллекции) + * [Универсальные методы](/docs/active_record.md#универсальные-методы) + * [Расширяемость](/docs/active_record.md#расширяемость) +* [Форматтеры](/docs/formatters.md) + * [Общая информация](/docs/formatters.md#общая-информация) + * [Простые типы](/docs/formatters.md#простые-типы) + * [Record](/docs/formatters.md#record) +* [Вспомогательные инструменты](/docs/tools.md) + * [Meta](/docs/tools.md#meta) + * [Guid](/docs/tools.md#guid) +* [Обработка исключений](/docs/exceptions.md) \ No newline at end of file diff --git a/docs/query_builder.md b/docs/query_builder.md new file mode 100644 index 00000000..d528659a --- /dev/null +++ b/docs/query_builder.md @@ -0,0 +1,285 @@ +# Конструктор запросов (Query) + +Простой, минимально абстрагированный от API Моего Склада способ собрать любой запрос. Для эффективного использования необходимо понимать, как работает API. + +* [Базовое использование](/docs/query_builder.md#базовое-использование) +* [Методы формирования url](/docs/query_builder.md#методы-формирования-url) + * [Сегменты](/docs/query_builder.md#сегменты) + * [Параметры запроса](/docs/query_builder.md#параметры-запроса) +* [Методы отправки запросов](/docs/query_builder.md#методы-отправки-запросов) +* [Универсальные методы](/docs/query_builder.md#универсальные-методы) +* [Итерация результатов](/docs/query_builder.md#итерация-результатов) +* [Отладка](/docs/query_builder.md#отладка) + +## Базовое использование + +Конструктор запросов вызывается при помощи метода `query()` базового объекта библиотеки. + +```php +use Evgeek\Moysklad\MoySklad; + +$ms = new MoySklad(['token']); +$ms->query()->...; +``` + +Взаимодействовать с API можно как при помощи реализованных в библиотеке сущностей, так и при помощи универсальных методов, которые позволят собрать любой запрос. К примеру, запрос `GET https://online.moysklad.ru/api/remap/1.2/entity/customerorder/00001c03-5227-11e8-9ff4-315000132d57/positions?limit=2` можно построить так: + +```php +$ms->query() + ->entity() + ->customerorder() + ->byId('00001c03-5227-11e8-9ff4-315000132d57') + ->positions() + ->limit(2) + ->get(); +``` + +Или так: + +```php +$ms->query() + ->endpoint('entity') + ->method('customerorder') + ->byId('00001c03-5227-11e8-9ff4-315000132d57') + ->method('positions') + ->param('limit', '2') + ->send('GET'); +``` + +## Методы формирования url + +### Сегменты + +Делятся на три вида: + +* `endpoint` - первый сегмент в ссылке после базового url. Примеры: `entity()`, `report()` - именные; `endpoint('entity)` - универсальный. +* `method` - сегменты, следующие после `endpoint`. Примеры: `customerorder()`, `positions()` - именные; `method('customerorder)` - универсальный. +* `id` - сегменты, содержащие guid-ы сущностей. Пример: `byId('00001c03-5227-11e8-9ff4-315000132d57')`. + +Друг от друга методы сегментов отличаются набором имеющихся у них методов. Универсальные содержат все возможные методы, именные - только те, которыми обладает сущность, представляемая сегментом. + +Если же вам требуется отправить запрос по имеющемуся url - вместо того, чтобы вручную составлять запрос по сегментам, воспользуйтесь методом `fromUrl($url, $withParams)`. Он самостоятельно разберёт `$url` на сегменты и вернёт конструктор запроса, позволяющий продолжить построение через fluent-цепочку. Параметры из url по умолчанию отбрасываются, если вы хотите сохранить их в конструкторе - передайте `true` в качестве второго аргумента. + +```php +$orderUrl = 'https://online.moysklad.ru/api/remap/1.2/entity/customerorder/3aba2611-c64f-11ed-0a80-108a00230a9c?expand=group'; +$orderPositions = $ms + ->query() + ->fromUrl($orderUrl, true) + ->positions() + ->expand('image') + ->get(); +//https://online.moysklad.ru/api/remap/1.2/entity/customerorder/3aba2611-c64f-11ed-0a80-108a00230a9c/positions?expand=group,image +``` + +### Параметры запроса + +* `limit($amount)` - ограничение количества записей в ответе. +* `offset($amount)` - сдвиг для пагинации. +* `search($text)` - контекстный поиск ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-kontextnyj-poisk)). +* `expand($field)` - разворачивание вложенных сущностей ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand)). Несколько полей можно задать при помощи нескольких вызовов метода, или передав в метод массив с названиями полей. Помните, что разворачивание работает только с limit <= 100 и до 3-го уровня вложенности (ограничение API). + +```php +$ms->query()->entity()->product() + ->limit(100) + ->expand('owner') + ->expand('minPrice.currency') + ->expand(['group', 'images']); +``` + +* `filter()` - фильтрация результатов выдачи ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter)). В метод можно передать три параметра (ключ, знак и значение), или только два (ключ и значение, в качестве знака по умолчанию будет использовано `=`). Знаком может быть строка (`'='`, `'!='` и пр.) или enum `Evgeek\Moysklad\Enums\FilterSign`. Несколько фильтров за раз можно передать как массив массивов с параметрами фильтрации. + +```php +$product = $ms->query()->entity()->product() + ->filter('archived', false) + ->filter('name', '=~', 'apple') + ->filter([ + ['minimumBalance', '=', '0'], + ['code', FilterSign::NEQ, 123], + ]); +``` + +* `order()` - сортировка ([doc](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sortirowka-ob-ektow)). Если направление не задано, будет сортироваться по возрастанию (`asc`). Несколько сортировок можно передать или массивом массивов, или через несколько вызовов метода. + +```php +$ms->query()->entity()->product() + ->order('updated', 'asc') + ->order([ + ['code', 'desc'], + ['name'], + ]); +``` + +* `param($key, $value)` - универсальный метод, позволяющий сформировать любой параметр. Несколько параметров можно передать массивом массивов. + +```php +$ms->query() + ->param('search', 'orange') + ->param([ + ['limit', 100], + ['offset', 200], + ]); +``` + +#### Нюансы + +* И `param()`, и специализированные методы при повторном вызове поддерживают дозапись в параметрах, где это возможно (`filter`, `expand`, `order`). В остальных параметрах ранее установленное значение перезаписывается. +* `filter()` автоматом экранирует `;`. `param()` - нет. + +## Методы отправки запросов + +Тело (`$body`) для запросов можно передавать в любом поддерживаемом формате (массив, stdClass, Record, json-строка). Зачастую в тело запроса требуется передавать метаданные сущности. Их удобно формировать при помощи класса [Meta](/docs/tools.md#meta). + +* `create($body)` - `POST` запрос для создания сущности. + +```php +$newProduct = $ms->query() + ->entity() + ->product() + ->create(['name' => 'orange']); +``` + +* `get()` - `GET` запрос для получения сущности. + +```php +$product = $ms->query() + ->entity() + ->product() + ->byId('c7a48c56-f252-11ed-0a80-0f6000639033') + ->get(); +``` + +* `update($body)` - `PUT` запрос для обновления сущности. + +```php +$updatedProduct = $ms->query() + ->entity() + ->product() + ->byId('c7a48c56-f252-11ed-0a80-0f6000639033') + ->update(['name' => 'tangerine']); +``` + +* `delete()` - `DELETE` запрос для удаления сущности. + +```php +$ms->query() + ->entity() + ->product() + ->byId('c7a48c56-f252-11ed-0a80-0f6000639033') + ->delete(); +``` + +* `massDelete($objects)` - `POST` запрос для массового удаления. в метод требуется передать массив удаляемых сущностей в любом формате. Сущность может быть как получена из Моего Склада, так и сформирована при помощи соответствующего класса либо хелпера `Meta`. + +```php +use Evgeek\Moysklad\Api\Record\Objects\Entities\Product; + +$product1 = $ms->query()->entity()->product()->byId('cc181c35-f259-11ed-0a80-00e900658c8f')->get(); +$product2 = Product::make($ms, ['id' => 'd540c409-f259-11ed-0a80-00e900658e53']); +$product3 = ['meta' => Meta::product('d540c409-f259-11ed-0a80-00e900658e53')]; + +$ms->query() + ->entity() + ->product() + ->massDelete([$product1, $product2, $product3]); +``` + +* `massCreateUpdate($objects)` - `POST` запрос для массового создания и/или обновления сущностей. В качестве параметра требуется передать массив сущностей в любом формате. Сущности с заданным id будут обновлены, без - созданы. Возвращает массив изменённых сущностей, отформатированный текущим форматтером. + +```php +$product1 = ['name' => 'Корнишоны']; +$product2 = Product::make($ms, [ + 'id' => 'efcddaff-f308-11ed-0a80-09ee0084c2c6', + 'name' => 'Кабачки', +]); +$product3 = [ + 'meta' => Meta::product('1a4d67b8-f309-11ed-0a80-086800825780'), + 'name' => 'Патиссоны', +]; + +$products = $ms->query() + ->entity() + ->product() + ->massCreateUpdate([$product1, $product2, $product3]); +``` + +* `send($method, $body)` - универсальный метод, позволяющий отправить любой HTTP запрос. + +```php +$newProduct = $ms->query() + ->entity() + ->product() + ->send('POST', ['name' => 'orange']); +``` + +## Универсальные методы + +Методы, позволяющие сформировать и отправить любой запрос. Если в библиотеке пока нет нужного вам именного метода, воспользуйтесь ими. Подробности и примеры использования были рассмотрены выше. + +* `endpoint($name)` - первый сегмент url, представляющий входную точку API. +* `method($name)` - последующие сегменты, представляющие сущности. +* `byId($guid)` - сегмент, представляющий конкретную сущность. +* `param($key, $value)` - query параметры url. +* `send($method, $body)` - отправка HTTP запроса. + +## Итерация результатов + +Перебор элементов коллекции, возвращаемой из API, является типичной задачей. Объекты коллекции содержатся в массиве `rows`. Таким образом, перебор полученного результата можно организовать следующим образом: + +```php +$products = $ms->query()->entity()->product()->limit(100)->get(); +foreach ($products->rows as $product) { + var_dump($product->name); +} +``` + +Однако, если нужно перебрать всю коллекцию, размер которой больше лимита, коллекцию придётся запрашивать несколько раз, изменяя параметр `offset`. Чтобы не организовывать такой перебор вручную, можно использовать метод `getGenerator()`. Он возвращает генератор, перебирающий массив `rows` с текущего `offset` и до последнего элемента (с отправкой новых запросов, если это необходимо). Пагинация осуществляется с шагом `limit`. + +```php +$generator = $ms->query()->entity()->product()->limit(100)->getGenerator(); +foreach ($generator as $product) { + var_dump($product->name); +} +``` + +## Отладка + +Метод `debug()` возвращает детали сформированного конструктором запроса без его реальной отправки. Просто разместите его перед любым `CRUD` методом, чтобы увидеть HTTP-метод, ссылку, заголовки и тело запроса. + +```php +$product = $ms + ->query() + ->entity() + ->product() + ->limit(1) + ->filter([ + ['archived', false], + ['name', '!=', 'tangerine'], + ]) + ->debug() + ->get(); +var_dump($product); +``` + +```bash +object(stdClass)#28 (5) { + ["method"]=> + string(3) "GET" + ["url"]=> + string(101) "https://online.moysklad.ru/api/remap/1.2/entity/product?limit=1&filter=archived=false;name!=tangerine" + ["url_encoded"]=> + string(109) "https://online.moysklad.ru/api/remap/1.2/entity/product?limit=1&filter=archived%3Dfalse%3Bname%21%3Dtangerine" + ["headers"]=> + object(stdClass)#29 (2) { + ["Content-Type"]=> + string(16) "application/json" + ["Authorization"]=> + string(38) "Basic ###############################" + } + ["body"]=> + array(0) { + } +} +``` + +| [<< Взаимодействие с API](/docs/api_interaction.md) | [Оглавление](/docs/index.md) | [Объектный подход (Record) >>](/docs/active_record.md) | +|:----------------------------------------------------|:----------------------------:|-------------------------------------------------------:| \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 00000000..a5af1ab5 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,43 @@ +# Настройка клиента + +Основной класс для взаимодействия с API - `Evgeek\Moysklad\MoySklad`. В минимальной конфигурации ему требуется только массив с `credentials`. + +```php +use Evgeek\Moysklad\MoySklad; +use Evgeek\Moysklad\Formatters\ArrayFormat; +use Evgeek\Moysklad\Http\GuzzleSenderFactory; + +//Минимум +$ms = new MoySklad(['token']); + +//С подробностями +$ms = new MoySklad( + credentials: ['login', 'password'], + formatter: new ArrayFormat(), + requestSenderFactory: new GuzzleSenderFactory( + retries: 3, + exceptionTruncateAt: 4000 + ) +); +``` + +### credentials + +Массив, содержащий либо [токен](https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api), либо логин и пароль. + +### formatter + +Объект, преобразующий json-строку ответа от API в нужный формат, и наоборот - передаваемый payload в json-строку. Должен реализовывать `\Evgeek\Moysklad\Formatters\JsonFormatterInterface`. Встроенные форматтеры - `StdClassFormat` (по умолчанию), `ArrayFormat`, `StringFormat` и `RecordFormat`. Все встроенные форматтеры могут принимать в качестве payload `stdClass`, `array`, `string` и `Record`. + +Подробности о работе форматтеров находятся в [соответствующем разделе](/docs/formatters.md) документации. + +### requestSenderFactory + +Фабрика, создающая объект для отправки HTTP запросов. Библиотека для этих целей использует [Guzzle](https://github.com/guzzle/guzzle). Фабрика внедряется через простой `PSR-7` совместимый интерфейс `Evgeek\Moysklad\Http\RequestSenderFactoryInterface`, поэтому не составит труда как просто настроить клиент Guzzle под свои предпочтения, так и реализовать собственный способ отправки. + +Библиотека содержит встроенную фабрику `GuzzleSenderFactory()`, принимающую следующие аргументы: +* `retries` - количество повторных попыток отправки запроса в случае неудачи. По умолчанию 0 (одна отправка, без повторных попыток). Задержка между повторами экспоненциальна. +* `exceptionTruncateAt` - максимальный размер сообщения об ошибке. Значение Guzzle по умолчанию - 120 символов, чего во многих ситуациях недостаточно. + +| - | [Оглавление](/docs/index.md) | [Взаимодействие с API >>](/docs/api_interaction.md) | +|:--|:----------------------------:|----------------------------------------------------:| \ No newline at end of file diff --git a/docs/tools.md b/docs/tools.md new file mode 100644 index 00000000..05c29bcf --- /dev/null +++ b/docs/tools.md @@ -0,0 +1,110 @@ +# Вспомогательные инструменты + +Дополнительные классы, помогающие взаимодействовать с Моим Складом. Располагаются в пространстве имён `Evgeek\Moysklad\Tools`. + +* [Meta](/docs/tools.md#meta) + * [Методы сущностей](/docs/formatters.md#методы-сущностей) + * [Форматирование](/docs/formatters.md#форматирование) + * [Альтернатива](/docs/formatters.md#альтернатива) +* [Guid](/docs/tools.md#guid) + +## Meta + +Формирование метадаты сущностей. Полезен для формирования тела запроса в [Query](/docs/query_builder.md). + +Хелпер можно использовать напрямую через статические методы класса, либо через метод `meta()` основного класса библиотеки. + +```php +$productMeta = Meta::product('757add1a-02c3-11ee-0a80-0bae005d263d'); +//Или +$productMeta = $ms->meta()->product('e028f279-02c4-11ee-0a80-0022005cc7f4'); + +$product = [ + 'meta' => $productMeta, + 'name' => 'lime', +]; +$ms->query()->entity()->product()->massCreateUpdate([$product]); +``` + +### Методы сущностей + +* Реализованные в библиотеке сущности вызываются при помощи одноимённых методов. + +```php +$employeeMeta = $ms->meta()->employee('f77457f1-a93d-11ed-0a80-0fba0011a6f6'); +$currencyMeta = Meta::currency('757add1a-02c3-11ee-0a80-0bae005d263d'); +``` + +* `create($path, $type)` позволяет создать метадату для ещё не реализованных сущностей. `$path` - массив сегментов пути из поля `href` меты, `$type` - строка с названием типа сущности из поля `type` оттуда же. + +```php +$serviceMeta = Meta::create(['entity', 'service'], 'service'); +var_dump($serviceMeta); + +//object(stdClass)#26 (3) { +//["href"]=> +// string(55) "https://online.moysklad.ru/api/remap/1.2/entity/service" +//["type"]=> +// string(7) "service" +//["mediaType"]=> +// string(16) "application/json" +//} +``` + +### Форматирование + +При использовании хелпера через `$ms->meta()`, результат возвращается в [формате](/docs/formatters.md), заданном в `$ms`. При использовани через статические методы, ответ возвращается в формате `stdClass`. Как правило, менять формат нет необходимости - однако, это можно сделать, передав желаемый форматтер в метод сущности. + +```php +$productMeta = Meta::product('3aba2611-c64f-11ed-0a80-108a00230a9c', new StringFormat()); +echo $productMeta; +//{"href":"https:\/\/online.moysklad.ru\/api\/remap\/1.2\/entity\/product\/3aba2611-c64f-11ed-0a80-108a00230a9c","type":"product","mediaType":"application\/json"} +``` + +### Альтернатива + +Объекты [Record](/docs/active_record.md) формируют свою мету самостоятельно, поэтому их использование может быть удобнее, чем формирование тела запроса вручную. + +```php +$product = [ + 'meta' => $ms->meta()->product('3aba2611-c64f-11ed-0a80-108a00230a9c'), + 'name' => 'orange', +]; + +//Или +$product = Product::make($ms, [ + 'id' => '3aba2611-c64f-11ed-0a80-108a00230a9c', + 'name' => 'orange', +]); +``` + + +## Guid + +Работа с id (guid/uuid) сущностей. + +* `extractAll($url)` - возвращает массив со всеми id, извлечёнными из переданной строки `$url`. +* `extractFirst($url)` - возвращает первый id из переданной строки. +* `extractLast($url)` - возвращает последний id из переданной строки. +* `check($id)` - возвращает `true`, если переданный `$id` является валидным guid, иначе `false`. + +```php +$url = 'https://online.moysklad.ru/api/remap/1.2/entity/customerorder/00001c03-5227-11e8-9ff4-315000132d57/positions/00002107-5227-11e8-9ff4-315000132d58'; +var_dump(Guid::extractAll($url)); +//array(2) { +// [0]=> +// string(36) "00001c03-5227-11e8-9ff4-315000132d57" +// [1]=> +// string(36) "00002107-5227-11e8-9ff4-315000132d58" +//} + +$firstId = Guid::extractFirst($url); +echo $firstId; +//00001c03-5227-11e8-9ff4-315000132d57 + +var_dump(Guid::check($firstId)); +//bool(true) +``` + +| [<< Форматтеры](/docs/formatters.md) | [Оглавление](/docs/index.md) | [Обработка исключений >>](/docs/exceptions.md) | +|:-------------------------------------|:----------------------------:|-----------------------------------------------:| \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5f854cd7..0f6759af 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,30 +1,27 @@ - - - - tests/Feature - - - tests/Unit - - - - - - src - - + + + + tests/Feature + + + tests/Unit + + + + + src + + diff --git a/src/Api/Query.php b/src/Api/Query.php deleted file mode 100644 index cb568659..00000000 --- a/src/Api/Query.php +++ /dev/null @@ -1,86 +0,0 @@ - - * $products = $ms->query() - * ->endpoint('entity') - * ->product() - * ->get(); - * - */ - public function endpoint(string $endpoint): EndpointCommon - { - return $this->resolveCommonBuilder(EndpointCommon::class, $endpoint); - } - - /** - * Entities and documents endpoint - * - * $products = $ms->query() - * ->entity() - * ->product() - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti - * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/ - */ - public function entity(): Entity - { - return $this->resolveNamedBuilder(Entity::class); - } - - /** - * Reports endpoint - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/reports/#otchety - */ - public function report(): Report - { - return $this->resolveNamedBuilder(Report::class); - } - - /** - * Audit endpoint - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/other/#audit - */ - public function audit(): Audit - { - return $this->resolveNamedBuilder(Audit::class); - } - - /** - * Notifications endpoint - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/other/#uwedomleniq - */ - public function notification(): Notification - { - return $this->resolveNamedBuilder(Notification::class); - } - - protected function makeCurrentPath(): array - { - return $this->prevPath; - } -} diff --git a/src/Api/AbstractBuilder.php b/src/Api/Query/AbstractBuilder.php similarity index 55% rename from src/Api/AbstractBuilder.php rename to src/Api/Query/AbstractBuilder.php index fa5c15cb..d0aeae9e 100644 --- a/src/Api/AbstractBuilder.php +++ b/src/Api/Query/AbstractBuilder.php @@ -2,19 +2,16 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api; +namespace Evgeek\Moysklad\Api\Query; -use Evgeek\Moysklad\Api\Segments\AbstractSegmentCommon; -use Evgeek\Moysklad\Api\Segments\AbstractSegmentNamed; -use Evgeek\Moysklad\Api\Traits\Actions\DebugTrait; +use Evgeek\Moysklad\Api\Query\Segments\AbstractSegmentCommon; +use Evgeek\Moysklad\Api\Query\Segments\AbstractSegmentNamed; +use Evgeek\Moysklad\Api\Query\Traits\Actions\DebugTrait; use Evgeek\Moysklad\Enums\HttpMethod; -use Evgeek\Moysklad\Enums\QueryParam; use Evgeek\Moysklad\Exceptions\RequestException; use Evgeek\Moysklad\Http\ApiClient; use Evgeek\Moysklad\Http\Payload; -use Evgeek\Moysklad\Services\Url; use Generator; -use InvalidArgumentException; abstract class AbstractBuilder { @@ -53,21 +50,6 @@ protected function apiGetGenerator(): Generator return $this->api->getGenerator($this->makePayload(HttpMethod::GET)); } - protected function getEnumHttpMethod(HttpMethod|string $method): HttpMethod - { - if (!is_string($method)) { - return $method; - } - - $method = strtoupper($method); - $enumMethod = HttpMethod::tryFrom($method); - if ($enumMethod === null) { - throw new InvalidArgumentException("'$method' is not valid HTTP method. Check " . HttpMethod::class); - } - - return $enumMethod; - } - protected function makePayload(HttpMethod $method, mixed $body = null): Payload { return new Payload( @@ -82,8 +64,6 @@ protected function makePayload(HttpMethod $method, mixed $body = null): Payload * @template T of AbstractSegmentCommon * * @param class-string $builderClass - * - * @return T */ protected function resolveCommonBuilder(string $builderClass, string $path): AbstractSegmentCommon { @@ -94,31 +74,9 @@ protected function resolveCommonBuilder(string $builderClass, string $path): Abs * @template T of AbstractSegmentNamed * * @param class-string $builderClass - * - * @return T */ protected function resolveNamedBuilder(string $builderClass): AbstractSegmentNamed { return new $builderClass($this->api, $this->path, $this->params); } - - protected function setQueryParam(QueryParam|string $queryParam, string|int|float|bool $value): void - { - $stringQueryParam = strtolower(is_string($queryParam) ? $queryParam : $queryParam->value); - $stringValue = Url::convertMixedValueToString($value); - - $separator = QueryParam::getSeparator($stringQueryParam); - if ($separator === '') { - $this->params[$stringQueryParam] = $stringValue; - - return; - } - - if (!array_key_exists($stringQueryParam, $this->params)) { - $this->params[$stringQueryParam] = ''; - } - $this->params[$stringQueryParam] .= $this->params[$stringQueryParam] === '' ? - $stringValue : - $separator . $stringValue; - } } diff --git a/src/Api/Debug.php b/src/Api/Query/Debug.php similarity index 65% rename from src/Api/Debug.php rename to src/Api/Query/Debug.php index 24db3d4a..42c8ba65 100644 --- a/src/Api/Debug.php +++ b/src/Api/Query/Debug.php @@ -2,15 +2,16 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api; +namespace Evgeek\Moysklad\Api\Query; -use Evgeek\Moysklad\Api\Segments\Special\MassDelete; +use Evgeek\Moysklad\Api\Query\Segments\Special\MassDeleteSegment; use Evgeek\Moysklad\Enums\HttpMethod; class Debug extends AbstractBuilder { /** - * Debug read request + * Возвращает отладочную информацию запроса на чтение. + * * * $debug = $ms->query * ->entity() @@ -25,7 +26,8 @@ public function get() } /** - * Debug create request + * Возвращает отладочную информацию запроса на создание. + * * * $debug = $ms->query * ->entity() @@ -40,7 +42,8 @@ public function create(mixed $body) } /** - * Debug update request + * Возвращает отладочную информацию запроса на изменение. + * * * $debug = $ms->query * ->entity() @@ -56,7 +59,8 @@ public function update(mixed $body) } /** - * Debug delete request + * Возвращает отладочную информацию запроса на удаление. + * * * $debug = $ms->query * ->entity() @@ -72,7 +76,8 @@ public function delete() } /** - * Debug Mass Delete request + * Возвращает отладочную информацию запроса на массовое удаление. + * * * $debug = $ms->query * ->entity() @@ -83,11 +88,12 @@ public function delete() */ public function massDelete(mixed $body) { - return (new MassDelete($this->api, $this->path, $this->params))->massDeleteDebug($body); + return (new MassDeleteSegment($this->api, $this->path, $this->params))->massDeleteDebug($body); } /** - * Debug general request + * Возвращает отладочную информацию произвольного запроса. + * * * $debug = $ms->query * ->entity() @@ -99,7 +105,7 @@ public function massDelete(mixed $body) */ public function send(HttpMethod|string $method, mixed $body = null) { - return $this->apiDebug($this->getEnumHttpMethod($method), $body); + return $this->apiDebug(HttpMethod::makeFrom($method), $body); } protected function makeCurrentPath(): array diff --git a/src/Api/Query/QueryBuilder.php b/src/Api/Query/QueryBuilder.php new file mode 100644 index 00000000..2ab46a9a --- /dev/null +++ b/src/Api/Query/QueryBuilder.php @@ -0,0 +1,114 @@ + + * $productUrl = "https://online.moysklad.ru/api/remap/1.2/entity/product/3aba2611-c64f-11ed-0a80-108a00230a9c?expand=image"; + * $product = $ms->query() + * ->fromUrl($productUrl, true) + * ->get(); + * + * + * @param mixed $withParams + */ + public function fromUrl(string $url, $withParams = false): MethodSegmentCommon + { + [$path, $params] = Url::parsePathAndParams($url); + $lastSegment = array_pop($path); + if (!$withParams) { + $params = []; + } + + return new MethodSegmentCommon($this->api, $path, $params, $lastSegment); + } + + /** + * Универсальный метод входных точек API + * + * + * $products = $ms->query() + * ->endpoint('entity') + * ->product() + * ->get(); + * + */ + public function endpoint(string $name): EndpointSegmentCommon + { + return $this->resolveCommonBuilder(EndpointSegmentCommon::class, $name); + } + + /** + * Входная точка для работы с Сущностями и Документами + * + * + * $products = $ms->query() + * ->entity() + * ->product() + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti + * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/ + */ + public function entity(): EntitySegment + { + return $this->resolveNamedBuilder(EntitySegment::class); + } + + /** + * Входная точка для работы с Отчётами + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/reports/#otchety + */ + public function report(): ReportSegment + { + return $this->resolveNamedBuilder(ReportSegment::class); + } + + /** + * Входная точка для работы с Аудитом + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/other/#audit + */ + public function audit(): AuditSegment + { + return $this->resolveNamedBuilder(AuditSegment::class); + } + + /** + * Входная точка для работы с Уведомлениями + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/other/#uwedomleniq + */ + public function notification(): NotificationSegment + { + return $this->resolveNamedBuilder(NotificationSegment::class); + } + + protected function makeCurrentPath(): array + { + return $this->prevPath; + } +} diff --git a/src/Api/Segments/AbstractSegmentCommon.php b/src/Api/Query/Segments/AbstractSegmentCommon.php similarity index 86% rename from src/Api/Segments/AbstractSegmentCommon.php rename to src/Api/Query/Segments/AbstractSegmentCommon.php index 2ce44ebc..36bdc25b 100644 --- a/src/Api/Segments/AbstractSegmentCommon.php +++ b/src/Api/Query/Segments/AbstractSegmentCommon.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Segments; +namespace Evgeek\Moysklad\Api\Query\Segments; -use Evgeek\Moysklad\Api\AbstractBuilder; +use Evgeek\Moysklad\Api\Query\AbstractBuilder; use Evgeek\Moysklad\Http\ApiClient; use InvalidArgumentException; diff --git a/src/Api/Segments/AbstractSegmentNamed.php b/src/Api/Query/Segments/AbstractSegmentNamed.php similarity index 81% rename from src/Api/Segments/AbstractSegmentNamed.php rename to src/Api/Query/Segments/AbstractSegmentNamed.php index 9f4d2ac1..54a0b719 100644 --- a/src/Api/Segments/AbstractSegmentNamed.php +++ b/src/Api/Query/Segments/AbstractSegmentNamed.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Segments; +namespace Evgeek\Moysklad\Api\Query\Segments; -use Evgeek\Moysklad\Api\AbstractBuilder; +use Evgeek\Moysklad\Api\Query\AbstractBuilder; use InvalidArgumentException; abstract class AbstractSegmentNamed extends AbstractBuilder diff --git a/src/Api/Query/Segments/ById/AbstractByIdSegment.php b/src/Api/Query/Segments/ById/AbstractByIdSegment.php new file mode 100644 index 00000000..0a25260f --- /dev/null +++ b/src/Api/Query/Segments/ById/AbstractByIdSegment.php @@ -0,0 +1,21 @@ + + * $products = $ms->query() + * ->entity() + * ->product() + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-towar + */ + public function product(): ProductSegment + { + return $this->resolveNamedBuilder(ProductSegment::class); + } + + /** + * Заказ покупателя + * + * + * $customerOrders = $ms->query() + * ->entity() + * ->customerorder() + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/#dokumenty-zakaz-pokupatelq + */ + public function customerorder(): CustomerorderSegment + { + return $this->resolveNamedBuilder(CustomerorderSegment::class); + } + + /** + * Ассортимент + * + * + * $assortments = $ms->query() + * ->entity() + * ->assortment() + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-assortiment + */ + public function assortment(): AssortmentSegment + { + return $this->resolveNamedBuilder(AssortmentSegment::class); + } + + /** + * Сотрудник + * + * + * $assortments = $ms->query() + * ->entity() + * ->employee() + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-sotrudnik + */ + public function employee(): EmployeeSegment + { + return $this->resolveNamedBuilder(EmployeeSegment::class); + } +} diff --git a/src/Api/Query/Segments/Endpoints/NotificationSegment.php b/src/Api/Query/Segments/Endpoints/NotificationSegment.php new file mode 100644 index 00000000..f24dda2a --- /dev/null +++ b/src/Api/Query/Segments/Endpoints/NotificationSegment.php @@ -0,0 +1,23 @@ +apiSend(HttpMethod::POST, $objects); + } + + public function massDeleteDebug(mixed $objects) + { + return $this->apiDebug(HttpMethod::POST, $objects); + } +} diff --git a/src/Api/Traits/Actions/CreateTrait.php b/src/Api/Query/Traits/Actions/CreateTrait.php similarity index 77% rename from src/Api/Traits/Actions/CreateTrait.php rename to src/Api/Query/Traits/Actions/CreateTrait.php index a035aac4..cb7045a6 100644 --- a/src/Api/Traits/Actions/CreateTrait.php +++ b/src/Api/Query/Traits/Actions/CreateTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Actions; +namespace Evgeek\Moysklad\Api\Query\Traits\Actions; use Evgeek\Moysklad\Enums\HttpMethod; use Evgeek\Moysklad\Exceptions\RequestException; @@ -10,7 +10,8 @@ trait CreateTrait { /** - * Create entity (POST http request) + * Создание сущности в Моём Складе. + * * * $product = $ms->query() * ->entity() diff --git a/src/Api/Traits/Actions/DebugTrait.php b/src/Api/Query/Traits/Actions/DebugTrait.php similarity index 51% rename from src/Api/Traits/Actions/DebugTrait.php rename to src/Api/Query/Traits/Actions/DebugTrait.php index 0c72c946..6d9d077a 100644 --- a/src/Api/Traits/Actions/DebugTrait.php +++ b/src/Api/Query/Traits/Actions/DebugTrait.php @@ -2,14 +2,16 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Actions; +namespace Evgeek\Moysklad\Api\Query\Traits\Actions; -use Evgeek\Moysklad\Api\Debug; +use Evgeek\Moysklad\Api\Query\Debug; trait DebugTrait { /** - * Set it before the CRUD method to generate debug information for the request + * Отладочный метод. + * Разместите его перед любым CRUD-методом, и он вернёт детальную информацию о запросе, не выполняя сам запрос. + * * * $products = $ms->query() * ->entity() diff --git a/src/Api/Traits/Actions/DeleteTrait.php b/src/Api/Query/Traits/Actions/DeleteTrait.php similarity index 81% rename from src/Api/Traits/Actions/DeleteTrait.php rename to src/Api/Query/Traits/Actions/DeleteTrait.php index 8e1768c3..a4b25b58 100644 --- a/src/Api/Traits/Actions/DeleteTrait.php +++ b/src/Api/Query/Traits/Actions/DeleteTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Actions; +namespace Evgeek\Moysklad\Api\Query\Traits\Actions; use Evgeek\Moysklad\Enums\HttpMethod; use Evgeek\Moysklad\Exceptions\RequestException; @@ -10,7 +10,8 @@ trait DeleteTrait { /** - * Delete entity (DELETE http request) + * Удаление сущности. + * * * $ms->query() * ->entity() diff --git a/src/Api/Traits/Actions/GetGeneratorTrait.php b/src/Api/Query/Traits/Actions/GetGeneratorTrait.php similarity index 57% rename from src/Api/Traits/Actions/GetGeneratorTrait.php rename to src/Api/Query/Traits/Actions/GetGeneratorTrait.php index 38e95b32..0a0a4d39 100644 --- a/src/Api/Traits/Actions/GetGeneratorTrait.php +++ b/src/Api/Query/Traits/Actions/GetGeneratorTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Actions; +namespace Evgeek\Moysklad\Api\Query\Traits\Actions; use Evgeek\Moysklad\Exceptions\RequestException; use Generator; @@ -10,7 +10,9 @@ trait GetGeneratorTrait { /** - * Create generator from request (only for iterable entities: with rows array and meta->limit/meta->offset fields) + * Создать генератор из запроса итерируемой сущности. + * Генератор можно перебирать в цикле, при этом подгружать новые страницы он будет самостоятельно. + * * * $generator = $ms->query() * ->entity() diff --git a/src/Api/Traits/Actions/GetTrait.php b/src/Api/Query/Traits/Actions/GetTrait.php similarity index 70% rename from src/Api/Traits/Actions/GetTrait.php rename to src/Api/Query/Traits/Actions/GetTrait.php index 541b2d56..cb278c66 100644 --- a/src/Api/Traits/Actions/GetTrait.php +++ b/src/Api/Query/Traits/Actions/GetTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Actions; +namespace Evgeek\Moysklad\Api\Query\Traits\Actions; use Evgeek\Moysklad\Enums\HttpMethod; use Evgeek\Moysklad\Exceptions\RequestException; @@ -10,7 +10,8 @@ trait GetTrait { /** - * Read single entity or list (GET http request) + * Получить одиночную сущность или коллекцию сущностей. + * * * $products = $ms->query() * ->entity() diff --git a/src/Api/Query/Traits/Actions/MassCreateUpdateTrait.php b/src/Api/Query/Traits/Actions/MassCreateUpdateTrait.php new file mode 100644 index 00000000..e80419a6 --- /dev/null +++ b/src/Api/Query/Traits/Actions/MassCreateUpdateTrait.php @@ -0,0 +1,43 @@ + + * $product1 = ['name' => 'Корнишоны']; + * $product2 = Product::make($ms, [ + * 'id' => 'efcddaff-f308-11ed-0a80-09ee0084c2c6', + * 'name' => 'Кабачки', + * ]); + * $product3 = [ + * 'meta' => Meta::product('1a4d67b8-f309-11ed-0a80-086800825780'), + * 'name' => 'Патиссоны', + * ]; + * + * $products = $ms->query() + * ->entity() + * ->product() + * ->massCreateUpdate([$product1, $product2, $product3]); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sozdanie-i-obnowlenie-neskol-kih-ob-ektow + * + * @throws RequestException + */ + public function massCreateUpdate(mixed $objects) + { + $objects = CollectionHelper::extractRows($objects); + + return $this->apiSend(HttpMethod::POST, $objects); + } +} diff --git a/src/Api/Query/Traits/Actions/MassDeleteTrait.php b/src/Api/Query/Traits/Actions/MassDeleteTrait.php new file mode 100644 index 00000000..a3a68a2a --- /dev/null +++ b/src/Api/Query/Traits/Actions/MassDeleteTrait.php @@ -0,0 +1,35 @@ + + * $product1 = $ms->query()->entity()->product()->byId('cc181c35-f259-11ed-0a80-00e900658c8f')->get(); + * $product2 = Product::make($ms, ['id' => 'd540c409-f259-11ed-0a80-00e900658e53']); + * $product3 = ['meta' => Meta::product('d540c409-f259-11ed-0a80-00e900658e53')]; + * + * $products = $ms->query() + * ->entity() + * ->customerorder() + * ->massDelete([$product1, $product2, $product3]); + * + * + * @throws RequestException + */ + public function massDelete(mixed $objects) + { + $objects = CollectionHelper::extractRows($objects); + + return (new MassDeleteSegment($this->api, $this->path, $this->params))->massDelete($objects); + } +} diff --git a/src/Api/Traits/Actions/SendTrait.php b/src/Api/Query/Traits/Actions/SendTrait.php similarity index 64% rename from src/Api/Traits/Actions/SendTrait.php rename to src/Api/Query/Traits/Actions/SendTrait.php index 0f526ebc..a6209241 100644 --- a/src/Api/Traits/Actions/SendTrait.php +++ b/src/Api/Query/Traits/Actions/SendTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Actions; +namespace Evgeek\Moysklad\Api\Query\Traits\Actions; use Evgeek\Moysklad\Enums\HttpMethod; use Evgeek\Moysklad\Exceptions\RequestException; @@ -10,7 +10,8 @@ trait SendTrait { /** - * Generic HTTP request + * Универсальный метод, позволяющий отправлять произвольный HTTP-запрос. + * * * $product = $ms->query() * ->entity() @@ -23,6 +24,6 @@ trait SendTrait */ public function send(HttpMethod|string $method, mixed $body = null) { - return $this->apiSend($this->getEnumHttpMethod($method), $body); + return $this->apiSend(HttpMethod::makeFrom($method), $body); } } diff --git a/src/Api/Traits/Actions/UpdateTrait.php b/src/Api/Query/Traits/Actions/UpdateTrait.php similarity index 82% rename from src/Api/Traits/Actions/UpdateTrait.php rename to src/Api/Query/Traits/Actions/UpdateTrait.php index f2bfdf65..afeddf73 100644 --- a/src/Api/Traits/Actions/UpdateTrait.php +++ b/src/Api/Query/Traits/Actions/UpdateTrait.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Actions; +namespace Evgeek\Moysklad\Api\Query\Traits\Actions; use Evgeek\Moysklad\Enums\HttpMethod; use Evgeek\Moysklad\Exceptions\RequestException; @@ -10,7 +10,8 @@ trait UpdateTrait { /** - * Update entity (PUT http request) + * Обновление сущности. + * * * $product = $ms->query() * ->entity() diff --git a/src/Api/Query/Traits/Params/ExpandTrait.php b/src/Api/Query/Traits/Params/ExpandTrait.php new file mode 100644 index 00000000..5b8c4884 --- /dev/null +++ b/src/Api/Query/Traits/Params/ExpandTrait.php @@ -0,0 +1,35 @@ + + * $products = $ms->query() + * ->entity() + * ->product() + * ->limit(100) + * ->expand('owner') + * ->expand('minPrice.currency') + * ->expand(['group', 'images']); + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand + */ + public function expand(array|string $field): static + { + $this->params = QueryParams::setExpand($this->params, $field); + + return $this; + } +} diff --git a/src/Api/Query/Traits/Params/FilterTrait.php b/src/Api/Query/Traits/Params/FilterTrait.php new file mode 100644 index 00000000..9309d74f --- /dev/null +++ b/src/Api/Query/Traits/Params/FilterTrait.php @@ -0,0 +1,41 @@ + + * $products = $ms->query()->entity()->product() + * ->filter('archived', false) + * ->filter('name', '=~', 'apple') + * ->filter([ + * ['minimumBalance', '=', '0'], + * ['code', FilterSign::NEQ, 123], + * ]) + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter + */ + public function filter( + array|string $key, + FilterSign|string|int|float|bool $sign = null, + string|int|float|bool $value = null + ): static { + $this->params = QueryParams::setFilter($this->params, $key, $sign, $value); + + return $this; + } +} diff --git a/src/Api/Query/Traits/Params/LimitOffsetTrait.php b/src/Api/Query/Traits/Params/LimitOffsetTrait.php new file mode 100644 index 00000000..2995d3b5 --- /dev/null +++ b/src/Api/Query/Traits/Params/LimitOffsetTrait.php @@ -0,0 +1,46 @@ + + * $products = $ms->query() + * ->entity() + * ->products() + * ->limit(100) + * ->get(); + * + */ + public function limit(int $amount): static + { + $this->params = QueryParams::setLimit($this->params, $amount); + + return $this; + } + + /** + * Смещение начала выборки, используется для пагинации. + * + * + * $products = $ms->query() + * ->entity() + * ->product() + * ->offset(100) + * ->get(); + * + */ + public function offset(int $amount): static + { + $this->params = QueryParams::setOffset($this->params, $amount); + + return $this; + } +} diff --git a/src/Api/Query/Traits/Params/OrderTrait.php b/src/Api/Query/Traits/Params/OrderTrait.php new file mode 100644 index 00000000..848a3f4d --- /dev/null +++ b/src/Api/Query/Traits/Params/OrderTrait.php @@ -0,0 +1,36 @@ + + * $products = $ms->query() + * ->entity() + * ->product() + * ->order('updated', 'asc') + * ->order([ + * ['code', 'desc'], + * ['name'], + * ]) + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sortirowka-ob-ektow + */ + public function order(array|string $field, OrderDirection|string $direction = 'asc'): static + { + $this->params = QueryParams::setOrder($this->params, $field, $direction); + + return $this; + } +} diff --git a/src/Api/Query/Traits/Params/ParamTrait.php b/src/Api/Query/Traits/Params/ParamTrait.php new file mode 100644 index 00000000..597fe979 --- /dev/null +++ b/src/Api/Query/Traits/Params/ParamTrait.php @@ -0,0 +1,33 @@ + + * $products = $ms->query() + * ->entity() + * ->product() + * ->param('limit', 10) + * ->param([ + * ['offset', '20'], + * ['search', 'orange'], + * ]) + * ->get(); + * + */ + public function param(array|string $key, string|int|float|bool $value = null): static + { + $this->params = QueryParams::setParam($this->params, $key, $value); + + return $this; + } +} diff --git a/src/Api/Traits/Params/SearchTrait.php b/src/Api/Query/Traits/Params/SearchTrait.php similarity index 61% rename from src/Api/Traits/Params/SearchTrait.php rename to src/Api/Query/Traits/Params/SearchTrait.php index fadce6dd..27f04648 100644 --- a/src/Api/Traits/Params/SearchTrait.php +++ b/src/Api/Query/Traits/Params/SearchTrait.php @@ -2,16 +2,17 @@ declare(strict_types=1); -namespace Evgeek\Moysklad\Api\Traits\Params; +namespace Evgeek\Moysklad\Api\Query\Traits\Params; -use Evgeek\Moysklad\Enums\QueryParam; +use Evgeek\Moysklad\Services\QueryParams; trait SearchTrait { /** - * Context search + * Контекстный поиск. + * * - * $product = $ms->query() + * $products = $ms->query() * ->entity() * ->product() * ->search('orange') @@ -22,7 +23,7 @@ trait SearchTrait */ public function search(string $text): static { - $this->setQueryParam(QueryParam::SEARCH, $text); + $this->params = QueryParams::setSearch($this->params, $text); return $this; } diff --git a/src/Api/Query/Traits/Segments/AttributesTrait.php b/src/Api/Query/Traits/Segments/AttributesTrait.php new file mode 100644 index 00000000..bf660338 --- /dev/null +++ b/src/Api/Query/Traits/Segments/AttributesTrait.php @@ -0,0 +1,27 @@ + + * $product = $ms->query() + * ->entity() + * ->product() + * ->metadata() + * ->attributes() + * ->get(); + * + */ + public function attributes(): AttributesSegment + { + return $this->resolveNamedBuilder(AttributesSegment::class); + } +} diff --git a/src/Api/Query/Traits/Segments/ByIdCommonTrait.php b/src/Api/Query/Traits/Segments/ByIdCommonTrait.php new file mode 100644 index 00000000..2d7e00e0 --- /dev/null +++ b/src/Api/Query/Traits/Segments/ByIdCommonTrait.php @@ -0,0 +1,26 @@ + + * $product = $ms->query() + * ->entity() + * ->product() + * ->byId('fb72fc83-7ef5-11e3-ad1c-002590a28eca') + * ->get(); + * + */ + public function byId(string $guid): ByIdSegmentCommon + { + return $this->resolveCommonBuilder(ByIdSegmentCommon::class, $guid); + } +} diff --git a/src/Api/Query/Traits/Segments/ByIdPositionedTrait.php b/src/Api/Query/Traits/Segments/ByIdPositionedTrait.php new file mode 100644 index 00000000..eb2d0386 --- /dev/null +++ b/src/Api/Query/Traits/Segments/ByIdPositionedTrait.php @@ -0,0 +1,28 @@ + + * $order = $ms->query() + * ->entity() + * ->customerorder() + * ->byId('efe3944b-980d-11ec-0a80-0d180027c266') + * ->positions() + * ->byId('fb72fc83-7ef5-11e3-ad1c-002590a28eca') + * ->get(); + * + */ + public function byId(string $guid): ByIdSegmentPositioned + { + return $this->resolveCommonBuilder(ByIdSegmentPositioned::class, $guid); + } +} diff --git a/src/Api/Query/Traits/Segments/MetadataTrait.php b/src/Api/Query/Traits/Segments/MetadataTrait.php new file mode 100644 index 00000000..de8e8dc3 --- /dev/null +++ b/src/Api/Query/Traits/Segments/MetadataTrait.php @@ -0,0 +1,28 @@ + + * $product = $ms->query() + * ->entity() + * ->product() + * ->metadata() + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-metadannye + */ + public function metadata(): MetadataSegment + { + return $this->resolveNamedBuilder(MetadataSegment::class); + } +} diff --git a/src/Api/Query/Traits/Segments/MethodCommonTrait.php b/src/Api/Query/Traits/Segments/MethodCommonTrait.php new file mode 100644 index 00000000..c2566d47 --- /dev/null +++ b/src/Api/Query/Traits/Segments/MethodCommonTrait.php @@ -0,0 +1,25 @@ + + * $product = $ms->query() + * ->entity() + * ->method('product') + * ->get(); + * + */ + public function method(string $name): MethodSegmentCommon + { + return $this->resolveCommonBuilder(MethodSegmentCommon::class, $name); + } +} diff --git a/src/Api/Query/Traits/Segments/PositionsTrait.php b/src/Api/Query/Traits/Segments/PositionsTrait.php new file mode 100644 index 00000000..28ede87c --- /dev/null +++ b/src/Api/Query/Traits/Segments/PositionsTrait.php @@ -0,0 +1,27 @@ + + * $order = $ms->query() + * ->entity() + * ->customerorder() + * ->byId('efe3944b-980d-11ec-0a80-0d180027c266') + * ->positions() + * ->get(); + * + */ + public function positions(): PositionsSegment + { + return $this->resolveNamedBuilder(PositionsSegment::class); + } +} diff --git a/src/Api/Query/Traits/Segments/SettingsTrait.php b/src/Api/Query/Traits/Segments/SettingsTrait.php new file mode 100644 index 00000000..1a8c0e82 --- /dev/null +++ b/src/Api/Query/Traits/Segments/SettingsTrait.php @@ -0,0 +1,26 @@ + + * $product = $ms->query() + * ->entity() + * ->assortment() + * ->settings() + * ->get(); + * + */ + public function settings(): SettingsSegment + { + return $this->resolveNamedBuilder(SettingsSegment::class); + } +} diff --git a/src/Api/Record/AbstractConcreteRecord.php b/src/Api/Record/AbstractConcreteRecord.php new file mode 100644 index 00000000..330a8d60 --- /dev/null +++ b/src/Api/Record/AbstractConcreteRecord.php @@ -0,0 +1,18 @@ +hydrate($content); + } + + public function __get(string $name) + { + return $this->contentContainer[$name] ?? null; + } + + public function __set(string $name, mixed $value) + { + if ( + !is_array($value) + && !(is_a($value, stdClass::class) && !is_a($value, self::class)) + && !(is_string($value) && AbstractMultiDecoder::checkStringIsValidJson($value)) + ) { + $this->contentContainer[$name] = $value; + + return; + } + + $decoded = $this->ms->getFormatter()->decode($value); + $encoded = $this->getRecordFormatter()->encode($decoded); + + $this->contentContainer[$name] = $encoded; + } + + public function __isset(string $name) + { + return array_key_exists($name, $this->contentContainer); + } + + public function __unset(string $name) + { + unset($this->contentContainer[$name]); + } + + /** + * Возвращает текущий объект преобразованным в строку + */ + public function toString(): string + { + return (new ArrayFormat())->decode($this->toArray()); + } + + /** + * Возвращает текущий объект преобразованным в массив. + */ + public function toArray(): array + { + return AbstractMultiDecoder::toArray($this->contentContainer); + } + + /** + * Возвращает текущий объект преобразованным в stdClass. + * + * @return array|stdClass + */ + public function toStdClass(): array|stdClass + { + return (new StdClassFormat())->encode($this->toString()); + } + + protected function hydrate(mixed $content): void + { + $this->contentContainer = []; + + $this->hydrateAdd($content); + } + + protected function hydrateAdd(mixed $content): void + { + $formatter = $this->ms->getFormatter(); + $arrayContent = (new ArrayFormat())->encode($formatter->decode($content)); + + foreach ($arrayContent as $key => $value) { + $this->{$key} = $value; + } + } + + protected function getRecordFormatter(): RecordFormat + { + $formatter = $this->ms->getFormatter(); + + return is_a($formatter, RecordFormat::class) ? + $formatter : + (new RecordFormat())->setMoySklad($this->ms); + } +} diff --git a/src/Api/Record/AbstractUnknownRecord.php b/src/Api/Record/AbstractUnknownRecord.php new file mode 100644 index 00000000..bf21c0b0 --- /dev/null +++ b/src/Api/Record/AbstractUnknownRecord.php @@ -0,0 +1,24 @@ +type) { + throw new InvalidArgumentException('path and type cannot be empty'); + } + + parent::__construct($ms, $content); + + $this->fillMeta($path); + } + + abstract protected function fillMeta(array $path): void; +} diff --git a/src/Api/Record/AutocompleteHelpers/Alcoholic.php b/src/Api/Record/AutocompleteHelpers/Alcoholic.php new file mode 100644 index 00000000..77afee76 --- /dev/null +++ b/src/Api/Record/AutocompleteHelpers/Alcoholic.php @@ -0,0 +1,21 @@ +ms, $type, $content); + } + + protected function resolveCollection(string $type): AbstractConcreteCollection + { + return RecordMappingHelper::resolveCollection($this->ms, $type); + } +} diff --git a/src/Api/Record/Builders/CollectionBuilder.php b/src/Api/Record/Builders/CollectionBuilder.php new file mode 100644 index 00000000..e7fa91ee --- /dev/null +++ b/src/Api/Record/Builders/CollectionBuilder.php @@ -0,0 +1,77 @@ +resolveCollection(Entity::PRODUCT); + } + + /** + * Создаёт коллекцию Сотрудников. + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-sotrudnik + * + * @return EmployeeCollection + */ + public function employee(): AbstractConcreteCollection + { + return $this->resolveCollection(Entity::EMPLOYEE); + } + + /** + * Создаёт коллекцию Ассортиментов (Товары, Услуги, Комплекты, Серии и Модификаци). + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-assortiment + * + * @return AssortmentCollection + */ + public function assortment(): AbstractConcreteCollection + { + return $this->resolveCollection(Entity::ASSORTMENT); + } + + /** + * Создаёт коллекцию Заков покупателя. + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/#dokumenty-zakaz-pokupatelq + * + * @return CustomerorderCollection + */ + public function customerorder(): AbstractConcreteCollection + { + return $this->resolveCollection(Document::CUSTOMERORDER); + } + + /** + * Создаёт неизвестную коллекцию. Используется для не реализованных в библиотеке коллекций. + * + * + * $productCollection = $ms->object()->collection()->unknown(['entity', 'product'], 'product'); + * + */ + public function unknown(array $path, string $type): UnknownCollection + { + return new UnknownCollection($this->ms, $path, $type); + } +} diff --git a/src/Api/Record/Builders/ObjectBuilder.php b/src/Api/Record/Builders/ObjectBuilder.php new file mode 100644 index 00000000..763446e2 --- /dev/null +++ b/src/Api/Record/Builders/ObjectBuilder.php @@ -0,0 +1,66 @@ +resolveObject(Entity::PRODUCT, $content); + } + + /** + * Создаёт сущность Сотрудник. + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-sotrudnik + * + * @return Employee + */ + public function employee(mixed $content = []): AbstractConcreteObject + { + return $this->resolveObject(Entity::EMPLOYEE, $content); + } + + /** + * Создаёт сущность Заказ покупателя. + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/#dokumenty-zakaz-pokupatelq + * + * @return Customerorder + */ + public function customerorder(mixed $content = []): AbstractConcreteObject + { + return $this->resolveObject(Document::CUSTOMERORDER, $content); + } + + /** + * Создаёт неизвестную сущность. Используется для не реализованных в библиотеке сущностей. + * + * + * $product = $ms->object() + * ->single() + * ->unknown(['entity', 'product', '1958e4df-f7ca-11ed-0a80-030500578f19'], 'product'); + * + */ + public function unknown(array $path, string $type, mixed $content = []): UnknownObject + { + return new UnknownObject($this->ms, $path, $type, $content); + } +} diff --git a/src/Api/Record/Builders/RecordBuilder.php b/src/Api/Record/Builders/RecordBuilder.php new file mode 100644 index 00000000..fc858ff3 --- /dev/null +++ b/src/Api/Record/Builders/RecordBuilder.php @@ -0,0 +1,24 @@ +ms); + } + + /** + * Конструктр коллекций. + */ + public function collection(): CollectionBuilder + { + return new CollectionBuilder($this->ms); + } +} diff --git a/src/Api/Record/Collections/AbstractConcreteCollection.php b/src/Api/Record/Collections/AbstractConcreteCollection.php new file mode 100644 index 00000000..c0f2cc9a --- /dev/null +++ b/src/Api/Record/Collections/AbstractConcreteCollection.php @@ -0,0 +1,31 @@ + + */ +class CustomerorderCollection extends AbstractConcreteCollection +{ + public const PATH = [ + Endpoint::ENTITY, + Document::CUSTOMERORDER, + ]; + public const TYPE = Document::CUSTOMERORDER; +} diff --git a/src/Api/Record/Collections/Entities/AssortmentCollection.php b/src/Api/Record/Collections/Entities/AssortmentCollection.php new file mode 100644 index 00000000..e0d80019 --- /dev/null +++ b/src/Api/Record/Collections/Entities/AssortmentCollection.php @@ -0,0 +1,28 @@ + + * @implements AbstractConcreteCollection + */ +class AssortmentCollection extends AbstractConcreteCollection +{ + public const PATH = [ + Endpoint::ENTITY, + Entity::ASSORTMENT, + ]; + public const TYPE = Entity::ASSORTMENT; +} diff --git a/src/Api/Record/Collections/Entities/EmployeeCollection.php b/src/Api/Record/Collections/Entities/EmployeeCollection.php new file mode 100644 index 00000000..7d487d2c --- /dev/null +++ b/src/Api/Record/Collections/Entities/EmployeeCollection.php @@ -0,0 +1,26 @@ + + */ +class EmployeeCollection extends AbstractConcreteCollection +{ + public const PATH = [ + Endpoint::ENTITY, + Entity::EMPLOYEE, + ]; + public const TYPE = Entity::EMPLOYEE; +} diff --git a/src/Api/Record/Collections/Entities/ProductCollection.php b/src/Api/Record/Collections/Entities/ProductCollection.php new file mode 100644 index 00000000..ac302133 --- /dev/null +++ b/src/Api/Record/Collections/Entities/ProductCollection.php @@ -0,0 +1,26 @@ + + */ +class ProductCollection extends AbstractConcreteCollection +{ + public const PATH = [ + Endpoint::ENTITY, + Entity::PRODUCT, + ]; + public const TYPE = Entity::PRODUCT; +} diff --git a/src/Api/Record/Collections/Traits/CrudCollectionTrait.php b/src/Api/Record/Collections/Traits/CrudCollectionTrait.php new file mode 100644 index 00000000..b84c25f2 --- /dev/null +++ b/src/Api/Record/Collections/Traits/CrudCollectionTrait.php @@ -0,0 +1,191 @@ + + * $products = Product::collection($ms)->get(); + * + * + * @throws RequestException + */ + public function get(): static + { + return $this->send(HttpMethod::GET); + } + + /** + * Загрузка следующей страницы коллекции. Если страницы не существует, вернёт null. + * + * + * $products = Product::collection($ms)->get(); + * $productNext = $product->getNext(); + * + * + * @throws RequestException + */ + public function getNext(): ?static + { + $nextHref = $this->meta->nextHref ?? null; + + if (!$nextHref) { + return null; + } + + $this->meta->href = $nextHref; + + return $this->send(HttpMethod::GET); + } + + /** + * Загрузка предыдущей страницы коллекции. Если страницы не существует, вернёт null. + * + * + * $products = Product::collection($ms)->get(); + * $productPrevious = $product->getPrevious(); + * + * + * @throws RequestException + */ + public function getPrevious(): ?static + { + $previousHref = $this->meta->previousHref ?? null; + + if (!$previousHref) { + return null; + } + + $this->meta->href = $previousHref; + + return $this->send(HttpMethod::GET); + } + + /** + * Массовое удаление сущностей. + * + * + * $product1 = $ms->query()->entity()->product()->byId('cc181c35-f259-11ed-0a80-00e900658c8f')->get(); + * $product2 = Product::make($ms, ['id' => 'd540c409-f259-11ed-0a80-00e900658e53']); + * $product3 = ['meta' => Meta::product('d540c409-f259-11ed-0a80-00e900658e53')]; + * + * Product::collection($ms)->massDelete([$product1, $product2, $product3]); + * + * //Или + * $oranges = Product::collection($ms)->search('orange')->get(); + * Product::collection($ms)->massDelete($oranges); + * + * + * @throws RequestException + */ + public function massDelete(mixed $objects): static + { + $objects = CollectionHelper::extractRows($objects); + + return $this->sendAndWrapResponse(HttpMethod::POST, $objects, 'delete'); + } + + /** + * Массовое изменение и/или удаление сущностей. Сущности с id будут обновлены, без - созданы. + * + * + * $product1 = ['name' => 'Корнишоны']; + * $product2 = Product::make($ms, [ + * 'id' => 'efcddaff-f308-11ed-0a80-09ee0084c2c6', + * 'name' => 'Кабачки', + * ]); + * $product3 = [ + * 'meta' => Meta::product('1a4d67b8-f309-11ed-0a80-086800825780'), + * 'name' => 'Патиссоны', + * ]; + * + * $products = Product::collection($ms) + * ->massCreateUpdate([$product1, $product2, $product3]); + * + * //Или + * $updatableProducts = Product::collection($ms)->get(); + * $updatableProducts->each(function (Product $product) { + * $product->name = mb_strtoupper($product->name); + * }); + * $products = Product::collection($ms)->massCreateUpdate($updatableProducts); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sozdanie-i-obnowlenie-neskol-kih-ob-ektow + * + * @throws RequestException + */ + public function massCreateUpdate(mixed $objects): static + { + $objects = CollectionHelper::extractRows($objects); + + return $this->sendAndWrapResponse(HttpMethod::POST, $objects); + } + + /** + * @throws RequestException + */ + protected function send(HttpMethod $method, mixed $body = []): static + { + $payload = $this->makePayload($method, $body); + + $response = $this->ms->getApiClient()->send($payload); + if (!RecordHelper::isCollection($this->ms, $response)) { + throw new InvalidArgumentException('Response must be a collection, object received'); + } + + $this->hydrate($response); + + return $this; + } + + /** + * @throws RequestException + */ + protected function sendAndWrapResponse(HttpMethod $method, mixed $body, ?string $additionalSegment = null): static + { + $response = []; + $meta = $this->meta ?? null; + $context = $this->context ?? null; + if ($meta) { + $response['meta'] = $meta; + } + if ($context) { + $response['context'] = $context; + } + + $payload = $this->makePayload($method, $body, $additionalSegment); + $response['rows'] = $this->ms->getApiClient()->send($payload); + + $this->hydrate($response); + + return $this; + } + + protected function makePayload(HttpMethod $method, mixed $body, ?string $additionalSegment = null): Payload + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $path = $additionalSegment ? + [...$path, $additionalSegment] : + $path; + + return new Payload( + method: $method, + path: $path, + params: $params, + body: $body, + ); + } +} diff --git a/src/Api/Record/Collections/Traits/FillMetaCollectionTrait.php b/src/Api/Record/Collections/Traits/FillMetaCollectionTrait.php new file mode 100644 index 00000000..958b69bd --- /dev/null +++ b/src/Api/Record/Collections/Traits/FillMetaCollectionTrait.php @@ -0,0 +1,22 @@ +meta) { + return; + } + + $meta = $this->ms->meta()->create($path, $this->type); + $formatter = $this->ms->getFormatter(); + + $this->meta = (new StdClassFormat())->encode($formatter->decode($meta)); + } +} diff --git a/src/Api/Record/Collections/Traits/IterateCollectionTrait.php b/src/Api/Record/Collections/Traits/IterateCollectionTrait.php new file mode 100644 index 00000000..bf448557 --- /dev/null +++ b/src/Api/Record/Collections/Traits/IterateCollectionTrait.php @@ -0,0 +1,80 @@ + + * Product::collection($ms) + * ->get() + * ->each(function (Product $product) { + * echo $product->name . PHP_EOL; + * }); + * + */ + public function each(callable $closure): void + { + foreach ($this->rows ?? [] as $row) { + $closure($row); + } + } + + /** + * Перебирает все сущности в API Моего Склада, отправляя их по одному в переданное замыкание. + * Самостоятельно отправляет дополнительные запросы, если ответ не вмещается в заданный limit (по умолчанию 1000). + * Начинает перебор с заданного offset (по умолчанию 0). + * + * + * Product::collection($ms) + * ->limit(100) + * ->get() + * ->eachGenerator(function (Product $product) { + * echo $product->name . PHP_EOL; + * }); + * + * + * @throws RequestException + */ + public function eachGenerator(callable $closure): void + { + $this->get(); + + do { + $this->each($closure); + } while ($this->getNext()); + } + + /** + * Перебирает все сущности в API Моего Склада, отправляя их постранично коллекциями в переданное замыкание. + * Хорошо сочетается с методами массового изменения (massCreateUpdate, massDelete). + * Самостоятельно отправляет дополнительные запросы, если ответ не вмещается в заданный limit (по умолчанию 1000). + * Начинает перебор с заданного offset (по умолчанию 0). + * + * + * Product::collection($ms) + * ->eachCollectionGenerator(function (ProductCollection $products) use ($ms) { + * $products->each(function (Product $product) { + * $product->name = mb_strtoupper($product->name); + * }); + * Product::collection($ms)->massCreateUpdate($products); + * }); + * + * + * @throws RequestException + */ + public function eachCollectionGenerator(callable $closure): void + { + $this->get(); + + do { + $closure(clone $this); + } while ($this->getNext()); + } +} diff --git a/src/Api/Record/Collections/Traits/IteratorTrait.php b/src/Api/Record/Collections/Traits/IteratorTrait.php new file mode 100644 index 00000000..8267d0b9 --- /dev/null +++ b/src/Api/Record/Collections/Traits/IteratorTrait.php @@ -0,0 +1,40 @@ +getRows()[$this->iteratorKey]; + } + + public function next(): void + { + ++$this->iteratorKey; + } + + public function key(): int + { + return $this->iteratorKey; + } + + public function valid(): bool + { + return array_key_exists($this->iteratorKey, $this->getRows()); + } + + public function rewind(): void + { + $this->iteratorKey = 0; + } + + private function getRows(): array + { + return $this->rows ?? []; + } +} diff --git a/src/Api/Record/Collections/Traits/ParamsCollectionTrait.php b/src/Api/Record/Collections/Traits/ParamsCollectionTrait.php new file mode 100644 index 00000000..128ab3dd --- /dev/null +++ b/src/Api/Record/Collections/Traits/ParamsCollectionTrait.php @@ -0,0 +1,169 @@ + + * $products = Product::collection($ms) + * ->limit(100) + * ->expand('owner') + * ->expand('minPrice.currency') + * ->expand(['group', 'images']); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand + */ + public function expand(array|string $field): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setExpand($params, $field); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } + + /** + * Фильтрация результатов. + * Принимает три параметра (имя свойства, знак, значение) или два (имя свойства и значение, знак по умолчанию - '='). + * Несколько фильтров можно применить, вызвав метод несколько раз, или при помощи массива массивов. + * + * + * $products = Product::collection($ms) + * ->filter('archived', false) + * ->filter('name', '=~', 'apple') + * ->filter([ + * ['minimumBalance', '=', '0'], + * ['code', FilterSign::NEQ, 123], + * ]); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter + */ + public function filter( + array|string $key, + FilterSign|string|int|float|bool $sign = null, + string|int|float|bool $value = null + ): static { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setFilter($params, $key, $sign, $value); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } + + /** + * Ограничение максимального количества возвращаемых элементов (по умолчанию 1000). + * + * + * $products = Product::collection($ms) + * ->limit(100) + * ->get(); + * + */ + public function limit(int $amount): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setLimit($params, $amount); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } + + /** + * Смещение начала выборки, используется для пагинации. + * + * + * $products = Product::collection($ms) + * ->offset(100) + * ->get(); + * + */ + public function offset(int $amount): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setOffset($params, $amount); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } + + /** + * Сортировка результата по переданному полю. + * Несколько сортировок можно применить, вызвав метод несколько раз, или при помощи массива массивов. + * + * + * $products = Product::collection($ms) + * ->order('updated', 'asc') + * ->order([ + * ['code', 'desc'], + * ['name'], + * ]); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sortirowka-ob-ektow + */ + public function order(array|string $field, OrderDirection|string $direction = 'asc'): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setOrder($params, $field, $direction); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } + + /** + * Контекстный поиск. + * + * + * $products = Product::collection($ms) + * ->search('orange') + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-kontextnyj-poisk + */ + public function search(string $text): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setSearch($params, $text); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } + + /** + * Универсальный метод для формирования произвольного параметра url. + * Несколько параметров можно применить, вызвав метод несколько раз, или при помощи массива массивов. + * + * + * $products = Product::collection($ms) + * ->param('limit', 10) + * ->param([ + * ['offset', '20'], + * ['search', 'orange'], + * ]) + * ->get(); + * + */ + public function param(array|string $key, string|int|float|bool $value = null): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setParam($params, $key, $value); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } +} diff --git a/src/Api/Record/Collections/UnknownCollection.php b/src/Api/Record/Collections/UnknownCollection.php new file mode 100644 index 00000000..c31e07fe --- /dev/null +++ b/src/Api/Record/Collections/UnknownCollection.php @@ -0,0 +1,32 @@ + + * $product = Product::make($ms, ['name' => 'orange']); + * + */ + public static function make(MoySklad $ms, mixed $content = []): static + { + return new static($ms, $content); + } + + /** + * Создаёт новый объект коллекции данного класса. + * + * + * $productCollection = Product::collection($ms); + * + * + * @return T + */ + public static function collection(MoySklad $ms): AbstractConcreteCollection + { + return RecordMappingHelper::resolveCollection($ms, static::TYPE); + } +} diff --git a/src/Api/Record/Objects/Documents/Customerorder.php b/src/Api/Record/Objects/Documents/Customerorder.php new file mode 100644 index 00000000..b1145dcb --- /dev/null +++ b/src/Api/Record/Objects/Documents/Customerorder.php @@ -0,0 +1,76 @@ + + */ +class Customerorder extends AbstractConcreteObject +{ + public const PATH = [ + Endpoint::ENTITY, + Document::CUSTOMERORDER, + ]; + public const TYPE = Document::CUSTOMERORDER; +} diff --git a/src/Api/Record/Objects/Entities/Assortment.php b/src/Api/Record/Objects/Entities/Assortment.php new file mode 100644 index 00000000..28aaedaa --- /dev/null +++ b/src/Api/Record/Objects/Entities/Assortment.php @@ -0,0 +1,26 @@ + + */ +class Assortment extends AbstractConcreteObject +{ + public const PATH = [ + Endpoint::ENTITY, + Entity::ASSORTMENT, + ]; + public const TYPE = Entity::ASSORTMENT; +} diff --git a/src/Api/Record/Objects/Entities/Employee.php b/src/Api/Record/Objects/Entities/Employee.php new file mode 100644 index 00000000..5b253d87 --- /dev/null +++ b/src/Api/Record/Objects/Entities/Employee.php @@ -0,0 +1,56 @@ + + * + * @property string $accountId + * @property bool $archived + * @property ?UnknownObject[] $attributes + * @property ?UnknownObject[] $cashiers + * @property ?string $code + * @property string $created + * @property ?string $description + * @property ?string $email + * @property string $externalCode + * @property ?string $firstName + * @property ?string $fullName + * @property UnknownObject $group + * @property string $id + * @property ?Image $image + * @property ?string $inn + * @property string $lastName + * @property ?MetaObject $meta + * @property ?string $middleName + * @property string $name + * @property Employee $owner + * @property ?string $phone + * @property ?string $position + * @property bool $shared + * @property ?string $shortFio + * @property ?string $uid + * @property string $updated + */ +class Employee extends AbstractConcreteObject +{ + public const PATH = [ + Endpoint::ENTITY, + Entity::EMPLOYEE, + ]; + public const TYPE = Entity::EMPLOYEE; +} diff --git a/src/Api/Record/Objects/Entities/Product.php b/src/Api/Record/Objects/Entities/Product.php new file mode 100644 index 00000000..ea0532f1 --- /dev/null +++ b/src/Api/Record/Objects/Entities/Product.php @@ -0,0 +1,81 @@ + + * + * @property string $id + * @property string $accountId + * @property ?Employee $owner + * @property string $name + * @property ?MetaObject $meta + * @property bool $shared + * @property UnknownObject $group + * @property ?UnknownCollection $images + * @property string $updated + * @property ?string $code + * @property string $externalCode + * @property bool $archived + * @property bool $useParentVat + * @property string $pathName + * @property ?Price $minPrice + * @property ?PriceWithType[] $salePrices + * @property ?Price $buyPrice + * @property ?Barcode[] $barcodes + * @property ?string $paymentItemType + * @property bool $discountProhibited + * @property ?int $weight + * @property ?int $volume + * @property int $variantsCount + * @property ?bool $isSerialTrackable + * @property ?string $trackingType + * @property ?UnknownCollection $files + * @property ?Alcoholic $alcoholic + * @property ?string $article + * @property ?string $description + * @property ?UnknownObject $country + * @property ?int $effectiveVat + * @property ?bool $effectiveVatEnabled + * @property ?bool $vatEnabled + * @property ?int $minimumBalance + * @property ?Pack[] $packs + * @property ?bool $partialDisposal + * @property ?string $ppeType + * @property ?UnknownObject $productFolder + * @property ?UnknownObject $supplier + * @property ?string $syncId + * @property ?string $taxSystem + * @property ?string[] $things + * @property ?string $tnved + * @property ?UnknownObject $uom + * @property ?int $vat + * @property ?UnknownObject[] $attributes + */ +class Product extends AbstractConcreteObject +{ + public const PATH = [ + Endpoint::ENTITY, + Entity::PRODUCT, + ]; + public const TYPE = Entity::PRODUCT; +} diff --git a/src/Api/Record/Objects/Traits/CrudObjectTrait.php b/src/Api/Record/Objects/Traits/CrudObjectTrait.php new file mode 100644 index 00000000..892bce9e --- /dev/null +++ b/src/Api/Record/Objects/Traits/CrudObjectTrait.php @@ -0,0 +1,123 @@ + + * $product = Product::make($ms, ['id' => '9aa1b41b-f2fc-11ed-0a80-0f60007ec621']) + * ->get(); + * + * + * @throws RequestException + */ + public function get(): static + { + return $this->send(HttpMethod::GET); + } + + /** + * Создание сущности в Моём Складе. + * + * + * $product = Product::make($ms, ['name' => 'orange'])->create(); + * + * //Или + * $product = Product::make($ms); + * $product->name = 'orange'; + * $product->create(); + * + * + * @throws RequestException + */ + public function create(): static + { + return $this->send(HttpMethod::POST); + } + + /** + * Обновление сущности. + * + * + * $product = Product::make($ms, ['id' => '9aa1b41b-f2fc-11ed-0a80-0f60007ec621']) + * ->update(['name' => 'orange']); + * + * //Или + * $product = Product::make($ms, ['id' => '9aa1b41b-f2fc-11ed-0a80-0f60007ec621']); + * $product->name = 'orange'; + * $product->update(); + * + * + * @throws RequestException + */ + public function update(mixed $content = []): static + { + $this->hydrateAdd($content); + + return $this->send(HttpMethod::PUT); + } + + /** + * Удаление сущности. + * + * + * Product::make($ms, ['id' => '9aa1b41b-f2fc-11ed-0a80-0f60007ec621']) + * ->delete(); + * + * + * @throws RequestException + */ + public function delete(): static + { + return $this->send(HttpMethod::DELETE); + } + + /** + * Универсальный метод, позволяющий отправлять произвольный HTTP-запрос. + * + * + * $product = Product::make($ms, ['id' => '825c1a20-f2ff-11ed-0a80-0868007fddf4']); + * $product->name = 'tangerine'; + * $product->send('PUT'); + * + * + * @throws RequestException + */ + public function send(HttpMethod|string $method): static + { + $payload = $this->makePayload(HttpMethod::makeFrom($method)); + + $response = $this->ms->getApiClient()->send($payload); + if (RecordHelper::isCollection($this->ms, $response)) { + throw new InvalidArgumentException('Response must be an object, collection received'); + } + + $this->hydrate($response); + + return $this; + } + + protected function makePayload(HttpMethod $method): Payload + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + + return new Payload( + method: $method, + path: $path, + params: $params, + body: $this, + ); + } +} diff --git a/src/Api/Record/Objects/Traits/FillMetaObjectTrait.php b/src/Api/Record/Objects/Traits/FillMetaObjectTrait.php new file mode 100644 index 00000000..926a06ca --- /dev/null +++ b/src/Api/Record/Objects/Traits/FillMetaObjectTrait.php @@ -0,0 +1,27 @@ +meta) { + return; + } + + $id = $this->id ?? null; + if ($id !== null && $path[array_key_last($path)] !== $id) { + $path[] = $id; + } + + $meta = $this->ms->meta()->create($path, $this->type); + $formatter = $this->ms->getFormatter(); + + $this->meta = (new StdClassFormat())->encode($formatter->decode($meta)); + } +} diff --git a/src/Api/Record/Objects/Traits/ParamsObjectTrait.php b/src/Api/Record/Objects/Traits/ParamsObjectTrait.php new file mode 100644 index 00000000..4b33a73d --- /dev/null +++ b/src/Api/Record/Objects/Traits/ParamsObjectTrait.php @@ -0,0 +1,58 @@ + + * $product = Product::make($ms, ['id' => '66046520-f26f-11ed-0a80-0f6000692310']) + * ->expand('owner') + * ->expand('minPrice.currency') + * ->expand(['group', 'images']) + * ->get(); + * + * + * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand + */ + public function expand(array|string $field): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setExpand($params, $field); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } + + /** + * Универсальный метод для формирования произвольного параметра url. + * Несколько параметров можно применить, вызвав метод несколько раз, или при помощи массива массивов. + * + * + * $product = Product::make($ms, ['id' => 'f98dc6a3-f323-11ed-0a80-0f600088f4ad']) + * ->param('expand', 'owner') + * ->param([ + * ['expand', 'group'], + * ['expand', 'images,images.owner'], + * ]) + * ->get(); + * + */ + public function param(array|string $key, string|int|float|bool $value = null): static + { + [$path, $params] = Url::parsePathAndParams($this->meta->href); + $params = QueryParams::setParam($params, $key, $value); + $this->meta->href = Url::makeFromPathAndParams($path, $params); + + return $this; + } +} diff --git a/src/Api/Record/Objects/Traits/SetIdInMetaHrefTrait.php b/src/Api/Record/Objects/Traits/SetIdInMetaHrefTrait.php new file mode 100644 index 00000000..60452b7c --- /dev/null +++ b/src/Api/Record/Objects/Traits/SetIdInMetaHrefTrait.php @@ -0,0 +1,60 @@ +__unset($name); + + return; + } + + if ($name === 'id') { + if (!Guid::check($value)) { + throw new InvalidArgumentException('id must be a guid'); + } + $this->setIdToMetaHref($value); + } + + parent::__set($name, $value); + } + + public function __unset(string $name) + { + if ($name === 'id') { + $this->setIdToMetaHref(null); + } + + parent::__unset($name); + } + + protected function setIdToMetaHref(?string $id): void + { + $href = $this->meta->href ?? null; + if (!$href) { + return; + } + + [$path, $params] = Url::parsePathAndParams($href); + $prevId = Url::getId($href); + + if ($prevId === null) { + $path[] = $id; + } elseif ($id === null) { + array_pop($path); + } else { + $path[array_key_last($path)] = $id; + } + + $this->meta->href = Url::makeFromPathAndParams($path, $params); + } +} diff --git a/src/Api/Record/Objects/UnknownObject.php b/src/Api/Record/Objects/UnknownObject.php new file mode 100644 index 00000000..b0156a0c --- /dev/null +++ b/src/Api/Record/Objects/UnknownObject.php @@ -0,0 +1,53 @@ + + * $product = UnknownObject::make($ms, ['entity', 'product'], 'product', ['name' => 'orange']); + * + */ + public static function make(MoySklad $ms, array $path, string $type, mixed $content = []): self + { + return new self($ms, $path, $type, $content); + } + + /** + * Создаёт новый объект неизвестной коллекции, опираясь на набор сегментов url $path и тип сущности $type. + * + * + * $productCollection = UnknownObject::collection($ms, ['entity', 'product'], 'product'); + * + */ + public static function collection(MoySklad $ms, array $path, string $type): UnknownCollection + { + return new UnknownCollection($ms, $path, $type); + } +} diff --git a/src/Api/Segments/ById/AbstractById.php b/src/Api/Segments/ById/AbstractById.php deleted file mode 100644 index 829eb94f..00000000 --- a/src/Api/Segments/ById/AbstractById.php +++ /dev/null @@ -1,21 +0,0 @@ - - * $products = $ms->query() - * ->entity() - * ->product() - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-towar - */ - public function product(): Product - { - return $this->resolveNamedBuilder(Product::class); - } - - /** - * Customer orders - * - * $customerOrders = $ms->query() - * ->entity() - * ->customerorder() - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/documents/#dokumenty-zakaz-pokupatelq - */ - public function customerorder(): Customerorder - { - return $this->resolveNamedBuilder(Customerorder::class); - } - - /** - * Assortments - * - * $assortments = $ms->query() - * ->entity() - * ->assortment() - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/dictionaries/#suschnosti-assortiment - */ - public function assortment(): Assortment - { - return $this->resolveNamedBuilder(Assortment::class); - } -} diff --git a/src/Api/Segments/Endpoints/Notification.php b/src/Api/Segments/Endpoints/Notification.php deleted file mode 100644 index 053c1874..00000000 --- a/src/Api/Segments/Endpoints/Notification.php +++ /dev/null @@ -1,22 +0,0 @@ -apiSend(HttpMethod::POST, $body); - } - - public function massDeleteDebug(mixed $body) - { - return $this->apiDebug(HttpMethod::POST, $body); - } -} diff --git a/src/Api/Traits/Actions/MassDeleteTrait.php b/src/Api/Traits/Actions/MassDeleteTrait.php deleted file mode 100644 index 30b1e056..00000000 --- a/src/Api/Traits/Actions/MassDeleteTrait.php +++ /dev/null @@ -1,27 +0,0 @@ - - * $products = $ms->query() - * ->entity() - * ->customerorder() - * ->massDelete($body); - * - * - * @throws RequestException - */ - public function massDelete(mixed $body) - { - return (new MassDelete($this->api, $this->path, $this->params))->massDelete($body); - } -} diff --git a/src/Api/Traits/Params/ExpandTrait.php b/src/Api/Traits/Params/ExpandTrait.php deleted file mode 100644 index 3e5122c6..00000000 --- a/src/Api/Traits/Params/ExpandTrait.php +++ /dev/null @@ -1,49 +0,0 @@ - - * $product = $ms->query() - * ->entity() - * ->product() - * ->limit(100) - * ->expand('owner') - * ->expand('minPrice.currency') - * ->expand(['group', 'images']); - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-zamena-ssylok-ob-ektami-s-pomosch-u-expand - */ - public function expand(array|string $field): static - { - if (is_array($field)) { - return $this->handleArrayOfExpands($field); - } - - $this->setQueryParam(QueryParam::EXPAND, $field); - - return $this; - } - - private function handleArrayOfExpands(array $expands): static - { - foreach ($expands as $expand) { - if (!is_string($expand)) { - throw new InvalidArgumentException('Each expand must be a string'); - } - $this->expand($expand); - } - - return $this; - } -} diff --git a/src/Api/Traits/Params/FilterTrait.php b/src/Api/Traits/Params/FilterTrait.php deleted file mode 100644 index dc777132..00000000 --- a/src/Api/Traits/Params/FilterTrait.php +++ /dev/null @@ -1,123 +0,0 @@ - - * $product = $ms->query()->entity()->product() - * ->filter('archived', false) - * ->filter('name', '=~', 'apple') - * ->filter([ - * ['minimumBalance', '=', '0'], - * ['code', FilterSign::NEQ, 123], - * ]) - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter - */ - public function filter( - array|string $key, - FilterSign|string|int|float|bool $sign = null, - string|int|float|bool $value = null - ): static { - if (is_array($key)) { - return $this->handleArrayOfFilters($key); - } - - [$signString, $valueString] = $this->prepareSignAndValueAsStrings($key, $sign, $value); - $filter = $key . $signString . $this->escapeSemicolon($valueString); - - $this->setQueryParam(QueryParam::FILTER, $filter); - - return $this; - } - - /** - * [DEPRECATED]: Use filter() instead. - * Filter results with multiple filters. $filters must contain arrays with 3 elements (key, sign and value) or - * only 2 (key and value, '=' will be used as default sign). - * - * $product = $ms->query() - * ->entity() - * ->product() - * ->filters([ - * ['archived', false], - * ['name', '=', 'tangerine'], - * ['code', FilterSign::NEQ, 123], - * ]) - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-fil-traciq-wyborki-s-pomosch-u-parametra-filter - * @deprecated - */ - public function filters(array $filters): static - { - return $this->handleArrayOfFilters($filters); - } - - private function handleArrayOfFilters(array $filters): static - { - foreach ($filters as $filter) { - if (!is_array($filter)) { - throw new InvalidArgumentException('Each filter must be an array'); - } - $this->filter(...$filter); - } - - return $this; - } - - /** - * @return string[] - */ - private function prepareSignAndValueAsStrings( - string $key, - FilterSign|string|int|float|bool|null $sign, - string|int|float|bool|null $value - ): array { - $prefix = "For filter key '$key': "; - - if ($sign === null) { - throw new InvalidArgumentException($prefix . 'sign missed'); - } - - if ($value === null) { - if (is_a($sign, FilterSign::class)) { - throw new InvalidArgumentException($prefix . 'with a sign, you must pass the value as the third parameter'); - } - - /** @var bool|float|int|string $sign */ - $value = $sign; - $sign = $this->defaultSign; - } elseif (!is_a($sign, FilterSign::class) && !is_string($sign)) { - throw new InvalidArgumentException($prefix . 'with a value, sign must be a string or ' . FilterSign::class); - } - - if (is_a($sign, FilterSign::class)) { - $sign = $sign->value; - } - $value = Url::convertMixedValueToString($value); - - return [$sign, $value]; - } - - private function escapeSemicolon(string $value): string - { - return str_replace(';', '\;', $value); - } -} diff --git a/src/Api/Traits/Params/LimitOffsetTrait.php b/src/Api/Traits/Params/LimitOffsetTrait.php deleted file mode 100644 index d6ff27c5..00000000 --- a/src/Api/Traits/Params/LimitOffsetTrait.php +++ /dev/null @@ -1,47 +0,0 @@ - - * $product = $ms->query() - * ->entity() - * ->product() - * ->limit(100) - * ->offset(0) - * ->debug() - * ->get(); - * - */ - public function limit(int $limit): static - { - $this->setQueryParam(QueryParam::LIMIT, $limit); - - return $this; - } - - /** - * Offset for pagination - * - * $product = $ms->entity() - * ->product() - * ->limit(100) - * ->offset(0) - * ->debug() - * ->get(); - * - */ - public function offset(int $offset): static - { - $this->setQueryParam(QueryParam::OFFSET, $offset); - - return $this; - } -} diff --git a/src/Api/Traits/Params/OrderTrait.php b/src/Api/Traits/Params/OrderTrait.php deleted file mode 100644 index d118d482..00000000 --- a/src/Api/Traits/Params/OrderTrait.php +++ /dev/null @@ -1,54 +0,0 @@ - - * $product = $ms->query() - * ->entity() - * ->product() - * ->order('updated', 'asc') - * ->order([ - * ['code', 'desc'], - * ['name'], - * ]) - * ->get(); - * - * - * @see https://dev.moysklad.ru/doc/api/remap/1.2/#mojsklad-json-api-obschie-swedeniq-sortirowka-ob-ektow - */ - public function order(array|string $field, OrderDirection|string $direction = 'asc'): static - { - if (is_array($field)) { - return $this->handleArrayOfOrders($field); - } - - $directionString = is_a($direction, OrderDirection::class) ? $direction->value : $direction; - $sort = $field . ',' . $directionString; - - $this->setQueryParam(QueryParam::ORDER, $sort); - - return $this; - } - - private function handleArrayOfOrders(array $orders): static - { - foreach ($orders as $order) { - if (!is_array($order)) { - throw new InvalidArgumentException('Each order must be an array'); - } - $this->order(...$order); - } - - return $this; - } -} diff --git a/src/Api/Traits/Params/ParamTrait.php b/src/Api/Traits/Params/ParamTrait.php deleted file mode 100644 index 6dcb18d6..00000000 --- a/src/Api/Traits/Params/ParamTrait.php +++ /dev/null @@ -1,51 +0,0 @@ - - * $order = $ms->query() - * ->entity() - * ->customerorder() - * ->param('limit', 10) - * ->param([ - * ['offset', '20'], - * ['order', 'name;created_at,desc'], - * ]) - * ->get(); - * - */ - public function param(array|string $key, string|int|float|bool $value = null): static - { - if (is_array($key)) { - return $this->handleArrayOfParams($key); - } - - if ($value === null) { - throw new InvalidArgumentException("Value can't be null for the key '$key'"); - } - - $this->setQueryParam($key, $value); - - return $this; - } - - private function handleArrayOfParams(array $params): static - { - foreach ($params as $param) { - if (!is_array($param)) { - throw new InvalidArgumentException('Each param must be an array'); - } - $this->param(...$param); - } - - return $this; - } -} diff --git a/src/Api/Traits/Segments/AttributesTrait.php b/src/Api/Traits/Segments/AttributesTrait.php deleted file mode 100644 index 4fc0fc66..00000000 --- a/src/Api/Traits/Segments/AttributesTrait.php +++ /dev/null @@ -1,26 +0,0 @@ - - * $product = $ms->query() - * ->entity() - * ->product() - * ->metadata() - * ->attributes() - * ->get(); - * - */ - public function attributes(): Attributes - { - return $this->resolveNamedBuilder(Attributes::class); - } -} diff --git a/src/Api/Traits/Segments/ByIdCommonTrait.php b/src/Api/Traits/Segments/ByIdCommonTrait.php deleted file mode 100644 index fbc8a016..00000000 --- a/src/Api/Traits/Segments/ByIdCommonTrait.php +++ /dev/null @@ -1,25 +0,0 @@ - - * $product = $ms->query() - * ->entity() - * ->product() - * ->byId('fb72fc83-7ef5-11e3-ad1c-002590a28eca') - * ->get(); - * - */ - public function byId(string $guid): ByIdCommon - { - return $this->resolveCommonBuilder(ByIdCommon::class, $guid); - } -} diff --git a/src/Api/Traits/Segments/ByIdPositionedTrait.php b/src/Api/Traits/Segments/ByIdPositionedTrait.php deleted file mode 100644 index df451c74..00000000 --- a/src/Api/Traits/Segments/ByIdPositionedTrait.php +++ /dev/null @@ -1,27 +0,0 @@ - - * $order = $ms->query() - * ->entity() - * ->customerorder() - * ->byId('efe3944b-980d-11ec-0a80-0d180027c266') - * ->positions() - * ->byId('fb72fc83-7ef5-11e3-ad1c-002590a28eca') - * ->get(); - * - */ - public function byId(string $guid): ByIdPositioned - { - return $this->resolveCommonBuilder(ByIdPositioned::class, $guid); - } -} diff --git a/src/Api/Traits/Segments/MetadataTrait.php b/src/Api/Traits/Segments/MetadataTrait.php deleted file mode 100644 index 25801fb5..00000000 --- a/src/Api/Traits/Segments/MetadataTrait.php +++ /dev/null @@ -1,25 +0,0 @@ - - * $product = $ms->query() - * ->entity() - * ->product() - * ->metadata() - * ->get(); - * - */ - public function metadata(): Metadata - { - return $this->resolveNamedBuilder(Metadata::class); - } -} diff --git a/src/Api/Traits/Segments/MethodCommonTrait.php b/src/Api/Traits/Segments/MethodCommonTrait.php deleted file mode 100644 index 2310a5e2..00000000 --- a/src/Api/Traits/Segments/MethodCommonTrait.php +++ /dev/null @@ -1,24 +0,0 @@ - - * $product = $ms->query() - * ->entity() - * ->method('product') - * ->get(); - * - */ - public function method(string $entity): MethodCommon - { - return $this->resolveCommonBuilder(MethodCommon::class, $entity); - } -} diff --git a/src/Api/Traits/Segments/PositionsTrait.php b/src/Api/Traits/Segments/PositionsTrait.php deleted file mode 100644 index 690453ae..00000000 --- a/src/Api/Traits/Segments/PositionsTrait.php +++ /dev/null @@ -1,26 +0,0 @@ - - * $order = $ms->query() - * ->entity() - * ->customerorder() - * ->byId('efe3944b-980d-11ec-0a80-0d180027c266') - * ->positions() - * ->get(); - * - */ - public function positions(): Positions - { - return $this->resolveNamedBuilder(Positions::class); - } -} diff --git a/src/Dictionaries/Document.php b/src/Dictionaries/Document.php new file mode 100644 index 00000000..937acc3d --- /dev/null +++ b/src/Dictionaries/Document.php @@ -0,0 +1,10 @@ +getPrevious(); + if (!$previous || !method_exists($previous, 'getRequest')) { + return null; + } + + $response = $previous->getRequest(); + if (!is_subclass_of($response, RequestInterface::class)) { + return null; + } + + return $response; + } + + /** + * Возвращает PSR-7 объект HTTP ответа, если он существует + */ + public function getResponse(): ?ResponseInterface + { + $previous = $this->getPrevious(); + if (!$previous || !method_exists($previous, 'getResponse')) { + return null; + } + + $response = $previous->getResponse(); + if (!is_subclass_of($response, ResponseInterface::class)) { + return null; + } + + return $response; + } + + /** + * Возвращает содержимое HTTP ответа + */ + public function getContent(): stdClass|array|string|null + { + if ($this->contentResolved) { + $this->getContentEncoded(); + } + + $this->content = $this->getResponse()?->getBody()->getContents(); + $this->contentResolved = true; + + return $this->getContentEncoded(); + } + + private function getContentEncoded(): stdClass|array|string|null + { + return $this->content === null ? + null : + $this->formatter->encode($this->content); + } } diff --git a/src/Formatters/AbstractMultiDecoder.php b/src/Formatters/AbstractMultiDecoder.php index a0a81280..4b213dd2 100644 --- a/src/Formatters/AbstractMultiDecoder.php +++ b/src/Formatters/AbstractMultiDecoder.php @@ -4,7 +4,9 @@ namespace Evgeek\Moysklad\Formatters; +use Evgeek\Moysklad\Api\Record\AbstractRecord; use InvalidArgumentException; +use stdClass; use Throwable; /** @@ -14,18 +16,22 @@ */ abstract class AbstractMultiDecoder implements JsonFormatterInterface { - public static function decode(mixed $content): string + public function decode(mixed $content): string { - if (static::contentIsEmpty($content)) { + if ($this->contentIsEmpty($content)) { return ''; } if (is_string($content)) { - static::validateStringIsJsonObject($content); + $this->validateStringIsJsonObject($content); return $content; } + if (is_array($content) || is_a($content, stdClass::class)) { + $content = static::toArray($content); + } + try { $decodedContent = json_encode($content, JSON_THROW_ON_ERROR); } catch (Throwable) { @@ -34,30 +40,55 @@ public static function decode(mixed $content): string throw new InvalidArgumentException("Can't convert content of '$type' type to json string."); } - static::validateStringIsJsonObject($decodedContent); + $this->validateStringIsJsonObject($decodedContent); return $decodedContent; } - protected static function validateStringIsJsonObject(string $content): void + public static function toArray(array|stdClass|AbstractRecord $content): array + { + if (is_a($content, AbstractRecord::class)) { + return $content->toArray(); + } + + $array = []; + foreach ($content as $key => $value) { + $array[$key] = is_array($value) || is_a($value, stdClass::class) ? + self::toArray($value) : + $value; + } + + return $array; + } + + public static function checkStringIsValidJson(string $content): bool { try { $decodedContent = json_decode($content, true, 512, JSON_THROW_ON_ERROR); } catch (Throwable) { - static::throwContentIsNotValidJsonObject($content); + return false; } if (!is_array($decodedContent)) { - static::throwContentIsNotValidJsonObject($content); + return false; + } + + return true; + } + + protected function validateStringIsJsonObject(string $content): void + { + if (!self::checkStringIsValidJson($content)) { + $this->throwContentIsNotValidJsonObject($content); } } - protected static function throwContentIsNotValidJsonObject(string $content): never + protected function throwContentIsNotValidJsonObject(string $content): never { throw new InvalidArgumentException('Passed content is not valid json. Content:' . $content . PHP_EOL); } - protected static function contentIsEmpty(mixed $content): bool + protected function contentIsEmpty(mixed $content): bool { return !$content || (is_object($content) && (array) $content === []); } diff --git a/src/Formatters/ArrayFormat.php b/src/Formatters/ArrayFormat.php index 8b7105cf..c11d677c 100644 --- a/src/Formatters/ArrayFormat.php +++ b/src/Formatters/ArrayFormat.php @@ -13,7 +13,7 @@ */ class ArrayFormat extends AbstractMultiDecoder { - public static function encode(string $content): array + public function encode(string $content): array { if ($content === '') { return []; @@ -22,11 +22,11 @@ public static function encode(string $content): array try { $encodedContent = json_decode($content, true, 512, JSON_THROW_ON_ERROR); } catch (Throwable) { - static::throwContentIsNotValidJsonObject($content); + $this->throwContentIsNotValidJsonObject($content); } if (!is_array($encodedContent)) { - static::throwContentIsNotValidJsonObject($content); + $this->throwContentIsNotValidJsonObject($content); } return $encodedContent; diff --git a/src/Formatters/JsonFormatterInterface.php b/src/Formatters/JsonFormatterInterface.php index 40ae302e..bb234bdd 100644 --- a/src/Formatters/JsonFormatterInterface.php +++ b/src/Formatters/JsonFormatterInterface.php @@ -14,10 +14,10 @@ interface JsonFormatterInterface * * @return T */ - public static function encode(string $content); + public function encode(string $content); /** * Decode content to json string format */ - public static function decode(mixed $content): string; + public function decode(mixed $content): string; } diff --git a/src/Formatters/RecordFormat.php b/src/Formatters/RecordFormat.php new file mode 100644 index 00000000..f8eafd68 --- /dev/null +++ b/src/Formatters/RecordFormat.php @@ -0,0 +1,133 @@ + + */ +class RecordFormat extends AbstractMultiDecoder implements WithMoySkladInterface +{ + protected MoySklad $ms; + + public function __construct(private readonly RecordMapping $mapping = new RecordMapping()) + { + } + + public function setMoySklad(MoySklad $ms): static + { + $this->ms = $ms; + + return $this; + } + + public function getMapping(): RecordMapping + { + return $this->mapping; + } + + /** + * @return AbstractRecord|array|array|stdClass + */ + public function encode(string $content): AbstractRecord|stdClass|array + { + if ($content === '') { + return new stdClass(); + } + + try { + $encodedContent = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + } catch (Throwable) { + $this->throwContentIsNotValidJsonObject($content); + } + + if (!is_array($encodedContent)) { + $this->throwContentIsNotValidJsonObject($content); + } + + return $this->encodeToStdClass($encodedContent); + } + + public function encodeToStdClass(array $content): AbstractRecord|stdClass|array + { + $result = $this->encodeArray($content); + + return $this->convertToStdClass($result); + } + + protected function encodeArray(array $content): array|AbstractRecord + { + $object = $this->tryConvertToRecord($content); + if (is_a($object, AbstractRecord::class)) { + return $object; + } + + foreach ($content as $key => $value) { + $content[$key] = is_array($value) ? + $this->tryConvertToRecord($value) : + $value; + + if (is_array($content[$key])) { + $content[$key] = $this->encodeArray($value); + } + } + + return $content; + } + + protected function tryConvertToRecord(array $content): array|AbstractRecord + { + $type = $content['meta']['type'] ?? null; + $href = $content['meta']['href'] ?? null; + if (!$type || !$href) { + return $content; + } + + $class = RecordHelper::isCollection($this->ms, $content) ? + $this->mapping->getCollection($type) : + $this->mapping->getObject($type); + + [$path] = Url::parsePathAndParams($href); + + return is_a($class, AbstractConcreteRecord::class, true) ? + new $class($this->ms, $content) : + new $class($this->ms, $path, $type, $content); + } + + protected function convertToStdClass(array|AbstractRecord $content): AbstractRecord|stdClass|array + { + if (is_a($content, AbstractRecord::class)) { + return $content; + } + + if (array_is_list($content)) { + $array = []; + foreach ($content as $item) { + $array[] = $this->convertToStdClass($item); + } + + return $array; + } + + $object = new stdClass(); + foreach ($content as $key => $value) { + if (is_array($value)) { + $value = $this->convertToStdClass($value); + } + $object->{$key} = $value; + } + + return $object; + } +} diff --git a/src/Formatters/RecordMapping.php b/src/Formatters/RecordMapping.php new file mode 100644 index 00000000..9081e09d --- /dev/null +++ b/src/Formatters/RecordMapping.php @@ -0,0 +1,126 @@ + Product::class, + Entity::EMPLOYEE => Employee::class, + Entity::ASSORTMENT => Assortment::class, + Document::CUSTOMERORDER => Customerorder::class, + ]; + protected const DEFAULT_MAPPING_COLLECTIONS = [ + Entity::PRODUCT => ProductCollection::class, + Entity::EMPLOYEE => EmployeeCollection::class, + Entity::ASSORTMENT => AssortmentCollection::class, + Document::CUSTOMERORDER => CustomerorderCollection::class, + ]; + + protected array $objects = self::DEFAULT_MAPPING_OBJECTS; + protected array $collections = self::DEFAULT_MAPPING_COLLECTIONS; + + public function __construct(?array $objects = null, ?array $collections = null) + { + if (null !== $objects) { + $this->objects = $objects; + } + if (null !== $collections) { + $this->collections = $collections; + } + } + + /** + * @param class-string|list> $class + */ + public function setObject(array|string $class): static + { + $this->set($this->objects, AbstractConcreteObject::class, $class); + + return $this; + } + + /** + * @param class-string|list> $class + */ + public function setCollection(array|string $class): static + { + $this->set($this->collections, AbstractConcreteCollection::class, $class); + + return $this; + } + + /** + * @return class-string + */ + public function getObject(string $type): string + { + return $this->get($this->objects, AbstractConcreteObject::class, $type) ?? UnknownObject::class; + } + + /** + * @return class-string + */ + public function getCollection(string $type): string + { + return $this->get($this->collections, AbstractConcreteCollection::class, $type) ?? UnknownCollection::class; + } + + /** @param class-string|list> $class */ + protected function set(array &$property, string $expectedClass, array|string $class): void + { + if (is_array($class)) { + foreach ($class as $nestedClass) { + $this->set($property, $expectedClass, $nestedClass); + } + + return; + } + + $type = $class::TYPE; + if (is_string($class)) { + $this->validateClassIs($class, $expectedClass); + $property[$type] = $class; + + return; + } + } + + protected function validateClassIs(string $class, string $expectedClass): void + { + if (!is_a($class, $expectedClass, true)) { + throw new InvalidArgumentException("$class is not a $expectedClass"); + } + } + + private function get(array $property, string $expectedClass, string $type): ?string + { + if (!array_key_exists($type, $property)) { + return null; + } + + $class = $property[$type]; + $this->validateClassIs($class, $expectedClass); + + return $class; + } +} diff --git a/src/Formatters/StdClassFormat.php b/src/Formatters/StdClassFormat.php index 5dd61ddf..2afaf900 100644 --- a/src/Formatters/StdClassFormat.php +++ b/src/Formatters/StdClassFormat.php @@ -17,7 +17,7 @@ class StdClassFormat extends AbstractMultiDecoder /** * @return array|stdClass */ - public static function encode(string $content): stdClass|array + public function encode(string $content): stdClass|array { if ($content === '') { return new stdClass(); @@ -26,11 +26,11 @@ public static function encode(string $content): stdClass|array try { $encodedContent = json_decode($content, false, 512, JSON_THROW_ON_ERROR); } catch (Throwable) { - static::throwContentIsNotValidJsonObject($content); + $this->throwContentIsNotValidJsonObject($content); } if (!is_a($encodedContent, stdClass::class) && !is_array($encodedContent)) { - static::throwContentIsNotValidJsonObject($content); + $this->throwContentIsNotValidJsonObject($content); } return $encodedContent; diff --git a/src/Formatters/StringFormat.php b/src/Formatters/StringFormat.php index 7ca6948a..0f11f414 100644 --- a/src/Formatters/StringFormat.php +++ b/src/Formatters/StringFormat.php @@ -11,13 +11,13 @@ */ class StringFormat extends AbstractMultiDecoder { - public static function encode(string $content): string + public function encode(string $content): string { if ($content === '') { return ''; } - static::validateStringIsJsonObject($content); + $this->validateStringIsJsonObject($content); return $content; } diff --git a/src/Formatters/WithMoySkladInterface.php b/src/Formatters/WithMoySkladInterface.php new file mode 100644 index 00000000..b560e46e --- /dev/null +++ b/src/Formatters/WithMoySkladInterface.php @@ -0,0 +1,12 @@ +formatter::encode($this->sendRequest($payload)); + return $this->formatter->encode($this->sendRequest($payload)); } public function debug(Payload $payload) @@ -42,10 +42,10 @@ public function debug(Payload $payload) 'url' => urldecode($url), 'url_encoded' => $url, 'headers' => $this->headers, - 'body' => ArrayFormat::encode($this->formatter::decode($payload->body)), + 'body' => (new ArrayFormat())->encode($this->formatter->decode($payload->body)), ]; - return $this->formatter::encode(ArrayFormat::decode($debug)); + return $this->formatter->encode((new ArrayFormat())->decode($debug)); } /** @@ -54,7 +54,7 @@ public function debug(Payload $payload) public function getGenerator(Payload $payload): Generator { do { - $content = ArrayFormat::encode($this->sendRequest($payload)); + $content = (new ArrayFormat())->encode($this->sendRequest($payload)); if (!array_key_exists('rows', $content)) { throw new UnexpectedValueException("Response is non-iterable (missed 'rows' property)"); } @@ -65,7 +65,7 @@ public function getGenerator(Payload $payload): Generator } foreach ($content['rows'] as $row) { - yield $this->formatter::encode(ArrayFormat::decode($row)); + yield $this->formatter->encode((new ArrayFormat())->decode($row)); } $next = $content['meta']['nextHref'] ?? null; @@ -82,7 +82,7 @@ public function getGenerator(Payload $payload): Generator private function sendRequest(Payload $payload): string { $uri = Url::make($payload); - $body = $this->formatter::decode($payload->body); + $body = $this->formatter->decode($payload->body); $request = new Request($payload->method->value, $uri, $this->headers, $body); try { @@ -91,7 +91,7 @@ private function sendRequest(Payload $payload): string ->getBody() ->getContents(); } catch (Throwable $e) { - throw new RequestException($e->getMessage(), $e->getCode(), $e); + throw new RequestException($this->formatter, $e->getMessage(), $e->getCode(), $e); } } @@ -109,7 +109,7 @@ private function addCredentialsToHeaders(array $credentials): void } if ($count === 2) { - $this->headers['Authorization'] = 'Basic ' . base64_encode($credentials[0] . ':' .$credentials[1]); + $this->headers['Authorization'] = 'Basic ' . base64_encode($credentials[0] . ':' . $credentials[1]); return; } diff --git a/src/Http/GuzzleSenderFactory.php b/src/Http/GuzzleSenderFactory.php index f064de04..a24005ff 100644 --- a/src/Http/GuzzleSenderFactory.php +++ b/src/Http/GuzzleSenderFactory.php @@ -16,8 +16,16 @@ class GuzzleSenderFactory implements RequestSenderFactoryInterface { - public function __construct(private readonly int $retries = 0, private readonly int $exceptionTruncateAt = 120) - { + /** + * Стандартная фабрика Guzzle. + * + * @param int $retries количество повторных попыток отправки запроса в случае неудачи + * @param int $exceptionTruncateAt максимальный размер сообщения об ошибке + */ + public function __construct( + private readonly int $retries = 0, + private readonly int $exceptionTruncateAt = 4000 + ) { } public function make(): GuzzleSender diff --git a/src/Meta/MetaMaker.php b/src/Meta/MetaMaker.php new file mode 100644 index 00000000..ee9a6b4e --- /dev/null +++ b/src/Meta/MetaMaker.php @@ -0,0 +1,35 @@ +formatter); + } + + public function employee(string $guid) + { + return Meta::employee($guid, $this->formatter); + } + + public function customerorder(string $guid) + { + return Meta::customerorder($guid, $this->formatter); + } + + public function create(array $path, string $type) + { + return Meta::create($path, $type, $this->formatter); + } +} diff --git a/src/MoySklad.php b/src/MoySklad.php index 7f499ab5..c09a0c5f 100644 --- a/src/MoySklad.php +++ b/src/MoySklad.php @@ -4,41 +4,99 @@ namespace Evgeek\Moysklad; -use Evgeek\Moysklad\Api\Query; +use Evgeek\Moysklad\Api\Query\QueryBuilder; +use Evgeek\Moysklad\Api\Record\Builders\RecordBuilder; use Evgeek\Moysklad\Formatters\JsonFormatterInterface; use Evgeek\Moysklad\Formatters\StdClassFormat; +use Evgeek\Moysklad\Formatters\WithMoySkladInterface; use Evgeek\Moysklad\Http\ApiClient; use Evgeek\Moysklad\Http\GuzzleSenderFactory; use Evgeek\Moysklad\Http\RequestSenderFactoryInterface; +use Evgeek\Moysklad\Meta\MetaMaker; class MoySklad { private ApiClient $api; /** - * @param array $credentials ['login', 'password'] or ['token'] - * @param JsonFormatterInterface $formatter API response formatter - * @param RequestSenderFactoryInterface $requestSenderFactory PSR-7 client factory + * Основной класс библиотеки. + * + * @param array $credentials ['login', 'password'] или ['token'] + * @param JsonFormatterInterface $formatter Класс, форматирующий ответ от API + * @param RequestSenderFactoryInterface $requestSenderFactory Фабрика, создающая PSR-7 совместимый клиент + * + * @see https://github.com/evgeek/moysklad/blob/master/docs/setup.md */ public function __construct( array $credentials, - JsonFormatterInterface $formatter = new StdClassFormat(), + private readonly JsonFormatterInterface $formatter = new StdClassFormat(), RequestSenderFactoryInterface $requestSenderFactory = new GuzzleSenderFactory(), ) { - $this->api = new ApiClient($credentials, $formatter, $requestSenderFactory->make()); + if (is_a($this->formatter, WithMoySkladInterface::class)) { + $this->formatter->setMoySklad($this); + } + + $this->api = new ApiClient($credentials, $this->formatter, $requestSenderFactory->make()); } /** - * Query builder + * Конструктор запросов + * * * $products = $ms->query() - * ->endpoint('entity') + * ->entity() * ->product() * ->get(); * */ - public function query(): Query + public function query(): QueryBuilder + { + return new QueryBuilder($this->api); + } + + /** + * Конструктор объектов API + * + * + * $product = $ms->record() + * ->single() + * ->product(['name' => 'cucumber']) + * ->create(); + * + */ + public function record(): RecordBuilder + { + return new RecordBuilder($this); + } + + /** + * Конструктор метаданных + * + * + * $employeeMeta = $ms->meta() + * ->employee('25cf41f2-b068-11ed-0a80-0e9700500d7e'); + * + */ + public function meta(): MetaMaker + { + return new MetaMaker($this->formatter); + } + + /** + * Возвращает текущий форматтер + * + * + * $product = Product::make($ms, ['name' => 'orange']); + * $productAsString = $ms->getFormatter()->decode($product); + * + */ + public function getFormatter(): JsonFormatterInterface + { + return $this->formatter; + } + + public function getApiClient(): ApiClient { - return new Query($this->api); + return $this->api; } } diff --git a/src/Services/CollectionHelper.php b/src/Services/CollectionHelper.php new file mode 100644 index 00000000..1ca8f376 --- /dev/null +++ b/src/Services/CollectionHelper.php @@ -0,0 +1,23 @@ +rows) + && is_array($objects->rows) + ) { + return $objects->rows; + } + + return $objects; + } +} diff --git a/src/Services/QueryParams.php b/src/Services/QueryParams.php new file mode 100644 index 00000000..e682ee51 --- /dev/null +++ b/src/Services/QueryParams.php @@ -0,0 +1,188 @@ +value : $direction; + $sort = $field . ',' . $directionString; + + return self::set($params, QueryParam::ORDER, $sort); + } + + public static function setLimit(array $params, int $limit): array + { + return self::set($params, QueryParam::LIMIT, $limit); + } + + public static function setOffset(array $params, int $offset): array + { + return self::set($params, QueryParam::OFFSET, $offset); + } + + public static function setFilter( + array $params, + array|string $key, + FilterSign|string|int|float|bool $sign = null, + string|int|float|bool $value = null + ): array { + if (is_array($key)) { + return self::handleArrayOfFilters($params, $key); + } + + [$signString, $valueString] = self::prepareSignAndValueAsStrings($key, $sign, $value); + $filter = $key . $signString . self::escapeSemicolon($valueString); + + return self::set($params, QueryParam::FILTER, $filter); + } + + public static function setExpand(array $params, array|string $field): array + { + if (is_array($field)) { + return self::handleArrayOfExpands($params, $field); + } + + return self::set($params, QueryParam::EXPAND, $field); + } + + public static function setParam(array $params, array|string $key, string|int|float|bool|null $value = null): array + { + if (is_array($key)) { + return self::handleArrayOfParams($params, $key); + } + + if ($value === null) { + throw new InvalidArgumentException("Value can't be null for the key '$key'"); + } + + return self::set($params, $key, $value); + } + + private static function handleArrayOfOrders(array $params, array $orders): array + { + foreach ($orders as $order) { + if (!is_array($order)) { + throw new InvalidArgumentException('Each order must be an array'); + } + $params = self::setOrder($params, ...$order); + } + + return $params; + } + + private static function handleArrayOfFilters(array $params, array $filters): array + { + foreach ($filters as $filter) { + if (!is_array($filter)) { + throw new InvalidArgumentException('Each filter must be an array'); + } + $params = self::setFilter($params, ...$filter); + } + + return $params; + } + + /** + * @return string[] + */ + private static function prepareSignAndValueAsStrings( + string $key, + FilterSign|string|int|float|bool|null $sign, + string|int|float|bool|null $value + ): array { + $prefix = "For filter key '$key': "; + + if ($sign === null) { + throw new InvalidArgumentException($prefix . 'sign missed'); + } + + if ($value === null) { + if (is_a($sign, FilterSign::class)) { + throw new InvalidArgumentException($prefix . 'with a sign, you must pass the value as the third parameter'); + } + + /** @var bool|float|int|string $sign */ + $value = $sign; + $sign = FilterSign::EQ; + } elseif (!is_a($sign, FilterSign::class) && !is_string($sign)) { + throw new InvalidArgumentException($prefix . 'with a value, sign must be a string or ' . FilterSign::class); + } + + if (is_a($sign, FilterSign::class)) { + $sign = $sign->value; + } + $value = Url::convertMixedValueToString($value); + + return [$sign, $value]; + } + + private static function escapeSemicolon(string $value): string + { + return str_replace(';', '\;', $value); + } + + private static function handleArrayOfExpands(array $params, array $expands): array + { + foreach ($expands as $expand) { + if (!is_string($expand)) { + throw new InvalidArgumentException('Each expand must be a string'); + } + + $params = self::setExpand($params, $expand); + } + + return $params; + } + + private static function handleArrayOfParams(array $params, array $settableParams): array + { + foreach ($settableParams as $param) { + if (!is_array($param)) { + throw new InvalidArgumentException('Each param must be an array'); + } + $params = self::setParam($params, ...$param); + } + + return $params; + } + + private static function set(array $params, QueryParam|string $queryParam, string|int|float|bool $value): array + { + $stringQueryParam = is_string($queryParam) ? $queryParam : $queryParam->value; + $stringValue = Url::convertMixedValueToString($value); + + $separator = QueryParam::getSeparator($stringQueryParam); + if ($separator === '') { + $params[$stringQueryParam] = $stringValue; + + return $params; + } + + if (!array_key_exists($stringQueryParam, $params)) { + $params[$stringQueryParam] = ''; + } + $params[$stringQueryParam] .= $params[$stringQueryParam] === '' ? + $stringValue : + $separator . $stringValue; + + return $params; + } +} diff --git a/src/Services/RecordHelper.php b/src/Services/RecordHelper.php new file mode 100644 index 00000000..9ab002c4 --- /dev/null +++ b/src/Services/RecordHelper.php @@ -0,0 +1,24 @@ +getFormatter(); + $arrayContent = (new ArrayFormat())->encode($formatter->decode($content)); + + return array_key_exists('rows', $arrayContent) + || isset($arrayContent['meta']['limit']) + || isset($arrayContent['meta']['offset']) + || isset($arrayContent['meta']['size']) + || isset($arrayContent['meta']['nextHref']) + || isset($arrayContent['meta']['previousHref']); + } +} diff --git a/src/Services/RecordMappingHelper.php b/src/Services/RecordMappingHelper.php new file mode 100644 index 00000000..8f91c9a8 --- /dev/null +++ b/src/Services/RecordMappingHelper.php @@ -0,0 +1,46 @@ +getObject($type); + + if (!is_a($class, AbstractConcreteObject::class, true)) { + throw new InvalidArgumentException("Object type '$type' has no mapped class"); + } + + return new $class($ms, $content); + } + + public static function resolveCollection(MoySklad $ms, string $type): AbstractConcreteCollection + { + $class = self::getMapping($ms)->getCollection($type); + + if (!is_a($class, AbstractConcreteCollection::class, true)) { + throw new InvalidArgumentException("Collection type '$type' has no mapped class"); + } + + return new $class($ms); + } + + private static function getMapping(MoySklad $ms): RecordMapping + { + $formatter = $ms->getFormatter(); + + return is_a($formatter, RecordFormat::class) ? + $formatter->getMapping() : + new RecordMapping(); + } +} diff --git a/src/Services/Url.php b/src/Services/Url.php index 7f1c7a08..9b4d34f8 100644 --- a/src/Services/Url.php +++ b/src/Services/Url.php @@ -5,8 +5,10 @@ namespace Evgeek\Moysklad\Services; use Evgeek\Moysklad\Http\Payload; +use Evgeek\Moysklad\Tools\Guid; +use InvalidArgumentException; -class Url +final class Url { public const API = 'https://online.moysklad.ru/api/remap/1.2'; @@ -21,7 +23,43 @@ public static function convertMixedValueToString(string|int|float|bool $value): public static function make(Payload $payload): string { - return self::prepareUrl($payload->path) . static::prepareQueryParams($payload->params); + return self::makeFromPathAndParams($payload->path, $payload->params); + } + + public static function makeFromPathAndParams(array $path, array $params = []): string + { + return self::prepareUrl($path) . self::prepareQueryParams($params); + } + + public static function parsePathAndParams(string $url): array + { + if (!str_starts_with($url, self::API)) { + throw new InvalidArgumentException("Url '$url' does not belongs to Moysklad JSON API v1.2"); + } + + $pathString = substr($url, strlen(self::API) + 1); + + $params = []; + $paramsString = parse_url($url)['query'] ?? null; + if ($paramsString) { + parse_str($paramsString, $params); + $pathString = str_replace("?$paramsString", '', $pathString); + } + + $path = explode('/', $pathString); + if ($path[array_key_last($path)] === '') { + array_pop($path); + } + + return [$path, $params]; + } + + public static function getId(string $url) + { + [$path] = self::parsePathAndParams($url); + $lastSegment = array_pop($path); + + return Guid::check($lastSegment) ? $lastSegment : null; } /** @@ -29,7 +67,7 @@ public static function make(Payload $payload): string */ private static function prepareUrl(array $path): string { - return static::API . '/' . implode('/', $path); + return self::API . '/' . implode('/', $path); } /** diff --git a/src/Tools/Guid.php b/src/Tools/Guid.php index 5d2e5168..16d80056 100644 --- a/src/Tools/Guid.php +++ b/src/Tools/Guid.php @@ -7,7 +7,7 @@ class Guid { /** - * Returns array with all guids from string + * Возвращает массив из всех guid, встречающихся в строке. */ public static function extractAll(string $url): array { @@ -18,7 +18,7 @@ public static function extractAll(string $url): array } /** - * Returns the first guid from the string (or null if there is no guid) + * Возвращает первый guid, найденный в строке, или null в случае его отсутствия. */ public static function extractFirst(string $url): ?string { @@ -28,7 +28,7 @@ public static function extractFirst(string $url): ?string } /** - * Returns the last guid from the string (or null if there is no guid) + * Возвращает последний guid, найденный в строке, или null в случае его отсутствия. */ public static function extractLast(string $url): ?string { @@ -37,4 +37,12 @@ public static function extractLast(string $url): ?string return $last ?: null; } + + /** + * Определяет, является ли строка валидным guid. + */ + public static function check(string $guid): bool + { + return $guid === static::extractFirst($guid); + } } diff --git a/src/Tools/Meta.php b/src/Tools/Meta.php index 7559cc02..c2c0bc8a 100644 --- a/src/Tools/Meta.php +++ b/src/Tools/Meta.php @@ -4,87 +4,105 @@ namespace Evgeek\Moysklad\Tools; +use Evgeek\Moysklad\Api\Record\Objects\Documents\Customerorder; +use Evgeek\Moysklad\Api\Record\Objects\Entities\Employee; +use Evgeek\Moysklad\Api\Record\Objects\Entities\Product; +use Evgeek\Moysklad\Dictionaries\Document; +use Evgeek\Moysklad\Dictionaries\Endpoint; +use Evgeek\Moysklad\Dictionaries\Entity; use Evgeek\Moysklad\Formatters\ArrayFormat; use Evgeek\Moysklad\Formatters\JsonFormatterInterface; use Evgeek\Moysklad\Formatters\StdClassFormat; +use Evgeek\Moysklad\Services\Url; use InvalidArgumentException; class Meta { - private static JsonFormatterInterface $formatter; + /** @deprecated */ + private static ?JsonFormatterInterface $formatter = null; - public static function state(string $guid, string $entity) + public static function state(string $entity, string $guid, JsonFormatterInterface $formatter = null) { - return static::entity([$entity, 'metadata', 'states', $guid], 'state'); + return static::entity([$entity, 'metadata', 'states', $guid], 'state', $formatter); } - public static function service(string $guid) + public static function service(string $guid, JsonFormatterInterface $formatter = null) { - return static::entity(['service', $guid], 'service'); + return static::entity(['service', $guid], 'service', $formatter); } - public static function product(string $guid) + public static function product(string $guid, JsonFormatterInterface $formatter = null) { - return static::entity(['product', $guid], 'product'); + return static::create([...Product::PATH, $guid], Entity::PRODUCT, $formatter); } - public static function saleschannel(string $guid) + public static function employee(string $guid, JsonFormatterInterface $formatter = null) { - return static::entity(['saleschannel', $guid], 'saleschannel'); + return static::create([...Employee::PATH, $guid], Entity::EMPLOYEE, $formatter); } - public static function currency(string $guid) + public static function customerorder(string $guid, JsonFormatterInterface $formatter = null) { - return static::entity(['currency', $guid], 'currency'); + return static::create([...Customerorder::PATH, $guid], Document::CUSTOMERORDER, $formatter); } - public static function store(string $guid) + public static function saleschannel(string $guid, JsonFormatterInterface $formatter = null) { - return static::entity(['store', $guid], 'store'); + return static::entity(['saleschannel', $guid], 'saleschannel', $formatter); } - public static function counterparty(string $guid) + public static function currency(string $guid, JsonFormatterInterface $formatter = null) { - return static::entity(['counterparty', $guid], 'counterparty'); + return static::entity(['currency', $guid], 'currency', $formatter); } - public static function organization(string $guid) + public static function store(string $guid, JsonFormatterInterface $formatter = null) { - return static::entity(['organization', $guid], 'organization'); + return static::entity(['store', $guid], 'store', $formatter); } - public static function entity(array $path, string $type) + public static function counterparty(string $guid, JsonFormatterInterface $formatter = null) { - return static::create(['entity', ...$path], $type); + return static::entity(['counterparty', $guid], 'counterparty', $formatter); + } + + public static function organization(string $guid, JsonFormatterInterface $formatter = null) + { + return static::entity(['organization', $guid], 'organization', $formatter); + } + + /** @deprecated */ + public static function entity(array $path, string $type, JsonFormatterInterface $formatter = null) + { + return static::create([Endpoint::ENTITY, ...$path], $type, $formatter); } /** * @param string[] $path */ - public static function create(array $path, string $type) + public static function create(array $path, string $type, JsonFormatterInterface $formatter = null) { - static::initFormatter(); + $formatter = $formatter ?? static::$formatter ?? new StdClassFormat(); - return static::$formatter::encode(ArrayFormat::decode([ + return $formatter->encode((new ArrayFormat())->decode([ 'href' => static::makeHref($path), 'type' => $type, 'mediaType' => 'application/json', ])); } + /** + * @deprecated + */ public static function setFormat(JsonFormatterInterface $formatter): void { static::$formatter = $formatter; } - private static function initFormatter(): void - { - static::$formatter = static::$formatter ?? new StdClassFormat(); - } - private static function makeHref(array $path): string { - $href = 'https://online.moysklad.ru/api/remap/1.2'; + $href = Url::API; + $path = array_values($path); foreach ($path as $key => $segment) { if (!is_string($segment)) { throw new InvalidArgumentException("{$key}th segment of path is not a string"); diff --git a/tests/Feature/Api/Builders/Endpoints/AuditTest.php b/tests/Feature/Api/Builders/Endpoints/AuditTest.php deleted file mode 100644 index fb2ada51..00000000 --- a/tests/Feature/Api/Builders/Endpoints/AuditTest.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public function testEndpointBuilder(): void - { - $this->assertNamedEndpointBuilder('audit'); - } -} diff --git a/tests/Feature/Api/Builders/Endpoints/ReportTest.php b/tests/Feature/Api/Builders/Endpoints/ReportTest.php deleted file mode 100644 index 54c75180..00000000 --- a/tests/Feature/Api/Builders/Endpoints/ReportTest.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public function testEndpointBuilder(): void - { - $this->assertNamedEndpointBuilder('report'); - } -} diff --git a/tests/Feature/Api/Builders/Methods/Documents/CustomerorderTest.php b/tests/Feature/Api/Builders/Methods/Documents/CustomerorderTest.php deleted file mode 100644 index 40449a48..00000000 --- a/tests/Feature/Api/Builders/Methods/Documents/CustomerorderTest.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public function testBuilder(): void - { - $this->assertNamedBuilderDebugSame(['entity', 'customerorder']); - } -} diff --git a/tests/Feature/Api/Builders/Methods/Entities/AssortmentTest.php b/tests/Feature/Api/Builders/Methods/Entities/AssortmentTest.php deleted file mode 100644 index b38ad8eb..00000000 --- a/tests/Feature/Api/Builders/Methods/Entities/AssortmentTest.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public function testBuilder(): void - { - $this->assertNamedBuilderDebugSame(['entity', 'assortment']); - } -} diff --git a/tests/Feature/Api/Builders/Methods/Entities/ProductTest.php b/tests/Feature/Api/Builders/Methods/Entities/ProductTest.php deleted file mode 100644 index f164f3bd..00000000 --- a/tests/Feature/Api/Builders/Methods/Entities/ProductTest.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - public function testBuilder(): void - { - $this->assertNamedBuilderDebugSame(['entity', 'product']); - } -} diff --git a/tests/Feature/Api/ApiTestCase.php b/tests/Feature/Api/Query/ApiTestCase.php similarity index 92% rename from tests/Feature/Api/ApiTestCase.php rename to tests/Feature/Api/Query/ApiTestCase.php index 9f188ece..ebc4b798 100644 --- a/tests/Feature/Api/ApiTestCase.php +++ b/tests/Feature/Api/Query/ApiTestCase.php @@ -1,9 +1,9 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\ById\ByIdSegmentCommon */ public function testBuilder(): void { diff --git a/tests/Feature/Api/Builders/ById/ByIdPositionedTest.php b/tests/Feature/Api/Query/Builders/ById/ByIdPositionedTest.php similarity index 74% rename from tests/Feature/Api/Builders/ById/ByIdPositionedTest.php rename to tests/Feature/Api/Query/Builders/ById/ByIdPositionedTest.php index 13a7a9b9..d24883df 100644 --- a/tests/Feature/Api/Builders/ById/ByIdPositionedTest.php +++ b/tests/Feature/Api/Query/Builders/ById/ByIdPositionedTest.php @@ -1,13 +1,13 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\ById\ByIdSegmentPositioned */ public function testBuilder(): void { diff --git a/tests/Feature/Api/Query/Builders/Endpoints/AuditTest.php b/tests/Feature/Api/Query/Builders/Endpoints/AuditTest.php new file mode 100644 index 00000000..4a125ba3 --- /dev/null +++ b/tests/Feature/Api/Query/Builders/Endpoints/AuditTest.php @@ -0,0 +1,16 @@ +assertNamedEndpointBuilder('audit'); + } +} diff --git a/tests/Feature/Api/Builders/Endpoints/EndpointCommonTest.php b/tests/Feature/Api/Query/Builders/Endpoints/EndpointCommonTest.php similarity index 64% rename from tests/Feature/Api/Builders/Endpoints/EndpointCommonTest.php rename to tests/Feature/Api/Query/Builders/Endpoints/EndpointCommonTest.php index ea8b16bc..d07a3fe1 100644 --- a/tests/Feature/Api/Builders/Endpoints/EndpointCommonTest.php +++ b/tests/Feature/Api/Query/Builders/Endpoints/EndpointCommonTest.php @@ -1,13 +1,13 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\Endpoints\EndpointSegmentCommon */ public function testEndpointBuilder(): void { diff --git a/tests/Feature/Api/Builders/Endpoints/EntityTest.php b/tests/Feature/Api/Query/Builders/Endpoints/EntityTest.php similarity index 69% rename from tests/Feature/Api/Builders/Endpoints/EntityTest.php rename to tests/Feature/Api/Query/Builders/Endpoints/EntityTest.php index 964cc7c6..6db8babf 100644 --- a/tests/Feature/Api/Builders/Endpoints/EntityTest.php +++ b/tests/Feature/Api/Query/Builders/Endpoints/EntityTest.php @@ -1,13 +1,13 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\Endpoints\EntitySegment */ public function testEndpointBuilder(): void { @@ -15,7 +15,7 @@ public function testEndpointBuilder(): void } /** - * @covers \Evgeek\Moysklad\Api\Segments\Endpoints\Entity::product + * @covers \Evgeek\Moysklad\Api\Query\Segments\Endpoints\EntitySegment::product */ public function testProductBuilder(): void { @@ -26,7 +26,7 @@ public function testProductBuilder(): void } /** - * @covers \Evgeek\Moysklad\Api\Segments\Endpoints\Entity::customerorder + * @covers \Evgeek\Moysklad\Api\Query\Segments\Endpoints\EntitySegment::customerorder */ public function testCustomerorderBuilder(): void { @@ -37,7 +37,7 @@ public function testCustomerorderBuilder(): void } /** - * @covers \Evgeek\Moysklad\Api\Segments\Endpoints\Entity::assortment + * @covers \Evgeek\Moysklad\Api\Query\Segments\Endpoints\EntitySegment::assortment */ public function testAssortmentBuilder(): void { diff --git a/tests/Feature/Api/Builders/Endpoints/NotificationTest.php b/tests/Feature/Api/Query/Builders/Endpoints/NotificationTest.php similarity index 50% rename from tests/Feature/Api/Builders/Endpoints/NotificationTest.php rename to tests/Feature/Api/Query/Builders/Endpoints/NotificationTest.php index 33a36fe6..e80c7a3a 100644 --- a/tests/Feature/Api/Builders/Endpoints/NotificationTest.php +++ b/tests/Feature/Api/Query/Builders/Endpoints/NotificationTest.php @@ -1,13 +1,13 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\Endpoints\NotificationSegment */ public function testEndpointBuilder(): void { diff --git a/tests/Feature/Api/Query/Builders/Endpoints/ReportTest.php b/tests/Feature/Api/Query/Builders/Endpoints/ReportTest.php new file mode 100644 index 00000000..1299b6c6 --- /dev/null +++ b/tests/Feature/Api/Query/Builders/Endpoints/ReportTest.php @@ -0,0 +1,16 @@ +assertNamedEndpointBuilder('report'); + } +} diff --git a/tests/Feature/Api/Query/Builders/Methods/Documents/CustomerorderTest.php b/tests/Feature/Api/Query/Builders/Methods/Documents/CustomerorderTest.php new file mode 100644 index 00000000..88c67eee --- /dev/null +++ b/tests/Feature/Api/Query/Builders/Methods/Documents/CustomerorderTest.php @@ -0,0 +1,16 @@ +assertNamedBuilderDebugSame(['entity', 'customerorder']); + } +} diff --git a/tests/Feature/Api/Query/Builders/Methods/Entities/AssortmentTest.php b/tests/Feature/Api/Query/Builders/Methods/Entities/AssortmentTest.php new file mode 100644 index 00000000..4588ea25 --- /dev/null +++ b/tests/Feature/Api/Query/Builders/Methods/Entities/AssortmentTest.php @@ -0,0 +1,16 @@ +assertNamedBuilderDebugSame(['entity', 'assortment']); + } +} diff --git a/tests/Feature/Api/Query/Builders/Methods/Entities/ProductTest.php b/tests/Feature/Api/Query/Builders/Methods/Entities/ProductTest.php new file mode 100644 index 00000000..3aa084dd --- /dev/null +++ b/tests/Feature/Api/Query/Builders/Methods/Entities/ProductTest.php @@ -0,0 +1,16 @@ +assertNamedBuilderDebugSame(['entity', 'product']); + } +} diff --git a/tests/Feature/Api/Builders/Methods/MethodCommonTest.php b/tests/Feature/Api/Query/Builders/Methods/MethodCommonTest.php similarity index 51% rename from tests/Feature/Api/Builders/Methods/MethodCommonTest.php rename to tests/Feature/Api/Query/Builders/Methods/MethodCommonTest.php index d7215139..d79eb3a7 100644 --- a/tests/Feature/Api/Builders/Methods/MethodCommonTest.php +++ b/tests/Feature/Api/Query/Builders/Methods/MethodCommonTest.php @@ -1,13 +1,13 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\Methods\Nested\AttributesSegment */ public function testBuilder(): void { diff --git a/tests/Feature/Api/Builders/Methods/Nested/MetadataTest.php b/tests/Feature/Api/Query/Builders/Methods/Nested/MetadataTest.php similarity index 51% rename from tests/Feature/Api/Builders/Methods/Nested/MetadataTest.php rename to tests/Feature/Api/Query/Builders/Methods/Nested/MetadataTest.php index 06c94876..69c2eb07 100644 --- a/tests/Feature/Api/Builders/Methods/Nested/MetadataTest.php +++ b/tests/Feature/Api/Query/Builders/Methods/Nested/MetadataTest.php @@ -1,13 +1,13 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\Methods\Nested\MetadataSegment */ public function testBuilder(): void { diff --git a/tests/Feature/Api/Builders/Methods/Nested/PositionsTest.php b/tests/Feature/Api/Query/Builders/Methods/Nested/PositionsTest.php similarity index 51% rename from tests/Feature/Api/Builders/Methods/Nested/PositionsTest.php rename to tests/Feature/Api/Query/Builders/Methods/Nested/PositionsTest.php index 8067a34b..833c8e9c 100644 --- a/tests/Feature/Api/Builders/Methods/Nested/PositionsTest.php +++ b/tests/Feature/Api/Query/Builders/Methods/Nested/PositionsTest.php @@ -1,13 +1,13 @@ + * @covers \Evgeek\Moysklad\Api\Query\Segments\Methods\Nested\PositionsSegment */ public function testBuilder(): void { diff --git a/tests/Feature/Api/Builders/Methods/Special/DebugTest.php b/tests/Feature/Api/Query/Builders/Methods/Special/DebugTest.php similarity index 89% rename from tests/Feature/Api/Builders/Methods/Special/DebugTest.php rename to tests/Feature/Api/Query/Builders/Methods/Special/DebugTest.php index 616640de..3682cfd7 100644 --- a/tests/Feature/Api/Builders/Methods/Special/DebugTest.php +++ b/tests/Feature/Api/Query/Builders/Methods/Special/DebugTest.php @@ -1,13 +1,13 @@ api = $this->getMockBuilder(ApiClient::class) - ->disableOriginalConstructor() - ->getMock(); + $this->api = $this->createMock(ApiClient::class); } - protected function expectsSendCalledWith(HttpMethod $httpMethod, array $path, array $params, mixed $body = null): void + protected function expectsSendCalledWith(HttpMethod $httpMethod, array $path, array $params, mixed $body = null, mixed $willReturn = null): void { - $this->mockApiClientMethodExpectsPayload('send', $httpMethod, $path, $params, $body); + $this->mockApiClientMethodExpectsPayload('send', $httpMethod, $path, $params, $body, $willReturn); } protected function expectsDebugCalledWith(HttpMethod $httpMethod, array $path, array $params, mixed $body = null): void @@ -35,9 +33,9 @@ protected function expectsGetGeneratorCalledWith(HttpMethod $httpMethod, array $ $this->mockApiClientMethodExpectsPayload('getGenerator', $httpMethod, $path, $params, $body); } - private function mockApiClientMethodExpectsPayload(string $method, HttpMethod $httpMethod, array $path, array $params, mixed $body): void + private function mockApiClientMethodExpectsPayload(string $method, HttpMethod $httpMethod, array $path, array $params, mixed $body, mixed $willReturn = null): void { - $this->api->expects($this->once()) + $mock = $this->api->expects($this->once()) ->method($method) ->with($this->callback( fn (Payload $payload) => $payload->method === $httpMethod @@ -45,5 +43,9 @@ private function mockApiClientMethodExpectsPayload(string $method, HttpMethod $h && $payload->params === $params && $payload->body === $body )); + + if ($willReturn !== null) { + $mock->willReturn($willReturn); + } } } diff --git a/tests/Traits/MoySkladMockerTrait.php b/tests/Traits/MoySkladMockerTrait.php new file mode 100644 index 00000000..5b352761 --- /dev/null +++ b/tests/Traits/MoySkladMockerTrait.php @@ -0,0 +1,31 @@ +createMockApiClient(); + + $this->ms = $this->createMock(MoySklad::class); + $this->ms->method('getApiClient')->willReturn($this->api); + $this->ms->method('meta')->willReturn(new MetaMaker(new ArrayFormat())); + $this->ms->method('record')->willReturn(new RecordBuilder($this->ms)); + $this->ms->method('getFormatter')->willReturn(new ArrayFormat()); + + return $this->ms; + } +} diff --git a/tests/Unit/Api/ApiTestCase.php b/tests/Unit/Api/Query/ApiTestCase.php similarity index 93% rename from tests/Unit/Api/ApiTestCase.php rename to tests/Unit/Api/Query/ApiTestCase.php index 83fe4699..cb8ceeca 100644 --- a/tests/Unit/Api/ApiTestCase.php +++ b/tests/Unit/Api/Query/ApiTestCase.php @@ -1,6 +1,6 @@ */ +/** + * @covers \Evgeek\Moysklad\Api\Query\AbstractBuilder + * @covers \Evgeek\Moysklad\Api\Query\Debug + */ class DebugTest extends ApiTestCase { private Debug $debug; @@ -70,7 +73,7 @@ public function testSendCallsApiClientWithCorrectPayloadFromStringHttpMethod(): public function testCannotSendWrongStringHttpMethod(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("'WRONG-METHOD' is not valid HTTP method. Check Evgeek\\Moysklad\\Enums\\HttpMethod"); + $this->expectExceptionMessage("'WRONG-METHOD' is not valid HTTP method"); $this->debug->send('WRONG-METHOD', static::BODY); } diff --git a/tests/Unit/Api/Query/QueryBuilderTest.php b/tests/Unit/Api/Query/QueryBuilderTest.php new file mode 100644 index 00000000..e40ec7e0 --- /dev/null +++ b/tests/Unit/Api/Query/QueryBuilderTest.php @@ -0,0 +1,116 @@ +builder = new QueryBuilder($this->api); + } + + public function testFromUrlReturnsCorrectClass(): void + { + $builder = $this->builder->fromUrl(Url::API . '/endpoint/segment'); + + $this->assertInstanceOf(MethodSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testFromUrlWithInvalidUrlThrowsError(): void + { + $wrongUrl = 'wrong-url'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Url '$wrongUrl' does not belongs to Moysklad JSON API v1.2"); + + $this->builder->fromUrl($wrongUrl); + } + + public function testFromUrlDropParamsByDefault(): void + { + $ms = new MoySklad(['token']); + $baseUrl = Url::API . '/endpoint/segment'; + $urlWithParams = $baseUrl . '?param=value'; + $url = $ms->query()->fromUrl($urlWithParams)->debug()->get()->url; + + $this->assertSame($baseUrl, $url); + } + + public function testFromUrlPreserveParamsWithFlag(): void + { + $ms = new MoySklad(['token']); + $baseUrl = Url::API . '/endpoint/segment'; + $urlWithParams = $baseUrl . '?param=value'; + $url = $ms->query()->fromUrl($urlWithParams, true)->debug()->get()->url; + + $this->assertSame($urlWithParams, $url); + } + + public function testEndpointReturnsCorrectClass(): void + { + $builder = $this->builder->endpoint('test'); + + $this->assertInstanceOf(EndpointSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testEntityReturnsCorrectClass(): void + { + $builder = $this->builder->entity(); + + $this->assertInstanceOf(EntitySegment::class, $builder); + $this->assertInstanceOf(AbstractEndpointSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testReportReturnsCorrectClass(): void + { + $builder = $this->builder->report(); + + $this->assertInstanceOf(ReportSegment::class, $builder); + $this->assertInstanceOf(AbstractEndpointSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testAuditReturnsCorrectClass(): void + { + $builder = $this->builder->audit(); + + $this->assertInstanceOf(AuditSegment::class, $builder); + $this->assertInstanceOf(AbstractEndpointSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testNotificationReturnsCorrectClass(): void + { + $builder = $this->builder->notification(); + + $this->assertInstanceOf(NotificationSegment::class, $builder); + $this->assertInstanceOf(AbstractEndpointSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Segments/AbstractSegmentCommonTest.php b/tests/Unit/Api/Query/Segments/AbstractSegmentCommonTest.php similarity index 75% rename from tests/Unit/Api/Segments/AbstractSegmentCommonTest.php rename to tests/Unit/Api/Query/Segments/AbstractSegmentCommonTest.php index fd017978..dfbcec9e 100644 --- a/tests/Unit/Api/Segments/AbstractSegmentCommonTest.php +++ b/tests/Unit/Api/Query/Segments/AbstractSegmentCommonTest.php @@ -1,14 +1,14 @@ builder = new EntitySegment($this->api, [], []); + } + + public function testProductReturnsCorrectClass(): void + { + $builder = $this->builder->product(); + + $this->assertInstanceOf(ProductSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testCustomerorderReturnsCorrectClass(): void + { + $builder = $this->builder->customerorder(); + + $this->assertInstanceOf(CustomerorderSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testAssortmentReturnsCorrectClass(): void + { + $builder = $this->builder->assortment(); + + $this->assertInstanceOf(AssortmentSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } + + public function testEmployeeReturnsCorrectClass(): void + { + $builder = $this->builder->employee(); + + $this->assertInstanceOf(EmployeeSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Segments/Special/MassDeleteTest.php b/tests/Unit/Api/Query/Segments/Special/MassDeleteTest.php similarity index 67% rename from tests/Unit/Api/Segments/Special/MassDeleteTest.php rename to tests/Unit/Api/Query/Segments/Special/MassDeleteTest.php index 67cfd262..867446b1 100644 --- a/tests/Unit/Api/Segments/Special/MassDeleteTest.php +++ b/tests/Unit/Api/Query/Segments/Special/MassDeleteTest.php @@ -1,19 +1,19 @@ */ +/** @covers \Evgeek\Moysklad\Api\Query\Segments\Special\MassDeleteSegment */ class MassDeleteTest extends ApiTestCase { private const PATH = [ ...self::PREV_PATH, 'delete', ]; - private MassDelete $builder; + private MassDeleteSegment $builder; protected function setUp(): void { @@ -21,7 +21,7 @@ protected function setUp(): void $this->createMockApiClient(); - $this->builder = new MassDelete($this->api, static::PREV_PATH, static::PARAMS); + $this->builder = new MassDeleteSegment($this->api, static::PREV_PATH, static::PARAMS); } public function testMassDeleteCallsApiClientWithCorrectPayload(): void diff --git a/tests/Unit/Api/Traits/Actions/CreateTraitTest.php b/tests/Unit/Api/Query/Traits/Actions/CreateTraitTest.php similarity index 63% rename from tests/Unit/Api/Traits/Actions/CreateTraitTest.php rename to tests/Unit/Api/Query/Traits/Actions/CreateTraitTest.php index bd0979c7..e9827893 100644 --- a/tests/Unit/Api/Traits/Actions/CreateTraitTest.php +++ b/tests/Unit/Api/Query/Traits/Actions/CreateTraitTest.php @@ -1,13 +1,13 @@ api, static::PREV_PATH, static::PARAMS) extends AbstractSegmentNamed { + use MassCreateUpdateTrait; + + protected const SEGMENT = 'test_segment'; + }; + + $this->expectsSendCalledWith(HttpMethod::POST, static::PATH, static::PARAMS, static::BODY); + $builder->massCreateUpdate(static::BODY); + } +} diff --git a/tests/Unit/Api/Traits/Actions/MassDeleteTraitTest.php b/tests/Unit/Api/Query/Traits/Actions/MassDeleteTraitTest.php similarity index 65% rename from tests/Unit/Api/Traits/Actions/MassDeleteTraitTest.php rename to tests/Unit/Api/Query/Traits/Actions/MassDeleteTraitTest.php index 0b91bbac..62bea04d 100644 --- a/tests/Unit/Api/Traits/Actions/MassDeleteTraitTest.php +++ b/tests/Unit/Api/Query/Traits/Actions/MassDeleteTraitTest.php @@ -1,13 +1,13 @@ expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("'WRONG-METHOD' is not valid HTTP method. Check Evgeek\\Moysklad\\Enums\\HttpMethod"); + $this->expectExceptionMessage("'WRONG-METHOD' is not valid HTTP method"); $builder->send('wrong-method', static::BODY); } } diff --git a/tests/Unit/Api/Traits/Actions/UpdateTraitTest.php b/tests/Unit/Api/Query/Traits/Actions/UpdateTraitTest.php similarity index 63% rename from tests/Unit/Api/Traits/Actions/UpdateTraitTest.php rename to tests/Unit/Api/Query/Traits/Actions/UpdateTraitTest.php index 67635d4c..3cab09ed 100644 --- a/tests/Unit/Api/Traits/Actions/UpdateTraitTest.php +++ b/tests/Unit/Api/Query/Traits/Actions/UpdateTraitTest.php @@ -1,13 +1,13 @@ get(); } - /** @deprecated */ - public function testFilters(): void - { - $params = static::PARAMS + ['filter' => 'filter1=value1;filter2>value2']; - $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, $params); - - $this->makeFilterBuilder() - ->filters([ - ['filter1', 'value1'], - ['filter2', FilterSign::GT, 'value2'], - ]) - ->get(); - } - /** @dataProvider incorrectSignsByTypeDataProvider */ public function testFilterPassedWithNotProperlySignTypeCauseException(mixed $incorrectSign): void { @@ -190,7 +173,7 @@ public function testFilterPassedWithNotProperlySignTypeCauseException(mixed $inc ->get(); } - public function incorrectSignsByTypeDataProvider(): array + public static function incorrectSignsByTypeDataProvider(): array { return [ [1], @@ -201,7 +184,7 @@ public function incorrectSignsByTypeDataProvider(): array ]; } - private function makeFilterBuilder() + private function makeFilterBuilder(): AbstractSegmentCommon { return new class($this->api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { use FilterTrait; diff --git a/tests/Unit/Api/Traits/Params/LimitOffsetTraitTest.php b/tests/Unit/Api/Query/Traits/Params/LimitOffsetTraitTest.php similarity index 82% rename from tests/Unit/Api/Traits/Params/LimitOffsetTraitTest.php rename to tests/Unit/Api/Query/Traits/Params/LimitOffsetTraitTest.php index 4ffbb4b8..06e624ec 100644 --- a/tests/Unit/Api/Traits/Params/LimitOffsetTraitTest.php +++ b/tests/Unit/Api/Query/Traits/Params/LimitOffsetTraitTest.php @@ -1,18 +1,15 @@ '0']; + $params = static::PARAMS + ['Param1' => '0']; $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, $params); $this->makeParamBuilder() @@ -54,7 +51,7 @@ public function testNewUnknownParamRewritesPrevious(): void $this->makeParamBuilder() ->param('param1', 200.0) - ->param('Param1', true) + ->param('param1', true) ->get(); } @@ -74,7 +71,7 @@ public function testNewAddableParamAddedToPrevious(): void $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, $params); $this->makeParamBuilder() - ->param('Order', 'name,desc') + ->param('order', 'name,desc') ->param('order', 'updated_at') ->get(); } @@ -100,7 +97,7 @@ public function testSingleArrayAddableParams(): void $this->makeParamBuilder() ->param('order', 'code') ->param([ - ['Order', 'name,desc'], + ['order', 'name,desc'], ['order', 'updated_at,asc'], ]) ->param('expand', 'group') @@ -131,7 +128,7 @@ public function testArrayParamWithSameSingleParamReplaced(): void ['param1', 0], ['param2', 1.0], ]) - ->param('Param2', 'value') + ->param('param2', 'value') ->get(); } @@ -153,7 +150,7 @@ public function testParamPassedThroughSegments(): void $this->makeParamBuilder() ->param([ - ['Order', 'name,desc'], + ['order', 'name,desc'], ['order', 'updated_at,asc'], ]) ->method('additional_segment') @@ -169,9 +166,9 @@ public function testParamKeysLowered(): void $this->makeParamBuilder() ->param([ ['order', 'name,desc'], - ['Limit', 1], + ['limit', 1], ]) - ->param('OrDeR', 'CODE') + ->param('order', 'CODE') ->get(); } diff --git a/tests/Unit/Api/Traits/Params/SearchTraitTest.php b/tests/Unit/Api/Query/Traits/Params/SearchTraitTest.php similarity index 75% rename from tests/Unit/Api/Traits/Params/SearchTraitTest.php rename to tests/Unit/Api/Query/Traits/Params/SearchTraitTest.php index ca42aae0..6db48406 100644 --- a/tests/Unit/Api/Traits/Params/SearchTraitTest.php +++ b/tests/Unit/Api/Query/Traits/Params/SearchTraitTest.php @@ -1,18 +1,15 @@ api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { + use AttributesTrait; + })->attributes(); + + $this->assertInstanceOf(AttributesSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Query/Traits/Segments/ByIdCommonTraitTest.php b/tests/Unit/Api/Query/Traits/Segments/ByIdCommonTraitTest.php new file mode 100644 index 00000000..f435bef1 --- /dev/null +++ b/tests/Unit/Api/Query/Traits/Segments/ByIdCommonTraitTest.php @@ -0,0 +1,24 @@ +api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { + use ByIdCommonTrait; + })->byId('id'); + + $this->assertInstanceOf(ByIdSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Query/Traits/Segments/ByIdPositionedTraitTest.php b/tests/Unit/Api/Query/Traits/Segments/ByIdPositionedTraitTest.php new file mode 100644 index 00000000..0a04aa44 --- /dev/null +++ b/tests/Unit/Api/Query/Traits/Segments/ByIdPositionedTraitTest.php @@ -0,0 +1,24 @@ +api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { + use ByIdPositionedTrait; + })->byId('id'); + + $this->assertInstanceOf(ByIdSegmentPositioned::class, $builder); + $this->assertInstanceOf(AbstractSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Query/Traits/Segments/MetadataTraitTest.php b/tests/Unit/Api/Query/Traits/Segments/MetadataTraitTest.php new file mode 100644 index 00000000..cde69a6b --- /dev/null +++ b/tests/Unit/Api/Query/Traits/Segments/MetadataTraitTest.php @@ -0,0 +1,25 @@ +api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { + use MetadataTrait; + })->metadata(); + + $this->assertInstanceOf(MetadataSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Query/Traits/Segments/MethodCommonTraitTest.php b/tests/Unit/Api/Query/Traits/Segments/MethodCommonTraitTest.php new file mode 100644 index 00000000..b3eb38e3 --- /dev/null +++ b/tests/Unit/Api/Query/Traits/Segments/MethodCommonTraitTest.php @@ -0,0 +1,24 @@ +api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { + use MethodCommonTrait; + })->method('test_method'); + + $this->assertInstanceOf(MethodSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractSegmentCommon::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Query/Traits/Segments/PositionsTraitTest.php b/tests/Unit/Api/Query/Traits/Segments/PositionsTraitTest.php new file mode 100644 index 00000000..06f220b0 --- /dev/null +++ b/tests/Unit/Api/Query/Traits/Segments/PositionsTraitTest.php @@ -0,0 +1,25 @@ +api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { + use PositionsTrait; + })->positions(); + + $this->assertInstanceOf(PositionsSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Query/Traits/Segments/SettingsTraitTest.php b/tests/Unit/Api/Query/Traits/Segments/SettingsTraitTest.php new file mode 100644 index 00000000..38743fe7 --- /dev/null +++ b/tests/Unit/Api/Query/Traits/Segments/SettingsTraitTest.php @@ -0,0 +1,25 @@ +api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { + use SettingsTrait; + })->settings(); + + $this->assertInstanceOf(SettingsSegment::class, $builder); + $this->assertInstanceOf(AbstractMethodSegmentNamed::class, $builder); + $this->assertInstanceOf(AbstractBuilder::class, $builder); + } +} diff --git a/tests/Unit/Api/Traits/TraitTestCase.php b/tests/Unit/Api/Query/Traits/TraitTestCase.php similarity index 68% rename from tests/Unit/Api/Traits/TraitTestCase.php rename to tests/Unit/Api/Query/Traits/TraitTestCase.php index 5b611741..2a8d42ee 100644 --- a/tests/Unit/Api/Traits/TraitTestCase.php +++ b/tests/Unit/Api/Query/Traits/TraitTestCase.php @@ -1,8 +1,8 @@ */ -class QueryTest extends ApiTestCase -{ - private Query $builder; - - protected function setUp(): void - { - parent::setUp(); - - $this->builder = new Query($this->api); - } - - public function testEndpointReturnsCorrectClass(): void - { - $builder = $this->builder->endpoint('test'); - - $this->assertInstanceOf(EndpointCommon::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } - - public function testEntityReturnsCorrectClass(): void - { - $builder = $this->builder->entity(); - - $this->assertInstanceOf(Entity::class, $builder); - $this->assertInstanceOf(AbstractEndpointNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } - - public function testReportReturnsCorrectClass(): void - { - $builder = $this->builder->report(); - - $this->assertInstanceOf(Report::class, $builder); - $this->assertInstanceOf(AbstractEndpointNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } - - public function testAuditReturnsCorrectClass(): void - { - $builder = $this->builder->audit(); - - $this->assertInstanceOf(Audit::class, $builder); - $this->assertInstanceOf(AbstractEndpointNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } - - public function testNotificationReturnsCorrectClass(): void - { - $builder = $this->builder->notification(); - - $this->assertInstanceOf(Notification::class, $builder); - $this->assertInstanceOf(AbstractEndpointNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Api/Record/AbstractRecordTest.php b/tests/Unit/Api/Record/AbstractRecordTest.php new file mode 100644 index 00000000..02ecfe63 --- /dev/null +++ b/tests/Unit/Api/Record/AbstractRecordTest.php @@ -0,0 +1,157 @@ + 'value', + 'array_key' => ['inner_key' => 'inner_value'], + ]; + + public function testEmptyPathThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('path and type cannot be empty'); + + $this->getUnknownObject([], 'type'); + } + + public function testEmptyTypeThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('path and type cannot be empty'); + + $this->getUnknownObject(['path'], ''); + } + + public function testMetaFilledFromConstants(): void + { + $object = $this->getConcreteObject(); + + $this->assertSame('unknown-type', $object->meta->type); + $this->assertStringEndsWith('endpoint/segment', $object->meta->href); + } + + public function testGetExistingProperty(): void + { + $key = 'test_key'; + $value = 'test_value'; + + $object = $this->getConcreteObject([$key => $value]); + + $this->assertSame($value, $object->{$key}); + } + + public function testGetNotExistingProperty(): void + { + $object = $this->getConcreteObject(); + + $this->assertNull($object->unknown_key); + } + + public function testSetMethodFillProperty(): void + { + $key = 'test_key'; + $value = 'test_value'; + + $object = $this->getConcreteObject(); + + $this->assertNull($object->{$value}); + + $object->{$key} = $value; + + $this->assertSame($value, $object->{$key}); + } + + public function testIssetAndUnsetWorks(): void + { + $key = 'test_key'; + $value = 'test_value'; + + $object = $this->getConcreteObject([$key => $value]); + + $this->assertTrue(isset($object->{$key})); + + unset($object->{$key}); + + $this->assertFalse(isset($object->{$key})); + } + + public function testToArray(): void + { + $object = $this->getConcreteObjectWithExpectedContent(); + $expected = $this->getExpectedObjectContentAsArray(); + + $this->assertSame($expected, $object->toArray()); + } + + public function testToString(): void + { + $object = $this->getConcreteObjectWithExpectedContent(); + $expected = json_encode($this->getExpectedObjectContentAsArray(), JSON_THROW_ON_ERROR); + + $this->assertSame($expected, $object->toString()); + } + + public function testToStdClass(): void + { + $object = $this->getConcreteObjectWithExpectedContent(); + $expected = $this->getExpectedObjectContentAsArray(); + + $result = $object->toStdClass(); + $resultAsString = json_encode($result, JSON_THROW_ON_ERROR); + $resultAsArray = json_decode($resultAsString, true, 512, JSON_THROW_ON_ERROR); + + $this->assertSame($expected, $resultAsArray); + } + + private function getConcreteObjectWithExpectedContent(): AbstractConcreteRecord + { + $ms = new MoySklad(['token'], new RecordFormat()); + + return $this->getConcreteObject(self::CONTENT, $ms); + } + + private function getExpectedObjectContentAsArray(): array + { + $expected = self::CONTENT; + $expected['meta'] = Meta::create(['endpoint', 'segment'], 'unknown-type', new ArrayFormat()); + + return $expected; + } + + private function getConcreteObject(array $content = [], MoySklad $ms = new MoySklad(['token'])): AbstractConcreteRecord + { + return new class($ms, $content) extends AbstractConcreteRecord { + use FillMetaCollectionTrait; + + public const PATH = ['endpoint', 'segment']; + public const TYPE = 'unknown-type'; + }; + } + + private function getUnknownObject(array $path, string $type): void + { + new class(new MoySklad(['token']), $path, $type, []) extends AbstractUnknownRecord { + use FillMetaCollectionTrait; + }; + } +} diff --git a/tests/Unit/Api/Record/Builders/CollectionBuilderTest.php b/tests/Unit/Api/Record/Builders/CollectionBuilderTest.php new file mode 100644 index 00000000..6fc16a7a --- /dev/null +++ b/tests/Unit/Api/Record/Builders/CollectionBuilderTest.php @@ -0,0 +1,77 @@ +builder = new CollectionBuilder(new MoySklad(['token'])); + } + + public function testResolvingUnregisteredCollectionThrowsException(): void + { + $mapping = new RecordMapping([], []); + $ms = new MoySklad(['token'], new RecordFormat($mapping)); + $builder = new CollectionBuilder($ms); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Collection type 'product' has no mapped class"); + + $builder->product(); + } + + public function testUnknownMethod(): void + { + $path = ['endpoint', 'segment']; + $type = 'unknown_type'; + $unknown = $this->builder->unknown($path, $type); + + $this->assertObjectResolvedWithExpectedMetaAndContent($unknown, UnknownCollection::class, $path, $type); + } + + public function testProductMethod(): void + { + $product = $this->builder->product(); + + $this->assertObjectResolvedWithExpectedMetaAndContent($product, ProductCollection::class); + } + + public function testEmployeeMethod(): void + { + $product = $this->builder->employee(); + + $this->assertObjectResolvedWithExpectedMetaAndContent($product, EmployeeCollection::class); + } + + public function testAssortmentMethod(): void + { + $product = $this->builder->assortment(); + + $this->assertObjectResolvedWithExpectedMetaAndContent($product, AssortmentCollection::class); + } + + public function testCustomerorderMethod(): void + { + $product = $this->builder->customerorder(); + + $this->assertObjectResolvedWithExpectedMetaAndContent($product, CustomerorderCollection::class); + } +} diff --git a/tests/Unit/Api/Record/Builders/ObjectBuilderTest.php b/tests/Unit/Api/Record/Builders/ObjectBuilderTest.php new file mode 100644 index 00000000..e5a3c0f4 --- /dev/null +++ b/tests/Unit/Api/Record/Builders/ObjectBuilderTest.php @@ -0,0 +1,69 @@ +builder = new ObjectBuilder(new MoySklad(['token'])); + } + + public function testResolvingUnregisteredObjectThrowsException(): void + { + $mapping = new RecordMapping([], []); + $ms = new MoySklad(['token'], new RecordFormat($mapping)); + $builder = new ObjectBuilder($ms); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Object type 'product' has no mapped class"); + + $builder->product(); + } + + public function testProductMethod(): void + { + $product = $this->builder->product(static::CONTENT); + + $this->assertObjectResolvedWithExpectedMetaAndContent($product, Product::class); + } + + public function testEmployeeMethod(): void + { + $product = $this->builder->employee(static::CONTENT); + + $this->assertObjectResolvedWithExpectedMetaAndContent($product, Employee::class); + } + + public function testCustomerorderMethod(): void + { + $product = $this->builder->customerorder(static::CONTENT); + + $this->assertObjectResolvedWithExpectedMetaAndContent($product, Customerorder::class); + } + + public function testUnknownMethod(): void + { + $path = ['endpoint', 'segment']; + $type = 'unknown_type'; + $unknown = $this->builder->unknown($path, $type, static::CONTENT); + + $this->assertObjectResolvedWithExpectedMetaAndContent($unknown, UnknownObject::class, $path, $type); + } +} diff --git a/tests/Unit/Api/Record/Builders/RecordBuilderTest.php b/tests/Unit/Api/Record/Builders/RecordBuilderTest.php new file mode 100644 index 00000000..4d72002a --- /dev/null +++ b/tests/Unit/Api/Record/Builders/RecordBuilderTest.php @@ -0,0 +1,37 @@ +builder = new RecordBuilder(new MoySklad(['token'])); + } + + public function testSingleMethodReturnsSingleBuilder(): void + { + $single = $this->builder->object(); + + $this->assertInstanceOf(ObjectBuilder::class, $single); + $this->assertInstanceOf(AbstractBuilder::class, $single); + } + + public function testCollectionMethodReturnsSingleBuilder(): void + { + $single = $this->builder->collection(); + + $this->assertInstanceOf(CollectionBuilder::class, $single); + $this->assertInstanceOf(AbstractBuilder::class, $single); + } +} diff --git a/tests/Unit/Api/Record/Builders/RecordResolversTestCase.php b/tests/Unit/Api/Record/Builders/RecordResolversTestCase.php new file mode 100644 index 00000000..de1bc16a --- /dev/null +++ b/tests/Unit/Api/Record/Builders/RecordResolversTestCase.php @@ -0,0 +1,37 @@ + 'test_value', + 'second_key' => false, + ]; + + /** @param class-string $expectedObjectClass */ + protected function assertObjectResolvedWithExpectedMetaAndContent( + AbstractRecord $object, + string $expectedObjectClass, + array $path = null, + string $type = null, + ): void { + $this->assertInstanceOf($expectedObjectClass, $object); + + $type = $type ?? $expectedObjectClass::TYPE; + $this->assertSame($type, $object->meta->type); + + $path = $path ?? $expectedObjectClass::PATH; + $this->assertStringEndsWith(implode('/', $path), $object->meta->href); + + if (is_subclass_of($expectedObjectClass, AbstractConcreteObject::class)) { + foreach (static::CONTENT as $key => $value) { + $this->assertSame($value, $object->{$key}); + } + } + } +} diff --git a/tests/Unit/Api/Record/Collections/Traits/CollectionTraitCase.php b/tests/Unit/Api/Record/Collections/Traits/CollectionTraitCase.php new file mode 100644 index 00000000..9ce59a66 --- /dev/null +++ b/tests/Unit/Api/Record/Collections/Traits/CollectionTraitCase.php @@ -0,0 +1,33 @@ +createMockMoySkladWithMockedApiClient(); + } + + protected function getTestCollection(array $content = []): AbstractConcreteCollection + { + return new class($this->ms, $content) extends AbstractConcreteCollection { + public const PATH = CollectionTraitCase::PATH; + public const TYPE = CollectionTraitCase::TYPE; + }; + } +} diff --git a/tests/Unit/Api/Record/Collections/Traits/CrudCollectionTraitTest.php b/tests/Unit/Api/Record/Collections/Traits/CrudCollectionTraitTest.php new file mode 100644 index 00000000..297599f7 --- /dev/null +++ b/tests/Unit/Api/Record/Collections/Traits/CrudCollectionTraitTest.php @@ -0,0 +1,153 @@ +getTestCollection(); + $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, ['limit' => '100'], [], ['rows' => '']); + + $collection->limit(100)->get(); + } + + public function testMassCreateUpdateMethodCallsSendWithExpectedParams(): void + { + $context = [ + 'employee' => [ + 'meta' => $this->ms->meta()->employee(static::GUID), + ], + ]; + $content = [ + [ + 'meta' => $this->ms->meta()->create(static::PATH, static::TYPE), + 'name' => 'create entity', + ], + [ + 'meta' => $this->ms->meta()->create([...static::PATH, static::GUID], static::TYPE), + 'id' => static::GUID, + 'name' => 'update entity', + ], + ]; + $collection = $this->getTestCollection(['context' => $context]); + $this->expectsSendCalledWith(HttpMethod::POST, static::PATH, [], $content); + + $collection->massCreateUpdate($content); + + $this->assertSame($context, $collection->toArray()['context']); + } + + public function testMassCreateUpdateMethodCallsSendWithExpectedParamsFromCollection(): void + { + $content = [ + [ + 'meta' => $this->ms->meta()->create(static::PATH, static::TYPE), + 'name' => 'create entity', + ], + [ + 'meta' => $this->ms->meta()->create([...static::PATH, static::GUID], static::TYPE), + 'id' => static::GUID, + 'name' => 'update entity', + ], + ]; + $ms = new MoySklad(['token']); + $updatableCollection = new UnknownCollection($ms, static::PATH, static::TYPE, ['rows' => $content]); + $collection = $this->getTestCollection(); + $this->expectsSendCalledWith(HttpMethod::POST, static::PATH, [], $updatableCollection->rows); + + $collection->massCreateUpdate($updatableCollection); + } + + public function testMassDeleteMethodCallsSendWithExpectedParams(): void + { + $content = [ + [ + 'meta' => $this->ms->meta()->create([...static::PATH, static::GUID], static::TYPE), + 'id' => static::GUID, + 'name' => 'update entity', + ], + ]; + $collection = $this->getTestCollection(); + $this->expectsSendCalledWith(HttpMethod::POST, [...static::PATH, 'delete'], [], $content); + + $collection->massDelete($content); + } + + public function testMassDeleteMethodCallsSendWithExpectedParamsFromCollection(): void + { + $content = [ + [ + 'meta' => $this->ms->meta()->create([...static::PATH, static::GUID], static::TYPE), + 'id' => static::GUID, + 'name' => 'update entity', + ], + ]; + $ms = new MoySklad(['token']); + $deletableCollection = new UnknownCollection($ms, static::PATH, static::TYPE, ['rows' => $content]); + $collection = $this->getTestCollection(); + $this->expectsSendCalledWith(HttpMethod::POST, [...static::PATH, 'delete'], [], $deletableCollection->rows); + + $collection->massDelete($deletableCollection); + } + + public function testGetNextMethodWithNext(): void + { + $params = ['limit' => '100', 'offset' => '100']; + $meta = $this->ms->meta()->create(static::PATH, static::TYPE); + $meta['nextHref'] = Url::makeFromPathAndParams(static::PATH, $params); + $collection = $this->getTestCollection(['meta' => $meta]); + + $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, $params, [], ['rows' => '']); + $collection->getNext(); + } + + public function testGetNextMethodWithoutNext(): void + { + $collection = $this->getTestCollection(); + + $this->assertNull($collection->getNext()); + } + + public function testGetPrevMethodWithPrev(): void + { + $newParams = ['limit' => '100', 'offset' => '100']; + $meta = $this->ms->meta()->create(static::PATH, static::TYPE); + [$path] = Url::parsePathAndParams($meta['href']); + $meta['href'] = Url::makeFromPathAndParams($path, $newParams); + $meta['previousHref'] = Url::makeFromPathAndParams(static::PATH, []); + $collection = $this->getTestCollection(['meta' => $meta]); + + $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, [], [], ['rows' => '']); + + $collection->getPrevious(); + } + + public function testGetPrevMethodWithoutPrev(): void + { + $collection = $this->getTestCollection(); + + $this->assertNull($collection->getPrevious()); + } + + public function testReceivedNotCollectionThrows() + { + $collection = $this->getTestCollection(); + $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, [], []); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Response must be a collection, object received'); + $collection->get(); + } +} diff --git a/tests/Unit/Api/Record/Collections/Traits/FillMetaCollectionTraitTest.php b/tests/Unit/Api/Record/Collections/Traits/FillMetaCollectionTraitTest.php new file mode 100644 index 00000000..28d778b5 --- /dev/null +++ b/tests/Unit/Api/Record/Collections/Traits/FillMetaCollectionTraitTest.php @@ -0,0 +1,33 @@ + 'random_value']; + $collection = $this->getTestCollection(['meta' => $meta]); + + $this->assertSame($meta, $collection->toArray()['meta']); + } + + public function testNotExistingMetaMakesFromPathAndType(): void + { + $collection = $this->getTestCollection(); + $expectedMeta = [ + 'href' => Url::API . '/' . implode('/', static::PATH), + 'type' => static::TYPE, + 'mediaType' => 'application/json', + ]; + + $this->assertSame($expectedMeta, $collection->toArray()['meta']); + } +} diff --git a/tests/Unit/Api/Record/Collections/Traits/IterateCollectionTraitTest.php b/tests/Unit/Api/Record/Collections/Traits/IterateCollectionTraitTest.php new file mode 100644 index 00000000..77487bc8 --- /dev/null +++ b/tests/Unit/Api/Record/Collections/Traits/IterateCollectionTraitTest.php @@ -0,0 +1,110 @@ +ms->record()->object()->product(['id' => static::GUID, 'name' => 'product1']), + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product2']), + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product3']), + ]; + $collection = $this->getTestCollection(['rows' => $objects]); + + $counter = 0; + $collection->each(function (Product $product) use (&$counter) { + ++$counter; + $this->assertSame("product$counter", $product->name); + }); + + $this->assertSame(3, $counter); + } + + public function testEachGeneratorCallsApiAsExpected(): void + { + $meta = $this->ms->meta()->create(static::PATH, static::TYPE); + [$path] = Url::parsePathAndParams($meta['href']); + $meta['nextHref'] = Url::makeFromPathAndParams($path, ['limit' => 2, 'offset' => 2]); + $this->api + ->expects($this->exactly(2)) + ->method('send') + ->willReturnCallback(fn (Payload $payload) => match (true) { + $payload->params === ['limit' => '2', 'offset' => '0'] => [ + 'meta' => $meta, + 'rows' => [ + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product1']), + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product2']), + ], + ], + $payload->params === ['limit' => '2', 'offset' => '2'] => [ + 'rows' => [ + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product3']), + ], + ], + default => throw new RuntimeException('Incorrect payload') + }); + + $collection = $this->getTestCollection()->limit(2)->offset(0); + + $counter = 0; + $collection->eachGenerator(function (Product $product) use (&$counter) { + ++$counter; + $this->assertSame("product$counter", $product->name); + }); + + $this->assertSame(3, $counter); + } + + public function testEachCollectionGeneratorCallsApiAsExpected(): void + { + $meta = $this->ms->meta()->create(Product::PATH, Product::TYPE); + [$path] = Url::parsePathAndParams($meta['href']); + $meta['nextHref'] = Url::makeFromPathAndParams($path, ['limit' => 2, 'offset' => 2]); + $this->api + ->expects($this->exactly(2)) + ->method('send') + ->willReturnCallback(fn (Payload $payload) => match (true) { + $payload->params === ['limit' => '2', 'offset' => '0'] => [ + 'meta' => $meta, + 'rows' => [ + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product1']), + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product2']), + ], + ], + $payload->params === ['limit' => '2', 'offset' => '2'] => [ + 'rows' => [ + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product3']), + ], + ], + default => throw new RuntimeException('Incorrect payload') + }); + + $collection = (new ProductCollection($this->ms))->limit(2)->offset(0); + + $collectionCount = 0; + $productsCount = 0; + $collection->eachCollectionGenerator(function (ProductCollection $products) use (&$collectionCount, &$productsCount) { + ++$collectionCount; + $products->each(function (Product $product) use (&$productsCount) { + ++$productsCount; + $this->assertSame("product$productsCount", $product->name); + }); + }); + + $this->assertSame(2, $collectionCount); + $this->assertSame(3, $productsCount); + } +} diff --git a/tests/Unit/Api/Record/Collections/Traits/IteratorTraitTest.php b/tests/Unit/Api/Record/Collections/Traits/IteratorTraitTest.php new file mode 100644 index 00000000..1dc135de --- /dev/null +++ b/tests/Unit/Api/Record/Collections/Traits/IteratorTraitTest.php @@ -0,0 +1,30 @@ +ms->record()->object()->product(['id' => static::GUID, 'name' => 'product1']), + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product2']), + $this->ms->record()->object()->product(['id' => static::GUID, 'name' => 'product3']), + ]; + $collection = $this->getTestCollection(['rows' => $objects]); + + $counter = 0; + foreach ($collection as $product) { + $this->assertSame($counter, $collection->key()); + ++$counter; + $this->assertSame("product$counter", $product->name); + } + + $this->assertSame(3, $counter); + } +} diff --git a/tests/Unit/Api/Record/Collections/Traits/ParamsCollectionTraitTest.php b/tests/Unit/Api/Record/Collections/Traits/ParamsCollectionTraitTest.php new file mode 100644 index 00000000..e5573b6d --- /dev/null +++ b/tests/Unit/Api/Record/Collections/Traits/ParamsCollectionTraitTest.php @@ -0,0 +1,221 @@ +expectedUrl = Url::API . '/' . implode('/', static::PATH); + } + + public function testSingleExpandMethod(): void + { + $collection = $this->getTestCollection() + ->expand('field1'); + $this->expectedUrl .= '?expand=field1'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testMultipleExpandMethods(): void + { + $collection = $this->getTestCollection() + ->expand('field1') + ->expand('field2'); + $this->expectedUrl .= '?expand=' . urlencode('field1,field2'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testExpandMethodWithMultipleExpands(): void + { + $collection = $this->getTestCollection() + ->expand(['field1', 'field2']) + ->expand('field3'); + $this->expectedUrl .= '?expand=' . urlencode('field1,field2,field3'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSingleFilterMethod(): void + { + $collection = $this->getTestCollection() + ->filter('field1', true); + $this->expectedUrl .= '?filter=' . urlencode('field1=true'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testMultipleFilterMethods(): void + { + $collection = $this->getTestCollection() + ->filter('field1', '!=', 123) + ->filter('field2', FilterSign::GTE, 'text'); + $this->expectedUrl .= '?filter=' . urlencode('field1!=123;field2>=text'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testFilterMethodWithMultipleFilters(): void + { + $collection = $this->getTestCollection() + ->filter([ + ['field2', true], + ['field3', '!!!', 123.45], + ]) + ->filter('field1', FilterSign::LT, 'value'); + $this->expectedUrl .= '?filter=' . urlencode('field2=true;field3!!!123.45;field1assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSingleLimitMethod(): void + { + $collection = $this->getTestCollection() + ->limit(111); + $this->expectedUrl .= '?limit=111'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSecondLimitMethodReplacePrevious(): void + { + $collection = $this->getTestCollection() + ->limit(100) + ->limit(200); + $this->expectedUrl .= '?limit=200'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSingleOffsetMethod(): void + { + $collection = $this->getTestCollection() + ->offset(111); + $this->expectedUrl .= '?offset=111'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSecondOffsetMethodReplacePrevious(): void + { + $collection = $this->getTestCollection() + ->offset(100) + ->offset(200); + $this->expectedUrl .= '?offset=200'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSingleOrderMethod(): void + { + $collection = $this->getTestCollection() + ->order('field'); + $this->expectedUrl .= '?order=' . urlencode('field,asc'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testMultipleOrderMethods(): void + { + $collection = $this->getTestCollection() + ->order('field1', 'desc') + ->order('field2', OrderDirection::ASC); + $this->expectedUrl .= '?order=' . urlencode('field1,desc;field2,asc'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testOrderMethodWithMultipleOrders(): void + { + $collection = $this->getTestCollection() + ->order([ + ['field2'], + ['field3', 'asc'], + ]) + ->order('field1', OrderDirection::DESC); + $this->expectedUrl .= '?order=' . urlencode('field2,asc;field3,asc;field1,desc'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSingleSearchMethod(): void + { + $collection = $this->getTestCollection() + ->search('value'); + $this->expectedUrl .= '?search=value'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSecondSearchMethodReplacePrevious(): void + { + $collection = $this->getTestCollection() + ->search('value1') + ->search('value2'); + $this->expectedUrl .= '?search=value2'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testSingleParamMethod(): void + { + $collection = $this->getTestCollection() + ->param('field', 'value'); + $this->expectedUrl .= '?field=value'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testMultipleParamMethodsWithReplacedParam(): void + { + $collection = $this->getTestCollection() + ->param('field', 'value1') + ->param('field', 'value2'); + $this->expectedUrl .= '?field=value2'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testMultipleParamMethodsWithAddedParam(): void + { + $collection = $this->getTestCollection() + ->param('filter', 'key1=value1') + ->param('filter', 'key2=value2'); + $this->expectedUrl .= '?filter=' . urlencode('key1=value1;key2=value2'); + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } + + public function testParamMethodsWithMultipleDifferentParams(): void + { + $collection = $this->getTestCollection() + ->param([ + ['filter', 'key1=value1'], + ['expand', 'field1'], + ['expand', 'field2'], + ['param', 'value1'], + ['param', 'value2'], + ]) + ->param('filter', 'key2=value2'); + $this->expectedUrl .= '?filter=' . urlencode('key1=value1;key2=value2') . + '&expand=' . urlencode('field1,field2') . + '¶m=value2'; + + $this->assertSame($this->expectedUrl, $collection->meta->href); + } +} diff --git a/tests/Unit/Api/Record/Objects/AbstractConcreteObjectTest.php b/tests/Unit/Api/Record/Objects/AbstractConcreteObjectTest.php new file mode 100644 index 00000000..bb697e57 --- /dev/null +++ b/tests/Unit/Api/Record/Objects/AbstractConcreteObjectTest.php @@ -0,0 +1,60 @@ + $class + * + * @dataProvider classAndCollection + */ + public function testMakeReturnsSameClassWithExpectedContent(string $class): void + { + $content = [ + 'name' => 'test_name', + 'archived' => true, + ]; + + $object = $class::make(new MoySklad(['token']), $content); + + $this->assertInstanceOf($class, $object); + + foreach ($content as $key => $value) { + $this->assertSame($value, $object->{$key}); + } + } + + /** + * @param class-string $class + * + * @dataProvider classAndCollection + */ + public function testCollectionReturnsExpectedDefaultCollection(string $class, string $collection): void + { + $result = $class::collection(new MoySklad(['token'])); + + $this->assertInstanceOf($collection, $result); + } + + public static function classAndCollection(): array + { + return [ + [Product::class, ProductCollection::class], + [Employee::class, EmployeeCollection::class], + ]; + } +} diff --git a/tests/Unit/Api/Record/Objects/Traits/CrudObjectTraitTest.php b/tests/Unit/Api/Record/Objects/Traits/CrudObjectTraitTest.php new file mode 100644 index 00000000..5793fc53 --- /dev/null +++ b/tests/Unit/Api/Record/Objects/Traits/CrudObjectTraitTest.php @@ -0,0 +1,56 @@ +getTestObject(['id' => static::GUID]); + $this->expectsSendCalledWith(HttpMethod::GET, [...static::PATH, static::GUID], ['expand' => 'field1'], $object); + + $object->expand('field1')->get(); + } + + public function testCreateMethodCallsSendWithExpectedParams(): void + { + $object = $this->getTestObject(); + $this->expectsSendCalledWith(HttpMethod::POST, static::PATH, [], $object); + + $object->create(); + } + + public function testUpdateMethodCallsSendWithExpectedParams(): void + { + $object = $this->getTestObject(['id' => static::GUID]); + $this->expectsSendCalledWith(HttpMethod::PUT, [...static::PATH, static::GUID], [], $object); + + $object->update(); + } + + public function testDeleteMethodCallsSendWithExpectedParams(): void + { + $object = $this->getTestObject(['id' => static::GUID]); + $this->expectsSendCalledWith(HttpMethod::DELETE, [...static::PATH, static::GUID], [], $object); + + $object->delete(); + } + + public function testReceivedNotCollectionThrows() + { + $object = $this->getTestObject(); + $this->expectsSendCalledWith(HttpMethod::GET, static::PATH, [], $object, ['rows' => []]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Response must be an object, collection received'); + $object->get(); + } +} diff --git a/tests/Unit/Api/Record/Objects/Traits/FillMetaObjectTraitTest.php b/tests/Unit/Api/Record/Objects/Traits/FillMetaObjectTraitTest.php new file mode 100644 index 00000000..687be094 --- /dev/null +++ b/tests/Unit/Api/Record/Objects/Traits/FillMetaObjectTraitTest.php @@ -0,0 +1,45 @@ +ms->meta()->create(['random', 'path'], 'random_type'); + $object = $this->getTestObject(['meta' => $meta]); + + $this->assertSame($meta, (array) $object->meta); + } + + public function testNotExistingMetaWithoutIdMakesFromPathAndType(): void + { + $object = $this->getTestObject(); + $expectedMeta = [ + 'href' => Url::API . '/' . implode('/', static::PATH), + 'type' => static::TYPE, + 'mediaType' => 'application/json', + ]; + + $this->assertSame($expectedMeta, (array) $object->meta); + } + + public function testNotExistingMetaWithIdMakesFromPathAndTypeAndId(): void + { + $object = $this->getTestObject(['id' => static::GUID]); + $expectedMeta = [ + 'href' => Url::API . '/' . implode('/', [...static::PATH, static::GUID]), + 'type' => static::TYPE, + 'mediaType' => 'application/json', + ]; + + $this->assertSame($expectedMeta, (array) $object->meta); + } +} diff --git a/tests/Unit/Api/Record/Objects/Traits/ObjectTraitCase.php b/tests/Unit/Api/Record/Objects/Traits/ObjectTraitCase.php new file mode 100644 index 00000000..9dd2dbb4 --- /dev/null +++ b/tests/Unit/Api/Record/Objects/Traits/ObjectTraitCase.php @@ -0,0 +1,33 @@ +createMockMoySkladWithMockedApiClient(); + } + + protected function getTestObject(array $content = []): AbstractConcreteObject + { + return new class($this->ms, $content) extends AbstractConcreteObject { + public const PATH = CrudObjectTraitTest::PATH; + public const TYPE = CrudObjectTraitTest::TYPE; + }; + } +} diff --git a/tests/Unit/Api/Record/Objects/Traits/ParamsObjectTraitTest.php b/tests/Unit/Api/Record/Objects/Traits/ParamsObjectTraitTest.php new file mode 100644 index 00000000..416dfc4b --- /dev/null +++ b/tests/Unit/Api/Record/Objects/Traits/ParamsObjectTraitTest.php @@ -0,0 +1,98 @@ +expectedUrl = Url::API . '/' . implode('/', [...static::PATH, static::GUID]); + } + + public function testSingleExpandMethod(): void + { + $object = $this->getTestObject(['id' => static::GUID]) + ->expand('field1'); + $this->expectedUrl .= '?expand=field1'; + + $this->assertSame($this->expectedUrl, $object->meta->href); + } + + public function testMultipleExpandMethods(): void + { + $object = $this->getTestObject(['id' => static::GUID]) + ->expand('field1') + ->expand('field2'); + $this->expectedUrl .= '?expand=' . urlencode('field1,field2'); + + $this->assertSame($this->expectedUrl, $object->meta->href); + } + + public function testExpandMethodWithMultipleExpands(): void + { + $object = $this->getTestObject(['id' => static::GUID]) + ->expand(['field1', 'field2']) + ->expand('field3'); + $this->expectedUrl .= '?expand=' . urlencode('field1,field2,field3'); + + $this->assertSame($this->expectedUrl, $object->meta->href); + } + + public function testSingleParamMethod(): void + { + $object = $this->getTestObject(['id' => static::GUID]) + ->param('field', 'value'); + $this->expectedUrl .= '?field=value'; + + $this->assertSame($this->expectedUrl, $object->meta->href); + } + + public function testMultipleParamMethodsWithReplacedParam(): void + { + $object = $this->getTestObject(['id' => static::GUID]) + ->param('field', 'value1') + ->param('field', 'value2'); + $this->expectedUrl .= '?field=value2'; + + $this->assertSame($this->expectedUrl, $object->meta->href); + } + + public function testMultipleParamMethodsWithAddedParam(): void + { + $object = $this->getTestObject(['id' => static::GUID]) + ->param('filter', 'key1=value1') + ->param('filter', 'key2=value2'); + $this->expectedUrl .= '?filter=' . urlencode('key1=value1;key2=value2'); + + $this->assertSame($this->expectedUrl, $object->meta->href); + } + + public function testParamMethodsWithMultipleDifferentParams(): void + { + $object = $this->getTestObject(['id' => static::GUID]) + ->param([ + ['filter', 'key1=value1'], + ['expand', 'field1'], + ['expand', 'field2'], + ['param', 'value1'], + ['param', 'value2'], + ]) + ->param('filter', 'key2=value2'); + $this->expectedUrl .= '?filter=' . urlencode('key1=value1;key2=value2') . + '&expand=' . urlencode('field1,field2') . + '¶m=value2'; + + $this->assertSame($this->expectedUrl, $object->meta->href); + } +} diff --git a/tests/Unit/Api/Record/Objects/Traits/SetIdInMetaHrefTraitTest.php b/tests/Unit/Api/Record/Objects/Traits/SetIdInMetaHrefTraitTest.php new file mode 100644 index 00000000..8f60114a --- /dev/null +++ b/tests/Unit/Api/Record/Objects/Traits/SetIdInMetaHrefTraitTest.php @@ -0,0 +1,72 @@ +expectedUrl = Url::API . '/' . implode('/', static::PATH); + } + + public function testSetNotGuidId(): void + { + $object = $this->getTestObject(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('id must be a guid'); + + $object->id = 'wrong-guid'; + } + + public function testSetIdInsteadOfEmptyIdAddedIdToPath(): void + { + $object = $this->getTestObject(); + $this->assertSame($this->expectedUrl, $object->meta->href); + + $object->id = static::GUID; + $this->assertSame($this->expectedUrl . '/' . static::GUID, $object->meta->href); + } + + public function testSetIdInsteadOfAnotherIdReplacedAnotherIdInPath(): void + { + $object = $this->getTestObject(['id' => static::GUID]); + $this->assertSame($this->expectedUrl . '/' . static::GUID, $object->meta->href); + + $object->id = static::GUID2; + $this->assertSame($this->expectedUrl . '/' . static::GUID2, $object->meta->href); + } + + /** @dataProvider propertyNames */ + public function testSettingPropertyToNullUnsetIt(string $name): void + { + $object = $this->getTestObject([$name => static::GUID]); + $this->assertTrue(isset($object->{$name})); + + $object->{$name} = null; + $this->assertFalse(isset($object->{$name})); + } + + public static function propertyNames(): array + { + return [ + ['id'], + ['name'], + ['random-property'], + ]; + } +} diff --git a/tests/Unit/Api/Record/Objects/UnknownObjectTest.php b/tests/Unit/Api/Record/Objects/UnknownObjectTest.php new file mode 100644 index 00000000..06663a71 --- /dev/null +++ b/tests/Unit/Api/Record/Objects/UnknownObjectTest.php @@ -0,0 +1,46 @@ + 'test_name', + 'archived' => true, + ]; + + $object = UnknownObject::make(new MoySklad(['token']), self::PATH, self::TYPE, $content); + + $this->assertInstanceOf(UnknownObject::class, $object); + + foreach ($content as $key => $value) { + $this->assertSame($value, $object->{$key}); + } + + $this->assertSame(Url::makeFromPathAndParams(self::PATH), $object->meta->href); + $this->assertSame(self::TYPE, $object->meta->type); + } + + public function testCollectionMethodReturnsExpectedCollectionAndMeta(): void + { + $result = UnknownObject::collection(new MoySklad(['token']), self::PATH, self::TYPE); + + $this->assertInstanceOf(UnknownCollection::class, $result); + } +} diff --git a/tests/Unit/Api/Segments/Endpoints/EntityTest.php b/tests/Unit/Api/Segments/Endpoints/EntityTest.php deleted file mode 100644 index b68c9415..00000000 --- a/tests/Unit/Api/Segments/Endpoints/EntityTest.php +++ /dev/null @@ -1,51 +0,0 @@ - */ -class EntityTest extends ApiTestCase -{ - private Entity $builder; - - protected function setUp(): void - { - parent::setUp(); - - $this->builder = new Entity($this->api, [], []); - } - - public function testProductReturnsCorrectClass(): void - { - $builder = $this->builder->product(); - - $this->assertInstanceOf(Product::class, $builder); - $this->assertInstanceOf(AbstractMethodNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } - - public function testCustomerorderReturnsCorrectClass(): void - { - $builder = $this->builder->customerorder(); - - $this->assertInstanceOf(Customerorder::class, $builder); - $this->assertInstanceOf(AbstractMethodNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } - - public function testAssortmentReturnsCorrectClass(): void - { - $builder = $this->builder->assortment(); - - $this->assertInstanceOf(Assortment::class, $builder); - $this->assertInstanceOf(AbstractMethodNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Api/Traits/Segments/AttributesTraitTest.php b/tests/Unit/Api/Traits/Segments/AttributesTraitTest.php deleted file mode 100644 index 10668314..00000000 --- a/tests/Unit/Api/Traits/Segments/AttributesTraitTest.php +++ /dev/null @@ -1,25 +0,0 @@ -api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { - use AttributesTrait; - })->attributes(); - - $this->assertInstanceOf(Attributes::class, $builder); - $this->assertInstanceOf(AbstractMethodNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Api/Traits/Segments/ByIdCommonTraitTest.php b/tests/Unit/Api/Traits/Segments/ByIdCommonTraitTest.php deleted file mode 100644 index 4e5bd557..00000000 --- a/tests/Unit/Api/Traits/Segments/ByIdCommonTraitTest.php +++ /dev/null @@ -1,24 +0,0 @@ -api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { - use ByIdCommonTrait; - })->byId('id'); - - $this->assertInstanceOf(ByIdCommon::class, $builder); - $this->assertInstanceOf(AbstractSegmentCommon::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Api/Traits/Segments/ByIdPositionedTraitTest.php b/tests/Unit/Api/Traits/Segments/ByIdPositionedTraitTest.php deleted file mode 100644 index 3832352b..00000000 --- a/tests/Unit/Api/Traits/Segments/ByIdPositionedTraitTest.php +++ /dev/null @@ -1,24 +0,0 @@ -api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { - use ByIdPositionedTrait; - })->byId('id'); - - $this->assertInstanceOf(ByIdPositioned::class, $builder); - $this->assertInstanceOf(AbstractSegmentCommon::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Api/Traits/Segments/MetadataTraitTest.php b/tests/Unit/Api/Traits/Segments/MetadataTraitTest.php deleted file mode 100644 index ff85735e..00000000 --- a/tests/Unit/Api/Traits/Segments/MetadataTraitTest.php +++ /dev/null @@ -1,25 +0,0 @@ -api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { - use MetadataTrait; - })->metadata(); - - $this->assertInstanceOf(Metadata::class, $builder); - $this->assertInstanceOf(AbstractMethodNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Api/Traits/Segments/MethodCommonTraitTest.php b/tests/Unit/Api/Traits/Segments/MethodCommonTraitTest.php deleted file mode 100644 index 4d483706..00000000 --- a/tests/Unit/Api/Traits/Segments/MethodCommonTraitTest.php +++ /dev/null @@ -1,24 +0,0 @@ -api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { - use MethodCommonTrait; - })->method('test_method'); - - $this->assertInstanceOf(MethodCommon::class, $builder); - $this->assertInstanceOf(AbstractSegmentCommon::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Api/Traits/Segments/PositionsTraitTest.php b/tests/Unit/Api/Traits/Segments/PositionsTraitTest.php deleted file mode 100644 index 4e4fad3d..00000000 --- a/tests/Unit/Api/Traits/Segments/PositionsTraitTest.php +++ /dev/null @@ -1,25 +0,0 @@ -api, static::PREV_PATH, static::PARAMS, 'test_segment') extends AbstractSegmentCommon { - use PositionsTrait; - })->positions(); - - $this->assertInstanceOf(Positions::class, $builder); - $this->assertInstanceOf(AbstractMethodNamed::class, $builder); - $this->assertInstanceOf(AbstractBuilder::class, $builder); - } -} diff --git a/tests/Unit/Enums/HttpMethodTest.php b/tests/Unit/Enums/HttpMethodTest.php new file mode 100644 index 00000000..c5a3cdfd --- /dev/null +++ b/tests/Unit/Enums/HttpMethodTest.php @@ -0,0 +1,39 @@ +assertSame($expectedMethod, HttpMethod::makeFrom($passedMethod)); + } + + public static function httpMethodsDataProvider(): array + { + return [ + [HttpMethod::GET, 'GET'], + [HttpMethod::POST, 'post'], + [HttpMethod::PUT, 'pUt'], + [HttpMethod::PATCH, HttpMethod::PATCH], + [HttpMethod::DELETE, 'DeLeTe'], + ]; + } + + public function testMakeFromInvalidMethod(): void + { + $invalidMethod = 'invalid method'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("'" . strtoupper($invalidMethod) . "' is not valid HTTP method"); + + HttpMethod::makeFrom($invalidMethod); + } +} diff --git a/tests/Unit/Enums/QueryParamTest.php b/tests/Unit/Enums/QueryParamTest.php index 69e19648..d61dfc5f 100644 --- a/tests/Unit/Enums/QueryParamTest.php +++ b/tests/Unit/Enums/QueryParamTest.php @@ -14,13 +14,7 @@ public function testSeparator(QueryParam $queryParam, string $separator): void $this->assertSame($separator, $queryParam->separator()); } - /** @dataProvider getSeparatorDataProvider */ - public function testGetSeparator(QueryParam|string $queryParam, string $separator): void - { - $this->assertSame($separator, QueryParam::getSeparator($queryParam)); - } - - private function separatorDataProvider(): array + public static function separatorDataProvider(): array { return [ [QueryParam::EXPAND, ','], @@ -32,9 +26,15 @@ private function separatorDataProvider(): array ]; } - private function getSeparatorDataProvider(): array + /** @dataProvider getSeparatorDataProvider */ + public function testGetSeparator(QueryParam|string $queryParam, string $separator): void + { + $this->assertSame($separator, QueryParam::getSeparator($queryParam)); + } + + public static function getSeparatorDataProvider(): array { - return array_merge($this->separatorDataProvider(), [ + return array_merge(self::separatorDataProvider(), [ ['expand', ','], ['FILTER', ';'], ['OrdEr', ';'], diff --git a/tests/Unit/Exceptions/RequestExceptionTest.php b/tests/Unit/Exceptions/RequestExceptionTest.php new file mode 100644 index 00000000..cbf5b7aa --- /dev/null +++ b/tests/Unit/Exceptions/RequestExceptionTest.php @@ -0,0 +1,142 @@ +makeRequestException(); + + $this->assertNull($exception->getResponse()); + } + + public function testResponseNotResolvedFromPreviousWithoutGetResponseMethod(): void + { + $exception = $this->makeRequestException(previous: new Exception()); + + $this->assertNull($exception->getResponse()); + } + + public function testRequestNotResolvedFromPreviousWithoutGetRequestMethod(): void + { + $exception = $this->makeRequestException(previous: new Exception()); + + $this->assertNull($exception->getRequest()); + } + + public function testResponseNotResolvedFromPreviousWithInvalidGetResponseMethod(): void + { + $invalidException = new class() extends Exception { + public function getResponse() + { + return null; + } + }; + $exception = $this->makeRequestException(previous: $invalidException); + + $this->assertNull($exception->getResponse()); + } + + public function testRequestNotResolvedFromPreviousWithInvalidGetRequestMethod(): void + { + $invalidException = new class() extends Exception { + public function getRequest() + { + return null; + } + }; + $exception = $this->makeRequestException(previous: $invalidException); + + $this->assertNull($exception->getRequest()); + } + + public function testResponseResolvedFromPreviousWithCorrectGetResponseMethod(): void + { + $exception = $this->makeCorrectWithResponseException(); + + $this->assertInstanceOf(Response::class, $exception->getResponse()); + } + + public function testRequestResolvedFromPreviousWithCorrectGetResponseMethod(): void + { + $exception = $this->makeCorrectWithRequestException('GET', Url::API); + + $this->assertInstanceOf(Request::class, $exception->getRequest()); + } + + public function testContentNotResolvedWithoutResponse(): void + { + $exception = $this->makeRequestException(); + + $this->assertNull($exception->getContent()); + } + + public function testContentResolvedCorrectly(): void + { + $exception = $this->makeCorrectWithResponseException('{"key": "value"}'); + $content = $exception->getContent(); + + $this->assertInstanceOf(stdClass::class, $content); + $this->assertCount(1, (array) $content); + $this->assertSame('value', $content->key); + $this->assertSame(json_encode($content), json_encode($exception->getContent())); + } + + private function makeCorrectWithResponseException(string $body = null): RequestException + { + $exception = new class($body) extends Exception { + public function __construct(private readonly ?string $body, string $message = '', int $code = 0, ?Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } + + public function getResponse(): Response + { + return new Response(body: $this->body); + } + }; + + return $this->makeRequestException($exception); + } + + private function makeCorrectWithRequestException(string $method, string $uri): RequestException + { + $exception = new class($method, $uri) extends Exception { + public function __construct( + private readonly string $method, + private readonly string $uri, + string $message = '', + int $code = 0, + ?Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); + } + + public function getRequest(): Request + { + return new Request($this->method, $this->uri); + } + }; + + return $this->makeRequestException($exception); + } + + private function makeRequestException(?Throwable $previous = null): RequestException + { + $formatter = (new MoySklad(['token']))->getFormatter(); + + return new RequestException($formatter, '', 0, $previous); + } +} diff --git a/tests/Unit/Formatters/ArrayFormatTest.php b/tests/Unit/Formatters/ArrayFormatTest.php index fa66c41d..80213329 100644 --- a/tests/Unit/Formatters/ArrayFormatTest.php +++ b/tests/Unit/Formatters/ArrayFormatTest.php @@ -4,19 +4,26 @@ use Evgeek\Moysklad\Formatters\ArrayFormat; -/** @covers \Evgeek\Moysklad\Formatters\ArrayFormat */ +/** + * @covers \Evgeek\Moysklad\Formatters\AbstractMultiDecoder + * @covers \Evgeek\Moysklad\Formatters\ArrayFormat + */ class ArrayFormatTest extends MultiDecoderTestCase { protected const FORMATTER = ArrayFormat::class; - protected function getEncodedObject(): array + public static function getEncodedObject(): array { return [ 'param' => 'test_param', + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/endpoint/segment', + 'type' => 'product', + ], 'context' => [ 'employee' => [ 'meta' => [ - 'href' => 'test_href_1', + 'href' => 'https://online.moysklad.ru/api/remap/1.2/context/employee', 'type' => 'employee', ], ], @@ -42,7 +49,7 @@ protected function getEncodedObject(): array ]; } - protected function getEncodedArray(): array + protected static function getEncodedArray(): array { return [ ['param' => 'value1', 'meta' => 'meta1'], @@ -50,7 +57,7 @@ protected function getEncodedArray(): array ]; } - protected function getEncodedEmpty(): array + protected static function getEncodedEmpty(): array { return []; } diff --git a/tests/Unit/Formatters/ExtendedObjects/ExtendedTestEmployee.php b/tests/Unit/Formatters/ExtendedObjects/ExtendedTestEmployee.php new file mode 100644 index 00000000..e524de74 --- /dev/null +++ b/tests/Unit/Formatters/ExtendedObjects/ExtendedTestEmployee.php @@ -0,0 +1,11 @@ +|JsonFormatterInterface */ - protected const FORMATTER = AbstractMultiDecoder::class; - - protected const OBJECT_JSON_STRING = '{"param":"test_param","context":{"employee":{"meta":' . - '{"href":"test_href_1","type":"employee"}}},"rows":[{"id":"id1","value":true},{"id":"id2","value":0},' . - '{"id":"id3","value":null},{"id":"id4","value":123.45}]}'; + protected const OBJECT_JSON_STRING = '{"param":"test_param","meta":{"href":' . + '"https:\/\/online.moysklad.ru\/api\/remap\/1.2\/endpoint\/segment","type":"product"},"context":' . + '{"employee":{"meta":{"href":"https:\/\/online.moysklad.ru\/api\/remap\/1.2\/context\/employee","type":"employee"}}},' . + '"rows":[{"id":"id1","value":true},{"id":"id2","value":0},{"id":"id3","value":null},{"id":"id4","value":123.45}]}'; protected const ARRAYS_JSON_STRING = '[{"param":"value1","meta":"meta1"},{"param":"value2","meta":"meta2"}]'; protected const EMPTY_JSON_STRING = ''; protected const NULL_JSON_STRING = 'null'; protected const FALSE_JSON_STRING = 'false'; + /** @var class-string */ + protected const FORMATTER = AbstractMultiDecoder::class; + protected JsonFormatterInterface $formatter; + + protected function setUp(): void + { + $formatterClass = static::FORMATTER; + $this->formatter = new $formatterClass(); + if (is_a($this->formatter, WithMoySkladInterface::class)) { + $this->formatter->setMoySklad(new MoySklad(['token'])); + } + } + /** @dataProvider correctEncodeDataProvider */ public function testEncodeCorrect(string $jsonString, mixed $formatted): void { - $this->assertSame($formatted, (static::FORMATTER)::encode($jsonString)); + $this->assertSame($formatted, $this->formatter->encode($jsonString)); } /** @dataProvider correctDecodeDataProvider */ public function testDecodeCorrect(string $jsonString, mixed $formatted): void { - $this->assertSame($jsonString, (static::FORMATTER)::decode($formatted)); + $this->assertSame($jsonString, $this->formatter->decode($formatted)); + } + + public static function correctDecodeDataProvider(): array + { + $json = '[{"param1":"value1","param2":false},{"param1":2.34,"param2":null}]'; + $array = [ + ['param1' => 'value1', 'param2' => false], + ['param1' => 2.34, 'param2' => null], + ]; + $object1 = new stdClass(); + $object1->param1 = 'value1'; + $object1->param2 = false; + $object2 = new stdClass(); + $object2->param1 = 2.34; + $object2->param2 = null; + $arrayOfObjects = [$object1, $object2]; + + return array_merge(static::correctEncodeDataProvider(), [ + ['', false], + ['', null], + [$json, $json], + [$json, $array], + [$json, $arrayOfObjects], + ]); } /** @dataProvider invalidJsonTypesDataProvider */ @@ -40,7 +78,7 @@ public function testEncodeUnexpectedDataType(string $jsonString): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Passed content is not valid json.'); - (static::FORMATTER)::encode($jsonString); + $this->formatter->encode($jsonString); } public function testDecodeInvalidType(): void @@ -48,7 +86,7 @@ public function testDecodeInvalidType(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage("Can't convert content of"); - (static::FORMATTER)::decode(NAN); + $this->formatter->decode(NAN); } /** @dataProvider invalidJsonTypesDataProvider */ @@ -57,57 +95,54 @@ public function testDecodeInvalidString(string $jsonString): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Passed content is not valid json.'); - (static::FORMATTER)::decode($jsonString); + $this->formatter->decode($jsonString); } - abstract protected function getEncodedObject(); - - abstract protected function getEncodedArray(); - - abstract protected function getEncodedEmpty(); - - protected function correctEncodeDataProvider(): array + public static function invalidJsonTypesDataProvider(): array { return [ - [self::OBJECT_JSON_STRING, $this->getEncodedObject()], - [self::ARRAYS_JSON_STRING, $this->getEncodedArray()], - ['', $this->getEncodedEmpty()], + ['invalid-json-encode'], + ['123.45'], + ['77'], + ['true'], + ['false'], + ['null'], ]; } - protected function correctDecodeDataProvider(): array + public static function correctEncodeDataProvider(): array { - $json = '[{"param1":"value1","param2":false},{"param1":2.34,"param2":null}]'; - $array = [ - ['param1' => 'value1', 'param2' => false], - ['param1' => 2.34, 'param2' => null], + return [ + [self::OBJECT_JSON_STRING, static::getEncodedObject()], + [self::ARRAYS_JSON_STRING, static::getEncodedArray()], + ['', static::getEncodedEmpty()], ]; - $object1 = new stdClass(); - $object1->param1 = 'value1'; - $object1->param2 = false; - $object2 = new stdClass(); - $object2->param1 = 2.34; - $object2->param2 = null; - $arrayOfObjects = [$object1, $object2]; - - return array_merge($this->correctEncodeDataProvider(), [ - ['', false], - ['', null], - [$json, $json], - [$json, $array], - [$json, $arrayOfObjects], - ]); } - private function invalidJsonTypesDataProvider(): array + abstract protected static function getEncodedObject(); + + abstract protected static function getEncodedArray(); + + abstract protected static function getEncodedEmpty(); + + protected function castToArrayWithNested(mixed $content): mixed { - return [ - ['invalid-json-encode'], - ['123.45'], - ['77'], - ['true'], - ['false'], - ['null'], - ]; + if (is_array($content)) { + foreach ($content as $key => $value) { + $content[$key] = $this->castToArrayWithNested($value); + } + + return $content; + } + + if (is_a($content, AbstractRecord::class)) { + return $content->toArray(); + } + + if (is_object($content)) { + return $this->castToArrayWithNested((array) $content); + } + + return $content; } } diff --git a/tests/Unit/Formatters/RecordFormatTest.php b/tests/Unit/Formatters/RecordFormatTest.php new file mode 100644 index 00000000..234f5c9c --- /dev/null +++ b/tests/Unit/Formatters/RecordFormatTest.php @@ -0,0 +1,204 @@ + [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/entity/product', + 'type' => 'product', + ], + 'context' => [ + 'employee' => [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/context/employee', + 'type' => 'employee', + ], + ], + ], + 'rows' => [ + [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/entity/product', + 'type' => 'product', + ], + 'id' => '25cf41f2-b068-11ed-0a80-0e9700500d7e', + 'owner' => [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/entity/employee', + 'type' => 'employee', + ], + ], + 'fake_collection' => [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/entity/fake_entity', + 'type' => 'fake_entity', + ], + 'rows' => [ + [], + ], + ], + ], + ], + ], + [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/entity/fake_entity', + 'type' => 'fake_entity', + ], + 'id' => 'f731148b-a93d-11ed-0a80-0fba0011a6c6', + ], + ]; + + /** @dataProvider correctEncodeDataProvider */ + public function testEncodeCorrect(string $jsonString, mixed $formatted): void + { + $formattedCasted = $this->castToArrayWithNested($formatted); + $result = $this->formatter->encode($jsonString); + + if (is_a($result, AbstractRecord::class)) { + $this->assertInstanceOf(ProductCollection::class, $result); + $this->assertInstanceOf(Employee::class, $result->context->employee); + } + + $resultFormatted = $this->castToArrayWithNested($result); + + $this->assertSame($formattedCasted, $resultFormatted); + } + + public function testGetMappingReturnsPassedMapping(): void + { + $mapping = new RecordMapping(); + $formatter = new RecordFormat($mapping); + + $this->assertSame($mapping, $formatter->getMapping()); + } + + /** @dataProvider arrayInputDataProvider */ + public function testEncodeToStdClassResultIsEqualToInput(array $input): void + { + $result = $this->encode($input); + $formattedResult = $this->castToArrayWithNested($result); + + $this->assertSame($input, $formattedResult); + } + + public static function arrayInputDataProvider(): array + { + return [ + [[]], + [['test_item' => 'test_value']], + [[['test_item' => 'test_value']]], + [[['test_item' => ['key' => []]]]], + [ + [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/context/employee', + 'type' => 'employee', + ], + ], + ], + [ + [[ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/context/employee', + 'type' => 'employee', + ], + ]], + ], + [ + [ + 'meta' => [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/entity/product', + 'type' => 'product', + ], + 'rows' => [], + ], + ], + ]; + } + + public function testArrayOfObjectEncodedCorrectly(): void + { + $result = $this->encode(self::COMPLEX_CASE); + + $this->assertIsArray($result); + $this->assertCount(2, $result); + + [$productCollection, $unknownObject] = $result; + + $this->assertInstanceOf(ProductCollection::class, $productCollection); + $this->assertInstanceOf(UnknownObject::class, $unknownObject); + } + + public function testContextOfCollectionEncodedAndContainsMeta(): void + { + [$productCollection] = $this->encode(self::COMPLEX_CASE); + + $this->assertInstanceOf(Employee::class, $productCollection->context->employee); + $this->assertIsObject($productCollection->context->employee->meta ?? null); + } + + public function testKnownElementOfCollectionEncodedToRegisteredObject(): void + { + [$productCollection] = $this->encode(self::COMPLEX_CASE); + + $product = $productCollection->rows[0]; + $this->assertInstanceOf(Product::class, $product); + } + + public function testUnknownElementOfCollectionEncodedToUnknownObject(): void + { + [, $unknownObject] = $this->encode(self::COMPLEX_CASE); + + $this->assertInstanceOf(UnknownObject::class, $unknownObject); + } + + public function testObjectIdAddedToMetaHref(): void + { + [$productCollection] = $this->encode(self::COMPLEX_CASE); + + $owner = $productCollection->rows[0]->owner; + + $this->assertInstanceOf(Employee::class, $owner); + $this->assertSame(Url::makeFromPathAndParams(Employee::PATH), $owner->meta->href); + + $owner->id = 'cbcf493b-55bc-11d9-848a-00112f43529a'; + $this->assertNotNull(Url::makeFromPathAndParams(Employee::PATH) . '/' . $owner->id, $owner->meta->href); + } + + public function testNestedUnknownCollectionEncodedToUnknownCollection(): void + { + [$productCollection] = $this->encode(self::COMPLEX_CASE); + + $this->assertInstanceOf(UnknownCollection::class, $productCollection->rows[0]->fake_collection); + } + + private function encode(array $content): AbstractRecord|array|stdClass + { + $formatter = new RecordFormat(); + $formatter->setMoySklad(new MoySklad(['token'])); + + return $formatter->encodeToStdClass($content); + } +} diff --git a/tests/Unit/Formatters/RecordMappingTest.php b/tests/Unit/Formatters/RecordMappingTest.php new file mode 100644 index 00000000..43a01847 --- /dev/null +++ b/tests/Unit/Formatters/RecordMappingTest.php @@ -0,0 +1,150 @@ +mapping = new RecordMapping(); + } + + public function testSetSingleObjectWithCorrectClassWorks(): void + { + $this->assertSame(Product::class, $this->mapping->getObject(Entity::PRODUCT)); + + $this->mapping->setObject(ExtendedTestProduct::class); + + $this->assertSame(ExtendedTestProduct::class, $this->mapping->getObject(Entity::PRODUCT)); + } + + public function testSetSingleObjectWithIncorrectClassWorks(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(ExtendedTestProductCollection::class . ' is not a ' . AbstractConcreteObject::class); + + $this->mapping->setObject(ExtendedTestProductCollection::class); + } + + public function testSetSingleObjectWithEmptyClassThrowsError(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('Class "product" not found'); + + $this->mapping->setObject(Entity::PRODUCT); + } + + public function testSetMultipleObjectWithCorrectClassesWorks(): void + { + $this->assertSame(Product::class, $this->mapping->getObject(Entity::PRODUCT)); + $this->assertSame(Employee::class, $this->mapping->getObject(Entity::EMPLOYEE)); + + $this->mapping->setObject([ + ExtendedTestProduct::class, + ExtendedTestEmployee::class, + ]); + + $this->assertSame(ExtendedTestProduct::class, $this->mapping->getObject(Entity::PRODUCT)); + $this->assertSame(ExtendedTestEmployee::class, $this->mapping->getObject(Entity::EMPLOYEE)); + } + + public function testGetNotRegisteredObjectReturnsUnknownObject(): void + { + $this->assertSame(UnknownObject::class, $this->mapping->getObject('wrong-object')); + } + + public function testSetCollectionWithCorrectClassWorks(): void + { + $this->assertSame(ProductCollection::class, $this->mapping->getCollection(Entity::PRODUCT)); + + $this->mapping->setCollection(ExtendedTestProductCollection::class); + + $this->assertSame(ExtendedTestProductCollection::class, $this->mapping->getCollection(Entity::PRODUCT)); + } + + public function testSetCollectionWithIncorrectClassWorks(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(ExtendedTestProduct::class . ' is not a ' . AbstractConcreteCollection::class); + + $this->mapping->setCollection(ExtendedTestProduct::class); + } + + public function testSetCollectionWithEmptyClassThrowsError(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('Class "product" not found'); + + $this->mapping->setCollection(Entity::PRODUCT); + } + + public function testSetMultipleCollectionWithCorrectClassesWorks(): void + { + $this->assertSame(ProductCollection::class, $this->mapping->getCollection(Entity::PRODUCT)); + $this->assertSame(EmployeeCollection::class, $this->mapping->getCollection(Entity::EMPLOYEE)); + + $this->mapping->setCollection([ + ExtendedTestProductCollection::class, + ExtendedTestEmployeeCollection::class, + ]); + + $this->assertSame(ExtendedTestProductCollection::class, $this->mapping->getCollection(Entity::PRODUCT)); + $this->assertSame(ExtendedTestEmployeeCollection::class, $this->mapping->getCollection(Entity::EMPLOYEE)); + } + + public function testGetNotRegisteredCollectionReturnsUnknownCollection(): void + { + $this->assertSame(UnknownCollection::class, $this->mapping->getCollection('wrong-collection')); + } + + public function testRegisterMappingsFromConstructor(): void + { + $objectMapping = [ + Entity::PRODUCT => ExtendedTestProduct::class, + Entity::EMPLOYEE => ExtendedTestEmployee::class, + ]; + $collectionMapping = [ + Entity::PRODUCT => ExtendedTestProductCollection::class, + Entity::EMPLOYEE => ExtendedTestEmployeeCollection::class, + ]; + $mapping = new RecordMapping($objectMapping, $collectionMapping); + + $this->assertSame(ExtendedTestProduct::class, $mapping->getObject(Entity::PRODUCT)); + $this->assertSame(ExtendedTestEmployee::class, $mapping->getObject(Entity::EMPLOYEE)); + $this->assertSame(ExtendedTestProductCollection::class, $mapping->getCollection(Entity::PRODUCT)); + $this->assertSame(ExtendedTestEmployeeCollection::class, $mapping->getCollection(Entity::EMPLOYEE)); + } + + public function testPurgeMappingsFromConstructor(): void + { + $mapping = new RecordMapping([], []); + + $this->assertSame(UnknownObject::class, $mapping->getObject(Entity::PRODUCT)); + $this->assertSame(UnknownObject::class, $mapping->getObject(Entity::EMPLOYEE)); + $this->assertSame(UnknownCollection::class, $mapping->getCollection(Entity::PRODUCT)); + $this->assertSame(UnknownCollection::class, $mapping->getCollection(Entity::EMPLOYEE)); + } +} diff --git a/tests/Unit/Formatters/StdClassFormatTest.php b/tests/Unit/Formatters/StdClassFormatTest.php index f0fe03fa..ae9b080f 100644 --- a/tests/Unit/Formatters/StdClassFormatTest.php +++ b/tests/Unit/Formatters/StdClassFormatTest.php @@ -2,33 +2,38 @@ namespace Evgeek\Tests\Unit\Formatters; -use Evgeek\Moysklad\Formatters\JsonFormatterInterface; use Evgeek\Moysklad\Formatters\StdClassFormat; use stdClass; -/** @covers \Evgeek\Moysklad\Formatters\StdClassFormat */ +/** + * @covers \Evgeek\Moysklad\Formatters\AbstractMultiDecoder + * @covers \Evgeek\Moysklad\Formatters\StdClassFormat + */ class StdClassFormatTest extends MultiDecoderTestCase { - /** @var JsonFormatterInterface */ protected const FORMATTER = StdClassFormat::class; /** @dataProvider correctEncodeDataProvider */ public function testEncodeCorrect(string $jsonString, mixed $formatted): void { $formattedCasted = $this->castToArrayWithNested($formatted); - $encodedCasted = $this->castToArrayWithNested((static::FORMATTER)::encode($jsonString)); + $encodedCasted = $this->castToArrayWithNested($this->formatter->encode($jsonString)); $this->assertSame($formattedCasted, $encodedCasted); } - protected function getEncodedObject(): stdClass + protected static function getEncodedObject(): stdClass { return (object) [ 'param' => 'test_param', + 'meta' => (object) [ + 'href' => 'https://online.moysklad.ru/api/remap/1.2/endpoint/segment', + 'type' => 'product', + ], 'context' => (object) [ 'employee' => (object) [ 'meta' => (object) [ - 'href' => 'test_href_1', + 'href' => 'https://online.moysklad.ru/api/remap/1.2/context/employee', 'type' => 'employee', ], ], @@ -54,7 +59,7 @@ protected function getEncodedObject(): stdClass ]; } - protected function getEncodedArray(): array + protected static function getEncodedArray(): array { return [ (object) ['param' => 'value1', 'meta' => 'meta1'], @@ -62,27 +67,8 @@ protected function getEncodedArray(): array ]; } - protected function getEncodedEmpty(): stdClass + protected static function getEncodedEmpty(): stdClass { return new stdClass(); } - - protected function castToArrayWithNested(stdClass|array $array): array - { - if (is_array($array)) { - foreach ($array as $key => $value) { - if (is_array($value)) { - $array[$key] = $this->castToArrayWithNested($value); - } - if (is_object($value)) { - $array[$key] = $this->castToArrayWithNested((array) $value); - } - } - } - if (is_object($array)) { - return $this->castToArrayWithNested((array) $array); - } - - return $array; - } } diff --git a/tests/Unit/Formatters/StringFormatTest.php b/tests/Unit/Formatters/StringFormatTest.php index 34c2f16a..7392f229 100644 --- a/tests/Unit/Formatters/StringFormatTest.php +++ b/tests/Unit/Formatters/StringFormatTest.php @@ -4,22 +4,25 @@ use Evgeek\Moysklad\Formatters\StringFormat; -/** @covers \Evgeek\Moysklad\Formatters\StringFormat */ +/** + * @covers \Evgeek\Moysklad\Formatters\AbstractMultiDecoder + * @covers \Evgeek\Moysklad\Formatters\StringFormat + */ class StringFormatTest extends MultiDecoderTestCase { protected const FORMATTER = StringFormat::class; - protected function getEncodedObject(): string + protected static function getEncodedObject(): string { return static::OBJECT_JSON_STRING; } - protected function getEncodedArray(): string + protected static function getEncodedArray(): string { return static::ARRAYS_JSON_STRING; } - protected function getEncodedEmpty(): string + protected static function getEncodedEmpty(): string { return ''; } diff --git a/tests/Unit/Http/ApiClientTest.php b/tests/Unit/Http/ApiClientTest.php index 77d3cac9..abc1c460 100644 --- a/tests/Unit/Http/ApiClientTest.php +++ b/tests/Unit/Http/ApiClientTest.php @@ -187,6 +187,14 @@ function (array $responseBody) use ($param) { ); } + public static function limitOffsetDataProvider(): array + { + return [ + ['limit'], + ['offset'], + ]; + } + public function testDebug(): void { $payload = new Payload(HttpMethod::GET, self::PATH, self::PARAMS, self::BODY); @@ -264,14 +272,6 @@ public function testWrongCountCredentialsThrow(): void new ApiClient($credentials, new ArrayFormat(), $this->guzzleSender); } - private function limitOffsetDataProvider(): array - { - return [ - ['limit'], - ['offset'], - ]; - } - private function assertGeneratorThrowWithout(string $expectedExceptionMessage, callable $unsetCallback): void { $payload = new Payload(HttpMethod::GET, self::PATH, [], null); diff --git a/tests/Unit/Http/GuzzleSenderFactoryTest.php b/tests/Unit/Http/GuzzleSenderFactoryTest.php index bbbf9b7e..bb183118 100644 --- a/tests/Unit/Http/GuzzleSenderFactoryTest.php +++ b/tests/Unit/Http/GuzzleSenderFactoryTest.php @@ -84,6 +84,15 @@ public function testRetryWorksWithRetryableCodes(int $code, string $exception): $this->assertCount(2, $factory->container); } + public static function retryableCodesDataProvider(): array + { + return [ + [500, ServerException::class], + [503, ServerException::class], + [429, RequestException::class], + ]; + } + /** @dataProvider notRetryableCodesDataProvider */ public function testRetryDoesNotWorksWithNotRetryableCodes(int $code, ?string $exception): void { @@ -102,6 +111,17 @@ public function testRetryDoesNotWorksWithNotRetryableCodes(int $code, ?string $e $this->assertCount(1, $factory->container); } + public static function notRetryableCodesDataProvider(): array + { + return [ + [200, null], + [201, null], + [304, null], + [403, ClientException::class], + [409, ClientException::class], + ]; + } + public function testRetryWorksOnClientException(): void { $factory = $this->createFactoryMock(1); @@ -144,26 +164,6 @@ public function testExceptionBodyTruncated(): void $requestSender->send(new Request('GET', Url::API)); } - private function retryableCodesDataProvider(): array - { - return [ - [500, ServerException::class], - [503, ServerException::class], - [429, RequestException::class], - ]; - } - - private function notRetryableCodesDataProvider(): array - { - return [ - [200, null], - [201, null], - [304, null], - [403, ClientException::class], - [409, ClientException::class], - ]; - } - private function createFactoryMock(int $retries = 0, int $exceptionTruncateAt = 120) { return new class($this->mockHandler, $retries, $exceptionTruncateAt) extends GuzzleSenderFactory { diff --git a/tests/Unit/Meta/MetaMakerTest.php b/tests/Unit/Meta/MetaMakerTest.php new file mode 100644 index 00000000..ac44371e --- /dev/null +++ b/tests/Unit/Meta/MetaMakerTest.php @@ -0,0 +1,41 @@ +ms = new MoySklad(['token'], new ArrayFormat()); + } + + /** @dataProvider creationDataProvider */ + public function testCreationMethod(string $method, array $params, string $expectedPath, string $expectedType): void + { + $expectedMeta = [ + 'href' => Url::API . $expectedPath, + 'type' => $expectedType, + 'mediaType' => 'application/json', + ]; + $this->assertSame($expectedMeta, $this->ms->meta()->{$method}(...$params)); + } + + public static function creationDataProvider(): array + { + return [ + ['create', [['segment1', 'segment2'], 'type'], '/segment1/segment2', 'type'], + ['product', [self::GUID], '/entity/product/' . self::GUID, 'product'], + ['employee', [self::GUID], '/entity/employee/' . self::GUID, 'employee'], + ['customerorder', [self::GUID], '/entity/customerorder/' . self::GUID, 'customerorder'], + ]; + } +} diff --git a/tests/Unit/MoySkladTest.php b/tests/Unit/MoySkladTest.php index da71811f..1a8d2b65 100644 --- a/tests/Unit/MoySkladTest.php +++ b/tests/Unit/MoySkladTest.php @@ -2,9 +2,14 @@ namespace Evgeek\Tests\Unit; -use Evgeek\Moysklad\Api\AbstractBuilder; -use Evgeek\Moysklad\Api\Query; +use Evgeek\Moysklad\Api\Query\AbstractBuilder; +use Evgeek\Moysklad\Api\Query\QueryBuilder; +use Evgeek\Moysklad\Api\Record\Builders\AbstractBuilder as AbstractRecordBuilder; +use Evgeek\Moysklad\Api\Record\Builders\RecordBuilder; +use Evgeek\Moysklad\Formatters\RecordFormat; +use Evgeek\Moysklad\Http\ApiClient; use Evgeek\Moysklad\Http\RequestSenderFactoryInterface; +use Evgeek\Moysklad\Meta\MetaMaker; use Evgeek\Moysklad\MoySklad; use PHPUnit\Framework\TestCase; @@ -16,18 +21,51 @@ public function testQuery(): void $ms = new MoySklad(['token']); $query = $ms->query(); - $this->assertInstanceOf(Query::class, $query); + $this->assertInstanceOf(QueryBuilder::class, $query); $this->assertInstanceOf(AbstractBuilder::class, $query); } + public function testObject(): void + { + $ms = new MoySklad(['token']); + $object = $ms->record(); + + $this->assertInstanceOf(RecordBuilder::class, $object); + $this->assertInstanceOf(AbstractRecordBuilder::class, $object); + } + + public function testMeta(): void + { + $ms = new MoySklad(['token']); + $meta = $ms->meta(); + + $this->assertInstanceOf(MetaMaker::class, $meta); + } + + public function testGetApiClient(): void + { + $ms = new MoySklad(['token']); + $apiClient = $ms->getApiClient(); + + $this->assertInstanceOf(ApiClient::class, $apiClient); + } + + public function testGetFormatter(): void + { + $expectedFormatter = new RecordFormat(); + $ms = new MoySklad(['token'], $expectedFormatter); + $formatter = $ms->getFormatter(); + + $this->assertSame($expectedFormatter, $formatter); + } + public function testRequestSenderInitialization(): void { $requestSenderFactoryMock = $this->createMock(RequestSenderFactoryInterface::class); - $ms = new MoySklad(['token']); $requestSenderFactoryMock->expects($this->once()) ->method('make'); - $ms->__construct(credentials: ['token'], requestSenderFactory: $requestSenderFactoryMock); + new MoySklad(credentials: ['token'], requestSenderFactory: $requestSenderFactoryMock); } } diff --git a/tests/Unit/Services/CollectionHelperTest.php b/tests/Unit/Services/CollectionHelperTest.php new file mode 100644 index 00000000..e869b5c2 --- /dev/null +++ b/tests/Unit/Services/CollectionHelperTest.php @@ -0,0 +1,41 @@ +ms = new MoySklad(['token']); + } + + public function testRowsCorrectlyExtractedFromCollection(): void + { + $content = [ + ['name' => 'orange'], + ['name' => 'lime'], + ]; + $collection = new UnknownCollection($this->ms, ['endpoint', 'segment'], 'type', ['rows' => $content]); + $rows = CollectionHelper::extractRows($collection); + + $this->assertSame($content, json_decode(json_encode($rows), true)); + } + + public function testRowsDoesNotExtractedFromNotCollection(): void + { + $content = 'test-content'; + $rows = CollectionHelper::extractRows($content); + + $this->assertSame($content, $rows); + } +} diff --git a/tests/Unit/Services/QueryParamsTest.php b/tests/Unit/Services/QueryParamsTest.php new file mode 100644 index 00000000..5f4436a5 --- /dev/null +++ b/tests/Unit/Services/QueryParamsTest.php @@ -0,0 +1,272 @@ + 'value1', + 'param2' => 'value2', + ]; + + public function testSetSearchAddsToParams(): void + { + $searchText = 'search_text'; + + $params = QueryParams::setSearch(self::PARAMS, $searchText); + $this->assertSame(self::PARAMS + ['search' => $searchText], $params); + } + + public function testSetSearchReplacedCurrentValue(): void + { + $params = self::PARAMS + ['search' => 'old_search']; + $searchText = 'new_search'; + + $params = QueryParams::setSearch($params, $searchText); + $this->assertSame(self::PARAMS + ['search' => $searchText], $params); + } + + public function testSetOrderWithDirection(): void + { + $params = QueryParams::setOrder(self::PARAMS, 'field', OrderDirection::DESC); + $this->assertSame(self::PARAMS + ['order' => 'field,desc'], $params); + } + + public function testSetOrderWithoutDirection(): void + { + $params = QueryParams::setOrder(self::PARAMS, 'field'); + $this->assertSame(self::PARAMS + ['order' => 'field,asc'], $params); + } + + public function testSetOrderAddsToExistingValue(): void + { + $params = self::PARAMS + ['order' => 'field1,desc']; + $params = QueryParams::setOrder($params, 'field2'); + $this->assertSame(self::PARAMS + ['order' => 'field1,desc;field2,asc'], $params); + } + + public function testSetCorrectMultipleOrders(): void + { + $params = self::PARAMS + ['order' => 'field1,desc']; + $params = QueryParams::setOrder($params, [ + ['field2'], + ['field3', 'desc'], + ['field4', OrderDirection::ASC], + ]); + $this->assertSame(self::PARAMS + ['order' => 'field1,desc;field2,asc;field3,desc;field4,asc'], $params); + } + + public function testSetIncorrectMultipleOrders(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Each order must be an array'); + + QueryParams::setOrder(self::PARAMS, ['field2']); + } + + public function testSetLimitAddsToParams(): void + { + $limit = 100; + $params = QueryParams::setLimit(self::PARAMS, $limit); + $this->assertSame(self::PARAMS + ['limit' => (string) $limit], $params); + } + + public function testSetLimitReplacedCurrentValue(): void + { + $limit = 200; + $params = QueryParams::setLimit(self::PARAMS + ['limit' => '100'], $limit); + $this->assertSame(self::PARAMS + ['limit' => (string) $limit], $params); + } + + public function testSetOffsetAddsToParams(): void + { + $offset = 100; + $params = QueryParams::setOffset(self::PARAMS, $offset); + $this->assertSame(self::PARAMS + ['offset' => (string) $offset], $params); + } + + public function testSetOffsetReplacedCurrentValue(): void + { + $offset = 200; + $params = QueryParams::setOffset(self::PARAMS + ['offset' => '100'], $offset); + $this->assertSame(self::PARAMS + ['offset' => (string) $offset], $params); + } + + public function testSetFilterWithSignAddsToParams(): void + { + $params = QueryParams::setFilter(self::PARAMS, 'field', FilterSign::NEQ, false); + $this->assertSame(self::PARAMS + ['filter' => 'field!=false'], $params); + } + + public function testSetFilterWithoutSignAddsToParams(): void + { + $params = QueryParams::setFilter(self::PARAMS, 'field', 123.45); + $this->assertSame(self::PARAMS + ['filter' => 'field=123.45'], $params); + } + + public function testSetFilterAddedToExistingValue(): void + { + $params = self::PARAMS + ['filter' => 'field1!=value1']; + $params = QueryParams::setFilter($params, 'field2', 100); + $this->assertSame(self::PARAMS + ['filter' => 'field1!=value1;field2=100'], $params); + } + + public function testSetFilterEscapesSemicolon(): void + { + $params = QueryParams::setFilter(self::PARAMS, 'field', FilterSign::GT, 'as;df'); + $this->assertSame(self::PARAMS + ['filter' => 'field>as\;df'], $params); + } + + public function testSetFilterWithOnlyKeyThrowsException(): void + { + $key = 'field'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("For filter key '$key': sign missed"); + + QueryParams::setFilter(self::PARAMS, $key); + } + + public function testSetFilterWithEnumSignWithoutValueThrowsException(): void + { + $key = 'field'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("For filter key '$key': with a sign, you must pass the value as the third parameter"); + + QueryParams::setFilter(self::PARAMS, $key, FilterSign::EQ); + } + + public function testSetFilterWithIncorrectSignTypeWithValueThrowsException(): void + { + $key = 'field'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("For filter key '$key': with a value, sign must be a string or " . FilterSign::class); + + QueryParams::setFilter(self::PARAMS, $key, 33, 'value'); + } + + public function testSetCorrectMultipleFilters(): void + { + $params = self::PARAMS + ['filter' => 'field1=value1']; + $params = QueryParams::setFilter($params, [ + ['field2', true], + ['field3', '!=', 123.45], + ['field4', FilterSign::LIKE, 'val;ue4'], + ]); + + $expectedParams = self::PARAMS + ['filter' => 'field1=value1;field2=true;field3!=123.45;field4~val\;ue4']; + $this->assertSame($expectedParams, $params); + } + + public function testSetIncorrectMultipleFilters(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Each filter must be an array'); + + QueryParams::setFilter(self::PARAMS, ['field2']); + } + + public function testSetExpandAddsToParams(): void + { + $field = 'field'; + + $params = QueryParams::setExpand(self::PARAMS, $field); + $this->assertSame(self::PARAMS + ['expand' => $field], $params); + } + + public function testSetExpandAddedToExistingValue(): void + { + $params = self::PARAMS + ['expand' => 'field1']; + + $params = QueryParams::setExpand($params, 'field2'); + $this->assertSame(self::PARAMS + ['expand' => 'field1,field2'], $params); + } + + public function testSetCorrectMultipleExpands(): void + { + $params = self::PARAMS + ['expand' => 'field1']; + $params = QueryParams::setExpand($params, ['field2', 'field3']); + $this->assertSame(self::PARAMS + ['expand' => 'field1,field2,field3'], $params); + } + + public function testSetIncorrectMultipleExpands(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Each expand must be a string'); + + QueryParams::setExpand(self::PARAMS, ['field2', false]); + } + + public function testSetParamAddsToParams(): void + { + $params = QueryParams::setParam(self::PARAMS, 'param', false); + $this->assertSame(self::PARAMS + ['param' => 'false'], $params); + } + + public function testSetUnknownParamReplacedCurrentParam(): void + { + $params = self::PARAMS + ['param3' => 'value1']; + + $params = QueryParams::setParam($params, 'param3', false); + $this->assertSame(self::PARAMS + ['param3' => 'false'], $params); + } + + public function testSetKnownAddableParamAddedToExistingParam(): void + { + $params = self::PARAMS + ['filter' => 'field1=value1']; + + $params = QueryParams::setParam($params, 'filter', 'field2!=v;alue2'); + $this->assertSame(self::PARAMS + ['filter' => 'field1=value1;field2!=v;alue2'], $params); + } + + public function testSetKnownParamCaseIndependent(): void + { + $params = self::PARAMS + ['expand' => 'field1']; + + $params = QueryParams::setParam($params, 'expand', 'field2'); + $this->assertSame(self::PARAMS + ['expand' => 'field1,field2'], $params); + } + + public function testSetSingleParamWithoutValueThrowsException(): void + { + $param = 'param'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Value can't be null for the key '$param'"); + + QueryParams::setParam(self::PARAMS, $param); + } + + public function testSetCorrectMultipleParams(): void + { + $params = self::PARAMS + ['order' => 'field1,desc']; + $params = QueryParams::setParam($params, [ + ['order', 'field2,asc'], + ['expand', 'field1'], + ['expand', 'field2'], + ]); + + $expectedParams = self::PARAMS + [ + 'order' => 'field1,desc;field2,asc', + 'expand' => 'field1,field2', + ]; + $this->assertSame($expectedParams, $params); + } + + public function testSetIncorrectMultipleOParams(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Each param must be an array'); + + QueryParams::setParam(self::PARAMS, ['field2']); + } +} diff --git a/tests/Unit/Services/RecordHelperTest.php b/tests/Unit/Services/RecordHelperTest.php new file mode 100644 index 00000000..4b070bc0 --- /dev/null +++ b/tests/Unit/Services/RecordHelperTest.php @@ -0,0 +1,30 @@ +ms = new MoySklad(['token']); + } + + public function testObjectWithRowsIsCollection(): void + { + $this->assertTrue(RecordHelper::isCollection($this->ms, ['rows' => []])); + } + + public function testObjectWithoutRowsIsNotCollection(): void + { + $this->assertFalse(RecordHelper::isCollection($this->ms, [])); + } +} diff --git a/tests/Unit/Services/RecordMappingHelperTest.php b/tests/Unit/Services/RecordMappingHelperTest.php new file mode 100644 index 00000000..bd4d516f --- /dev/null +++ b/tests/Unit/Services/RecordMappingHelperTest.php @@ -0,0 +1,75 @@ + 'test_name', + 'archived' => false, + 'amount' => 1.23, + ]; + + /** @dataProvider standardEntities */ + public function testResolvingRegisteredObject(string $type, string $expectedObjectClass): void + { + $object = RecordMappingHelper::resolveObject($this->getMoySklad(), $type, self::CONTENT); + + $this->assertInstanceOf($expectedObjectClass, $object); + } + + public function testResolvingUnregisteredObjectThrowsException(): void + { + $type = 'unregistered_type'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Object type '$type' has no mapped class"); + + RecordMappingHelper::resolveObject($this->getMoySklad(), $type); + } + + /** @dataProvider standardEntities */ + public function testResolvingRegisteredCollection(string $type, string $expectedObjectClass, string $expectedCollectionClass): void + { + $object = RecordMappingHelper::resolveCollection($this->getMoySklad(true), $type); + + $this->assertInstanceOf($expectedCollectionClass, $object); + } + + public function testResolvingUnregisteredCollectionThrowsException(): void + { + $type = 'unregistered_type'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Collection type '$type' has no mapped class"); + + RecordMappingHelper::resolveCollection($this->getMoySklad(), $type); + } + + public static function standardEntities(): array + { + return [ + [Entity::PRODUCT, Product::class, ProductCollection::class], + [Entity::EMPLOYEE, Employee::class, EmployeeCollection::class], + ]; + } + + private function getMoySklad(bool $withRecordFormat = false): MoySklad + { + return $withRecordFormat ? + new MoySklad(['token'], new RecordFormat()) : + new MoySklad(['token']); + } +} diff --git a/tests/Unit/Services/UrlTest.php b/tests/Unit/Services/UrlTest.php index 996cc438..f1489bbf 100644 --- a/tests/Unit/Services/UrlTest.php +++ b/tests/Unit/Services/UrlTest.php @@ -5,12 +5,16 @@ use Evgeek\Moysklad\Enums\HttpMethod; use Evgeek\Moysklad\Http\Payload; use Evgeek\Moysklad\Services\Url; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; +use RuntimeException; /** @covers \Evgeek\Moysklad\Services\Url */ class UrlTest extends TestCase { private const API_URL = 'https://online.moysklad.ru/api/remap/1.2'; + private const GUID1 = '25cf41f2-b068-11ed-0a80-0e9700500d7e'; + private const GUID2 = 'f731148b-a93d-11ed-0a80-0fba0011a6c6'; /** @dataProvider payloadDataProvider */ public function testMakeWithCorrectPayload(string $expectedPath, array $path, array $params = []): void @@ -20,24 +24,7 @@ public function testMakeWithCorrectPayload(string $expectedPath, array $path, ar $this->assertSame(self::API_URL . $expectedPath, Url::make($payload)); } - public function testMakeWithWrongPath(): void - { - $path = [['entity'], ['product']]; - $payload = new Payload(HttpMethod::GET, $path, [], []); - - $this->expectWarning(); - $this->expectWarningMessage('Array to string conversion'); - - Url::make($payload); - } - - /** @dataProvider mixedValuesDataProvider */ - public function testConvertMixedValueToString(string $expectedValue, mixed $value): void - { - $this->assertSame($expectedValue, Url::convertMixedValueToString($value)); - } - - private function payloadDataProvider(): array + public static function payloadDataProvider(): array { return [ [ @@ -57,7 +44,29 @@ private function payloadDataProvider(): array ]; } - private function mixedValuesDataProvider(): array + public function testMakeWithWrongPath(): void + { + $path = [['entity']]; + $payload = new Payload(HttpMethod::GET, $path, [], []); + + set_error_handler(static function (int $code, string $message): never { + throw new RuntimeException($message, $code); + }, E_ALL); + + $this->expectExceptionMessage('Array to string conversion'); + + Url::make($payload); + + restore_error_handler(); + } + + /** @dataProvider mixedValuesDataProvider */ + public function testConvertMixedValueToString(string $expectedValue, mixed $value): void + { + $this->assertSame($expectedValue, Url::convertMixedValueToString($value)); + } + + public static function mixedValuesDataProvider(): array { return [ ['', ''], @@ -75,4 +84,78 @@ private function mixedValuesDataProvider(): array ['-1.0001', -1.0001], ]; } + + /** @dataProvider urlForParsingDataProvider */ + public function testParsePathAndParamsWorksWithCorrectUrl(string $url, array $expectedPath, array $expectedParams): void + { + [$path, $params] = Url::parsePathAndParams($url); + + $this->assertSame($expectedPath, $path); + $this->assertSame($expectedParams, $params); + } + + public static function urlForParsingDataProvider(): array + { + return [ + [URL::API . '/segment1/segment2', ['segment1', 'segment2'], []], + [URL::API . '/segment3/segment4/', ['segment3', 'segment4'], []], + [ + URL::API . '/segment1/segment2?param1=value1¶m2=value2', + [ + 'segment1', + 'segment2', + ], + [ + 'param1' => 'value1', + 'param2' => 'value2', + ], + ], + [ + URL::API . '/segment1/segment2/?param1=value1¶m2=value2', + [ + 'segment1', + 'segment2', + ], + [ + 'param1' => 'value1', + 'param2' => 'value2', + ], + ], + [ + URL::API . '/segment3/segment4?filter=aa=bb;cc!=d\;d', + [ + 'segment3', + 'segment4', + ], + [ + 'filter' => 'aa=bb;cc!=d\;d', + ], + ], + ]; + } + + public function testParsePathAndParamsWorksWithWrongUrl(): void + { + $url = 'https://wrong-url.com'; + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Url '$url' does not belongs to Moysklad JSON API v1.2"); + + Url::parsePathAndParams($url); + } + + /** @dataProvider urlForGetIdDataProvider */ + public function testGetId(string $url, ?string $expectedId): void + { + $this->assertSame($expectedId, Url::getId($url)); + } + + public static function urlForGetIdDataProvider(): array + { + return [ + [URL::API . '/' . self::GUID1, self::GUID1], + [URL::API . '/' . self::GUID1 . '/', self::GUID1], + [URL::API . '/' . self::GUID1 . '/' . self::GUID2, self::GUID2], + [URL::API . '/' . self::GUID1 . '/segment', null], + ]; + } } diff --git a/tests/Unit/Tools/GuidTest.php b/tests/Unit/Tools/GuidTest.php index 7da2a4b5..9ee9def3 100644 --- a/tests/Unit/Tools/GuidTest.php +++ b/tests/Unit/Tools/GuidTest.php @@ -18,19 +18,7 @@ public function testExtractAll(array $expected, string $url): void $this->assertSame($expected, Guid::extractAll($url)); } - /** @dataProvider extractFirstDataProvider */ - public function testExtractFirst(?string $expected, string $url): void - { - $this->assertSame($expected, Guid::extractFirst($url)); - } - - /** @dataProvider extractLastDataProvider */ - public function testExtractLast(?string $expected, string $url): void - { - $this->assertSame($expected, Guid::extractLast($url)); - } - - private function extractAllDataProvider(): array + public static function extractAllDataProvider(): array { return [ [[], 'entity/product/position'], @@ -40,7 +28,13 @@ private function extractAllDataProvider(): array ]; } - private function extractFirstDataProvider(): array + /** @dataProvider extractFirstDataProvider */ + public function testExtractFirst(?string $expected, string $url): void + { + $this->assertSame($expected, Guid::extractFirst($url)); + } + + public static function extractFirstDataProvider(): array { return [ [null, 'entity/product/position'], @@ -50,7 +44,13 @@ private function extractFirstDataProvider(): array ]; } - private function extractLastDataProvider(): array + /** @dataProvider extractLastDataProvider */ + public function testExtractLast(?string $expected, string $url): void + { + $this->assertSame($expected, Guid::extractLast($url)); + } + + public static function extractLastDataProvider(): array { return [ [null, 'entity/product/position'], @@ -59,4 +59,14 @@ private function extractLastDataProvider(): array [self::GUID1, 'method/' . self::GUID3 . '/method/' . self::GUID2 . '/method/' . self::GUID1], ]; } + + public function testIsGuidRecognizeCorrectGuid(): void + { + $this->assertTrue(Guid::check(self::GUID1)); + } + + public function testIsGuidRejectIncorrectGuid(): void + { + $this->assertFalse(Guid::check('wrong-guid')); + } } diff --git a/tests/Unit/Tools/MetaTest.php b/tests/Unit/Tools/MetaTest.php index 75e39497..ea68b497 100644 --- a/tests/Unit/Tools/MetaTest.php +++ b/tests/Unit/Tools/MetaTest.php @@ -41,6 +41,15 @@ public function testCreateFromPathWithStringSegmentsWorks(string $expectedSegmen $this->assertSame($expected, $meta); } + public static function correctPathAndTypeDataProvider(): array + { + return [ + ['', [], 'type1'], + ['/endpoint', ['endpoint'], 'type2'], + ['/endpoint/method', ['endpoint', 'method'], 'type3'], + ]; + } + /** @dataProvider incorrectPathSegmentDataProvider */ public function testCreateFromPathWithNotStringSegmentsDoesNotWorks(mixed $segment): void { @@ -50,6 +59,19 @@ public function testCreateFromPathWithNotStringSegmentsDoesNotWorks(mixed $segme Meta::create(['endpoint', $segment], 'type'); } + public static function incorrectPathSegmentDataProvider(): array + { + return [ + [null], + [false], + [true], + [0], + [1.1], + [['segment']], + [new stdClass()], + ]; + } + public function testEntity(): void { Meta::setFormat(new ArrayFormat()); @@ -68,7 +90,7 @@ public function testState(): void { Meta::setFormat(new ArrayFormat()); - $meta = Meta::state('guid-state', 'product'); + $meta = Meta::state('product', 'guid-state'); $expected = [ 'href' => Url::API . '/entity/product/metadata/states/guid-state', 'type' => 'state', @@ -80,7 +102,7 @@ public function testState(): void public function testOrganization(): void { - $this->assertMetaMethodNyGuidWorks( + $this->assertMetaMethodByGuidWorks( 'organization', '/entity/organization/guid1', 'organization', @@ -90,7 +112,7 @@ public function testOrganization(): void public function testCounterparty(): void { - $this->assertMetaMethodNyGuidWorks( + $this->assertMetaMethodByGuidWorks( 'counterparty', '/entity/counterparty/guid2', 'counterparty', @@ -100,7 +122,7 @@ public function testCounterparty(): void public function testStore(): void { - $this->assertMetaMethodNyGuidWorks( + $this->assertMetaMethodByGuidWorks( 'store', '/entity/store/guid', 'store', @@ -110,7 +132,7 @@ public function testStore(): void public function testCurrency(): void { - $this->assertMetaMethodNyGuidWorks( + $this->assertMetaMethodByGuidWorks( 'currency', '/entity/currency/guid', 'currency', @@ -120,7 +142,7 @@ public function testCurrency(): void public function testSaleschannel(): void { - $this->assertMetaMethodNyGuidWorks( + $this->assertMetaMethodByGuidWorks( 'saleschannel', '/entity/saleschannel/guid', 'saleschannel', @@ -130,7 +152,7 @@ public function testSaleschannel(): void public function testProduct(): void { - $this->assertMetaMethodNyGuidWorks( + $this->assertMetaMethodByGuidWorks( 'product', '/entity/product/guid', 'product', @@ -138,9 +160,29 @@ public function testProduct(): void ); } + public function testEmployee(): void + { + $this->assertMetaMethodByGuidWorks( + 'employee', + '/entity/employee/guid', + 'employee', + 'guid' + ); + } + + public function testCustomerorder(): void + { + $this->assertMetaMethodByGuidWorks( + 'customerorder', + '/entity/customerorder/guid', + 'customerorder', + 'guid' + ); + } + public function testService(): void { - $this->assertMetaMethodNyGuidWorks( + $this->assertMetaMethodByGuidWorks( 'service', '/entity/service/guid', 'service', @@ -148,7 +190,7 @@ public function testService(): void ); } - private function assertMetaMethodNyGuidWorks(string $method, string $expectedSegment, string $expectedType, string $guid) + private function assertMetaMethodByGuidWorks(string $method, string $expectedSegment, string $expectedType, string $guid): void { Meta::setFormat(new ArrayFormat()); @@ -161,26 +203,4 @@ private function assertMetaMethodNyGuidWorks(string $method, string $expectedSeg $this->assertSame($expected, $meta); } - - private function correctPathAndTypeDataProvider(): array - { - return [ - ['', [], 'type1'], - ['/endpoint', ['endpoint'], 'type2'], - ['/endpoint/method', ['endpoint', 'method'], 'type3'], - ]; - } - - private function incorrectPathSegmentDataProvider(): array - { - return [ - [null], - [false], - [true], - [0], - [1.1], - [['segment']], - [new stdClass()], - ]; - } }