diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..9160059 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +service_name: travis-ci 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/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eb67138 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,46 @@ +language: php +dist: bionic +sudo: required + +matrix: + include: + - php: 5.3 + dist: precise + - php: 5.4 + dist: trusty + - php: 5.5 + dist: trusty + - php: 5.6 + dist: trusty + - php: 7.0 + dist: xenial + env: PHPUNIT=5.7 + - php: 7.1 + env: PHPUNIT=7.5 + - php: 7.2 + - php: 7.3 + env: COVERAGE=true + +install: + - composer install + - | + if [[ $PHPUNIT ]]; then + composer require "phpunit/phpunit:$PHPUNIT" + fi + +before_script: + - mkdir -p build/logs + +script: + - | + if [[ $PHPUNIT ]]; then + vendor/bin/phpunit --coverage-clover build/logs/clover.xml + else + phpunit --coverage-clover build/logs/clover.xml + fi + +after_success: + - | + if [[ $COVERAGE ]]; then + php vendor/bin/coveralls --config .coveralls.yml -v + fi diff --git a/LICENSE b/LICENSE index 9ccb0e7..7dcf161 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Josh Sherman +Copyright (c) 2016, 2017, 2018, 2019 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..302867a 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,211 @@ -# php-holidayapi -Official PHP library for [Holiday API](https://holidayapi.com) +# Holiday API PHP Library + +[![License](https://img.shields.io/npm/l/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) +![Build Status](https://img.shields.io/travis/holidayapi/holidayapi-php/master?style=for-the-badge) +[![Coverage Status](https://img.shields.io/coveralls/github/holidayapi/holidayapi-php/master?style=for-the-badge)](https://coveralls.io/github/holidayapi/holidayapi-php?branch=master) + +Official PHP library for [Holiday API](https://holidayapi.com) providing quick +and easy access to holiday information from applications written in PHP. + +## Migrating from 1.x + +Please note, version 2.x of this library is a full rewrite of the 1.x series. +The interfacing to the library has been simplified and existing applications +upgrading to 2.x will need to be updated. + +| Version 1.x Syntax (Old) | Version 2.x Syntax (New) | +|--------------------------------------------|-----------------------------------------------------------| +| `$holiday_api = new \HolidayAPI\v1($key);` | `$holiday_api = new \HolidayAPI\Client(['key' => $key]);` | + +Version 1.x of the library can still be found +[here](https://github.com/joshtronic/php-holidayapi). + +## 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 = 'Insert your API key here'; +$holiday_api = new \HolidayAPI\Client(['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 +$holiday_api->countries(); +``` + +#### Search for a country by code or name + +```php +$holiday_api->countries([ + 'search' => 'united', +]); +``` + +### Languages + +#### Fetch all supported languages + +```php +$holiday_api->languages(); +``` + +#### Search for a language by code or name + +```php +$holiday_api->languages([ + 'search' => 'Chinese', +]); +``` + +### Holidays + +#### Fetch holidays for a specific year + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, +]); +``` + +#### Fetch holidays for a specific month + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, +]); +``` + +#### Fetch holidays for a specific day + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, + 'day' => 4, +]); +``` + +#### Fetch upcoming holidays based on a specific date + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, + 'day' => 4, + 'upcoming' => true, +]); +``` + +#### Fetch previous holidays based on a specific date + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'month' => 7, + 'day' => 4, + 'previous' => true, +]); +``` + +#### Fetch only public holidays + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'public' => true, +]); +``` + +#### Fetch holidays for a specific subdivision + +```php +$holiday_api->holidays([ + 'country' => 'GB-ENG', + 'year' => 2019, +]); +``` + +#### Include subdivision holidays with countrywide holidays + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'subdivisions' => true, +]); +``` + +#### Search for a holiday by name + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'search' => 'New Year', +]); +``` + +#### Translate holidays to another language + +```php +$holiday_api->holidays([ + 'country' => 'US', + 'year' => 2019, + 'language' => 'zh', // Chinese (Simplified) +]); +``` + +#### Fetch holidays for multiple countries + +```php +$holiday_api->holidays([ + 'country' => 'US,GB,NZ', + 'year' => 2019, +]); + +$holiday_api->holidays([ + 'country' => ['US', 'GB', 'NZ'], + 'year' => 2019, +]); +``` diff --git a/composer.json b/composer.json index 78f9a03..c6fa09f 100644 --- a/composer.json +++ b/composer.json @@ -1,23 +1,28 @@ { - "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": "2.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": ">=5.3.0" + }, + "require-dev": { + "satooshi/php-coveralls": "~1.0" + }, + "autoload": { + "psr-4": { "HolidayAPI\\": "src/" } + } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..fa05f6b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + tests + + + + + ./src + + + diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..058f1d6 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,112 @@ +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)); + + /* + $curl = curl_init(); + + curl_setopt_array($curl, array( + CURLOPT_URL => $this->createUrl($endpoint, $request), + CURLOPT_HEADER => false, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_RETURNTRANSFER => true, + )); + + $response = curl_exec($curl); + + if ($error = curl_error($curl)) { + throw new \Exception($error); + } + + curl_close($curl); + $response = json_decode($response, true); + + if (!$response) { + throw new \Exception('Empty response received'); + } + + return $response; + */ + } + + public function countries($request = array()) + { + return $this->request('countries', $request); + } + + public function holidays($request) + { + if (!isset($request['country'])) { + throw new \Exception('Missing country'); + } elseif (!isset($request['year'])) { + throw new \Exception('Missing year'); + } elseif ( + isset($request['previous'], $request['upcoming']) + && $request['previous'] && $request['upcoming'] + ) { + throw new \Exception('Previous and upcoming are mutually exclusive'); + } + + return $this->request('holidays', $request); + } + + public function languages($request = array()) + { + return $this->request('languages', $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..c77611a --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,491 @@ +assertRegExp('/missing api key/i', $e->getMessage()); + } + } + + public function testInvalidKey() + { + try { + $client = new Client(array( + 'key' => 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz', + )); + } catch (\Exception $e) { + $this->assertRegExp('/invalid api key/i', $e->getMessage()); + } + } + + public function testVersionTooLow() + { + try { + $client = new Client(array('key' => self::KEY, 'version' => 0)); + } catch (\Exception $e) { + $this->assertRegExp('/invalid version/i', $e->getMessage()); + } + } + + public function testVersionTooHigh() + { + try { + $client = 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 testSearchCountries() + { + $url = self::BASE_URL . 'countries?key=' . self::KEY . '&search=Sao'; + + $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(array('search' => 'Sao'))); + } + + 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 testSearchHolidays() + { + $url = self::BASE_URL . 'holidays?key=' . self::KEY + . '&country=US&year=2015&search=Independence'; + + $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, + 'search' => 'Independence', + ))); + } + + public function testCountryMissing() + { + $client = new Client(array('key' => self::KEY)); + + try { + $client->holidays(array('year' => 2015)); + } catch (\Exception $e) { + $this->assertRegExp('/missing country/i', $e->getMessage()); + } + } + + public function testYearMissing() + { + $client = new Client(array('key' => self::KEY)); + + try { + $client->holidays(array('country' => 'US')); + } catch (\Exception $e) { + $this->assertRegExp('/missing year/i', $e->getMessage()); + } + } + + public function testBothPreviousAndUpcoming() + { + $client = new Client(array('key' => self::KEY)); + + try { + $client->holidays(array( + 'country' => 'US', + 'year' => 2015, + 'month' => 7, + 'day' => 4, + 'upcoming' => true, + 'previous' => true, + )); + } catch (\Exception $e) { + $this->assertRegExp('/previous and upcoming/i', $e->getMessage()); + } + } + + 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 testSearchLanguages() + { + $url = self::BASE_URL . 'languages?key=' . self::KEY . '&search=Eng'; + + $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(array('search' => 'Eng'))); + } + + 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()); + } + } +} + diff --git a/tests/RequestTest.php b/tests/RequestTest.php new file mode 100644 index 0000000..31594f3 --- /dev/null +++ b/tests/RequestTest.php @@ -0,0 +1,45 @@ +assertFalse($request->execute($curl)); + } + + public function testGet() + { + $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()); + } + } +} +