CHANGELOG.md000064400000045042145331651420006366 0ustar00# Change Log ## [1.2.0](https://github.com/guzzle/guzzle-services/tree/1.2.0) (2020-11-13) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.2.0...HEAD) **Closed issues:** - Fix weird "equal equal not" operator [\#154](https://github.com/guzzle/guzzle-services/issues/154) **Merged pull requests:** - Support Guzzle 7 [\#176](https://github.com/guzzle/guzzle-services/pull/176) ([ptlevi](https://github.com/ptlevi)) ## [1.1.3](https://github.com/guzzle/guzzle-services/tree/1.1.3) (2017-10-06) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.2...HEAD) **Closed issues:** - Parameter type configuration causes issue when filters change input type [\#147](https://github.com/guzzle/guzzle-services/issues/147) **Merged pull requests:** - Use wire name when visiting array [\#152](https://github.com/guzzle/guzzle-services/pull/152) ([my2ter](https://github.com/my2ter)) - Adding descriptive error message on parameter failure [\#144](https://github.com/guzzle/guzzle-services/pull/144) ([igorsantos07](https://github.com/igorsantos07)) ## [1.1.2](https://github.com/guzzle/guzzle-services/tree/1.1.2) (2017-05-19) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.1...1.1.2) **Closed issues:** - Default values ignored in 1.1 [\#146](https://github.com/guzzle/guzzle-services/issues/146) - Operations extends is broken in 1.1.1 [\#145](https://github.com/guzzle/guzzle-services/issues/145) ## [1.1.1](https://github.com/guzzle/guzzle-services/tree/1.1.1) (2017-05-15) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.1.0...1.1.1) **Closed issues:** - Filters are applied twice [\#134](https://github.com/guzzle/guzzle-services/issues/134) - Is it possible to NOT urlencode a specific uri parameter value? [\#97](https://github.com/guzzle/guzzle-services/issues/97) **Merged pull requests:** - Fix minor typos in documentation. [\#139](https://github.com/guzzle/guzzle-services/pull/139) ([forevermatt](https://github.com/forevermatt)) - Do not mutate command at validation [\#135](https://github.com/guzzle/guzzle-services/pull/135) ([danizord](https://github.com/danizord)) - Added tests for JSON array of arrays and array of objects [\#131](https://github.com/guzzle/guzzle-services/pull/131) ([selfcatering](https://github.com/selfcatering)) - Allow filters on response model [\#138](https://github.com/guzzle/guzzle-services/pull/138) ([danizord](https://github.com/danizord)) - Exposing properties to a parent class [\#136](https://github.com/guzzle/guzzle-services/pull/136) ([Napas](https://github.com/Napas)) ## [1.1.0](https://github.com/guzzle/guzzle-services/tree/1.1.0) (2017-01-31) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.0.1...1.1.0) **Closed issues:** - Grab a list of objects when they are not located at top level of a json response \(HATEOAS\) [\#90](https://github.com/guzzle/guzzle-services/issues/90) - Regression of Issue \#51 - XmlLocation response not handling multiple tags of the same name correctly [\#82](https://github.com/guzzle/guzzle-services/issues/82) - PUT requests with parameters with location of "postField" result in Exception [\#78](https://github.com/guzzle/guzzle-services/issues/78) - Allow to provide Post Body as an Array [\#77](https://github.com/guzzle/guzzle-services/issues/77) **Merged pull requests:** - Bring more flexibility to query params serialization [\#132](https://github.com/guzzle/guzzle-services/pull/132) ([bakura10](https://github.com/bakura10)) - Allow to fix validation for parameters with a format [\#130](https://github.com/guzzle/guzzle-services/pull/130) ([bakura10](https://github.com/bakura10)) ## [1.0.1](https://github.com/guzzle/guzzle-services/tree/1.0.1) (2017-01-13) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/1.0.0...1.0.1) **Implemented enhancements:** - Set a name when pushing ValidatedDescriptionHandler to stack [\#127](https://github.com/guzzle/guzzle-services/issues/127) **Fixed bugs:** - combine method in Uri [\#101](https://github.com/guzzle/guzzle-services/issues/101) - Undefined Variable [\#88](https://github.com/guzzle/guzzle-services/issues/88) - Regression in array parameter serialization [\#128](https://github.com/guzzle/guzzle-services/issues/128) - Unable to POST multiple multipart parameters [\#123](https://github.com/guzzle/guzzle-services/issues/123) **Closed issues:** - Tag pre 1.0.0 release [\#121](https://github.com/guzzle/guzzle-services/issues/121) - Adjust inline documentation of Parameter [\#120](https://github.com/guzzle/guzzle-services/issues/120) - postField location not recognized after upgrading to 1.0 [\#119](https://github.com/guzzle/guzzle-services/issues/119) - Create a new release for the guzzle6 branch [\#118](https://github.com/guzzle/guzzle-services/issues/118) - Compatibility problem with PHP7.0 ? [\#116](https://github.com/guzzle/guzzle-services/issues/116) - What is the correct type of Parameter static option [\#113](https://github.com/guzzle/guzzle-services/issues/113) - Improve the construction of baseUri in Description [\#112](https://github.com/guzzle/guzzle-services/issues/112) - Please create version tag for current master branch [\#110](https://github.com/guzzle/guzzle-services/issues/110) - Problems with postField params [\#98](https://github.com/guzzle/guzzle-services/issues/98) **Merged pull requests:** - Fix serialization of query params [\#129](https://github.com/guzzle/guzzle-services/pull/129) ([bakura10](https://github.com/bakura10)) ## [1.0.0](https://github.com/guzzle/guzzle-services/tree/1.0.0) (2016-11-24) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.6.0...1.0.0) **Closed issues:** - AbstractClient' not found [\#117](https://github.com/guzzle/guzzle-services/issues/117) **Merged pull requests:** - Make Guzzle Services compatible with Guzzle6 [\#109](https://github.com/guzzle/guzzle-services/pull/109) ([Konafets](https://github.com/Konafets)) ## [0.6.0](https://github.com/guzzle/guzzle-services/tree/0.6.0) (2016-10-21) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.5.0...0.6.0) **Closed issues:** - Broken composer install [\#111](https://github.com/guzzle/guzzle-services/issues/111) - The visit\(\) method is expected to return a RequestInterface but it doesn't in JsonLocation [\#106](https://github.com/guzzle/guzzle-services/issues/106) - Allow parameters in baseUrl [\#102](https://github.com/guzzle/guzzle-services/issues/102) - Have default params at client construction, gone away? [\#100](https://github.com/guzzle/guzzle-services/issues/100) - Runtime Exception Error is always empty [\#99](https://github.com/guzzle/guzzle-services/issues/99) - PHP Fatal error: Unsupported operand types in guzzlehttp/guzzle-services/src/GuzzleClient.php on line 72 [\#95](https://github.com/guzzle/guzzle-services/issues/95) - Date of next version [\#94](https://github.com/guzzle/guzzle-services/issues/94) - Map null reponse values to defined reponse model properties [\#91](https://github.com/guzzle/guzzle-services/issues/91) - Map a json-array into a Model [\#80](https://github.com/guzzle/guzzle-services/issues/80) - If property specified in json model but empty, notice raised [\#75](https://github.com/guzzle/guzzle-services/issues/75) - Allow primitive response types for operations [\#73](https://github.com/guzzle/guzzle-services/issues/73) - Allow shortened definition of properties in models [\#71](https://github.com/guzzle/guzzle-services/issues/71) - Where's the ServiceDescriptionLoader/AbstractConfigLoader? [\#68](https://github.com/guzzle/guzzle-services/issues/68) - errorResposnes from operation is never used [\#66](https://github.com/guzzle/guzzle-services/issues/66) - Updating the description [\#65](https://github.com/guzzle/guzzle-services/issues/65) - Parameter type validation is too strict [\#7](https://github.com/guzzle/guzzle-services/issues/7) **Merged pull requests:** - fix code example [\#115](https://github.com/guzzle/guzzle-services/pull/115) ([snoek09](https://github.com/snoek09)) - Bug Fix for GuzzleClient constructor [\#96](https://github.com/guzzle/guzzle-services/pull/96) ([peterfox](https://github.com/peterfox)) - add plugin section to readme [\#93](https://github.com/guzzle/guzzle-services/pull/93) ([gimler](https://github.com/gimler)) - Allow mapping null response values to defined response model properties [\#92](https://github.com/guzzle/guzzle-services/pull/92) ([shaun785](https://github.com/shaun785)) - Updated exception message for better debugging [\#85](https://github.com/guzzle/guzzle-services/pull/85) ([stovak](https://github.com/stovak)) - Gracefully handle null return from $this-\>getConfig\('defaults'\) [\#84](https://github.com/guzzle/guzzle-services/pull/84) ([fuhry](https://github.com/fuhry)) - Fixing issue \#82 to address regression for handling elements with the sa... [\#83](https://github.com/guzzle/guzzle-services/pull/83) ([sprak3000](https://github.com/sprak3000)) - Fix for specified property but no value in json \(notice for undefined in... [\#76](https://github.com/guzzle/guzzle-services/pull/76) ([rfink](https://github.com/rfink)) - Add ErrorHandler subscriber [\#67](https://github.com/guzzle/guzzle-services/pull/67) ([bakura10](https://github.com/bakura10)) - Fix combine base url and command uri [\#108](https://github.com/guzzle/guzzle-services/pull/108) ([vlastv](https://github.com/vlastv)) - Fixing JsonLocation::visit\(\) not returning a request \#106 [\#107](https://github.com/guzzle/guzzle-services/pull/107) ([Pinolo](https://github.com/Pinolo)) - Fix call to undefined method "GuzzleHttp\Psr7\Uri::combine" [\#105](https://github.com/guzzle/guzzle-services/pull/105) ([horrorin](https://github.com/horrorin)) - fix description for get request example [\#87](https://github.com/guzzle/guzzle-services/pull/87) ([snoek09](https://github.com/snoek09)) - Allow raw values \(non array/object\) for root model definitions [\#74](https://github.com/guzzle/guzzle-services/pull/74) ([rfink](https://github.com/rfink)) - Allow shortened definition of properties by assigning them directly to a type [\#72](https://github.com/guzzle/guzzle-services/pull/72) ([rfink](https://github.com/rfink)) ## [0.5.0](https://github.com/guzzle/guzzle-services/tree/0.5.0) (2014-12-23) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.4.0...0.5.0) **Closed issues:** - Does it supports custom class instantiate to define an operation using a service description [\#62](https://github.com/guzzle/guzzle-services/issues/62) - Tag version 0.4.0 [\#61](https://github.com/guzzle/guzzle-services/issues/61) - XmlLocation not adding attributes to non-leaf child nodes [\#52](https://github.com/guzzle/guzzle-services/issues/52) - XmlLocation response not handling multiple tags of the same name correctly [\#51](https://github.com/guzzle/guzzle-services/issues/51) - Validation Bug [\#47](https://github.com/guzzle/guzzle-services/issues/47) - CommandException doesn't contain response data [\#44](https://github.com/guzzle/guzzle-services/issues/44) - \[Fix included\] XmlLocation requires text value to have attributes [\#37](https://github.com/guzzle/guzzle-services/issues/37) - Question: Mocking a Response does not throw exception [\#35](https://github.com/guzzle/guzzle-services/issues/35) - allow default 'location' on Model [\#26](https://github.com/guzzle/guzzle-services/issues/26) - create mock subscriber requests from descriptions [\#25](https://github.com/guzzle/guzzle-services/issues/25) **Merged pull requests:** - Documentation: Add 'boolean-string' as a supported "format" value [\#63](https://github.com/guzzle/guzzle-services/pull/63) ([jwcobb](https://github.com/jwcobb)) ## [0.4.0](https://github.com/guzzle/guzzle-services/tree/0.4.0) (2014-11-03) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.3.0...0.4.0) **Closed issues:** - Exceptions Thrown From Subscribers Are Ignored? [\#58](https://github.com/guzzle/guzzle-services/issues/58) - Totally Broken With Guzzle 5 [\#57](https://github.com/guzzle/guzzle-services/issues/57) - GuzzleHTTP/Command Dependency fail [\#50](https://github.com/guzzle/guzzle-services/issues/50) - Request parameter PathLocation [\#46](https://github.com/guzzle/guzzle-services/issues/46) - Requesting a new version tag [\#45](https://github.com/guzzle/guzzle-services/issues/45) - CommandException expects second parameter to be CommandTransaction instance [\#43](https://github.com/guzzle/guzzle-services/issues/43) - Cannot add Autorization header to my requests [\#39](https://github.com/guzzle/guzzle-services/issues/39) - Resouce Itterators [\#36](https://github.com/guzzle/guzzle-services/issues/36) - Question [\#33](https://github.com/guzzle/guzzle-services/issues/33) - query location array can be comma separated [\#31](https://github.com/guzzle/guzzle-services/issues/31) - Automatically returns array from command? [\#30](https://github.com/guzzle/guzzle-services/issues/30) - Arrays nested under objects in JSON response broken? [\#27](https://github.com/guzzle/guzzle-services/issues/27) - Question? [\#23](https://github.com/guzzle/guzzle-services/issues/23) **Merged pull requests:** - Bump the version in the readme [\#60](https://github.com/guzzle/guzzle-services/pull/60) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Bump the next version to 0.4 [\#56](https://github.com/guzzle/guzzle-services/pull/56) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Fixed the guzzlehttp/command version constraint [\#55](https://github.com/guzzle/guzzle-services/pull/55) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Work with latest Guzzle 5 and Command updates [\#54](https://github.com/guzzle/guzzle-services/pull/54) ([mtdowling](https://github.com/mtdowling)) - Addressing Issue \#51 & Issue \#52 [\#53](https://github.com/guzzle/guzzle-services/pull/53) ([sprak3000](https://github.com/sprak3000)) - added description interface to extend it [\#49](https://github.com/guzzle/guzzle-services/pull/49) ([danieledangeli](https://github.com/danieledangeli)) - Update readme to improve documentation \(\#46\) [\#48](https://github.com/guzzle/guzzle-services/pull/48) ([bonndan](https://github.com/bonndan)) - Fixed the readme version constraint [\#42](https://github.com/guzzle/guzzle-services/pull/42) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Update .travis.yml [\#41](https://github.com/guzzle/guzzle-services/pull/41) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Added a branch alias [\#40](https://github.com/guzzle/guzzle-services/pull/40) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Fixes Response\XmlLocation requires text value [\#38](https://github.com/guzzle/guzzle-services/pull/38) ([magnetik](https://github.com/magnetik)) - Removing unnecessary \(\) from docblock [\#32](https://github.com/guzzle/guzzle-services/pull/32) ([jamiehannaford](https://github.com/jamiehannaford)) - Fix JSON response location so that both is supported: arrays nested unde... [\#28](https://github.com/guzzle/guzzle-services/pull/28) ([ukautz](https://github.com/ukautz)) - Throw Any Exceptions On Process [\#59](https://github.com/guzzle/guzzle-services/pull/59) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Allow extension to work recursively over models [\#34](https://github.com/guzzle/guzzle-services/pull/34) ([jamiehannaford](https://github.com/jamiehannaford)) - A custom class can be configured for command instances. [\#29](https://github.com/guzzle/guzzle-services/pull/29) ([robinvdvleuten](https://github.com/robinvdvleuten)) - \[WIP\] doing some experimentation [\#24](https://github.com/guzzle/guzzle-services/pull/24) ([cordoval](https://github.com/cordoval)) ## [0.3.0](https://github.com/guzzle/guzzle-services/tree/0.3.0) (2014-06-01) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.2.0...0.3.0) **Closed issues:** - Testing Guzzle Services doesn't work [\#19](https://github.com/guzzle/guzzle-services/issues/19) - Description factory [\#18](https://github.com/guzzle/guzzle-services/issues/18) - support to load service description from file [\#15](https://github.com/guzzle/guzzle-services/issues/15) - Update dependency on guzzlehttp/command [\#11](https://github.com/guzzle/guzzle-services/issues/11) **Merged pull requests:** - Add license file [\#22](https://github.com/guzzle/guzzle-services/pull/22) ([siwinski](https://github.com/siwinski)) - Fix 'Invalid argument supplied for foreach\(\)' [\#21](https://github.com/guzzle/guzzle-services/pull/21) ([Olden](https://github.com/Olden)) - Fixed string zero \('0'\) values not being filtered in XML. [\#20](https://github.com/guzzle/guzzle-services/pull/20) ([dragonwize](https://github.com/dragonwize)) - baseUrl can be a string or an uri template [\#16](https://github.com/guzzle/guzzle-services/pull/16) ([robinvdvleuten](https://github.com/robinvdvleuten)) ## [0.2.0](https://github.com/guzzle/guzzle-services/tree/0.2.0) (2014-03-30) [Full Changelog](https://github.com/guzzle/guzzle-services/compare/0.1.0...0.2.0) **Closed issues:** - please remove wiki [\#13](https://github.com/guzzle/guzzle-services/issues/13) - Parameter validation fails for union types [\#12](https://github.com/guzzle/guzzle-services/issues/12) - question on integration with Guzzle4 [\#8](https://github.com/guzzle/guzzle-services/issues/8) - typehints for operations property [\#6](https://github.com/guzzle/guzzle-services/issues/6) - improve exception message [\#5](https://github.com/guzzle/guzzle-services/issues/5) **Merged pull requests:** - Update composer.json [\#14](https://github.com/guzzle/guzzle-services/pull/14) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Update composer.json [\#9](https://github.com/guzzle/guzzle-services/pull/9) ([GrahamCampbell](https://github.com/GrahamCampbell)) - some fixes [\#4](https://github.com/guzzle/guzzle-services/pull/4) ([cordoval](https://github.com/cordoval)) - Fix the CommandException path used in ValidateInput [\#2](https://github.com/guzzle/guzzle-services/pull/2) ([mookle](https://github.com/mookle)) - Minor improvements [\#1](https://github.com/guzzle/guzzle-services/pull/1) ([GrahamCampbell](https://github.com/GrahamCampbell)) - Use latest guzzlehttp/command to fix dependencies [\#10](https://github.com/guzzle/guzzle-services/pull/10) ([sbward](https://github.com/sbward)) - some collaboration using Gush :\) [\#3](https://github.com/guzzle/guzzle-services/pull/3) ([cordoval](https://github.com/cordoval)) ## [0.1.0](https://github.com/guzzle/guzzle-services/tree/0.1.0) (2014-03-15) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* LICENSE000064400000002303145331651420005553 0ustar00The MIT License (MIT) Copyright (c) 2014 Michael Dowling Copyright (c) 2014 Graham Campbell Copyright (c) 2016 Stefano Kowalke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. README.md000064400000011020145331651420006021 0ustar00# Guzzle Services Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures. ```php use GuzzleHttp\Client; use GuzzleHttp\Command\Guzzle\GuzzleClient; use GuzzleHttp\Command\Guzzle\Description; $client = new Client(); $description = new Description([ 'baseUri' => 'http://httpbin.org/', 'operations' => [ 'testing' => [ 'httpMethod' => 'GET', 'uri' => '/get{?foo}', 'responseModel' => 'getResponse', 'parameters' => [ 'foo' => [ 'type' => 'string', 'location' => 'uri' ], 'bar' => [ 'type' => 'string', 'location' => 'query' ] ] ] ], 'models' => [ 'getResponse' => [ 'type' => 'object', 'additionalProperties' => [ 'location' => 'json' ] ] ] ]); $guzzleClient = new GuzzleClient($client, $description); $result = $guzzleClient->testing(['foo' => 'bar']); echo $result['args']['foo']; // bar ``` ## Installing This project can be installed using Composer: ``composer require guzzlehttp/guzzle-services`` For **Guzzle 5**, use ``composer require guzzlehttp/guzzle-services:0.6``. **Note:** If Composer is not installed [globally](https://getcomposer.org/doc/00-intro.md#globally) then you may need to run the preceding Composer commands using ``php composer.phar`` (where ``composer.phar`` is the path to your copy of Composer), instead of just ``composer``. ## Plugins * Load Service description from file [https://github.com/gimler/guzzle-description-loader] ## Transition guide from Guzzle 5.0 to 6.0 ### Change regarding PostField and PostFile The request locations `postField` and `postFile` were removed in favor of `formParam` and `multipart`. If your description looks like ```php [ 'baseUri' => 'http://httpbin.org/', 'operations' => [ 'testing' => [ 'httpMethod' => 'GET', 'uri' => '/get{?foo}', 'responseModel' => 'getResponse', 'parameters' => [ 'foo' => [ 'type' => 'string', 'location' => 'postField' ], 'bar' => [ 'type' => 'string', 'location' => 'postFile' ] ] ] ], ] ``` you need to change `postField` to `formParam` and `postFile` to `multipart`. More documentation coming soon. ## Cookbook ### Changing the way query params are serialized By default, query params are serialized using strict RFC3986 rules, using `http_build_query` method. With this, array params are serialized this way: ```php $client->myMethod(['foo' => ['bar', 'baz']]); // Query params will be foo[0]=bar&foo[1]=baz ``` However, a lot of APIs in the wild require the numeric indices to be removed, so that the query params end up being `foo[]=bar&foo[]=baz`. You can easily change the behaviour by creating your own serializer and overriding the "query" request location: ```php use GuzzleHttp\Command\Guzzle\GuzzleClient; use GuzzleHttp\Command\Guzzle\RequestLocation\QueryLocation; use GuzzleHttp\Command\Guzzle\QuerySerializer\Rfc3986Serializer; use GuzzleHttp\Command\Guzzle\Serializer; $queryLocation = new QueryLocation('query', new Rfc3986Serializer(true)); $serializer = new Serializer($description, ['query' => $queryLocation]); $guzzleClient = new GuzzleClient($client, $description, $serializer); ``` You can also create your own serializer if you have specific needs. ## Security If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/guzzle-services/security/policy) for more information. ## License Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information. ## For Enterprise Available as part of the Tidelift Subscription The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-guzzle-services?utm_source=packagist-guzzlehttp-guzzle-services&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) composer.json000064400000003554145331651420007301 0ustar00{ "name": "guzzlehttp/guzzle-services", "description": "Provides an implementation of the Guzzle Command library that uses Guzzle service descriptions to describe web services, serialize requests, and parse responses into easy to use model structures.", "license": "MIT", "authors": [ { "name": "Graham Campbell", "email": "hello@gjcampbell.co.uk", "homepage": "https://github.com/GrahamCampbell" }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, { "name": "Stefano Kowalke", "email": "blueduck@mail.org", "homepage": "https://github.com/Konafets" }, { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", "homepage": "https://github.com/Nyholm" } ], "require": { "php": "^7.2.5 || ^8.0", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/command": "^1.3.1", "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", "guzzlehttp/uri-template": "^1.0.1" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "phpunit/phpunit": "^8.5.19 || ^9.5.8" }, "autoload": { "psr-4": { "GuzzleHttp\\Command\\Guzzle\\": "src/" } }, "autoload-dev": { "psr-4": { "GuzzleHttp\\Tests\\Command\\Guzzle\\": "tests/" } }, "suggest": { "gimler/guzzle-description-loader": "^0.0.4" }, "extra": { "bamarni-bin": { "bin-links": true, "forward-command": false } }, "config": { "preferred-install": "dist", "sort-packages": true, "allow-plugins": { "bamarni/composer-bin-plugin": true } } } src/Description.php000064400000015600145331651420010335 0ustar00{$key} = $config[$key]; } } // Set the baseUri // Account for the old style of using baseUrl if (isset($config['baseUrl'])) { $config['baseUri'] = $config['baseUrl']; } $this->baseUri = isset($config['baseUri']) ? new Uri($config['baseUri']) : new Uri(); // Ensure that the models and operations properties are always arrays $this->models = (array) $this->models; $this->operations = (array) $this->operations; // We want to add operations differently than adding the other properties $defaultKeys[] = 'operations'; // Create operations for each operation if (isset($config['operations'])) { foreach ($config['operations'] as $name => $operation) { if (!is_array($operation)) { throw new \InvalidArgumentException('Operations must be arrays'); } $this->operations[$name] = $operation; } } // Get all of the additional properties of the service description and // store them in a data array foreach (array_diff(array_keys($config), $defaultKeys) as $key) { $this->extraData[$key] = $config[$key]; } // Configure the schema formatter if (isset($options['formatter'])) { $this->formatter = $options['formatter']; } else { static $defaultFormatter; if (!$defaultFormatter) { $defaultFormatter = new SchemaFormatter(); } $this->formatter = $defaultFormatter; } } /** * Get the basePath/baseUri of the description * * @return Uri */ public function getBaseUri() { return $this->baseUri; } /** * Get the API operations of the service * * @return Operation[] Returns an array of {@see Operation} objects */ public function getOperations() { return $this->operations; } /** * Check if the service has an operation by name * * @param string $name Name of the operation to check * * @return bool */ public function hasOperation($name) { return isset($this->operations[$name]); } /** * Get an API operation by name * * @param string $name Name of the command * * @return Operation * * @throws \InvalidArgumentException if the operation is not found */ public function getOperation($name) { if (!$this->hasOperation($name)) { throw new \InvalidArgumentException("No operation found named $name"); } // Lazily create operations as they are retrieved if (!($this->operations[$name] instanceof Operation)) { $this->operations[$name]['name'] = $name; $this->operations[$name] = new Operation($this->operations[$name], $this); } return $this->operations[$name]; } /** * Get a shared definition structure. * * @param string $id ID/name of the model to retrieve * * @return Parameter * * @throws \InvalidArgumentException if the model is not found */ public function getModel($id) { if (!$this->hasModel($id)) { throw new \InvalidArgumentException("No model found named $id"); } // Lazily create models as they are retrieved if (!($this->models[$id] instanceof Parameter)) { $this->models[$id] = new Parameter( $this->models[$id], ['description' => $this] ); } return $this->models[$id]; } /** * Get all models of the service description. * * @return array */ public function getModels() { $models = []; foreach ($this->models as $name => $model) { $models[$name] = $this->getModel($name); } return $models; } /** * Check if the service description has a model by name. * * @param string $id Name/ID of the model to check * * @return bool */ public function hasModel($id) { return isset($this->models[$id]); } /** * Get the API version of the service * * @return string */ public function getApiVersion() { return $this->apiVersion; } /** * Get the name of the API * * @return string */ public function getName() { return $this->name; } /** * Get a summary of the purpose of the API * * @return string */ public function getDescription() { return $this->description; } /** * Format a parameter using named formats. * * @param string $format Format to convert it to * @param mixed $input Input string * * @return mixed */ public function format($format, $input) { return $this->formatter->format($format, $input); } /** * Get arbitrary data from the service description that is not part of the * Guzzle service description specification. * * @param string $key Data key to retrieve or null to retrieve all extra * * @return mixed|null */ public function getData($key = null) { if ($key === null) { return $this->extraData; } elseif (isset($this->extraData[$key])) { return $this->extraData[$key]; } else { return null; } } } src/DescriptionInterface.php000064400000004506145331651420012161 0ustar00 new BodyLocation(), 'header' => new HeaderLocation(), 'reasonPhrase' => new ReasonPhraseLocation(), 'statusCode' => new StatusCodeLocation(), 'xml' => new XmlLocation(), 'json' => new JsonLocation(), ]; } $this->responseLocations = $responseLocations + $defaultResponseLocations; $this->description = $description; $this->process = $process; } /** * Deserialize the response into the specified result representation * * @param RequestInterface|null $request * * @return Result|ResultInterface|void|ResponseInterface */ public function __invoke(ResponseInterface $response, RequestInterface $request, CommandInterface $command) { // If the user don't want to process the result, just return the plain response here if ($this->process === false) { return $response; } $name = $command->getName(); $operation = $this->description->getOperation($name); $this->handleErrorResponses($response, $request, $command, $operation); // Add a default Model as the result if no matching schema was found if (!($modelName = $operation->getResponseModel())) { // Not sure if this should be empty or contains the response. // Decided to do it how it was in the old version for now. return new Result(); } $model = $operation->getServiceDescription()->getModel($modelName); if (!$model) { throw new \RuntimeException("Unknown model: {$modelName}"); } return $this->visit($model, $response); } /** * Handles visit() and after() methods of the Response locations * * @return Result|ResultInterface|void */ protected function visit(Parameter $model, ResponseInterface $response) { $result = new Result(); $context = ['visitors' => []]; if ($model->getType() === 'object') { $result = $this->visitOuterObject($model, $result, $response, $context); } elseif ($model->getType() === 'array') { $result = $this->visitOuterArray($model, $result, $response, $context); } else { throw new \InvalidArgumentException('Invalid response model: '.$model->getType()); } // Call the after() method of each found visitor /** @var ResponseLocationInterface $visitor */ foreach ($context['visitors'] as $visitor) { $result = $visitor->after($result, $response, $model); } return $result; } /** * Handles the before() method of Response locations * * @param string $location * * @return ResultInterface */ private function triggerBeforeVisitor( $location, Parameter $model, ResultInterface $result, ResponseInterface $response, array &$context ) { if (!isset($this->responseLocations[$location])) { throw new \RuntimeException("Unknown location: $location"); } $context['visitors'][$location] = $this->responseLocations[$location]; $result = $this->responseLocations[$location]->before( $result, $response, $model ); return $result; } /** * Visits the outer object * * @return ResultInterface */ private function visitOuterObject( Parameter $model, ResultInterface $result, ResponseInterface $response, array &$context ) { $parentLocation = $model->getLocation(); // If top-level additionalProperties is a schema, then visit it $additional = $model->getAdditionalProperties(); if ($additional instanceof Parameter) { // Use the model location if none set on additionalProperties. $location = $additional->getLocation() ?: $parentLocation; $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context); } // Use 'location' from all individual defined properties, but fall back // to the model location if no per-property location is set. Collect // the properties that need to be visited into an array. $visitProperties = []; foreach ($model->getProperties() as $schema) { $location = $schema->getLocation() ?: $parentLocation; if ($location) { $visitProperties[] = [$location, $schema]; // Trigger the before method on each unique visitor location if (!isset($context['visitors'][$location])) { $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context); } } } // Actually visit each response element foreach ($visitProperties as $property) { $result = $this->responseLocations[$property[0]]->visit($result, $response, $property[1]); } return $result; } /** * Visits the outer array * * @return ResultInterface|void */ private function visitOuterArray( Parameter $model, ResultInterface $result, ResponseInterface $response, array &$context ) { // Use 'location' defined on the top of the model if (!($location = $model->getLocation())) { return; } // Trigger the before method on each unique visitor location if (!isset($context['visitors'][$location])) { $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context); } // Visit each item in the response $result = $this->responseLocations[$location]->visit($result, $response, $model); return $result; } /** * Reads the "errorResponses" from commands, and trigger appropriate exceptions * * In order for the exception to be properly triggered, all your exceptions must be instance * of "GuzzleHttp\Command\Exception\CommandException". If that's not the case, your exceptions will be wrapped * around a CommandException */ protected function handleErrorResponses( ResponseInterface $response, RequestInterface $request, CommandInterface $command, Operation $operation ) { $errors = $operation->getErrorResponses(); // We iterate through each errors in service description. If the descriptor contains both a phrase and // status code, there must be an exact match of both. Otherwise, a match of status code is enough $bestException = null; foreach ($errors as $error) { $code = (int) $error['code']; if ($response->getStatusCode() !== $code) { continue; } if (isset($error['phrase']) && !($error['phrase'] === $response->getReasonPhrase())) { continue; } $bestException = $error['class']; // If there is an exact match of phrase + code, then we cannot find a more specialized exception in // the array, so we can break early instead of iterating the remaining ones if (isset($error['phrase'])) { break; } } if (null !== $bestException) { throw new $bestException($response->getReasonPhrase(), $command, null, $request, $response); } // If we reach here, no exception could be match from descriptor, and Guzzle exception will propagate if // option "http_errors" is set to true, which is the default setting. } } src/GuzzleClient.php000064400000012313145331651420010467 0ustar00config = $config; $this->description = $description; $serializer = $this->getSerializer($commandToRequestTransformer); $deserializer = $this->getDeserializer($responseToResultTransformer); parent::__construct($client, $serializer, $deserializer, $commandHandlerStack); $this->processConfig($config); } /** * Returns the command if valid; otherwise an Exception * * @param string $name * * @return CommandInterface * * @throws \InvalidArgumentException */ public function getCommand($name, array $args = []) { if (!$this->description->hasOperation($name)) { $name = ucfirst($name); if (!$this->description->hasOperation($name)) { throw new \InvalidArgumentException( "No operation found named {$name}" ); } } // Merge in default command options $args += $this->getConfig('defaults'); return parent::getCommand($name, $args); } /** * Return the description * * @return DescriptionInterface */ public function getDescription() { return $this->description; } /** * Returns the passed Serializer when set, a new instance otherwise * * @param callable|null $commandToRequestTransformer * * @return \GuzzleHttp\Command\Guzzle\Serializer */ private function getSerializer($commandToRequestTransformer) { return $commandToRequestTransformer !== null ? $commandToRequestTransformer : new Serializer($this->description); } /** * Returns the passed Deserializer when set, a new instance otherwise * * @param callable|null $responseToResultTransformer * * @return \GuzzleHttp\Command\Guzzle\Deserializer */ private function getDeserializer($responseToResultTransformer) { $process = (!isset($this->config['process']) || $this->config['process'] === true); return $responseToResultTransformer !== null ? $responseToResultTransformer : new Deserializer($this->description, $process); } /** * Get the config of the client * * @param array|string $option * * @return mixed */ public function getConfig($option = null) { return $option === null ? $this->config : (isset($this->config[$option]) ? $this->config[$option] : []); } public function setConfig($option, $value) { $this->config[$option] = $value; } /** * Prepares the client based on the configuration settings of the client. * * @param array $config Constructor config as an array */ protected function processConfig(array $config) { // set defaults as an array if not provided if (!isset($config['defaults'])) { $config['defaults'] = []; } // Add the handlers based on the configuration option $stack = $this->getHandlerStack(); if (!isset($config['validate']) || $config['validate'] === true) { $stack->push(new ValidatedDescriptionHandler($this->description), 'validate_description'); } if (!isset($config['process']) || $config['process'] === true) { // TODO: This belongs to the Deserializer and should be handled there. // Question: What is the result when the Deserializer is bypassed? // Possible answer: The raw response. } } } src/Handler/ValidatedDescriptionHandler.php000064400000005405145331651420015030 0ustar00 */ class ValidatedDescriptionHandler { /** @var SchemaValidator */ private $validator; /** @var DescriptionInterface */ private $description; /** * ValidatedDescriptionHandler constructor. */ public function __construct(DescriptionInterface $description, SchemaValidator $schemaValidator = null) { $this->description = $description; $this->validator = $schemaValidator ?: new SchemaValidator(); } /** * @return \Closure */ public function __invoke(callable $handler) { return function (CommandInterface $command) use ($handler) { $errors = []; $operation = $this->description->getOperation($command->getName()); foreach ($operation->getParams() as $name => $schema) { $value = $command[$name]; if ($value) { $value = $schema->filter($value); } if (!$this->validator->validate($schema, $value)) { $errors = array_merge($errors, $this->validator->getErrors()); } elseif ($value !== $command[$name]) { // Update the config value if it changed and no validation errors were encountered. // This happen when the user extending an operation // See https://github.com/guzzle/guzzle-services/issues/145 $command[$name] = $value; } } if ($params = $operation->getAdditionalParameters()) { foreach ($command->toArray() as $name => $value) { // It's only additional if it isn't defined in the schema if (!$operation->hasParam($name)) { // Always set the name so that error messages are useful $params->setName($name); if (!$this->validator->validate($params, $value)) { $errors = array_merge($errors, $this->validator->getErrors()); } elseif ($value !== $command[$name]) { $command[$name] = $value; } } } } if ($errors) { throw new CommandException('Validation errors: '.implode("\n", $errors), $command); } return $handler($command); }; } } src/Operation.php000064400000020363145331651420010014 0ustar00 '', 'httpMethod' => '', 'uri' => '', 'responseModel' => null, 'notes' => '', 'summary' => '', 'documentationUrl' => null, 'deprecated' => false, 'data' => [], 'parameters' => [], 'additionalParameters' => null, 'errorResponses' => [], ]; $this->description = $description === null ? new Description([]) : $description; if (isset($config['extends'])) { $config = $this->resolveExtends($config['extends'], $config); } $this->config = $config + $defaults; // Account for the old style of using responseClass if (isset($config['responseClass'])) { $this->config['responseModel'] = $config['responseClass']; } $this->resolveParameters(); } /** * @return array */ public function toArray() { return $this->config; } /** * Get the service description that the operation belongs to * * @return Description */ public function getServiceDescription() { return $this->description; } /** * Get the params of the operation * * @return Parameter[] */ public function getParams() { return $this->parameters; } /** * Get additionalParameters of the operation * * @return Parameter|null */ public function getAdditionalParameters() { return $this->additionalParameters; } /** * Check if the operation has a specific parameter by name * * @param string $name Name of the param * * @return bool */ public function hasParam($name) { return isset($this->parameters[$name]); } /** * Get a single parameter of the operation * * @param string $name Parameter to retrieve by name * * @return Parameter|null */ public function getParam($name) { return isset($this->parameters[$name]) ? $this->parameters[$name] : null; } /** * Get the HTTP method of the operation * * @return string|null */ public function getHttpMethod() { return $this->config['httpMethod']; } /** * Get the name of the operation * * @return string|null */ public function getName() { return $this->config['name']; } /** * Get a short summary of what the operation does * * @return string|null */ public function getSummary() { return $this->config['summary']; } /** * Get a longer text field to explain the behavior of the operation * * @return string|null */ public function getNotes() { return $this->config['notes']; } /** * Get the documentation URL of the operation * * @return string|null */ public function getDocumentationUrl() { return $this->config['documentationUrl']; } /** * Get the name of the model used for processing the response. * * @return string */ public function getResponseModel() { return $this->config['responseModel']; } /** * Get whether or not the operation is deprecated * * @return bool */ public function getDeprecated() { return $this->config['deprecated']; } /** * Get the URI that will be merged into the generated request * * @return string */ public function getUri() { return $this->config['uri']; } /** * Get the errors that could be encountered when executing the operation * * @return array */ public function getErrorResponses() { return $this->config['errorResponses']; } /** * Get extra data from the operation * * @param string $name Name of the data point to retrieve or null to * retrieve all of the extra data. * * @return mixed|null */ public function getData($name = null) { if ($name === null) { return $this->config['data']; } elseif (isset($this->config['data'][$name])) { return $this->config['data'][$name]; } else { return null; } } /** * @return array */ private function resolveExtends($name, array $config) { if (!$this->description->hasOperation($name)) { throw new \InvalidArgumentException('No operation named '.$name); } // Merge parameters together one level deep $base = $this->description->getOperation($name)->toArray(); $result = $config + $base; if (isset($base['parameters']) && isset($config['parameters'])) { $result['parameters'] = $config['parameters'] + $base['parameters']; } return $result; } /** * Process the description and extract the parameter config * * @return void */ private function resolveParameters() { // Parameters need special handling when adding foreach ($this->config['parameters'] as $name => $param) { if (!is_array($param)) { throw new \InvalidArgumentException( "Parameters must be arrays, {$this->config['name']}.$name is ".gettype($param) ); } $param['name'] = $name; $this->parameters[$name] = new Parameter( $param, ['description' => $this->description] ); } if ($this->config['additionalParameters']) { if (is_array($this->config['additionalParameters'])) { $this->additionalParameters = new Parameter( $this->config['additionalParameters'], ['description' => $this->description] ); } else { $this->additionalParameters = $this->config['additionalParameters']; } } } } src/Parameter.php000064400000042731145331651420007777 0ustar00originalData = $data; if (isset($options['description'])) { $this->serviceDescription = $options['description']; if (!($this->serviceDescription instanceof DescriptionInterface)) { throw new \InvalidArgumentException('description must be a Description'); } if (isset($data['$ref'])) { if ($model = $this->serviceDescription->getModel($data['$ref'])) { $name = isset($data['name']) ? $data['name'] : null; $data = $model->toArray() + $data; if ($name) { $data['name'] = $name; } } } elseif (isset($data['extends'])) { // If this parameter extends from another parameter then start // with the actual data union in the parent's data (e.g. actual // supersedes parent) if ($extends = $this->serviceDescription->getModel($data['extends'])) { $data += $extends->toArray(); } } } // Pull configuration data into the parameter foreach ($data as $key => $value) { $this->{$key} = $value; } $this->required = (bool) $this->required; $this->data = (array) $this->data; if ($this->filters) { $this->setFilters((array) $this->filters); } if ($this->type == 'object' && $this->additionalProperties === null) { $this->additionalProperties = true; } } /** * Convert the object to an array * * @return array */ public function toArray() { return $this->originalData; } /** * Get the default or static value of the command based on a value * * @param string $value Value that is currently set * * @return mixed Returns the value, a static value if one is present, or a default value */ public function getValue($value) { if ($this->static || ($this->default !== null && $value === null)) { return $this->default; } return $value; } /** * Run a value through the filters OR format attribute associated with the * parameter. * * @param mixed $value Value to filter * * @return mixed Returns the filtered value * * @throws \RuntimeException when trying to format when no service * description is available. */ public function filter($value) { // Formats are applied exclusively and supersed filters if ($this->format) { if (!$this->serviceDescription) { throw new \RuntimeException('No service description was set so ' .'the value cannot be formatted.'); } return $this->serviceDescription->format($this->format, $value); } // Convert Boolean values if ($this->type == 'boolean' && !is_bool($value)) { $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); } // Apply filters to the value if ($this->filters) { foreach ($this->filters as $filter) { if (is_array($filter)) { // Convert complex filters that hold value place holders foreach ($filter['args'] as &$data) { if ($data == '@value') { $data = $value; } elseif ($data == '@api') { $data = $this; } } $value = call_user_func_array( $filter['method'], $filter['args'] ); } else { $value = call_user_func($filter, $value); } } } return $value; } /** * Get the name of the parameter * * @return string */ public function getName() { return $this->name; } /** * Set the name of the parameter * * @param string $name Name to set */ public function setName($name) { $this->name = $name; } /** * Get the key of the parameter, where sentAs will supersede name if it is * set. * * @return string */ public function getWireName() { return $this->sentAs ?: $this->name; } /** * Get the type(s) of the parameter * * @return string|array */ public function getType() { return $this->type; } /** * Get if the parameter is required * * @return bool */ public function isRequired() { return $this->required; } /** * Get the default value of the parameter * * @return string|null */ public function getDefault() { return $this->default; } /** * Get the description of the parameter * * @return string|null */ public function getDescription() { return $this->description; } /** * Get the minimum acceptable value for an integer * * @return int|null */ public function getMinimum() { return $this->minimum; } /** * Get the maximum acceptable value for an integer * * @return int|null */ public function getMaximum() { return $this->maximum; } /** * Get the minimum allowed length of a string value * * @return int */ public function getMinLength() { return $this->minLength; } /** * Get the maximum allowed length of a string value * * @return int|null */ public function getMaxLength() { return $this->maxLength; } /** * Get the maximum allowed number of items in an array value * * @return int|null */ public function getMaxItems() { return $this->maxItems; } /** * Get the minimum allowed number of items in an array value * * @return int */ public function getMinItems() { return $this->minItems; } /** * Get the location of the parameter * * @return string|null */ public function getLocation() { return $this->location; } /** * Get the sentAs attribute of the parameter that used with locations to * sentAs an attribute when it is being applied to a location. * * @return string|null */ public function getSentAs() { return $this->sentAs; } /** * Retrieve a known property from the parameter by name or a data property * by name. When no specific name value is passed, all data properties * will be returned. * * @param string|null $name Specify a particular property name to retrieve * * @return array|mixed|null */ public function getData($name = null) { if (!$name) { return $this->data; } elseif (isset($this->data[$name])) { return $this->data[$name]; } elseif (isset($this->{$name})) { return $this->{$name}; } return null; } /** * Get whether or not the default value can be changed * * @return bool */ public function isStatic() { return $this->static; } /** * Get an array of filters used by the parameter * * @return array */ public function getFilters() { return $this->filters ?: []; } /** * Get the properties of the parameter * * @return Parameter[] */ public function getProperties() { if (!$this->propertiesCache) { $this->propertiesCache = []; foreach (array_keys($this->properties) as $name) { $this->propertiesCache[$name] = $this->getProperty($name); } } return $this->propertiesCache; } /** * Get a specific property from the parameter * * @param string $name Name of the property to retrieve * * @return Parameter|null */ public function getProperty($name) { if (!isset($this->properties[$name])) { return null; } if (!($this->properties[$name] instanceof self)) { $this->properties[$name]['name'] = $name; $this->properties[$name] = new static( $this->properties[$name], ['description' => $this->serviceDescription] ); } return $this->properties[$name]; } /** * Get the additionalProperties value of the parameter * * @return bool|Parameter|null */ public function getAdditionalProperties() { if (is_array($this->additionalProperties)) { $this->additionalProperties = new static( $this->additionalProperties, ['description' => $this->serviceDescription] ); } return $this->additionalProperties; } /** * Get the item data of the parameter * * @return Parameter */ public function getItems() { if (is_array($this->items)) { $this->items = new static( $this->items, ['description' => $this->serviceDescription] ); } return $this->items; } /** * Get the enum of strings that are valid for the parameter * * @return array|null */ public function getEnum() { return $this->enum; } /** * Get the regex pattern that must match a value when the value is a string * * @return string */ public function getPattern() { return $this->pattern; } /** * Get the format attribute of the schema * * @return string */ public function getFormat() { return $this->format; } /** * Set the array of filters used by the parameter * * @param array $filters Array of functions to use as filters * * @return self */ private function setFilters(array $filters) { $this->filters = []; foreach ($filters as $filter) { $this->addFilter($filter); } return $this; } /** * Add a filter to the parameter * * @param string|array $filter Method to filter the value through * * @return self * * @throws \InvalidArgumentException */ private function addFilter($filter) { if (is_array($filter)) { if (!isset($filter['method'])) { throw new \InvalidArgumentException( 'A [method] value must be specified for each complex filter' ); } } if (!$this->filters) { $this->filters = [$filter]; } else { $this->filters[] = $filter; } return $this; } /** * Check if a parameter has a specific variable and if it set. * * @param string $var * * @return bool */ public function has($var) { if (!is_string($var)) { throw new \InvalidArgumentException('Expected a string. Got: '.(is_object($var) ? get_class($var) : gettype($var))); } return isset($this->{$var}) && !empty($this->{$var}); } } src/QuerySerializer/QuerySerializerInterface.php000064400000000403145331651420016164 0ustar00removeNumericIndices = $removeNumericIndices; } /** * {@inheritDoc} */ public function aggregate(array $queryParams) { $queryString = http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986); if ($this->removeNumericIndices) { $queryString = preg_replace('/%5B[0-9]+%5D/simU', '%5B%5D', $queryString); } return $queryString; } } src/RequestLocation/AbstractLocation.php000064400000004773145331651420014440 0ustar00locationName = $locationName; } /** * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { return $request; } /** * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { return $request; } /** * Prepare (filter and set desired name for request item) the value for * request. * * @param mixed $value * * @return array|mixed */ protected function prepareValue($value, Parameter $param) { return is_array($value) ? $this->resolveRecursively($value, $param) : $param->filter($value); } /** * Recursively prepare and filter nested values. * * @param array $value Value to map * @param Parameter $param Parameter related to the current key. * * @return array Returns the mapped array */ protected function resolveRecursively(array $value, Parameter $param) { foreach ($value as $name => &$v) { switch ($param->getType()) { case 'object': if ($subParam = $param->getProperty($name)) { $key = $subParam->getWireName(); $value[$key] = $this->prepareValue($v, $subParam); if ($name != $key) { unset($value[$name]); } } elseif ($param->getAdditionalProperties() instanceof Parameter) { $v = $this->prepareValue($v, $param->getAdditionalProperties()); } break; case 'array': if ($items = $param->getItems()) { $v = $this->prepareValue($v, $items); } break; } } return $param->filter($value); } } src/RequestLocation/BodyLocation.php000064400000002015145331651420013555 0ustar00getBody()->getContents(); $value = $command[$param->getName()]; $value = $param->getName().'='.$param->filter($value); if ($oldValue !== '') { $value = $oldValue.'&'.$value; } return $request->withBody(Psr7\Utils::streamFor($value)); } } src/RequestLocation/FormParamLocation.php000064400000004033145331651420014546 0ustar00formParamsData['form_params'][$param->getWireName()] = $this->prepareValue( $command[$param->getName()], $param ); return $request; } /** * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $data = $this->formParamsData; $this->formParamsData = []; $modify = []; // Add additional parameters to the form_params array $additional = $operation->getAdditionalParameters(); if ($additional && $additional->getLocation() == $this->locationName) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $data['form_params'][$key] = $this->prepareValue($value, $additional); } } } $body = http_build_query($data['form_params'], '', '&'); $modify['body'] = Psr7\Utils::streamFor($body); $modify['set_headers']['Content-Type'] = $this->contentType; return Psr7\Utils::modifyRequest($request, $modify); } } src/RequestLocation/HeaderLocation.php000064400000002754145331651420014062 0ustar00getName()]; return $request->withHeader($param->getWireName(), $param->filter($value)); } /** * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { /** @var Parameter $additional */ $additional = $operation->getAdditionalParameters(); if ($additional && ($additional->getLocation() === $this->locationName)) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $request = $request->withHeader($key, $additional->filter($value)); } } } return $request; } } src/RequestLocation/JsonLocation.php000064400000004556145331651420013605 0ustar00jsonContentType = $contentType; } /** * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { $this->jsonData[$param->getWireName()] = $this->prepareValue( $command[$param->getName()], $param ); return $request->withBody(Psr7\Utils::streamFor(Utils::jsonEncode($this->jsonData))); } /** * @return MessageInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $data = $this->jsonData; $this->jsonData = []; // Add additional parameters to the JSON document $additional = $operation->getAdditionalParameters(); if ($additional && ($additional->getLocation() === $this->locationName)) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $data[$key] = $this->prepareValue($value, $additional); } } } // Don't overwrite the Content-Type if one is set if ($this->jsonContentType && !$request->hasHeader('Content-Type')) { $request = $request->withHeader('Content-Type', $this->jsonContentType); } return $request->withBody(Psr7\Utils::streamFor(Utils::jsonEncode($data))); } } src/RequestLocation/MultiPartLocation.php000064400000003433145331651420014606 0ustar00multipartData[] = [ 'name' => $param->getWireName(), 'contents' => $this->prepareValue($command[$param->getName()], $param), ]; return $request; } /** * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $data = $this->multipartData; $this->multipartData = []; $modify = []; $body = new Psr7\MultipartStream($data); $modify['body'] = Psr7\Utils::streamFor($body); $request = Psr7\Utils::modifyRequest($request, $modify); if ($request->getBody() instanceof Psr7\MultipartStream) { // Use a multipart/form-data POST if a Content-Type is not set. $request->withHeader('Content-Type', $this->contentType.$request->getBody()->getBoundary()); } return $request; } } src/RequestLocation/QueryLocation.php000064400000004443145331651420013774 0ustar00querySerializer = $querySerializer ?: new Rfc3986Serializer(); } /** * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { $uri = $request->getUri(); $query = Psr7\Query::parse($uri->getQuery()); $query[$param->getWireName()] = $this->prepareValue( $command[$param->getName()], $param ); $uri = $uri->withQuery($this->querySerializer->aggregate($query)); return $request->withUri($uri); } /** * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { $additional = $operation->getAdditionalParameters(); if ($additional && $additional->getLocation() == $this->locationName) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $uri = $request->getUri(); $query = Psr7\Query::parse($uri->getQuery()); $query[$key] = $this->prepareValue( $value, $additional ); $uri = $uri->withQuery($this->querySerializer->aggregate($query)); $request = $request->withUri($uri); } } } return $request; } } src/RequestLocation/RequestLocationInterface.php000064400000002360145331651420016134 0ustar00contentType = $contentType; } /** * @return RequestInterface */ public function visit( CommandInterface $command, RequestInterface $request, Parameter $param ) { // Buffer and order the parameters to visit based on if they are // top-level attributes or child nodes. // @link https://github.com/guzzle/guzzle/pull/494 if ($param->getData('xmlAttribute')) { array_unshift($this->buffered, $param); } else { $this->buffered[] = $param; } return $request; } /** * @return RequestInterface */ public function after( CommandInterface $command, RequestInterface $request, Operation $operation ) { foreach ($this->buffered as $param) { $this->visitWithValue( $command[$param->getName()], $param, $operation ); } $this->buffered = []; $additional = $operation->getAdditionalParameters(); if ($additional && $additional->getLocation() == $this->locationName) { foreach ($command->toArray() as $key => $value) { if (!$operation->hasParam($key)) { $additional->setName($key); $this->visitWithValue($value, $additional, $operation); } } $additional->setName(null); } // If data was found that needs to be serialized, then do so $xml = ''; if ($this->writer) { $xml = $this->finishDocument($this->writer); } elseif ($operation->getData('xmlAllowEmpty')) { // Check if XML should always be sent for the command $writer = $this->createRootElement($operation); $xml = $this->finishDocument($writer); } if ($xml !== '') { $request = $request->withBody(Psr7\Utils::streamFor($xml)); // Don't overwrite the Content-Type if one is set if ($this->contentType && !$request->hasHeader('Content-Type')) { $request = $request->withHeader('Content-Type', $this->contentType); } } $this->writer = null; return $request; } /** * Create the root XML element to use with a request * * @param Operation $operation Operation object * * @return \XMLWriter */ protected function createRootElement(Operation $operation) { static $defaultRoot = ['name' => 'Request']; // If no root element was specified, then just wrap the XML in 'Request' $root = $operation->getData('xmlRoot') ?: $defaultRoot; // Allow the XML declaration to be customized with xmlEncoding $encoding = $operation->getData('xmlEncoding'); $writer = $this->startDocument($encoding); $writer->startElement($root['name']); // Create the wrapping element with no namespaces if no namespaces were present if (!empty($root['namespaces'])) { // Create the wrapping element with an array of one or more namespaces foreach ((array) $root['namespaces'] as $prefix => $uri) { $nsLabel = 'xmlns'; if (!is_numeric($prefix)) { $nsLabel .= ':'.$prefix; } $writer->writeAttribute($nsLabel, $uri); } } return $writer; } /** * Recursively build the XML body * * @param \XMLWriter $writer XML to modify * @param Parameter $param API Parameter * @param mixed $value Value to add */ protected function addXml(\XMLWriter $writer, Parameter $param, $value) { $value = $param->filter($value); $type = $param->getType(); $name = $param->getWireName(); $prefix = null; $namespace = $param->getData('xmlNamespace'); if (false !== strpos($name, ':')) { list($prefix, $name) = explode(':', $name, 2); } if ($type == 'object' || $type == 'array') { if (!$param->getData('xmlFlattened')) { if ($namespace) { $writer->startElementNS(null, $name, $namespace); } else { $writer->startElement($name); } } if ($param->getType() == 'array') { $this->addXmlArray($writer, $param, $value); } elseif ($param->getType() == 'object') { $this->addXmlObject($writer, $param, $value); } if (!$param->getData('xmlFlattened')) { $writer->endElement(); } return; } if ($param->getData('xmlAttribute')) { $this->writeAttribute($writer, $prefix, $name, $namespace, $value); } else { $this->writeElement($writer, $prefix, $name, $namespace, $value); } } /** * Write an attribute with namespace if used * * @param \XMLWriter $writer XMLWriter instance * @param string $prefix Namespace prefix if any * @param string $name Attribute name * @param string $namespace The uri of the namespace * @param string $value The attribute content */ protected function writeAttribute($writer, $prefix, $name, $namespace, $value) { if ($namespace) { $writer->writeAttributeNS($prefix, $name, $namespace, $value); } else { $writer->writeAttribute($name, $value); } } /** * Write an element with namespace if used * * @param \XMLWriter $writer XML writer resource * @param string $prefix Namespace prefix if any * @param string $name Element name * @param string $namespace The uri of the namespace * @param string $value The element content */ protected function writeElement(\XMLWriter $writer, $prefix, $name, $namespace, $value) { if ($namespace) { $writer->startElementNS($prefix, $name, $namespace); } else { $writer->startElement($name); } if (strpbrk($value, '<>&')) { $writer->writeCData($value); } else { $writer->writeRaw($value); } $writer->endElement(); } /** * Create a new xml writer and start a document * * @param string $encoding document encoding * * @return \XMLWriter the writer resource * * @throws \RuntimeException if the document cannot be started */ protected function startDocument($encoding) { $this->writer = new \XMLWriter(); if (!$this->writer->openMemory()) { throw new \RuntimeException('Unable to open XML document in memory'); } if (!$this->writer->startDocument('1.0', $encoding)) { throw new \RuntimeException('Unable to start XML document'); } return $this->writer; } /** * End the document and return the output * * @param \XMLWriter $writer * * @return string the writer resource */ protected function finishDocument($writer) { $writer->endDocument(); return $writer->outputMemory(); } /** * Add an array to the XML */ protected function addXmlArray(\XMLWriter $writer, Parameter $param, &$value) { if ($items = $param->getItems()) { foreach ($value as $v) { $this->addXml($writer, $items, $v); } } } /** * Add an object to the XML */ protected function addXmlObject(\XMLWriter $writer, Parameter $param, &$value) { $noAttributes = []; // add values which have attributes foreach ($value as $name => $v) { if ($property = $param->getProperty($name)) { if ($property->getData('xmlAttribute')) { $this->addXml($writer, $property, $v); } else { $noAttributes[] = ['value' => $v, 'property' => $property]; } } } // now add values with no attributes foreach ($noAttributes as $element) { $this->addXml($writer, $element['property'], $element['value']); } } private function visitWithValue( $value, Parameter $param, Operation $operation ) { if (!$this->writer) { $this->createRootElement($operation); } $this->addXml($this->writer, $param, $value); } } src/ResponseLocation/AbstractLocation.php000064400000002145145331651420014575 0ustar00locationName = $locationName; } /** * @return ResultInterface */ public function before( ResultInterface $result, ResponseInterface $response, Parameter $model ) { return $result; } /** * @return ResultInterface */ public function after( ResultInterface $result, ResponseInterface $response, Parameter $model ) { return $result; } /** * @return ResultInterface */ public function visit( ResultInterface $result, ResponseInterface $response, Parameter $param ) { return $result; } } src/ResponseLocation/BodyLocation.php000064400000001421145331651420013723 0ustar00getName()] = $param->filter($response->getBody()); return $result; } } src/ResponseLocation/HeaderLocation.php000064400000002007145331651420014217 0ustar00getName(); if ($header = $response->getHeader($param->getWireName())) { if (is_array($header)) { $header = array_shift($header); } $result[$name] = $param->filter($header); } return $result; } } src/ResponseLocation/JsonLocation.php000064400000012477145331651420013754 0ustar00getBody(); $body = $body ?: '{}'; $this->json = \GuzzleHttp\json_decode($body, true); // relocate named arrays, so that they have the same structure as // arrays nested in objects and visit can work on them in the same way if ($model->getType() === 'array' && ($name = $model->getName())) { $this->json = [$name => $this->json]; } return $result; } /** * @return ResultInterface */ public function after( ResultInterface $result, ResponseInterface $response, Parameter $model ) { // Handle additional, undefined properties $additional = $model->getAdditionalProperties(); if (!($additional instanceof Parameter)) { return $result; } // Use the model location as the default if one is not set on additional $addLocation = $additional->getLocation() ?: $model->getLocation(); if ($addLocation == $this->locationName) { foreach ($this->json as $prop => $val) { if (!isset($result[$prop])) { // Only recurse if there is a type specified $result[$prop] = $additional->getType() ? $this->recurse($additional, $val) : $val; } } } $this->json = []; return $result; } /** * @return Result|ResultInterface */ public function visit( ResultInterface $result, ResponseInterface $response, Parameter $param ) { $name = $param->getName(); $key = $param->getWireName(); // Check if the result should be treated as a list if ($param->getType() == 'array') { // Treat as javascript array if ($name) { // name provided, store it under a key in the array $subArray = isset($this->json[$key]) ? $this->json[$key] : null; $result[$name] = $this->recurse($param, $subArray); } else { // top-level `array` or an empty name $result = new Result(array_merge( $result->toArray(), $this->recurse($param, $this->json) )); } } elseif (isset($this->json[$key])) { $result[$name] = $this->recurse($param, $this->json[$key]); } return $result; } /** * Recursively process a parameter while applying filters * * @param Parameter $param API parameter being validated * @param mixed $value Value to process. * * @return mixed|null */ private function recurse(Parameter $param, $value) { if (!is_array($value)) { return $param->filter($value); } $result = []; $type = $param->getType(); if ($type == 'array') { $items = $param->getItems(); foreach ($value as $val) { $result[] = $this->recurse($items, $val); } } elseif ($type == 'object' && !isset($value[0])) { // On the above line, we ensure that the array is associative and // not numerically indexed if ($properties = $param->getProperties()) { foreach ($properties as $property) { $key = $property->getWireName(); if (array_key_exists($key, $value)) { $result[$property->getName()] = $this->recurse( $property, $value[$key] ); // Remove from the value so that AP can later be handled unset($value[$key]); } } } // Only check additional properties if everything wasn't already // handled if ($value) { $additional = $param->getAdditionalProperties(); if ($additional === null || $additional === true) { // Merge the JSON under the resulting array $result += $value; } elseif ($additional instanceof Parameter) { // Process all child elements according to the given schema foreach ($value as $prop => $val) { $result[$prop] = $this->recurse($additional, $val); } } } } return $param->filter($result); } } src/ResponseLocation/ReasonPhraseLocation.php000064400000001510145331651420015417 0ustar00getName()] = $param->filter( $response->getReasonPhrase() ); return $result; } } src/ResponseLocation/ResponseLocationInterface.php000064400000003474145331651420016457 0ustar00getName()] = $param->filter($response->getStatusCode()); return $result; } } src/ResponseLocation/XmlLocation.php000064400000022530145331651420013572 0ustar00xml = simplexml_load_string((string) $response->getBody()); return $result; } /** * @return Result|ResultInterface */ public function after( ResultInterface $result, ResponseInterface $response, Parameter $model ) { // Handle additional, undefined properties $additional = $model->getAdditionalProperties(); if ($additional instanceof Parameter && $additional->getLocation() == $this->locationName ) { $result = new Result(array_merge( $result->toArray(), self::xmlToArray($this->xml) )); } $this->xml = null; return $result; } /** * @return ResultInterface */ public function visit( ResultInterface $result, ResponseInterface $response, Parameter $param ) { $sentAs = $param->getWireName(); $ns = null; if (null !== $sentAs && strstr($sentAs, ':')) { list($ns, $sentAs) = explode(':', $sentAs); } // Process the primary property if (count($this->xml->children($ns, true)->{$sentAs})) { $result[$param->getName()] = $this->recursiveProcess( $param, $this->xml->children($ns, true)->{$sentAs} ); } return $result; } /** * Recursively process a parameter while applying filters * * @param Parameter $param API parameter being processed * @param \SimpleXMLElement $node Node being processed * * @return array */ private function recursiveProcess( Parameter $param, \SimpleXMLElement $node ) { $result = []; $type = $param->getType(); if ($type == 'object') { $result = $this->processObject($param, $node); } elseif ($type == 'array') { $result = $this->processArray($param, $node); } else { // We are probably handling a flat data node (i.e. string or // integer), so let's check if it's childless, which indicates a // node containing plain text. if ($node->children()->count() == 0) { // Retrieve text from node $result = (string) $node; } } // Filter out the value if (isset($result)) { $result = $param->filter($result); } return $result; } /** * @return array */ private function processArray(Parameter $param, \SimpleXMLElement $node) { // Cast to an array if the value was a string, but should be an array $items = $param->getItems(); $sentAs = $items->getWireName(); $result = []; $ns = null; if (null !== $sentAs && strstr($sentAs, ':')) { // Get namespace from the wire name list($ns, $sentAs) = explode(':', $sentAs); } else { // Get namespace from data $ns = $items->getData('xmlNs'); } if ($sentAs === null) { // A general collection of nodes foreach ($node as $child) { $result[] = $this->recursiveProcess($items, $child); } } else { // A collection of named, repeating nodes // (i.e. ) $children = $node->children($ns, true)->{$sentAs}; foreach ($children as $child) { $result[] = $this->recursiveProcess($items, $child); } } return $result; } /** * Process an object * * @param Parameter $param API parameter being parsed * @param \SimpleXMLElement $node Value to process * * @return array */ private function processObject(Parameter $param, \SimpleXMLElement $node) { $result = $knownProps = $knownAttributes = []; // Handle known properties if ($properties = $param->getProperties()) { foreach ($properties as $property) { $name = $property->getName(); $sentAs = $property->getWireName(); $knownProps[$sentAs] = 1; if (strpos($sentAs, ':')) { list($ns, $sentAs) = explode(':', $sentAs); } else { $ns = $property->getData('xmlNs'); } if ($property->getData('xmlAttribute')) { // Handle XML attributes $result[$name] = (string) $node->attributes($ns, true)->{$sentAs}; $knownAttributes[$sentAs] = 1; } elseif (count($node->children($ns, true)->{$sentAs})) { // Found a child node matching wire name $childNode = $node->children($ns, true)->{$sentAs}; $result[$name] = $this->recursiveProcess( $property, $childNode ); } } } // Handle additional, undefined properties $additional = $param->getAdditionalProperties(); if ($additional instanceof Parameter) { // Process all child elements according to the given schema foreach ($node->children($additional->getData('xmlNs'), true) as $childNode) { $sentAs = $childNode->getName(); if (!isset($knownProps[$sentAs])) { $result[$sentAs] = $this->recursiveProcess( $additional, $childNode ); } } } elseif ($additional === null || $additional === true) { // Blindly transform the XML into an array preserving as much data // as possible. Remove processed, aliased properties. $array = array_diff_key(self::xmlToArray($node), $knownProps); // Remove @attributes that were explicitly plucked from the // attributes list. if (isset($array['@attributes']) && $knownAttributes) { $array['@attributes'] = array_diff_key($array['@attributes'], $knownProps); if (!$array['@attributes']) { unset($array['@attributes']); } } // Merge it together with the original result $result = array_merge($array, $result); } return $result; } /** * Convert an XML document to an array. * * @param int $nesting * @param null $ns * * @return array */ private static function xmlToArray( \SimpleXMLElement $xml, $ns = null, $nesting = 0 ) { $result = []; $children = $xml->children($ns, true); foreach ($children as $name => $child) { $attributes = (array) $child->attributes($ns, true); if (!isset($result[$name])) { $childArray = self::xmlToArray($child, $ns, $nesting + 1); $result[$name] = $attributes ? array_merge($attributes, $childArray) : $childArray; continue; } // A child element with this name exists so we're assuming // that the node contains a list of elements if (!is_array($result[$name])) { $result[$name] = [$result[$name]]; } elseif (!isset($result[$name][0])) { // Convert the first child into the first element of a numerically indexed array $firstResult = $result[$name]; $result[$name] = []; $result[$name][] = $firstResult; } $childArray = self::xmlToArray($child, $ns, $nesting + 1); if ($attributes) { $result[$name][] = array_merge($attributes, $childArray); } else { $result[$name][] = $childArray; } } // Extract text from node $text = trim((string) $xml); if ($text === '') { $text = null; } // Process attributes $attributes = (array) $xml->attributes($ns, true); if ($attributes) { if ($text !== null) { $result['value'] = $text; } $result = array_merge($attributes, $result); } elseif ($text !== null) { $result = $text; } // Make sure we're always returning an array if ($nesting == 0 && !is_array($result)) { $result = [$result]; } return $result; } } src/SchemaFormatter.php000064400000007173145331651420011144 0ustar00formatDateTime($value); case 'date-time-http': return $this->formatDateTimeHttp($value); case 'date': return $this->formatDate($value); case 'time': return $this->formatTime($value); case 'timestamp': return $this->formatTimestamp($value); case 'boolean-string': return $this->formatBooleanAsString($value); default: return $value; } } /** * Perform the actual DateTime formatting * * @param int|string|\DateTime $dateTime Date time value * @param string $format Format of the result * * @return string * * @throws \InvalidArgumentException */ protected function dateFormatter($dateTime, $format) { if (is_numeric($dateTime)) { return gmdate($format, (int) $dateTime); } if (is_string($dateTime)) { $dateTime = new \DateTime($dateTime); } if ($dateTime instanceof \DateTimeInterface) { static $utc; if (!$utc) { $utc = new \DateTimeZone('UTC'); } return $dateTime->setTimezone($utc)->format($format); } throw new \InvalidArgumentException('Date/Time values must be either ' .'be a string, integer, or DateTime object'); } /** * Create a ISO 8601 (YYYY-MM-DDThh:mm:ssZ) formatted date time value in * UTC time. * * @param string|int|\DateTime $value Date time value * * @return string */ private function formatDateTime($value) { return $this->dateFormatter($value, 'Y-m-d\TH:i:s\Z'); } /** * Create an HTTP date (RFC 1123 / RFC 822) formatted UTC date-time string * * @param string|int|\DateTime $value Date time value * * @return string */ private function formatDateTimeHttp($value) { return $this->dateFormatter($value, 'D, d M Y H:i:s \G\M\T'); } /** * Create a YYYY-MM-DD formatted string * * @param string|int|\DateTime $value Date time value * * @return string */ private function formatDate($value) { return $this->dateFormatter($value, 'Y-m-d'); } /** * Create a hh:mm:ss formatted string * * @param string|int|\DateTime $value Date time value * * @return string */ private function formatTime($value) { return $this->dateFormatter($value, 'H:i:s'); } /** * Formats a boolean value as a string * * @param string|int|bool $value Value to convert to a boolean * 'true' / 'false' value * * @return string */ private function formatBooleanAsString($value) { return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false'; } /** * Return a UNIX timestamp in the UTC timezone * * @param string|int|\DateTime $value Time value * * @return int */ private function formatTimestamp($value) { return (int) $this->dateFormatter($value, 'U'); } } src/SchemaValidator.php000064400000026511145331651420011123 0ustar00castIntegerToStringType = $castIntegerToStringType; } /** * @return bool */ public function validate(Parameter $param, &$value) { $this->errors = []; $this->recursiveProcess($param, $value); if (empty($this->errors)) { return true; } else { sort($this->errors); return false; } } /** * Get the errors encountered while validating * * @return array */ public function getErrors() { return $this->errors ?: []; } /** * From the allowable types, determine the type that the variable matches * * @param string|array $type Parameter type * @param mixed $value Value to determine the type * * @return string|false Returns the matching type on */ protected function determineType($type, $value) { foreach ((array) $type as $t) { if ($t == 'string' && (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) ) { return 'string'; } elseif ($t == 'object' && (is_array($value) || is_object($value))) { return 'object'; } elseif ($t == 'array' && is_array($value)) { return 'array'; } elseif ($t == 'integer' && is_integer($value)) { return 'integer'; } elseif ($t == 'boolean' && is_bool($value)) { return 'boolean'; } elseif ($t == 'number' && is_numeric($value)) { return 'number'; } elseif ($t == 'numeric' && is_numeric($value)) { return 'numeric'; } elseif ($t == 'null' && !$value) { return 'null'; } elseif ($t == 'any') { return 'any'; } } return false; } /** * Recursively validate a parameter * * @param Parameter $param API parameter being validated * @param mixed $value Value to validate and validate. The value may * change during this validate. * @param string $path Current validation path (used for error reporting) * @param int $depth Current depth in the validation validate * * @return bool Returns true if valid, or false if invalid */ protected function recursiveProcess( Parameter $param, &$value, $path = '', $depth = 0 ) { // Update the value by adding default or static values $value = $param->getValue($value); $required = $param->isRequired(); // if the value is null and the parameter is not required or is static, // then skip any further recursion if ((null === $value && !$required) || $param->isStatic()) { return true; } $type = $param->getType(); // Attempt to limit the number of times is_array is called by tracking // if the value is an array $valueIsArray = is_array($value); // If a name is set then update the path so that validation messages // are more helpful if ($name = $param->getName()) { $path .= "[{$name}]"; } if ($type == 'object') { // Determine whether or not this "value" has properties and should // be traversed $traverse = $temporaryValue = false; // Convert the value to an array if (!$valueIsArray && $value instanceof ToArrayInterface) { $value = $value->toArray(); } if ($valueIsArray) { // Ensure that the array is associative and not numerically // indexed if (isset($value[0])) { $this->errors[] = "{$path} must be an array of properties. Got a numerically indexed array."; return false; } $traverse = true; } elseif ($value === null) { // Attempt to let the contents be built up by default values if // possible $value = []; $temporaryValue = $valueIsArray = $traverse = true; } if ($traverse) { if ($properties = $param->getProperties()) { // if properties were found, validate each property foreach ($properties as $property) { $name = $property->getName(); if (isset($value[$name])) { $this->recursiveProcess($property, $value[$name], $path, $depth + 1); } else { $current = null; $this->recursiveProcess($property, $current, $path, $depth + 1); // Only set the value if it was populated if (null !== $current) { $value[$name] = $current; } } } } $additional = $param->getAdditionalProperties(); if ($additional !== true) { // If additional properties were found, then validate each // against the additionalProperties attr. $keys = array_keys($value); // Determine the keys that were specified that were not // listed in the properties of the schema $diff = array_diff($keys, array_keys($properties)); if (!empty($diff)) { // Determine which keys are not in the properties if ($additional instanceof Parameter) { foreach ($diff as $key) { $this->recursiveProcess($additional, $value[$key], "{$path}[{$key}]", $depth); } } else { // if additionalProperties is set to false and there // are additionalProperties in the values, then fail foreach ($diff as $prop) { $this->errors[] = sprintf('%s[%s] is not an allowed property', $path, $prop); } } } } // A temporary value will be used to traverse elements that // have no corresponding input value. This allows nested // required parameters with default values to bubble up into the // input. Here we check if we used a temp value and nothing // bubbled up, then we need to remote the value. if ($temporaryValue && empty($value)) { $value = null; $valueIsArray = false; } } } elseif ($type == 'array' && $valueIsArray && $param->getItems()) { foreach ($value as $i => &$item) { // Validate each item in an array against the items attribute of the schema $this->recursiveProcess($param->getItems(), $item, $path."[{$i}]", $depth + 1); } } // If the value is required and the type is not null, then there is an // error if the value is not set if ($required && $value === null && $type != 'null') { $message = "{$path} is ".($param->getType() ? ('a required '.implode(' or ', (array) $param->getType())) : 'required'); if ($param->has('description')) { $message .= ': '.$param->getDescription(); } $this->errors[] = $message; return false; } // Validate that the type is correct. If the type is string but an // integer was passed, the class can be instructed to cast the integer // to a string to pass validation. This is the default behavior. if ($type && (!$type = $this->determineType($type, $value))) { if ($this->castIntegerToStringType && $param->getType() == 'string' && is_integer($value) ) { $value = (string) $value; } else { $this->errors[] = "{$path} must be of type ".implode(' or ', (array) $param->getType()); } } // Perform type specific validation for strings, arrays, and integers if ($type == 'string') { // Strings can have enums which are a list of predefined values if (($enum = $param->getEnum()) && !in_array($value, $enum)) { $this->errors[] = "{$path} must be one of ".implode(' or ', array_map(function ($s) { return '"'.addslashes($s).'"'; }, $enum)); } // Strings can have a regex pattern that the value must match if (($pattern = $param->getPattern()) && !preg_match($pattern, $value)) { $this->errors[] = "{$path} must match the following regular expression: {$pattern}"; } $strLen = null; if ($min = $param->getMinLength()) { $strLen = strlen($value); if ($strLen < $min) { $this->errors[] = "{$path} length must be greater than or equal to {$min}"; } } if ($max = $param->getMaxLength()) { if (($strLen ?: strlen($value)) > $max) { $this->errors[] = "{$path} length must be less than or equal to {$max}"; } } } elseif ($type == 'array') { $size = null; if ($min = $param->getMinItems()) { $size = count($value); if ($size < $min) { $this->errors[] = "{$path} must contain {$min} or more elements"; } } if ($max = $param->getMaxItems()) { if (($size ?: count($value)) > $max) { $this->errors[] = "{$path} must contain {$max} or fewer elements"; } } } elseif ($type == 'integer' || $type == 'number' || $type == 'numeric') { if (($min = $param->getMinimum()) && $value < $min) { $this->errors[] = "{$path} must be greater than or equal to {$min}"; } if (($max = $param->getMaximum()) && $value > $max) { $this->errors[] = "{$path} must be less than or equal to {$max}"; } } return empty($this->errors); } } src/Serializer.php000064400000012501145331651420010160 0ustar00 new BodyLocation(), 'query' => new QueryLocation(), 'header' => new HeaderLocation(), 'json' => new JsonLocation(), 'xml' => new XmlLocation(), 'formParam' => new FormParamLocation(), 'multipart' => new MultiPartLocation(), ]; } $this->locations = $requestLocations + $defaultRequestLocations; $this->description = $description; } /** * @return RequestInterface */ public function __invoke(CommandInterface $command) { $request = $this->createRequest($command); return $this->prepareRequest($command, $request); } /** * Prepares a request for sending using location visitors * * @param RequestInterface $request Request being created * * @return RequestInterface * * @throws \RuntimeException If a location cannot be handled */ protected function prepareRequest( CommandInterface $command, RequestInterface $request ) { $visitedLocations = []; $operation = $this->description->getOperation($command->getName()); // Visit each actual parameter foreach ($operation->getParams() as $name => $param) { /* @var Parameter $param */ $location = $param->getLocation(); // Skip parameters that have not been set or are URI location if ($location == 'uri' || !$command->hasParam($name)) { continue; } if (!isset($this->locations[$location])) { throw new \RuntimeException("No location registered for $name"); } $visitedLocations[$location] = true; $request = $this->locations[$location]->visit($command, $request, $param); } // Ensure that the after() method is invoked for additionalParameters /** @var Parameter $additional */ if ($additional = $operation->getAdditionalParameters()) { $visitedLocations[$additional->getLocation()] = true; } // Call the after() method for each visited location foreach (array_keys($visitedLocations) as $location) { $request = $this->locations[$location]->after($command, $request, $operation); } return $request; } /** * Create a request for the command and operation * * @return RequestInterface * * @throws \RuntimeException */ protected function createRequest(CommandInterface $command) { $operation = $this->description->getOperation($command->getName()); // If command does not specify a template, assume the client's base URL. if (null === $operation->getUri()) { return new Request( $operation->getHttpMethod() ?: 'GET', $this->description->getBaseUri() ); } return $this->createCommandWithUri($operation, $command); } /** * Create a request for an operation with a uri merged onto a base URI * * @return \GuzzleHttp\Psr7\Request */ private function createCommandWithUri( Operation $operation, CommandInterface $command ) { // Get the path values and use the client config settings $variables = []; foreach ($operation->getParams() as $name => $arg) { /* @var Parameter $arg */ if ($arg->getLocation() == 'uri') { if (isset($command[$name])) { $variables[$name] = $arg->filter($command[$name]); if (!is_array($variables[$name])) { $variables[$name] = (string) $variables[$name]; } } } } // Expand the URI template. $uri = new Uri(UriTemplate::expand($operation->getUri(), $variables)); return new Request( $operation->getHttpMethod() ?: 'GET', UriResolver::resolve($this->description->getBaseUri(), $uri) ); } }