Encapsulating the OpenWeatherMap API in a data source


1Introduction

In this tutorial, we'll look at how to create an object that can be used as a data source. For the example, we're going to make an object that interfaces with the OpenWeatherMap service API, enabling us to retrieve the current weather for any location (to be precise, the OpenWeatherMap API takes a latitude and longitude as parameters; we'll therefore use the Geocoding API to obtain the geographic coordinates of the requested city).


2Configuration

In the etc/temma.php configuration file, we'll add the definition of a rather special data source. As this is not a data source natively managed by Temma, we'll prefix the DSN with the name of the object to be used (in square brackets).

The DSN will take the form openweather://API_KEY
API_KEY being the API key provided by OpenWeatherMap.

Example configuration:

<?php

return [
    "application" => [
        "dataSources" => [
            "meteo" => '[\OpenWeatherMap\Weather]openweather://0123456789abcdef0123456789abcdef'
        ]
    ]
];

3Source code

Here's the code for our data source, which we'll save in the lib/OpenWeatherMap/Weather.php file:

<?php

namespace OpenWeatherMap;

class Weather extends \Temma\Base\Datasource {
    /** Constant: DSN prefix. */
    const DSN_SCHEME = 'weather://';
    /** Constant: URL of the OpenWeatherMap data API. */
    const DATA_API_URL = 'https://api.openweathermap.org/data/2.5/weather';
    /** Constant: URL of the OpenWeatherMap geocoding API. */
    const GEO_API_URL = 'http://api.openweathermap.org/geo/1.0/direct';
    /** API access key. */
    private $_key = null;

    /**
     * Factory. Creates an instance of the object from a DSN.
     * @param   string   $dsn   Configuration string.
     * @return  \OpenWeatherMap\Weather    The instantiated object.
     * @throws  \Exception   If the DSN is incorrect.
     */
    static public function factory(string $dsn) : \OpenWeatherMap\Weather {
        if (!str_starts_with($dsn, self::DSN_SCHEME)) {
            throw new \Exception("Invalid OpenWeatherMap DSN '$dsn'.");
        }
        $key = mb_substr($dsn, mb_strlen(self::DSN_SCHEME));
        return (new self($key));
    }
    /**
     * Constructor.
     * @param   string   $key   API access key.
     */
    public function __construct(string $key) {
        $this->_key = $key;
    }
    /**
     * Retrieves the weather for a location.
     * @param   string   $place    Location, in the form "City" or "City, Country" or "City, Region, Country".
     * @param   mixed    $default  Unused parameter (required for inheritance reasons).
     * @param   mixed    $options  Unused parameter (required for inheritance reasons).
     * @return  mixed    Associative array containing weather data.
     * @throws  \Exceptions      If an error has occurred.
     */
    public function get(string $place, mixed $default=null, mixed $options=null) : mixed {
        // geographic data retrieval
        $query = http_build_query([
            'appid' => $this->_key,
            'limit' => 1,
            'q'     => $place,
        ]);
        $result = file_get_contents(self::GEO_API_URL . '?' . $query);
        $geo = json_decode($result, true);
        if (!isset($geo[0]['lat']) || !isset($get[0]['lon']) {
            throw new \Exception("Unable to get geographic data for '$place'.");
        }
        // weather data retrieval
        $query = http_build_query([
            'appid' => $this->_key,
            'lat'   => $geo[0]['lat'],
            'lon'   => $get[0]['lon'],
            'units' => 'metric',
        ]);
        $result = file_get_contents(self::DATA_API_URL . '?' . $query);
        $weather = json_decode($result, true);
        if (!isset($weather['main']['temp']))
            throw new \Exception("Unable to get weather data for '$place'.");
        return [
            'temperature' => $weather['main']['temp'],
            'perceived'   => $weather['main']['feels_like'],
            'pressure'    => $weather['main']['pressure'],
            'humidity'    => $weather['main']['humidity'],
        ];
    }
}
  • Lines 21 to 27: factory() method, called by Temma when the data source is created. This method interprets the DSN and calls the constructor.
  • Lines 32 to 34: Constructor, used to create an instance of the object directly.
  • Lines 43 to 72: get() method, which overrides the get() method of the parent \Temma\Base\Datasource object. This method can be used directly, but is also called automatically when read as an array.
  • Lines 45 to 54: Connection to the Geocoding API, to obtain the latitude and longitude of the requested city.
  • Lines 56 to 65: Connection to the OpenWeatherMap API, to obtain weather information from the latitude and longitude previously retrieved.

4Usage

In a controller, the data source named "meteo" in the configuration file is available by writing $this->meteo. Weather information can be retrieved by calling the get() method or by using it as an array:

// use get() method
$data = $this->meteo->get('Paris, FR');
// write as an array
$data = $this->meteo['Paris, FR'];

In another object, you need to use the dependency injection component:

$data = $this->_loader->meteo->get('Paris, FR');
$data = $this->_loader->meteo['Paris, FR'];

The retrieved data is stored in an associative array:


printf("Temperature measured: %f °C\n", $data['temperature']);
printf("    Temperature felt: %f °C\n", $data['perceived']);
printf("Atmospheric pressure: %f hPa\n", $data['pressure']);
printf("            Humidity: %f %%\n", $data['humidity']);