Month: December 2017

  • PSR-7 Standard  – Part 5  – HTTP Client

    PSR-7 Standard – Part 5 – HTTP Client

    This post is part of series:


    The fifth part of the PSR-7 series describes the HTTP Client.

    The HTTP client is tool which sends a request to a server and returns the response.

    Sadly PSR-7 does not contain an interface for the HTTP client. The Standard contains only the HTTP messages. The client itself is part of the proposed PSR-18.

    PSR-18 is very small. It contains only an Interface with one method and some exception classes. The important part is the sendRequest method. It is easy as pie. A request has to be passed and the client should return a response or throw an ClientException exception.

    interface ClientInterface
    {
        /**
         * Sends a PSR-7 request and returns a PSR-7 response. 
         * 
         * @param RequestInterface $request
         *
         * @return ResponseInterface
         *
         * @throws \Psr\Http\Client\ClientException If an error happens during processing the request.
         */
        public function sendRequest(RequestInterface $request): ResponseInterface;
    }
    

    At the moment we cannot use PSR-18 in a production environment until it is accepted by the FIG. Fortunately there is an other project which tries to close the gap until the PSR-18 is accepted. The project is the father of the PSR-18. We talk about the HTTPPlug Project (former PHP-HTTP).

    HTTPPlug Project

    The project’s homepage is http://httplug.io/. The main idea is to decouple a PHP package/library from implementation by providing a HTTP client abstraction and some standard implementations.

    It provides a Composer meta package. You don’t know what a Composer meta package is? Think of it as a kind of interface for packages.

    This meta package can be added as requirements in your composer.json. Any compatible package can “provide” an implementation for the meta package.

    If you need a HTTP Client Implementation in your project, you should relay on the meta package instead of an real implementation.

    The implementation can be fulfilled in a project where the PHP library should be used. It is possible to pick any implementation from a huge list of existing implementations.

    An Example Library

    First we need a composer.json which requires a client implementation. For testing purposes we can add a development requirement to add some example scripts to the library. The HTTP-Plug project comes with a small and handy CURL based implementation which provides an implementation for the meta package. For the message implementation i.e. the Request, we make use of Guzzle’s PSR-7 implementation (see previous posts).

    {
      "name": "acme/my-super-php-library",
      "require": {
        "php-http/client-implementation": "^1.0",
        "guzzlehttp/psr7": "^1.4"
      },
      "autoload": {
        "psr-4": {
          "Acme\\MyLibrary\\": "src"
        }
      },
      "require-dev": {
        "php-http/curl-client": "^1.7"
      }
    }
    

    After a composer install  we are ready to create a sample service which uses the HTTP client. Let’s try to fetch the XML feed from dev98.de.

    We create a service class to fetch the XML feed. The service has a dependency to the HTTP Client interface. The file should be placed in the “src” sub-directory according to our PSR4 autoloading setting in composer.json file.

    <?php
    
    namespace Acme\MyLibrary;
    
    use GuzzleHttp\Psr7\Request;
    
    class FetchXmlFeedService
    {
        /**
         * @var \Http\Client\HttpClient
         */
        private $client;
    
        /**
         * ExampleService constructor.
         * @param \Http\Client\HttpClient $client
         */
        public function __construct(\Http\Client\HttpClient $client)
        {
            $this->client = $client;
        }
    
        /**
         * @return \SimpleXMLElement
         * @throws \Exception
         */
        public function fetch(): \SimpleXMLElement
        {
            $request = (new Request('GET', 'https://dev98.de/feed/'))
                ->withHeader('Accept', 'application/xml');
    
            $response = $this->client->sendRequest($request);
    
            if ($response->getStatusCode() !== 200) {
                throw new \Exception('Could not fetch XML feed');
            }
    
            return simplexml_load_string($response->getBody()->getContents());
        }
    }

    The service does not rely on a real implementation. The \Http\Client\HttpClient  is only an interface.

    Now we create a sample script which calls our service in the sub-directory “examples”.

    <?php
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    $httpClient = new \Http\Client\Curl\Client();
    $service = new \Acme\MyLibrary\FetchXmlFeedService($httpClient);
    $xmlData = $service->fetch();
    
    foreach ($xmlData->channel->item as $item) {
        echo (string) $item->title . "\n";
    }
    

    That’s our simple library with an example script. Testing this library is also not a big deal. We can easily mock the Interface.

    Use the Library

    If we want to use the library in a real project, we need to add the library in our project’s composer.json file. Then we add an implementation for the http client. This must not be the same as the development requirements in our library. This is a big advantage. If our PHP framework comes with an implementation which fits, we can re-use it.

    The complete source of the blog post is available on Github: https://github.com/cmuench/psr7-example-library

    The next blog post in this series describes the Server-Request.

  • Chrome 63 vs. dev domains

    Chrome 63 vs. dev domains

    With the recent update to version 63 for Chrome they introduced a feature, that redirects all *.dev  calls from HTTP to HTTPS.

    If you are using *.dev  domains to access your development machine, this behaviour is probably not what you want for all your local urls.

    Configuring dnsmasq

    At netz98 we are using dnsmasq to redirect all *.dev  calls to our localhost.

    dnsmasq has a setting where you can easily change this to another domain like *.localhost .

    First of locate the config file for dnsmasq. If you have installed it using brew you will probably find it under /usr/local/etc/dnsmasq.conf .

    Open the file in an editor of your choice and look for the following line:

    address=/dev/127.0.0.1

    And change it to:

    address=/localhost/127.0.0.1

    If you cannot find a line setting the address like shown above, just add a new line with the setting.

    To restart dnsmasq you can use

    brew services restart dnsmasq

    Configuring apache / nginx

    Depending on your choice of setup you might be using an nginx or apache setup.

    You most likely have some virtual-hosts or server settings that might need to be adjusted to the new domain.

    This could look something like this for nginx.

    server {
      listen 80;
    
      server_name shop.dev;
    
      // ....
    }

    Locate the server_name setting and any other occurences of your *.dev  domain and change it to .localhost

    Do not forget to change your Magento1 or Magento2 Urls in the core_config_data table.

    MacOS Nameserver

    For Firefox and Safari and a working ping foo.bar.localhost , you also have to add a setting for that.

    Create the file /etc/resolver/localhost  with the following content:

    nameserver 127.0.0.1

    After doing so, you need to restart the DNS Resolver and flush caches.
    I am on the latest MacOS 10.13 so I had to run the following command:

    sudo killall -HUP mDNSResponder; sudo killall mDNSResponderHelper; sudo dscacheutil -flushcache

    If you are running a different MacOS version, you can find a great blog post over at Dreamhost where they list the commands for each MacOS version: https://help.dreamhost.com/hc/en-us/articles/214981288-Flushing-your-DNS-cache-in-Mac-OS-X-and-Linux

    UPDATE:
    You might have to do a reboot of your machine so that the resolver works.

    UPDATE 2:
    Firefox and Safari require the above mentioned resolver to work.

    If you got anything to add or there even is a better solution feel free to leave a comment below.