Guzzle6 Extra Tutorial

I could tell you how many hours it takes to develop a MODX extra Transport Package complete with a build script, properties, multiple MODX elements, internationalized strings, error checks, and then fully test it, but you wouldn't believe me. If you use this extra and like it, please consider donating. The suggested donation for this extra is $10.00, but any amount you want to give is fine (really). For a one-time donation of $50.00 you can use all of my non-commercial extras with a clear conscience.


PayPal

Guzzle 6

Guzzle is rapidly becoming the standard for reliably making and processing HTTP requests. This extra does nothing by itself. It simply installs the Guzzle 6 files and autoloader for use on your site without the hassle of rolling your own cUrl code. Guzzle will use cURL if it's installed on your site, or it will use PHP streams if not. Guzzle 6 also includes PSR7 capabilities, though I haven't used them.

I don't claim to be an expert at Guzzle. The examples below work, but I can't guarantee that they are best practices.

This tutorial provides some simple examples for using Guzzle to fetch web pages, check if a remote URL exists, or download files from a web site. Most of the code below is from the UpgradeMODX extra.


Installing Guzzle6

Go to System | Package Management on the main menu in the MODX Manager and click on the "Download Extras" button. That will take you to the Revolution Repository. Put Guzzle6 in the search box and press Enter. Click on the "Download" button, and once the package is downloaded, click on the "Back to Package Manager" button. That should bring you back to your Package Management grid. Click on the "Install" button next to Guzzle6 in the grid. The Guzzle6 package should now be installed.


Basic Usage

For any code that uses Guzzle, you should include some version of these lines at the very top of your code (just below the PHP tag):

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
/* Include these if you need Psr7 support */
// use GuzzleHttp\Psr7;
// use GuzzleHttp\Psr7\Request;

/* This next line can go anywhere, as long as it's above any code that uses Guzzle */

require_once MODX_CORE_PATH .
    'components/guzzle6/vendor/autoload.php';

Once you've included the lines above you can use any Guzzle 6 features. There are some examples below, but you can also consult the Guzzle Documentation.


Simplest Possible Request

This will get the content from a web page or URL and echo it to the screen:

$client = new \GuzzleHttp\Client();
$url = '//api.github.com/repos/modxcms/revolution/tags';
$response = $client->request('GET', $url);
echo $response->getBody();

Important: The examples below are inside a class. They will need to be modified if you use them outside of a class.


Parsing Guzzle Errors

Before we get to the rest of the examples, this is a handy function I created for parsing messages from Guzzle exceptions:


 /**
 * Creates beautified error message based on
 * exception thrown
 *
 * @param $e GuzzleHttp\Exception\RequestException
 * @param $verbose - Show the full, raw message
 * @return string - Error message based on Exception
 *
 */
public function parseException($e, $verbose = false) {
    /** @var  $response \Psr\Http\Message\MessageInterface */
    $msg = $e->getMessage();
    $prefix = $this->modx->lexicon('ugm_no_version_list_from_github')
        . ' -- ';
    $retVal = $msg; // default to entire message;
    $code = $e->getCode();

    if ($e->hasResponse()) {
        $response = $e->getResponse();
        $rawMessage = $response->getBody();
        $message = json_decode( (string) $rawMessage);
        if (empty($message)) {
            $message = $response->getReasonPhrase();
            $retVal = $code . ' ' . $message;
        } else {
            $ex = (array) $message;
            $retVal = $code . ' ' . $ex['message'];
        }
    } elseif (empty($code) || ($code >= 500)) {
        $code = ((int) $code === 0)
            ? 'No Code Returned'
            : $code;
        $retVal = $code . ' ' .
            'Connection error (no internet?)';
    }
    $retVal = $verbose? $prefix . ' ' .
        $msg : $prefix . $retVal;
    return $retVal;
}

Adding Some Options

This method is a little more complex, since it gets data from GitHub, and has options for the API response, authentication, exception handling, and SSL verification:

/**
 * * Gets raw JSON version list from GitHub
 *  Long param list is mainly for unit tests
 *
 * @throws \GuzzleHttp\Exception\ClientException
 * @throws \GuzzleHttp\Exception\GuzzleException
 * @param $url string -- GitHub URL
 * @param $githubTimeout int
 * @param $verifyPeer bool
 * @param null $githubUsername string
 * @param null $githubToken string
 * @param null $certPath string -- optional path to SSL cert file
 * @param bool $verbose -- show verbose error messages
 *
 *
 * @return mixed returns JSON version list
    as string or false on failure
 */
public function getRawVersions($url, $githubTimeout = 6,
        $verifyPeer = true,  $githubUsername = null,
        $githubToken = null, $certPath = null,
        $verbose = false) {

    /* Create the client */
    $this->client = new \GuzzleHttp\Client();

    $options = array();
    /* User GitHub credentials if we have them
    if ((!empty($githubUsername)) && (!empty($githubToken))) {
        $options['auth'] = array($githubUsername, $githubToken);
    }
    $options['header'] = array (
        'Cache-Control' => 'no-cache',
        'Accept' => 'application/json',
    );
    /* Optional Path to .pem file for SSL
       Usually not necessary */
    if (!empty ($certPath))  {
        $options['cert'] = $certPath;
    }

    $options['timeout'] = $githubTimeout;

    if ($verifyPeer !== true) {
        $options['verify'] = false;
    }

    try {
        $response = $this->client->request('GET',
            $url, $options);
        $retVal = $response->getBody();
    } catch (RequestException $e) {
        $msg = $this->parseException($e, $verbose);
        $retVal = false;
    } catch (\Exception $e) {
        /** @var $e \GuzzleHttp\Exception\RequestException */
        $msg = $this->parseException($e, $verbose);
        $retVal = false;
    }
    return $retVal;
}

Rather than have so many arguments, the method above could easily use class variables set elsewhere in the class (say, in the initialization() method). The multiple arguments make the method's behavior easier to test with unit tests.

The $certPath setting is rarely necessary. It allows you to tell Guzzle where the certification file (in .pem format) is located by giving it the full path to the file. Guzzle looks in several places for the file and is very good at finding the server's SSL certification file without the setting. If it can't be found, usually the server admin can put it in a more traditional place, but you can also tell Guzzle where it is with this setting.


See if a Remote URL exists

This makes a very simple request to verify that the URL can be reached without problems. It will report a variety of kinds of HTTP errors and failures. It returns true if things went well. If not, it returns false and writes the error to the MODX error log.

It's handy for use in a link checker. UpgradeMODX uses it to make sure the MODX .zip file is available before downloading it.

public function remoteFileExists($url) {
        /** @var $client GuzzleHttp\Client */
        $client = $this->client;

        try {
            $client->head($url);
            return true;
        } catch (GuzzleHttp\Exception\ClientException $e) {
            $this->modx->log(modX::LOG_LEVEL_ERROR,
                'Request Failed: ' . $this->parseException($e));
            return false;
        } catch (/Exception $e) {
            $this->modx->log(modX::LOG_LEVEL_ERROR,
                'Request Failed: ' . $this->parseException($e));
            return false;
        }
    }

Notice that there is a second catch block using to catch regular exceptions using catch (/Exception) in case of 500-level errors, or a failed internet connection.


Downloading a File

This method downloads a .zip file from a URL. In this case the $options argument is required, since it tells Guzzle where to put the downloaded file. The method also shows an example of catching an exception, then throwing a new one that can be caught in the method that calls this one. I also throws its own exception if the file if the downloaded file is empty.

/** @throws Exception
* @throws GuzzleException
* @param $url string - URL to download fro
* @param $destinationPath - full path to save file
*/

public function download($url, $destinationPath) {

    /* See if the file is available for download */
    if (!$this->remoteFileExists($url)) {
        throw new Exception($this->modx->lexicon('ugm_no_such_version'));
    }

    $client = $this->client;
    $destFile = fopen($destinationPath, 'w');
    if (!$destFile) {
        $msg = '[Download Files Processor] ' .
            $this->modx->lexicon('ugm_could_not_open') . ' ' .
            $this->destinationPath . ' ' .
                $this->modx->lexicon('ugm_for_writing');
        throw new Exception($msg);

    }

    set_time_limit(0);
    $options = array(
       /* where to put the body of a response */
        RequestOptions::SINK => $destFile,
        RequestOptions::CONNECT_TIMEOUT => $this->modxTimeout,
        RequestOptions::VERIFY => (bool) $this->sslVerifyPeer,
       // RequestOptions::TIMEOUT => 0.0,
    );

    if (!empty($this->certPath)) {
        $options[RequestOptions::CERT] = $this->certPath;
    }

    $options['headers'] = array(
        'Cache-Control' => 'no-cache',
        'Accept' => 'application/zip'
    );

    try {
        $response = $client->request('GET', $url, $options);
    } catch (Exception $e) {
        fclose($destFile);
        unlink($destinationPath);
        throw new exception($this->modx->lexicon('ugm_download_failed') .
            ' -- ' . $e->getMessage());
    }

    fclose($destFile);

    if (filesize($this->destinationPath) === 0) {
        throw new exception($this->modx->lexicon('ugm_download_failed')
            . ' Filesize: 0');
    } else {
        $msg = $this->modx->lexicon('ugm_downloaded') . ' ' .
            $_SESSION['ugm_version'] .
            ' -> ' . $destinationPath;
        echo $msg;
    }
}

The code above may throw an exception, so it should always be called inside a try block with an associated catchblock.

 

My book, MODX: The Official Guide - Digital Edition is now available here. The paper version of the book is available from Amazon.

If you have the book and would like to download the code, you can find it here.

If you have the book and would like to see the updates and corrections page, you can find it here.

MODX: The Official Guide is 772 pages long and goes far beyond this web site in explaining beginning and advanced MODX techniques. It includes detailed information on:

  • Installing MODX
  • How MODX Works
  • Working with MODX resources and Elements
  • Using Git with MODX
  • Using common MODX add-on components like SPForm, Login, getResources, and FormIt
  • MODX security Permissions
  • Customizing the MODX Manager
  • Using Form Customization
  • Creating Transport Packages
  • MODX and xPDO object methods
  • MODX System Events
  • Using PHP with MODX

Go here for more information about the book.

Thank you for visiting BobsGuides.com

  —  Bob Ray