From 5b37c48d36b310f7ecf2f72904324d7937ddc783 Mon Sep 17 00:00:00 2001 From: docjyJ Date: Sat, 22 Aug 2020 11:34:59 +0200 Subject: [PATCH] Add OpenFoodFacts 0.2.3 lib --- openfoodfacts-php-0.2.3/.gitignore | 18 + openfoodfacts-php-0.2.3/.travis.yml | 5 + openfoodfacts-php-0.2.3/CHANGELOG.md | 3 + openfoodfacts-php-0.2.3/CONTRIBUTING.md | 7 + openfoodfacts-php-0.2.3/LICENSE | 21 + openfoodfacts-php-0.2.3/README.md | 78 +++ openfoodfacts-php-0.2.3/composer.json | 41 ++ openfoodfacts-php-0.2.3/doc/home.md | 1 + .../examples/00_flat_request/index.html | 13 + .../examples/00_flat_request/request.php | 31 + .../examples/00_flat_request/response.html | 28 + .../01-basic_api_usage/cached_example.php | 13 + openfoodfacts-php-0.2.3/phpunit.xml | 36 ++ openfoodfacts-php-0.2.3/src/Api.php | 536 ++++++++++++++++++ openfoodfacts-php-0.2.3/src/Collection.php | 133 +++++ openfoodfacts-php-0.2.3/src/Document.php | 83 +++ .../src/Document/BeautyDocument.php | 10 + .../src/Document/FoodDocument.php | 10 + .../src/Document/PetDocument.php | 10 + .../src/Document/ProductDocument.php | 10 + .../src/Exception/BadRequestException.php | 14 + .../Exception/ProductNotFoundException.php | 14 + .../src/FilesystemTrait.php | 23 + .../src/RecursiveSortingTrait.php | 38 ++ .../tests/ApiFoodCacheTest.php | 178 ++++++ openfoodfacts-php-0.2.3/tests/ApiFoodTest.php | 248 ++++++++ openfoodfacts-php-0.2.3/tests/ApiPetTest.php | 80 +++ 27 files changed, 1682 insertions(+) create mode 100644 openfoodfacts-php-0.2.3/.gitignore create mode 100644 openfoodfacts-php-0.2.3/.travis.yml create mode 100644 openfoodfacts-php-0.2.3/CHANGELOG.md create mode 100644 openfoodfacts-php-0.2.3/CONTRIBUTING.md create mode 100644 openfoodfacts-php-0.2.3/LICENSE create mode 100644 openfoodfacts-php-0.2.3/README.md create mode 100644 openfoodfacts-php-0.2.3/composer.json create mode 100644 openfoodfacts-php-0.2.3/doc/home.md create mode 100644 openfoodfacts-php-0.2.3/examples/00_flat_request/index.html create mode 100644 openfoodfacts-php-0.2.3/examples/00_flat_request/request.php create mode 100644 openfoodfacts-php-0.2.3/examples/00_flat_request/response.html create mode 100644 openfoodfacts-php-0.2.3/examples/01-basic_api_usage/cached_example.php create mode 100644 openfoodfacts-php-0.2.3/phpunit.xml create mode 100644 openfoodfacts-php-0.2.3/src/Api.php create mode 100644 openfoodfacts-php-0.2.3/src/Collection.php create mode 100644 openfoodfacts-php-0.2.3/src/Document.php create mode 100644 openfoodfacts-php-0.2.3/src/Document/BeautyDocument.php create mode 100644 openfoodfacts-php-0.2.3/src/Document/FoodDocument.php create mode 100644 openfoodfacts-php-0.2.3/src/Document/PetDocument.php create mode 100644 openfoodfacts-php-0.2.3/src/Document/ProductDocument.php create mode 100644 openfoodfacts-php-0.2.3/src/Exception/BadRequestException.php create mode 100644 openfoodfacts-php-0.2.3/src/Exception/ProductNotFoundException.php create mode 100644 openfoodfacts-php-0.2.3/src/FilesystemTrait.php create mode 100644 openfoodfacts-php-0.2.3/src/RecursiveSortingTrait.php create mode 100644 openfoodfacts-php-0.2.3/tests/ApiFoodCacheTest.php create mode 100644 openfoodfacts-php-0.2.3/tests/ApiFoodTest.php create mode 100644 openfoodfacts-php-0.2.3/tests/ApiPetTest.php diff --git a/openfoodfacts-php-0.2.3/.gitignore b/openfoodfacts-php-0.2.3/.gitignore new file mode 100644 index 0000000..90424b8 --- /dev/null +++ b/openfoodfacts-php-0.2.3/.gitignore @@ -0,0 +1,18 @@ +# Created by .ignore support plugin (hsz.mobi) +### Example user template template +### Example user template +/build/ + +# IntelliJ project files +.idea +*.iml +out +gen### Composer template +composer.phar +/vendor/ +/log/ +/tests/tmp/* + +# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file +# You may choose to ignore a library lock file https://getcomposer.org/doc/02-libraries.md#lock-file +composer.lock diff --git a/openfoodfacts-php-0.2.3/.travis.yml b/openfoodfacts-php-0.2.3/.travis.yml new file mode 100644 index 0000000..6b6c613 --- /dev/null +++ b/openfoodfacts-php-0.2.3/.travis.yml @@ -0,0 +1,5 @@ +language: php +php: + - '7.2' +before_script: composer install --dev +script: vendor/bin/phpunit diff --git a/openfoodfacts-php-0.2.3/CHANGELOG.md b/openfoodfacts-php-0.2.3/CHANGELOG.md new file mode 100644 index 0000000..2156042 --- /dev/null +++ b/openfoodfacts-php-0.2.3/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 (June 19, 2016) ## + +* Upload project struct \ No newline at end of file diff --git a/openfoodfacts-php-0.2.3/CONTRIBUTING.md b/openfoodfacts-php-0.2.3/CONTRIBUTING.md new file mode 100644 index 0000000..f7cbf08 --- /dev/null +++ b/openfoodfacts-php-0.2.3/CONTRIBUTING.md @@ -0,0 +1,7 @@ +## Contributing + +1. Fork it ( https://github.com/openfoodfacts/openfoodfacts-php/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request \ No newline at end of file diff --git a/openfoodfacts-php-0.2.3/LICENSE b/openfoodfacts-php-0.2.3/LICENSE new file mode 100644 index 0000000..09ada78 --- /dev/null +++ b/openfoodfacts-php-0.2.3/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Open Food Facts + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/openfoodfacts-php-0.2.3/README.md b/openfoodfacts-php-0.2.3/README.md new file mode 100644 index 0000000..9599159 --- /dev/null +++ b/openfoodfacts-php-0.2.3/README.md @@ -0,0 +1,78 @@ +# openfoodfacts-php +![Open Food Facts](https://static.openfoodfacts.org/images/misc/openfoodfacts-logo-en-178x150.png) + +PHP API Wrapper for [Open Food Facts](https://openfoodfacts.org/), the open database about food. + +[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges) +[![Build Status](https://travis-ci.org/openfoodfacts/openfoodfacts-php.svg?branch=master)](https://travis-ci.org/openfoodfacts/openfoodfacts-php) +[![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/openfoodfacts/openfoodfacts-php.svg)](https://isitmaintained.com/project/openfoodfacts/openfoodfacts-php "Average time to resolve an issue") +[![Percentage of issues still open](https://isitmaintained.com/badge/open/openfoodfacts/openfoodfacts-php.svg)](https://isitmaintained.com/project/openfoodfacts/openfoodfacts-php "Percentage of issues still open") + +## Installation + +With Composer: + +```bash +composer require openfoodfacts/openfoodfacts-php +``` + +## Usage +This is the most basic way of creating the API: +```php +$api = new OpenFoodFacts\Api('food','fr'); +$product = $api->getProduct('3057640385148'); +``` +In the example above you access the "food" database, limited to the French language/country scope. +The first parameter is either + - "food" + - "beauty" or + - "pet" + +to decide which product database you want to use. + +The second parameter decides the language/country scope of the chosen database: f.e. "world" or "de" or "fr". + +For more details on this topic: see the [API Documentation](https://en.wiki.openfoodfacts.org/API/Read#Countries_and_Language_of_the_Response) + +These are all the parameters you really need for basic usage. + +As return types for ```$api->getProduct``` you get an ```Document::class``` Object. +This may also be an Object of Type ```FoodProduct::class```,```PetProduct::class```, ```BeautyProduct::class``` depending on which API you are creating. +These objects inherit from the more generic ```Document::class``` + +In the example above, we use the 'food' API and there will get a ```FoodProduct::class``` + +For getting a first overview the ```Document::class``` has a function to return an array representation(sorted) for a first start. +```php +$product = $api->getProduct('3057640385148'); +$productDataAsArray = $product->getData(); +``` + + +#### Optional Parameters +The other parameters are optional and for a more sophisticated use of the api (from a software development point of view): + +An example in code is found here: [cached_example.php](examples/01-basic_api_usage/cached_example.php) + +LoggerInterface: A logger which decieds where to log errors to (file, console , etc) + +see: [PSR-3 Loggerinterface](https://www.php-fig.org/psr/psr-3/) + +ClientInterface: The HTTP Client - to adjust the connection configs to your needs and more + +see: [Guzzle HTTP Client](https://packagist.org/packages/guzzlehttp/guzzle) + +CacheInterface: To temporarily save the results of API request to improve the performance and to reduce the load on the API- Server + +see: [PSR-16 Simple Cache](https://www.php-fig.org/psr/psr-16/) + +## Development + + +### Contributing + +1. Fork it ( https://github.com/openfoodfacts/openfoodfacts-php/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request diff --git a/openfoodfacts-php-0.2.3/composer.json b/openfoodfacts-php-0.2.3/composer.json new file mode 100644 index 0000000..17c8f8e --- /dev/null +++ b/openfoodfacts-php-0.2.3/composer.json @@ -0,0 +1,41 @@ +{ + "name": "openfoodfacts/openfoodfacts-php", + "description": "Open Food Facts API Wrapper, the open database about food.", + "homepage": "https://world.openfoodfacts.org/", + "type": "library", + "minimum-stability": "stable", + "license": "MIT", + "support": { + "email": "rmorenp@rampamster.org", + "issues": "https://github.com/openfoodfacts/openfoodfacts-php/issues", + "chat": "https://slack.openfoodfacts.org/", + "source": "https://github.com/openfoodfacts/openfoodfacts-php" + }, + "authors": [ + { + "name": "Roberto Moreno", + "email": "rmorenp@rampmaster.org", + "homepage": "http://www.rampmaster.org", + "role": "Wrapper Developer" + } + ], + "require": { + "php": ">=7.1", + "guzzlehttp/guzzle": "^6.3", + "psr/log": "^1.0", + "psr/simple-cache": "^1.0", + "ext-json": "*", + "ext-curl": "*" + }, + "autoload": { + "psr-4": { + "OpenFoodFacts\\": "src/" + } + }, + "require-dev": { + "phpunit/phpunit": "^7.0", + "symfony/cache": "^4.3", + "monolog/monolog": "^1.23", + "ext-gd": ">=7.2" + } +} diff --git a/openfoodfacts-php-0.2.3/doc/home.md b/openfoodfacts-php-0.2.3/doc/home.md new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/openfoodfacts-php-0.2.3/doc/home.md @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/openfoodfacts-php-0.2.3/examples/00_flat_request/index.html b/openfoodfacts-php-0.2.3/examples/00_flat_request/index.html new file mode 100644 index 0000000..af992d7 --- /dev/null +++ b/openfoodfacts-php-0.2.3/examples/00_flat_request/index.html @@ -0,0 +1,13 @@ + + + + + OFF + + +
+ + +
+ + \ No newline at end of file diff --git a/openfoodfacts-php-0.2.3/examples/00_flat_request/request.php b/openfoodfacts-php-0.2.3/examples/00_flat_request/request.php new file mode 100644 index 0000000..a4e5b61 --- /dev/null +++ b/openfoodfacts-php-0.2.3/examples/00_flat_request/request.php @@ -0,0 +1,31 @@ + + + + + OFF + + +

Example Output

+ + + + + + + + + + + + + +
Product Name{productName}
Brand{brand}
Image
+

Response Struct (Array Format)

+
+    {json}
+
+ + \ No newline at end of file diff --git a/openfoodfacts-php-0.2.3/examples/01-basic_api_usage/cached_example.php b/openfoodfacts-php-0.2.3/examples/01-basic_api_usage/cached_example.php new file mode 100644 index 0000000..0b166b8 --- /dev/null +++ b/openfoodfacts-php-0.2.3/examples/01-basic_api_usage/cached_example.php @@ -0,0 +1,13 @@ + + +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; + +include '../../vendor/autoload.php'; +$logger = new \Monolog\Logger('test'); +$httpClient = new \GuzzleHttp\Client(); +// the PSR-6 cache object that you want to use (you might also use a PSR-16 Interface Object directly) +$psr6Cache = new FilesystemAdapter(); +$psr16Cache = new Psr16Cache($psr6Cache); +$api = new \OpenFoodFacts\Api('food', 'world', $logger, $httpClient, $psr16Cache); +$product = $api->getProduct(rand(1, 50)); diff --git a/openfoodfacts-php-0.2.3/phpunit.xml b/openfoodfacts-php-0.2.3/phpunit.xml new file mode 100644 index 0000000..88485a4 --- /dev/null +++ b/openfoodfacts-php-0.2.3/phpunit.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + tests + + + + + + src + + + + + + + + + + + diff --git a/openfoodfacts-php-0.2.3/src/Api.php b/openfoodfacts-php-0.2.3/src/Api.php new file mode 100644 index 0000000..cd875e0 --- /dev/null +++ b/openfoodfacts-php-0.2.3/src/Api.php @@ -0,0 +1,536 @@ + 'https://%s.openfoodfacts.org', + 'beauty' => 'https://%s.openbeautyfacts.org', + 'pet' => 'https://%s.openpetfoodfacts.org', + 'product' => 'https://%s.openproductsfacts.org', + ]; + + /** + * This constant defines the facets usable by the API + * + * This variable is used to create the magic functions like "getIngredients" or "getBrands" + * @var array + */ + private const FACETS = [ + 'additives', + 'allergens', + 'brands', + 'categories', + 'countries', + 'contributors', + 'code', + 'entry_dates', + 'ingredients', + 'label', + 'languages', + 'nutrition_grade', + 'packaging', + 'packaging_codes', + 'purchase_places', + 'photographer', + 'informer', + 'states', + 'stores', + 'traces', + ]; + + /** + * This constant defines the extensions authorized for the downloading of the data + * @var array + */ + private const FILE_TYPE_MAP = [ + "mongodb" => "openfoodfacts-mongodbdump.tar.gz", + "csv" => "en.openfoodfacts.org.products.csv", + "rdf" => "en.openfoodfacts.org.products.rdf" + ]; + + /** + * the constructor of the function + * + * @param string $api the environment to search + * @param string $geography this parameter represent the the country code and the interface of the language + * @param LoggerInterface $logger this parameter define an logger + * @param ClientInterface|null $clientInterface + * @param CacheInterface|null $cacheInterface + */ + public function __construct( + string $api = 'food', + string $geography = 'world', + LoggerInterface $logger = null, + ClientInterface $clientInterface = null, + CacheInterface $cacheInterface = null + ) + { + $this->cache = $cacheInterface; + $this->logger = $logger ?? new NullLogger(); + $this->httpClient = $clientInterface ?? new Client(); + + $this->geoUrl = sprintf(self::LIST_API[$api], $geography); + $this->geography = $geography; + $this->currentAPI = $api; + } + + /** + * This function allows you to perform tests + * The domain is correct and for testing purposes only + */ + public function activeTestMode() : void + { + $this->geoUrl = 'https://world.openfoodfacts.net'; + $this->authentification('off', 'off'); + } + + /** + * This function store the authentication parameter + * @param string $username + * @param string $password + */ + public function authentification(string $username, string $password) :void + { + $this->auth = [ + 'user_id' => $username, + 'password' => $password + ]; + } + + /** + * It's a magic function, it works only for facets + * @param string $name The name of the function + * @param void $arguments not use yet (probably needed for ingredients) + * @return Collection The list of all documents found + * @throws InvalidArgumentException + * @throws BadRequestException + * @example getIngredients() + */ + public function __call(string $name, $arguments) : Collection + { + //TODO : test with argument for ingredient + if (strpos($name, 'get') === 0) { + $facet = strtolower(substr($name, 3)); + //TODO: what about PSR-12, e.g.: getNutritionGrade() ? + + if (!in_array($facet, self::FACETS)) { + throw new BadRequestException('Facet "' . $facet . '" not found'); + } + + if ($facet === "purchase_places") { + $facet = "purchase-places"; + } elseif ($facet === "packaging_codes") { + $facet = "packager-codes"; + } elseif ($facet === "entry_dates") { + $facet = "entry-dates"; + } + + $url = $this->buildUrl(null, $facet, []); + $result = $this->fetch($url); + if ($facet !== 'ingredients') { + $result = [ + 'products' => $result['tags'], + 'count' => $result['count'], + 'page' => 1, + 'skip' => 0, + 'page_size' => $result['count'], + ]; + } + return new Collection($result, $this->currentAPI); + } + + throw new BadRequestException('Call to undefined method '.__CLASS__.'::'.$name.'()'); + } + + + /** + * this function search an Document by barcode + * @param string $barcode the barcode [\d]{13} + * @return Document A Document if found + * @throws InvalidArgumentException + * @throws ProductNotFoundException + * @throws BadRequestException + */ + public function getProduct(string $barcode) : Document + { + $url = $this->buildUrl('api', 'product', $barcode); + + $rawResult = $this->fetch($url); + if ($rawResult['status'] === 0) { + //TODO: maybe return null here? (just throw an exception if something really went wrong? + throw new ProductNotFoundException("Product not found", 1); + } + + return Document::createSpecificDocument($this->currentAPI, $rawResult['product']); + } + + /** + * This function return a Collection of Document search by facets + * @param array $query list of facets with value + * @param integer $page Number of the page + * @return Collection The list of all documents found + * @throws InvalidArgumentException + * @throws BadRequestException + */ + public function getByFacets(array $query = [], int $page = 1) : Collection + { + if (empty($query)) { + return new Collection(); + } + $search = []; + ksort($query); + foreach ($query as $key => $value) { + $search[] = $key; + $search[] = $value; + } + + $url = $this->buildUrl(null, $search, $page); + $result = $this->fetch($url); + return new Collection($result, $this->currentAPI); + } + + /** + * this function help you to add a new product (or update ??) + * @param array $postData The post data + * @return bool|string bool if the product has been added or the error message + * @throws BadRequestException + * @throws InvalidArgumentException + */ + public function addNewProduct(array $postData) + { + if (!isset($postData['code']) || !isset($postData['product_name'])) { + throw new BadRequestException('code or product_name not found!'); + } + + $url = $this->buildUrl('cgi', 'product_jqm2.pl', []); + $result = $this->fetchPost($url, $postData); + + if ($result['status_verbose'] === 'fields saved' && $result['status'] === 1) { + return true; + } + return $result['status_verbose']; + } + + /** + * [uploadImage description] + * @param string $code the barcode of the product + * @param string $imageField th name of the image + * @param string $imagePath the path of the image + * @return array the http post response (cast in array) + * @throws BadRequestException + * @throws InvalidArgumentException + */ + public function uploadImage(string $code, string $imageField, string $imagePath) + { + //TODO : need test + if ($this->currentAPI !== 'food') { + throw new BadRequestException('not Available yet'); + } + if (!in_array($imageField, ["front", "ingredients", "nutrition"])) { + throw new BadRequestException('ImageField not valid!'); + } + if (!file_exists($imagePath)) { + throw new BadRequestException('Image not found'); + } + + + $url = $this->buildUrl('cgi', 'product_image_upload.pl', []); + $postData = [ + 'code' => $code, + 'imagefield' => $imageField, + 'imgupload_' . $imageField => fopen($imagePath, 'r') + ]; + return $this->fetchPost($url, $postData, true); + } + + /** + * A search function + * @param string $search a search term (fulltext) + * @param integer $page Number of the page + * @param integer $pageSize The page size + * @param string $sortBy the sort + * @return Collection The list of all documents found + * @throws BadRequestException + * @throws InvalidArgumentException + */ + public function search(string $search, int $page = 1, int $pageSize = 20, string $sortBy = 'unique_scans') + { + $parameters = [ + 'search_terms' => $search, + 'page' => $page, + 'page_size' => $pageSize, + 'sort_by' => $sortBy, + 'json' => '1', + ]; + + $url = $this->buildUrl('cgi', 'search.pl', $parameters); + $result = $this->fetch($url, false); + return new Collection($result, $this->currentAPI); + } + + /** + * This function download all data from OpenFoodFact + * @param string $filePath the location where you want to put the stream + * @param string $fileType mongodb/csv/rdf + * @return bool return true when download is complete + * @throws BadRequestException + */ + public function downloadData(string $filePath, string $fileType = "mongodb") + { + + if (!isset(self::FILE_TYPE_MAP[$fileType])) { + $this->logger->warning( + 'OpenFoodFact - fetch - failed - File type not recognized!', + ['fileType' => $fileType, 'availableTypes' => self::FILE_TYPE_MAP] + ); + throw new BadRequestException('File type not recognized!'); + } + + $url = $this->buildUrl('data', self::FILE_TYPE_MAP[$fileType]); + try { + $response = $this->httpClient->request('get', $url, ['sink' => $filePath]); + } catch (GuzzleException $guzzleException) { + $this->logger->warning(sprintf('OpenFoodFact - fetch - failed - GET : %s', $url), ['exception' => $guzzleException]); + $exception = new BadRequestException($guzzleException->getMessage(), $guzzleException->getCode(), $guzzleException); + + throw $exception; + } + + $this->logger->info('OpenFoodFact - fetch - GET : ' . $url . ' - ' . $response->getStatusCode()); + + //TODO: validate response here (server may respond with 200 - OK but you might not get valid data as a response) + + return $response->getStatusCode() == 200; + } + + + /** + * This private function do a http request + * @param string $url the url to fetch + * @param boolean $isJsonFile the request must be finish by '.json' ? + * @return array return the result of the request in array format + * @throws InvalidArgumentException + * @throws BadRequestException + */ + private function fetch(string $url, bool $isJsonFile = true) : array + { + + $url .= ($isJsonFile? '.json' : ''); + $realUrl = $url; + $cacheKey = md5($realUrl); + + if (!empty($this->cache) && $this->cache->has($cacheKey)) { + $cachedResult = $this->cache->get($cacheKey); + return $cachedResult; + } + + $data = [ + 'on_stats' => function (TransferStats $stats) use (&$realUrl) { + // this function help to find redirection + // On redirect we lost some parameters like page + $realUrl= (string)$stats->getEffectiveUri(); + } + ]; + if ($this->auth) { + $data['auth'] = array_values($this->auth); + } + + try { + $response = $this->httpClient->request('get', $url, $data); + } catch (GuzzleException $guzzleException) { + $this->logger->warning(sprintf('OpenFoodFact - fetch - failed - GET : %s', $url), ['exception' => $guzzleException]); + //TODO: What to do on a error? - return empty array? + $exception = new BadRequestException($guzzleException->getMessage(), $guzzleException->getCode(), $guzzleException); + + throw $exception; + } + if ($realUrl !== $url) { + $this->logger->warning('OpenFoodFact - The url : '. $url . ' has been redirect to ' . $realUrl); + trigger_error('OpenFoodFact - Your request has been redirect'); + } + $this->logger->info('OpenFoodFact - fetch - GET : ' . $url . ' - ' . $response->getStatusCode()); + + $jsonResult = json_decode($response->getBody(), true); + + if (!empty($this->cache) && !empty($jsonResult)) { + $this->cache->set($cacheKey, $jsonResult); + } + + return $jsonResult; + } + + /** + * This function performs the same job of the "fetch" function except the call method and parameters + * @param string $url The url to fetch + * @param array $postData The post data + * @param boolean $isMultipart The data is multipart ? + * @return array return the result of the request in array format + * @throws InvalidArgumentException + * @throws BadRequestException + */ + private function fetchPost(string $url, array $postData, bool $isMultipart = false) : array + { + $data = []; + if ($this->auth) { + $data['auth'] = array_values($this->auth); + } + if ($isMultipart) { + foreach ($postData as $key => $value) { + $data['multipart'][] = [ + 'name' => $key, + 'contents' => $value + ]; + } + } else { + $data['form_params'] = $postData; + } + + $cacheKey = md5($url . json_encode($data)); + + if (!empty($this->cache) && $this->cache->has($cacheKey)) { + return $this->cache->get($cacheKey); + } + + try { + $response = $this->httpClient->request('post', $url, $data); + }catch (GuzzleException $guzzleException){ + $exception = new BadRequestException($guzzleException->getMessage(), $guzzleException->getCode(), $guzzleException); + + throw $exception; + } + + $this->logger->info('OpenFoodFact - fetch - GET : ' . $url . ' - ' . $response->getStatusCode()); + + $jsonResult = json_decode($response->getBody(), true); + + if (!empty($this->cache) && !empty($jsonResult)) { + $this->cache->set($cacheKey, $jsonResult); + } + + return $jsonResult; + } + + /** + * This private function generates an url according to the parameters + * @param string|null $service + * @param string|array|null $resourceType + * @param string|array|null $parameters + * @return string the generated url + */ + private function buildUrl(string $service = null, $resourceType = null, $parameters = null) : string + { + $baseUrl = null; + switch ($service) { + case 'api': + $baseUrl = implode('/', [ + $this->geoUrl, + $service, + 'v0', + $resourceType, + $parameters + ]); + break; + case 'data': + $baseUrl = implode('/', [ + $this->geoUrl, + $service, + $resourceType + ]); + break; + case 'cgi': + $baseUrl = implode('/', [ + $this->geoUrl, + $service, + $resourceType + ]); + $baseUrl .= '?' . http_build_query($parameters); + break; + case null: + default: + if (is_array($resourceType)) { + $resourceType = implode('/', $resourceType); + } + if ($resourceType == 'ingredients') { + //need test + $resourceType = implode('/', ["state", "complete", $resourceType]); + $parameters = 1; + } + $baseUrl = implode('/', array_filter([ + $this->geoUrl, + $resourceType, + $parameters + ], function ($value) { + return !empty($value); + })); + break; + } + return $baseUrl; + } +} diff --git a/openfoodfacts-php-0.2.3/src/Collection.php b/openfoodfacts-php-0.2.3/src/Collection.php new file mode 100644 index 0000000..02695e5 --- /dev/null +++ b/openfoodfacts-php-0.2.3/src/Collection.php @@ -0,0 +1,133 @@ + [], + 'count' => 0, + 'page' => 0, + 'skip' => 0, + 'page_size' => 0, + ]; + $this->listDocuments = []; + + if (!empty($data['products'])) { + $currentApi = ''; + if (null !== $api) { + $currentApi = $api; + } + foreach ($data['products'] as $document) { + if($document instanceof Document){ + $this->listDocuments[] = $document; + }elseif (is_array($document)){ + $this->listDocuments[] = Document::createSpecificDocument($currentApi, $document); + }else { + throw new \InvalidArgumentException(sprintf('Would expect an OpenFoodFacts\Document Interface or Array here. Got: %s', gettype($document))); + } + + } + } + + $this->count = $data['count']; + $this->page = $data['page']; + $this->skip = $data['skip']; + $this->pageSize = $data['page_size']; + } + + /** + * @return int get the current page + */ + public function getPage() : int + { + return $this->page; + } + /** + * @return int get the number of element skipped + */ + public function getSkip() : int + { + return $this->skip; + } + /** + * @return int get the number of element by page for this collection + */ + public function getPageSize() : int + { + return $this->pageSize; + } + + /** + * @return int the number of element in this Collection + */ + public function pageCount() : int + { + return count($this->listDocuments); + } + + /** + * @return int the number of element for this search + */ + public function searchCount() : int + { + return $this->count; + } + + /** + * Implementation of Iterator + */ + + /** + * @inheritDoc + */ + public function rewind() + { + reset($this->listDocuments); + } + /** + * @inheritDoc + */ + public function current() + { + return current($this->listDocuments); + } + /** + * @inheritDoc + */ + public function key() + { + return key($this->listDocuments); + } + /** + * @inheritDoc + */ + public function next() + { + return next($this->listDocuments); + } + /** + * @inheritDoc + */ + public function valid() + { + $key = key($this->listDocuments); + return ($key !== null && $key !== false); + } +} diff --git a/openfoodfacts-php-0.2.3/src/Document.php b/openfoodfacts-php-0.2.3/src/Document.php new file mode 100644 index 0000000..6034000 --- /dev/null +++ b/openfoodfacts-php-0.2.3/src/Document.php @@ -0,0 +1,83 @@ +data = $data; + $this->api = $api; + } + + /** + * @inheritDoc + */ + public function __get(string $name) + { + return $this->data[$name]; + } + /** + * @inheritDoc + */ + public function __isset(string $name):bool + { + return isset($this->data[$name]); + } + + /** + * Returns a sorted representation of the complete Document Data + * @return array + */ + public function getData(): array + { + $this->recursiveSortArray($this->data); + return $this->data; + } + + /** + * Returns a Document in the type regarding to the API used. + * May be a Child of "Document" e.g.: FoodDocument or ProductDocument + * @param string $apiIdentifier + * @param array $data + * @return Document + */ + public static function createSpecificDocument(string $apiIdentifier, array $data): Document + { + if ($apiIdentifier === '') { + return new Document($data, $apiIdentifier); + } + + $className = "OpenFoodFacts\Document\\" . ucfirst($apiIdentifier) . 'Document'; + + if (class_exists($className) && is_subclass_of($className, Document::class)) { + return new $className($data, $apiIdentifier); + } + + return new Document($data, $apiIdentifier); + } + +} diff --git a/openfoodfacts-php-0.2.3/src/Document/BeautyDocument.php b/openfoodfacts-php-0.2.3/src/Document/BeautyDocument.php new file mode 100644 index 0000000..99a2e55 --- /dev/null +++ b/openfoodfacts-php-0.2.3/src/Document/BeautyDocument.php @@ -0,0 +1,10 @@ +recursiveDeleteDirectory($dir . "/" . $object); + } else { + unlink($dir . "/" . $object); + } + } + } + rmdir($dir); + } + } +} \ No newline at end of file diff --git a/openfoodfacts-php-0.2.3/src/RecursiveSortingTrait.php b/openfoodfacts-php-0.2.3/src/RecursiveSortingTrait.php new file mode 100644 index 0000000..fcb6259 --- /dev/null +++ b/openfoodfacts-php-0.2.3/src/RecursiveSortingTrait.php @@ -0,0 +1,38 @@ +isAssoc($arr)) { + ksort($arr); + } else { + asort($arr); + } + foreach ($arr as &$a) { + if (is_array($a)) { + $this->recursiveSortArray($a); + } + } + } +} diff --git a/openfoodfacts-php-0.2.3/tests/ApiFoodCacheTest.php b/openfoodfacts-php-0.2.3/tests/ApiFoodCacheTest.php new file mode 100644 index 0000000..c91d8fe --- /dev/null +++ b/openfoodfacts-php-0.2.3/tests/ApiFoodCacheTest.php @@ -0,0 +1,178 @@ +pushHandler(new StreamHandler('log/test.log')); + $psr6Cache = new FilesystemAdapter(sprintf('testrun_%u', rand(0, 1000)), 10, 'tests/tmp/cache'); + $cache = new Psr16Cache($psr6Cache); + + $httpClient = new GuzzleHttp\Client([ +// "http_errors" => false, // MUST not use as it crashes error handling + 'Connection' => 'close', + CURLOPT_FORBID_REUSE => true, + CURLOPT_FRESH_CONNECT => true, + 'defaults' => [ + 'headers' => [ + 'CURLOPT_USERAGENT' => 'OFF - PHP - SDK - Unit Test', + ], + ], + ]); + + $api = new Api('food', 'fr-en', $log, $httpClient, $cache); + $this->assertInstanceOf(Api::class, $api); + $this->api = $api; + + } + + public function testApi(): void + { + + $prd = $this->api->getProduct('3057640385148'); + + $this->assertInstanceOf(FoodDocument::class, $prd); + $this->assertInstanceOf(Document::class, $prd); + + $this->assertTrue(isset($prd->product_name)); + $this->assertNotEmpty($prd->product_name); + + try { + $product = $this->api->getProduct('305764038514800'); + $this->assertTrue(false); + } catch (ProductNotFoundException $e) { + $this->assertTrue(true); + } + + try { + $result = $this->api->downloadData('tests/mongodb', 'nopeFile'); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'File type not recognized!'); + } + + // $result = $this->api->downloadData('tests/tmp/mongodb'); + // $this->assertTrue(true); + } + + public function testApiCollection(): void + { + + $collection = $this->api->getByFacets([]); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 0); + + try { + $collection = $this->api->getByFacets(['trace' => 'egg', 'country' => 'france'], 3); + $this->assertTrue(false); + } catch (\PHPUnit\Framework\Error\Notice $e) { + $this->assertEquals($e->getMessage(), 'OpenFoodFact - Your request has been redirect'); + } + + $collection = $this->api->getByFacets(['trace' => 'eggs', 'country' => 'france'], 3); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 20); + $this->assertEquals($collection->getPage(), 3); + $this->assertEquals($collection->getSkip(), 40); + $this->assertEquals($collection->getPageSize(), 20); + $this->assertGreaterThan(1000, $collection->searchCount()); + + foreach ($collection as $key => $doc) { + if ($key > 1) { + break; + } + + $this->assertInstanceOf(FoodDocument::class, $doc); + $this->assertInstanceOf(Document::class, $doc); + + } + + } + + public function testApiSearch(): void + { + + $collection = $this->api->search('volvic', 3, 30); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 30); + $this->assertGreaterThan(100, $collection->searchCount()); + + } + + + public function testFacets(): void + { + + $collection = $this->api->getIngredients(); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 20); + $this->assertEquals($collection->getPageSize(), 20); + $this->assertGreaterThan(70000, $collection->searchCount()); + + try { + $collection = $this->api->getIngredient(); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'Facet "ingredient" not found'); + } + + $collection = $this->api->getPurchase_places(); + $this->assertInstanceOf(Collection::class, $collection); + $collection = $this->api->getPackaging_codes(); + $this->assertInstanceOf(Collection::class, $collection); + $collection = $this->api->getEntry_dates(); + $this->assertInstanceOf(Collection::class, $collection); + + try { + $collection = $this->api->getIngredient(); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'Facet "ingredient" not found'); + } + + try { + $collection = $this->api->nope(); + } catch (\Exception $e) { + $this->assertTrue(true); + } + } + + protected function tearDown() + { + $this->recursiveDeleteDirectory('tests/tmp'); + } + +} diff --git a/openfoodfacts-php-0.2.3/tests/ApiFoodTest.php b/openfoodfacts-php-0.2.3/tests/ApiFoodTest.php new file mode 100644 index 0000000..8b8ae32 --- /dev/null +++ b/openfoodfacts-php-0.2.3/tests/ApiFoodTest.php @@ -0,0 +1,248 @@ +pushHandler(new StreamHandler('log/test.log')); + + $this->api = new Api('food', 'fr-en', $log); + @rmdir('tests/tmp'); + @mkdir('tests/tmp'); + } + + public function testApi(): void + { + + $prd = $this->api->getProduct('3057640385148'); + + $this->assertInstanceOf(FoodDocument::class, $prd); + $this->assertInstanceOf(Document::class, $prd); + $this->assertTrue(isset($prd->product_name)); + $this->assertNotEmpty($prd->product_name); + + try { + $product = $this->api->getProduct('305764038514800'); + $this->assertTrue(false); + } catch (ProductNotFoundException $e) { + $this->assertTrue(true); + } + + try { + $result = $this->api->downloadData('tests/mongodb', 'nopeFile'); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'File type not recognized!'); + } + + // $result = $this->api->downloadData('tests/tmp/mongodb'); + // $this->assertTrue(true); + } + + public function testApiCollection(): void + { + + $collection = $this->api->getByFacets([]); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 0); + + try { + $collection = $this->api->getByFacets(['trace' => 'egg', 'country' => 'france'], 3); + $this->assertTrue(false); + } catch (\PHPUnit\Framework\Error\Notice $e) { + $this->assertEquals($e->getMessage(), 'OpenFoodFact - Your request has been redirect'); + } + + $collection = $this->api->getByFacets(['trace' => 'eggs', 'country' => 'france'], 3); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 20); + $this->assertEquals($collection->getPage(), 3); + $this->assertEquals($collection->getSkip(), 40); + $this->assertEquals($collection->getPageSize(), 20); + $this->assertGreaterThan(1000, $collection->searchCount()); + + foreach ($collection as $key => $doc) { + if ($key > 1) { + break; + } + $this->assertInstanceOf(FoodDocument::class, $doc); + $this->assertInstanceOf(Document::class, $doc); + + } + + } + + public function testApiAddProduct(): void + { + $this->api->activeTestMode(); + try { + $prd = $this->api->getProduct('3057640385148'); + $this->assertInstanceOf(FoodDocument::class, $prd); + $this->assertInstanceOf(Document::class, $prd); + } catch (Exception $exception) { + if ($exception->getPrevious() instanceof ServerException && $exception->getPrevious()->getCode() === 503) { + $this->markTestSkipped( + 'Testing API currently not available.' + ); + } + } + + $postData = ['code' => $prd->code, 'product_name' => $prd->product_name]; + + $result = $this->api->addNewProduct($postData); + $this->assertTrue(is_bool($result)); + + + $postData = ['product_name' => $prd->product_name]; + + try { + $result = $this->api->addNewProduct($postData); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertTrue(true); + } + $postData = ['code' => '', 'product_name' => $prd->product_name]; + $result = $this->api->addNewProduct($postData); + $this->assertTrue(is_string($result)); + $this->assertEquals($result, 'no code or invalid code'); + + } + + public function testApiAddImage(): void + { + + $this->api->activeTestMode(); + try { + $prd = $this->api->getProduct('3057640385148'); + $this->assertInstanceOf(Collection::class, $prd); + } catch (Exception $exception) { + if ($exception->getPrevious() instanceof ServerException && $exception->getPrevious()->getCode() === 503) { + $this->markTestSkipped( + 'Testing API currently not available.' + ); + } + } + + try { + $this->api->uploadImage('3057640385148', 'fronts', 'nothing'); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'ImageField not valid!'); + } + try { + $this->api->uploadImage('3057640385148', 'front', 'nothing'); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'Image not found'); + } + $file1 = $this->createRandomImage(); + + $result = $this->api->uploadImage('3057640385148', 'front', $file1); + $this->assertEquals($result['status'], 'status ok'); + $this->assertTrue(isset($result['imagefield'])); + $this->assertTrue(isset($result['image'])); + $this->assertTrue(isset($result['image']['imgid'])); + + + } + + public function testApiSearch(): void + { + + $collection = $this->api->search('volvic', 3, 30); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 30); + $this->assertGreaterThan(100, $collection->searchCount()); + + } + + + public function testFacets(): void + { + + $collection = $this->api->getIngredients(); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 20); + $this->assertEquals($collection->getPageSize(), 20); + $this->assertGreaterThan(70000, $collection->searchCount()); + + try { + $collection = $this->api->getIngredient(); + $this->assertInstanceOf(Collection::class, $collection); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'Facet "ingredient" not found'); + } + + $collection = $this->api->getPurchase_places(); + $this->assertInstanceOf(Collection::class, $collection); + $collection = $this->api->getPackaging_codes(); + $this->assertInstanceOf(Collection::class, $collection); + $collection = $this->api->getEntry_dates(); + $this->assertInstanceOf(Collection::class, $collection); + + try { + $collection = $this->api->getIngredient(); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'Facet "ingredient" not found'); + } + + try { + $collection = $this->api->nope(); + } catch (\Exception $e) { + $this->assertTrue(true); + } + } + + + private function createRandomImage(): string + { + + $width = 400; + $height = 200; + + $imageRes = imagecreatetruecolor($width, $height); + for ($row = 0; $row <= $height; $row++) { + for ($column = 0; $column <= $width; $column++) { + $colour = imagecolorallocate($imageRes, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255)); + imagesetpixel($imageRes, $column, $row, $colour); + } + } + $path = 'tests/tmp/image_' . time() . '.jpg'; + if (imagejpeg($imageRes, $path)) { + return $path; + } + throw new \Exception("Error Processing Request", 1); + + } + + protected function tearDown() + { + $this->recursiveDeleteDirectory('tests/tmp'); + } + +} diff --git a/openfoodfacts-php-0.2.3/tests/ApiPetTest.php b/openfoodfacts-php-0.2.3/tests/ApiPetTest.php new file mode 100644 index 0000000..a727b74 --- /dev/null +++ b/openfoodfacts-php-0.2.3/tests/ApiPetTest.php @@ -0,0 +1,80 @@ +pushHandler(new StreamHandler('log/test.log')); + + $this->api = new Api('pet', 'fr', $log); + + foreach (glob('tests/images/*') as $file) { + unlink($file); + } + } + + public function testApi() + { + + $prd = $this->api->getProduct('7613035799738'); + + $this->assertInstanceOf(PetDocument::class, $prd); + $this->assertInstanceOf(Document::class, $prd); + $this->assertTrue(isset($prd->product_name)); + $this->assertNotEmpty($prd->product_name); + + } + + public function testApiAddImage() + { + try { + $this->api->uploadImage('7613035799738', 'fronts', 'nothing'); + $this->assertTrue(false); + } catch (BadRequestException $e) { + $this->assertEquals($e->getMessage(), 'not Available yet'); + $this->markTestSkipped( + $e->getMessage() + ); + } + + } + + public function testApiSearch() + { + + $collection = $this->api->search('chat', 3, 30); + + $this->assertInstanceOf(Collection::class, $collection); + $this->assertEquals($collection->pageCount(), 30); + $this->assertGreaterThan(100, $collection->searchCount()); + + } + + protected function tearDown() + { + $this->recursiveDeleteDirectory('tests/tmp'); + } + +}