diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7e0764d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Test +on: [push, pull_request] +jobs: + test: + name: Test PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + strategy: + matrix: + php-version: ['8.0', '8.1', '8.2', '8.3', '8.4'] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + - name: PHP Version + run: php --version + - name: Composer Version + run: composer --version + - name: Install Dependencies + run: COMPOSER_MEMORY_LIMIT=-1 composer install + - name: Run Tests + run: vendor/bin/phpunit --coverage-clover ./coverage.xml + - name: Upload Coverage + if: ${{ matrix.php-version == '8.2' }} + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml diff --git a/.gitignore b/.gitignore index c422267..a2144a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ +composer.lock composer.phar +.phpunit.result.cache /vendor/ - -# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file -# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock diff --git a/LICENSE b/LICENSE index 9ccb0e7..ab5ad02 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Josh Sherman +Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Gravity Boulevard, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6a07365..81a1456 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,266 @@ -# php-holidayapi -Official PHP library for [Holiday API](https://holidayapi.com) +# Holiday API PHP Library + +[![License](https://img.shields.io/packagist/l/holidayapi/holidayapi-php?style=for-the-badge)](https://github.com/holidayapi/holidayapi-php/blob/master/LICENSE) +![PHP Version](https://img.shields.io/packagist/php-v/holidayapi/holidayapi-php?style=for-the-badge) +[![Code Coverage](https://img.shields.io/codecov/c/github/holidayapi/holidayapi-php?style=for-the-badge)](https://codecov.io/gh/holidayapi/holidayapi-php) + +Official PHP library for [Holiday API](https://holidayapi.com) providing quick +and easy access to holiday information from applications written in PHP. + +## Documentation + +Full documentation of the Holiday API endpoints is available +[here](https://holidayapi.com/docs). ## Installation ```shell -composer require "joshtronic/php-holidayapi:dev-master" +composer require holidayapi/holidayapi-php ``` ## Usage ```php -$hapi = new HolidayAPI\v1('_YOUR_API_KEY_'); + $key]); -$parameters = array( - // Required - 'country' => 'US', - 'year' => 2016, - // Optional - // 'month' => 7, - // 'day' => 4, - // 'previous' => true, - // 'upcoming' => true, - // 'public' => true, - // 'pretty' => true, -); +try { + // Fetch supported countries and subdivisions + $countries = $holiday_api->countries(); -$response = $hapi->holidays($parameters); + // Fetch supported languages + $languages = $holiday_api->languages(); + + // Fetch holidays with minimum parameters + $holidays = $holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + ]); + + var_dump($countries, $languages, $holidays); +} catch (Exception $e) { + var_dump($e); +} ``` +## Examples + +### Countries + +#### Fetch all supported countries + +```php +countries(); +``` + +#### Fetch only countries with public holidays + +```php +countries([ + 'public' => true, +]); +``` + +#### Fetch a supported country by code + +```php +countries([ + 'country' => 'NO', +]); +``` + +#### Search for countries by code or name + +```php +countries([ + 'search' => 'united', +]); +``` + +### Languages + +#### Fetch all supported languages + +```php +languages(); +``` + +#### Fetch a supported language by code + +```php +languages([ + 'language' => 'es', +]); +``` + +#### Search for languages by code or name + +```php +languages([ + 'search' => 'Chinese', +]); +``` + +### Holidays + +#### Fetch holidays for a specific year + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, +]); +``` + +#### Fetch holidays for a specific month + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, +]); +``` + +#### Fetch holidays for a specific day + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, + 'day' => 4, +]); +``` + +#### Fetch upcoming holidays based on a specific date + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, + 'day' => 4, + 'upcoming' => true, +]); +``` + +#### Fetch previous holidays based on a specific date + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, + 'day' => 4, + 'previous' => true, +]); +``` + +#### Fetch only public holidays + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'public' => true, +]); +``` + +#### Fetch holidays for a specific subdivision + +```php +holidays([ + 'country' => 'GB-ENG', + 'year' => 2019, +]); +``` + +#### Include subdivision holidays with countrywide holidays + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'subdivisions' => true, +]); +``` + +#### Search for a holiday by name + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'search' => 'New Year', +]); +``` + +#### Translate holidays to another language + +```php +holidays([ + 'country' => 'US', + 'year' => 2019, + 'language' => 'zh', // Chinese (Simplified) +]); +``` + +#### Fetch holidays for multiple countries + +```php +holidays([ + 'country' => 'US,GB,NZ', + 'year' => 2019, +]); + +$holiday_api->holidays([ + 'country' => ['US', 'GB', 'NZ'], + 'year' => 2019, +]); +``` + +### Workday + +#### Fetch workday 7 business days after a date + +```php +workday([ + 'country' => 'US', + 'start' => '2019-07-01', + 'days' => 7, +]); +``` + +### Workdays + +#### Fetch number of workdays between two dates + +```php +workdays([ + 'country' => 'US', + 'start' => '2019-07-01', + 'end' => '2019-07-10', +]); +``` diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..657d8f7 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,20 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: no diff --git a/composer.json b/composer.json index 78f9a03..3129ca8 100644 --- a/composer.json +++ b/composer.json @@ -1,23 +1,37 @@ { - "name": "joshtronic/php-holidayapi", - "description": "Official PHP library for Holiday API", - "version": "1.0.0", - "type": "library", - "keywords": [ "holiday", "holidays", "holidayapi" ], - "homepage": "https://github.com/joshtronic/php-holidayapi", - "license": "MIT", - "authors": [{ - "name": "Josh Sherman", - "email": "hello@holidayapi.com", - "homepage": "https://holidayapi.com," - }], - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "satooshi/php-coveralls": "~1.0" - }, - "autoload": { - "psr-4": { "HolidayAPI\\": "src/" } + "name": "holidayapi/holidayapi-php", + "description": "Official PHP library for Holiday API", + "version": "4.0.0", + "type": "library", + "keywords": [ + "calendar", + "holiday", + "holidays", + "holidayapi" + ], + "homepage": "https://github.com/holidayapi/holidayapi-php", + "license": "MIT", + "authors": [{ + "name": "Josh Sherman", + "email": "hello@holidayapi.com", + "homepage": "https://holidayapi.com" + }], + "require": { + "php": ">=8.0", + "ext-json": "*", + "ext-curl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "autoload": { + "psr-4": { + "HolidayAPI\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "HolidayAPI\\Tests\\": "tests/" + } + } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..92f2230 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,24 @@ + + + + + tests + + + + + ./src + + + diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..0793c97 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,85 @@ +baseUrl = "https://holidayapi.com/v{$version}/"; + $this->key = $options['key']; + + if (isset($options['handler'])) { + $this->handler = $options['handler']; + } else { + $this->handler = new Request(); + } + } + + private function createUrl($endpoint, $request = array()) + { + $parameters = array_merge(array('key' => $this->key), $request); + $parameters = http_build_query($parameters); + + return "{$this->baseUrl}{$endpoint}?{$parameters}"; + } + + private function request($endpoint, $request) + { + return $this->handler->get($this->createUrl($endpoint, $request)); + } + + public function countries($request = array()) + { + return $this->request('countries', $request); + } + + public function holidays($request) + { + return $this->request('holidays', $request); + } + + public function languages($request = array()) + { + return $this->request('languages', $request); + } + + public function workday($request) + { + return $this->request('workday', $request); + } + + public function workdays($request) + { + return $this->request('workdays', $request); + } +} + diff --git a/src/Request.php b/src/Request.php new file mode 100644 index 0000000..355e69a --- /dev/null +++ b/src/Request.php @@ -0,0 +1,79 @@ +handlers = $handlers; + } + + public function execute($curl) + { + if (isset($this->handlers['execute'])) { + $info = curl_getinfo($curl); + $url = $info['url']; + + if (isset($this->handlers['execute'][$url])) { + return $this->handlers['execute'][$url]($curl); + } + } + + return curl_exec($curl); + } + + public function error($curl) + { + if (isset($this->handlers['error'])) { + $info = curl_getinfo($curl); + $url = $info['url']; + + if (isset($this->handlers['error'][$url])) { + return $this->handlers['error'][$url]($curl); + } + } + + return curl_error($curl); + } + + public function get($url) + { + $curl = curl_init(); + + curl_setopt_array($curl, array( + CURLOPT_URL => $url, + CURLOPT_HEADER => false, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_RETURNTRANSFER => true, + )); + + $response = $this->execute($curl); + + if ($error = $this->error($curl)) { + throw new \Exception($error); + } + + curl_close($curl); + $response = json_decode($response, true); + + if (!$response) { + throw new \Exception('Empty response received'); + } + + if (isset($response['error'])) { + throw new \Exception($response['error'], $response['status']); + } + + return $response; + } +} + diff --git a/src/v1.php b/src/v1.php deleted file mode 100644 index 472bd62..0000000 --- a/src/v1.php +++ /dev/null @@ -1,51 +0,0 @@ -parameters[$variable] = $value; - } - - public function __construct($key = null) - { - if ($key) { - $this->key = $key; - } - } - - public function holidays($parameters = array()) - { - $parameters = array_merge($this->parameters, $parameters); - $parameters = http_build_query($parameters); - - $url = 'https://holidayapi.com/v1/holidays?' . $parameters; - $curl = curl_init(); - - curl_setopt_array($curl, array( - CURLOPT_URL => $url, - CURLOPT_HEADER => false, - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_RETURNTRANSFER => true, - )); - - $response = curl_exec($curl); - - if ($error = curl_error($curl)) { - return false; - } - - curl_close($curl); - $response = json_decode($response, true); - - if (!$response) { - return false; - } - - return $response; - } -} - diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 0000000..14f83c7 --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,539 @@ +=')) { + $assertRegExp = 'assertMatchesRegularExpression'; + } else { + $assertRegExp = 'assertRegExp'; + } + + try { + new Client(array()); + } catch (\Exception $e) { + $this->$assertRegExp('/missing api key/i', $e->getMessage()); + } + } + + public function testInvalidKey() + { + if (version_compare(PHP_VERSION, '7.3.0', '>=')) { + $assertRegExp = 'assertMatchesRegularExpression'; + } else { + $assertRegExp = 'assertRegExp'; + } + + try { + new Client(array( + 'key' => 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz', + )); + } catch (\Exception $e) { + $this->$assertRegExp('/invalid api key/i', $e->getMessage()); + } + } + + public function testVersionTooLow() + { + if (version_compare(PHP_VERSION, '7.3.0', '>=')) { + $assertRegExp = 'assertMatchesRegularExpression'; + } else { + $assertRegExp = 'assertRegExp'; + } + + try { + new Client(array('key' => self::KEY, 'version' => 0)); + } catch (\Exception $e) { + $this->$assertRegExp('/invalid version/i', $e->getMessage()); + } + } + + public function testVersionTooHigh() + { + if (version_compare(PHP_VERSION, '7.3.0', '>=')) { + $assertRegExp = 'assertMatchesRegularExpression'; + } else { + $assertRegExp = 'assertRegExp'; + } + + try { + new Client(array('key' => self::KEY, 'version' => 2)); + } catch (\Exception $e) { + $this->$assertRegExp('/invalid version/i', $e->getMessage()); + } + } + + public function testAssignClassMembers() + { + $client = new Client(array('key' => self::KEY)); + + $this->assertSame(self::BASE_URL, $client->baseUrl); + $this->assertSame(self::KEY, $client->key); + } + + public function testReturnCountries() + { + $url = self::BASE_URL . 'countries?key=' . self::KEY; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 200, + 'countries' => array( + array( + 'code' => 'ST', + 'name' => 'Sao Tome and Principle', + ), + ), + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + $this->assertEquals(array( + 'status' => 200, + 'countries' => array( + array( + 'code' => 'ST', + 'name' => 'Sao Tome and Principle', + ), + ), + ), $client->countries()); + } + + public function testCountriesRaise4xxErrors() + { + $url = self::BASE_URL . 'countries?key=' . self::KEY; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 429, + 'error' => 'Rate limit exceeded', + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->countries(); + } catch (\Exception $e) { + $this->assertSame(429, $e->getCode()); + $this->assertSame('Rate limit exceeded', $e->getMessage()); + } + } + + public function testCountriesRaise5xxErrors() + { + $url = self::BASE_URL . 'countries?key=' . self::KEY; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return false; + }, + ), + 'error' => array( + $url => function () + { + return 'Internal server error'; + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->countries(); + } catch (\Exception $e) { + $this->assertSame('Internal server error', $e->getMessage()); + } + } + + public function testReturnHolidays() + { + $url = self::BASE_URL . 'holidays?key=' . self::KEY . '&country=US&year=2015&month=7&day=4'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 200, + 'holidays' => array( + array( + 'name' => 'Independence Day', + 'date' => '2015-07-04', + 'observed' => '2015-07-03', + 'public' => true, + ), + ), + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + $this->assertEquals(array( + 'status' => 200, + 'holidays' => array( + array( + 'name' => 'Independence Day', + 'date' => '2015-07-04', + 'observed' => '2015-07-03', + 'public' => true, + ), + ), + ), $client->holidays(array( + 'country' => 'US', + 'year' => 2015, + 'month' => 7, + 'day' => 4, + ))); + } + + public function testHolidaysRaise4xxErrors() + { + $url = self::BASE_URL . 'holidays?key=' . self::KEY . '&country=US&year=2019'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 429, + 'error' => 'Rate limit exceeded', + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->holidays(array('country' => 'US', 'year' => 2019)); + } catch (\Exception $e) { + $this->assertSame(429, $e->getCode()); + $this->assertSame('Rate limit exceeded', $e->getMessage()); + } + } + + public function testHolidaysRaise5xxErrors() + { + $url = self::BASE_URL . 'holidays?key=' . self::KEY . '&country=US&year=2019'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return false; + }, + ), + 'error' => array( + $url => function () + { + return 'Internal server error'; + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->holidays(array('country' => 'US', 'year' => 2019)); + } catch (\Exception $e) { + $this->assertSame('Internal server error', $e->getMessage()); + } + } + + public function testReturnLanguages() + { + $url = self::BASE_URL . 'languages?key=' . self::KEY; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 200, + 'languages' => array( + array( + 'code' => 'en', + 'name' => 'English', + ), + ), + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + $this->assertEquals(array( + 'status' => 200, + 'languages' => array( + array( + 'code' => 'en', + 'name' => 'English', + ), + ), + ), $client->languages()); + } + + public function testLanguagesRaise4xxErrors() + { + $url = self::BASE_URL . 'languages?key=' . self::KEY; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 429, + 'error' => 'Rate limit exceeded', + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->languages(); + } catch (\Exception $e) { + $this->assertSame(429, $e->getCode()); + $this->assertSame('Rate limit exceeded', $e->getMessage()); + } + } + + public function testLanguagesRaise5xxErrors() + { + $url = self::BASE_URL . 'languages?key=' . self::KEY; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return false; + }, + ), + 'error' => array( + $url => function () + { + return 'Internal server error'; + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->languages(); + } catch (\Exception $e) { + $this->assertSame('Internal server error', $e->getMessage()); + } + } + + public function testReturnWorkday() + { + $url = self::BASE_URL . 'workday?key=' . self::KEY . '&country=US&start=2019-07-01&days=10'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 200, + 'workday' => array( + 'date' => '2019-07-16', + ), + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + $this->assertEquals(array( + 'status' => 200, + 'workday' => array( + 'date' => '2019-07-16', + ), + ), $client->workday(array( + 'country' => 'US', + 'start' => '2019-07-01', + 'days' => 10, + ))); + } + + public function testWorkdayRaise4xxErrors() + { + $url = self::BASE_URL . 'workday?key=' . self::KEY . '&country=US&start=2019-07-01&days=10'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 429, + 'error' => 'Rate limit exceeded', + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->workday(array( + 'country' => 'US', + 'start' => '2019-07-01', + 'days' => 10, + )); + } catch (\Exception $e) { + $this->assertSame(429, $e->getCode()); + $this->assertSame('Rate limit exceeded', $e->getMessage()); + } + } + + public function testWorkdayRaise5xxErrors() + { + $url = self::BASE_URL . 'workday?key=' . self::KEY . '&country=US&start=2019-07-01&days=10'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return false; + }, + ), + 'error' => array( + $url => function () + { + return 'Internal server error'; + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->workday(array( + 'country' => 'US', + 'start' => '2019-07-01', + 'days' => 10, + )); + } catch (\Exception $e) { + $this->assertSame('Internal server error', $e->getMessage()); + } + } + + public function testReturnWorkdays() + { + $url = self::BASE_URL . 'workdays?key=' . self::KEY . '&country=US&start=2019-07-01&end=2019-07-10'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 200, + 'workdays' => 7, + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + $this->assertEquals(array( + 'status' => 200, + 'workdays' => 7, + ), $client->workdays(array( + 'country' => 'US', + 'start' => '2019-07-01', + 'end' => '2019-07-10', + ))); + } + + public function testWorkdaysRaise4xxErrors() + { + $url = self::BASE_URL . 'workdays?key=' . self::KEY . '&country=US&start=2019-07-01&end=2019-07-10'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return json_encode(array( + 'status' => 429, + 'error' => 'Rate limit exceeded', + )); + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->workdays(array( + 'country' => 'US', + 'start' => '2019-07-01', + 'end' => '2019-07-10', + )); + } catch (\Exception $e) { + $this->assertSame(429, $e->getCode()); + $this->assertSame('Rate limit exceeded', $e->getMessage()); + } + } + + public function testWorkdaysRaise5xxErrors() + { + $url = self::BASE_URL . 'workdays?key=' . self::KEY . '&country=US&start=2019-07-01&end=2019-07-10'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return false; + }, + ), + 'error' => array( + $url => function () + { + return 'Internal server error'; + }, + ), + )); + + $client = new Client(array('key' => self::KEY, 'handler' => $request)); + + try { + $client->workdays(array( + 'country' => 'US', + 'start' => '2019-07-01', + 'end' => '2019-07-10', + )); + } catch (\Exception $e) { + $this->assertSame('Internal server error', $e->getMessage()); + } + } +} + diff --git a/tests/RequestTest.php b/tests/RequestTest.php new file mode 100644 index 0000000..f42776d --- /dev/null +++ b/tests/RequestTest.php @@ -0,0 +1,42 @@ +assertFalse($request->execute($curl)); + } + + public function testGet() + { + if (version_compare(PHP_VERSION, '7.3.0', '>=')) { + $assertRegExp = 'assertMatchesRegularExpression'; + } else { + $assertRegExp = 'assertRegExp'; + } + + $url = 'https://holidayapi.com'; + + $request = new Request(array( + 'execute' => array( + $url => function () + { + return ''; + }, + ), + )); + + try { + $request->get($url); + } catch (\Exception $e) { + $this->$assertRegExp('/empty response/i', $e->getMessage()); + } + } +} +