src/DMI.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
declare(strict_types=1);

namespace Rugaard\DMI;

use DateTime;
use DateTimeZone;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException as GuzzleClientException;
use GuzzleHttp\Exception\ServerException as GuzzleServerException;
use GuzzleHttp\Exception\GuzzleException;
use Rugaard\DMI\DTO\Forecast\Text;
use Rugaard\DMI\DTO\Location;
use Rugaard\DMI\DTO\SeaStation;
use Rugaard\DMI\Endpoints\Archive;
use Rugaard\DMI\Endpoints\Forecast;
use Rugaard\DMI\Endpoints\Pollen;
use Rugaard\DMI\Endpoints\Search;
use Rugaard\DMI\Endpoints\SeaStations;
use Rugaard\DMI\Endpoints\SunTimes;
use Rugaard\DMI\Endpoints\UV;
use Rugaard\DMI\Endpoints\Warnings;
use Rugaard\DMI\Exceptions\ClientException;
use Rugaard\DMI\Exceptions\DMIException;
use Rugaard\DMI\Exceptions\ParsingFailedException;
use Rugaard\DMI\Exceptions\ServerException;
use Rugaard\DMI\Exceptions\RequestException;
use Rugaard\DMI\Support\Traits\Municipalities;
use Throwable;
use Tightenco\Collect\Support\Collection;

/**
 * Class DMI.
 *
 * @package Rugaard\DMI
 */
class DMI
{
    use Municipalities;

    /**
     * Base URL of "NinJo" web service.
     *
     * @const string
     */
    public const DMI_WS_BASE_URL_NINJO = 'https://www.dmi.dk/NinJo2DmiDk/ninjo2dmidk';

    /**
     * Base URL of "city weather" web service.
     *
     * @const string
     */
    public const DMI_WS_BASE_URL_CITY_WEATHER = 'https://www.dmi.dk/dmidk_byvejrWS/rest';

    /**
     * Base URL of "observing" web service.
     *
     * @const string
     */
    public const DMI_WS_BASE_URL_OBSERVER = 'https://www.dmi.dk/dmidk_obsWS/rest';

    /**
     * Base URL of "search" web service.
     *
     * @const string
     */
    public const DMI_WS_BASE_URL_SEARCH = 'https://www.dmi.dk/solr/city_core/select';

    /**
     * Client instance.
     *
     * @var \GuzzleHttp\ClientInterface
     */
    protected $client;

    /**
     * Location ID.
     *
     * @var int
     */
    protected $id;

    /**
     * DMI constructor.
     *
     * @param int|null                         $defaultLocationId
     * @param \GuzzleHttp\ClientInterface|null $client
     */
    public function __construct(?int $defaultLocationId = null, ?ClientInterface $client = null)
    {
        if ($defaultLocationId !== null) {
            $this->setId($defaultLocationId);
        }

        if ($client !== null) {
            $this->setClient($client);
        }
    }

    /**
     * Search locations (City, POI etc.)
     *
     * @param  string $query
     * @param  int    $limit
     * @return \Rugaard\DMI\Endpoints\Search
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function search(string $query, int $limit = 20) : Search
    {
        try {
            // Request DMI API data.
            $data = $this->request(self::DMI_WS_BASE_URL_SEARCH . '?' . http_build_query([
                'q' => '(name:"' . $query . '" AND realm:1)^4 OR (name_ngram:"' . $query . '" AND realm:1)',
                'rows' => $limit,
                'sort' => 'score desc, realm desc, population desc',
                'wt' => 'json'
            ]));

            return new Search($data['response']['docs']);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get descriptive national forecast.
     *
     * @return \Rugaard\DMI\DTO\Forecast\Text
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function forecast() : Text
    {
        try {
            // Request DMI API data.
            $data = $this->request(self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/forecast/land/Danmark');

            // Extract forecast from data.
            $forecast = $data[0]['products'];

            // Decode nested XML.
            $forecast['text'] = (array) simplexml_load_string($forecast['text']);

            return new Text([
                'title' => 'Udsigt for hele Danmark',
                'text' => (string) ((array) $forecast['text']['udsigt'])['text'],
                'date' => (string) ((array) $forecast['text']['dato'])['text'],
                'validity' => (string) ((array) $forecast['text']['gyldighed'])['text'],
                'timestamp' => $forecast['timestamp']
            ]);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get extended (7 days) descriptive national forecast.
     *
     * @return \Rugaard\DMI\Endpoints\Forecast
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function extendedForecast() : Forecast
    {
        try {
            // Request DMI API data.
            $data = $this->request(self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/forecast/land7/Danmark');

            // Extract forecast from data.
            $forecast = $data[0]['products'];

            // Decode nested XML.
            $forecast['text'] = simplexml_load_string($forecast['text']);

            return new Forecast([
                'title' => (string) $forecast['text']->overskriftsyvdgn->text,
                'text' => (string) $forecast['text']->oversigt->text,
                'date' => (string) $forecast['text']->dato->text,
                'uncertainty' => trim((string) $forecast['text']->usikkerhed->text),
                'timestamp' => $forecast['timestamp'],
                'days' => [
                    $forecast['text']->dagnavnnul,
                    $forecast['text']->dagnavnet,
                    $forecast['text']->dagnavnto,
                    $forecast['text']->dagnavntre,
                    $forecast['text']->dagnavnfire,
                    $forecast['text']->dagnavnfem,
                    $forecast['text']->dagnavnsekssyv,
                ]
            ]);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get location by ID.
     *
     * @param  int|null $id
     * @param  bool     $includeRegional
     * @param  bool     $includeWarnings
     * @return \Rugaard\DMI\DTO\Location
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function location(?int $id = null, $includeRegional = true, bool $includeWarnings = true) : Location
    {
        try {
            // Request DMI API data.
            $data = $this->requestNinJo('llj', [
                'id' => $id ?? $this->getId(),
            ]);

            // Parse data.
            $location = new Location($data);

            // Include regional data and forecast.
            // NOTE: This will make an additional request to DMI.
            if ($includeRegional) {
                try {
                    // Request descriptive forecast.
                    $regionalData = $this->request(sprintf(
                        self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/%d',
                        $location->getId()
                    ));

                    // Parse descriptive forecast.
                    $location->parseRegional($regionalData);
                } catch (Throwable $e) { /* Silent fail */ }
            }

            // Include warnings for location.
            // NOTE: This will make an additional request to DMI.
            if ($includeWarnings) {
                try {
                    $data = $this->requestWithLocationId(
                        self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/varsler/geonameid/%d',
                        $location->getId()
                    );

                    // Collection of warnings.
                    $location->parseWarnings($data);
                } catch (Throwable $e) { /* Silent fail */ }
            }

            return $location;
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get location by coordinate (aka. nearest weather station).
     *
     * @param  float $latitude
     * @param  float $longitude
     * @param  bool  $includeRegional
     * @param  bool  $includeWarnings
     * @return \Rugaard\DMI\DTO\Location
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function locationByCoordinate(float $latitude, float $longitude, bool $includeRegional = true, bool $includeWarnings = true) : Location
    {
        try {
            // Request DMI API.
            $data = $this->requestNinJo('llj', [
                'lat' => $latitude,
                'lon' => $longitude
            ]);

            // Parse data.
            $location = new Location($data);

            // Include regional data and forecast.
            // NOTE: This will make an additional request to DMI.
            if ($includeRegional) {
                try {
                    // Request descriptive forecast.
                    $regionalData = $this->request(sprintf(
                        self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/%d',
                        $location->getId()
                    ));

                    // Parse descriptive forecast.
                    $location->parseRegional($regionalData);
                } catch (Throwable $e) { /* Silent fail */ }
            }

            // Include warnings for location.
            // NOTE: This will make an additional request to DMI.
            if ($includeWarnings) {
                try {
                    $data = $this->requestWithLocationId(
                        self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/varsler/geonameid/%d',
                        $location->getId()
                    );

                    // Collection of warnings.
                    $location->parseWarnings($data);
                } catch (Throwable $e) { /* Silent fail */ }
            }

            return $location;
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get national warnings.
     *
     * @return \Rugaard\DMI\Endpoints\Warnings|null
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function warnings() :? Warnings
    {
        try {
            // Request DMI API.
            $data = $this->request(self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/warnings/overview/Danmark');

            // Parse data.
            return new Warnings($data);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get sun times.
     *
     * @param  int|null $id
     * @return \Rugaard\DMI\Endpoints\SunTimes
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function sunTimes(?int $id = null) : SunTimes
    {
        try {
            // Request DMI API.
            $data = $this->requestWithLocationId(
                self::DMI_WS_BASE_URL_CITY_WEATHER . '/sunUpDown/%d',
                $id ?? $this->getId()
            );

            // Parse data.
            return new SunTimes($data);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get UV index.
     *
     * @param  int|null $id
     * @return \Rugaard\DMI\Endpoints\UV
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function uv(?int $id = null) : UV
    {
        try {
            // Request DMI API.
            $data = $this->requestWithLocationId(
                self::DMI_WS_BASE_URL_CITY_WEATHER . '/sunUpDown/UV/%d',
                $id ?? $this->getId()
            );

            // Parse data.
            return new UV($data['UV']);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get national pollen measurements.
     *
     * @return \Rugaard\DMI\Endpoints\Pollen
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function pollen() : Pollen
    {
        try {
            // Request DMI API.
            $data = $this->request(self::DMI_WS_BASE_URL_CITY_WEATHER . '/texts/forecast/pollen/Danmark');

            // Parse data.
            return new Pollen($data[0]);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get active sea stations.
     *
     * @param  bool $withObservations
     * @param  bool $withForecast
     * @return \Rugaard\DMI\Endpoints\SeaStations
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function seaStations(bool $withObservations = false, bool $withForecast = false) : SeaStations
    {
        try {
            // Request DMI API.
            $data = $this->request(self::DMI_WS_BASE_URL_CITY_WEATHER . '/vandstand/active');

            // Parse data.
            $seaStations = new SeaStations($data);

            if ($withObservations === true || $withForecast === true) {
                // Collect all station ID's.
                $seaStationIds = $seaStations->getData()->map(function ($seaStation) {
                    /* @var $seaStation \Rugaard\DMI\DTO\SeaStation */
                    return $seaStation->getId();
                });

                // Include sea station observations.
                // NOTE: This will make an additional request to DMI.
                if ($withObservations === true) {
                    // Request DMI API for observations.
                    $observationsData = Collection::make($this->requestNinJo('odj', [
                        'stations' => $seaStationIds->implode(','),
                        'datatype' => 'obs'
                    ]));

                    $seaStations->getData()->each(function ($seaStation) use ($observationsData) {
                        /* @var $seaStation \Rugaard\DMI\DTO\SeaStation */

                        // Get observations by station ID.
                        $observations = $observationsData->where('stationId', '=', $seaStation->getId())->first();

                        // Convert timestamp to a DateTime object.
                        $lastUpdatedTimestamp = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', (string) $observations['generatedTime'], new DateTimeZone('Europe/Copenhagen'));

                        // Add observations to station.
                        $seaStation->setObservations(Collection::make([
                            'lastUpdated' => $lastUpdatedTimestamp,
                            'data' => Collection::make($observations['values'])->reject(function ($item) {
                                return empty($item);
                            })->map(function ($item) {
                                // Convert timestamp to a DateTime object.
                                $item['timestamp'] = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', (string) $item['time'], new DateTimeZone('Europe/Copenhagen'));
                                unset($item['time']);
                                return Collection::make($item);
                            })
                        ]));
                    });
                }

                // Include sea station forecast.
                // NOTE: This will make an additional request to DMI.
                if ($withForecast === true) {
                    // Request DMI API for forecast.
                    $forecastData = Collection::make($this->requestNinJo('odj', [
                        'stations' => $seaStationIds->implode(','),
                        'datatype' => 'flt'
                    ]));

                    $seaStations->getData()->each(function ($seaStation) use ($forecastData) {
                        /* @var $seaStation \Rugaard\DMI\DTO\SeaStation */
                        // Get forecast by station ID.
                        $forecast = $forecastData->where('stationId', '=', $seaStation->getId())->first();

                        // Convert timestamp to a DateTime object.
                        $lastUpdatedTimestamp = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', (string) $forecast['generatedTime'], new DateTimeZone('Europe/Copenhagen'));

                        // Add observations to station.
                        $seaStation->setForecast(Collection::make([
                            'lastUpdated' => $lastUpdatedTimestamp,
                            'data' => Collection::make($forecast['values'])->reject(function ($item) {
                                return empty($item);
                            })->map(function ($item) {
                                // Convert timestamp to a DateTime object.
                                $item['timestamp'] = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', (string) $item['time'], new DateTimeZone('Europe/Copenhagen'));
                                unset($item['time']);
                                return Collection::make($item);
                            })
                        ]));

                        return $seaStation;
                    });
                }
            }

            // Parse data.
            return $seaStations;
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Get sea station by ID.
     *
     * @param  int  $stationId
     * @param  bool $withObservations
     * @param  bool $withForecast
     * @return \Rugaard\DMI\DTO\SeaStation|null
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function seaStation(int $stationId, bool $withObservations = false, bool $withForecast = false) :? SeaStation
    {
        // Get all active sea stations.
        $seaStations = $this->seaStations($withObservations, $withForecast)->getData();

        return $seaStations->first(function ($seaStation) use ($stationId) {
            /* @var $seaStation \Rugaard\DMI\DTO\SeaStation */
            return $seaStation->getId() === $stationId;
        });
    }

    /**
     * @param  string           $measurement
     * @param  string           $frequency
     * @param  \DateTime|string $period
     * @param  int|null         $municipalityId
     * @param  string           $country
     * @return \Rugaard\DMI\Endpoints\Archive
     * @throws \Rugaard\DMI\Exceptions\DMIException
     */
    public function archive(string $measurement, string $frequency, $period, ?int $municipalityId = null, string $country = 'DK') : Archive
    {
        // Validate required measurement.
        $supportedMeasurements = Collection::make(['temperature', 'precipitation', 'wind', 'wind-direction', 'humidity', 'pressure', 'sun', 'drought', 'lightning', 'snow']);
        if (!$supportedMeasurements->contains($measurement)) {
            throw new DMIException('Invalid measurement. Supported measurements are: "' . $supportedMeasurements->implode('", "') . '"', 400);
        }

        // Validate requested frequency..
        $supportedFrequencies = Collection::make(['hourly', 'daily', 'monthly', 'yearly']);
        if (!$supportedFrequencies->contains($frequency)) {
            throw new DMIException('Invalid frequency. Supported frequencies are: "' . $supportedFrequencies->implode('", "') . '"', 400);
        }

        // Validate municipality.
        if ($municipalityId !== null && !$this->getMunicipalities()->has($municipalityId)) {
            throw new DMIException('Invalid municipality ID. Check the documentation for a list of municipality ID\'s.', 400);
        }

        // Validate requested country.
        $supportedCountries = Collection::make(['DK' => 'Danmark', 'GL' => 'Grønland', 'FO' => 'Færøerne']);
        if (!$supportedCountries->has($country)) {
            throw new DMIException('Invalid country. Supported countries are: "' . $supportedCountries->keys()->implode('", "') . '"', 400);
        }

        // Validate requested time period.
        if (!($period instanceof DateTime)) {
            if ($frequency === 'yearly') {
                $period = DateTime::createFromFormat('Y-m-d H:i:s', $period . '-01-01 02:00:00', new DateTimeZone('Europe/Copenhagen'));
            } elseif ($frequency === 'monthly') {
                $period = DateTime::createFromFormat('Y-m-d H:i:s', $period . '-01 02:00:00', new DateTimeZone('Europe/Copenhagen'));
            } else {
                $period = DateTime::createFromFormat('Y-m-d H:i:s', $period . ' 02:00:00', new DateTimeZone('Europe/Copenhagen'));
            }

            if ($period === false) {
                throw new DMIException('Invalid time period provided. Provide either a DateTime object or a string in the format corresponding to the frequency.');
            }
        }

        // Convert measurement type to URL slug.
        switch ($measurement) {
            case 'wind-direction':
                $measurementSlug = 'winddir';
                break;
            case 'precipitation':
                $measurementSlug = 'precip';
                break;
            case 'sun':
                $measurementSlug = 'sunhours';
                break;
            default:
                $measurementSlug = $measurement;
        }

        // For some reason DMI has some inconsistency
        // in how their country naming works. This is a fix for that.
        if ($country === 'DK') {
            $country = 'danmark';
        }

        try {
            // Request DMI API.
            switch ($frequency) {
                case 'yearly':
                    $data = $this->request(sprintf(
                        self::DMI_WS_BASE_URL_OBSERVER . '/archive/%s/%s/%s/%s/%d/%d',
                        $frequency,
                        $country,
                        $measurementSlug,
                        $municipalityId !== null ? $this->getMunicipalities()->get($municipalityId) : 'Hele landet',
                        $period->format('Y') - 8,
                        $period->format('Y')
                    ));
                    break;
                case 'monthly':
                    $data = $this->request(sprintf(
                        self::DMI_WS_BASE_URL_OBSERVER . '/archive/%s/%s/%s/%s/%d',
                        $frequency,
                        $country,
                        $measurementSlug,
                        $municipalityId !== null ? $this->getMunicipalities()->get($municipalityId) : 'Hele landet',
                        $period->format('Y')
                    ));
                    break;
                case 'daily':
                    $data = $this->request(sprintf(
                        self::DMI_WS_BASE_URL_OBSERVER . '/archive/%s/%s/%s/%s/%d/%s',
                        $frequency,
                        $country,
                        $measurementSlug,
                        $municipalityId !== null ? $this->getMunicipalities()->get($municipalityId) : 'Hele landet',
                        $period->format('Y'),
                        getDanishMonthNameByMonthNo((int) $period->format('n'))
                    ));
                    break;
                default:
                    $data = $this->request(sprintf(
                        self::DMI_WS_BASE_URL_OBSERVER . '/archive/%s/%s/%s/%s/%d/%s/%d',
                        $frequency,
                        $country,
                        $measurementSlug,
                        $municipalityId !== null ? $this->getMunicipalities()->get($municipalityId) : 'Hele landet',
                        $period->format('Y'),
                        getDanishMonthNameByMonthNo((int) $period->format('n')),
                        $period->format('d')
                    ));
            }

            // Parse data.
            return new Archive($measurement, $frequency, $period, $data);
        } catch (Throwable $e) {
            throw new DMIException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Send request to DMI's NinJo web service.
     *
     * @param  string $command
     * @param  array  $parameters
     * @return array
     * @throws \Rugaard\DMI\Exceptions\ParsingFailedException
     */
    private function requestNinJo(string $command, array $parameters) : array
    {
        return $this->request(sprintf(
            '%s?cmd=%s&%s',
            self::DMI_WS_BASE_URL_NINJO,
            $command,
            http_build_query($parameters)
        ));
    }

    /**
     * Send request to DMI's city web service.
     *
     * @param  string $url
     * @param  int    $locationId
     * @return array
     * @throws \Rugaard\DMI\Exceptions\ParsingFailedException
     * @throws \Rugaard\DMI\Exceptions\ServerException
     * @throws \Rugaard\DMI\Exceptions\ClientException
     * @throws \Rugaard\DMI\Exceptions\RequestException
     */
    private function requestWithLocationId(string $url, int $locationId) : array
    {
        return $this->request(sprintf($url, $locationId));
    }

    /**
     * Send request to DMI.
     *
     * @param  string $url
     * @return array
     * @throws \Rugaard\DMI\Exceptions\ParsingFailedException
     * @throws \Rugaard\DMI\Exceptions\ServerException
     * @throws \Rugaard\DMI\Exceptions\ClientException
     * @throws \Rugaard\DMI\Exceptions\RequestException
     */
    private function request(string $url) : array
    {
        // If no client instance has been set,
        // we'll setup a default one with gzip enabled.
        if (!$this->hasClient()) {
            $this->defaultClient();
        }

        try {
            // Send request.
            /* @var $response \Psr\Http\Message\ResponseInterface */
            $response = $this->getClient()->request('get', $url);

            // If response is being returned with "204 No Content"
            // we'll just return an empty array.
            if ($response->getStatusCode() === 204) {
                return [];
            }

            // Extract body from response.
            $body = (string) $response->getBody();

            // JSON Decode response.
            $data = json_decode($body, true);

            // Make sure that the decoding procedure didn't fail.
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new ParsingFailedException(sprintf('Could not JSON decode response. Reason: %s.', json_last_error_msg()), 400);
            }

            return $data;
        } catch (GuzzleServerException $e) {
            throw new ServerException($e->getMessage(), $e->getRequest(), $e->getResponse(), $e);
        } catch (GuzzleClientException $e) {
            throw new ClientException($e->getMessage(), $e->getRequest(), $e->getResponse(), $e);
        } catch (GuzzleException $e) {
            throw new RequestException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Set location ID.
     *
     * @param  int $id
     * @return \Rugaard\DMI\DMI
     */
    public function setId(int $id) : self
    {
        $this->id = $id;
        return $this;
    }

    /**
     * Get location ID.
     *
     * @return int|null
     */
    public function getId() :? int
    {
        return $this->id;
    }

    /**
     * Set a default client instance.
     *
     * @return void
     */
    protected function defaultClient() : void
    {
        $this->setClient(new GuzzleClient([
            'headers' => [
                'Accept-Encoding' => 'gzip',
            ]
        ]));
    }

    /**
     * Check that we have a client instance.
     *
     * @return bool
     */
    public function hasClient() : bool
    {
        return $this->getClient() !== null;
    }

    /**
     * Set client instance.
     *
     * @param  \GuzzleHttp\ClientInterface $client
     * @return $this
     */
    public function setClient(ClientInterface $client) : self
    {
        $this->client = $client;
        return $this;
    }

    /**
     * Get client instance.
     *
     * @return \GuzzleHttp\ClientInterface|null
     */
    public function getClient() :? ClientInterface
    {
        return $this->client;
    }
}