123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659 |
- ======================
- The web service client
- ======================
- The ``Guzzle\Service`` namespace contains various abstractions that help to make it easier to interact with a web
- service API, including commands, service descriptions, and resource iterators.
- In this chapter, we'll build a simple `Twitter API client <https://dev.twitter.com/docs/api/1.1>`_.
- Creating a client
- =================
- A class that extends from ``Guzzle\Service\Client`` or implements ``Guzzle\Service\ClientInterface`` must implement a
- ``factory()`` method in order to be used with a :doc:`service builder <using-the-service-builder>`.
- Factory method
- --------------
- You can use the ``factory()`` method of a client directly if you do not need a service builder.
- .. code-block:: php
- use mtdowling\TwitterClient;
- // Create a client and pass an array of configuration data
- $twitter = TwitterClient::factory(array(
- 'consumer_key' => '****',
- 'consumer_secret' => '****',
- 'token' => '****',
- 'token_secret' => '****'
- ));
- .. note::
- If you'd like to follow along, here's how to get your Twitter API credentials:
- 1. Visit https://dev.twitter.com/apps
- 2. Click on an application that you've created
- 3. Click on the "OAuth tool" tab
- 4. Copy all of the settings under "OAuth Settings"
- Implementing a factory method
- -----------------------------
- Creating a client and its factory method is pretty simple. You just need to implement ``Guzzle\Service\ClientInterface``
- or extend from ``Guzzle\Service\Client``.
- .. code-block:: php
- namespace mtdowling;
- use Guzzle\Common\Collection;
- use Guzzle\Plugin\Oauth\OauthPlugin;
- use Guzzle\Service\Client;
- use Guzzle\Service\Description\ServiceDescription;
- /**
- * A simple Twitter API client
- */
- class TwitterClient extends Client
- {
- public static function factory($config = array())
- {
- // Provide a hash of default client configuration options
- $default = array('base_url' => 'https://api.twitter.com/1.1');
- // The following values are required when creating the client
- $required = array(
- 'base_url',
- 'consumer_key',
- 'consumer_secret',
- 'token',
- 'token_secret'
- );
- // Merge in default settings and validate the config
- $config = Collection::fromConfig($config, $default, $required);
- // Create a new Twitter client
- $client = new self($config->get('base_url'), $config);
- // Ensure that the OauthPlugin is attached to the client
- $client->addSubscriber(new OauthPlugin($config->toArray()));
- return $client;
- }
- }
- Service Builder
- ---------------
- A service builder is used to easily create web service clients, provides a simple configuration driven approach to
- creating clients, and allows you to share configuration settings across multiple clients. You can find out more about
- Guzzle's service builder in :doc:`using-the-service-builder`.
- .. code-block:: php
- use Guzzle\Service\Builder\ServiceBuilder;
- // Create a service builder and provide client configuration data
- $builder = ServiceBuilder::factory('/path/to/client_config.json');
- // Get the client from the service builder by name
- $twitter = $builder->get('twitter');
- The above example assumes you have JSON data similar to the following stored in "/path/to/client_config.json":
- .. code-block:: json
- {
- "services": {
- "twitter": {
- "class": "mtdowling\\TwitterClient",
- "params": {
- "consumer_key": "****",
- "consumer_secret": "****",
- "token": "****",
- "token_secret": "****"
- }
- }
- }
- }
- .. note::
- A service builder becomes much more valuable when using multiple web service clients in a single application or
- if you need to utilize the same client with varying configuration settings (e.g. multiple accounts).
- Commands
- ========
- Commands are a concept in Guzzle that helps to hide the underlying implementation of an API by providing an easy to use
- parameter driven object for each action of an API. A command is responsible for accepting an array of configuration
- parameters, serializing an HTTP request, and parsing an HTTP response. Following the
- `command pattern <http://en.wikipedia.org/wiki/Command_pattern>`_, commands in Guzzle offer a greater level of
- flexibility when implementing and utilizing a web service client.
- Executing commands
- ------------------
- You must explicitly execute a command after creating a command using the ``getCommand()`` method. A command has an
- ``execute()`` method that may be called, or you can use the ``execute()`` method of a client object and pass in the
- command object. Calling either of these execute methods will return the result value of the command. The result value is
- the result of parsing the HTTP response with the ``process()`` method.
- .. code-block:: php
- // Get a command from the client and pass an array of parameters
- $command = $twitter->getCommand('getMentions', array(
- 'count' => 5
- ));
- // Other parameters can be set on the command after it is created
- $command['trim_user'] = false;
- // Execute the command using the command object.
- // The result value contains an array of JSON data from the response
- $result = $command->execute();
- // You can retrieve the result of the command later too
- $result = $command->getResult().
- Command object also contains methods that allow you to inspect the HTTP request and response that was utilized with
- the command.
- .. code-block:: php
- $request = $command->getRequest();
- $response = $command->getResponse();
- .. note::
- The format and notation used to retrieve commands from a client can be customized by injecting a custom command
- factory, ``Guzzle\Service\Command\Factory\FactoryInterface``, on the client using ``$client->setCommandFactory()``.
- Executing with magic methods
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- When using method missing magic methods with a command, the command will be executed right away and the result of the
- command is returned.
- .. code-block:: php
- $jsonData = $twitter->getMentions(array(
- 'count' => 5,
- 'trim_user' => true
- ));
- Creating commands
- -----------------
- Commands are created using either the ``getCommand()`` method of a client or a magic missing method of a client. Using
- the ``getCommand()`` method allows you to create a command without executing it, allowing for customization of the
- command or the request serialized by the command.
- When a client attempts to create a command, it uses the client's ``Guzzle\Service\Command\Factory\FactoryInterface``.
- By default, Guzzle will utilize a command factory that first looks for a concrete class for a particular command
- (concrete commands) followed by a command defined by a service description (operation commands). We'll learn more about
- concrete commands and operation commands later in this chapter.
- .. code-block:: php
- // Get a command from the twitter client.
- $command = $twitter->getCommand('getMentions');
- $result = $command->execute();
- Unless you've skipped ahead, running the above code will throw an exception.
- PHP Fatal error: Uncaught exception 'Guzzle\Common\Exception\InvalidArgumentException' with message
- 'Command was not found matching getMentions'
- This exception was thrown because the "getMentions" command has not yet been implemented. Let's implement one now.
- Concrete commands
- ~~~~~~~~~~~~~~~~~
- Commands can be created in one of two ways: create a concrete command class that extends
- ``Guzzle\Service\Command\AbstractCommand`` or
- :doc:`create an OperationCommand based on a service description <guzzle-service-descriptions>`. The recommended
- approach is to use a service description to define your web service, but you can use concrete commands when custom
- logic must be implemented for marshaling or unmarshaling a HTTP message.
- Commands are the method in which you abstract away the underlying format of the requests that need to be sent to take
- action on a web service. Commands in Guzzle are meant to be built by executing a series of setter methods on a command
- object. Commands are only validated right before they are executed. A ``Guzzle\Service\Client`` object is responsible
- for executing commands. Commands created for your web service must implement
- ``Guzzle\Service\Command\CommandInterface``, but it's easier to extend the ``Guzzle\Service\Command\AbstractCommand``
- class, implement the ``build()`` method, and optionally implement the ``process()`` method.
- Serializing requests
- ^^^^^^^^^^^^^^^^^^^^
- The ``build()`` method of a command is responsible for using the arguments of the command to build and serialize a
- HTTP request and set the request on the ``$request`` property of the command object. This step is usually taken care of
- for you when using a service description driven command that uses the default
- ``Guzzle\Service\Command\OperationCommand``. You may wish to implement the process method yourself when you aren't
- using a service description or need to implement more complex request serialization.
- .. important::::
- When implementing a custom ``build()`` method, be sure to set the class property of ``$this->request`` to an
- instantiated and ready to send request.
- The following example shows how to implement the ``getMentions``
- `Twitter API <https://dev.twitter.com/docs/api/1.1/get/statuses/mentions_timeline>`_ method using a concrete command.
- .. code-block:: php
- namespace mtdowling\Twitter\Command;
- use Guzzle\Service\Command\AbstractCommand;
- class GetMentions extends AbstractCommand
- {
- protected function build()
- {
- // Create the request property of the command
- $this->request = $this->client->get('statuses/mentions_timeline.json');
- // Grab the query object of the request because we will use it for
- // serializing command parameters on the request
- $query = $this->request->getQuery();
- if ($this['count']) {
- $query->set('count', $this['count']);
- }
- if ($this['since_id']) {
- $query->set('since_id', $this['since_id']);
- }
- if ($this['max_id']) {
- $query->set('max_id', $this['max_id']);
- }
- if ($this['trim_user'] !== null) {
- $query->set('trim_user', $this['trim_user'] ? 'true' : 'false');
- }
- if ($this['contributor_details'] !== null) {
- $query->set('contributor_details', $this['contributor_details'] ? 'true' : 'false');
- }
- if ($this['include_entities'] !== null) {
- $query->set('include_entities', $this['include_entities'] ? 'true' : 'false');
- }
- }
- }
- By default, a client will attempt to find concrete command classes under the ``Command`` namespace of a client. First
- the client will attempt to find an exact match for the name of the command to the name of the command class. If an
- exact match is not found, the client will calculate a class name using inflection. This is calculated based on the
- folder hierarchy of a command and converting the CamelCased named commands into snake_case. Here are some examples on
- how the command names are calculated:
- #. ``Foo\Command\JarJar`` **->** jar_jar
- #. ``Foo\Command\Test`` **->** test
- #. ``Foo\Command\People\GetCurrentPerson`` **->** people.get_current_person
- Notice how any sub-namespace beneath ``Command`` is converted from ``\`` to ``.`` (a period). CamelCasing is converted
- to lowercased snake_casing (e.g. JarJar == jar_jar).
- Parsing responses
- ^^^^^^^^^^^^^^^^^
- The ``process()`` method of a command is responsible for converting an HTTP response into something more useful. For
- example, a service description operation that has specified a model object in the ``responseClass`` attribute of the
- operation will set a ``Guzzle\Service\Resource\Model`` object as the result of the command. This behavior can be
- completely modified as needed-- even if you are using operations and responseClass models. Simply implement a custom
- ``process()`` method that sets the ``$this->result`` class property to whatever you choose. You can reuse parts of the
- default Guzzle response parsing functionality or get inspiration from existing code by using
- ``Guzzle\Service\Command\OperationResponseParser`` and ``Guzzle\Service\Command\DefaultResponseParser`` classes.
- If you do not implement a custom ``process()`` method and are not using a service description, then Guzzle will attempt
- to guess how a response should be processed based on the Content-Type header of the response. Because the Twitter API
- sets a ``Content-Type: application/json`` header on this response, we do not need to implement any custom response
- parsing.
- Operation commands
- ~~~~~~~~~~~~~~~~~~
- Operation commands are commands in which the serialization of an HTTP request and the parsing of an HTTP response are
- driven by a Guzzle service description. Because request serialization, validation, and response parsing are
- described using a DSL, creating operation commands is a much faster process than writing concrete commands.
- Creating operation commands for our Twitter client can remove a great deal of redundancy from the previous concrete
- command, and allows for a deeper runtime introspection of the API. Here's an example service description we can use to
- create the Twitter API client:
- .. code-block:: json
- {
- "name": "Twitter",
- "apiVersion": "1.1",
- "baseUrl": "https://api.twitter.com/1.1",
- "description": "Twitter REST API client",
- "operations": {
- "GetMentions": {
- "httpMethod": "GET",
- "uri": "statuses/mentions_timeline.json",
- "summary": "Returns the 20 most recent mentions for the authenticating user.",
- "responseClass": "GetMentionsOutput",
- "parameters": {
- "count": {
- "description": "Specifies the number of tweets to try and retrieve",
- "type": "integer",
- "location": "query"
- },
- "since_id": {
- "description": "Returns results with an ID greater than the specified ID",
- "type": "integer",
- "location": "query"
- },
- "max_id": {
- "description": "Returns results with an ID less than or equal to the specified ID.",
- "type": "integer",
- "location": "query"
- },
- "trim_user": {
- "description": "Limits the amount of data returned for each user",
- "type": "boolean",
- "location": "query"
- },
- "contributor_details": {
- "description": "Adds more data to contributor elements",
- "type": "boolean",
- "location": "query"
- },
- "include_entities": {
- "description": "The entities node will be disincluded when set to false.",
- "type": "boolean",
- "location": "query"
- }
- }
- }
- },
- "models": {
- "GetMentionsOutput": {
- "type": "object",
- "additionalProperties": {
- "location": "json"
- }
- }
- }
- }
- If you're lazy, you can define the API in a less descriptive manner using ``additionalParameters``.
- ``additionalParameters`` define the serialization and validation rules of parameters that are not explicitly defined
- in a service description.
- .. code-block:: json
- {
- "name": "Twitter",
- "apiVersion": "1.1",
- "baseUrl": "https://api.twitter.com/1.1",
- "description": "Twitter REST API client",
- "operations": {
- "GetMentions": {
- "httpMethod": "GET",
- "uri": "statuses/mentions_timeline.json",
- "summary": "Returns the 20 most recent mentions for the authenticating user.",
- "responseClass": "GetMentionsOutput",
- "additionalParameters": {
- "location": "query"
- }
- }
- },
- "models": {
- "GetMentionsOutput": {
- "type": "object",
- "additionalProperties": {
- "location": "json"
- }
- }
- }
- }
- You should attach the service description to the client at the end of the client's factory method:
- .. code-block:: php
- // ...
- class TwitterClient extends Client
- {
- public static function factory($config = array())
- {
- // ... same code as before ...
- // Set the service description
- $client->setDescription(ServiceDescription::factory('path/to/twitter.json'));
- return $client;
- }
- }
- The client can now use operations defined in the service description instead of requiring you to create concrete
- command classes. Feel free to delete the concrete command class we created earlier.
- .. code-block:: php
- $jsonData = $twitter->getMentions(array(
- 'count' => 5,
- 'trim_user' => true
- ));
- Executing commands in parallel
- ------------------------------
- Much like HTTP requests, Guzzle allows you to send multiple commands in parallel. You can send commands in parallel by
- passing an array of command objects to a client's ``execute()`` method. The client will serialize each request and
- send them all in parallel. If an error is encountered during the transfer, then a
- ``Guzzle\Service\Exception\CommandTransferException`` is thrown, which allows you to retrieve a list of commands that
- succeeded and a list of commands that failed.
- .. code-block:: php
- use Guzzle\Service\Exception\CommandTransferException;
- $commands = array();
- $commands[] = $twitter->getCommand('getMentions');
- $commands[] = $twitter->getCommand('otherCommandName');
- // etc...
- try {
- $result = $client->execute($commands);
- foreach ($result as $command) {
- echo $command->getName() . ': ' . $command->getResponse()->getStatusCode() . "\n";
- }
- } catch (CommandTransferException $e) {
- // Get an array of the commands that succeeded
- foreach ($e->getSuccessfulCommands() as $command) {
- echo $command->getName() . " succeeded\n";
- }
- // Get an array of the commands that failed
- foreach ($e->getFailedCommands() as $command) {
- echo $command->getName() . " failed\n";
- }
- }
- .. note::
- All commands executed from a client using an array must originate from the same client.
- Special command options
- -----------------------
- Guzzle exposes several options that help to control how commands are validated, serialized, and parsed.
- Command options can be specified when creating a command or in the ``command.params`` parameter in the
- ``Guzzle\Service\Client``.
- =========================== ============================================================================================
- command.request_options Option used to add :ref:`Request options <request-options>` to the request created by a
- command
- command.hidden_params An array of the names of parameters ignored by the ``additionalParameters`` parameter schema
- command.disable_validation Set to true to disable JSON schema validation of the command's input parameters
- command.response_processing Determines how the default response parser will parse the command. One of "raw" no parsing,
- "model" (the default method used to parse commands using response models defined in service
- descriptions)
- command.headers (deprecated) Option used to specify custom headers. Use ``command.request_options`` instead
- command.on_complete (deprecated) Option used to add an onComplete method to a command. Use
- ``command.after_send`` event instead
- command.response_body (deprecated) Option used to change the entity body used to store a response.
- Use ``command.request_options`` instead
- =========================== ============================================================================================
- Advanced client configuration
- =============================
- Default command parameters
- --------------------------
- When creating a client object, you can specify default command parameters to pass into all commands. Any key value pair
- present in the ``command.params`` settings of a client will be added as default parameters to any command created
- by the client.
- .. code-block:: php
- $client = new Guzzle\Service\Client(array(
- 'command.params' => array(
- 'default_1' => 'foo',
- 'another' => 'bar'
- )
- ));
- Magic methods
- -------------
- Client objects will, by default, attempt to create and execute commands when a missing method is invoked on a client.
- This powerful concept applies to both concrete commands and operation commands powered by a service description. This
- makes it appear to the end user that you have defined actual methods on a client object, when in fact, the methods are
- invoked using PHP's magic ``__call`` method.
- The ``__call`` method uses the ``getCommand()`` method of a client, which uses the client's internal
- ``Guzzle\Service\Command\Factory\FactoryInterface`` object. The default command factory allows you to instantiate
- operations defined in a client's service description. The method in which a client determines which command to
- execute is defined as follows:
- 1. The client will first try to find a literal match for an operation in the service description.
- 2. If the literal match is not found, the client will try to uppercase the first character of the operation and find
- the match again.
- 3. If a match is still not found, the command factory will inflect the method name from CamelCase to snake_case and
- attempt to find a matching command.
- 4. If a command still does not match, an exception is thrown.
- .. code-block:: php
- // Use the magic method
- $result = $twitter->getMentions();
- // This is exactly the same as:
- $result = $twitter->getCommand('getMentions')->execute();
- You can disable magic methods on a client by passing ``false`` to the ``enableMagicMethod()`` method.
- Custom command factory
- ----------------------
- A client by default uses the ``Guzzle\Service\Command\Factory\CompositeFactory`` which allows multiple command
- factories to attempt to create a command by a certain name. The default CompositeFactory uses a ``ConcreteClassFactory``
- and a ``ServiceDescriptionFactory`` if a service description is specified on a client. You can specify a custom
- command factory if your client requires custom command creation logic using the ``setCommandFactory()`` method of
- a client.
- Custom resource Iterator factory
- --------------------------------
- Resource iterators can be retrieved from a client using the ``getIterator($name)`` method of a client. This method uses
- a client's internal ``Guzzle\Service\Resource\ResourceIteratorFactoryInterface`` object. A client by default uses a
- ``Guzzle\Service\Resource\ResourceIteratorClassFactory`` to attempt to find concrete classes that implement resource
- iterators. The default factory will first look for matching iterators in the ``Iterator`` subdirectory of the client
- followed by the ``Model`` subdirectory of a client. Use the ``setResourceIteratorFactory()`` method of a client to
- specify a custom resource iterator factory.
- Plugins and events
- ==================
- ``Guzzle\Service\Client`` exposes various events that allow you to hook in custom logic. A client object owns a
- ``Symfony\Component\EventDispatcher\EventDispatcher`` object that can be accessed by calling
- ``$client->getEventDispatcher()``. You can use the event dispatcher to add listeners (a simple callback function) or
- event subscribers (classes that listen to specific events of a dispatcher).
- .. _service-client-events:
- Events emitted from a Service Client
- ------------------------------------
- A ``Guzzle\Service\Client`` object emits the following events:
- +------------------------------+--------------------------------------------+------------------------------------------+
- | Event name | Description | Event data |
- +==============================+============================================+==========================================+
- | client.command.create | The client created a command object | * client: Client object |
- | | | * command: Command object |
- +------------------------------+--------------------------------------------+------------------------------------------+
- | command.before_prepare | Before a command is validated and built. | * command: Command being prepared |
- | | This is also before a request is created. | |
- +------------------------------+--------------------------------------------+------------------------------------------+
- | command.after_prepare | After a command instantiates and | * command: Command that was prepared |
- | | configures its request object. | |
- +------------------------------+--------------------------------------------+------------------------------------------+
- | command.before_send | The client is about to execute a prepared | * command: Command to execute |
- | | command | |
- +------------------------------+--------------------------------------------+------------------------------------------+
- | command.after_send | The client successfully completed | * command: The command that was executed |
- | | executing a command | |
- +------------------------------+--------------------------------------------+------------------------------------------+
- | command.parse_response | Called when ``responseType`` is ``class`` | * command: The command with a response |
- | | and the response is about to be parsed. | about to be parsed. |
- +------------------------------+--------------------------------------------+------------------------------------------+
- .. code-block:: php
- use Guzzle\Common\Event;
- use Guzzle\Service\Client;
- $client = new Client();
- // create an event listener that operates on request objects
- $client->getEventDispatcher()->addListener('command.after_prepare', function (Event $event) {
- $command = $event['command'];
- $request = $command->getRequest();
- // do something with request
- });
- .. code-block:: php
- use Guzzle\Common\Event;
- use Guzzle\Common\Client;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- class EventSubscriber implements EventSubscriberInterface
- {
- public static function getSubscribedEvents()
- {
- return array(
- 'client.command.create' => 'onCommandCreate',
- 'command.parse_response' => 'onParseResponse'
- );
- }
- public function onCommandCreate(Event $event)
- {
- $client = $event['client'];
- $command = $event['command'];
- // operate on client and command
- }
- public function onParseResponse(Event $event)
- {
- $command = $event['command'];
- // operate on the command
- }
- }
- $client = new Client();
- $client->addSubscriber(new EventSubscriber());
|