Kroger API Examples How To [PHP Guzzle Async]

The Kroger API examples below collect grocery store location and product JSON data. As a bonus, we’ll use PHP Guzzle async to run concurrent requests (saving 10X the time). This will give you a leg up on utilizing the Kroger API in any PHP project.

Due to Kroger subsidiaries, this becomes a Ralphs API example, a Fred Meyer API example, Smith’s Food and Drug API example, and King Soopers API example. In addition, you may use this for any Kroger-owned grocer, market, gas station, or company.

The steps below will walk through the logic. However, I put the complete Kroger API Examples code on Github. I built this for Laravel, but it could be adapted to other PHP frameworks.

Let’s get started.


Sign Up For A Kroger API Account

  1. Sign up for a Kroger API account.
    – Complete the steps to create and verify your account.
  2. Once logged, click the “Manage” button in the upper right corner.
  3. On the “Manage Organizations” page, click the blue “New Org” button.
  4. Register your organization’s name and description.
    – chose Production or Certification (Dev) environment
  5. Save, Download, and/or Print your registration details and click “Continue”.
    – especially your client_id and client_secret.

Generate API Auth Key And Save

Within your terminal tool, generate an API Key.

# replace the values below and run
echo -n 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET'  | base64

# you will end up with a value like this:
# MTIzNDU2Nzg5MTIzNDU2Nzg5MTIzNDU2Nzg5MTIzNDU2Nzg5MTIzNDU2Nzg5MTIzNDU2Nzg5MTIzNDU2Nzg5MDoxMjM0NTY3ODkxMjM0NTY3ODkxMjM0NTY3ODkxMjM0NTY3ODkxMjM0

Update Your Laravel .env file with the API URL and Auth Key.

# Dev
KROGER_API_URL="https://api-ce.kroger.com/v1"
KROGER_API_AUTH_KEY="........."

Make a Kroger API Database Table

You don’t need MySQL to use the Kroger API examples. However, we’ll use a table to track API access.

  • track the number of API calls made each day.
    – Kroger limits it to 10,000 per day
  • save and access generated API tokens while they are valid
    – token stay valid for 1800 seconds (30 minutes)
    – so reusing a valid token reduces the number of calls we make each day.

Carrying on, I will use Laravel Sail to execute Laravel 9 command line code.

./vendor/bin/sail artisan make:migration create_kroger_api_table

Edit the migration file.

<?php // file: database/migrations/0000_00_00_000000_create_kroger_api_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration {
  /**
   * Run the migrations.
   *
   * @return void
   */
  public function up()
  {
    if (!Schema::hasTable("kroger_api")) {
      DB::unprepared("
        CREATE TABLE `m_kroger_api` (
          `id` int unsigned NOT NULL AUTO_INCREMENT,
          `api_day` timestamp NULL DEFAULT NULL,
          `api_call_count` bigint DEFAULT '0',
          `token_generic_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
          `token_generic_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
          `token_generic_expires_in` bigint DEFAULT NULL,
          `token_generic_created_at` timestamp NULL DEFAULT NULL,
          `token_product_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ,
          `token_product_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL ,
          `token_product_expires_in` bigint DEFAULT NULL,
          `token_product_created_at` timestamp NULL DEFAULT NULL,
          `created_at` timestamp NULL DEFAULT NULL,
          `updated_at` timestamp NULL DEFAULT NULL,
          `deleted_at` timestamp NULL DEFAULT NULL,
          PRIMARY KEY (`id`),
          UNIQUE KEY `api_day` (`api_day`)
        ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
      ");
    }
  }

  /**
   * Reverse the migrations.
   *
   * @return void
   */
  public function down()
  {
    Schema::dropIfExists("kroger_api");
  }
};

Then migrate the table.

./vendor/bin/sail artisan migrate

You will end up with a table like this.

kroger api examples database table


Create A Kroger API Class

Run the following in your command line.

./vendor/bin/sail artisan make:model KrogerApi

Add the class structure that will hold all our methods/functions for the Kroger API examples.

I will explain each as we fill out the code below.

<?php // file: app/Models/KrogerApi.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;

class KrogerApi extends Model
{
  use HasFactory;

  protected $table = "mkroger_api";

  private static $api_context;

  private static function get_context() {}

  private static function get_token() {}

  private static function increment_api_call_count() {}

  public static function get_locations() {}

  public static function get_products() {}

  private static function guzzle_pool() {}
}

Get Context

Here we create an OOP Singleton that manages the “self::$api_context” variable.

  • We get today’s API row from the database.
  • If it doesn’t exist, we’ll create it.
  • It’s saved in the Laravel active record format.
  • In this way, updates to the variable will save to the database.
  private static function get_context()
  {
    /*
     * get_context()
     *
     *  - manages the "m_kroger_api" row for today
     *  - main duties
     *    - count how many api calls we made today
     *    - store valid generic and product tokens
     *    - and update tokens when they expire
     */
    if (empty(self::$api_context)) {
      // get the api row for today
      // OR create it
      $date = Carbon::now(config("app.timezone"))->format("Y-m-d");
      self::$api_context = self::whereRaw("DATE(created_at) = DATE(?)", [
        $date,
      ])->firstOr(function () {
        // create a new active record
        $date_object = Carbon::now(config("app.timezone"));
        $date = $date_object->format("Y-m-d");
        $timestamp = $date_object->toDateTimeString();
        $context = new self();
        $context->api_day = $date;
        $context->api_call_count = 0;
        $context->created_at = $timestamp;
        $context->updated_at = $timestamp;
        $context->save();
        // return it to the static variable
        return $context;
      });
    }
    return self::$api_context;
  }

Get Token

The method manages our tokens within the API context.

Purpose & Output:

  • Manage valid tokens and return the appropriate token for locations or products.

Inputs:

  • Our Kroger API examples use two token URL GET variable scopes:
    – blank scope (“scope=”) which we’ll use for getting locations.
    – and product scope (“scope=product.compact”) which we’ll use for products.
  • This function receives “generic” (blank) and “product” to let us know what token we want.

Order of Operations:

  1. Get the current API context
  2. Get the token type from the API content
  3. If it exists and is not expired, then return the value
  4. If it is expired or doesn’t exist, then…
    – get a new token
    – save it to API context (and the database)
    – return the new value.

Here’s our first call to PHP Guzzle async pool.

  • in this case, it calls to get the Kroger API token we need.
  • we’ll cover this when we go over that method.
  /*
   * get_token()
   *
   *  - manages the tokens inside self::$api_context
   *  - (both the generic and product tokens)
   *  - checks if it exists
   *  - checks if it's expired
   *  - if it doesn't exist or is expired, then this...
   *    - gets a new token from the Kroger API
   *    - updates the database and local context
   *
   *  - input: type ("generic" or "product")
   *
   *  - output: token value string
   */
  private static function get_token($type = "generic")
  {
    // get current context
    self::get_context();

    // set context key names
    $type = $type == "product" ? "product" : "generic";
    $token_value_key = "token_" . $type . "_value";
    $token_type_key = "token_" . $type . "_type";
    $token_expires_in_key = "token_" . $type . "_expires_in";
    $token_created_at_key = "token_" . $type . "_created_at";

    // get context values
    $access_token = self::$api_context->{$token_value_key} ?? "";
    $expires_in = self::$api_context->{$token_expires_in_key} ?? "";
    $created_at = self::$api_context->{$token_created_at_key} ?? "";

    // test if token is expired
    // - Kroger API tokens expires in 30 minutes (1800 seconds).
    // - The API also tells you how many seconds it will expire it.
    // - We save this in the database and context.
    // - So if this value changes, this will dynamically check
    $token_is_expired = true;
    if (!empty($access_token) && !empty($created_at) && !empty($expires_in)) {
      $created_object = new Carbon($created_at);
      $now = Carbon::now(config("app.timezone"));
      $seconds_diff = $created_object->diffInSeconds($now);
      // find if token expired (with a 3 minute buffer)
      $token_is_expired = intval($seconds_diff + 180) > intval($expires_in);
    }

    // if the token expired or we don't have a token
    // then create a new one
    if (empty($access_token) || (!empty($access_token) && $token_is_expired)) {
      // set scope
      $scope = $type == "product" ? "product.compact" : "";
      // set curl data
      $api = env("KROGER_API_URL");
      $url = "$api/connect/oauth2/token?grant_type=client_credentials&scope=$scope";
      $urls_data = [
        [
          "url" => $url,
          "method" => "POST",
        ],
      ];
      // call
      $responses = self::guzzle_pool($urls_data, "basic");
      $response = $responses[0] ?? [];
      $status = $response["status"] ?? "";

      if ($status != "success") {
        $message = $response["message"] ?? "error getting api token";
        throw new Exception($message, 1);
      }

      // get output values
      $data = $response["data"] ?? [];
      $access_token = trim($data["access_token"] ?? "");
      $expires_in = intval($data["expires_in"] ?? "");
      $token_type = trim($data["token_type"] ?? "");
      $timestamp = Carbon::now(config("app.timezone"))->toDateTimeString();

      // update context
      self::$api_context->{$token_value_key} = $access_token;
      self::$api_context->{$token_expires_in_key} = $expires_in;
      self::$api_context->{$token_type_key} = $token_type;
      self::$api_context->{$token_created_at_key} = $timestamp;
      self::$api_context->save();
    }
    return self::$api_context->{$token_value_key};
  }

Increment API Call Count

We’ll keep track of our daily API call count to monitor usage.

  • It gets the current count from the API context.
  • Then the function will increment it.
  • Finally, it will save it back to the context and the database.
  private static function increment_api_call_count()
  {
    // make sure we have context
    self::get_context();
    // update values
    $timestamp = Carbon::now(config("app.timezone"))->toDateTimeString();
    self::$api_context->api_call_count += 1;
    self::$api_context->updated_at = $timestamp;
    // save
    self::$api_context->save();
    return;
  }

Get Locations

The Kroger API locations docs allow you to search by many factors.

For my purposes, I centered this function around a search by postal code.

Purpose & Output:

  • return all unique location addresses based on an array of provided zip codes.

Inputs:

  • Search a batch of zip codes (required)
  • Also, search by only locations with specific departments (optional)
  • Limit the number of stores to return (Kroger allows 1 to 200)
  • Limit the number of miles to search nearby (1 to 100 miles)

Order of operations:

  1. First, build a URL for each postal code we will search.
  2. Then it calls the PHP Guzzle async pool to concurrently gather responses from the Kroger API.
  3. Next, it gathers each unique location from all responses.
  4. Along with that, it formats the location address/details in a more compact format.
  5. Finally, the function returns a breakdown of status, (error) message, and data (locations).
  /*
   * get_locations()
   *
   * Inputs:
   *  - postal_codes: array of 5 digit strings
   *    [ "32118", "32207", "32216", "32218", "32256 ]
   *  - optional departments: comma separated two digit codes
   *    - this finds stores with these departments
   *  - limit: number of stores to return
   *    - Kroger allows between 1 to 200
   *  - radius: number of miles to search near by
   *    - Kroger allows 1 to 100 miles
   *
   * Output:
   *  - Kroger store locations array in $result["data"]
   *
   * Kroger API location docs
   *  - https://developer.kroger.com/reference/#tag/Locations
   */
  public static function get_locations(
    $postal_codes = [], // required
    $departments = "04,05", // optional comma separated two digit codes
    $limit = 200, // 1 to 200
    $radius_miles = 100
  ) {
    try {
      $start_time = microtime(true);

      // if postal codes is a string, recast as an array
      if (!empty($postal_codes) && is_string($postal_codes)) {
        $postal_codes = preg_split("/(,|;)/", $postal_codes);
      }

      // clean postal codes
      if (is_array($postal_codes)) {
        $postal_codes = array_map(function ($code) {
          // allow only numbers
          $code = preg_replace("/[^0-9]/", "", $code);
          // only first 5 characters
          $code = substr($code, 0, 5);
          return $code;
        }, $postal_codes);
      }

      // if we have no postal codes, then return
      if (empty($postal_codes) || count($postal_codes) < 1) {
        throw new Exception("no valid zip codes sent", 1);
      }

      // set and clean values
      $api = env("KROGER_API_URL");
      $limit = intval($limit) > 0 ? intval($limit) : 200;
      $radius_miles = intval($radius_miles) > 0 ? intval($radius_miles) : 100;

      $urls_data = [];
      foreach ($postal_codes as $postal_code) {
        // build url
        $url = "$api/locations?filter.zipCode.near=$postal_code";
        $url .= "&filter.limit=$limit&filter.radiusInMiles=$radius_miles";
        if ($departments) {
          $url .= "&filter.department=$departments";
        }
        $urls_data[] = [
          "url" => $url,
          "method" => "GET",
        ];
      }

      // call
      $responses = [];
      if (count($urls_data) > 0) {
        $responses = self::guzzle_pool($urls_data, "generic");
      }

      // consolidate locations
      $locations = [];
      foreach ($responses as $response) {
        // did this fail?
        if (($response["status"] ?? "") != "success") {
          $message = $response["message"] ?? "error getting api token";
          throw new Exception($message, 1);
        }

        $api_locations = $response["data"]["data"] ?? [];
        $meta = $response["data"]["meta"] ?? [];

        if (count($api_locations) < 1) {
          continue;
        }

        $required_fields = [
          "locationId",
          "chain",
          "name",
          "zipCode",
          "state",
          "county",
          "city",
          "locationId",
        ];

        foreach ($api_locations as $key => $location) {
          // merge address to root of array
          $location_id = $location["locationId"] ?? "-1";
          $location = array_merge($location, $location["address"] ?? []);

          $missing_required = false;
          foreach ($required_fields as $required) {
            if (empty($location[$required])) {
              $missing_required = true;
              break;
            }
          }

          // if already in new array or missing required fields, then skip
          if (!empty($locations[$location_id]) || $missing_required) {
            unset($api_locations[$key]);
            continue;
          }

          // standardize and add to new  array
          $new_location = [
            "store_parent" => "KROGER",
            "store_chain" => $location["chain"],
            "location_id" => "" . $location["locationId"] . "",
            "store_name" => $location["name"],
            "address_line1" => $location["addressLine1"] ?? "",
            "address_line2" => $location["addressLine2"] ?? "",
            "address_line3" => $location["addressLine3"] ?? "",
            "city" => $location["city"],
            "region" => $location["state"],
            "postal_code" => $location["zipCode"],
            "county" => $location["county"],
            "latitude" => $location["geolocation"]["latitude"] ?? "",
            "longitude" => $location["geolocation"]["longitude"] ?? "",
            "timezone" => $location["hours"]["timezone"] ?? "",
            "phone" => $location["phone"] ?? "",
          ];

          // add to new locations array
          $locations[$location["locationId"]] = $new_location;

          // free up memory
          unset($api_locations[$key]);
        }
      }

      $status = "success";
      $message = "script complete";
    } catch (Exception $e) {
      $status = "fail";
      $message = $e->getMessage();
      $error_line = $e->getLine();
      $error_file = $e->getFile();
    }

    return [
      "status" => $status ?? "fail",
      "message" => $message ?? "",
      "error_line" => $error_line ?? 0,
      "error_file" => $error_file ?? "",
      "inputs" => [
        "postal_codes" => $postal_codes ?? [],
        "departments" => $departments ?? "",
        "limit" => $limit ?? "",
        "radius_miles" => $radius_miles ?? "",
      ],
      "data" => $locations ?? [],
    ];
  }

Get Products

Like locations, the Kroger Products docs allow you to search by many variables.

Purpose & Output:

  • Return the first 1,000 products based on a search phrase.

Inputs:

  • search term: fuzzy search based on input (ex. “milk”, “fish sticks”, “candy”)
  • brand: limit products to a brand (ex. “Kellog’s”, “Red Baron”, “Coffee-Mate”)
  • location ID: limit search to a specific store
    – if no ID, then it will return national products with no prices
    – if it has an ID, it will return products from that store with prices
  • start at: this is the number of items in the results to start at.
    – the search returns results in batches of 1 to 50 products.
    – in order to return 200 results you have to search 4 times (start at 1, 51, 101, and 151)
    – this defaults to “1”.
    – the function will auto loop through all “start at” batches up to 1,000 products.

Order of Operations:

  1. First, build a URL for each postal code we will search.
  2. Then it calls the PHP Guzzle async pool to concurrently gather responses from the Kroger API.
  3. Next, it gathers each unique location from all responses.
  4. Along with that, it formats the location address/details in a more compact format.
  5. Finally, the function returns a breakdown of status, (error) message, and data (locations).
  public static function get_products(
    $search_term = "",
    $brand = "",
    $location_id = "",
    $start_at = 1
  ) {
    try {
      $start_time = microtime(true);

      // what batch do we need to start at?
      $start_at = intval($start_at) > 0 ? intval($start_at) : 1;
      $limit = 50;

      // build batch product urls
      $urls_data = [];
      for ($i = $start_at; $i < 1000; $i += $limit) {
        // build url
        $api = env("KROGER_API_URL");
        $url = "$api/products?filter.limit=$limit&filter.start=$i";
        // add other url vars
        if ($search_term != "") {
          $url .= "&filter.term=" . urlencode($search_term);
        }
        if ($brand != "") {
          $url .= "&filter.brand=" . urlencode($brand);
        }
        if ($location_id != "") {
          $url .= "&filter.locationId=" . urlencode($location_id);
        }
        $urls_data[] = [
          "url" => $url,
          "method" => "GET",
        ];
        // if ($i >= 200) {
        //
        // }
      }

      $responses = [];
      if (count($urls_data) > 0) {
        $responses = self::guzzle_pool($urls_data, "product");
      }

      // consolidate products
      $all_products = [];
      foreach ($responses as $key => $response) {
        // did this fail?
        if (($response["status"] ?? "") != "success") {
          $message = $response["message"] ?? "error getting api token";
          throw new Exception($message, 1);
        }

        // set vars
        $api_products = $response["data"]["data"] ?? [];

        // if no products then skip
        if (count($api_products) < 1) {
          continue;
        }

        // add each product to all_products
        foreach ($api_products as $key => $product) {
          // id
          $upc = $product["upc"];
          // if this exists, then skip
          if (!empty($all_products[$upc])) {
            continue;
          }
          // collect "front" images
          if (!empty($product["images"])) {
            $product["image"] = [];
            foreach ($product["images"] as $view) {
              if (($view["perspective"] ?? "") == "front") {
                foreach ($view["sizes"] as $image) {
                  $product["image"][$image["size"]] = $image["url"];
                }
                break;
              }
            }
          }
          $all_products[$upc] = $product;
          // remove this product
          // so we don't process it again
          unset($api_products[$key]);
        }
      }

      foreach ($all_products as $upc => $product) {
        // gather prices
        $prices = $product["items"][0]["price"] ?? [];
        $retail = $prices["regular"] ?? 0;
        $promo = $prices["promo"] ?? 0;
        $discount = $retail - $promo;

        if ($retail > 0 && $promo > 0 && $discount > 0) {
          $discount_percent = round(($discount / $retail) * 100);
        } else {
          $discount_percent = 0;
        }

        // format keys
        $all_products[$upc] = [
          "product_id" => $product["productId"] ?? "",
          "upc" => $product["upc"] ?? "",
          "brand" => $product["brand"] ?? "",
          "description" => $product["description"] ?? "",
          "price_regular" => $retail,
          "price_promo" => $promo,
          "price_discount" => $discount,
          "price_discount_percent" => $discount_percent,
          "images" => json_encode($product["image"] ?? []),
          "store_categories" => json_encode($product["categories"] ?? []),
        ];
      }

      $status = "success";
      $message = "script complete";
    } catch (Exception $e) {
      $status = "fail";
      $message = $e->getMessage();
      $error_line = $e->getLine();
      $error_file = $e->getFile();
    }

    return [
      "status" => $status ?? "fail",
      "message" => $message ?? "",
      "error_line" => $error_line ?? 0,
      "error_file" => $error_file ?? "",
      "inputs" => [
        "search_term" => $search_term ?? "",
        "brand" => $brand ?? "",
        "location_id" => $location_id ?? "",
        "$start_at" => $$start_at ?? "",
      ],
      "data" => $all_products ?? [],
    ];
  }

PHP Guzzle Async Pool

Now we will handle all Kroger API examples URLs.

Purpose & Output:

  • Concurrently handle all URL requests asynchronously
  • Wait until all are done and return an array of responses.

Inputs:

  • Receives an array of URLs in the format:
    [
    ["url" => "http://domain.com/this/action", "method" => "GET"],
    [...],
    [...],
    ]
  • Also receives, the types of calls it is making (“generic”, “basic”, “product”).
    – “generic”: the blank scope for locations
    – “product”: the product scope for products
    – “basic”: the type of authorization for generating a new token.

Order of Operations:

  1. Recieve, URLs data array.
  2. Start a PHP “do/while” loop attempting these calls 3 times.
  3. Make the authorization headers options for the type of calls we’re making.
  4. Create a function closure variable that we can trigger guzzle async requests.
  5. Call the closure variable with PHP guzzle async Pool.
  6. Loop through the results to find the “Response” (success) and “Exception” (fail) objects.
  7. If there are any serious errors, throw an Exception and stop.
  8. If there are any minor errors (timeouts, API temporarily down), loopback and try it again.
  9. Once complete, return an array of responses.
  /*
   *  about guzzle_pool()
   *
   *  - sends multiple requests concurrently and waits for all responses
   *  - retries up 3 times for any requests that return an invalid response
   *    - example: Kroger API will often return "all origins are down"
   *        which means we need to retry the request
   *    - if we retry, then we will generate a new header
   *        so that the tokens are all current and valid
   *  - NOTE: this pool expects the batch URLs to be of the same type.
   *    - example: all product API urls or all location API urls
   *
   *  - returns an array of responses
   *    - example: [
   *        [
   *          "code" => string
   *          "reason" => string
   *          "body" => array
   *        ],
   *        [...],
   *        [...],
   *      ]
   *
   *  - reference:
   *    - https://docs.guzzlephp.org/en/stable/quickstart.html?highlight=pool
   *    - https://hotexamples.com/examples/guzzlehttp/Pool/batch/php-pool-batch-method-examples.html
   */
  private static function guzzle_pool($urls_data = [], $auth_type = "generic")
  {
    $ret = [];
    $start_time = microtime(true);

    if (
      !is_array($urls_data) ||
      count($urls_data) < 1 ||
      !in_array($auth_type, ["generic", "product", "basic"])
    ) {
      return $ret;
    }

    // set authorization
    if ($auth_type == "generic") {
      $options = [
        "headers" => [
          "Accept" => "application/json",
          "Authorization" => "Bearer " . self::get_token("generic"),
        ],
      ];
    } elseif ($auth_type == "product") {
      $options = [
        "headers" => [
          "Accept" => "application/json",
          "Authorization" => "Bearer " . self::get_token("product"),
        ],
      ];
    } elseif ($auth_type == "basic") {
      $options = [
        "headers" => [
          "Content-Type" => "application/x-www-form-urlencoded",
          "Authorization" => "Basic " . env("KROGER_API_AUTH_KEY"),
        ],
      ];
    }

    // get client
    $client = new Client();
    // loop through batches
    $good_responses = [];
    $attempt_count = 0;
    if (count($urls_data) > 0) {
      do {
        $attempt_count += 1;

        // use closure that returns a promise
        $requests = function ($urls_data, $options) use ($client) {
          // loop through batches
          foreach ($urls_data as $url_data) {
            yield function () use ($client, $url_data, $options) {
              // type of request
              if (strtoupper($url_data["method"]) == "GET") {
                // if GET
                self::increment_api_call_count();
                return $client->getAsync($url_data["url"], $options);
              } elseif (strtoupper($url_data["method"]) == "POST") {
                // if POST
                self::increment_api_call_count();
                return $client->postAsync($url_data["url"], $options);
              }
            };
          }
        };

        // - Guzzle pool has a static method that will
        // async call the requests and wait.
        // - Then we translate this into a readable array
        $results = Pool::batch($client, $requests($urls_data, $options));

        // handle the responses
        foreach ($results as $key => $response) {
          $ret[$key] = [];
          $obj_class = is_object($response)
            ? get_class($response)
            : "not object";
          $ret[$key]["object_class"] = $obj_class;

          // success
          if (
            $obj_class == "GuzzleHttp\\Message\\Response" ||
            $obj_class == "GuzzleHttp\\Psr7\\Response"
          ) {
            // SUCCESS: response we're looking for
            $headers = $response->getHeaders();
            $contentType = $headers["Content-Type"][0];
            $contentType = explode(";", $contentType);
            $ret[$key]["status"] = "success";
            $ret[$key]["status_code"] = $response->getStatusCode();
            $ret[$key]["message"] = $response->getReasonPhrase();
            $ret[$key]["reason"] = $response->getReasonPhrase();
            $ret[$key]["content_type"] = $contentType[0];
            if ($contentType[0] == "application/json") {
              $ret[$key]["content_type"] = "json";
              $ret[$key]["data"] = json_decode($response->getBody(), true);
            } else {
              $ret[$key]["data"] = $response->getBody();
              $ret[$key]["full"] = $response;
            }
          } elseif (strpos($obj_class, "Exception") !== false) {
            // FAIL: guzzle raised an exception

            $message = strtolower($response->getMessage() ?? "");

            $details = "(Kroger API: $message)";
            $ret[$key]["status"] = "fail";
            $ret[$key]["message"] = $message;

            // decode the message
            if (strpos($message, "invalid credentials") !== false) {
              // Kroger API base64 key is missing
              // stop and fix
              throw new Exception("invalid credentials $details", 1);
            } elseif (strpos($message, "404 not found") !== false) {
              // The URL is incorrect
              // stop and fix
              throw new Exception("url not found $details", 1);
            } elseif (strpos($message, "missing parameter") !== false) {
              // URL get parameter missing
              // stop and fix
              throw new Exception("url not found $details", 1);
            } elseif (strpos($message, "invalid access token") !== false) {
              // access token is invalid,
            } elseif (strpos($message, "all origins are down") !== false) {
              // or Kroger API is down (temporarily)
            } elseif (strpos($message, "internal server Error") !== false) {
              // Kroger API had a problem
            }
            $ret[$key]["status"] = "fail";
            $ret[$key]["message"] = "guzzle connection error, try again";
            $ret[$key]["full"] = $response;
            // echo "<pre>return: " . print_r($ret[$key], true) . "</pre>\n";
          } else {
            $ret[$key]["status"] = "fail";
            $ret[$key]["full"] = $response;
            // echo "<pre>return: " . print_r($ret[$key], true) . "</pre>\n";
          }
        }

        // gather good responses
        // and URLs to check again
        $check_again = [];
        foreach ($ret as $key => $result) {
          // if success
          if (($result["status"] ?? "") == "success") {
            $good_responses[] = $result;
            unset($urls_data[$key]);
            continue;
          }

          // if fail
          $result_json = json_encode($result);
          echo "<pre>(check again) result: $result_json</pre>\n";
          $check_again[] = $urls_data[$key];
        }
        $urls_data = $check_again;

        // testing output
        // echo "<pre>good responses: " . count($good_responses) . "</pre>\n";
        // echo "<pre>check again count: " . count($urls_data) . "</pre>\n";
        // if (count($urls_data) > 0) {
        //   echo "<pre>check again: " . print_r($urls_data, true) . "</pre>\n";
        // }
        // $exec_time = round(microtime(true) - $start_time, 3) . " seconds";
        // echo "<p>exec_time: $exec_time</p>\n";

        // loop again if we need to
      } while (count($urls_data) > 0 && $attempt_count <= 3);
    }

    // clean up memory
    unset($client);

    return $good_responses;
  }

Add A Testing Controller and Route

Create A Controller

Run the following.

./vendor/bin/sail artisan make:controller KrogerApiController

Edit the file.

<?php // file: app/Http/Controllers/KrogerApiController.php

<?php

namespace App\Http\Controllers;

use App\Models\M\KrogerApi;
use Illuminate\Http\Request;

class KrogerApiController extends Controller
{
  public static function get_locations(Request $request)
  {
    $r = $request;
    $pattern = "/[^0-9,;]/";
    // call the api
    $response = KrogerApi::get_locations(
      preg_replace($pattern, "", $r->input("postal_codes", "")),
      preg_replace($pattern, "", $r->input("departments", "")),
      intval($r->input("limit", 20)),
      intval($r->input("radius_miles", 10))
    );
    // output json
    return response()->json($response);
  }

  public static function get_products(Request $request)
  {
    $r = $request;
    $pattern = "/[^a-zA-Z0-9\s\-\'\,\.\&]/";
    // call api
    $reponse = KrogerApi::get_products(
      preg_replace($pattern, "", $r->input("search_term", "")),
      preg_replace($pattern, "", $r->input("brand", "")),
      preg_replace("/[^0-9]/", "", $r->input("location_id", "")),
      intval($r->input("start_at", 1))
    );
    return response()->json($reponse);
  }
}

Create a Route

Edit the routes/api.php file.

// file: routes/api.php

use App\Http\Controllers\KrogerApiController;

Route::controller(KrogerApiController::class)->group(function () {
  Route::get("/get_locations", "get_locations");
  Route::get("/get_products", "get_products");
});

Kroger API Examples Results

Now we can view results in the browser.

http://localhost/api/get_locations?postal_codes=75201,75244

kroger api examples locations

http://localhost/api/get_products?search_term=apples&location_id=03500529

kroger api examples products

Leave a Comment