Small PSR7 compatible library to perform http requests based on pre-configured set of requests
# requests.yml
my_cool_api:
endpoint: http://my_cool_api.com/v2
requests:
get_user:
path: /user/${USER_ID}
create_user:
path: /user/${USER_ID}
method: POST
headers:
Content-Type: application/json
body:
name: ${NAME}
email: ${EMAIL}
$coolApi = ClientBuilder::create()
->withYamlConfig('requests.yml')
->build('my_cool_api');
$user = $client->json('get_user', ['user_id' => $userId]);
echo "Hello ".$user->name;
Require the package as usual
composer require pluggit/http-client
To use Guzzle as a sender (the default option), you have to require it manually
composer require guzzlehttp/guzzle "^6.0"
php >=5.5
guzzlehttp/psr7 ^1.0
psr/log ^1.0
Note: This package relies on the implementation of guzzle for the PSR-7: HTTP message interfaces
The library allows to use different senders to execute requests; and adapter for guzzlehttp/guzzle
it's included in the library but you must require the dependency manually
composer require guzzlehttp/guzzle "^6.0"
If you want to use a Yaml file as a configuration source you can:
a) Use you own parser and pass the config as an array
b) Require symfony/yaml
as dependency and pass the yaml filepath to the builder
composer require symfony/yaml "^3.1"
Do you want to know why your request is failing? No problem, the library accepts any Psr-3 logger logger implementation (like Monolog) to write debug and error messages.
For a quick start you can use Symfony's console package to have debug messages in the console
composer require symfony/console "^3.1"
You will now be able to activate the console debug mode
$builder->withConsoleDebug();
If you use pimple for building your dependencies you can use the provider Cmp\Http\Provider\HttpClientServiceProvider
to register the builder and the multi client.
composer require pimple/pimple "^3.0"
This library has been tested with the following PHP versions and setups
PHP | guzzlehttp/psr7 | guzzlehttp/guzzle | result |
---|---|---|---|
5.5.25 | 1.3.1 | 6.2.2 | âś… |
5.6.16 | 1.3.1 | 6.2.2 | âś… |
7.0.7 | 1.3.1 | 6.2.2 | âś… |
hhvm | 1.3.1 | 6.2.2 | âś… |
The library allows to build requests from a set configuration values, execute them trough the client and get a response
The configuration is what tells the client how to build the requests, let's see some examples of configuration
my_users_api:
endpoint: https://api.mysite.com
headers:
api_version: 1.2
options:
timeout: 2
requests:
list_users:
path: /users
options:
timeout: 15
get_user:
path: /users/${USER_ID}
create_user:
path: /users/${USER_ID}
headers:
- Content-Type: application/json
method: POST
# Another service
the_comments_api:
endpoint: http://messaging_service.com/v2/
...
The first line defines a name for a service, in this case my_users_api
NOTE: You can see a full-fledged sample configuration file in config-sample/requests.yaml
for working with a REST API
endpoint
(required): This is the endpoint for the all the request for this serviceheaders
: A key-value array with the headers to add to all the requests for this servicequery
: A key-value array with the query parameters to append in the URI of all the requests for this servicebody
: A key-value array with the post parameters to send in the body of all the requests for this serviceversion
: HTTP protocol version, default one1.1
options
: A key-value array with options to pass to the http client sender to modify it's behaviourrequests
: A key-value array with the allowed requests for this service
The request configuration allows the set almost the same options as the service configuration, overwriting the values provided for the service, this allows to define a general behaviour for a service but tweak some options for specific requests, like for example, allowing a longer timeout
path
(required): Defines the path that follows the endpointmethod
: Defines the HTTP method for the request.GET
by defaultheaders
: A key-value array with the headers to add to this requestquery
: A key-value array with the query parameters to append in the URI of this requestbody
: A key-value array with the post parameters to send in the body of this requestversion
: HTTP protocol version, default one1.1
options
: A key-value array with options to pass to the http client sender to modify it's behaviour
The request options allows to customize the client behaviour without the library having to know specific details about how to handle them.
Taking the provided integration with Guzzle as examples, we could have:
- Basic auth:
auth: ['myUser', 'myPass']
- Timeout:
timeout: 2
- Connect timeout:
timeout: 15
- Add a certificate:
['cert' => ['/path/server.pem', 'password']]
The library allows to indicate the presence of dynamic values in this configuration parameters as placeholders
- Placeholders are allowed in path, query, headers and body only
- Use this format to add a placeholder:
${PLACEHOLDER_NAME}
- Placeholders tag names must be uppercase (although lowercase keys can be used when replacing them)
- You can have the same placeholder multiple times across all options, it will be replaced in all of them.
To replace the placeholders, pass the values as paramters when creating the request
secure_api:
endpoint: https://topsecret.com
headers:
api_key: MyPersonalApiKey
token: ${TOKEN}
requests:
get_user:
path: /users/${USER_ID}
$request = $client->create('secure_api', 'get_user', [
'token' => $oath->sign($secret),
'user_id' => $userId
]);
The builder (Cmp\Http\ClientBuilder
) is an object that will help you create you client, easing the customization process. This are the options available:
$builder = ClientBuilder::create()
->withConfig($requests)
->withGuzzleSender($myCustomGuzzleClient)
->withConsoleDebug();
Once you've configured the builder you're ready to create clients for your http services
There are 2 different clients that allow to create and execute requests:
Cmp\Http\Client\MultiClientInterface
: This client has access to all servicesCmp\Http\Client\ServiceClientInterface
: This client can execute request from a single service only
NOTE: You should try to inject always a ServiceClientInterface
in your services, this will prevent sideeffects triggering requests from other services by mistake
The available methods in the clients are:
create
: Creates a request (Cmp\Http\Message\Request
)send
: Sends a request returning aCmp\Http\Message\Response
They also provide some shortcuts to ease the use:
execute
: Creates and sends a request in a single calljson
: Creates and sends a request parsing a json responsejsonAsArray
: Creates and sends a request parsing as an array from a json response
The easiest way to built the client is to use the method build
on the builder
$client = $builder->build();
To build a service client specify the name of the service when building the client in the last step of the builder
$myServiceApi = $builder->build('my_service');
The client require 3 dependencies to work, the builder has methods to override all of them with custom implementations
This is the class that sends the requests, it has to implement the Cmp\Http\Sender\SenderInterface
and return a Psr-7 compatible response
The builder will try to use the provided Cmp\Http\Sender\GuzzleSender
by default, to specify a different one use:
$builder->withSender($sender);
Even if you want to use guzzle, ou can also customize the internal client used
// tweak the client
$customGuzzleClient = new Guzzlehttp\Guzzle();
// Pass it to the builder
$builder->withGuzzleSender($customGuzzleClient);
This is the only required dependency, a configuration defining the available the requests
$builder->withConfig($config);
If symfony/yaml
is installed, you can pass a filepath with a yaml configuration
$builder->withYamlConfig($yamlFile);
You can pass a Psr\Log\LoggerInterface
to receive debug and error messages
$builder->withLogger($logger);
If you want to debug the raw http request/responses in the console you can activate the debug output in the builder (You'll need to have symfony/console
installed)
$builder->withConsoleDebug();
To create a request you need to identify the service and the request that you want. Additionally you can pass dynamic values to substitute placeholders
$request = $client->request('weather', 'forecast', ['city' => $city]);
// Service specific clients do not need the service name
$request = $weather->request('forecast', ['city' => $city]);
The requests created are Cmp\Http\Message\Request
intances, implementing Psr\Http\Message\RequestInterface
, making them suitable to share between libraries and frameworks
The library request object provides some helpers to work with them in an easier way:
withQueryParameter($key, $value)
: Allows you to add or modify a query parameterwithPost(array $params)
: Allows to pass a key-value array of params to send as bodywithJsonPost(array $params)
: Allows to pass a key-value array of params; it will be codified as json and, the headerContent-Type: application/json
will be added__toString()
: This makes the request embeddable in string
If you provide a request factory to the builder that complais with Cmp\Http\RequestFactoryInterface
, you'll be able to extend the Request class and change the behaviour.
You may ask "hey, why would I like to do this for?"
For example to:
- Change all outgoing endpoints to a dummy server to prevent to executed certain requests on test environments
- Apply a configuration to all requests from all services
$factory = new TestEnvironmentsRequestFactory($config);
$builder->withRequestFactory($factory);
After sending the request you'll receive a Cmp\Http\Message\Response
instance compatible with Psr\Http\Message\ResponseInterface
This response object also provides some helper methods:
json($asArray = true)
: Parses the body as a json an returns either astdClass
object or anarray
jsonAsArray()
: Same as before, but forcing the return type to be an array__toString()
: This makes the response embeddable in string
Not everything works as expected; to make your life easier handling this, the library provides a small set of exceptions:
Cmp\Http\Exception\RuntimeException
: This is the base exception used in the library, all the exceptions thrown within the library code uses or extends this oneCmp\Http\Exception\RequestBuildException
: This exception is thrown when the process of building a request cannot be completedCmp\Http\Exception\RequestExecutionException
: This exception is thrown when some error happens sending the request
I'm going to show you how to perform the most common operation in a REST API.
Lets imagine that we want to interact with an API endpoint to manage our application users.
The user entity looks like:
{
"id": "1",
"first_name": "John",
"last_name": "Doe"
}
Now let's define the typical requests in our configuration file
# Service definition
my_app:
# The endpoint is required
endpoint: https://api.myapp.com/v1
options:
auth: ['apikey', 'secret']
# At least one request is required too
requests:
list_users:
path: /users
get_user:
path: /users/${USER_ID}
delete_user:
path: /users/${USER_ID}
method: DELETE
put_user:
path: /users/${USER_ID}
method: PUT
headers:
Content-Type: application/json
body:
first_name: ${FIRST_NAME}
last_name: ${LAST_NAME}
create_user:
path: /users
method: POST
headers:
Content-Type: application/json
body:
first_name: ${FIRST_NAME}
last_name: ${LAST_NAME}
update_user:
path: /users/${USER_ID}
method: PATCH
headers:
Content-Type: application/json
# body: No need to define all the body params here, we can do it at request time
Let's build a service client for our api, as per the configuration, all the requests will include a basic authentication
$api = $builder->build('my_app');
###List users This call would return an array of users
// We are going to modify the request to pass a limit on the number of users to retrieve
$users = $api->send($api->request('list_users')->withQueryParameter('limit', 25))->json();
This call will inject the user id in the path of the uri to request the user
// The json shortcut creates, executes and parses the body response all in a single call
$users = $api->json('get_user', ['user_id => 1]);
Here we're going to replace the placeholders in the body array, if the api only accepts a json body, we have to add the correct content type header, like in the example so the request is created correctly
$user = $api->json('create_user', ['name' => 'Jane', 'last_name' => 'Roe']);
We can replace placeholders in both path or body params at the same time
$user = $api->json('put_user', ['user_id' => 1, 'name' => 'Jane', 'last_name' => 'Roe']);
When we update some fields only, we don't known in advance what fields we are going to send, so it's better to decide later
$request = $api->create('update_user', ['user_id' => 1]);
// Remember to assign the result, as the request return always a new instance when modifying it
$request = $request->withPost(['name' => 'Jane']);
$updatedUser = $api->send($request)->json();
Deleting a user is even easier
$api->execute('delete_user', ['user_id' => 1]);
The library includes a service provider for Pimple to register a client in an easy way:
Cmp\Http\Provider\HttpClientServiceProvider
The provider will register the builder Cmp\Http\ClientBuilder
object at key http_client.builder
and a general purpouse multi-client Cmp\Http\Client\MultiClient
in http_client.client
, but it also accepts alias for both services
$container->register(new HttpClientServiceProvider('requester', 'client'), [
'http_client.yaml' => 'config/requests.yml',
]);
$builder = $container['requester']; // or http_client.builder
$client = $container['client']; // or http_client.client
The options that the provider accepts are:
http_client.yaml
: A filepath with a ymal configuration filehttp_client.config
: A configuration array with the requestshttp_client.logger
: Pass a Psr\Logger\LoggerInterface object to add logging to the clienthttp_client.sender
: A custom implementation of theCmp\Http\Sender\SenderInterface
interfacehttp_client.guzzle
: A customGuzzleHttp\ClientInterface
object (useful to keep a reference)http_client.factory
: A customCmp\Http\RequestFactoryInterface
implementationhttp_client.debug
: Pass true and this will activate console output for debugging
To build the test environment you'll need docker and docker-compose installed:
make dev
make unit
make integration
You can run the tests only for a php version like this
make unit PHP_VERSION=5.6
make integration PHP_VERSION=5.6
You can build a report for code coverage in HTML format. It will be available in bin/code-coverage
make code-coverage
make nodev
You can delete the docker images for a total clean-up
make nodev IMAGES=true