- Better refactorisation using Customized api response objects

- Replace Raw Guzzle usage with Illuminate Http Client
This commit is contained in:
Jérémy Munsch 2023-04-14 10:30:27 +02:00
parent 87ec0d7f6f
commit 39be89f10f
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
4 changed files with 127 additions and 74 deletions

View file

@ -9,6 +9,11 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\DB;
/**
* Fetch and store the top media list of a specific ig keyword.
*
* @package App\Jobs
*/
class InstaGetTopMediasJob implements ShouldQueue
{
use Dispatchable;
@ -48,7 +53,7 @@ class InstaGetTopMediasJob implements ShouldQueue
/** @var \Illuminate\Database\Eloquent\Collection<array-key,\App\Models\InstaMediaExclude> */
$excludeUsers = InstaMediaExclude::query()->where('type', ExcludeTypeEnum::user)->get();
/** @var \App\Lib\InstagramApiFetcher */
/** @var \App\Lib\Instagram\ApiFetcher */
$igFetcherClient = \app('Instagram');
$igFetcherClient->fetchHashtagTopMediaList(
$hashtag_id,

View file

@ -1,9 +1,13 @@
<?php
namespace App\Lib;
namespace App\Lib\Instagram;
use App\Lib\Instagram\Objects\TopMedia;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Client\HttpClientException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\Http;
use Psr\Http\Message\UriInterface;
/**
@ -11,7 +15,7 @@ use Psr\Http\Message\UriInterface;
*
* @package App\Lib
*/
class InstagramApiFetcher
class ApiFetcher
{
/**
* Top media fields to Fetch
@ -54,7 +58,8 @@ class InstagramApiFetcher
* @param \Illuminate\Database\Eloquent\Collection<\App\Models\InstaMediaExclude> $excludedUsers The IG user id we want to fetch.
* @param integer $limit The limit of how many post should be fetched.
* @param integer $media_count How many media we want to fetch.
* @return \Illuminate\Support\Collection<array-key,object>
* @return \Illuminate\Support\Collection<array-key,\App\Lib\Instagram\Objects\TopMedia>
* @throws \RuntimeException If a call error or a response parsing error occurs.
*/
public function fetchHashtagTopMediaList(
int $hashtag_id,
@ -73,6 +78,7 @@ class InstagramApiFetcher
'limit' => $limit
];
// * Recursive Closure to fetch paginated ig media lists.
$fetchHashtagTopMediaList = null;
$fetchHashtagTopMediaList = function (
int &$count,
@ -84,8 +90,9 @@ class InstagramApiFetcher
$media_count,
$fetchHashtagTopMediaList
) {
$json = $this->callApi('GET', $path, $parameters);
$outCollection = \collect($json?->data)->map(function (
$response = $this->callApi('GET', $path, $parameters);
$json = $response->object();
$outCollection = \collect($json->data)->map(function (
object $ig_media
) use (
&$count,
@ -112,18 +119,13 @@ class InstagramApiFetcher
return null;
}
$count++;
return (object)[
'insta_id' => $ig_media->id,
'type' => $ig_media->media_type,
'permalink' => $ig_media->permalink,
'timestamp' => $ig_media->timestamp,
'like_count' => \optional($ig_media)->like_count,
'comments_count' => $ig_media->comments_count,
'username' => $username,
'caption' => $ig_media->caption,
];
$topMedia = new TopMedia();
$topMedia->setData($ig_media);
$topMedia->setUserName($username);
return $topMedia;
})->filter();
// * Manual pagination using IG cursors.
if (
$count < $media_count &&
!empty($json->paging->cursors->after) &&
@ -139,7 +141,11 @@ class InstagramApiFetcher
}
return $outCollection;
};
return $fetchHashtagTopMediaList($count, $parameters);
try {
return $fetchHashtagTopMediaList($count, $parameters);
} catch (\ErrorException $e) {
throw new \RuntimeException($e->getMessage(), $e->getCode(), $e);
}
}
/**
@ -147,28 +153,32 @@ class InstagramApiFetcher
*
* @param string $url
* @return null|string
* @throws \RuntimeException RuntimeException.
* @throws \RuntimeException If a call error or a response parsing error occurs.
*/
public function fetchUsernameFromPostPermalink(string $url): ?string
{
$result = $this->curlCall('GET', $url, parse_json: false);
$html = $result->__toString();
$dom = new \DOMDocument();
$dom->loadHTML($html);
try {
$response = $this->httpCall('GET', $url);
$html = $response->body();
$dom = new \DOMDocument();
$dom->loadHTML($html);
$scripts = $dom->getElementsByTagName('script');
return collect($scripts)->filter()->map(function (\DOMElement $script) {
if ($script->getAttribute('type') != 'application/ld+json') {
return null;
}
$entity = json_decode($script->nodeValue, true);
if ($entity['@type'] !== 'SocialMediaPosting') {
return null;
}
return \strval($entity['author']['identifier']['value']);
})->filter(function ($el) {
return !empty($el);
})->first();
$scripts = $dom->getElementsByTagName('script');
return collect($scripts)->filter()->map(function (\DOMElement $script) {
if ($script->getAttribute('type') != 'application/ld+json') {
return null;
}
$entity = json_decode($script->nodeValue, true);
if ($entity['@type'] !== 'SocialMediaPosting') {
return null;
}
return \strval($entity['author']['identifier']['value']);
})->filter(function ($el) {
return !empty($el);
})->first();
} catch (\Exception $e) {
throw new \RuntimeException($e->getMessage(), $e->getCode(), $e);
} //end try
}
/**
@ -177,15 +187,15 @@ class InstagramApiFetcher
* @param string $method The HTTP VERB to use.
* @param string $path The path api to call.
* @param array $params Api call parameters to pass on.
* @return object
* @return \Illuminate\Http\Client\Response
* @throws \RuntimeException Error on call.
*/
private function callApi(
string $method,
string $path,
array $params = []
): object {
return $this->curlCall(
): Response {
return $this->httpCall(
$method,
\sprintf(
"%s/%s/%s",
@ -193,53 +203,43 @@ class InstagramApiFetcher
$this->version,
$path
),
[
'query' => array_merge(
[
'access_token' => $this->access_token
],
$params
)
]
array_merge(
[
'access_token' => $this->access_token
],
$params
)
);
}
/**
* Call url and fetch result in array.
*
* @param string $method The HTTP VERB to use.
* @param string $method The HTTP VERB to use (GET, POST).
* @param string|\Psr\Http\Message\UriInterface $uri The uri to call.
* @param array $options The options to pass on.
* @param boolean $parse_json Wether to return data as parsed Json array or raw ouput.
* @return object|\Psr\Http\Message\StreamInterface
* @param array $parameters The call parameters to pass on.
* @return \Illuminate\Http\Client\Response
* @throws \Error If the request method is not supported.
* @throws \RuntimeException If something goes wrong.
*/
private function curlCall(
private function httpCall(
string $method,
string|UriInterface $uri = '',
array $options = [],
bool $parse_json = true
): object {
array $parameters = []
): Response {
try {
$client = new \GuzzleHttp\Client();
$res = $client->request($method, $uri, $options);
$status = $res->getStatusCode();
$body = $res->getBody();
$e = new \RuntimeException("Failed to decode IG response \n {$body}");
if (!\in_array(\strtoupper($method), ['GET', 'POST'])) {
throw new \Error("Unsupported method {$method}");
}
/** @var \Illuminate\Http\Client\Response */
$response = Http::connectTimeout(\config('app.debug') ? 3 : 30)
->retry(2, 100)
->{strtolower($method)}($uri, $parameters);
// * Throw an exception if an error occured.
$response->throw();
if ($status !== 200) {
throw $e;
}
if (!$parse_json) {
return $body;
}
/** @var array|boolean|null */
$out = \json_decode($body);
if (!\is_object($out)) {
throw $e;
}
return $out;
} catch (\GuzzleHttp\Exception\ClientException $e) {
return $response;
} catch (HttpClientException $e) {
throw new \RuntimeException($e);
} //end try
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Lib\Instagram\Objects;
use stdClass;
/**
* @property string $insta_id
* @property string $type
* @property string $permalink
* @property string $timestamp
* @property integer $comments_count
* @property null|integer $like_count
* @property string $username
* @property string $caption
*
* @package App\Lib\Instagram\Objects
*/
class TopMedia extends stdClass
{
/**
* Set this object data from Ig api data
*
* @param object $ig_media
* @return void
*/
public function setData(object $ig_media): void
{
$this->insta_id = $ig_media->id;
$this->type = $ig_media->media_type;
$this->permalink = $ig_media->permalink;
$this->timestamp = $ig_media->timestamp;
$this->comments_count = \intval($ig_media->comments_count);
$this->like_count = \optional($ig_media)->like_count ? \intval($ig_media->like_count) : null;
$this->caption = $ig_media->caption;
}
/**
* Set this object username
*
* @param string $username
* @return void
*/
public function setUserName(string $username): void
{
$this->username = $username;
}
}

View file

@ -2,7 +2,7 @@
namespace App\Providers;
use App\Lib\InstagramApiFetcher;
use App\Lib\Instagram\ApiFetcher;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
@ -72,7 +72,7 @@ class AppServiceProvider extends ServiceProvider
);
}
return new InstagramApiFetcher(
return new ApiFetcher(
$url,
$version,
$access_token