Remove OpenFoodFact client

This commit is contained in:
docjyJ 2020-08-23 13:00:20 +02:00
parent 862b6a4741
commit 384328739e
27 changed files with 0 additions and 1682 deletions

View file

@ -1,18 +0,0 @@
# 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

View file

@ -1,5 +0,0 @@
language: php
php:
- '7.2'
before_script: composer install --dev
script: vendor/bin/phpunit

View file

@ -1,3 +0,0 @@
## 0.0.1 (June 19, 2016) ##
* Upload project struct

View file

@ -1,7 +0,0 @@
## 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

View file

@ -1,21 +0,0 @@
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.

View file

@ -1,78 +0,0 @@
# 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

View file

@ -1,41 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
TODO

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OFF</title>
</head>
<body>
<form action="./request.php" method="get">
<input type="text" class="form-control" placeholder="barcode" name="ean13">
<input type="submit" name="Ok" value="Find this Barcode !">
</form>
</body>
</html>

View file

@ -1,31 +0,0 @@
<?php
// Default parameters by case
$country = 'fr'; // Country by using OFF
$productSlug = 'produit'; // Product by language (producto in spanish or product in english)
// Format URL
$url = 'https://{country}.openfoodfacts.org/api/v0/{product}/{scan}.json';
// Where we will set the value of the scan
$barcode = (int) $_GET['ean13'];
$url = str_replace(['{country}','{product}','{scan}'],[$country,$productSlug,$barcode],$url);
// Connection to the API (french version here)
$result = file_get_contents($url);
// Decoding the JSON into an usable array (the value "true" confirms that the return is only an array)
$json = json_decode($result, true);
// Get the datas we want
$productName = $json['product']['product_name'];
$brand = $json['product']['brands'];
$image = $json['product']['image_small_url'];
$viewData = file_get_contents('response.html');
echo str_replace(
['{productName}','{brand}','{image}','{json}'],
[$productName,$brand,$image,print_r($json,true)],
$viewData);

View file

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OFF</title>
</head>
<body>
<h2>Example Output</h2>
<table>
<tr>
<td>Product Name</td>
<td>{productName}</td>
</tr>
<tr>
<td>Brand</td>
<td>{brand}</td>
</tr>
<tr>
<td>Image</td>
<td><img src="{image}"/></td>
</tr>
</table>
<h2>Response Struct (Array Format)</h2>
<pre>
{json}
</pre>
</body>
</html>

View file

@ -1,13 +0,0 @@
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));

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="KERNEL_CLASS" value="AppKernel" />
</php>
<testsuites>
<testsuite name="AllTests">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
<!--<exclude>
<directory>src/*Bundle/Resources</directory>
</exclude>-->
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="./build/coverrage" />
<log type="coverage-clover" target="./build/logs/clover.xml" />
<log type="junit" target="./build/logs/junit.xml" />
<log type="testdox-html" target="./build/logstestdox.html" />
</logging>
</phpunit>

View file

@ -1,536 +0,0 @@
<?php /** @noinspection ALL */
namespace OpenFoodFacts;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\TransferStats;
use OpenFoodFacts\Exception\BadRequestException;
use OpenFoodFacts\Exception\ProductNotFoundException;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
/**
* this class provide [...]
*
* It a fork of the python OpenFoodFact rewrite on PHP 7.2
*/
class Api
{
/**
* the httpClient for all http request
* @var ClientInterface
*/
private $httpClient;
/**
* this property store the current base of the url
* @var string
*/
private $geoUrl = 'https://%s.openfoodfacts.org';
/**
* this property store the current API (it could be : food/beauty/pet )
* @var string
*/
private $currentAPI = '';
/**
* This property store the current location for http call
*
* This property could be world for all product or you can specify le country code (cc) and
* language of the interface (lc). If you want filter on french product you can set fr as country code.
* We strongly recommend to use english as language of the interface
*
* @example fr-en
* @link https://en.wiki.openfoodfacts.org/API/Read#Country_code_.28cc.29_and_Language_of_the_interface_.28lc.29
* @var string
*/
private $geography = 'world';
/**
* this property store the auth parameter (username and password)
* @var array
*/
private $auth = null;
/**
* this property help you to log information
* @var LoggerInterface
*/
private $logger = null;
/**
* this constant defines the environments usable by the API
* @var array
*/
private const LIST_API = [
'food' => '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;
}
}

View file

@ -1,133 +0,0 @@
<?php
namespace OpenFoodFacts;
use Iterator;
class Collection implements Iterator
{
private $listDocuments = null;
private $count = null;
private $page = null;
private $skip = null;
private $pageSize = null;
/**
* initialization of the collection
* @param array|null $data the raw data
* @param string|null $api this information help to type the collection (not use yet)
*/
public function __construct(array $data = null, string $api = null)
{
$data = $data ?? [
'products' => [],
'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);
}
}

View file

@ -1,83 +0,0 @@
<?php
namespace OpenFoodFacts;
/**
* In mongoDB all element are object, it not possible to define property.
* All property of the mongodb entity are store in one property of this class and the magic call try to access to it
*/
class Document
{
use RecursiveSortingTrait;
/**
* the whole data
* @var array
*/
private $data;
/**
* the whole data
* @var array
*/
private $api;
/**
* Initialization the document and specify from which API it was extract
* @param array $data the whole data
* @param string $api the api name
*/
public function __construct(array $data, string $api = null)
{
$this->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);
}
}

View file

@ -1,10 +0,0 @@
<?php
namespace OpenFoodFacts\Document;
use OpenFoodFacts\Document;
class BeautyDocument extends Document
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace OpenFoodFacts\Document;
use OpenFoodFacts\Document;
class FoodDocument extends Document
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace OpenFoodFacts\Document;
use OpenFoodFacts\Document;
class PetDocument extends Document
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace OpenFoodFacts\Document;
use OpenFoodFacts\Document;
class ProductDocument extends Document
{
}

View file

@ -1,14 +0,0 @@
<?php
namespace OpenFoodFacts\Exception;
use Exception;
/**
* Just an exception class for the try catch
*/
class BadRequestException extends Exception
{
}

View file

@ -1,14 +0,0 @@
<?php
namespace OpenFoodFacts\Exception;
use Exception;
/**
* Just an exception class for the try catch
*/
class ProductNotFoundException extends Exception
{
}

View file

@ -1,23 +0,0 @@
<?php
namespace OpenFoodFacts;
trait FilesystemTrait
{
function recursiveDeleteDirectory($dir)
{
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
if (is_dir($dir . "/" . $object)) {
$this->recursiveDeleteDirectory($dir . "/" . $object);
} else {
unlink($dir . "/" . $object);
}
}
}
rmdir($dir);
}
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace OpenFoodFacts;
/**
* Trait RecursiveSortingTrait
*/
trait RecursiveSortingTrait
{
/**
* @param array $arr
* @return bool
*/
private function isAssoc(array $arr): bool
{
return array_keys($arr) !== range(0, count($arr) - 1);
}
/**
* Sorts referenced array of arrays in a recursive way for better understandability
* @param array $arr
* @see ksort
* @see asort
*/
public function recursiveSortArray(array &$arr): void
{
if ($this->isAssoc($arr)) {
ksort($arr);
} else {
asort($arr);
}
foreach ($arr as &$a) {
if (is_array($a)) {
$this->recursiveSortArray($a);
}
}
}
}

View file

@ -1,178 +0,0 @@
<?php
use GuzzleHttp\Exception\ServerException;
use OpenFoodFacts\FilesystemTrait;
use PHPUnit\Framework\TestCase;
use OpenFoodFacts\Api;
use OpenFoodFacts\Collection;
use OpenFoodFacts\Document\FoodDocument;
use OpenFoodFacts\Document;
use OpenFoodFacts\Exception\{
ProductNotFoundException,
BadRequestException
};
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Symfony\Component\Console\Logger\ConsoleLogger;
class ApiFoodCacheTest extends TestCase
{
use FilesystemTrait;
/**
* @var Api
*/
private $api;
protected function setUp()
{
@rmdir('tests/tmp');
@mkdir('tests/tmp');
@mkdir('tests/tmp/cache');
$log = new Logger('name');
$log->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');
}
}

View file

@ -1,248 +0,0 @@
<?php
use GuzzleHttp\Exception\ServerException;
use OpenFoodFacts\FilesystemTrait;
use PHPUnit\Framework\TestCase;
use OpenFoodFacts\Api;
use OpenFoodFacts\Collection;
use OpenFoodFacts\Document\FoodDocument;
use OpenFoodFacts\Document;
use OpenFoodFacts\Exception\{
ProductNotFoundException,
BadRequestException
};
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class ApiFoodTest extends TestCase
{
use FilesystemTrait;
private $api;
protected function setUp()
{
$log = new Logger('name');
$log->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');
}
}

View file

@ -1,80 +0,0 @@
<?php
use OpenFoodFacts\FilesystemTrait;
use PHPUnit\Framework\TestCase;
use OpenFoodFacts\Api;
use OpenFoodFacts\Collection;
use OpenFoodFacts\Document\PetDocument;
use OpenFoodFacts\Document;
use OpenFoodFacts\Exception\{
ProductNotFoundException,
BadRequestException
};
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class ApiPetTest extends TestCase
{
use FilesystemTrait;
private $api;
protected function setUp()
{
$log = new Logger('name');
$log->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');
}
}