A Native PHP Function Override with namespace & PHP-Mock: Shopify API Example

Willem Luijt
LEAD Backend Developer

The Problem and the PHP Function Relationship

The PHPShopify library is great for common Shopify API integration needs in customizing your project. However, it doesn’t provide the direct Shopify API response to your code and lacks extension points to retrieve that raw API response. Let’s see why a PHP override function is necessary to overcoming the problem.

In our application, we needed to get at the raw Shopify API response data in order to create detailed debug logs, for instance, when we hit Shopify’s API rate limit. In this article, we’ll show you how to override native PHP functions using a trick made possible by PHP’s namespacing feature and PHP-Mock.

What is PHP-Mock?

PHP-Mock is a library that is primarily used in a testing environment to override the definition of a native PHP function that is called in a namespaced file.  A small example given by the library shows a basic idea of when this library can be used:

 

 

namespace foo;

$time = time(); // This call can be mocked, a call to \time() can't.

The PHP-Mock library also points out PHP’s fallback namespace policy.  In that document, it mentions the following:

For functions and constants, PHP will fall back to global functions or constants if a namespaced function or constant does not exist.

This is the trick we needed.

What We Did in the PHP Function

After reviewing the entire PHPShopify code base looking for an extension point to get at the raw Shopify API response, there weren’t many options.  The included HttpRequestJson and CurlRequest classes were being used statically within the PHPShopify library, which makes it very difficult to use your own class.  Within the CurlRequest class, though, there is this line:

$output = curl_exec($ch);

Notice that it shows “curl_exec(…)” and not “\curl_exec(…)”.  There is no slash before the function call.  And luckily for us, the CurlRequest file was defined in the “PHPShopify” namespace.  All the pieces are in place for us to redefine PHP’s native curl_exec() function using a namespace trick.

The following code is what was developed to log the raw Shopify API responses:


namespace App\Service;

use phpmock\environment\MockEnvironment;
use phpmock\MockEnabledException;
use phpmock\spy\Spy;
use PHPShopify\CurlResponse;
use Psr\Log\LoggerInterface;

class MockMethods
{
   private $mockEnvironment;
   private $mocks;
   private $constantNames = [];
   private $lastShopifyRequest = [];
   private $logger;
   private $logWorkerId;
   private $commandName;

   public function __construct()
   {
       $this->loadConstantNames();
       $this->enableMockObjects();
   }

   public function setLogger(LoggerInterface $logger = null)
   {
       $this->logger = $logger;
   }

   public function setCommandName($commandName)
   {
       $this->commandName = $commandName;
   }

   public function setLogWorkerId($logWorkerId)
   {
       $this->logWorkerId = $logWorkerId;
   }

   public function enableMockObjects()
   {
       $this->mocks['shopify_curl_setopt'] = new Spy('\PHPShopify', 'curl_setopt', [$this, 'shopifyCurlSetopt']);
       $this->mocks['shopify_curl_exec'] = new Spy('\PHPShopify', 'curl_exec', [$this, 'shopifyCurlExec']);

       $this->mockEnvironment = new MockEnvironment();
       foreach ($this->mocks as $mock) {
           $this->mockEnvironment->addMock($mock);
       }
       try {
           $this->mockEnvironment->enable();
       } catch (MockEnabledException $e) {
           // do nothing
       }
   }

   public function disableMockObjects()
   {
       if ($this->mockEnvironment) {
           $this->mockEnvironment->disable();
       }
   }

   public function loadConstantNames()
   {
       $constants = get_defined_constants(true);
       $constants = $constants['curl'] ?? [];
       $this->constantNames = array_flip($constants);
   }

   public function shopifyCurlSetopt($ch, $option, $value)
   {
       $optionName = $option;
       if (isset($this->constantNames[$option])) {
           $optionName = $this->constantNames[$option];
       }
       $this->lastShopifyRequest[$optionName] = $value;

       return \curl_setopt($ch, $option, $value);
   }

   public function shopifyCurlExec($ch)
   {
       $response = \curl_exec($ch);

       $info = \curl_getinfo($ch);

       $commandName = $this->getCommandName();

       $shopifyResponse = new CurlResponse($response);
       $httpMethod = $this->lastShopifyRequest['CURLOPT_CUSTOMREQUEST'] ?? 'GET';
       $urlParts = parse_url($info['url']);
       $path = $urlParts['path'].(isset($urlParts['query']) ? '?'.$urlParts['query'] : '');
       // code 429 indicates a rate limit error response, exclude these for now
       if (429 != $info['http_code']) {
           if ($this->logger) {
               $this->logger->debug(sprintf('Executed Shopify API Request - Status Code: %s | Method: %s | URL: %s', $info['http_code'], $httpMethod, $path), [
                   'commandName' => $commandName ?: 'Shopify API Request',
                   'logWorkerId' => $this->logWorkerId,
                   'request' => $this->lastShopifyRequest,
                   'handle_info' => $info,
                   'response_headers' => $shopifyResponse->getHeaders(),
                   'response' => json_decode($shopifyResponse->getBody(), true),
               ]);
           }

           $this->lastShopifyRequest = [];
       } else {
           if (!isset($this->lastShopifyRequest['middleware_shopify_api_rate_limit_triggered'])) {
               $this->lastShopifyRequest['middleware_shopify_api_rate_limit_triggered'] = 0;
           }
           ++$this->lastShopifyRequest['middleware_shopify_api_rate_limit_triggered'];
       }

       return $response;
   }

   private function getCommandName()
   {
       if ($this->commandName) {
           return $this->commandName;
       }

       $commandName = '';

       $traceString = $this->generateCallTrace();

       $matches = [];
       preg_match('`src[^a-zA-Z0-9_-]{1,2}Command[^a-zA-Z0-9_-]{1,2}([a-zA-Z0-9_-]+)Command`', $traceString, $matches);

       if (isset($matches[1])) {
           $path = __DIR__.'/../../src/Command/'.$matches[1].'Command.php';
           if (file_exists($path)) {
               $file = file_get_contents($path);
               $commandNameMatches = [];
               preg_match('`defaultName\s*=\s*[\"\']([a-zA-Z0-9:_-]*)[\"\'];`', $file, $commandNameMatches);

               if (isset($commandNameMatches[1])) {
                   $commandName = $commandNameMatches[1];
               }
           }
       }

       return $commandName;
   }

   private function generateCallTrace()
   {
       // @see https://www.php.net/manual/en/function.debug-backtrace.php
       $e = new \Exception();

       $trace = explode("\n", $e->getTraceAsString());
       // reverse array to make steps line up chronologically
       $trace = array_reverse($trace);
       array_shift($trace); // remove {main}
       array_pop($trace); // remove call to this method
       $length = count($trace);
       $result = [];

       for ($i = 0; $i < $length; ++$i) {
           $result[] = ($i + 1).')'.substr($trace[$i], strpos($trace[$i], ' ')); // replace '#someNum' with '$i)', set the right ordering
       }

       return "\t".implode("\n\t", $result);
   }
}

When the MockObjects class above is instantiated, it will trigger the “enableMockObjects()” method.  In that method, there is a PHP-Mock Spy object defined for the \PHPShopify\curl_exec() function that should execute the local MockObjects::shopifyCurlExec() method instead of the built-in PHP \curl_exec() function.

Notice that within the MockObjects::shopifyCurlExec() method, there is this line:

$response = \curl_exec($ch);

This function call must have the slash before it because we want to execute the global namespace functionality.  Also, if we didn’t include the slash, that might lead to an infinite loop or cause other issues when this code is executed.

At this moment, we have the raw Shopify API response in the “$response” variable and can process it to create the debug logs.

PHP Override Complete

Complete success! This solution helped us create a robust logging system for our Shopify API integration with different levels of logging. These logs were critical to discovering and measuring Shopify API rate limit errors to help us determine how to control the speed of our private Shopify app. Once you understand the layout of an API, you can find the php function override solution you we looking for.

- Willem LuijtLEAD Backend Developer | 

Filed under: <BlogSoftwareWeb Development>

Upgrade Your Shopify Site Today !

Contact Us