123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- <?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;
- }
- }
|