Repository: tebru/retrofit-php Branch: master Commit: dbd1c4a2cf8c Files: 164 Total size: 362.3 KB Directory structure: gitextract_syq3xf3v/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.md ├── README.md ├── bin/ │ └── retrofit ├── composer.json ├── docs/ │ ├── advanced_usage.md │ ├── annotations.md │ ├── installation.md │ ├── upgrade_2_3.md │ └── usage.md ├── phpcs.xml ├── phpunit.xml ├── src/ │ ├── Annotation/ │ │ ├── Body.php │ │ ├── DELETE.php │ │ ├── Encodable.php │ │ ├── ErrorBody.php │ │ ├── Field.php │ │ ├── FieldMap.php │ │ ├── GET.php │ │ ├── HEAD.php │ │ ├── Header.php │ │ ├── HeaderMap.php │ │ ├── Headers.php │ │ ├── HttpRequest.php │ │ ├── OPTIONS.php │ │ ├── PATCH.php │ │ ├── POST.php │ │ ├── PUT.php │ │ ├── ParameterAnnotation.php │ │ ├── ParameterAwareAnnotation.php │ │ ├── Part.php │ │ ├── PartMap.php │ │ ├── Path.php │ │ ├── Query.php │ │ ├── QueryMap.php │ │ ├── QueryName.php │ │ ├── REQUEST.php │ │ ├── ResponseBody.php │ │ └── Url.php │ ├── AnnotationHandler.php │ ├── Call.php │ ├── CallAdapter.php │ ├── CallAdapterFactory.php │ ├── Command/ │ │ └── CompileCommand.php │ ├── Converter.php │ ├── ConverterFactory.php │ ├── DefaultProxyFactoryAware.php │ ├── Exception/ │ │ └── ResponseHandlingFailedException.php │ ├── Finder/ │ │ └── ServiceResolver.php │ ├── Http/ │ │ └── MultipartBody.php │ ├── HttpClient.php │ ├── Internal/ │ │ ├── AnnotationHandler/ │ │ │ ├── BodyAnnotHandler.php │ │ │ ├── FieldAnnotHandler.php │ │ │ ├── FieldMapAnnotHandler.php │ │ │ ├── HeaderAnnotHandler.php │ │ │ ├── HeaderMapAnnotHandler.php │ │ │ ├── HeadersAnnotHandler.php │ │ │ ├── HttpRequestAnnotHandler.php │ │ │ ├── PartAnnotHandler.php │ │ │ ├── PartMapAnnotHandler.php │ │ │ ├── PathAnnotHandler.php │ │ │ ├── QueryAnnotHandler.php │ │ │ ├── QueryMapAnnotHandler.php │ │ │ ├── QueryNameAnnotHandler.php │ │ │ └── UrlAnnotHandler.php │ │ ├── AnnotationProcessor.php │ │ ├── CacheProvider.php │ │ ├── CallAdapter/ │ │ │ ├── CallAdapterProvider.php │ │ │ ├── DefaultCallAdapter.php │ │ │ └── DefaultCallAdapterFactory.php │ │ ├── Converter/ │ │ │ ├── ConverterProvider.php │ │ │ ├── DefaultConverterFactory.php │ │ │ ├── DefaultRequestBodyConverter.php │ │ │ ├── DefaultResponseBodyConverter.php │ │ │ ├── DefaultStringConverter.php │ │ │ └── NoopStringConverter.php │ │ ├── DefaultProxyFactory.php │ │ ├── Filesystem.php │ │ ├── HttpClientCall.php │ │ ├── ParameterHandler/ │ │ │ ├── AbstractParameterHandler.php │ │ │ ├── BodyParamHandler.php │ │ │ ├── FieldMapParamHandler.php │ │ │ ├── FieldParamHandler.php │ │ │ ├── HeaderMapParamHandler.php │ │ │ ├── HeaderParamHandler.php │ │ │ ├── PartMapParamHandler.php │ │ │ ├── PartParamHandler.php │ │ │ ├── PathParamHandler.php │ │ │ ├── QueryMapParamHandler.php │ │ │ ├── QueryNameParamHandler.php │ │ │ ├── QueryParamHandler.php │ │ │ └── UrlParamHandler.php │ │ ├── RequestBuilder.php │ │ ├── RetrofitResponse.php │ │ ├── ServiceMethod/ │ │ │ ├── DefaultServiceMethod.php │ │ │ ├── DefaultServiceMethodBuilder.php │ │ │ └── ServiceMethodFactory.php │ │ └── ServiceMethod.php │ ├── ParameterHandler.php │ ├── Proxy/ │ │ └── AbstractProxy.php │ ├── Proxy.php │ ├── ProxyFactory.php │ ├── RequestBodyConverter.php │ ├── Response.php │ ├── ResponseBodyConverter.php │ ├── Retrofit.php │ ├── RetrofitBuilder.php │ ├── ServiceMethodBuilder.php │ └── StringConverter.php └── tests/ ├── Mock/ │ └── Unit/ │ ├── Internal/ │ │ ├── AnnotationProcessorTest/ │ │ │ ├── AnnotationProcessorTestMock.php │ │ │ └── BadConverterAnnotation.php │ │ ├── HttpClientCallTest/ │ │ │ ├── HttpClientCallTestClientMock.php │ │ │ ├── HttpClientCallTestErrorBodyMock.php │ │ │ ├── HttpClientCallTestResponseBodyMock.php │ │ │ └── HttpClientCallTestServiceMethodMock.php │ │ ├── ProxyFactoryTest/ │ │ │ ├── PFTCTestCreate.php │ │ │ ├── PFTCTestCreateCacheDirectoryFail.php │ │ │ ├── PFTCTestCreateClientFail.php │ │ │ ├── PFTCTestCreateTwice.php │ │ │ ├── PFTCTestCreateWithoutCache.php │ │ │ ├── ProxyFactoryTestClientNoReturnType.php │ │ │ ├── ProxyFactoryTestClientNoTypehint.php │ │ │ ├── ProxyFactoryTestFilesystem.php │ │ │ └── ProxyFactoryTestHttpClient.php │ │ └── ServiceMethod/ │ │ └── ServiceMethodFactoryTest/ │ │ ├── ServiceMethodFactoryTestClient.php │ │ └── ServiceMethodFactoryTestConverterFactory.php │ ├── MockCall.php │ ├── MockConverterFactory.php │ └── RetrofitTest/ │ ├── ApiClient.php │ ├── CacheableApiClient.php │ ├── DefaultParamsApiClient.php │ ├── InvalidSyntaxApiClient.php │ ├── RetrofitTestAdaptedCallMock.php │ ├── RetrofitTestCallAdapterFactory.php │ ├── RetrofitTestCallAdapterMock.php │ ├── RetrofitTestConverterFactory.php │ ├── RetrofitTestCustomAnnotation.php │ ├── RetrofitTestCustomAnnotationHandler.php │ ├── RetrofitTestDelegateProxy.php │ ├── RetrofitTestHttpClient.php │ ├── RetrofitTestProxyFactory.php │ ├── RetrofitTestRequestBodyConverter.php │ ├── RetrofitTestRequestBodyMock.php │ ├── RetrofitTestResponseBodyConverter.php │ └── RetrofitTestResponseBodyMock.php ├── Unit/ │ ├── Internal/ │ │ ├── AnnotationHandlersTest.php │ │ ├── AnnotationProcessorTest.php │ │ ├── CallAdapterTest.php │ │ ├── ConverterTest.php │ │ ├── DefaultProxyFactoryTest.php │ │ ├── FilesystemTest.php │ │ ├── HttpClientCallTest.php │ │ ├── ParameterHandlersTest.php │ │ ├── RequestBuilderTest.php │ │ ├── RetrofitResponseTest.php │ │ └── ServiceMethod/ │ │ ├── DefaultServiceMethodBuilderTest.php │ │ ├── ServiceMethodFactoryTest.php │ │ └── ServiceMethodTest.php │ └── RetrofitTest.php └── bootstrap.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /vendor /composer.lock /tests/cache /build /.php_cs.cache ================================================ FILE: .travis.yml ================================================ sudo: false language: php php: - 7.1 - 7.2 - 7.3 - 7.4 env: - SF_CACHE=3 - SF_CACHE=4 - SF_CACHE=4.3 - SF_CACHE=5 jobs: exclude: - php: 7.1 env: - SF_CACHE=5 # remove composer.lock (it should not be published) before_script: - composer install --no-interaction -o - if [ "$SF_CACHE" = "3" ]; then composer require symfony/cache:^3.3; fi; - if [ "$SF_CACHE" = "4" ]; then composer require symfony/cache:">=4.0 <4.3"; fi; - if [ "$SF_CACHE" = "4.3" ]; then composer require symfony/cache:^4.3; fi; - if [ "$SF_CACHE" = "5" ]; then composer require symfony/cache:5.0; fi; script: - mkdir -p build/logs - vendor/bin/phpunit ================================================ FILE: CHANGELOG.md ================================================ Change Log ========== This document keeps track of important changes between releases of the library. 3.2.0 ----- * ADDED: Exception handling on response body conversion (Should not be a BC break unless applications are checking for specific exceptions) 3.1.0 ----- * ADDED: Ability to set custom cache adapter * CHANGED: Default cache to use php file cache over filesystem cache 3.0.3 ----- * FIXED: Issue with default parameters and nulls 3.0.2 ----- * ADDED: Symfony 4 support 3.0.1 ----- * FIXED: Issue with query encoding 3.0.0 ----- * See [upgrade guide](docs/upgrade_2_3.md) 2.9.0 ----- * Dependency updates * Added request/response to return event 2.8.3 ----- * Fixed issue with AfterSendEvent 2.8.2 ----- * Fixed issue with events 2.8.1 ----- * Updating dependencies 2.8.0 ----- * Added option to return complete response instead of body * Removed logging of which events occurred * Dependency updates * Added multipart support 2.7.0 ----- * Updated dynamo library 2.6.1 ----- * Fixed issue with urlencoding parameters 2.6.0 ----- * Changing client adapter to only accept request interface * Updating client dependency 2.5.5 ----- * Updating Dynamo library * Updating minimum dependencies * Added ability to set a client adapter 2.5.4 ----- * Event updates: adding request 2.5.3 ----- * Fixed bug AfterSendEvent 2.5.2 ----- * Fixed bug in event system 2.5.1 ----- * Fixed issue with urlencode 2.5.0 ----- * Added logging * Fixed Dyanmo minimum version dependency 2.4.0 ----- * Added support for async requests * Using http_build_query() to build all query strings * Allowing modification of return data through event 2.3.0 ----- * Fixed events to dispatch objects that can be modified 2.2.0 ----- * Allowed body object to be form encoded * Added support for serializing \JsonSerializable * Fixed issue with booleans getting cast to ints * Fixed issue with serializing null body objects * Improved generated code to exclude serialization contexts if not applicable 2.1.0 ----- * Added an event dispatcher * Added a library specific exception 2.0.0 ----- * Removed composer wrapper * Removed guzzle dependency (added support for either v5 or v6) * Abstracted class generation into tebru/dynamo * Fixed issue with query map * Fixed issue with interface inheritance 1.2.1 ----- * Upgraded twig to resolve security vulnerability 1.2.0 ----- * Added annotation for JMS serialization contexts 1.1.0 ----- * Added support for JMS serialization contexts 1.0.0 ----- * Mirrored functionality of Square's Retrofit library. * PSR autoloading of generated classes * Created binary tool for managing cache. ================================================ FILE: CONTRIBUTING.md ================================================ Contributing Guidelines ======================= Before contributing code, please be aware of the following: Code Style ---------- This project uses the [PSR-2] code style. Please follow these formatting standards where possible. [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md ================================================ FILE: CONTRIBUTORS.md ================================================ Contributors ============ This is a credits file of people that have contributed Retrofit-PHP Library. * Nate Brunette * Email: n@tebru.net * Maxwell Vandervelde * Email: Max@MaxVandervelde.com * PGP: `4096R/2B95C590 C21D 81E7 1F58 6C96 955F 5141 444C 4178 2B95 C590` * Matthew Loberg * Email: loberg.matt@gmail.com * Edward Pfremmer * Email: edward.pfremmer@nerdery.com * Tony Nelson * Email: tonynelson19@gmail.com ================================================ FILE: LICENSE.md ================================================ Project License =============== The MIT License (MIT) Copyright (c) 2015 Nate Brunette 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. ================================================ FILE: README.md ================================================ Retrofit PHP ============ [![Build Status](https://travis-ci.org/tebru/retrofit-php.svg?branch=master)](https://travis-ci.org/tebru/retrofit-php) [![Code Coverage](https://scrutinizer-ci.com/g/tebru/retrofit-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/tebru/retrofit-php/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tebru/retrofit-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tebru/retrofit-php/?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d2188bf8-8248-4df6-8bc5-8150fc0b8898/mini.png)](https://insight.sensiolabs.com/projects/d2188bf8-8248-4df6-8bc5-8150fc0b8898) Retrofit is a type-safe REST client. It is blatantly stolen from [square/retrofit](https://github.com/square/retrofit) and implemented in PHP. ❗UPGRADE NOTICE❗ ---------------- **Version 3 introduces many breaking changes. Please review the [upgrade guide](docs/upgrade_2_3.md) before upgrading.** Overview -------- *The following is for version 3, please check out the corresponding tag for version 2 documentation* Retrofit allows you to define your REST API with a simple interface. The follow example will attempt to display a typical use-case, but requires two additional libraries. The first uses Guzzle to make http requests as Retrofit does not ship with any default way to make network requests. The second uses a serializer (Gson) to hook into Retrofit's Converter functionality. This allows for automatic serialization of request bodies and deserialization of response bodies. ```php interface GitHubService { /** * @GET("/users/{user}/list") * @Path("user") * @ResponseBody("App\GithubService\ListRepo") * @ErrorBody("App\GitHubService\ApiError") */ public function listRepos(string $user): Call; } ``` Annotations are used to configure the endpoint. Then, the `Retrofit` class generates a working implementation of the service interface. ```php $retrofit = Retrofit::builder() ->setBaseUrl('https://api.github.com') ->setHttpClient(new Guzzle6HttpClient(new Client())) // requires a separate library ->addConverterFactory(new GsonConverterFactory(Gson::builder()->build())) // requies a separate library ->build(); $gitHubService = $retrofit->create(GitHubService::class); ``` Our newly created service is capable of making GET requests to /users/{user}/list, which returns a `Call` object. ```php $call = $gitHubService->listRepos('octocat'); ``` The `Call` object is then used to execute the request synchronously or asynchronously, returning a response. ```php $response = $call->execute(); // or $call->enqueue( function(Response $response) { }, // response callback (optional) function(Throwable $throwable) { } // error callback (optional) ); $call->wait(); ``` You can then check to see if the request was successful and get the deserialized response body. ```php if (!$response->isSuccessful()) { throw new ApiException($response->errorBody()); } $responseBody = $response->body(); ``` *Usage examples are referenced from Square's documentation* Installation & Usage -------------------- *Retrofit 3 requires PHP 7.1* ```bash composer require tebru/retrofit-php ``` Please make sure you also install an http client. ```bash composer require tebru/retrofit-php-http-guzzle6 ``` Install a converter to handle more advanced request and response body conversions. ```bash composer require tebru/retrofit-php-converter-gson ``` ### Documentation - [Installation](docs/installation.md) - [Getting Started](docs/usage.md) - [Advanced Usage](docs/advanced_usage.md) - [Annotation Reference](docs/annotations.md) License ------- This project is licensed under the MIT license. Please see the `LICENSE` file for more information. ================================================ FILE: bin/retrofit ================================================ #!/usr/bin/env php add(new CompileCommand); $application->run(); ================================================ FILE: composer.json ================================================ { "name": "tebru/retrofit-php", "description": "Retrofit for PHP - A type-safe PHP REST client.", "require": { "php": ">= 7.1", "guzzlehttp/psr7": "^1.0", "nikic/php-parser": "~3.0.6|^4.0", "symfony/cache": "^3.3|^4.0|^5.0", "symfony/console": "^3.0|^4.0|^5.0", "tebru/doctrine-annotation-reader": "^0.3.0", "tebru/php-type": "^0.1.1" }, "require-dev": { "phpunit/phpunit": "^7.3", "mikey179/vfsstream": "^1.6" }, "license": "MIT", "authors": [ { "name": "Nate Brunette", "email": "n@tebru.net" } ], "autoload": { "psr-4": { "Tebru\\Retrofit\\": "src/" } }, "autoload-dev": { "psr-4": { "Tebru\\Retrofit\\Test\\": "tests/" } }, "suggest": { "guzzlehttp/guzzle": "Required to make requests" }, "bin": ["bin/retrofit"] } ================================================ FILE: docs/advanced_usage.md ================================================ Advanced Usage ============== Caching ------- By default, Retrofit doesn't do any filesystem caching. In production, you should enable caching to increase performance. ```php Retrofit::builder() ->setCacheDir(__DIR__.'/cache') ->enableCache(); ``` Custom Converters ----------------- By default, Retrofit can convert any type to a string, but only handles PSR-7 `StreamInterface` for request and response bodies. Use a custom converter to allow handling different types. ```php Retrofit::builder() ->addConverterFactory(new CustomConverterFactory()); ``` This should implement `Tebru\Retrofit\ConverterFactory` with three methods to convert to string, to a stream for a request body, or from a stream for a response body. Return null from any of them will cause Retrofit to skip that converter and move on to the next one. Each method will receive a `TypeToken`, which you can use to determine the type of the parameter you're converting. Currently only a Gson converter exists to handle more complex conversions. ```bash composer require tebru/retrofit-php-converter-gson ``` Custom Http Client ------------------ Retrofit doesn't provide any way to make http requests out of the box. Currently, Guzzle 6 is the only supported library. ```bash composer require tebru/retrofit-php-http-guzzle6 ``` ```php Retrofit::builder() ->setHttpClient(new Guzzle6HttpClient(new Client())); ``` This implements `Tebru\Retrofit\HttpClient` which has methods to make requests synchronously and asynchronously using PSR-7 request objects. Custom Call Adapters -------------------- The default call adapter doesn't modify the Call at all. Implement a `Tebru\Retrofit\CallAdapter` and `Tebru\Retrofit\CallAdapterFactory` if you want to change the return type from service methods. This supporting library doesn't exist yet, but if you wanted to use Retrofit with Rx-PHP, you could do so like this. ```php /** * @GET("/") */ public function test(): Observable; ``` ```php Retrofit::builder() ->addCallAdapterFactory(new RxCallAdapterFactory()); ``` ```php interface CallAdapterFactory { public function supports(TypeToken $type): bool { return $type->isA(Observable::class); } public function create(TypeToken $type): CallAdapter { return new RxCallAdapter(); } } ``` The `RxCallAdapter` would then wrap the call in an observable and return that instead of the normal Call. Custom Proxy ------------ You may find it necessary to alter the default behavior of sending http requests. This could be for testing purposes, or perhaps because an api is not built yet and you need to fetch the data somewhere else. This can be handled by implementing `Tebru\Retrofit\ProxyFactory` and `\Tebru\Retrofit\Proxy`. ```php Retrofit::builder() ->addProxyFactory(new CustomProxyFactory()); ``` Your proxy should implement the service interface. If you need access to the default proxy factory, you can implement `\Tebru\Retrofit\DefaultProxyFactoryAware` which will tell Retrofit to set the default proxy to your proxy. This is useful if you only need to override specific methods and want to delegate the rest to the default. ================================================ FILE: docs/annotations.md ================================================ Annotation Reference ==================== A simple example will be provided to show usage for each annotation, but may not be a completely functioning snippet. All usages may not have an example. - [Body](#body) - [DELETE](#delete) - [ErrorBody](#errorbody) - [Field](#field) - [FieldMap](#fieldmap) - [GET](#get) - [HEAD](#head) - [Header](#header) - [HeaderMap](#headermap) - [Headers](#headers) - [OPTIONS](#options) - [Part](#part) - [PartMap](#partmap) - [PATCH](#patch) - [Path](#path) - [POST](#post) - [PUT](#put) - [Query](#query) - [QueryMap](#querymap) - [QueryName](#queryname) - [REQUEST](#request) - [ResponseBody](#responsebody) - [Url](#url) Body ---- Defines a json body for the HTTP request. This annotation sets the content type of the request to `application/json`. By default, the parameter must be a PSR-7 `StreamInterface`, but adding a converter will allow additional types to be provided and converted to json. ```php /** * @Body("myBody") */ public function example(Mybody $myBody): Call; ``` DELETE ------ Performs a DELETE request to the provided path. ```php /** * @DELETE("/foo?q=test") */ public function example(): Call; ``` ErrorBody --------- Defines the type the response body should be converted to on errors. Errors are defined as any http status code outside of 200-300. By default, only `StreamInterface` is supported, but a converter will extend the supported types. ```php /** * @ErrorBody("App\MyClass") */ public function example(): Call; ``` Field ----- Adds values to a form encoded body. This annotation sets the request content type to `application/x-www-form-urlencoded`. The value of this annotation is both the name of the field and the name of the method parameter. This can be overridden using the `var` key in the annotation. The provided argument will be converted to a string before getting applied to the request. Passing in an array will apply each value to the field name. The `encoded` key will specify if the data is already encoded, and therefore shouldn't be re-encoded. This value defaults to false. ```php /** * @Field("field1") * @Field("field2", encoded=true) * @Field("field3[]", var="field3") */ public function example(int $field1, bool $field2, array $field3): Call; ``` FieldMap -------- This works much like [@Field](#field), except it must be provided an iterable map. Each key/value pair will be treated as a `@Field` with the key acting as the field name. The value may be anything that `@Field` supports. ```php /** * @FieldMap("fieldMap") */ public function example(Traversable $fieldMap): Call; ``` GET --- Performs a GET request to the provided path. ```php /** * @GET("/foo?q=test") */ public function example(): Call; ``` HEAD ---- Performs a HEAD request to the provided path. ```php /** * @HEAD("/foo?q=test") */ public function example(): Call; ``` Header ------ Add a single header to a request. The value of this annotation represents both the name of the header and the parameter name. Header names are lower-cased before getting added to the request. Headers follow PSR-7 request format, so multiple values with the same name may be added to the request. Passing an array as the header value also allows multiple values to be added under the same header name. Headers will converted to a string before being applied to the request. ```php /** * @Header("X-Foo", var="header1") * @Header("X-Foo", var="header2") * @Header("X-Foo", var="header3") */ public function example(string $header1, bool $header2, array $header3): Call; ``` The above example will add multiple different values to the `x-foo` header. HeaderMap --------- Allows a way to add multiple headers to a request at once. The annotation acts much like [FieldMap](#fieldmap), and accepts the same values as [Header](#header). ```php /** * @HeaderMap("headerMap") */ public function example(Traversable $headerMap): Call; ``` Headers ------- Adds headers statically supplied in the annotation value. ```php /** * @Headers({ * "X-Foo: Bar", * "X-Ping: Pong" * }) */ ``` OPTIONS ------- Performs an OPTIONS request to the provided path. ```php /** * @OPTIONS("/foo?q=test") */ public function example(): Call; ``` Part ---- Adds values to a multipart body. This annotation set the request content type to `multipart/form-data`. This annotation can accept any value [@Body](#body) can or a special `MultipartBody` class, which you can use to set additional properties like headers or the filename. The annotation value specifies the part name. If you're using `MultipartBody` the part name will be pulled from the object and the annotation value will be ignored. ```php /** * @Part("part1") * @Part("part2") */ public function example(MyBody $part1, MultipartBody $part2): Call; ``` PartMap ------- This annotation works like previous map annotations. Keys represent part names, and values may be anything that a [@Part](#part) can accept. The same rules apply. ```php /** * @PartMap("partMap") */ public function example(Traversable $partMap): Call; ``` PATCH ----- Performs a PATCH request to the provided path. ```php /** * @PATCH("/foo?q=test") */ public function example(): Call; ``` Path ---- This annotation allows you to create a dynamic url, and map path parts to method parameters. By default, the annotation value will be the same as the url placeholder and parameter name, but this is overridable by using the `var` annotation key. Path values will get converted to strings before getting added to the url. ```php /** * @GET("/foo/{user}/{foo-bar}" * @Path("user") * @Path("foo-bar", var="fooBar") */ public function example(int $user, string $fooBar): Call; ``` POST ---- Performs a POST request to the provided path. ```php /** * @POST("/foo?q=test") */ public function example(): Call; ``` PUT --- Performs a PUT request to the provided path. ```php /** * @PUT("/foo?q=test") */ public function example(): Call; ``` Query ----- Add a query to the url. The annotation value represents the query name. The value will be converted to a string before being added to the url. Use the `var` key if the parameter name doesn't match the query name and the `encoded` key to specify if the query data is already encoded. This defaults to false. Passing an array will apply each value to to the specified name. ```php /** * @Query("query1") * @Query("query2", encoded=true) * @Query("query3[]", var="field3") */ public function example(int $query1, bool $query2, array $query3): Call; ``` QueryMap -------- Adds multiple queries to a url. This annotation works the same as other map annotations and adds key/value pairs of queries to a url where keys represent the query name and the value may be anything that [@Query](#query) allows. ```php /** * @QueryMap("queryMap") */ public function example(Traversable $queryMap): Call; ``` QueryName --------- Adds only the query name part of a query, excluding the `=` and anything after it. The annotation value represents the name (which can be changed with the `var` key), and you can specify that the data is already encoded with the `encoded` key. Data is converted to a string before getting added to the url. ```php /** * @QueryName("queryName1") * @QueryName("queryName2", encoded=true) */ public function example(float $queryName1, string $queryName2): Call; ``` REQUEST ------- Performs a custom request method to the provided path. Additionally, the method type and whether the request contains a body must be specified. ```php /** * @REQUEST("/foo?q=test", type="FOO", body=false) */ public function example(): Call; ``` ResponseBody ------------ Defines the type the response body should be converted to on success. Successful responses are defined as any http status code with 200-300. By default, only `StreamInterface` is supported, but a converter will extend the supported types. ```php /** * @ResponseBody("App\MyClass") */ public function example(): Call; ``` Url --- This annotation allows changing the base url of a request. This will allow overriding whatever url is specified on the `RetrofitBuilder`. ```php /** * @Url("baseUrl") */ public function example(string $baseUrl): Call; ``` ================================================ FILE: docs/installation.md ================================================ Installation ============ Installation with Composer -------------------------- ```bash composer require tebru/retrofit-php:~3.0 ``` Retrofit does not include an http client, please install an implementation. ```bash composer require tebru/retrofit-php-http-guzzle6 ``` Retrofit does not support converting request or response bodies outside PSR-7 `StreamInterface`. Install a converter to handle custom conversions. ```bash composer require tebru/retrofit-php-converter-gson ``` Setup ----- The easiest way to get setup is to add the psr-4 autoload location where you include the autoloader. ```php $loader = require __DIR__ . '/vendor/autoload.php'; $loader->addPsr4('Tebru\\Retrofit\\Proxy\\', __DIR__ . '/retrofit'); ``` If you do not have environment specific cache directories, you could specify the psr-4 autoload location in your composer.json instead. ```json "autoload": { "psr-4": { "Tebru\\Retrofit\\Proxy\\": "/retrofit" } } ``` ================================================ FILE: docs/upgrade_2_3.md ================================================ Upgrading from Retrofit 2 to Retrofit 3 ======================================= Retrofit 3 is a complete rewrite of Retrofit 2 in an attempt to make the library operate more closely with the Square version. This includes changing how annotations operate. Therefore, creating a friendly upgrade path would have been extremely time consuming, and not that useful, as nearly everything has been deprecated. - [Overview](#overview) - [Feature Updates](#feature-updates) - [Dependency Updates](#dependency-updates) - [File Updates](#file-updates) - [Annotation Updates](#annotation-updates) Overview -------- The way that requests are sent and responses are handled has changed dramatically. Walking through how the steps have changed is the best way to illustrate these differences. ### Retrofit 2 1. Collect all services class names 2. Read annotations and generate PHP code based on annotations 3. Cache code on filesystem 4. Use RestAdapterBuilder to build a RestAdapter instance 5. Use RestAdapter to load the service class from the filesystem and return instance 6. Call methods on the service to make http requests 7. Return expected response based on annotations ### Retrofit 3 1. Use RetrofitBuilder to build a Retrofit instance 2. Use Retrofit to ask for a Proxy object based on interface class name 3. Generate proxy object, which is an implementation of the provided interface 4. Call methods on proxy object, which internally reads annotations and returns a Call object 5. Use the call object to execute a request synchronously or asynchronously, and returns a Response object 6. Fetch deserialized response body from Response object based on success/failure of request The new method has several advantages. - Nothing is written to the filesystem unless caching is enabled. This fixes some issues with interfaces changing in development. - All requests can be made asynchronously. Previously a Callback needed to be defined in the method signature to enable async requests. - There is more flexibility in getting different deserialized response bodies depending on the success or failure of the request. - The code is testable, as the only generated code is tiny and the same for every method as it just delegate to a real class. Previously testing what got generated was painful and prone to error. In addition, the generated class namespace has changed from `Tebru\Retrofit\Generated` to `Tebru\Retrofit\Proxy`. Feature Updates --------------- The following are high level feature changes - The event system has been removed - All methods require a return type (this must be `Call` if not using a custom adapter) - All method parameter require a typehint - All types are supported for all annotations mapped to a parameter. This is handled through the converter system. - Custom annotations may be used to modify the request Dependency Updates ------------------ ### Upgraded - php: from 5.4 to 7.1 - nikic/php-parer: from ~1.3 to ~3.0.6 ### Added - symfony/cache (switched from doctrine/cache) - tebru/doctrine-annotation-reader - tebru/php-type ### Removed - jms/serializer - phpoption/phpption - tebru/assert - tebru/dynamo - tebru/retrofit-http-clients - symfony/event-dispatcher - symfony/filesystem - psr/log - doctrine/cache - doctrine/collections Additionally removed the coveralls binary that was used for pushing code coverage to Coveralls. This unfortunately added Guzzle 3 classes, which could be confusing if the binary wasn't ignored. File Updates ------------ There are many new files, but the directory structure has changed. There is now a an `Internal` package that represents classes that should only be used internally by the library. They may change between releases, but will not represent a BC break. All classes will be referenced through an interface or abstract class with few exceptions. ### Removed Directories The following directories and all files have been removed. - src/Adapter - src/Generation - src/Event - src/Exception - src/Subscriber ### Removed Files The following files have been removed. - src/Http/AsyncAware.php - src/Http/Callback.php - src/Http/Response.php Annotation Updates ------------------ Many annotations have changed behavior. Additionally, annotations have been added, removed, and renamed. ### Changed Annotations - @Body now only works with application/json requests - @Body may not appear with requests that do not have a body - @Body removed jsonSerializable option, use a custom converter instead - @GET, @HEAD, @OPTIONS, @DELETE may no longer be used with an request that contains a body - @Part now specifically denotes a multipart request - @Part now allows specification of encoding type (defaults to binary) - @Part must accept a body value that can be converted, or the new MultipartBody - @PartMap is an iterable where keys are names as strings and values are the same as @Part - @Query added ability to specify whether data is pre-encoded or not - Passing an array to a @Query parameter will now create separate parameters with the name as the query name and the value as one of the array - @QueryMap is now an iterable with supported values the same as @Query ### Added Annotations - @Field: Used only for form encoded request bodies. An array will add all values to the field name - @FieldMap, @HeaderMap: Like corresponding annotation, but for any iterable where string keys are the name - @Path is new and required to map url placeholders to parameters - @QueryName allows adding a query parameter that doesn’t have a value - @REQUEST is a generic http request annotation like @GET or @POST ### Removed Annotations - @FormUrlEncoded - use @Field/@FieldMap instead - @JsonBody - use @Body instead - @Multipart - use @Part/@PartMap instead - @ResponseType – this was specifically created to handle cases where a response instead of body was returned and is no longer needed - @SerializationContext/@DeserializationContext - jms/serializer was removed from project ### Renamed Annotations - @BaseUrl to @Url - @Returns to @ResponseBody ================================================ FILE: docs/usage.md ================================================ Usage ===== To start, it would be a good idea to get an understanding of what Retrofit is doing under the hood. Let's assume we're using the following service definition ```php interface GitHubService { /** * @GET("/users/{user}/list") * @Path("user") * @Query("limit") */ public function listRepos(string $user, int $limit): Call; } ``` First, you need an instance of `Retrofit`. Here is the easiest way to create one. ```php $retrofit = Retrofit::builder() ->setBaseUrl('http://api.example.com') ->setHttpClient(new Guzzle6HttpClient(new Client())) ->build(); ``` Creating a `GitHubService` implementation is easy ```php $githubService = $retrofit->create(GitHubService::class); ``` This is going to generate a `Proxy` object that implements the `GitHubService` interface. You can use the proxy like you normally would use an instance of `GitHubService`. ```php $call = $githubService->listRepos('octocat', 10); ``` This will parse the `GitHubService` interface, and store the following information about the `listRepos` method: 1. It should make a `GET` request to `/users/{user}/list` 2. `{user}` is provided by the `$user` parameter, which is a string 3. `limit` is a query parameter and provided by `$limit`, which is an integer 3. We're returning a `Call` object 4. We provided two arguments and they're `octocat` and `10` This information is available in the `Call` object, so when you're ready to make the request, it has enough information to construct the request. Doing so can be done synchronously or asynchronously. Here's an example of a synchronous request ```php $response = $call->execute(); ``` And asynchronously ```php $call->enqueue( function(Response $response) { // handle any response (200, 404, 500) }, function(Throwable $throwable) { // handle an exception (network unreachable) } ); $call->wait(); ``` Both of the callbacks are optional. If you just wanted to make the request and didn't care about the response, you could do this instead ```php $call->enqueue(); $call->wait(); ``` Calling `->wait()` is necessary to trigger sending the requests. Using the guzzle client, the requests are sent in batches of 5 using a Pool. This allows callbacks to trigger as soon as one response is received. `Response` here is a Retrofit Response (`Tebru\Retrofit\Response`). It provides an easy way to check if the request was successful (if the status code is between 200 and 300) and methods for getting the success or error response bodies. ```php if ($response->isSuccessful()) { // success body $response->body(); // StreamInterface } else { // error body $response->errorBody(); // StreamInterface } ``` By default, Retrofit only works with a PSR-7 `StreamInterface` for setting request bodies and handling response bodies. This is somewhat limiting, but can be enhanced by using converters. The converter I'm going to demonstrate uses [Gson](https://github.com/tebru/gson-php) because it can handle the conversion from any type to json and back. Adding it to the builder is simple ```php Retrofit::builder() // ... ->addConverterFactory(new GsonConverterFactory(Gson::builder()->build())) // ... ``` And allows using a lot more annotations of the client. Here's an example using the `ResponseBody` and `ErrorBody` annotations to inform Retrofit how to convert success and error responses ```php interface GitHubService { /** * @GET("/users/{user}/list") * @Path("user") * @Query("limit") * @ResponseBody("App\GithubService\ListRepo") * @ErrorBody("App\GitHubService\ApiError") */ public function listRepos(string $user, int $limit): Call; } ``` The annotation value is the class name that the response should be deserialized into. This allows for cleaner handling of responses ```php if (!$response->isSuccessful()) { // throw an exception with the deserialized body to be handled elsewhere throw new ApiException($response->errorBody()); } $listRepos = $response->body(); foreach ($listRepos as $repo) { // iterate over the repo list } ``` Converters can also be used when sending json. For example, assume this service definition ```php /** * @ErrorBody("App\GitHubService\ApiError") */ interface GitHubService { /** * @POST("/user/repos") * @Body("repo") * @ResponseBody("App\GitHubService\Repo") */ public function createRepo(Repo $repo): Call; } ``` Because we have already registered a converter that can handle any object, Retrofit will make a POST request to `/user/repos`, convert the `Repo` object to json, add a `Content-Type` header of `application/json`, and deserialize the response into a `App\GitHubService\Repo` object. We've discussed `RequestBodyConverter`s and `ResponseBodyConverters`s. The third type is a `StringConverter`. Adding query parameters to the request was introduced earlier and uses a string converter to convert various types to strings. As seen before, we were able to easily convert an integer to a string. The same works for a string, float, and boolean types. Booleans get converted to `"true"` or `"false"`. However, using an array as a query parameter has a unique effect as it will apply each item in the array to the same query key. Let's look at an example. ```php interface GitHubService { /** * @GET("/user/list") * @Query("affiliation[]", var="affiliations") */ public function listRepos(array $affiliations): Call; } $githubService->listRepos(['owner', 'collaborator']); ``` *The actual GitHub API passes affiliations as a comma separated string, but for the purposes of this example, we'll pretend it's in array syntax.* Here we're using the `var` key to tell Retrofit to not look for a parameter called `$affiliation[]`—as that's illegal—but `$affiliations` instead. This will create the query string ``` affiliation[]=owner&affiliation[]=collaborator ``` The `[]` at the end is not magic, Retrofit would behave the same way if it was missing. It just tells servers that this data is an array. Multiple `@Query` annotations may be used on the same method ```php interface GitHubService { /** * @GET("/user/list") * @Query("affiliation[]", var="affiliations") * @Query("sort") */ public function listRepos(array $affiliations, string $sort): Call; } ``` This can tend to get lengthy, which is where `@QueryMap` becomes useful. A QueryMap is just an iterable hash of string keys and values. The values can be anything that can be passed to a regular `@Query`. Here's an example using an array ```php interface GitHubService { /** * @GET("/user/list") * @QueryMap("queries") */ public function listRepos(array $queries): Call; } $githubService->listRepos([ 'affiliations[]' => ['owner', 'collaborator'], 'sort' => 'created', ]); ``` But an object that implements `Iterable` could also be used ```php interface GitHubService { /** * @GET("/user/list") * @QueryMap("queries") */ public function listRepos(ListReposQueries $queries): Call; } $githubService->listRepos(new ListReposQueries()); ``` Parameters (and all parameters) are optional and accept default values. Specifying default null here will not send any query parameters. ```php interface GitHubService { /** * @GET("/user/list") * @QueryMap("queries") */ public function listRepos(?ListReposQueries $queries = null): Call; } $githubService->listRepos(); ``` This behavior is consistent with `@Field`/`@FieldMap` and `@Header`/`@HeaderMap`. Please see the [Annotation Reference](annotations.md) for a more in-depth look at the different annotations and how they function. ================================================ FILE: phpcs.xml ================================================ ./src ================================================ FILE: phpunit.xml ================================================ tests/Unit src ================================================ FILE: src/Annotation/Body.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Body extends ParameterAnnotation { /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return string */ public function converterType(): ?string { return RequestBodyConverter::class; } } ================================================ FILE: src/Annotation/DELETE.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class DELETE extends AbstractAnnotation implements HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return 'delete'; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return false; } } ================================================ FILE: src/Annotation/Encodable.php ================================================ */ abstract class Encodable extends ParameterAnnotation { /** * The values are already encoded * * @var bool */ private $encoded; /** * Initialize annotation data */ protected function init(): void { parent::init(); $this->encoded = $this->data['encoded'] ?? false; } /** * Returns true if the values are already encoded * * @return bool */ public function isEncoded(): bool { return $this->encoded; } /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return string */ public function converterType(): ?string { return StringConverter::class; } } ================================================ FILE: src/Annotation/ErrorBody.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class ErrorBody extends AbstractAnnotation { } ================================================ FILE: src/Annotation/Field.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Field extends Encodable { /** * Whether or not multiple annotations of this type can * be added to a method * * @return bool */ public function allowMultiple(): bool { return true; } } ================================================ FILE: src/Annotation/FieldMap.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class FieldMap extends Encodable { /** * Returns true if multiple annotations of this type are allowed * * @return bool */ public function allowMultiple(): bool { return true; } } ================================================ FILE: src/Annotation/GET.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class GET extends AbstractAnnotation implements HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return 'get'; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return false; } } ================================================ FILE: src/Annotation/HEAD.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class HEAD extends AbstractAnnotation implements HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return 'head'; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return false; } } ================================================ FILE: src/Annotation/Header.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Header extends ParameterAnnotation { /** * Whether or not multiple annotations of this type can * be added to a method * * @return bool */ public function allowMultiple(): bool { return true; } /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return string */ public function converterType(): ?string { return StringConverter::class; } } ================================================ FILE: src/Annotation/HeaderMap.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class HeaderMap extends ParameterAnnotation { /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return string */ public function converterType(): ?string { return StringConverter::class; } /** * Returns true if multiple annotations of this type are allowed * * @return bool */ public function allowMultiple(): bool { return true; } } ================================================ FILE: src/Annotation/Headers.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Headers extends AbstractAnnotation { /** * Initialize annotation data * * @throws \RuntimeException */ protected function init(): void { // loop through each string and break on ':' foreach ((array)$this->getValue() as $header) { $position = strpos($header, ':'); if ($position === false) { throw new RuntimeException('Retrofit: Header in an incorrect format. Expected "Name: value"'); } $name = trim(substr($header, 0, $position)); $value = trim(substr($header, $position + 1)); $this->value[$name] = $value; } } } ================================================ FILE: src/Annotation/HttpRequest.php ================================================ */ interface HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string; /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool; /** * Get the url value * * @return mixed */ public function getValue(); } ================================================ FILE: src/Annotation/OPTIONS.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class OPTIONS extends AbstractAnnotation implements HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return 'options'; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return false; } } ================================================ FILE: src/Annotation/PATCH.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class PATCH extends AbstractAnnotation implements HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return 'patch'; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return true; } } ================================================ FILE: src/Annotation/POST.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class POST extends AbstractAnnotation implements HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return 'post'; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return true; } } ================================================ FILE: src/Annotation/PUT.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class PUT extends AbstractAnnotation implements HttpRequest { /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return 'put'; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return true; } } ================================================ FILE: src/Annotation/ParameterAnnotation.php ================================================ $parameterName] format * * @author Nate Brunette */ abstract class ParameterAnnotation extends AbstractAnnotation implements ParameterAwareAnnotation { /** * An alias for the variable name * * @var string $var */ private $var; /** * Initialize annotation data */ protected function init(): void { parent::init(); $this->var = $this->data['var'] ?? null; } /** * Get the variable name * * @return string */ public function getVariableName(): string { return $this->var ?? $this->getValue(); } } ================================================ FILE: src/Annotation/ParameterAwareAnnotation.php ================================================ */ interface ParameterAwareAnnotation { /** * The variable name, which will either be the default value or the value of 'var' if * specified. The variable name excludes the '$'. * * @return string */ public function getVariableName(): string; /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return null|string */ public function converterType(): ?string; } ================================================ FILE: src/Annotation/Part.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Part extends ParameterAnnotation { /** * How the multipart request is encoded * * @var string */ private $encoding; /** * Initialize annotation data */ protected function init(): void { parent::init(); $this->encoding = $this->data['encoding'] ?? 'binary'; } /** * Get the encoding type * * @return string */ public function getEncoding(): string { return $this->encoding; } /** * Whether or not multiple annotations of this type can * be added to a method * * @return bool */ public function allowMultiple(): bool { return true; } /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return string */ public function converterType(): ?string { return RequestBodyConverter::class; } } ================================================ FILE: src/Annotation/PartMap.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class PartMap extends ParameterAnnotation { /** * How the multipart request is encoded * * @var string */ private $encoding; /** * Initialize annotation data */ protected function init(): void { parent::init(); $this->encoding = $this->data['encoding'] ?? 'binary'; } /** * Get the encoding type * * @return string */ public function getEncoding(): string { return $this->encoding; } /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return string */ public function converterType(): ?string { return RequestBodyConverter::class; } /** * Returns true if multiple annotations of this type are allowed * * @return bool */ public function allowMultiple(): bool { return true; } } ================================================ FILE: src/Annotation/Path.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Path extends ParameterAnnotation { /** * Returns true if multiple annotations of this type are allowed * * @return bool */ public function allowMultiple(): bool { return true; } /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return string */ public function converterType(): ?string { return StringConverter::class; } } ================================================ FILE: src/Annotation/Query.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Query extends Encodable { /** * Whether or not multiple annotations of this type can * be added to a method * * @return bool */ public function allowMultiple(): bool { return true; } } ================================================ FILE: src/Annotation/QueryMap.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class QueryMap extends Encodable { /** * Returns true if multiple annotations of this type are allowed * * @return bool */ public function allowMultiple(): bool { return true; } } ================================================ FILE: src/Annotation/QueryName.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class QueryName extends Encodable { /** * Whether or not multiple annotations of this type can * be added to a method * * @return bool */ public function allowMultiple(): bool { return true; } } ================================================ FILE: src/Annotation/REQUEST.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class REQUEST extends AbstractAnnotation implements HttpRequest { /** * The request method * * @var string */ private $type; /** * If the request contains a body * * @var bool */ private $body; /** * Initialize annotation data */ protected function init(): void { $this->assertKey('type'); $this->type = $this->data['type']; $this->body = $this->data['body'] ?? false; } /** * Returns the type of the annotation (get, post, put, etc) * * @return string */ public function getType(): string { return $this->type; } /** * Returns true if the request type contains a body * * @return bool */ public function hasBody(): bool { return $this->body; } } ================================================ FILE: src/Annotation/ResponseBody.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class ResponseBody extends AbstractAnnotation { } ================================================ FILE: src/Annotation/Url.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class Url extends ParameterAnnotation { /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return null|string */ public function converterType(): ?string { return StringConverter::class; } } ================================================ FILE: src/AnnotationHandler.php ================================================ */ interface AnnotationHandler { /** * Handle an annotation, mutating the [@see ServiceMethodBuilder] based on the value * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter|RequestBodyConverter|null $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void; } ================================================ FILE: src/Call.php ================================================ */ interface Call { /** * Execute request synchronously * * A [@see Response] will be returned * * @return Response */ public function execute(): Response; /** * Execute request asynchronously * * This method accepts two optional callbacks. * * onResponse() will be called for any request that gets a response, * whether it was successful or not. It will send a [@see Call] and * a [@see Response] as the first and second parameters. * * onFailure() will be called in the event a network request failed. It * will send the [@see Throwable] that was encountered. * * Example of method signatures: * * $call->enqueue( * function (\Tebru\Retrofit\Call $call, \Tebru\Retrofit\Response $response) {}, * function (\Throwable $throwable) {} * ); * * @param callable $onResponse On any response * @param callable $onFailure On any network request failure * @return Call */ public function enqueue(?callable $onResponse = null, ?callable $onFailure = null): Call; /** * When making requests asynchronously, call wait() to execute the requests * * @return void */ public function wait(): void; /** * Get the PSR-7 request * * @return RequestInterface */ public function request(): RequestInterface; } ================================================ FILE: src/CallAdapter.php ================================================ */ interface CallAdapter { /** * Accepts a [@see Call] and converts it to the appropriate type * * @param Call $call * @return mixed */ public function adapt(Call $call); } ================================================ FILE: src/CallAdapterFactory.php ================================================ */ interface CallAdapterFactory { /** * Returns true if the factory supports this type * * @param TypeToken $type * @return bool */ public function supports(TypeToken $type): bool; /** * Create a new factory from type * * @param TypeToken $type * @return CallAdapter */ public function create(TypeToken $type): CallAdapter; } ================================================ FILE: src/Command/CompileCommand.php ================================================ * @codeCoverageIgnore */ class CompileCommand extends Command { /** * Configure command * * @throws InvalidArgumentException */ protected function configure(): void { $this->setName('compile'); $this->setDescription('Compiles and caches all services found in the project'); $this->addArgument('sourceDirectory', InputArgument::REQUIRED, 'Enter the source directory'); $this->addArgument('cacheDirectory', InputArgument::REQUIRED, 'Enter the cache directory'); } /** * Execute command * * @param InputInterface $input * @param OutputInterface $output * @return int|null|void */ protected function execute(InputInterface $input, OutputInterface $output) { $srcDir = $input->getArgument('sourceDirectory'); $cacheDir = $input->getArgument('cacheDirectory'); $clientStub = new class implements HttpClient { /** * Send a request synchronously and return a PSR-7 [@see ResponseInterface] * * @param RequestInterface $request * @return ResponseInterface */ public function send(RequestInterface $request): ResponseInterface { return new Response(); } /** * Send a request asynchronously * * The response callback must be called if any response is returned from the request, and the failure * callback should only be executed if a request was not completed. * * The response callback should pass a PSR-7 [@see ResponseInterface] as the one and only argument. The * failure callback should pass a [@see Throwable] as the one and only argument. * * @param RequestInterface $request * @param callable $onResponse * @param callable $onFailure * @return void */ public function sendAsync(RequestInterface $request, callable $onResponse, callable $onFailure): void { } /** * Calling this method should execute any enqueued requests asynchronously * * @return void */ public function wait(): void { } }; $retrofit = Retrofit::builder() ->setBaseUrl('') ->setHttpClient($clientStub) ->setCacheDir($cacheDir) ->enableCache() ->build(); $count = $retrofit->createAll($srcDir); $output->writeln(sprintf('Compiled %s %s successfully', $count, ($count === 1) ? 'class' : 'classes')); } } ================================================ FILE: src/Converter.php ================================================ */ interface Converter { } ================================================ FILE: src/ConverterFactory.php ================================================ */ interface ConverterFactory { /** * Return a [@see ResponseBodyConverter] or null * * @param TypeToken $type * @return null|ResponseBodyConverter */ public function responseBodyConverter(TypeToken $type): ?ResponseBodyConverter; /** * Return a [@see RequestBodyConverter] or null * * @param TypeToken $type * @return null|RequestBodyConverter */ public function requestBodyConverter(TypeToken $type): ?RequestBodyConverter; /** * Return a [@see StringConverter] or null * * @param TypeToken $type * @return null|StringConverter */ public function stringConverter(TypeToken $type): ?StringConverter; } ================================================ FILE: src/DefaultProxyFactoryAware.php ================================================ */ interface DefaultProxyFactoryAware { /** * Set the default proxy factory * * @param ProxyFactory $proxyFactory * @return void */ public function setDefaultProxyFactory(ProxyFactory $proxyFactory): void; } ================================================ FILE: src/Exception/ResponseHandlingFailedException.php ================================================ */ class ResponseHandlingFailedException extends RuntimeException { /** * @var RequestInterface */ private $request; /** * @var ResponseInterface */ private $response; /** * Constructor * * @param RequestInterface $request * @param ResponseInterface $response * @param string $message * @param Throwable|null $previous */ public function __construct( RequestInterface $request, ResponseInterface $response, string $message = '', Throwable $previous = null ) { parent::__construct($message, 0, $previous); $this->request = $request; $this->response = $response; } /** * @return RequestInterface */ public function getRequest(): RequestInterface { return $this->request; } /** * @return ResponseInterface */ public function getResponse(): ResponseInterface { return $this->response; } } ================================================ FILE: src/Finder/ServiceResolver.php ================================================ */ class ServiceResolver { private const ANNOTATION_REGEX = '/Tebru\\\\Retrofit\\\\Annotation/'; private const FILE_REGEX = '/^.+\.php$/i'; private const INTERFACE_REGEX = '/^interface\s+([\w\\\\]+)[\s{\n]?/m'; private const NAMESPACE_REGEX = '/^namespace\s+([\w\\\\]+)/m'; /** * Find all services given a source directory * * @param string $srcDir * @return string[] */ public function findServices(string $srcDir): array { $directory = new RecursiveDirectoryIterator($srcDir); $iterator = new RecursiveIteratorIterator($directory); $files = new RegexIterator($iterator, self::FILE_REGEX, RecursiveRegexIterator::GET_MATCH); $services = []; foreach ($files as $file) { $fileString = file_get_contents($file[0]); $annotationMatchesFound = preg_match(self::ANNOTATION_REGEX, $fileString); if (!$annotationMatchesFound) { continue; } $interfaceMatchesFound = preg_match(self::INTERFACE_REGEX, $fileString, $interfaceMatches); if (!$interfaceMatchesFound) { continue; } $namespaceMatchesFound = preg_match(self::NAMESPACE_REGEX, $fileString, $namespaceMatches); $className = ''; if ($namespaceMatchesFound) { $className .= $namespaceMatches[1]; } $className .= '\\' . $interfaceMatches[1]; $services[] = $className; } return $services; } } ================================================ FILE: src/Http/MultipartBody.php ================================================ */ final class MultipartBody { /** * @var string */ private $name; /** * @var StreamInterface */ private $contents; /** * @var array */ private $headers; /** * @var string */ private $filename; /** * Constructor * * @param string $name * @param mixed $contents * @param array[] $headers * @param null|string $filename */ public function __construct(string $name, $contents, array $headers = [], ?string $filename = null) { $this->name = $name; $this->contents = stream_for($contents); $this->headers = $headers; $this->filename = $filename; } /** * @return string */ public function getName(): string { return $this->name; } /** * @return StreamInterface */ public function getContents(): StreamInterface { return $this->contents; } /** * @return array */ public function getHeaders(): array { return $this->headers; } /** * @return string|null */ public function getFilename(): ?string { return $this->filename; } } ================================================ FILE: src/HttpClient.php ================================================ */ interface HttpClient { /** * Send a request synchronously and return a PSR-7 [@see ResponseInterface] * * @param RequestInterface $request * @return ResponseInterface */ public function send(RequestInterface $request): ResponseInterface; /** * Send a request asynchronously * * The response callback must be called if any response is returned from the request, and the failure * callback should only be executed if a request was not completed. * * The response callback should pass a PSR-7 [@see ResponseInterface] as the one and only argument. The * failure callback should pass a [@see Throwable] as the one and only argument. * * @param RequestInterface $request * @param callable $onResponse * @param callable $onFailure * @return void */ public function sendAsync(RequestInterface $request, callable $onResponse, callable $onFailure): void; /** * Calling this method should execute any enqueued requests asynchronously * * @return void */ public function wait(): void; } ================================================ FILE: src/Internal/AnnotationHandler/BodyAnnotHandler.php ================================================ */ final class BodyAnnotHandler implements AnnotationHandler { /** * Sets the request method as 'json' and adds a parameter handler for body json data * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|RequestBodyConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$converter instanceof RequestBodyConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a RequestBodyConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->setIsJson(); $serviceMethodBuilder->addParameterHandler($index, new BodyParamHandler($converter)); } } ================================================ FILE: src/Internal/AnnotationHandler/FieldAnnotHandler.php ================================================ */ final class FieldAnnotHandler implements AnnotationHandler { /** * Set the content type to form encoded and adds a parameter handler for individual fields * * @param Field|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof Encodable) { throw new InvalidArgumentException('Retrofit: Annotation must be encodable'); } if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->setIsFormUrlEncoded(); $serviceMethodBuilder->addParameterHandler( $index, new FieldParamHandler($converter, $annotation->getValue(), $annotation->isEncoded()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/FieldMapAnnotHandler.php ================================================ */ final class FieldMapAnnotHandler implements AnnotationHandler { /** * Set the content type to form encoded and adds a parameter handler for a field map * * @param FieldMap|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof Encodable) { throw new InvalidArgumentException('Retrofit: Annotation must be encodable'); } if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->setIsFormUrlEncoded(); $serviceMethodBuilder->addParameterHandler( $index, new FieldMapParamHandler($converter, $annotation->isEncoded()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/HeaderAnnotHandler.php ================================================ */ final class HeaderAnnotHandler implements AnnotationHandler { /** * Adds header param handler * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->addParameterHandler( $index, new HeaderParamHandler($converter, $annotation->getValue()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/HeaderMapAnnotHandler.php ================================================ */ final class HeaderMapAnnotHandler implements AnnotationHandler { /** * Adds header map param handler * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param null|Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->addParameterHandler($index, new HeaderMapParamHandler($converter)); } } ================================================ FILE: src/Internal/AnnotationHandler/HeadersAnnotHandler.php ================================================ */ final class HeadersAnnotHandler implements AnnotationHandler { /** * Set each header to request * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|null $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if ($converter !== null) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be null, %s found', \gettype($converter) )); } /** @var string[] $headerList */ $headerList = $annotation->getValue(); foreach ($headerList as $name => $header) { $serviceMethodBuilder->addHeader($name, $header); } } } ================================================ FILE: src/Internal/AnnotationHandler/HttpRequestAnnotHandler.php ================================================ */ final class HttpRequestAnnotHandler implements AnnotationHandler { /** * Sets the request method, uri, and whether or not the request contains a body * * @param HttpRequest|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|null $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \LogicException * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof HttpRequest) { throw new InvalidArgumentException('Retrofit: Annotation must be an HttpRequest'); } if ($converter !== null) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be null, %s found', \gettype($converter) )); } $uri = $annotation->getValue(); $serviceMethodBuilder->setMethod($annotation->getType()); $serviceMethodBuilder->setPath($uri); if (!$annotation->hasBody()) { $serviceMethodBuilder->setHasBody($annotation->hasBody()); } } } ================================================ FILE: src/Internal/AnnotationHandler/PartAnnotHandler.php ================================================ */ final class PartAnnotHandler implements AnnotationHandler { /** * Handle an annotation, mutating the [@see ServiceMethodBuilder] based on the value * * @param Part|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|RequestBodyConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof Part) { throw new InvalidArgumentException('Retrofit: Annotation must be a Part'); } if (!$converter instanceof RequestBodyConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a RequestBodyConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->setIsMultipart(); $serviceMethodBuilder->addParameterHandler( $index, new PartParamHandler($converter, $annotation->getValue(), $annotation->getEncoding()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/PartMapAnnotHandler.php ================================================ */ final class PartMapAnnotHandler implements AnnotationHandler { /** * Add part map param handler * * @param PartMap|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|RequestBodyConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof PartMap) { throw new InvalidArgumentException('Retrofit: Annotation must be a PartMap'); } if (!$converter instanceof RequestBodyConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a RequestBodyConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->setIsMultipart(); $serviceMethodBuilder->addParameterHandler( $index, new PartMapParamHandler($converter, $annotation->getEncoding()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/PathAnnotHandler.php ================================================ */ final class PathAnnotHandler implements AnnotationHandler { /** * Add a path param handler * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->addParameterHandler($index, new PathParamHandler($converter, $annotation->getValue())); } } ================================================ FILE: src/Internal/AnnotationHandler/QueryAnnotHandler.php ================================================ */ final class QueryAnnotHandler implements AnnotationHandler { /** * Add a query param handler * * @param Query|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof Encodable) { throw new InvalidArgumentException('Retrofit: Annotation must be encodable'); } if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->addParameterHandler( $index, new QueryParamHandler($converter, $annotation->getValue(), $annotation->isEncoded()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/QueryMapAnnotHandler.php ================================================ */ final class QueryMapAnnotHandler implements AnnotationHandler { /** * Add query map param handler * * @param QueryMap|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof Encodable) { throw new InvalidArgumentException('Retrofit: Annotation must be encodable'); } if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->addParameterHandler( $index, new QueryMapParamHandler($converter, $annotation->isEncoded()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/QueryNameAnnotHandler.php ================================================ */ final class QueryNameAnnotHandler implements AnnotationHandler { /** * Add a query name param handler * * @param QueryName|AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$annotation instanceof Encodable) { throw new InvalidArgumentException('Retrofit: Annotation must be encodable'); } if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->addParameterHandler( $index, new QueryNameParamHandler($converter, $annotation->isEncoded()) ); } } ================================================ FILE: src/Internal/AnnotationHandler/UrlAnnotHandler.php ================================================ */ final class UrlAnnotHandler implements AnnotationHandler { /** * Add url param handler * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void * @throws \InvalidArgumentException */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { if (!$converter instanceof StringConverter) { throw new InvalidArgumentException(sprintf( 'Retrofit: Converter must be a StringConverter, %s found', \gettype($converter) )); } $serviceMethodBuilder->addParameterHandler($index, new UrlParamHandler($converter)); } } ================================================ FILE: src/Internal/AnnotationProcessor.php ================================================ */ final class AnnotationProcessor { /** * An array of annotation handlers * * @var AnnotationHandler[] */ private $handlers; /** * Constructor * * @param AnnotationHandler[] $handlers */ public function __construct(array $handlers) { $this->handlers = $handlers; } /** * Accepts an annotation and delegates to an [@see AnnotationHandler] * * @param AbstractAnnotation $annotation * @param ServiceMethodBuilder $serviceMethodBuilder * @param ConverterProvider $converterProvider * @param ReflectionMethod $reflectionMethod * @throws \LogicException */ public function process( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ConverterProvider $converterProvider, ReflectionMethod $reflectionMethod ): void { $name = $annotation->getName(); if (!isset($this->handlers[$name])) { return; } $handler = $this->handlers[$name]; $converter = null; $index = null; if ($annotation instanceof ParameterAwareAnnotation) { $index = $this->findMethodParameterIndex($reflectionMethod, $annotation->getVariableName()); $type = $this->getParameterType($reflectionMethod, $index); $converter = $this->getConverter($annotation, $converterProvider, $type); } $handler->handle($annotation, $serviceMethodBuilder, $converter, $index); } /** * Find the position of the method parameter * * @param ReflectionMethod $reflectionMethod * @param string $name * @return int * @throws \LogicException */ private function findMethodParameterIndex(ReflectionMethod $reflectionMethod, string $name): int { $reflectionParameters = $reflectionMethod->getParameters(); foreach ($reflectionParameters as $index => $reflectionParameter) { if ($reflectionParameter->name === $name) { return $index; } } throw new LogicException(sprintf( 'Retrofit: Could not find parameter named %s in %s::%s. Please double check that annotations are properly ' . 'referencing method parameters.', $name, $reflectionMethod->getDeclaringClass()->name, $reflectionMethod->name )); } /** * Get the parameter type * * @param ReflectionMethod $reflectionMethod * @param int $index * @return TypeToken * @throws \LogicException */ private function getParameterType(ReflectionMethod $reflectionMethod, int $index): TypeToken { $reflectionParameter = $reflectionMethod->getParameters()[$index]; $reflectionType = $reflectionParameter->getType(); if ($reflectionType === null) { throw new LogicException(sprintf( 'Retrofit: Parameter type was not found for method %s::%s', $reflectionMethod->getDeclaringClass()->name, $reflectionMethod->name )); } /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ return new TypeToken($reflectionType->getName()); } /** * Get the converter from annotation converter class * * @param ParameterAwareAnnotation $annotation * @param ConverterProvider $converterProvider * @param TypeToken $type * @return Converter * @throws \LogicException */ private function getConverter( ParameterAwareAnnotation $annotation, ConverterProvider $converterProvider, TypeToken $type ): Converter { switch ($annotation->converterType()) { case RequestBodyConverter::class: return $converterProvider->getRequestBodyConverter($type); case StringConverter::class: return $converterProvider->getStringConverter($type); } throw new LogicException(sprintf( 'Retrofit: Unable to handle converter of type %s. Please use RequestBodyConverter or StringConverter', $annotation->converterType() )); } } ================================================ FILE: src/Internal/CacheProvider.php ================================================ = Symfony 4.3 if (class_exists('Symfony\Component\Cache\Psr16Cache')) { return new Psr16Cache(new ChainAdapter([ new Psr16Adapter(self::createMemoryCache()), new PhpFilesAdapter('', 0, $cacheDir), ])); } return new ChainCache([ self::createMemoryCache(), new PhpFilesCache('', 0, $cacheDir) ]); } /** * Create a "memory cache" depending on symfony/cache version * @return CacheInterface */ public static function createMemoryCache(): CacheInterface { // >= Symfony 4.3 if (class_exists('Symfony\Component\Cache\Psr16Cache')) { return new Psr16Cache(new ArrayAdapter(0, false)); } return new ArrayCache(0, false); } /** * Create a "null" cache (for annotations) depending on symfony/cache version * * @return CacheInterface */ public static function createNullCache(): CacheInterface { // >= Symfony 4.3 if (class_exists('Symfony\Component\Cache\Psr16Cache')) { return new Psr16Cache(new NullAdapter()); } return new NullCache(); } } ================================================ FILE: src/Internal/CallAdapter/CallAdapterProvider.php ================================================ */ final class CallAdapterProvider { /** * @var CallAdapterFactory[] */ private $callAdapterFactories; /** * Constructor * * @param CallAdapterFactory[] $callAdapterFactories */ public function __construct(array $callAdapterFactories) { $this->callAdapterFactories = $callAdapterFactories; } /** * Given a type, find the first available [@see CallAdapterFactory] and return it * * @param TypeToken $type * @return CallAdapter * @throws \LogicException */ public function get(TypeToken $type): CallAdapter { foreach ($this->callAdapterFactories as $callAdapterFactory) { if ($callAdapterFactory->supports($type)) { return $callAdapterFactory->create($type); } } throw new LogicException(sprintf('Retrofit: Could not get call adapter for type %s', $type)); } } ================================================ FILE: src/Internal/CallAdapter/DefaultCallAdapter.php ================================================ */ final class DefaultCallAdapter implements CallAdapter { /** * Accepts a [@see Call] and converts it to the appropriate type * * @param Call $call * @return Call */ public function adapt(Call $call): Call { return $call; } } ================================================ FILE: src/Internal/CallAdapter/DefaultCallAdapterFactory.php ================================================ */ final class DefaultCallAdapterFactory implements CallAdapterFactory { /** * Returns true if the factory supports this type * * @param TypeToken $type * @return bool */ public function supports(TypeToken $type): bool { return $type->isA(Call::class); } /** * Create a new factory from type * * @param TypeToken $type * @return CallAdapter */ public function create(TypeToken $type): CallAdapter { return new DefaultCallAdapter(); } } ================================================ FILE: src/Internal/Converter/ConverterProvider.php ================================================ */ final class ConverterProvider { /** * A cache of [@see ResponseBodyConverter]'s * * @var ResponseBodyConverter[] */ private $responseBodyConverters = []; /** * A cache of [@see RequestBodyConverter]'s * * @var RequestBodyConverter[] */ private $requestBodyConverters = []; /** * A cache of [@see StringConverter]'s * * @var StringConverter[] */ private $stringConverters = []; /** * An array of [@see ConverterFactory]'s * * @var ConverterFactory[] */ private $converterFactories; /** * Constructor * * @param ConverterFactory[] $factories */ public function __construct(array $factories) { $this->converterFactories = array_values($factories); } /** * Get a response body converter for type * * @param TypeToken $type * @return ResponseBodyConverter * @throws LogicException */ public function getResponseBodyConverter(TypeToken $type): ResponseBodyConverter { $key = (string)$type; if (isset($this->responseBodyConverters[$key])) { return $this->responseBodyConverters[$key]; } foreach ($this->converterFactories as $converterFactory) { $converter = $converterFactory->responseBodyConverter($type); if ($converter === null) { continue; } $this->responseBodyConverters[$key] = $converter; return $converter; } throw new LogicException(sprintf( 'Retrofit: Could not get response body converter for type %s', $type )); } /** * Get a request body converter for type * * @param TypeToken $type * @return RequestBodyConverter * @throws \LogicException */ public function getRequestBodyConverter(TypeToken $type): RequestBodyConverter { $key = (string)$type; if (isset($this->requestBodyConverters[$key])) { return $this->requestBodyConverters[$key]; } foreach ($this->converterFactories as $converterFactory) { $converter = $converterFactory->requestBodyConverter($type); if ($converter === null) { continue; } $this->requestBodyConverters[$key] = $converter; return $converter; } throw new LogicException(sprintf( 'Retrofit: Could not get request body converter for type %s', $type )); } /** * Get a string converter for type * * @param TypeToken $type * @return StringConverter * @throws \LogicException */ public function getStringConverter(TypeToken $type): StringConverter { $key = (string)$type; if (isset($this->stringConverters[$key])) { return $this->stringConverters[$key]; } foreach ($this->converterFactories as $converterFactory) { $converter = $converterFactory->stringConverter($type); if ($converter === null) { continue; } $this->stringConverters[$key] = $converter; return $converter; } throw new LogicException(sprintf( 'Retrofit: Could not get string converter for type %s', $type )); } } ================================================ FILE: src/Internal/Converter/DefaultConverterFactory.php ================================================ */ final class DefaultConverterFactory implements ConverterFactory { /** * Return default converter if type is a stream * * @param TypeToken $type * @return null|ResponseBodyConverter */ public function responseBodyConverter(TypeToken $type): ?ResponseBodyConverter { if (!$type->isA(StreamInterface::class)) { return null; } return new DefaultResponseBodyConverter(); } /** * Return default converter if type is a stream * * @param TypeToken $type * @return null|RequestBodyConverter */ public function requestBodyConverter(TypeToken $type): ?RequestBodyConverter { if (!$type->isA(StreamInterface::class)) { return null; } return new DefaultRequestBodyConverter(); } /** * Return default string converter for any type * * If the type is a string already, use a converter that doesn't do * any type checking. * * @param TypeToken $type * @return null|StringConverter */ public function stringConverter(TypeToken $type): ?StringConverter { if ($type->isString()) { return new NoopStringConverter(); } return new DefaultStringConverter(); } } ================================================ FILE: src/Internal/Converter/DefaultRequestBodyConverter.php ================================================ */ final class DefaultRequestBodyConverter implements RequestBodyConverter { /** * The value here should already be a stream, so we can return it * * @param mixed $value * @return StreamInterface */ public function convert($value): StreamInterface { return $value; } } ================================================ FILE: src/Internal/Converter/DefaultResponseBodyConverter.php ================================================ */ final class DefaultResponseBodyConverter implements ResponseBodyConverter { /** * By default, returns the stream * * @param StreamInterface $value * @return StreamInterface */ public function convert(StreamInterface $value): StreamInterface { return $value; } } ================================================ FILE: src/Internal/Converter/DefaultStringConverter.php ================================================ */ final class DefaultStringConverter implements StringConverter { /** * Convert any supported value to a string * * @param mixed $value * @return string */ public function convert($value): string { // if it's an array or object, just serialize it if (\is_array($value) || \is_object($value)) { return serialize($value); } if ($value === true) { return 'true'; } if ($value === false) { return 'false'; } return (string)$value; } } ================================================ FILE: src/Internal/Converter/NoopStringConverter.php ================================================ */ class NoopStringConverter implements StringConverter { /** * Only types that are known to be strings should be passed to this converter, * so we can just return the value. * * @param string $value * @return string */ public function convert($value): string { return $value; } } ================================================ FILE: src/Internal/DefaultProxyFactory.php ================================================ */ final class DefaultProxyFactory implements ProxyFactory { public const PROXY_PREFIX = 'Tebru\Retrofit\Proxy\\'; /** * @var BuilderFactory */ private $builderFactory; /** * @var PrettyPrinterAbstract */ private $printer; /** * @var ServiceMethodFactory */ private $serviceMethodFactory; /** * @var HttpClient */ private $httpClient; /** * @var Filesystem */ private $filesystem; /** * @var bool */ private $enableCache; /** * @var string */ private $cacheDir; /** * Constructor * * @param BuilderFactory $builderFactory * @param PrettyPrinterAbstract $printer * @param ServiceMethodFactory $serviceMethodFactory * @param HttpClient $httpClient * @param Filesystem $filesystem * @param bool $enableCache * @param string $cacheDir */ public function __construct( BuilderFactory $builderFactory, PrettyPrinterAbstract $printer, ServiceMethodFactory $serviceMethodFactory, HttpClient $httpClient, Filesystem $filesystem, bool $enableCache, string $cacheDir ) { $this->builderFactory = $builderFactory; $this->printer = $printer; $this->serviceMethodFactory = $serviceMethodFactory; $this->httpClient = $httpClient; $this->filesystem = $filesystem; $this->enableCache = $enableCache; $this->cacheDir = $cacheDir; } /** * Create a new proxy class given an interface name. This returns a class * in a string to be cached. * * @param string $service * @return Proxy * @throws \RuntimeException * @throws \LogicException * @throws \Tebru\PhpType\Exception\MalformedTypeException * @throws \InvalidArgumentException */ public function create(string $service): ?Proxy { $className = self::PROXY_PREFIX.$service; if ($this->enableCache && class_exists($className)) { return new $className($this->serviceMethodFactory, $this->httpClient); } if (!$this->enableCache && class_exists($className, false)) { return new $className($this->serviceMethodFactory, $this->httpClient); } if (!interface_exists($service)) { throw new InvalidArgumentException(sprintf('Retrofit: %s is expected to be an interface', $service)); } /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ $reflectionClass = new ReflectionClass($service); $builder = $this->builderFactory ->class($reflectionClass->getShortName()) ->extend('\\'.AbstractProxy::class) ->implement('\\'.$reflectionClass->name); foreach ($reflectionClass->getMethods() as $reflectionMethod) { $methodBuilder = $this->builderFactory ->method($reflectionMethod->name) ->makePublic(); if ($reflectionMethod->isStatic()) { $methodBuilder->makeStatic(); } $defaultValues = []; foreach ($reflectionMethod->getParameters() as $reflectionParameter) { $paramBuilder = $this->builderFactory->param($reflectionParameter->name); if ($reflectionParameter->isDefaultValueAvailable()) { $paramBuilder->setDefault($reflectionParameter->getDefaultValue()); } if ($reflectionParameter->getType() === null) { throw new LogicException(sprintf( 'Retrofit: Parameter types are required. None found for parameter %s in %s::%s()', $reflectionParameter->name, $reflectionClass->name, $reflectionMethod->name )); } $reflectionTypeName = $reflectionParameter->getType()->getName(); if ((new TypeToken($reflectionTypeName))->isObject()) { $reflectionTypeName = '\\'.$reflectionTypeName; } $type = $reflectionParameter->getType()->allowsNull() ? new NullableType($reflectionTypeName): $reflectionTypeName; $paramBuilder->setTypeHint($type); if ($reflectionParameter->isPassedByReference()) { $paramBuilder->makeByRef(); } if ($reflectionParameter->isVariadic()) { $paramBuilder->makeVariadic(); } $methodBuilder->addParam($paramBuilder->getNode()); // set all default values // if a method is called with two few arguments, a native exception will be thrown // so we can safely use null as a placeholder here. $defaultValues[] = $reflectionParameter->isDefaultValueAvailable() ? $reflectionParameter->getDefaultValue() : null; } if (!$reflectionMethod->hasReturnType()) { throw new LogicException(sprintf( 'Retrofit: Method return types are required. None found for %s::%s()', $reflectionClass->name, $reflectionMethod->name )); } /** @noinspection NullPointerExceptionInspection */ $methodBuilder->setReturnType('\\'.$reflectionMethod->getReturnType()->getName()); $defaultNodes = $this->mapArray($defaultValues); $methodBuilder->addStmt( new Return_( new MethodCall( new Variable('this'), '__handleRetrofitRequest', [ new String_($reflectionClass->name), new ConstFetch(new Name('__FUNCTION__')), new FuncCall(new Name('func_get_args')), new Array_($defaultNodes) ] ) ) ); $builder->addStmt($methodBuilder->getNode()); } $namespaceBuilder = $this->builderFactory ->namespace(self::PROXY_PREFIX.$reflectionClass->getNamespaceName()) ->addStmt($builder); $source = $this->printer->prettyPrint([$namespaceBuilder->getNode()]); eval($source); if (!$this->enableCache) { return new $className($this->serviceMethodFactory, $this->httpClient); } $directory = $this->cacheDir.DIRECTORY_SEPARATOR.$reflectionClass->getNamespaceName(); $directory = str_replace('\\', DIRECTORY_SEPARATOR, $directory); $filename = $directory.DIRECTORY_SEPARATOR.$reflectionClass->getShortName().'.php'; $class = 'filesystem->makeDirectory($directory)) { throw new RuntimeException( sprintf( 'Retrofit: There was an issue creating the cache directory: %s', $directory ) ); } if (!$this->filesystem->put($filename, $class)) { throw new RuntimeException(sprintf('Retrofit: There was an issue writing proxy class to: %s', $filename)); } return new $className($this->serviceMethodFactory, $this->httpClient); } /** * Convert array to an array of [@see Expr] to add to builder * * @param array $array * @return Expr[] */ private function mapArray(array $array): array { // for each element in the array, create an Expr object $values = array_values(array_map(function ($value) { $type = TypeToken::createFromVariable($value); switch ($type) { case TypeToken::STRING: return new String_($value); case TypeToken::INTEGER: return new LNumber($value); case TypeToken::FLOAT: return new DNumber($value); case TypeToken::BOOLEAN: return $value === true ? new ConstFetch(new Name('true')) : new ConstFetch(new Name('false')); case TypeToken::HASH: // recurse if array contains an array return new Array_($this->mapArray($value)); case TypeToken::NULL: return new ConstFetch(new Name('null')); } }, $array)); $keys = \array_keys($array); $isNumericKeys = \count(\array_filter($keys, '\is_string')) === 0; // a 0-indexed array can be returned as-is if ($isNumericKeys) { return $values; } // if we're dealing with an associative array, run the keys through the mapper $keys = $this->mapArray($keys); // create an array of ArrayItem objects for an associative array $items = []; foreach ($values as $index => $value) { $items[] = new Expr\ArrayItem($value, $keys[$index]); } return $items; } } ================================================ FILE: src/Internal/Filesystem.php ================================================ */ class Filesystem { /** * Wraps the php mkdir() function, but defaults to recursive directory creation * * @param string $pathname * @param int $mode * @param bool $recursive * @return bool */ public function makeDirectory(string $pathname, int $mode = 0777, bool $recursive = true): bool { return !(!@mkdir($pathname, $mode, $recursive) && !is_dir($pathname)); } /** * Write contents to file * * @param string $filename * @param string $contents * @return bool */ public function put(string $filename, string $contents): bool { $written = file_put_contents($filename, $contents); return !($written === 0); } } ================================================ FILE: src/Internal/HttpClientCall.php ================================================ */ final class HttpClientCall implements Call { /** * A retrofit http client implementation * * @var HttpClient */ private $client; /** * A web service resource as a method model * * @var ServiceMethod */ private $serviceMethod; /** * The runtime arguments that a request should be constructed with * * @var array */ private $args; /** * The constructed request * * @var RequestInterface */ private $request; /** * Constructor * * @param HttpClient $client * @param ServiceMethod $serviceMethod * @param array $args */ public function __construct(HttpClient $client, ServiceMethod $serviceMethod, array $args) { $this->client = $client; $this->serviceMethod = $serviceMethod; $this->args = $args; } /** * Execute request synchronously * * A [@see Response] will be returned * * @return Response */ public function execute(): Response { $response = $this->client->send($this->request()); return $this->createResponse($response); } /** * Execute request asynchronously * * This method accepts two optional callbacks. * * onResponse() will be called for any request that gets a response, * whether it was successful or not. It will send a [@see Response] as * the parameter. * * onFailure() will be called in the event a network request failed. It * will send the [@see Throwable] that was encountered. * * Example of method signatures: * * $call->enqueue( * function (\Tebru\Retrofit\Call $call, \Tebru\Retrofit\Response $response) {}, * function (\Throwable $throwable) {} * ); * * @param callable $onResponse On any response * @param callable $onFailure On any network request failure * @return Call * @throws \LogicException */ public function enqueue(?callable $onResponse = null, ?callable $onFailure = null): Call { $this->client->sendAsync( $this->request(), function (ResponseInterface $response) use ($onResponse) { if ($onResponse !== null) { $onResponse($this->createResponse($response)); } }, function (Throwable $throwable) use ($onFailure) { if ($onFailure === null) { throw $throwable; } $onFailure($throwable); } ); return $this; } /** * When making requests asynchronously, call wait() to execute the requests * * @return void */ public function wait(): void { $this->client->wait(); } /** * Get the PSR-7 request * * @return RequestInterface */ public function request(): RequestInterface { if ($this->request === null) { $this->request = $this->serviceMethod->toRequest($this->args); } return $this->request; } /** * Create a [@see Response] from a PSR-7 response * * @param ResponseInterface $response * @return RetrofitResponse */ private function createResponse(ResponseInterface $response): RetrofitResponse { $code = $response->getStatusCode(); if ($code >= 200 && $code < 300) { try { $responseBody = $this->serviceMethod->toResponseBody($response); } catch (Throwable $throwable) { throw new ResponseHandlingFailedException( $this->request(), $response, 'Retrofit: Could not convert response body', $throwable ); } return new RetrofitResponse($response, $responseBody, null); } try { $errorBody = $this->serviceMethod->toErrorBody($response); } catch (Throwable $throwable) { throw new ResponseHandlingFailedException( $this->request(), $response, 'Retrofit: Could not convert error body', $throwable ); } return new RetrofitResponse($response, null, $errorBody); } } ================================================ FILE: src/Internal/ParameterHandler/AbstractParameterHandler.php ================================================ */ abstract class AbstractParameterHandler implements ParameterHandler { private const HEADER_CON_TRANS_ENC = 'Content-Transfer-Encoding'; /** * Convert a value to a generator * * This method is used when a value can optionally be an array and each element in the * array should be processed the same way. * * @param array|mixed $list * @return Generator * @throws \RuntimeException */ protected function getListValues($list): Generator { foreach ((array)$list as $key => $element) { if (!\is_int($key)) { throw new RuntimeException('Retrofit: Array value must use numeric keys'); } yield $element; } } /** * Handle Part or PartMap annotations * * This could use a simple method using name and value, or if a [@see MultipartBody] is passed in as the * value, then a filename and additional headers could be set as well. * * @param RequestBuilder $requestBuilder * @param RequestBodyConverter $converter * @param string $name * @param mixed $value * @param string $encoding * @return void */ protected function handlePart( RequestBuilder $requestBuilder, RequestBodyConverter $converter, string $name, $value, string $encoding ): void { if ($value === null) { return; } // if not a MultipartBody, only set name, contents, and content header if (!$value instanceof MultipartBody) { $requestBuilder->addPart($name, $converter->convert($value), [self::HEADER_CON_TRANS_ENC => $encoding]); return; } $headers = $value->getHeaders(); if (!isset($headers[self::HEADER_CON_TRANS_ENC])) { $headers[self::HEADER_CON_TRANS_ENC] = $encoding; } $requestBuilder->addPart($value->getName(), $value->getContents(), $headers, $value->getFilename()); } } ================================================ FILE: src/Internal/ParameterHandler/BodyParamHandler.php ================================================ */ final class BodyParamHandler extends AbstractParameterHandler { /** * @var RequestBodyConverter */ private $converter; /** * Constructor * * @param RequestBodyConverter $converter */ public function __construct(RequestBodyConverter $converter) { $this->converter = $converter; } /** * Converts the value to a stream, then sets the body to the request builder * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { return; } $requestBuilder->setBody($this->converter->convert($value)); } } ================================================ FILE: src/Internal/ParameterHandler/FieldMapParamHandler.php ================================================ */ final class FieldMapParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * @var bool */ private $encoded; /** * Constructor * * @param StringConverter $converter * @param bool $encoded */ public function __construct(StringConverter $converter, bool $encoded) { $this->converter = $converter; $this->encoded = $encoded; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param array|Iterator $map * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $map): void { if ($map === null) { return; } foreach ($map as $name => $value) { foreach ($this->getListValues($value) as $element) { $requestBuilder->addField($name, $this->converter->convert($element), $this->encoded); } } } } ================================================ FILE: src/Internal/ParameterHandler/FieldParamHandler.php ================================================ */ final class FieldParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * @var string */ private $name; /** * @var bool */ private $encoded; /** * Constructor * * @param string $name * @param StringConverter $converter * @param bool $encoded */ public function __construct(StringConverter $converter, string $name, bool $encoded) { $this->converter = $converter; $this->name = $name; $this->encoded = $encoded; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { return; } foreach ($this->getListValues($value) as $element) { $requestBuilder->addField($this->name, $this->converter->convert($element), $this->encoded); } } } ================================================ FILE: src/Internal/ParameterHandler/HeaderMapParamHandler.php ================================================ */ final class HeaderMapParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * Constructor * * @param StringConverter $converter */ public function __construct(StringConverter $converter) { $this->converter = $converter; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param array|Iterator $map * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $map): void { if ($map === null) { return; } foreach ($map as $name => $value) { foreach ($this->getListValues($value) as $element) { $requestBuilder->addHeader($name, $this->converter->convert($element)); } } } } ================================================ FILE: src/Internal/ParameterHandler/HeaderParamHandler.php ================================================ */ final class HeaderParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * @var string */ private $name; /** * Constructor * * @param StringConverter $converter * @param string $name */ public function __construct(StringConverter $converter, string $name) { $this->converter = $converter; $this->name = $name; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { return; } foreach ($this->getListValues($value) as $element) { $requestBuilder->addHeader($this->name, $this->converter->convert($element)); } } } ================================================ FILE: src/Internal/ParameterHandler/PartMapParamHandler.php ================================================ */ final class PartMapParamHandler extends AbstractParameterHandler { /** * @var RequestBodyConverter */ private $converter; /** * @var string */ private $encoding; /** * Constructor * * @param RequestBodyConverter $converter * @param string $encoding */ public function __construct(RequestBodyConverter $converter, string $encoding) { $this->converter = $converter; $this->encoding = $encoding; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param array|Iterator $map * @return void */ public function apply(RequestBuilder $requestBuilder, $map): void { if ($map === null) { return; } foreach ($map as $name => $value) { $this->handlePart($requestBuilder, $this->converter, $name, $value, $this->encoding); } } } ================================================ FILE: src/Internal/ParameterHandler/PartParamHandler.php ================================================ */ final class PartParamHandler extends AbstractParameterHandler { /** * @var RequestBodyConverter */ private $converter; /** * @var string */ private $name; /** * @var string */ private $encoding; /** * Constructor * * @param RequestBodyConverter $converter * @param string $name * @param string $encoding */ public function __construct(RequestBodyConverter $converter, string $name, string $encoding) { $this->converter = $converter; $this->name = $name; $this->encoding = $encoding; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { return; } $this->handlePart($requestBuilder, $this->converter, $this->name, $value, $this->encoding); } } ================================================ FILE: src/Internal/ParameterHandler/PathParamHandler.php ================================================ */ final class PathParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * @var string */ private $name; /** * Constructor * * @param StringConverter $converter * @param string $name */ public function __construct(StringConverter $converter, string $name) { $this->converter = $converter; $this->name = $name; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { throw new RuntimeException('Path parameters cannot be null'); } $requestBuilder->replacePath($this->name, $this->converter->convert($value)); } } ================================================ FILE: src/Internal/ParameterHandler/QueryMapParamHandler.php ================================================ */ final class QueryMapParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * @var bool */ private $encoded; /** * Constructor * * @param StringConverter $converter * @param bool $encoded */ public function __construct(StringConverter $converter, bool $encoded) { $this->converter = $converter; $this->encoded = $encoded; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param array|Iterator $map * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $map): void { if ($map === null) { return; } foreach ($map as $name => $value) { foreach ($this->getListValues($value) as $element) { $requestBuilder->addQuery($name, $this->converter->convert($element), $this->encoded); } } } } ================================================ FILE: src/Internal/ParameterHandler/QueryNameParamHandler.php ================================================ */ final class QueryNameParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * @var bool */ private $encoded; /** * Constructor * * @param StringConverter $converter * @param bool $encoded */ public function __construct(StringConverter $converter, bool $encoded) { $this->converter = $converter; $this->encoded = $encoded; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { return; } foreach ($this->getListValues($value) as $element) { $requestBuilder->addQueryName($this->converter->convert($element), $this->encoded); } } } ================================================ FILE: src/Internal/ParameterHandler/QueryParamHandler.php ================================================ */ final class QueryParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * @var string */ private $name; /** * @var bool */ private $encoded; /** * Constructor * * @param string $name * @param StringConverter $converter * @param bool $encoded */ public function __construct(StringConverter $converter, string $name, bool $encoded) { $this->converter = $converter; $this->name = $name; $this->encoded = $encoded; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { return; } foreach ($this->getListValues($value) as $element) { $requestBuilder->addQuery($this->name, $this->converter->convert($element), $this->encoded); } } } ================================================ FILE: src/Internal/ParameterHandler/UrlParamHandler.php ================================================ */ final class UrlParamHandler extends AbstractParameterHandler { /** * @var StringConverter */ private $converter; /** * Constructor * * @param StringConverter $converter */ public function __construct(StringConverter $converter) { $this->converter = $converter; } /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void * @throws \RuntimeException */ public function apply(RequestBuilder $requestBuilder, $value): void { if ($value === null) { throw new RuntimeException('Url parameters cannot be null'); } $requestBuilder->setBaseUrl($this->converter->convert($value)); } } ================================================ FILE: src/Internal/RequestBuilder.php ================================================ */ final class RequestBuilder { /** * The request method * * @var string */ private $method; /** * The request [@see Uri] object * * @var Uri */ private $uri; /** * An array of query strings that can be appended together * * @var array */ private $queries = []; /** * An array of headers in PSR-7 format * * @var array */ private $headers; /** * The request body * * @var StreamInterface */ private $body; /** * An array of request body fields that can be appended together * * @var array */ private $fields = []; /** * An array of arrays of multipart parts, each with name, content, headers, and filename * * @var array[] */ private $parts = []; /** * Constructor * * @param string $method * @param string $baseUrl * @param string $uri * @param array $headers */ public function __construct(string $method, string $baseUrl, string $uri, array $headers) { $this->method = $method; $this->uri = new Uri($baseUrl.$uri); $this->headers = $headers; } /** * Set the uri base url * * @param string $value */ public function setBaseUrl(string $value): void { $uri = new Uri($value); $this->uri = $this->uri ->withScheme($uri->getScheme()) ->withHost($uri->getHost()) ->withPort($uri->getPort()); } /** * Replace a url path placeholder with a value * * @param string $name * @param string $value */ public function replacePath(string $name, string $value): void { $path = rawurldecode($this->uri->getPath()); $path = str_replace(sprintf('{%s}', $name), $value, $path); $this->uri = $this->uri->withPath($path); } /** * Add a query string; if encoded, decodes to be encoded later * * @param string $name * @param string $value * @param bool $encoded */ public function addQuery(string $name, string $value, bool $encoded): void { $name = rawurlencode($name); if ($encoded === false) { $value = rawurlencode($value); } $this->queries[] = $name.'='.$value; } /** * Adds a query string without value; if encoded, decodes to be encoded later * * @param string $value * @param bool $encoded */ public function addQueryName(string $value, bool $encoded): void { if ($encoded === false) { $value = rawurlencode($value); } $this->queries[] = $value; } /** * Add a header in PSR-7 format * * @param string $name * @param string $value */ public function addHeader(string $name, string $value): void { $name = strtolower($name); $this->headers[$name][] = $value; } /** * Set the request body * * @param StreamInterface $body */ public function setBody(StreamInterface $body): void { $this->body = $body; } /** * Add a field; if not encoded, encodes first * * @param string $name * @param string $value * @param bool $encoded */ public function addField(string $name, string $value, bool $encoded): void { $name = rawurlencode($name); if ($encoded === false) { $value = rawurlencode($value); } $this->fields[] = $name.'='.$value; } /** * Add a multipart part * * @param string $name * @param StreamInterface $contents * @param array $headers * @param null|string $filename */ public function addPart(string $name, StreamInterface $contents, array $headers = [], ?string $filename = null): void { $this->parts[] = [ 'name' => $name, 'contents' => $contents, 'headers' => $headers, 'filename' => $filename, ]; } /** * Create a PSR-7 request * * @return Request * @throws \LogicException */ public function build(): Request { $uri = $this->uri; if ($this->queries !== []) { $query = implode('&', $this->queries); $uri = $this->uri->getQuery() === '' ? $this->uri->withQuery($query) : $this->uri->withQuery($query.'&'.$this->uri->getQuery()); } if ($this->fields !== []) { if ($this->body !== null) { throw new LogicException('Retrofit: Cannot mix @Field and @Body annotations.'); } $this->body = Psr7\stream_for(implode('&', $this->fields)); } if ($this->parts !== []) { if ($this->body !== null) { throw new LogicException('Retrofit: Cannot mix @Part and @Body annotations.'); } $this->body = new MultipartStream($this->parts); } return new Request($this->method, $uri, $this->headers, $this->body); } } ================================================ FILE: src/Internal/RetrofitResponse.php ================================================ */ final class RetrofitResponse implements Response { /** * The PSR-7 response * * @var ResponseInterface */ private $response; /** * Converted body on success * * @var mixed */ private $body; /** * Converted body on failure * * @var mixed */ private $errorBody; /** * Constructor * * @param ResponseInterface $response * @param mixed $body * @param mixed $errorBody */ public function __construct(ResponseInterface $response, $body, $errorBody) { $this->response = $response; $this->body = $body; $this->errorBody = $errorBody; } /** * Get the raw PSR-7 response * * @return ResponseInterface */ public function raw(): ResponseInterface { return $this->response; } /** * Get the response status code * * @return int */ public function code(): int { return $this->response->getStatusCode(); } /** * Get the response message * * @return string */ public function message(): string { return $this->response->getReasonPhrase(); } /** * Get response headers * * @return array */ public function headers(): array { return $this->response->getHeaders(); } /** * Returns true if the response was successful * * @return bool */ public function isSuccessful(): bool { return $this->response->getStatusCode() >= 200 && $this->response->getStatusCode() < 300; } /** * Get converted body * * @return mixed */ public function body() { return $this->body; } /** * Get converted body on errors * * @return mixed */ public function errorBody() { return $this->errorBody; } } ================================================ FILE: src/Internal/ServiceMethod/DefaultServiceMethod.php ================================================ */ final class DefaultServiceMethod implements ServiceMethod { /** * Request method * * @var string */ private $method; /** * Request base url * * @var string */ private $baseUrl; /** * Request path * * @var string */ private $path; /** * @var array */ private $headers; /** * Array of parameter handlers * * @var ParameterHandler[] */ private $parameterHandlers; /** * The call adapter to use * * @var CallAdapter */ private $callAdapter; /** * Response body converter * * @var ResponseBodyConverter */ private $responseBodyConverter; /** * Error body converter * * @var ResponseBodyConverter */ private $errorBodyConverter; /** * Constructor * * @param string $method * @param string $baseUrl * @param string $uri * @param array $headers * @param array $parameterHandlers * @param CallAdapter $callAdapter * @param ResponseBodyConverter $responseBodyConverter * @param ResponseBodyConverter $errorBodyConverter */ public function __construct( string $method, string $baseUrl, string $uri, array $headers, array $parameterHandlers, CallAdapter $callAdapter, ResponseBodyConverter $responseBodyConverter, ResponseBodyConverter $errorBodyConverter ) { $this->method = $method; $this->baseUrl = $baseUrl; $this->path = $uri; $this->headers = $headers; $this->parameterHandlers = $parameterHandlers; $this->callAdapter = $callAdapter; $this->responseBodyConverter = $responseBodyConverter; $this->errorBodyConverter = $errorBodyConverter; } /** * Apply runtime arguments and build request * * @param array $args * @return RequestInterface * @throws \LogicException */ public function toRequest(array $args): RequestInterface { if (\count($this->parameterHandlers) !== \count($args)) { throw new LogicException(sprintf( 'Retrofit: Incompatible number of arguments. Expected %d and got %s. This either ' . 'means that the service method was not called with the correct number of parameters, ' . 'or there is not an annotation for every parameter.', \count($this->parameterHandlers), \count($args) )); } $requestBuilder = new RequestBuilder( $this->method, $this->baseUrl, $this->path, $this->headers ); foreach ($this->parameterHandlers as $index => $parameterHandler) { $parameterHandler->apply($requestBuilder, $args[$index]); } return $requestBuilder->build(); } /** * Take a response and convert it to expected value * * @param ResponseInterface $response * @return mixed */ public function toResponseBody(ResponseInterface $response) { return $this->responseBodyConverter->convert($response->getBody()); } /** * Take a response and convert it to expected value * * @param ResponseInterface $response * @return mixed */ public function toErrorBody(ResponseInterface $response) { return $this->errorBodyConverter->convert($response->getBody()); } /** * Take a [@see Call] and adapt to expected value * * @param Call $call * @return mixed */ public function adapt(Call $call) { return $this->callAdapter->adapt($call); } } ================================================ FILE: src/Internal/ServiceMethod/DefaultServiceMethodBuilder.php ================================================ */ final class DefaultServiceMethodBuilder implements ServiceMethodBuilder { /** * The request method * * @var string */ private $method; /** * The request base url * * @var string */ private $baseUrl; /** * The request path * * @var string */ private $path; /** * True if the request has a body * * @var bool */ private $hasBody; /** * The request body content type * * @var string */ private $contentType; /** * Array of headers * * @var array */ private $headers = []; /** * Array of Parameter handlers, indexed to match the position of the parameters * * @var ParameterHandler[] */ private $parameterHandlers = []; /** * Converts successful response bodies to expected value * * @var ResponseBodyConverter */ private $responseBodyConverter; /** * Converts error response bodies to expected value * * @var ResponseBodyConverter */ private $errorBodyConverter; /** * Adapts a [@see Call] to expected value * * @var CallAdapter */ private $callAdapter; /** * Set the request method (e.g. GET, POST) * * @param string $method * @return ServiceMethodBuilder * @throws \LogicException */ public function setMethod(string $method): ServiceMethodBuilder { if ($this->method !== null) { throw new LogicException(sprintf( 'Retrofit: Only one http method is allowed. Trying to set %s, but %s already exists', strtoupper($method), $this->method )); } $this->method = strtoupper($method); return $this; } /** * Set the request base url (e.g. http://example.com) * * @param string $baseUrl * @return ServiceMethodBuilder */ public function setBaseUrl(string $baseUrl): ServiceMethodBuilder { $this->baseUrl = $baseUrl; return $this; } /** * Set the request path * * @param string $path * @return ServiceMethodBuilder */ public function setPath(string $path): ServiceMethodBuilder { $this->path = $path; return $this; } /** * Set to true if an annotation exists that denotes a request body. This should also set * the request content type. * * @param bool $hasBody * @return ServiceMethodBuilder * @throws \LogicException */ public function setHasBody(bool $hasBody): ServiceMethodBuilder { if ($this->hasBody !== null && $this->hasBody !== $hasBody) { throw new LogicException( 'Retrofit: Body cannot be changed after it has been set. This indicates a conflict between ' . 'HTTP Request annotations, body annotations, and request type annotations. For example, ' . '@GET cannot be used with @Body, @Field, or @Part annotations' ); } $this->hasBody = $hasBody; return $this; } /** * Set the content type of the request. A content type should not be set if there * isn't a request body. * * @param string $contentType * @return ServiceMethodBuilder * @throws \LogicException */ public function setContentType(string $contentType): ServiceMethodBuilder { if ($this->contentType !== null && $this->contentType !== $contentType) { throw new LogicException( 'Retrofit: Content type cannot be changed after it has been set. This indicates a conflict between ' . 'HTTP Request annotations, body annotations, and request type annotations. For example, ' . '@GET cannot be used with @Body, @Field, or @Part annotations' ); } $this->contentType = $contentType; return $this; } /** * Convenience method to declare that the request has content and is json * * @return ServiceMethodBuilder * @throws \LogicException */ public function setIsJson(): ServiceMethodBuilder { $this->setHasBody(true); $this->setContentType('application/json'); return $this; } /** * Convenience method to declare that the request has content and is form encoded * * @return ServiceMethodBuilder * @throws \LogicException */ public function setIsFormUrlEncoded(): ServiceMethodBuilder { $this->setHasBody(true); $this->setContentType('application/x-www-form-urlencoded'); return $this; } /** * Convenience method to declare that the request has content and is multipart * * @return ServiceMethodBuilder * @throws \LogicException */ public function setIsMultipart(): ServiceMethodBuilder { $this->setHasBody(true); $this->setContentType('multipart/form-data'); return $this; } /** * Add a request header. Header name should be normalized. * * @param string $name * @param string $header * @return ServiceMethodBuilder */ public function addHeader(string $name, string $header): ServiceMethodBuilder { $this->headers[strtolower($name)][] = $header; return $this; } /** * Add a [@see ParameterHandler] at the position the parameter exists * * @param int $index * @param ParameterHandler $parameterHandler * @return ServiceMethodBuilder */ public function addParameterHandler(int $index, ParameterHandler $parameterHandler): ServiceMethodBuilder { $this->parameterHandlers[$index] = $parameterHandler; return $this; } /** * Set the [@see CallAdapter] * * @param CallAdapter $callAdapter * @return ServiceMethodBuilder */ public function setCallAdapter(CallAdapter $callAdapter): ServiceMethodBuilder { $this->callAdapter = $callAdapter; return $this; } /** * Set the response body converter to convert successful responses * * @param ResponseBodyConverter $responseBodyConverter * @return ServiceMethodBuilder */ public function setResponseBodyConverter(ResponseBodyConverter $responseBodyConverter): ServiceMethodBuilder { $this->responseBodyConverter = $responseBodyConverter; return $this; } /** * Set the response body converter to convert error responses * * @param ResponseBodyConverter $errorBodyConverter * @return ServiceMethodBuilder */ public function setErrorBodyConverter(ResponseBodyConverter $errorBodyConverter): ServiceMethodBuilder { $this->errorBodyConverter = $errorBodyConverter; return $this; } /** * Create a new [@see DefaultServiceMethod] from previously set parameters * * @return DefaultServiceMethod * @throws \LogicException */ public function build(): DefaultServiceMethod { if ($this->method === null) { throw new LogicException( 'Retrofit: Cannot build service method without HTTP method. Please specify @GET, @POST, etc' ); } if ($this->baseUrl === null) { throw new LogicException( 'Retrofit: Cannot build service method without base url. Please specify on RetrofitBuilder' ); } if ($this->path === null) { throw new LogicException( 'Retrofit: Cannot build service method without HTTP method. Please specify @GET, @POST, etc' ); } if ($this->hasBody === true && $this->contentType === null) { throw new LogicException( 'Retrofit: Cannot build service method with body and no content type. Set one using @Body, ' . '@Field, or @Part' ); } if ($this->hasBody !== true && $this->contentType !== null) { throw new LogicException( 'Retrofit: Cannot set a content-type without a body. This indicates a conflict between ' . 'HTTP Request annotations, body annotations, and request type annotations. For example, ' . '@GET cannot be used with @Body, @Field, or @Part annotations' ); } if ($this->responseBodyConverter === null) { throw new LogicException( 'Retrofit: Cannot build service method without response body converter' ); } if ($this->errorBodyConverter === null) { throw new LogicException( 'Retrofit: Cannot build service method without error body converter' ); } if ($this->callAdapter === null) { throw new LogicException( 'Retrofit: Cannot build service method without call adapter' ); } if ($this->contentType !== null && !isset($this->headers['content-type'])) { $this->addHeader('content-type', $this->contentType); } if ($this->hasBody === null) { $this->hasBody = false; } ksort($this->parameterHandlers); return new DefaultServiceMethod( $this->method, $this->baseUrl, $this->path, $this->headers, $this->parameterHandlers, $this->callAdapter, $this->responseBodyConverter, $this->errorBodyConverter ); } } ================================================ FILE: src/Internal/ServiceMethod/ServiceMethodFactory.php ================================================ */ final class ServiceMethodFactory { /** * Handles an [@see AbstractAnnotation] * * @var AnnotationProcessor */ private $annotationProcessor; /** * Fetches a [@see CallAdapter] * * @var CallAdapterProvider */ private $callAdapterProvider; /** * Fetches a [@see Converter] * * @var ConverterProvider */ private $converterProvider; /** * Reads annotations from service interface * * @var AnnotationReaderAdapter */ private $annotationReader; /** * The request base url * * @var string */ private $baseUrl; /** * Constructor * * @param AnnotationProcessor $annotationProcessor * @param CallAdapterProvider $callAdapterProvider * @param ConverterProvider $converterProvider * @param AnnotationReaderAdapter $annotationReader * @param string $baseUrl */ public function __construct( AnnotationProcessor $annotationProcessor, CallAdapterProvider $callAdapterProvider, ConverterProvider $converterProvider, AnnotationReaderAdapter $annotationReader, string $baseUrl ) { $this->annotationProcessor = $annotationProcessor; $this->callAdapterProvider = $callAdapterProvider; $this->converterProvider = $converterProvider; $this->annotationReader = $annotationReader; $this->baseUrl = $baseUrl; } /** * Creates a [@see DefaultServiceMethod] * * @param string $interfaceName * @param string $methodName * @return DefaultServiceMethod * @throws \LogicException */ public function create(string $interfaceName, string $methodName): DefaultServiceMethod { $serviceMethodBuilder = new DefaultServiceMethodBuilder(); $annotations = $this->annotationReader->readMethod($methodName, $interfaceName, true, true); $reflectionMethod = new ReflectionMethod($interfaceName, $methodName); $returnType = $reflectionMethod->getReturnType(); if ($returnType === null) { throw new LogicException(sprintf( 'Retrofit: All service methods must contain a return type. None found for %s::%s()', $reflectionMethod->getDeclaringClass()->name, $reflectionMethod->name )); } /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ $returnTypeToken = new TypeToken($returnType->getName()); $serviceMethodBuilder->setBaseUrl($this->baseUrl); $serviceMethodBuilder->setCallAdapter($this->callAdapterProvider->get($returnTypeToken)); foreach ($annotations as $annotationArray) { if (!\is_array($annotationArray)) { $annotationArray = [$annotationArray]; } foreach ($annotationArray as $annotation) { try { $this->annotationProcessor->process( $annotation, $serviceMethodBuilder, $this->converterProvider, $reflectionMethod ); } catch (LogicException $exception) { throw new LogicException( $exception->getMessage() . sprintf( ' for %s::%s()', $reflectionMethod->getDeclaringClass()->name, $reflectionMethod->name ) ); } } } $this->applyConverters($annotations, $serviceMethodBuilder); return $serviceMethodBuilder->build(); } /** * @param AnnotationCollection $annotations * @param DefaultServiceMethodBuilder $builder * @throws \LogicException */ private function applyConverters(AnnotationCollection $annotations, DefaultServiceMethodBuilder $builder): void { $responseBody = $annotations->get(Annot\ResponseBody::class); if ($responseBody !== null) { /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ $builder->setResponseBodyConverter( $this->converterProvider->getResponseBodyConverter(new TypeToken($responseBody->getValue())) ); } else { /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ $builder->setResponseBodyConverter( $this->converterProvider->getResponseBodyConverter(new TypeToken(StreamInterface::class)) ); } $errorBody = $annotations->get(Annot\ErrorBody::class); if ($errorBody !== null) { /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ $builder->setErrorBodyConverter( $this->converterProvider->getResponseBodyConverter(new TypeToken($errorBody->getValue())) ); } else { /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ $builder->setErrorBodyConverter( $this->converterProvider->getResponseBodyConverter(new TypeToken(StreamInterface::class)) ); } } } ================================================ FILE: src/Internal/ServiceMethod.php ================================================ */ interface ServiceMethod { /** * Apply runtime arguments and build request * * @param array $args * @return RequestInterface */ public function toRequest(array $args): RequestInterface; /** * Take a response and convert it to expected value * * @param ResponseInterface $response * @return mixed */ public function toResponseBody(ResponseInterface $response); /** * Take a response and convert it to expected value * * @param ResponseInterface $response * @return mixed */ public function toErrorBody(ResponseInterface $response); /** * Take a [@see Call] and adapt to expected value * * @param Call $call * @return mixed */ public function adapt(Call $call); } ================================================ FILE: src/ParameterHandler.php ================================================ */ interface ParameterHandler { /** * Set a value to the [@see RequestBuilder] for parameter type * * @param RequestBuilder $requestBuilder * @param mixed $value * @return void */ public function apply(RequestBuilder $requestBuilder, $value): void; } ================================================ FILE: src/Proxy/AbstractProxy.php ================================================ */ abstract class AbstractProxy implements Proxy { public const RETROFIT_NO_DEFAULT_VALUE = '__retrofit_no_default_value__'; /** * Creates a [@see DefaultServiceMethod] * * @var ServiceMethodFactory */ private $serviceMethodFactory; /** * A retrofit http client * * @var HttpClient */ private $client; /** * Constructor * * @param ServiceMethodFactory $serviceMethodFactory * @param HttpClient $client */ public function __construct(ServiceMethodFactory $serviceMethodFactory, HttpClient $client) { $this->serviceMethodFactory = $serviceMethodFactory; $this->client = $client; } /** @noinspection MagicMethodsValidityInspection */ /** * Constructs a [@see Call] object based on an interface method and arguments, then passes it through a * [@see CallAdapter] before returning. * * @param string $interfaceName * @param string $methodName * @param array $args * @param array $defaultArgs * @return mixed */ public function __handleRetrofitRequest(string $interfaceName, string $methodName, array $args, array $defaultArgs) { $args = $this->createArgs($args, $defaultArgs); $serviceMethod = $this->serviceMethodFactory->create($interfaceName, $methodName); return $serviceMethod->adapt(new HttpClientCall($this->client, $serviceMethod, $args)); } /** * Append any default args to argument array * * @param array $args * @param array $defaultArgs * @return array */ private function createArgs(array $args, array $defaultArgs): array { $numProvidedArgs = \count($args); $numArgs = \count($defaultArgs); if ($numArgs === $numProvidedArgs) { return $args; } // get arguments from end that were not provided $appendedArgs = \array_slice($defaultArgs, $numProvidedArgs); return \array_merge($args, $appendedArgs); } } ================================================ FILE: src/Proxy.php ================================================ */ interface Proxy { /** * Constructs a [@see Call] object based on an interface method and arguments, then passes it through a * [@see CallAdapter] before returning. * * @param string $interfaceName * @param string $methodName * @param array $args * @param array $defaultArgs * @return mixed */ public function __handleRetrofitRequest(string $interfaceName, string $methodName, array $args, array $defaultArgs); } ================================================ FILE: src/ProxyFactory.php ================================================ */ interface ProxyFactory { /** * Create a new [@see Proxy] from given service name * * Returns null if the factory cannot handle the service * * @param string $service * @return null|Proxy */ public function create(string $service): ?Proxy; } ================================================ FILE: src/RequestBodyConverter.php ================================================ */ interface RequestBodyConverter extends Converter { /** * Convert to stream * * @param mixed $value * @return StreamInterface */ public function convert($value): StreamInterface; } ================================================ FILE: src/Response.php ================================================ */ interface Response { /** * Get the raw PSR-7 response * * @return ResponseInterface */ public function raw(): ResponseInterface; /** * Get the response status code * * @return int */ public function code(): int; /** * Get the response message * * @return string */ public function message(): string; /** * Get response headers * * @return array */ public function headers(): array; /** * Returns true if the response was successful * * @return bool */ public function isSuccessful(): bool; /** * Get converted body * * @return mixed */ public function body(); /** * Get converted body on errors * * @return mixed */ public function errorBody(); } ================================================ FILE: src/ResponseBodyConverter.php ================================================ */ interface ResponseBodyConverter extends Converter { /** * Convert from stream to any type * * @param StreamInterface $value * @return mixed */ public function convert(StreamInterface $value); } ================================================ FILE: src/Retrofit.php ================================================ */ class Retrofit { /** * Registered services * * @var array $services */ private $services = []; /** * Finds all services in a given source directory * * @var ServiceResolver $serviceResolver */ private $serviceResolver; /** * An array of proxy factories * * @var ProxyFactory[] */ private $proxyFactories; /** * Constructor * * @param ServiceResolver $serviceResolver Finds service classes * @param ProxyFactory[] $proxyFactories */ public function __construct(ServiceResolver $serviceResolver, array $proxyFactories) { $this->serviceResolver = $serviceResolver; $this->proxyFactories = $proxyFactories; } /** * Create a new builder * * @return RetrofitBuilder */ public static function builder(): RetrofitBuilder { return new RetrofitBuilder(); } /** * Register an array of classes * * @param array $services * @return void */ public function registerServices(array $services): void { foreach ($services as $service) { $this->registerService($service); } } /** * Register a single class * * @param string $service * @return void */ public function registerService(string $service): void { $this->services[] = $service; } /** * Use the service resolver to find all the services dynamically * * @param string $srcDir * @return int Number of services cached * @throws \RuntimeException * @throws \BadMethodCallException */ public function createAll(string $srcDir): int { $this->services = $this->serviceResolver->findServices($srcDir); return $this->createServices(); } /** * Creates cache files based on registered services * * @return int Number of services cached * @throws \RuntimeException * @throws \BadMethodCallException */ public function createServices(): int { foreach ($this->services as $service) { $this->create($service); } return \count($this->services); } /** * Create a new service proxy given an interface name * * The returned proxy object should be used as if it's an * instance of the service provided. * * @param string $service * @return Proxy */ public function create(string $service): Proxy { foreach ($this->proxyFactories as $proxyFactory) { $object = $proxyFactory->create($service); if ($object !== null) { return $object; } } /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ throw new LogicException(sprintf('Retrofit: Could not find a proxy factory for %s', $service)); } } ================================================ FILE: src/RetrofitBuilder.php ================================================ */ class RetrofitBuilder { /** * A cache interface to be used in place of defaults * * If this is set, [@see RetrofitBuilder::$shouldCache] will be ignored for internal * caches, however, [@see RetrofitBuilder::$shouldCache] and * [@see RetrofitBuilder::$cacheDir] will still be used to cache proxy clients. * * @var CacheInterface */ private $cache; /** * Directory to store generated proxy clients * * @var string */ private $cacheDir; /** * A Retrofit http client used to make requests * * @var HttpClient */ private $httpClient; /** * The service's base url * * @var string */ private $baseUrl; /** * An array of factories used to create [@see CallAdapter]s * * @var CallAdapterFactory[] */ private $callAdapterFactories = []; /** * An array of factories used to convert types * * @var ConverterFactory[] */ private $converterFactories = []; /** * An array of factories used to create [@see Proxy] objects * * @var ProxyFactory[] */ private $proxyFactories = []; /** * An array of handlers used to modify the request based on an annotation * * @var AnnotationHandler[] */ private $annotationHandlers = []; /** * If we should cache the proxies * * @var bool */ private $shouldCache = false; /** * Override default cache adapters * * @param CacheInterface $cache * @return RetrofitBuilder */ public function setCache(CacheInterface $cache): RetrofitBuilder { $this->cache = $cache; return $this; } /** * Set the cache directory * * @param string $cacheDir * @return RetrofitBuilder */ public function setCacheDir(string $cacheDir): RetrofitBuilder { $this->cacheDir = $cacheDir; return $this; } /** * Set the Retrofit http client * * @param HttpClient $client * @return RetrofitBuilder */ public function setHttpClient(HttpClient $client): RetrofitBuilder { $this->httpClient = $client; return $this; } /** * Set the base url * * @param string $baseUrl * @return RetrofitBuilder */ public function setBaseUrl(string $baseUrl): RetrofitBuilder { $this->baseUrl = $baseUrl; return $this; } /** * Add a [@see CallAdapterFactory] * * @param CallAdapterFactory $callAdapterFactory * @return RetrofitBuilder */ public function addCallAdapterFactory(CallAdapterFactory $callAdapterFactory): RetrofitBuilder { $this->callAdapterFactories[] = $callAdapterFactory; return $this; } /** * Add a [@see ConverterFactory] * * @param ConverterFactory $converterFactory * @return RetrofitBuilder */ public function addConverterFactory(ConverterFactory $converterFactory): RetrofitBuilder { $this->converterFactories[] = $converterFactory; return $this; } /** * Add a [@see ProxyFactory] * * @param ProxyFactory $proxyFactory * @return RetrofitBuilder */ public function addProxyFactory(ProxyFactory $proxyFactory): RetrofitBuilder { $this->proxyFactories[] = $proxyFactory; return $this; } /** * Add an [@see AnnotationHandler] * * @param string $annotationName * @param AnnotationHandler $annotationHandler * @return RetrofitBuilder */ public function addAnnotationHandler(string $annotationName, AnnotationHandler $annotationHandler): RetrofitBuilder { $this->annotationHandlers[$annotationName] = $annotationHandler; return $this; } /** * Enable caching proxies * * @param bool $enable * @return RetrofitBuilder */ public function enableCache(bool $enable = true): RetrofitBuilder { $this->shouldCache = $enable; return $this; } /** * Build a retrofit instance * * @return Retrofit * @throws \LogicException */ public function build(): Retrofit { $defaultProxyFactory = $this->createDefaultProxyFactory(); foreach ($this->proxyFactories as $proxyFactory) { if ($proxyFactory instanceof DefaultProxyFactoryAware) { $proxyFactory->setDefaultProxyFactory($defaultProxyFactory); } } $this->proxyFactories[] = $defaultProxyFactory; return new Retrofit(new ServiceResolver(), $this->proxyFactories); } /** * Creates the default proxy factory and all necessary dependencies * * @return ProxyFactory * @throws \LogicException */ private function createDefaultProxyFactory(): ProxyFactory { if ($this->baseUrl === null) { throw new LogicException('Retrofit: Base URL must be provided'); } if ($this->httpClient === null) { throw new LogicException('Retrofit: Must set http client to make requests'); } if ($this->shouldCache && $this->cacheDir === null) { throw new LogicException('Retrofit: If caching is enabled, must specify cache directory'); } $this->cacheDir .= '/retrofit'; // add defaults to any user registered $this->callAdapterFactories[] = new DefaultCallAdapterFactory(); $this->converterFactories[] = new DefaultConverterFactory(); if ($this->cache === null) { $this->cache = $this->shouldCache === true ? CacheProvider::createFileCache($this->cacheDir) : CacheProvider::createMemoryCache(); } $httpRequestHandler = new AnnotHandler\HttpRequestAnnotHandler(); /** @noinspection ClassConstantUsageCorrectnessInspection */ $annotationHandlers = array_merge( [ Annot\Body::class => new AnnotHandler\BodyAnnotHandler(), Annot\DELETE::class => $httpRequestHandler, Annot\Field::class => new AnnotHandler\FieldAnnotHandler(), Annot\FieldMap::class => new AnnotHandler\FieldMapAnnotHandler(), Annot\GET::class => $httpRequestHandler, Annot\HEAD::class => $httpRequestHandler, Annot\Header::class => new AnnotHandler\HeaderAnnotHandler(), Annot\HeaderMap::class => new AnnotHandler\HeaderMapAnnotHandler(), Annot\Headers::class => new AnnotHandler\HeadersAnnotHandler(), Annot\OPTIONS::class => $httpRequestHandler, Annot\Part::class => new AnnotHandler\PartAnnotHandler(), Annot\PartMap::class => new AnnotHandler\PartMapAnnotHandler(), Annot\PATCH::class => $httpRequestHandler, Annot\Path::class => new AnnotHandler\PathAnnotHandler(), Annot\POST::class => $httpRequestHandler, Annot\PUT::class => $httpRequestHandler, Annot\Query::class => new AnnotHandler\QueryAnnotHandler(), Annot\QueryMap::class => new AnnotHandler\QueryMapAnnotHandler(), Annot\QueryName::class => new AnnotHandler\QueryNameAnnotHandler(), Annot\REQUEST::class => $httpRequestHandler, Annot\Url::class => new AnnotHandler\UrlAnnotHandler(), ], $this->annotationHandlers ); $serviceMethodFactory = new ServiceMethodFactory( new AnnotationProcessor($annotationHandlers), new CallAdapterProvider($this->callAdapterFactories), new ConverterProvider($this->converterFactories), new AnnotationReaderAdapter(new AnnotationReader(), $this->cache), $this->baseUrl ); return new DefaultProxyFactory( new BuilderFactory(), new Standard(), $serviceMethodFactory, $this->httpClient, new Filesystem(), $this->shouldCache, $this->cacheDir ); } } ================================================ FILE: src/ServiceMethodBuilder.php ================================================ */ interface ServiceMethodBuilder { /** * Set the request method (e.g. GET, POST) * * @param string $method * @return ServiceMethodBuilder */ public function setMethod(string $method): ServiceMethodBuilder; /** * Set the request base url (e.g. http://example.com) * * @param string $baseUrl * @return ServiceMethodBuilder */ public function setBaseUrl(string $baseUrl): ServiceMethodBuilder; /** * Set the request path * * @param string $path * @return ServiceMethodBuilder */ public function setPath(string $path): ServiceMethodBuilder; /** * Set to true if an annotation exists that denotes a request body. This should also set * the request content type. * * @param bool $hasBody * @return ServiceMethodBuilder */ public function setHasBody(bool $hasBody): ServiceMethodBuilder; /** * Set the content type of the request. A content type should not be set if there * isn't a request body. * * @param string $contentType * @return ServiceMethodBuilder */ public function setContentType(string $contentType): ServiceMethodBuilder; /** * Convenience method to declare that the request has content and is json * * @return ServiceMethodBuilder */ public function setIsJson(): ServiceMethodBuilder; /** * Convenience method to declare that the request has content and is form encoded * * @return ServiceMethodBuilder */ public function setIsFormUrlEncoded(): ServiceMethodBuilder; /** * Convenience method to declare that the request has content and is multipart * * @return ServiceMethodBuilder */ public function setIsMultipart(): ServiceMethodBuilder; /** * Add a request header. Header name should be normalized. * * @param string $name * @param string $header * @return ServiceMethodBuilder */ public function addHeader(string $name, string $header): ServiceMethodBuilder; /** * Add a [@see ParameterHandler] at the position the parameter exists * * @param int $index * @param ParameterHandler $parameterHandler * @return ServiceMethodBuilder */ public function addParameterHandler(int $index, ParameterHandler $parameterHandler): ServiceMethodBuilder; /** * Set the [@see CallAdapter] * * @param CallAdapter $callAdapter * @return ServiceMethodBuilder */ public function setCallAdapter(CallAdapter $callAdapter): ServiceMethodBuilder; } ================================================ FILE: src/StringConverter.php ================================================ */ interface StringConverter extends Converter { /** * Convert any supported value to a string * * @param mixed $value * @return string */ public function convert($value): string; } ================================================ FILE: tests/Mock/Unit/Internal/AnnotationProcessorTest/AnnotationProcessorTestMock.php ================================================ */ interface AnnotationProcessorTestMock { public function foo(int $bar): Call; public function body(StreamInterface $bar): Call; public function noType($bar): Call; } ================================================ FILE: tests/Mock/Unit/Internal/AnnotationProcessorTest/BadConverterAnnotation.php ================================================ * * @Annotation */ class BadConverterAnnotation extends AbstractAnnotation implements ParameterAwareAnnotation { /** * The variable name, which will either be the default value or the value of 'var' if * specified. The variable name excludes the '$'. * * @return string */ public function getVariableName(): string { return $this->getValue(); } /** * Return the converter interface class * * Can be one of RequestBodyConverter, ResponseBodyConverter, or StringConverter * * @return null|string */ public function converterType(): ?string { return 'Foo'; } } ================================================ FILE: tests/Mock/Unit/Internal/HttpClientCallTest/HttpClientCallTestClientMock.php ================================================ */ class HttpClientCallTestClientMock implements HttpClient { /** * @var ResponseInterface */ private $response; /** * @var callable */ private $onResponse; /** * @var callable */ private $onFailure; /** * Constructor * * @param ResponseInterface $response */ public function __construct(?ResponseInterface $response = null) { $this->response = $response; } /** * Send a request synchronously and return a PSR-7 [@see ResponseInterface] * * @param RequestInterface $request * @return ResponseInterface */ public function send(RequestInterface $request): ResponseInterface { return $this->response; } /** * Send a request asynchronously * * The response callback must be called if any response is returned from the request, and the failure * callback should only be executed if a request was not completed. * * The response callback should pass a PSR-7 [@see ResponseInterface] as the one and only argument. The * failure callback should pass a [@see Throwable] as the one and only argument. * * @param RequestInterface $request * @param callable $onResponse * @param callable $onFailure * @return void */ public function sendAsync(RequestInterface $request, callable $onResponse, callable $onFailure): void { $this->onResponse = $onResponse; $this->onFailure = $onFailure; } /** * Calling this method should execute any enqueued requests asynchronously * * @return void */ public function wait(): void { if ($this->response !== null) { $onResponse = $this->onResponse; $onResponse($this->response); return; } $onFailure = $this->onFailure; $onFailure(new RuntimeException()); } } ================================================ FILE: tests/Mock/Unit/Internal/HttpClientCallTest/HttpClientCallTestErrorBodyMock.php ================================================ */ class HttpClientCallTestErrorBodyMock { } ================================================ FILE: tests/Mock/Unit/Internal/HttpClientCallTest/HttpClientCallTestResponseBodyMock.php ================================================ */ class HttpClientCallTestResponseBodyMock { } ================================================ FILE: tests/Mock/Unit/Internal/HttpClientCallTest/HttpClientCallTestServiceMethodMock.php ================================================ */ class HttpClientCallTestServiceMethodMock implements ServiceMethod { /** * @var RequestInterface */ private $request; /** * @var HttpClientCallTestResponseBodyMock */ private $response; /** * @var HttpClientCallTestErrorBodyMock */ private $error; /** * Constructor * * @param RequestInterface $request * @param HttpClientCallTestResponseBodyMock $response * @param HttpClientCallTestErrorBodyMock $error */ public function __construct( RequestInterface $request, ?HttpClientCallTestResponseBodyMock $response = null, ?HttpClientCallTestErrorBodyMock $error = null ) { $this->request = $request; $this->response = $response; $this->error = $error; } /** * Apply runtime arguments and build request * * @param array $args * @return RequestInterface */ public function toRequest(array $args): RequestInterface { return $this->request; } /** * Take a response and convert it to expected value * * @param ResponseInterface $response * @return mixed */ public function toResponseBody(ResponseInterface $response) { $this->checkResponse($response); return $this->response; } /** * Take a response and convert it to expected value * * @param ResponseInterface $response * @return mixed */ public function toErrorBody(ResponseInterface $response) { $this->checkResponse($response); return $this->error; } /** * Take a [@see Call] and adapt to expected value * * @param Call $call * @return mixed */ public function adapt(Call $call) { return $call; } private function checkResponse(ResponseInterface $response): void { $body = (string)$response->getBody(); if ($body === '') { return; } json_decode($body); if (json_last_error() !== JSON_ERROR_NONE) { throw new \RuntimeException(); } } } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/PFTCTestCreate.php ================================================ */ interface PFTCTestCreate { /** * @POST("/foo/path") * @Path("path") * @Body("body") * @Query("query") * @Field("field", var="fields") * * @param string $path * @param stdClass $body * @param string $query * @param string[] $fields * @return Call */ public function simple(string $path, stdClass $body, string &$query = 'foo', string ...$fields): Call; /** * @GET("/") * * @return Call */ public static function static(): Call; } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/PFTCTestCreateCacheDirectoryFail.php ================================================ */ interface PFTCTestCreateCacheDirectoryFail { /** * @POST("/foo/path") * @Path("path") * @Body("body") * @Query("query") * @Field("field", var="fields") * * @param string $path * @param stdClass $body * @param string $query * @param string[] $fields * @return Call */ public function simple(string $path, stdClass $body, string &$query = 'foo', string ...$fields): Call; /** * @GET("/") * * @return Call */ public static function static(): Call; } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/PFTCTestCreateClientFail.php ================================================ */ interface PFTCTestCreateClientFail { /** * @POST("/foo/path") * @Path("path") * @Body("body") * @Query("query") * @Field("field", var="fields") * * @param string $path * @param stdClass $body * @param string $query * @param string[] $fields * @return Call */ public function simple(string $path, stdClass $body, string &$query = 'foo', string ...$fields): Call; /** * @GET("/") * * @return Call */ public static function static(): Call; } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/PFTCTestCreateTwice.php ================================================ */ interface PFTCTestCreateTwice { /** * @POST("/foo/path") * @Path("path") * @Body("body") * @Query("query") * @Field("field", var="fields") * * @param string $path * @param stdClass $body * @param string $query * @param string[] $fields * @return Call */ public function simple(string $path, stdClass $body, string &$query = 'foo', string ...$fields): Call; /** * @GET("/") * * @return Call */ public static function static(): Call; } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/PFTCTestCreateWithoutCache.php ================================================ */ interface PFTCTestCreateWithoutCache { /** * @POST("/foo/path") * @Path("path") * @Body("body") * @Query("query") * @Field("field", var="fields") * * @param string $path * @param stdClass $body * @param string $query * @param string[] $fields * @return Call */ public function simple(string $path, stdClass $body, string &$query = 'foo', string ...$fields): Call; /** * @GET("/") * * @return Call */ public static function static(): Call; } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/ProxyFactoryTestClientNoReturnType.php ================================================ */ interface ProxyFactoryTestClientNoReturnType { public function foo(stdClass $foo); } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/ProxyFactoryTestClientNoTypehint.php ================================================ */ interface ProxyFactoryTestClientNoTypehint { public function foo($foo): Call; } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/ProxyFactoryTestFilesystem.php ================================================ */ class ProxyFactoryTestFilesystem extends Filesystem { public $makeDirectory = true; public $put = true; public $directory; public $filename; public $contents; public function makeDirectory(string $pathname, int $mode = 0777, bool $recursive = false, $context = null): bool { $this->directory = $pathname; return $this->makeDirectory; } public function put(string $filename, string $contents): bool { $this->filename = $filename; $this->contents = $contents; return $this->put; } } ================================================ FILE: tests/Mock/Unit/Internal/ProxyFactoryTest/ProxyFactoryTestHttpClient.php ================================================ */ class ProxyFactoryTestHttpClient implements HttpClient { /** * Send a request synchronously and return a PSR-7 [@see ResponseInterface] * * @param RequestInterface $request * @return ResponseInterface */ public function send(RequestInterface $request): ResponseInterface { // TODO: Implement send() method. } /** * Send a request asynchronously * * The response callback must be called if any response is returned from the request, and the failure * callback should only be executed if a request was not completed. * * The response callback should pass a PSR-7 [@see ResponseInterface] as the one and only argument. The * failure callback should pass a [@see Throwable] as the one and only argument. * * @param RequestInterface $request * @param callable $onResponse * @param callable $onFailure * @return void */ public function sendAsync(RequestInterface $request, callable $onResponse, callable $onFailure): void { // TODO: Implement sendAsync() method. } /** * Calling this method should execute any enqueued requests asynchronously * * @return void */ public function wait(): void { // TODO: Implement wait() method. } } ================================================ FILE: tests/Mock/Unit/Internal/ServiceMethod/ServiceMethodFactoryTest/ServiceMethodFactoryTestClient.php ================================================ */ interface ServiceMethodFactoryTestClient { /** * @GET("/foo") */ public function foo(): Call; /** * @GET("/bar") */ public function bar(); /** * @GET("/") * @Body("body") */ public function baz(Body $body): Call; /** * @GET("/") * @ResponseBody("Foo") * @ErrorBody("Bar") */ public function qux(): Call; } ================================================ FILE: tests/Mock/Unit/Internal/ServiceMethod/ServiceMethodFactoryTest/ServiceMethodFactoryTestConverterFactory.php ================================================ */ class ServiceMethodFactoryTestConverterFactory implements ConverterFactory { /** * Return a [@see ResponseBodyConverter] or null * * @param TypeToken $type * @return null|ResponseBodyConverter */ public function responseBodyConverter(TypeToken $type): ?ResponseBodyConverter { return new class implements ResponseBodyConverter { public function convert(StreamInterface $value) { return $value; } }; } /** * Return a [@see RequestBodyConverter] or null * * @param TypeToken $type * @return null|RequestBodyConverter */ public function requestBodyConverter(TypeToken $type): ?RequestBodyConverter { return new class implements RequestBodyConverter { public function convert($value): StreamInterface { return $value; } }; } /** * Return a [@see StringConverter] or null * * @param TypeToken $type * @return null|StringConverter */ public function stringConverter(TypeToken $type): ?StringConverter { return new class implements StringConverter { public function convert($value): string { return (string)$value; } }; } } ================================================ FILE: tests/Mock/Unit/MockCall.php ================================================ */ class MockCall implements Call { /** * @return Response */ public function execute(): Response { } /** * @param callable $onResponse * @param callable $onFailure * @return Call */ public function enqueue(?callable $onResponse = null, ?callable $onFailure = null): Call { } /** * @return void */ public function wait(): void { } /** * @return RequestInterface */ public function request(): RequestInterface { } } ================================================ FILE: tests/Mock/Unit/MockConverterFactory.php ================================================ */ class MockConverterFactory implements ConverterFactory { /** * Return a [@see ResponseBodyConverter] or null * * @param TypeToken $type * @return null|ResponseBodyConverter */ public function responseBodyConverter(TypeToken $type): ?ResponseBodyConverter { return null; } /** * Return a [@see RequestBodyConverter] or null * * @param TypeToken $type * @return null|RequestBodyConverter */ public function requestBodyConverter(TypeToken $type): ?RequestBodyConverter { return null; } /** * Return a [@see StringConverter] or null * * @param TypeToken $type * @return null|StringConverter */ public function stringConverter(TypeToken $type): ?StringConverter { return null; } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/ApiClient.php ================================================ */ interface ApiClient { /** * @GET("/") */ public function get(): Call; /** * @OPTIONS("/{my-path}?q=test") * @Url("newUrl") * @Path("my-path", var="path") * @Query("query[]", var="query1") * @QueryName("query2") * @QueryMap("queryMap") */ public function uri(string $newUrl, array $queryMap, array $query1, bool $query2, string $path): Call; /** * @HEAD("/") * @Headers({ * "X-Foo: bar", * "X-Baz: qux", * "X-Header[]: first" * }) * @Header("X-Header[]", var="header1") * @Header("header2") * @HeaderMap("headerMap") */ public function headers(array $headerMap, array $header1, int $header2): Call; /** * @POST("/") */ public function postWithoutBody(): Call; /** * @PUT("/") * @Body("requestBody") * @ResponseBody("Tebru\Retrofit\Test\Mock\Unit\RetrofitTest\RetrofitTestResponseBodyMock") */ public function body(RetrofitTestRequestBodyMock $requestBody): Call; /** * @PATCH("/") * @Field("field1") * @Field("field2") * @Field("field3", encoded=true) * @FieldMap("fieldMap") */ public function field(float $field1, bool $field2, string $field3, array $fieldMap): Call; /** * @REQUEST("/", type="FOO", body=true) * @Part("part1") * @Part("part2") * @PartMap("partMap") */ public function part(RetrofitTestRequestBodyMock $part1, MultipartBody $part2, array $partMap): Call; /** * @GET("/") */ public function callAdapter(): RetrofitTestAdaptedCallMock; /** * @DELETE("/") * @RetrofitTestCustomAnnotation */ public function customAnnotation(): Call; } ================================================ FILE: tests/Mock/Unit/RetrofitTest/CacheableApiClient.php ================================================ */ interface CacheableApiClient extends ApiClient { } ================================================ FILE: tests/Mock/Unit/RetrofitTest/DefaultParamsApiClient.php ================================================ */ interface DefaultParamsApiClient { /** * @GET("/") * @Query("string") * @Query("bool") * @Query("int") * @Query("float") * @QueryMap("client") * @HeaderMap("array") * * @param string $string * @param bool $bool * @param int $int * @param float $float * @param array $array * @param ApiClient|null $client * @return Call */ public function getWithDefaults( ?string $string = 'test', ?bool $bool = true, ?int $int = 1, ?float $float = 3.2, ?array $array = ['test' => ['value']], ?ApiClient $client = null ): Call; } ================================================ FILE: tests/Mock/Unit/RetrofitTest/InvalidSyntaxApiClient.php ================================================ */ interface InvalidSyntaxApiClient { /** * @GET("/") * @Headers({"asdf": "asdf"}) */ public function get(): Call; } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestAdaptedCallMock.php ================================================ */ class RetrofitTestAdaptedCallMock { } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestCallAdapterFactory.php ================================================ */ class RetrofitTestCallAdapterFactory implements CallAdapterFactory { /** * Returns true if the factory supports this type * * @param TypeToken $type * @return bool */ public function supports(TypeToken $type): bool { return $type->isA(RetrofitTestAdaptedCallMock::class); } /** * Create a new factory from type * * @param TypeToken $type * @return CallAdapter */ public function create(TypeToken $type): CallAdapter { return new RetrofitTestCallAdapterMock(); } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestCallAdapterMock.php ================================================ */ class RetrofitTestCallAdapterMock implements CallAdapter { /** * Accepts a [@see Call] and converts it to the appropriate type * * @param Call $call * @return mixed */ public function adapt(Call $call) { return new RetrofitTestAdaptedCallMock(); } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestConverterFactory.php ================================================ */ class RetrofitTestConverterFactory implements ConverterFactory { /** * Return a [@see ResponseBodyConverter] or null * * @param TypeToken $type * @return null|ResponseBodyConverter */ public function responseBodyConverter(TypeToken $type): ?ResponseBodyConverter { return new RetrofitTestResponseBodyConverter(); } /** * Return a [@see RequestBodyConverter] or null * * @param TypeToken $type * @return null|RequestBodyConverter */ public function requestBodyConverter(TypeToken $type): ?RequestBodyConverter { return new RetrofitTestRequestBodyConverter(); } /** * Return a [@see StringConverter] or null * * @param TypeToken $type * @return null|StringConverter */ public function stringConverter(TypeToken $type): ?StringConverter { return null; } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestCustomAnnotation.php ================================================ * * @Annotation * @Target({"CLASS", "METHOD"}) */ class RetrofitTestCustomAnnotation extends AbstractAnnotation { protected function init(): void { } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestCustomAnnotationHandler.php ================================================ */ class RetrofitTestCustomAnnotationHandler implements AnnotationHandler { /** * Handle an annotation, mutating the [@see ServiceMethodBuilder] based on the value * * @param AbstractAnnotation $annotation The annotation to handle * @param ServiceMethodBuilder $serviceMethodBuilder Used to construct a [@see ServiceMethod] * @param Converter|StringConverter|RequestBodyConverter|null $converter Converter used to convert types before sending to service method * @param int|null $index The position of the parameter or null if annotation does not reference parameter * @return void */ public function handle( AbstractAnnotation $annotation, ServiceMethodBuilder $serviceMethodBuilder, ?Converter $converter, ?int $index ): void { $serviceMethodBuilder->addHeader('foo', 'bar'); } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestDelegateProxy.php ================================================ */ class RetrofitTestDelegateProxy implements ApiClient, DefaultParamsApiClient, Proxy { /** * @var Proxy */ private $proxy; /** * Constructor * */ public function __construct(Proxy $proxy) { $this->proxy = $proxy; } public function get(): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } public function uri(string $newUrl, array $queryMap, array $query1, bool $query2, string $path): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } public function headers(array $headerMap, array $header1, int $header2): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } /** * @POST("/") */ public function postWithoutBody(): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } public function body(RetrofitTestRequestBodyMock $requestBody): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } public function field(float $field1, bool $field2, string $field3, array $fieldMap): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } public function part(RetrofitTestRequestBodyMock $part1, MultipartBody $part2, array $partMap): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } public function callAdapter(): RetrofitTestAdaptedCallMock { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } public function customAnnotation(): Call { return $this->__handleRetrofitRequest(ApiClient::class, __FUNCTION__, func_get_args(), []); } /** * @param string $string * @param bool $bool * @param int $int * @param float $float * @param array $array * @param ApiClient|null $client * @return Call */ public function getWithDefaults( ?string $string = 'test', ?bool $bool = true, ?int $int = 1, ?float $float = 3.2, ?array $array = [], ?ApiClient $client = null ): Call { return $this->__handleRetrofitRequest( DefaultParamsApiClient::class, __FUNCTION__, func_get_args(), ['test', true, 1, 3.2, [], null] ); } /** * Constructs a [@see Call] object based on an interface method and arguments, then passes it through a * [@see CallAdapter] before returning. * * @param string $interfaceName * @param string $methodName * @param array $args * @param array $defaultArgs * @return mixed */ public function __handleRetrofitRequest(string $interfaceName, string $methodName, array $args, array $defaultArgs) { return $this->proxy->__handleRetrofitRequest($interfaceName, $methodName, $args, []); } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestHttpClient.php ================================================ */ class RetrofitTestHttpClient implements HttpClient { /** * @var RequestInterface[] */ public $requests = []; /** * Send a request synchronously and return a PSR-7 [@see ResponseInterface] * * @param RequestInterface $request * @return ResponseInterface */ public function send(RequestInterface $request): ResponseInterface { $this->requests[] = $request; return new Response(); } /** * Send a request asynchronously * * The response callback must be called if any response is returned from the request, and the failure * callback should only be executed if a request was not completed. * * The response callback should pass a PSR-7 [@see ResponseInterface] as the one and only argument. The * failure callback should pass a [@see Throwable] as the one and only argument. * * @param RequestInterface $request * @param callable $onResponse * @param callable $onFailure * @return void */ public function sendAsync(RequestInterface $request, callable $onResponse, callable $onFailure): void { $this->requests[] = $request; } /** * Calling this method should execute any enqueued requests asynchronously * * @return void */ public function wait(): void { } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestProxyFactory.php ================================================ */ class RetrofitTestProxyFactory implements ProxyFactory, DefaultProxyFactoryAware { /** * @var ProxyFactory */ private $proxyFactory; /** * Create a new [@see Proxy] from given service name * * Returns null if the factory cannot handle the service * * @param string $service * @return null|Proxy */ public function create(string $service): ?Proxy { return new RetrofitTestDelegateProxy($this->proxyFactory->create(ApiClient::class)); } /** * Set the default proxy factory * * @param ProxyFactory $proxyFactory * @return void */ public function setDefaultProxyFactory(ProxyFactory $proxyFactory): void { $this->proxyFactory = $proxyFactory; } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestRequestBodyConverter.php ================================================ */ class RetrofitTestRequestBodyConverter implements RequestBodyConverter { /** * Convert to stream * * @param RetrofitTestRequestBodyMock $value * @return StreamInterface */ public function convert($value): StreamInterface { return stream_for(json_encode(['id' => $value->id, 'name' => $value->name])); } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestRequestBodyMock.php ================================================ */ class RetrofitTestRequestBodyMock { public $id; public $name; } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestResponseBodyConverter.php ================================================ */ class RetrofitTestResponseBodyConverter implements ResponseBodyConverter { /** * Convert from stream to any type * * @param StreamInterface $value * @return mixed */ public function convert(StreamInterface $value) { return new RetrofitTestResponseBodyMock(); } } ================================================ FILE: tests/Mock/Unit/RetrofitTest/RetrofitTestResponseBodyMock.php ================================================ */ class RetrofitTestResponseBodyMock { } ================================================ FILE: tests/Unit/Internal/AnnotationHandlersTest.php ================================================ serviceMethodBuilder = new DefaultServiceMethodBuilder(); $this->requestBodyConverter = new DefaultRequestBodyConverter(); $this->stringConverter = new DefaultStringConverter(); } public function testHandleBodyAnnotation() { (new BodyAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->requestBodyConverter, 1 ); self::assertAttributeSame(true, 'hasBody', $this->serviceMethodBuilder); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new BodyParamHandler($this->requestBodyConverter)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleBodyAnnotationWrongConverter() { try { (new BodyAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a RequestBodyConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleFieldAnnotation() { (new FieldAnnotHandler())->handle( new Field(['value' => 'foo', 'encoded' => true]), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeSame(true, 'hasBody', $this->serviceMethodBuilder); self::assertAttributeSame('application/x-www-form-urlencoded', 'contentType', $this->serviceMethodBuilder); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new FieldParamHandler($this->stringConverter, 'foo', true)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleFieldAnnotationWrongAnnotation() { try { (new FieldAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be encodable', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleFieldAnnotationWrongConverter() { try { (new FieldAnnotHandler())->handle( new Field(['value' => 'foo', 'encoded' => true]), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleFieldMapAnnotation() { (new FieldMapAnnotHandler())->handle( new FieldMap(['value' => 'foo', 'encoded' => true]), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeSame(true, 'hasBody', $this->serviceMethodBuilder); self::assertAttributeSame('application/x-www-form-urlencoded', 'contentType', $this->serviceMethodBuilder); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new FieldMapParamHandler($this->stringConverter, true)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleFieldMapAnnotationWrongAnnotation() { try { (new FieldMapAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be encodable', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleFieldMapAnnotationWrongConverter() { try { (new FieldMapAnnotHandler())->handle( new FieldMap(['value' => 'foo', 'encoded' => true]), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleHeaderAnnotation() { (new HeaderAnnotHandler())->handle( new Header(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new HeaderParamHandler($this->stringConverter, 'foo')], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleHeaderAnnotationWrongConverter() { try { (new HeaderAnnotHandler())->handle( new Header(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleHeaderMapAnnotation() { (new HeaderMapAnnotHandler())->handle( new HeaderMap(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new HeaderMapParamHandler($this->stringConverter)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleHeaderMapAnnotationWrongConverter() { try { (new HeaderMapAnnotHandler())->handle( new HeaderMap(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleHeadersAnnotation() { (new HeadersAnnotHandler())->handle( new Headers(['value' => ['Foo: bar', 'Baz: true']]), $this->serviceMethodBuilder, null, 1 ); self::assertAttributeSame(['foo' => ['bar'], 'baz' => ['true']], 'headers', $this->serviceMethodBuilder); } public function testHandleHeadersAnnotationWrongConverter() { try { (new HeadersAnnotHandler())->handle( new Headers(['value' => ['Foo: bar', 'Baz: true']]), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be null, object found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleGETAnnotation() { (new HttpRequestAnnotHandler())->handle( new GET(['value' => '/my/path?q=test']), $this->serviceMethodBuilder, null, 1 ); self::assertAttributeSame('GET', 'method', $this->serviceMethodBuilder); self::assertAttributeSame('/my/path?q=test', 'path', $this->serviceMethodBuilder); self::assertAttributeSame(false, 'hasBody', $this->serviceMethodBuilder); } public function testHandleGETAnnotationWrongConverter() { try { (new HttpRequestAnnotHandler())->handle( new GET(['value' => '/my/path?q=test']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be null, object found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandlePOSTAnnotation() { (new HttpRequestAnnotHandler())->handle( new POST(['value' => '/my/path?q=test']), $this->serviceMethodBuilder, null, 1 ); self::assertAttributeSame('POST', 'method', $this->serviceMethodBuilder); self::assertAttributeSame('/my/path?q=test', 'path', $this->serviceMethodBuilder); } public function testHandlePOSTAnnotationWrongAnnotation() { try { (new HttpRequestAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be an HttpRequest', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandlePartAnnotation() { (new PartAnnotHandler())->handle( new Part(['value' => 'foo']), $this->serviceMethodBuilder, $this->requestBodyConverter, 1 ); self::assertAttributeSame(true, 'hasBody', $this->serviceMethodBuilder); self::assertAttributeSame('multipart/form-data', 'contentType', $this->serviceMethodBuilder); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new PartParamHandler($this->requestBodyConverter, 'foo', 'binary')], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandlePartAnnotationWrongAnnotation() { try { (new PartAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be a Part', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandlePartAnnotationWrongConverter() { try { (new PartAnnotHandler())->handle( new Part(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a RequestBodyConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandlePartMapAnnotation() { (new PartMapAnnotHandler())->handle( new PartMap(['value' => 'foo']), $this->serviceMethodBuilder, $this->requestBodyConverter, 1 ); self::assertAttributeSame(true, 'hasBody', $this->serviceMethodBuilder); self::assertAttributeSame('multipart/form-data', 'contentType', $this->serviceMethodBuilder); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new PartMapParamHandler($this->requestBodyConverter, 'binary')], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandlePartMapAnnotationWrongAnnotation() { try { (new PartMapAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be a PartMap', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandlePartMapAnnotationWrongConverter() { try { (new PartMapAnnotHandler())->handle( new PartMap(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a RequestBodyConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandlePathAnnotation() { (new PathAnnotHandler())->handle( new Path(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new PathParamHandler($this->stringConverter, 'foo')], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandlePathAnnotationWrongConverter() { try { (new PathAnnotHandler())->handle( new Path(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleQueryAnnotation() { (new QueryAnnotHandler())->handle( new Query(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new QueryParamHandler($this->stringConverter, 'foo', false)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleQueryAnnotationWrongAnnotation() { try { (new QueryAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be encodable', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleQueryAnnotationWrongConverter() { try { (new QueryAnnotHandler())->handle( new Query(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleQueryMapAnnotation() { (new QueryMapAnnotHandler())->handle( new QueryMap(['value' => 'foo', 'encoded' => true]), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new QueryMapParamHandler($this->stringConverter, true)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleQueryMapAnnotationWrongAnnotation() { try { (new QueryMapAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be encodable', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleQueryMapAnnotationWrongConverter() { try { (new QueryMapAnnotHandler())->handle( new QueryMap(['value' => 'foo', 'encoded' => true]), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleQueryNameAnnotation() { (new QueryNameAnnotHandler())->handle( new QueryName(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new QueryNameParamHandler($this->stringConverter, false)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleQueryNameAnnotationWrongAnnotation() { try { (new QueryNameAnnotHandler())->handle( new Body(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Annotation must be encodable', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleQueryNameAnnotationWrongConverter() { try { (new QueryNameAnnotHandler())->handle( new QueryName(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testHandleUrlAnnotation() { (new UrlAnnotHandler())->handle( new Url(['value' => 'foo']), $this->serviceMethodBuilder, $this->stringConverter, 1 ); self::assertAttributeCount(1, 'parameterHandlers', $this->serviceMethodBuilder); self::assertAttributeEquals([1 => new UrlParamHandler($this->stringConverter)], 'parameterHandlers', $this->serviceMethodBuilder); } public function testHandleUrlAnnotationWrongConverter() { try { (new UrlAnnotHandler())->handle( new Url(['value' => 'foo']), $this->serviceMethodBuilder, null, 1 ); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Converter must be a StringConverter, NULL found', $exception->getMessage()); return; } self::fail('Exception not thrown'); } } ================================================ FILE: tests/Unit/Internal/AnnotationProcessorTest.php ================================================ annotationProcessor = new AnnotationProcessor([ Header::class => new HeadersAnnotHandler(), Body::class => new BodyAnnotHandler(), Query::class => new QueryAnnotHandler(), Headers::class => new HeadersAnnotHandler(), BadConverterAnnotation::class => new HeadersAnnotHandler(), ]); $this->serviceMethodBuilder = new DefaultServiceMethodBuilder(); $this->converterProvider = new ConverterProvider([new DefaultConverterFactory()]); } public function testParameterAwareAnnotation() { $this->annotationProcessor->process( new Query(['value' => 'bar']), $this->serviceMethodBuilder, $this->converterProvider, new ReflectionMethod(AnnotationProcessorTestMock::class, 'foo') ); self::assertAttributeEquals( [new QueryParamHandler(new DefaultStringConverter(), 'bar', false)], 'parameterHandlers', $this->serviceMethodBuilder ); } public function testNonParameterAwareAnnotation() { $this->annotationProcessor->process( new Headers(['value' => ['foo:bar']]), $this->serviceMethodBuilder, $this->converterProvider, new ReflectionMethod(AnnotationProcessorTestMock::class, 'foo') ); self::assertAttributeSame( ['foo' => ['bar']], 'headers', $this->serviceMethodBuilder ); } public function testRequestBodyAnnotation() { $this->annotationProcessor->process( new Body(['value' => 'bar']), $this->serviceMethodBuilder, $this->converterProvider, new ReflectionMethod(AnnotationProcessorTestMock::class, 'body') ); self::assertAttributeEquals( [new BodyParamHandler(new DefaultRequestBodyConverter())], 'parameterHandlers', $this->serviceMethodBuilder ); } public function testNoHandler() { $this->annotationProcessor->process( new Path(['value' => 'bar']), $this->serviceMethodBuilder, $this->converterProvider, new ReflectionMethod(AnnotationProcessorTestMock::class, 'body') ); self::assertAttributeEquals( [], 'parameterHandlers', $this->serviceMethodBuilder ); } public function testParameterNotFound() { try { $this->annotationProcessor->process( new Query(['value' => 'bar2']), $this->serviceMethodBuilder, $this->converterProvider, new ReflectionMethod(AnnotationProcessorTestMock::class, 'foo') ); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Could not find parameter named bar2 in ' . 'Tebru\Retrofit\Test\Mock\Unit\Internal\AnnotationProcessorTest\AnnotationProcessorTestMock::foo. ' . 'Please double check that annotations are properly referencing method parameters.', $exception->getMessage() ); return; } self::fail('Exception was not thrown'); } public function testParameterTypeNotFound() { try { $this->annotationProcessor->process( new Query(['value' => 'bar']), $this->serviceMethodBuilder, $this->converterProvider, new ReflectionMethod(AnnotationProcessorTestMock::class, 'noType') ); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Parameter type was not found for method ' . 'Tebru\Retrofit\Test\Mock\Unit\Internal\AnnotationProcessorTest\AnnotationProcessorTestMock::noType', $exception->getMessage() ); return; } self::fail('Exception was not thrown'); } public function testConverterNotFound() { try { $this->annotationProcessor->process( new BadConverterAnnotation(['value' => 'bar']), $this->serviceMethodBuilder, $this->converterProvider, new ReflectionMethod(AnnotationProcessorTestMock::class, 'foo') ); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Unable to handle converter of type Foo. Please use RequestBodyConverter or StringConverter', $exception->getMessage() ); return; } self::fail('Exception was not thrown'); } } ================================================ FILE: tests/Unit/Internal/CallAdapterTest.php ================================================ */ class CallAdapterTest extends TestCase { /** * @var CallAdapterProvider */ private $callAdapterProvider; public function setUp() { $this->callAdapterProvider = new CallAdapterProvider([new DefaultCallAdapterFactory()]); } public function testAdaptDefaultCall() { $call = new MockCall(); $adaptedCall = $this->callAdapterProvider->get(new TypeToken(MockCall::class))->adapt($call); self::assertSame($adaptedCall, $call); } public function testCallAdapterNotFound() { try { $this->callAdapterProvider->get(new TypeToken(self::class)); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Could not get call adapter for type ' . 'Tebru\Retrofit\Test\Unit\Internal\CallAdapterTest', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } } ================================================ FILE: tests/Unit/Internal/ConverterTest.php ================================================ */ class ConverterTest extends TestCase { /** * @var ConverterProvider */ private $converterProvider; public function setUp() { $this->converterProvider = new ConverterProvider([new DefaultConverterFactory()]); } public function testRequestBodyConverter() { $stream = new AppendStream(); $converted = $this->converterProvider->getRequestBodyConverter(new TypeToken(StreamInterface::class))->convert($stream); self::assertSame($stream, $converted); } public function testRequestBodyConverterProviderCache() { $converter = $this->converterProvider->getRequestBodyConverter(new TypeToken(StreamInterface::class)); $converter2 = $this->converterProvider->getRequestBodyConverter(new TypeToken(StreamInterface::class)); self::assertSame($converter, $converter2); } public function testRequestBodyConverterProviderException() { try { $this->converterProvider->getRequestBodyConverter(new TypeToken('Foo')); } catch (LogicException $exception) { self::assertSame('Retrofit: Could not get request body converter for type Foo', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testResponseBodyConverter() { $stream = new AppendStream(); $converted = $this->converterProvider->getResponseBodyConverter(new TypeToken(StreamInterface::class))->convert($stream); self::assertSame($stream, $converted); } public function testResponseBodyConverterProviderCache() { $converter = $this->converterProvider->getResponseBodyConverter(new TypeToken(StreamInterface::class)); $converter2 = $this->converterProvider->getResponseBodyConverter(new TypeToken(StreamInterface::class)); self::assertSame($converter, $converter2); } public function testResponseBodyConverterProviderException() { try { $this->converterProvider->getResponseBodyConverter(new TypeToken('Foo')); } catch (LogicException $exception) { self::assertSame('Retrofit: Could not get response body converter for type Foo', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testStringConverter() { $converted = $this->converterProvider->getStringConverter(new TypeToken('string'))->convert('foo'); self::assertSame('foo', $converted); } public function testStringConverterInt() { $converted = $this->converterProvider->getStringConverter(new TypeToken('int'))->convert(1); self::assertSame('1', $converted); } public function testStringConverterFloat() { $converted = $this->converterProvider->getStringConverter(new TypeToken('float'))->convert(1.5); self::assertSame('1.5', $converted); } public function testStringConverterTrue() { $converted = $this->converterProvider->getStringConverter(new TypeToken('boolean'))->convert(true); self::assertSame('true', $converted); } public function testStringConverterFalse() { $converted = $this->converterProvider->getStringConverter(new TypeToken('boolean'))->convert(false); self::assertSame('false', $converted); } public function testStringConverterArray() { $converted = $this->converterProvider->getStringConverter(new TypeToken('array'))->convert([1]); self::assertSame('a:1:{i:0;i:1;}', $converted); } public function testStringConverterObject() { $converted = $this->converterProvider->getStringConverter(new TypeToken('object'))->convert(new \stdClass()); self::assertSame('O:8:"stdClass":0:{}', $converted); } public function testStringConverterProviderCache() { $converter = $this->converterProvider->getStringConverter(new TypeToken('string')); $converter2 = $this->converterProvider->getStringConverter(new TypeToken('string')); self::assertSame($converter, $converter2); } public function testStringConverterProviderException() { $converterProvider = new ConverterProvider([new MockConverterFactory()]); try { $converterProvider->getStringConverter(new TypeToken('Foo')); } catch (LogicException $exception) { self::assertSame('Retrofit: Could not get string converter for type Foo', $exception->getMessage()); return; } self::fail('Exception not thrown'); } } ================================================ FILE: tests/Unit/Internal/DefaultProxyFactoryTest.php ================================================ filesystem = new ProxyFactoryTestFilesystem(); } public function testCreate() { $client = $this->createFactory()->create(PFTCTestCreate::class); self::assertInstanceOf(PFTCTestCreate::class, $client); self::assertSame($this->getExpectedClass(), $this->filesystem->contents); self::assertSame( '/tmp/cache/retrofit/Tebru/Retrofit/Test/Mock/Unit/Internal/ProxyFactoryTest', $this->filesystem->directory ); self::assertSame( '/tmp/cache/retrofit/Tebru/Retrofit/Test/Mock/Unit/Internal/ProxyFactoryTest/PFTCTestCreate.php', $this->filesystem->filename ); } public function testCreateTwice() { $this->createFactory()->create(PFTCTestCreateTwice::class); $this->filesystem->directory = null; $this->filesystem->filename = null; $this->filesystem->contents = null; $client = $this->createFactory()->create(PFTCTestCreateTwice::class); self::assertInstanceOf(PFTCTestCreateTwice::class, $client); self::assertNull($this->filesystem->contents); self::assertNull($this->filesystem->directory); self::assertNull($this->filesystem->filename); } public function testCreateWithoutCache() { $this->createFactory(false)->create(PFTCTestCreateWithoutCache::class); $client = $this->createFactory(false)->create(PFTCTestCreateWithoutCache::class); self::assertInstanceOf(PFTCTestCreateWithoutCache::class, $client); self::assertNull($this->filesystem->contents); self::assertNull($this->filesystem->directory); self::assertNull($this->filesystem->filename); } public function testCreateInvalidInterface() { try { $this->createFactory()->create('Foo'); } catch (InvalidArgumentException $exception) { self::assertSame('Retrofit: Foo is expected to be an interface', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testCreateNoTypehint() { try { $this->createFactory()->create(ProxyFactoryTestClientNoTypehint::class); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Parameter types are required. None found for parameter foo in ' . 'Tebru\Retrofit\Test\Mock\Unit\Internal\ProxyFactoryTest\ProxyFactoryTestClientNoTypehint::foo()', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testCreateNoReturnType() { try { $this->createFactory()->create(ProxyFactoryTestClientNoReturnType::class); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Method return types are required. None found for ' . 'Tebru\Retrofit\Test\Mock\Unit\Internal\ProxyFactoryTest\ProxyFactoryTestClientNoReturnType::foo()', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testCreateCacheDirectoryFail() { $this->filesystem->makeDirectory = false; try { $this->createFactory()->create(PFTCTestCreateCacheDirectoryFail::class); } catch (RuntimeException $exception) { self::assertSame( 'Retrofit: There was an issue creating the cache directory: ' . '/tmp/cache/retrofit/Tebru/Retrofit/Test/Mock/Unit/Internal/ProxyFactoryTest', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testCreateClientFail() { $this->filesystem->put = false; try { $this->createFactory()->create(PFTCTestCreateClientFail::class); } catch (RuntimeException $exception) { self::assertSame( 'Retrofit: There was an issue writing proxy class to: ' . '/tmp/cache/retrofit/Tebru/Retrofit/Test/Mock/Unit/Internal/ProxyFactoryTest/PFTCTestCreateClientFail.php', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } private function createFactory(bool $enableCache = true): DefaultProxyFactory { return new DefaultProxyFactory( new BuilderFactory(), new Standard(), new ServiceMethodFactory( new AnnotationProcessor([ ]), new CallAdapterProvider([new DefaultCallAdapterFactory()]), new ConverterProvider([new DefaultConverterFactory()]), new AnnotationReaderAdapter(new AnnotationReader(), CacheProvider::createNullCache()), 'http://example.com' ), new ProxyFactoryTestHttpClient(), $this->filesystem, $enableCache, '/tmp/cache/retrofit' ); } private function getExpectedClass(): string { return <<< 'EOT' __handleRetrofitRequest('Tebru\\Retrofit\\Test\\Mock\\Unit\\Internal\\ProxyFactoryTest\\PFTCTestCreate', __FUNCTION__, func_get_args(), array(null, null, 'foo', null)); } public static function static() : \Tebru\Retrofit\Call { return $this->__handleRetrofitRequest('Tebru\\Retrofit\\Test\\Mock\\Unit\\Internal\\ProxyFactoryTest\\PFTCTestCreate', __FUNCTION__, func_get_args(), array()); } } EOT; } } ================================================ FILE: tests/Unit/Internal/FilesystemTest.php ================================================ filesystem = new Filesystem(); } public function testCreateDirectory() { $directory = vfsStream::url('cache/retrofit/Test'); self::assertTrue($this->filesystem->makeDirectory($directory)); } public function testCreateFile() { $directory = vfsStream::url('cache/retrofit/Test'); $this->filesystem->makeDirectory($directory); $file = vfsStream::url('cache/retrofit/Test/Service.php'); self::assertTrue($this->filesystem->put($file, 'foo')); } } ================================================ FILE: tests/Unit/Internal/HttpClientCallTest.php ================================================ execute(); self::assertInstanceOf(RetrofitResponse::class, $response); self::assertSame(204, $response->raw()->getStatusCode()); self::assertInstanceOf(HttpClientCallTestResponseBodyMock::class, $response->body()); self::assertNull($response->errorBody()); } public function testSyncError() { $request = new Request('GET', 'http://example.com/'); $response = new Response(500); $responseBody = new HttpClientCallTestErrorBodyMock(); $call = new HttpClientCall( new HttpClientCallTestClientMock($response), new HttpClientCallTestServiceMethodMock($request, null, $responseBody), [] ); $response = $call->execute(); self::assertInstanceOf(RetrofitResponse::class, $response); self::assertSame(500, $response->raw()->getStatusCode()); self::assertNull($response->body()); self::assertInstanceOf(HttpClientCallTestErrorBodyMock::class, $response->errorBody()); } public function testAsync() { $request = new Request('GET', 'http://example.com/'); $response = new Response(204); $responseBody = new HttpClientCallTestResponseBodyMock(); $call = new HttpClientCall( new HttpClientCallTestClientMock($response), new HttpClientCallTestServiceMethodMock($request, $responseBody), [] ); $call->enqueue( function (RetrofitResponse $response) { self::assertInstanceOf(RetrofitResponse::class, $response); self::assertSame(204, $response->raw()->getStatusCode()); self::assertInstanceOf(HttpClientCallTestResponseBodyMock::class, $response->body()); self::assertNull($response->errorBody()); }, function (Throwable $throwable) { self::fail('Error callback should not be called'); } ); $call->wait(); } public function testAsyncError() { $request = new Request('GET', 'http://example.com/'); $response = new Response(500); $responseBody = new HttpClientCallTestErrorBodyMock(); $call = new HttpClientCall( new HttpClientCallTestClientMock($response), new HttpClientCallTestServiceMethodMock($request, null, $responseBody), [] ); $call->enqueue( function (RetrofitResponse $response) { self::assertInstanceOf(RetrofitResponse::class, $response); self::assertSame(500, $response->raw()->getStatusCode()); self::assertNull($response->body()); self::assertInstanceOf(HttpClientCallTestErrorBodyMock::class, $response->errorBody()); }, function (Throwable $throwable) { self::fail('Error callback should not be called'); } ); $call->wait(); } public function testAsyncFailure() { $request = new Request('GET', 'http://example.com/'); $call = new HttpClientCall( new HttpClientCallTestClientMock(), new HttpClientCallTestServiceMethodMock($request), [] ); $call->enqueue( function (RetrofitResponse $response) { self::fail('Response callback should not be called'); }, function (Throwable $throwable) { self::assertInstanceOf(RuntimeException::class, $throwable); } ); $call->wait(); } public function testAsyncFailureThrowsException() { $request = new Request('GET', 'http://example.com/'); $call = new HttpClientCall( new HttpClientCallTestClientMock(), new HttpClientCallTestServiceMethodMock($request), [] ); $call->enqueue( function (RetrofitResponse $response) { self::fail('Response callback should not be called'); } ); try { $call->wait(); } catch (RuntimeException $exception) { self::assertTrue(true); return; } self::fail('Exception not thrown'); } public function testSyncInvalidJsonResponseThrowsException() { $request = new Request('GET', 'http://example.com/'); $response = new Response(204, [], '{'); $responseBody = new HttpClientCallTestResponseBodyMock(); $call = new HttpClientCall( new HttpClientCallTestClientMock($response), new HttpClientCallTestServiceMethodMock($request, $responseBody), [] ); try { $call->execute(); } catch (ResponseHandlingFailedException $exception) { self::assertSame('GET', $exception->getRequest()->getMethod()); self::assertSame('{', (string)$exception->getResponse()->getBody()); return; } self::fail('Exception not thrown'); } public function testSyncInvalidJsonErrorThrowsException() { $request = new Request('GET', 'http://example.com/'); $response = new Response(400, [], '{'); $responseBody = new HttpClientCallTestResponseBodyMock(); $call = new HttpClientCall( new HttpClientCallTestClientMock($response), new HttpClientCallTestServiceMethodMock($request, $responseBody), [] ); try { $call->execute(); } catch (ResponseHandlingFailedException $exception) { self::assertSame('GET', $exception->getRequest()->getMethod()); self::assertSame('{', (string)$exception->getResponse()->getBody()); return; } self::fail('Exception not thrown'); } public function testAsyncInvalidJsonResponseThrowsExceptions() { $request = new Request('GET', 'http://example.com/'); $response = new Response(204, [], '{'); $responseBody = new HttpClientCallTestResponseBodyMock(); $call = new HttpClientCall( new HttpClientCallTestClientMock($response), new HttpClientCallTestServiceMethodMock($request, $responseBody), [] ); $call->enqueue(function () {}); try { $call->wait(); } catch (ResponseHandlingFailedException $exception) { self::assertSame('GET', $exception->getRequest()->getMethod()); self::assertSame('{', (string)$exception->getResponse()->getBody()); return; } self::fail('Exception not thrown'); } public function testAsyncInvalidJsonerrorThrowsExceptions() { $request = new Request('GET', 'http://example.com/'); $response = new Response(400, [], '{'); $responseBody = new HttpClientCallTestResponseBodyMock(); $call = new HttpClientCall( new HttpClientCallTestClientMock($response), new HttpClientCallTestServiceMethodMock($request, $responseBody), [] ); $call->enqueue(function () {}); try { $call->wait(); } catch (ResponseHandlingFailedException $exception) { self::assertSame('GET', $exception->getRequest()->getMethod()); self::assertSame('{', (string)$exception->getResponse()->getBody()); return; } self::fail('Exception not thrown'); } } ================================================ FILE: tests/Unit/Internal/ParameterHandlersTest.php ================================================ */ class ParameterHandlersTest extends TestCase { /** * @var RequestBuilder */ private $requestBuilder; public function setUp() { $this->requestBuilder = new RequestBuilder('GET', 'http://example.com', '/test/{path}?q=test', []); } public function testBodyHandler() { $stream = new AppendStream(); (new BodyParamHandler(new DefaultRequestBodyConverter()))->apply($this->requestBuilder, $stream); self::assertAttributeSame($stream, 'body', $this->requestBuilder); } public function testBodyHandlerNull() { (new BodyParamHandler(new DefaultRequestBodyConverter()))->apply($this->requestBuilder, null); self::assertAttributeSame(null, 'body', $this->requestBuilder); } public function testFieldMapHandler() { $map = [ 'foo' => 'bar', 'afoo[]' => ['baz', 'qux'], 'nfoo' => null, ]; $expected = [ 'foo=bar', 'afoo%5B%5D=baz', 'afoo%5B%5D=qux', ]; (new FieldMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, $map); self::assertAttributeSame($expected, 'fields', $this->requestBuilder); } public function testFieldMapHandlerIterator() { $map = new ArrayIterator([ 'foo' => 'bar', 'afoo[]' => ['baz', 'qux'], 'nfoo' => null, ]); $expected = [ 'foo=bar', 'afoo%5B%5D=baz', 'afoo%5B%5D=qux', ]; (new FieldMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, $map); self::assertAttributeSame($expected, 'fields', $this->requestBuilder); } public function testFieldMapHandlerEmpty() { (new FieldMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, []); self::assertAttributeSame([], 'fields', $this->requestBuilder); } public function testFieldMapHandlerNull() { (new FieldMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'fields', $this->requestBuilder); } public function testFieldHandler() { (new FieldParamHandler(new DefaultStringConverter(), 'foo', false))->apply($this->requestBuilder, 'bar'); self::assertAttributeSame(['foo=bar'], 'fields', $this->requestBuilder); } public function testFieldHandlerArray() { (new FieldParamHandler(new DefaultStringConverter(), 'foo', false))->apply($this->requestBuilder, ['bar', 'baz']); self::assertAttributeSame(['foo=bar', 'foo=baz'], 'fields', $this->requestBuilder); } public function testFieldHandlerNull() { (new FieldParamHandler(new DefaultStringConverter(), 'foo', false))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'fields', $this->requestBuilder); } public function testHeaderMapHandler() { $map = [ 'foo' => 'bar', 'afoo' => ['baz', 'qux'], 'nfoo' => null, ]; $expected = [ 'foo' => ['bar'], 'afoo' => ['baz', 'qux'], ]; (new HeaderMapParamHandler(new DefaultStringConverter()))->apply($this->requestBuilder, $map); self::assertAttributeSame($expected, 'headers', $this->requestBuilder); } public function testHeaderMapHandlerIterator() { $map = new ArrayIterator([ 'foo' => 'bar', 'afoo' => ['baz', 'qux'], 'nfoo' => null, ]); $expected = [ 'foo' => ['bar'], 'afoo' => ['baz', 'qux'], ]; (new HeaderMapParamHandler(new DefaultStringConverter()))->apply($this->requestBuilder, $map); self::assertAttributeSame($expected, 'headers', $this->requestBuilder); } public function testHeaderMapHandlerEmpty() { (new HeaderMapParamHandler(new DefaultStringConverter()))->apply($this->requestBuilder, []); self::assertAttributeSame([], 'headers', $this->requestBuilder); } public function testHeaderMapHandlerNull() { (new HeaderMapParamHandler(new DefaultStringConverter()))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'headers', $this->requestBuilder); } public function testHeaderHandler() { (new HeaderParamHandler(new DefaultStringConverter(), 'foo'))->apply($this->requestBuilder, 'bar'); self::assertAttributeSame(['foo' => ['bar']], 'headers', $this->requestBuilder); } public function testHeaderHandlerArray() { (new HeaderParamHandler(new DefaultStringConverter(), 'foo'))->apply($this->requestBuilder, ['bar', 'baz']); self::assertAttributeSame(['foo' => ['bar', 'baz']], 'headers', $this->requestBuilder); } public function testHeaderHandlerNull() { (new HeaderParamHandler(new DefaultStringConverter(), 'foo'))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'headers', $this->requestBuilder); } public function testPartMapHandler() { $stream1 = stream_for('bar'); $stream2 = stream_for('bar'); $stream3 = new AppendStream(); $simpleMultipart = new MultipartBody('mfoo1', $stream2); $streamMultipart = new MultipartBody('mfoo2', $stream3, ['a' => 'b'], 'ParameterHandlersTest.php'); $map = [ 'foo' => $stream1, 'simple' => $simpleMultipart, 'stream' => $streamMultipart, 'nfoo' => null, ]; $expected = [ [ 'name' => 'foo', 'contents' => $stream1, 'headers' => ['Content-Transfer-Encoding' => 'binary'], 'filename' => null, ], [ 'name' => 'mfoo1', 'contents' => $stream2, 'headers' => ['Content-Transfer-Encoding' => 'binary'], 'filename' => null, ], [ 'name' => 'mfoo2', 'contents' => new $stream3, 'headers' => ['a' => 'b', 'Content-Transfer-Encoding' => 'binary'], 'filename' => 'ParameterHandlersTest.php', ], ]; (new PartMapParamHandler(new DefaultRequestBodyConverter(), 'binary'))->apply($this->requestBuilder, $map); self::assertAttributeEquals($expected, 'parts', $this->requestBuilder); } public function testPartMapHandlerIterator() { $stream1 = stream_for('bar'); $stream2 = stream_for('bar'); $stream3 = new AppendStream(); $simpleMultipart = new MultipartBody('mfoo1', $stream2); $streamMultipart = new MultipartBody('mfoo2', $stream3, ['a' => 'b'], 'ParameterHandlersTest.php'); $map = new ArrayIterator([ 'foo' => $stream1, 'simple' => $simpleMultipart, 'stream' => $streamMultipart, 'nfoo' => null, ]); $expected = [ [ 'name' => 'foo', 'contents' => $stream1, 'headers' => ['Content-Transfer-Encoding' => 'binary'], 'filename' => null, ], [ 'name' => 'mfoo1', 'contents' => $stream2, 'headers' => ['Content-Transfer-Encoding' => 'binary'], 'filename' => null, ], [ 'name' => 'mfoo2', 'contents' => new $stream3, 'headers' => ['a' => 'b', 'Content-Transfer-Encoding' => 'binary'], 'filename' => 'ParameterHandlersTest.php', ], ]; (new PartMapParamHandler(new DefaultRequestBodyConverter(), 'binary'))->apply($this->requestBuilder, $map); self::assertAttributeEquals($expected, 'parts', $this->requestBuilder); } public function testPartMapHandlerEmpty() { (new PartMapParamHandler(new DefaultRequestBodyConverter(), 'binary'))->apply($this->requestBuilder, []); self::assertAttributeSame([], 'parts', $this->requestBuilder); } public function testPartMapHandlerNull() { (new PartMapParamHandler(new DefaultRequestBodyConverter(), 'binary'))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'parts', $this->requestBuilder); } public function testPartHandler() { $stream = stream_for('bar'); $expected = [[ 'name' => 'foo', 'contents' => $stream, 'headers' => ['Content-Transfer-Encoding' => 'binary'], 'filename' => null, ]]; (new PartParamHandler(new DefaultRequestBodyConverter(), 'foo', 'binary'))->apply($this->requestBuilder, $stream); self::assertAttributeSame($expected, 'parts', $this->requestBuilder); } public function testPartHandlerMultipart() { $stream = stream_for('bar'); $multipart = new MultipartBody('foo', $stream); $expected = [[ 'name' => 'foo', 'contents' => $stream, 'headers' => ['Content-Transfer-Encoding' => 'binary'], 'filename' => null, ]]; (new PartParamHandler(new DefaultRequestBodyConverter(), 'foo', 'binary'))->apply($this->requestBuilder, $multipart); self::assertAttributeSame($expected, 'parts', $this->requestBuilder); } public function testPartHandlerMultipartHeadersAndFilename() { $stream = stream_for('bar'); $multipart = new MultipartBody('foo', $stream, ['a' => 'b'], 'Test.php'); $expected = [[ 'name' => 'foo', 'contents' => $stream, 'headers' => ['a' => 'b', 'Content-Transfer-Encoding' => 'binary'], 'filename' => 'Test.php', ]]; (new PartParamHandler(new DefaultRequestBodyConverter(), 'foo', 'binary'))->apply($this->requestBuilder, $multipart); self::assertAttributeSame($expected, 'parts', $this->requestBuilder); } public function testPartHandlerNull() { (new PartParamHandler(new DefaultRequestBodyConverter(), 'foo', 'binary'))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'parts', $this->requestBuilder); } public function testPathHandler() { (new PathParamHandler(new DefaultStringConverter(), 'path'))->apply($this->requestBuilder, 'bar'); self::assertAttributeEquals(new Uri('http://example.com/test/bar?q=test'), 'uri', $this->requestBuilder); } public function testPathHandlerNull() { try { (new PathParamHandler(new DefaultStringConverter(), 'path'))->apply($this->requestBuilder, null); } catch (RuntimeException $exception) { self::assertSame('Path parameters cannot be null', $exception->getMessage()); return; } self::fail('Exception was not thrown'); } public function testQueryMapHandler() { $map = [ 'foo' => 'bar', 'afoo[]' => ['baz', 'qux'], 'nfoo' => null, ]; $expected = [ 'foo=bar', 'afoo%5B%5D=baz', 'afoo%5B%5D=qux', ]; (new QueryMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, $map); self::assertAttributeSame($expected, 'queries', $this->requestBuilder); } public function testQueryMapHandlerIterator() { $map = new ArrayIterator([ 'foo' => 'bar', 'afoo[]' => ['baz', 'qux'], 'nfoo' => null, ]); $expected = [ 'foo=bar', 'afoo%5B%5D=baz', 'afoo%5B%5D=qux', ]; (new QueryMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, $map); self::assertAttributeSame($expected, 'queries', $this->requestBuilder); } public function testQueryMapHandlerEmpty() { (new QueryMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, []); self::assertAttributeSame([], 'queries', $this->requestBuilder); } public function testQueryMapHandlerNull() { (new QueryMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'queries', $this->requestBuilder); } public function testQueryNameHandler() { (new QueryNameParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, 'bar'); self::assertAttributeSame(['bar'], 'queries', $this->requestBuilder); } public function testQueryNameHandlerArray() { (new QueryNameParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, ['bar', 'baz']); self::assertAttributeSame(['bar', 'baz'], 'queries', $this->requestBuilder); } public function testQueryNameHandlerNull() { (new QueryNameParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'queries', $this->requestBuilder); } public function testQueryHandler() { (new QueryParamHandler(new DefaultStringConverter(), 'foo', false))->apply($this->requestBuilder, 'bar'); self::assertAttributeSame(['foo=bar'], 'queries', $this->requestBuilder); } public function testQueryHandlerArray() { (new QueryParamHandler(new DefaultStringConverter(), 'foo', false))->apply($this->requestBuilder, ['bar', 'baz']); self::assertAttributeSame(['foo=bar', 'foo=baz'], 'queries', $this->requestBuilder); } public function testQueryHandlerNull() { (new QueryParamHandler(new DefaultStringConverter(), 'foo', false))->apply($this->requestBuilder, null); self::assertAttributeSame([], 'queries', $this->requestBuilder); } public function testUrlHandler() { (new UrlParamHandler(new DefaultStringConverter()))->apply($this->requestBuilder, 'http://example2.com'); self::assertAttributeEquals(new Uri('http://example2.com/test/{path}?q=test'), 'uri', $this->requestBuilder); } public function testUrlHandlerNull() { try { (new UrlParamHandler(new DefaultStringConverter()))->apply($this->requestBuilder, null); } catch (RuntimeException $exception) { self::assertSame('Url parameters cannot be null', $exception->getMessage()); return; } self::fail('Exception was not thrown'); } public function testUsingMapAsListThrowsException() { $map = ['foo' => ['foo' => 'bar']]; try { (new FieldMapParamHandler(new DefaultStringConverter(), false))->apply($this->requestBuilder, $map); } catch (RunTimeException $exception) { self::assertSame('Retrofit: Array value must use numeric keys', $exception->getMessage()); return; } self::fail('Exception was not thrown'); } } ================================================ FILE: tests/Unit/Internal/RequestBuilderTest.php ================================================ build(); self::assertSame('GET', $request->getMethod()); self::assertSame('http://example.com/test?q=test', (string)$request->getUri()); self::assertSame(['Host' => ['example.com']], $request->getHeaders()); self::assertSame('', (string)$request->getBody()); } public function testSetBody() { $stream = new AppendStream(); $requestBuilder = new RequestBuilder('POST', 'http://example.com', '/test?q=test', []); $requestBuilder->setBody($stream); $request = $requestBuilder->build(); self::assertSame('POST', $request->getMethod()); self::assertSame('http://example.com/test?q=test', (string)$request->getUri()); self::assertSame(['Host' => ['example.com']], $request->getHeaders()); self::assertSame($stream, $request->getBody()); } public function testUrlSetters() { $requestBuilder = new RequestBuilder('GET', 'http://example.com', '/{part}?q=test', []); $requestBuilder->setBaseUrl('https://example2.com'); $requestBuilder->replacePath('part', 'test'); $requestBuilder->addQuery('q2', 'test2', false); $requestBuilder->addQueryName('contains(foo)', false); $request = $requestBuilder->build(); self::assertSame('GET', $request->getMethod()); self::assertSame('https://example2.com/test?q2=test2&contains%28foo%29&q=test', (string)$request->getUri()); self::assertSame(['Host' => ['example2.com']], $request->getHeaders()); self::assertSame('', (string)$request->getBody()); } public function testUrlSettersEncoded() { $requestBuilder = new RequestBuilder('GET', 'http://example.com', '/{part}', []); $requestBuilder->setBaseUrl('https://example2.com'); $requestBuilder->replacePath('part', 'test'); $requestBuilder->addQuery('q2', 'test2', true); $requestBuilder->addQueryName('contains%28foo%29', true); $request = $requestBuilder->build(); self::assertSame('GET', $request->getMethod()); self::assertSame('https://example2.com/test?q2=test2&contains%28foo%29', (string)$request->getUri()); self::assertSame(['Host' => ['example2.com']], $request->getHeaders()); self::assertSame('', (string)$request->getBody()); } public function testAddHeader() { $requestBuilder = new RequestBuilder('GET', 'http://example.com', '/test?q=test', []); $requestBuilder->addHeader('foo', 'bar'); $request = $requestBuilder->build(); self::assertSame('GET', $request->getMethod()); self::assertSame('http://example.com/test?q=test', (string)$request->getUri()); self::assertSame(['Host' => ['example.com'], 'foo' => ['bar']], $request->getHeaders()); self::assertSame('', (string)$request->getBody()); } public function testFields() { $requestBuilder = new RequestBuilder('POST', 'http://example.com', '/', []); $requestBuilder->addField('foo', 'bar', false); $requestBuilder->addField('foo()', 'bar()', false); $request = $requestBuilder->build(); self::assertSame('POST', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame(['Host' => ['example.com']], $request->getHeaders()); self::assertSame('foo=bar&foo%28%29=bar%28%29', (string)$request->getBody()); } public function testFieldsEncoded() { $requestBuilder = new RequestBuilder('POST', 'http://example.com', '/', []); $requestBuilder->addField('foo', 'bar', true); $requestBuilder->addField('foo()', 'bar%28%29', true); $request = $requestBuilder->build(); self::assertSame('POST', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame(['Host' => ['example.com']], $request->getHeaders()); self::assertSame('foo=bar&foo%28%29=bar%28%29', (string)$request->getBody()); } public function testParts() { $stream = stream_for('foo'); $requestBuilder = new RequestBuilder('POST', 'http://example.com', '/', []); $requestBuilder->addPart('foo', $stream); $request = $requestBuilder->build(); self::assertSame('POST', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame(['Host' => ['example.com']], $request->getHeaders()); self::assertNotFalse(strpos((string)$request->getBody(), 'Content-Disposition: form-data; name="foo"')); self::assertNotFalse(strpos((string)$request->getBody(), 'Content-Length: 3')); } public function testPartsFilenameAndHeader() { $stream = stream_for('foo'); $requestBuilder = new RequestBuilder('POST', 'http://example.com', '/', []); $requestBuilder->addPart('foo', $stream, ['foo' => 'bar'], 'Test.php'); $request = $requestBuilder->build(); self::assertSame('POST', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame(['Host' => ['example.com']], $request->getHeaders()); self::assertNotFalse(strpos((string)$request->getBody(), 'foo: bar')); self::assertNotFalse(strpos((string)$request->getBody(), 'Content-Disposition: form-data; name="foo"; filename="Test.php"')); self::assertNotFalse(strpos((string)$request->getBody(), 'Content-Length: 3')); } public function testFieldAndBody() { $requestBuilder = new RequestBuilder('POST', 'http://example.com', '/test?q=test', []); $requestBuilder->setBody(new AppendStream()); $requestBuilder->addField('foo', 'bar', false); try { $requestBuilder->build(); } catch (LogicException $exception) { self::assertSame('Retrofit: Cannot mix @Field and @Body annotations.', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testPartAndBody() { $requestBuilder = new RequestBuilder('POST', 'http://example.com', '/test?q=test', []); $requestBuilder->setBody(new AppendStream()); $requestBuilder->addPart('foo', new AppendStream()); try { $requestBuilder->build(); } catch (LogicException $exception) { self::assertSame('Retrofit: Cannot mix @Part and @Body annotations.', $exception->getMessage()); return; } self::fail('Exception not thrown'); } } ================================================ FILE: tests/Unit/Internal/RetrofitResponseTest.php ================================================ raw()); self::assertSame(200, $retrofitResponse->code()); self::assertSame('OK', $retrofitResponse->message()); self::assertSame([], $retrofitResponse->headers()); self::assertTrue($retrofitResponse->isSuccessful()); self::assertSame($responseBody, $retrofitResponse->body()); self::assertNull($retrofitResponse->errorBody()); } public function testGettersFailure() { $response = new Response(500); $responseBody = new AppendStream(); $retrofitResponse = new RetrofitResponse($response, null, $responseBody); self::assertSame($response, $retrofitResponse->raw()); self::assertSame(500, $retrofitResponse->code()); self::assertSame('Internal Server Error', $retrofitResponse->message()); self::assertSame([], $retrofitResponse->headers()); self::assertFalse($retrofitResponse->isSuccessful()); self::assertNull($retrofitResponse->body()); self::assertSame($responseBody, $retrofitResponse->errorBody()); } } ================================================ FILE: tests/Unit/Internal/ServiceMethod/DefaultServiceMethodBuilderTest.php ================================================ serviceMethodBuilder = new DefaultServiceMethodBuilder(); } public function testCreateServiceMethodGet() { $this->serviceMethodBuilder->setMethod('get'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar?q=test'); $this->serviceMethodBuilder->setCallAdapter(new DefaultCallAdapter()); $this->serviceMethodBuilder->setResponseBodyConverter(new DefaultResponseBodyConverter()); $this->serviceMethodBuilder->setErrorBodyConverter(new DefaultResponseBodyConverter()); $serviceMethod = $this->serviceMethodBuilder->build(); self::assertAttributeSame('GET', 'method', $serviceMethod); self::assertAttributeSame('http://example.com', 'baseUrl', $serviceMethod); self::assertAttributeSame('/foo/bar?q=test', 'path', $serviceMethod); } public function testCreateServiceMethodPost() { $paramHandler = new BodyParamHandler(new DefaultRequestBodyConverter()); $this->serviceMethodBuilder->setMethod('post'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar?q=test'); $this->serviceMethodBuilder->setIsJson(); $this->serviceMethodBuilder->addParameterHandler(0, $paramHandler); $this->serviceMethodBuilder->setCallAdapter(new DefaultCallAdapter()); $this->serviceMethodBuilder->setResponseBodyConverter(new DefaultResponseBodyConverter()); $this->serviceMethodBuilder->setErrorBodyConverter(new DefaultResponseBodyConverter()); $serviceMethod = $this->serviceMethodBuilder->build(); self::assertAttributeSame('POST', 'method', $serviceMethod); self::assertAttributeSame('http://example.com', 'baseUrl', $serviceMethod); self::assertAttributeSame('/foo/bar?q=test', 'path', $serviceMethod); self::assertAttributeSame(['content-type' => ['application/json']], 'headers', $serviceMethod); self::assertAttributeSame([$paramHandler], 'parameterHandlers', $serviceMethod); } public function testCreateServiceMethodForm() { $paramHandler = new FieldParamHandler(new DefaultStringConverter(), 'foo', false); $this->serviceMethodBuilder->setMethod('post'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar?q=test'); $this->serviceMethodBuilder->setIsFormUrlEncoded(); $this->serviceMethodBuilder->addParameterHandler(0, $paramHandler); $this->serviceMethodBuilder->setCallAdapter(new DefaultCallAdapter()); $this->serviceMethodBuilder->setResponseBodyConverter(new DefaultResponseBodyConverter()); $this->serviceMethodBuilder->setErrorBodyConverter(new DefaultResponseBodyConverter()); $serviceMethod = $this->serviceMethodBuilder->build(); self::assertAttributeSame('POST', 'method', $serviceMethod); self::assertAttributeSame('http://example.com', 'baseUrl', $serviceMethod); self::assertAttributeSame('/foo/bar?q=test', 'path', $serviceMethod); self::assertAttributeSame(['content-type' => ['application/x-www-form-urlencoded']], 'headers', $serviceMethod); self::assertAttributeSame([$paramHandler], 'parameterHandlers', $serviceMethod); } public function testCreateServiceMethodMultipart() { $paramHandler = new PartParamHandler(new DefaultRequestBodyConverter(), 'foo', 'binary'); $this->serviceMethodBuilder->setMethod('post'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar?q=test'); $this->serviceMethodBuilder->setIsMultipart(); $this->serviceMethodBuilder->addParameterHandler(0, $paramHandler); $this->serviceMethodBuilder->setCallAdapter(new DefaultCallAdapter()); $this->serviceMethodBuilder->setResponseBodyConverter(new DefaultResponseBodyConverter()); $this->serviceMethodBuilder->setErrorBodyConverter(new DefaultResponseBodyConverter()); $serviceMethod = $this->serviceMethodBuilder->build(); self::assertAttributeSame('POST', 'method', $serviceMethod); self::assertAttributeSame('http://example.com', 'baseUrl', $serviceMethod); self::assertAttributeSame('/foo/bar?q=test', 'path', $serviceMethod); self::assertAttributeSame(['content-type' => ['multipart/form-data']], 'headers', $serviceMethod); self::assertAttributeSame([$paramHandler], 'parameterHandlers', $serviceMethod); } public function testSetMethodTwice() { $this->serviceMethodBuilder->setMethod('get'); try { $this->serviceMethodBuilder->setMethod('post'); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Only one http method is allowed. Trying to set POST, but GET already exists', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testSetHasBodyAfterSet() { $this->serviceMethodBuilder->setIsJson(); try { $this->serviceMethodBuilder->setHasBody(false); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Body cannot be changed after it has been set. This indicates a conflict between ' . 'HTTP Request annotations, body annotations, and request type annotations. For example, ' . '@GET cannot be used with @Body, @Field, or @Part annotations', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testChangeContentType() { $this->serviceMethodBuilder->setIsJson(); try { $this->serviceMethodBuilder->setIsFormUrlEncoded(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Content type cannot be changed after it has been set. This indicates a conflict between ' . 'HTTP Request annotations, body annotations, and request type annotations. For example, ' . '@GET cannot be used with @Body, @Field, or @Part annotations', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutMethod() { try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot build service method without HTTP method. Please specify @GET, @POST, etc', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutBaseUrl() { $this->serviceMethodBuilder->setMethod('GET'); try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot build service method without base url. Please specify on RetrofitBuilder', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutPath() { $this->serviceMethodBuilder->setMethod('GET'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot build service method without HTTP method. Please specify @GET, @POST, etc', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutContentType() { $this->serviceMethodBuilder->setMethod('POST'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar'); $this->serviceMethodBuilder->setHasBody(true); try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot build service method with body and no content type. Set one using @Body, ' . '@Field, or @Part', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutBody() { $this->serviceMethodBuilder->setMethod('POST'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar'); $this->serviceMethodBuilder->setContentType('application/json'); try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot set a content-type without a body. This indicates a conflict between ' . 'HTTP Request annotations, body annotations, and request type annotations. For example, ' . '@GET cannot be used with @Body, @Field, or @Part annotations', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutResponseConverter() { $this->serviceMethodBuilder->setMethod('POST'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar'); $this->serviceMethodBuilder->setIsJson(); try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot build service method without response body converter', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutErrorConverter() { $this->serviceMethodBuilder->setMethod('POST'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar'); $this->serviceMethodBuilder->setIsJson(); $this->serviceMethodBuilder->setResponseBodyConverter(new DefaultResponseBodyConverter()); try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot build service method without error body converter', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWithoutCallAdapter() { $this->serviceMethodBuilder->setMethod('POST'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar'); $this->serviceMethodBuilder->setIsJson(); $this->serviceMethodBuilder->setResponseBodyConverter(new DefaultResponseBodyConverter()); $this->serviceMethodBuilder->setErrorBodyConverter(new DefaultResponseBodyConverter()); try { $this->serviceMethodBuilder->build(); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Cannot build service method without call adapter', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testBuildWillNotOverrideContentType() { $this->serviceMethodBuilder->setMethod('POST'); $this->serviceMethodBuilder->setBaseUrl('http://example.com'); $this->serviceMethodBuilder->setPath('/foo/bar'); $this->serviceMethodBuilder->setHasBody(true); $this->serviceMethodBuilder->setContentType('foo'); $this->serviceMethodBuilder->setResponseBodyConverter(new DefaultResponseBodyConverter()); $this->serviceMethodBuilder->setErrorBodyConverter(new DefaultResponseBodyConverter()); $this->serviceMethodBuilder->setCallAdapter(new DefaultCallAdapter()); $serviceMethod = $this->serviceMethodBuilder->build(); self::assertAttributeSame(['content-type' => ['foo']], 'headers', $serviceMethod); } } ================================================ FILE: tests/Unit/Internal/ServiceMethod/ServiceMethodFactoryTest.php ================================================ serviceMethodFactory = new ServiceMethodFactory( new AnnotationProcessor([ GET::class => new HttpRequestAnnotHandler(), Body::class => new BodyAnnotHandler(), ]), new CallAdapterProvider([new DefaultCallAdapterFactory()]), new ConverterProvider([new DefaultConverterFactory(), new ServiceMethodFactoryTestConverterFactory()]), new AnnotationReaderAdapter(new AnnotationReader(), CacheProvider::createNullCache()), 'http://example.com' ); } public function testCreateServiceMethod() { $serviceMethod = $this->serviceMethodFactory->create(ServiceMethodFactoryTestClient::class, 'foo'); self::assertAttributeSame('GET', 'method', $serviceMethod); self::assertAttributeSame('http://example.com', 'baseUrl', $serviceMethod); self::assertAttributeSame('/foo', 'path', $serviceMethod); self::assertAttributeSame([], 'headers', $serviceMethod); self::assertAttributeSame([], 'parameterHandlers', $serviceMethod); self::assertAttributeEquals(new DefaultResponseBodyConverter(), 'responseBodyConverter', $serviceMethod); self::assertAttributeEquals(new DefaultResponseBodyConverter(), 'errorBodyConverter', $serviceMethod); } public function testCreateServiceMethodCustomConverters() { $serviceMethod = $this->serviceMethodFactory->create(ServiceMethodFactoryTestClient::class, 'qux'); self::assertAttributeSame('GET', 'method', $serviceMethod); self::assertAttributeSame('http://example.com', 'baseUrl', $serviceMethod); self::assertAttributeSame('/', 'path', $serviceMethod); self::assertAttributeSame([], 'headers', $serviceMethod); self::assertAttributeSame([], 'parameterHandlers', $serviceMethod); self::assertAttributeNotEquals(new DefaultResponseBodyConverter(), 'responseBodyConverter', $serviceMethod); self::assertAttributeNotEquals(new DefaultResponseBodyConverter(), 'errorBodyConverter', $serviceMethod); } public function testCreateServiceMethodNoReturnType() { try { $this->serviceMethodFactory->create(ServiceMethodFactoryTestClient::class, 'bar'); } catch (LogicException $exception) { self::assertSame( 'Retrofit: All service methods must contain a return type. None found for ' . 'Tebru\Retrofit\Test\Mock\Unit\Internal\ServiceMethod\ServiceMethodFactoryTest\ServiceMethodFactoryTestClient::bar()', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testCreateServiceMethodAnnotationException() { try { $this->serviceMethodFactory->create(ServiceMethodFactoryTestClient::class, 'baz'); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Body cannot be changed after it has been set. This indicates a conflict between HTTP Request ' . 'annotations, body annotations, and request type annotations. For example, @GET cannot be used with ' . '@Body, @Field, or @Part annotations for ' . 'Tebru\Retrofit\Test\Mock\Unit\Internal\ServiceMethod\ServiceMethodFactoryTest\ServiceMethodFactoryTestClient::baz()', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } } ================================================ FILE: tests/Unit/Internal/ServiceMethod/ServiceMethodTest.php ================================================ serviceMethod = new DefaultServiceMethod( 'POST', 'http://example.com', '/foo/bar?q=test', ['content-type' => ['application/json']], [new BodyParamHandler(new DefaultRequestBodyConverter())], new DefaultCallAdapter(), new DefaultResponseBodyConverter(), new DefaultResponseBodyConverter() ); } public function testCreateRequest() { $body = new AppendStream(); $request = $this->serviceMethod->toRequest([$body]); $expected = new Request( 'POST', 'http://example.com/foo/bar?q=test', ['content-type' => ['application/json']], $body ); self::assertEquals($expected, $request); } public function testCreateRequestDifferentParameters() { try { $this->serviceMethod->toRequest([]); } catch (LogicException $exception) { self::assertSame( 'Retrofit: Incompatible number of arguments. Expected 1 and got 0. This either ' . 'means that the service method was not called with the correct number of parameters, ' . 'or there is not an annotation for every parameter.', $exception->getMessage() ); return; } self::fail('Exception not thrown'); } public function testGetResponseBody() { $body = new AppendStream(); $response = new Response(200, [], $body); $result = $this->serviceMethod->toResponseBody($response); self::assertSame($body, $result); } public function testGetErrorBody() { $body = new AppendStream(); $response = new Response(200, [], $body); $result = $this->serviceMethod->toErrorBody($response); self::assertSame($body, $result); } public function testAdaptCall() { $call = new MockCall(); $result = $this->serviceMethod->adapt($call); self::assertSame($call, $result); } } ================================================ FILE: tests/Unit/RetrofitTest.php ================================================ httpClient = new RetrofitTestHttpClient(); $this->retrofitBuilder = Retrofit::builder() ->setBaseUrl('http://example.com') ->setHttpClient($this->httpClient); } public function testSimple() { $retrofit = $this->retrofitBuilder->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $service->get()->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('GET', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); } public function testUri() { $retrofit = $this->retrofitBuilder->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $service ->uri( 'https://example2.com', ['foo' => 'bar'], ['one', 2, true], false, 'testpart' ) ->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('OPTIONS', $request->getMethod()); self::assertSame('https://example2.com/testpart?foo=bar&query[]=one&query[]=2&query[]=true&false&q=test', rawurldecode((string)$request->getUri())); self::assertSame('', (string)$request->getBody()); } public function testHeaders() { $retrofit = $this->retrofitBuilder->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $service ->headers( ['X-Header[]' => ['one', 2, false]], [true, 3.14], 5 ) ->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('HEAD', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); self::assertSame( [ 'Host' => ['example.com'], 'x-foo' => ['bar'], 'x-baz' => ['qux'], 'x-header[]' => ['first', 'one', '2', 'false', 'true', '3.14'], 'header2' => ['5'] ], $request->getHeaders() ); } public function testPostWithoutBody() { $retrofit = $this->retrofitBuilder->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $service->postWithoutBody()->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('POST', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); } public function testBody() { $retrofit = $this->retrofitBuilder ->addConverterFactory(new RetrofitTestConverterFactory()) ->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $body = new RetrofitTestRequestBodyMock(); $body->id = 1; $body->name = 'Nate'; $responseBody = $service->body($body)->execute()->body(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('PUT', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('{"id":1,"name":"Nate"}', (string)$request->getBody()); self::assertSame(['Host' => ['example.com'], 'content-type' => ['application/json']], $request->getHeaders()); self::assertInstanceOf(RetrofitTestResponseBodyMock::class, $responseBody); } public function testField() { $retrofit = $this->retrofitBuilder->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $service->field(5.3, false, 'foo%28%29', ['foo' => 'bar'])->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('PATCH', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('field1=5.3&field2=false&field3=foo%28%29&foo=bar', (string)$request->getBody()); self::assertSame(['Host' => ['example.com'], 'content-type' => ['application/x-www-form-urlencoded']], $request->getHeaders()); } public function testPart() { $retrofit = $this->retrofitBuilder ->addConverterFactory(new RetrofitTestConverterFactory()) ->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $body = new RetrofitTestRequestBodyMock(); $body->id = 1; $body->name = 'Nate'; $multipartRequestBody = new RetrofitTestRequestBodyMock(); $multipartRequestBody->id = 2; $multipartRequestBody->name = 'Mike'; $multipartBody = new MultipartBody('foo', 'bar'); $service->part($body, $multipartBody, ['baz' => $multipartRequestBody])->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('FOO', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame(['Host' => ['example.com'], 'content-type' => ['multipart/form-data']], $request->getHeaders()); self::assertNotFalse(strpos( (string)$request->getBody(), 'Content-Disposition: form-data; name="part1"' )); self::assertNotFalse(strpos( (string)$request->getBody(), 'Content-Disposition: form-data; name="foo"' )); self::assertNotFalse(strpos( (string)$request->getBody(), 'Content-Disposition: form-data; name="baz"' )); self::assertNotFalse(strpos( (string)$request->getBody(), '{"id":1,"name":"Nate"}' )); self::assertNotFalse(strpos( (string)$request->getBody(), '{"id":2,"name":"Mike"}' )); } public function testCallAdapter() { $retrofit = $this->retrofitBuilder ->addCallAdapterFactory(new RetrofitTestCallAdapterFactory()) ->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $adaptedCall = $service->callAdapter(); self::assertInstanceOf(RetrofitTestAdaptedCallMock::class, $adaptedCall); } public function testCustomProxy() { $retrofit = $this->retrofitBuilder ->addProxyFactory(new RetrofitTestProxyFactory()) ->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $service->get()->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('GET', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); } public function testCustomAnnotation() { $retrofit = $this->retrofitBuilder ->addAnnotationHandler(RetrofitTestCustomAnnotation::class, new RetrofitTestCustomAnnotationHandler()) ->build(); /** @var ApiClient $service */ $service = $retrofit->create(ApiClient::class); $service->customAnnotation()->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('DELETE', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); self::assertSame(['Host' => ['example.com'], 'foo' => ['bar']], $request->getHeaders()); } public function testGetWithDefaults() { $retrofit = $this->retrofitBuilder->build(); /** @var DefaultParamsApiClient $service */ $service = $retrofit->create(DefaultParamsApiClient::class); $service->getWithDefaults()->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('GET', $request->getMethod()); self::assertSame('http://example.com/?string=test&bool=true&int=1&float=3.2', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); self::assertSame(['Host' => ['example.com'], 'test' => ['value']], $request->getHeaders()); } public function testGetWithSomeDefaults() { $retrofit = $this->retrofitBuilder->build(); /** @var DefaultParamsApiClient $service */ $service = $retrofit->create(DefaultParamsApiClient::class); $service->getWithDefaults('test2', false)->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('GET', $request->getMethod()); self::assertSame('http://example.com/?string=test2&bool=false&int=1&float=3.2', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); self::assertSame(['Host' => ['example.com'], 'test' => ['value']], $request->getHeaders()); } public function testGetWithNullDefaults() { $retrofit = $this->retrofitBuilder->build(); /** @var DefaultParamsApiClient $service */ $service = $retrofit->create(DefaultParamsApiClient::class); $service->getWithDefaults(null, null, null, null, null, null)->execute(); self::assertCount(1, $this->httpClient->requests); $request = $this->httpClient->requests[0]; self::assertSame('GET', $request->getMethod()); self::assertSame('http://example.com/', (string)$request->getUri()); self::assertSame('', (string)$request->getBody()); self::assertSame(['Host' => ['example.com']], $request->getHeaders()); } public function testCache() { $cacheDir = __DIR__.'/../cache'; $file = $cacheDir.'/retrofit/Tebru/Retrofit/Test/Mock/Unit/RetrofitTest/CacheableApiClient.php'; if (file_exists($file)) { $success = unlink($file); if (!$success) { throw new RuntimeException('Could not cleanup test'); } } $retrofit = $this->retrofitBuilder ->enableCache() ->setCacheDir($cacheDir) ->build(); /** @var CacheableApiClient $service */ $service = $retrofit->create(CacheableApiClient::class); $service->get()->execute(); self::assertFileExists($file); unlink($file); } public function testCustomCache() { $cache = CacheProvider::createMemoryCache(); $retrofit = $this->retrofitBuilder ->setCache($cache) ->build(); /** @var CacheableApiClient $service */ $service = $retrofit->create(CacheableApiClient::class); $service->get()->execute(); $annotation = new GET(['value' => '/']); self::assertEquals([GET::class => $annotation], $cache->get('annotationreader.TebruRetrofitTestMockUnitRetrofitTestCacheableApiClientget')); } public function testBuilderThrowsExceptionWithoutBaseUrl() { try { Retrofit::builder()->build(); } catch (LogicException $exception) { self::assertSame('Retrofit: Base URL must be provided', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testBuilderThrowsExceptionWithoutHttpClient() { try { Retrofit::builder() ->setBaseUrl('http://example.com') ->build(); } catch (LogicException $exception) { self::assertSame('Retrofit: Must set http client to make requests', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testBuilderThrowsExceptionWithoutCacheDir() { try { $this->retrofitBuilder->enableCache()->build(); } catch (LogicException $exception) { self::assertSame('Retrofit: If caching is enabled, must specify cache directory', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testCreateServices() { $retrofit = $this->retrofitBuilder->build(); $retrofit->registerServices([ApiClient::class]); self::assertSame(1, $retrofit->createServices()); } public function testCreateAll() { $retrofit = $this->retrofitBuilder->build(); self::assertSame(4, $retrofit->createAll(__DIR__.'/../Mock/Unit/RetrofitTest/')); } public function testCreateThrowsExceptionWithoutFactory() { $retrofit = new Retrofit(new ServiceResolver(), []); try { $retrofit->create(ApiClient::class); } catch (LogicException $exception) { self::assertSame('Retrofit: Could not find a proxy factory for Tebru\Retrofit\Test\Mock\Unit\RetrofitTest\ApiClient', $exception->getMessage()); return; } self::fail('Exception not thrown'); } public function testCreateThrowsExceptionWithInvalidHeaderSyntax() { $retrofit = $this->retrofitBuilder->build(); /** @var InvalidSyntaxApiClient $service */ $service = $retrofit->create(InvalidSyntaxApiClient::class); try { $service->get(); } catch (RuntimeException $exception) { self::assertSame('Retrofit: Header in an incorrect format. Expected "Name: value"', $exception->getMessage()); return; } self::fail('Exception not thrown'); } } ================================================ FILE: tests/bootstrap.php ================================================