- Better refactorisation using Customized api response objects
- Replace Raw Guzzle usage with Illuminate Http Client
This commit is contained in:
parent
87ec0d7f6f
commit
39be89f10f
4 changed files with 127 additions and 74 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
48
app/Lib/Instagram/Objects/TopMedia.php
Normal file
48
app/Lib/Instagram/Objects/TopMedia.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue