Add (article): ai gen, front views
This commit is contained in:
44
app/Console/Commands/GenerateSitemap.php
Normal file
44
app/Console/Commands/GenerateSitemap.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Spatie\Sitemap\SitemapGenerator;
|
||||
|
||||
class GenerateSitemap extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'sitemap:generate';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate the sitemap.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
SitemapGenerator::create(config('app.url'))
|
||||
->writeToFile(public_path('sitemap.xml'));
|
||||
}
|
||||
}
|
||||
53
app/Helpers/FirstParty/OSSUploader/OSSUploader.php
Normal file
53
app/Helpers/FirstParty/OSSUploader/OSSUploader.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\OSSUploader;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class OSSUploader
|
||||
{
|
||||
public static function readJson($storage_driver, $relative_directory, $filename)
|
||||
{
|
||||
$filepath = rtrim($relative_directory, '/').'/'.$filename;
|
||||
|
||||
try {
|
||||
$jsonContent = Storage::disk($storage_driver)->get($filepath);
|
||||
|
||||
$decodedJson = json_decode($jsonContent, false, 512);
|
||||
|
||||
return $decodedJson;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function uploadJson($storage_driver, $relative_directory, $filename, $jsonData)
|
||||
{
|
||||
$jsonString = json_encode($jsonData, JSON_PRETTY_PRINT);
|
||||
|
||||
try {
|
||||
return self::uploadFile($storage_driver, $relative_directory, $filename, $jsonString);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public static function uploadFile($storage_driver, $relative_directory, $filename, $file)
|
||||
{
|
||||
$filepath = rtrim($relative_directory, '/').'/'.$filename;
|
||||
|
||||
// if(!Storage::disk($storage_driver)->exists($relative_directory))
|
||||
// {
|
||||
// Storage::disk($storage_driver)->makeDirectory($relative_directory);
|
||||
// }
|
||||
|
||||
return Storage::disk($storage_driver)->put($filepath, $file);
|
||||
}
|
||||
}
|
||||
90
app/Helpers/FirstParty/OpenAI/OpenAI.php
Normal file
90
app/Helpers/FirstParty/OpenAI/OpenAI.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\OpenAI;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class OpenAI
|
||||
{
|
||||
public static function writeArticle($title, $description, $article_type, $min, $max)
|
||||
{
|
||||
$system_prompt = "
|
||||
Using the general article structure, please create a Markdown format article on the topic given. The article should prioritize accuracy and provide genuine value to readers. Avoid making assumptions or adding unverified facts. Ensure the article is between {$min}-{$max} words. Write with 8th & 9th grade US english standard.\n\n
|
||||
Structure:\n\n
|
||||
Title\n
|
||||
Provide a headline that captures the essence of the article's focus and is tailored to the reader's needs.\n\n
|
||||
Introduction\n
|
||||
Offer a brief overview or background of the topic, ensuring it's engaging and invites readers to continue reading.\n\n
|
||||
Main Body\n\n
|
||||
Subsection\n
|
||||
Introduce foundational information about the topic, ensuring content is accurate and based on known facts. Avoid generic or speculative statements.\n\n
|
||||
Subsection (if applicable)\n
|
||||
If helpful, use Markdown to create tables to convey comparison of data. Ensure data is accurate and relevant to the reader.\n\n
|
||||
Subsection\n
|
||||
Dive deep into primary elements or facets of the topic, ensuring content is factual and offers value.\n\n
|
||||
Subsection\n
|
||||
Discuss real-world applications or significance, highlighting practical implications or actionable insights for the reader.\n\n
|
||||
Subsection (optional)\n
|
||||
Provide context or relate the topic to relevant past events or trends, making it relatable and more comprehensive.\n\n
|
||||
Subsection (if applicable)\n
|
||||
Predict outcomes, trends, or ramifications, but ensure predictions are rooted in known information or logical reasoning.\n\n
|
||||
Subsection\n
|
||||
Summarise key points or lessons, ensuring they resonate with the initial objectives of the article and provide clear takeaways.\n\n
|
||||
Conclusion\n
|
||||
Revisit main points and offer final thoughts or recommendations that are actionable and beneficial to the reader.\n\n
|
||||
FAQs\n
|
||||
Address anticipated questions or misconceptions about the topic. Prioritize questions that readers are most likely to have and provide clear, concise answers based on factual information.\n
|
||||
Q: Question\n
|
||||
A: Answer\n
|
||||
";
|
||||
|
||||
$user_prompt = "Title: {$title}\nDescription: {$description}\nArticleType: {$article_type}";
|
||||
|
||||
return self::chatCompletion($system_prompt, $user_prompt, 'gpt-3.5-turbo');
|
||||
|
||||
}
|
||||
|
||||
public static function suggestArticleTitles($current_title, $supporting_data, $suggestion_counts)
|
||||
{
|
||||
$system_prompt = "Based on provided article title, identify the main keyword in 1-2 words. Once identified, use the main keyword only to generate {$suggestion_counts} easy-to-read unique, helpful title articles.\n\n
|
||||
Requirements:\n
|
||||
2 descriptive photos keywords to represent article title when put together side-by-side\n
|
||||
No realtime information required\n
|
||||
No guides and how tos\n
|
||||
No punctuation in titles especially colons :\n
|
||||
90-130 characters\n\n
|
||||
|
||||
return in following json format {\"main_keyword\":\"(Main Keyword)\",\"suggestions\":[{\"title\":\"(Title in 90-130 letters)\",\"short_title\":\"(Short Title in 30-40 letters)\",\"article_type\":\"(How-tos|Guides|Interview|Review|Commentary|Feature|News|Editorial|Report|Research|Case-study|Overview|Tutorial|Update|Spotlight|Insights)\",\"description\":\"(SEO description based on main keyword)\",\"photo_keywords\":[\"photo keyword 1\",\"photo keyword 2\"]}]}";
|
||||
|
||||
$user_prompt = "Article Title: {$current_title}";
|
||||
|
||||
$reply = self::chatCompletion($system_prompt, $current_title, 'gpt-3.5-turbo');
|
||||
|
||||
try {
|
||||
return json_decode($reply, false);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function chatCompletion($system_prompt, $user_prompt, $model)
|
||||
{
|
||||
$response = Http::timeout(500)->withToken(config('platform.ai.openai.api_key'))
|
||||
->post('https://api.openai.com/v1/chat/completions', [
|
||||
'model' => $model,
|
||||
'max_tokens' => 2500,
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => $system_prompt.' '.$user_prompt],
|
||||
],
|
||||
]);
|
||||
|
||||
$json_response = json_decode($response->body());
|
||||
|
||||
$reply = $json_response?->choices[0]?->message?->content;
|
||||
|
||||
return $reply;
|
||||
|
||||
}
|
||||
}
|
||||
46
app/Helpers/Global/geo_helper.php
Normal file
46
app/Helpers/Global/geo_helper.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('get_current_ip')) {
|
||||
function get_current_ip()
|
||||
{
|
||||
$ip = null;
|
||||
|
||||
if (app()->environment() == 'local') {
|
||||
return config('platform.general.dev_default_ip');
|
||||
}
|
||||
|
||||
if (getenv('HTTP_CF_CONNECTING_IP')) {
|
||||
$ip = getenv('HTTP_CF_CONNECTING_IP');
|
||||
} elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||
}
|
||||
|
||||
if (! is_empty($ip)) {
|
||||
return $ip;
|
||||
}
|
||||
|
||||
$ip_add_set = null;
|
||||
|
||||
if (getenv('HTTP_CLIENT_IP')) {
|
||||
$ip_add_set = getenv('HTTP_CLIENT_IP');
|
||||
} elseif (getenv('HTTP_X_FORWARDED_FOR')) {
|
||||
$ip_add_set = getenv('HTTP_X_FORWARDED_FOR');
|
||||
} elseif (getenv('HTTP_X_FORWARDED')) {
|
||||
$ip_add_set = getenv('HTTP_X_FORWARDED');
|
||||
} elseif (getenv('HTTP_FORWARDED_FOR')) {
|
||||
$ip_add_set = getenv('HTTP_FORWARDED_FOR');
|
||||
} elseif (getenv('HTTP_FORWARDED')) {
|
||||
$ip_add_set = getenv('HTTP_FORWARDED');
|
||||
} elseif (getenv('REMOTE_ADDR')) {
|
||||
$ip_add_set = getenv('REMOTE_ADDR');
|
||||
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip_add_set = $_SERVER['REMOTE_ADDR'];
|
||||
} else {
|
||||
$ip_add_set = '127.0.0.0';
|
||||
}
|
||||
|
||||
$ip_add_set = explode(',', $ip_add_set);
|
||||
|
||||
return $ip_add_set[0];
|
||||
}
|
||||
}
|
||||
5
app/Helpers/Global/helpers.php
Normal file
5
app/Helpers/Global/helpers.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
require 'string_helper.php';
|
||||
require 'geo_helper.php';
|
||||
require 'platform_helper.php';
|
||||
13
app/Helpers/Global/platform_helper.php
Normal file
13
app/Helpers/Global/platform_helper.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('get_slack_channel_by_env')) {
|
||||
|
||||
function get_slack_channel_by_env($slack_channel = 'slack')
|
||||
{
|
||||
if (app()->environment() == 'local') {
|
||||
return config("platform.notifications.{$slack_channel}.development_channel");
|
||||
} else {
|
||||
return config("platform.notifications.{$slack_channel}.production_channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
120
app/Helpers/Global/string_helper.php
Normal file
120
app/Helpers/Global/string_helper.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
if (! function_exists('epoch_now_timestamp')) {
|
||||
function epoch_now_timestamp()
|
||||
{
|
||||
return (int) round(microtime(true) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('unslug')) {
|
||||
function unslug($slug, $delimiter = '-')
|
||||
{
|
||||
return ucwords(str_replace($delimiter, ' ', $slug));
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_slug')) {
|
||||
function str_slug($string, $delimiter = '-')
|
||||
{
|
||||
return Str::of(trim($string))->slug($delimiter);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_first_sentence')) {
|
||||
function str_first_sentence($str)
|
||||
{
|
||||
// Split the string at ., !, or ?
|
||||
$sentences = preg_split('/(\.|!|\?)(\s|$)/', $str, 2);
|
||||
|
||||
// Return the first part of the array if available
|
||||
if (isset($sentences[0])) {
|
||||
return trim($sentences[0]);
|
||||
}
|
||||
|
||||
// If no sentence ending found, return the whole string
|
||||
return $str;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (! function_exists('is_empty')) {
|
||||
/**
|
||||
* A better function to check if a value is empty or null. Strings, arrays, and Objects are supported.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
function is_empty($value): bool
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if ($value === '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
if (count($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
|
||||
if (count($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_country_name_by_iso')) {
|
||||
function get_country_name_by_iso($country_iso)
|
||||
{
|
||||
if (! is_empty($country_iso)) {
|
||||
|
||||
$country_iso = strtoupper($country_iso);
|
||||
|
||||
try {
|
||||
return config("platform.country_codes.{$country_iso}")['name'];
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return 'International';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_country_emoji_by_iso')) {
|
||||
function get_country_emoji_by_iso($country_iso)
|
||||
{
|
||||
if (! is_empty($country_iso)) {
|
||||
|
||||
$country_iso = strtoupper($country_iso);
|
||||
|
||||
try {
|
||||
return config("platform.country_codes.{$country_iso}")['emoji'];
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return '🌎';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_random')) {
|
||||
function str_random($length = 10)
|
||||
{
|
||||
return Str::random($length);
|
||||
}
|
||||
}
|
||||
580
app/Helpers/ThirdParty/DFS/AbstractModel.php
vendored
Normal file
580
app/Helpers/ThirdParty/DFS/AbstractModel.php
vendored
Normal file
@@ -0,0 +1,580 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\ThirdParty\DFS;
|
||||
|
||||
use DFSClientV3\Bootstrap\Application;
|
||||
use DFSClientV3\Models\DataMapper;
|
||||
use DFSClientV3\Services\HttpClient\Handlers\Responses;
|
||||
use DFSClientV3\Services\HttpClient\HttpClient;
|
||||
|
||||
abstract class AbstractModel
|
||||
{
|
||||
/**
|
||||
* @var null|int timeout for http request
|
||||
*/
|
||||
protected $timeOut = null;
|
||||
|
||||
protected $apiVersion = null;
|
||||
|
||||
/**
|
||||
* @var string | null url to DataForSeo API
|
||||
*/
|
||||
protected $url = null;
|
||||
|
||||
/**
|
||||
* @var int|null PostId is needed for a request, for more information check an example or DFSApi
|
||||
*/
|
||||
protected $postId = null;
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
public $statusCode;
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
public $statusMessage;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
protected $requiredField;
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
protected $mainData;
|
||||
|
||||
/**
|
||||
* @var static RequestType, this param will be sent to DFS API, .xml, .gzip etc...
|
||||
*/
|
||||
protected $requestType;
|
||||
|
||||
/**
|
||||
* @var string Method for http request. POST, GET , PUT, DELETE
|
||||
*/
|
||||
protected $method = 'POST';
|
||||
|
||||
/**
|
||||
* This variable is required for all extended Classes from AbstractModel.
|
||||
*
|
||||
* @var null|string It is a name, when you want to send a request to api, use this param as an api endpoint, example: cmn_user
|
||||
*/
|
||||
protected $requestToFunction;
|
||||
|
||||
/**
|
||||
* This variable is required for all extended Classes from AbstractModel
|
||||
* example for path: results->0->related.
|
||||
*
|
||||
* results - is object link from DFSResponse
|
||||
* 0 - is Index of array or postID
|
||||
* related - is object link containing main data
|
||||
*
|
||||
* @var null|string It is a system variable, it contains a path to main data from response and creates iterable(IteratorAggregator) response.
|
||||
*/
|
||||
protected $pathToMainData = null;
|
||||
|
||||
/**
|
||||
* @var bool If payload contains postId
|
||||
*/
|
||||
protected $isSupportedMerge = false;
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $DFSLogin = null;
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $DFSPassword = null;
|
||||
|
||||
public $response;
|
||||
|
||||
protected $application;
|
||||
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $payload;
|
||||
|
||||
protected $isProcessed;
|
||||
|
||||
protected $mappedMainModel;
|
||||
|
||||
protected $seTypes = ['organic', 'maps', 'local_finder', 'news', 'images', 'search_by_image'];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $jsonContainVariadicType = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pathsToVariadicTypesAndValue = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $customFunctionForPaths = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pathsToDictionary = [];
|
||||
|
||||
/**
|
||||
* @var bool Temp variable, for detect when use new mapper
|
||||
*/
|
||||
protected $useNewMapper = false;
|
||||
|
||||
/**
|
||||
* new version of DataForSeo has two variations of result
|
||||
* 1. Object contains other objects for response.
|
||||
* 2. Object contains other objects, but they is iterable.
|
||||
*
|
||||
* if model has property $resultShouldBeTransformedToArray as true, result index will be transformed to array
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $resultShouldBeTransformedToArray = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->application = Application::getInstance();
|
||||
$this->config = $this->application->getConfig();
|
||||
$this->initDefaultMethods();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $postID mixed
|
||||
*/
|
||||
public function setPostID($postID)
|
||||
{
|
||||
$this->postId = $postID;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $headers array
|
||||
*/
|
||||
public function setHeaders($headers)
|
||||
{
|
||||
if (count($headers) > 0) {
|
||||
foreach ($headers as $key => $value) {
|
||||
$this->config['headers'][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will run request to api.
|
||||
*/
|
||||
protected function get()
|
||||
{
|
||||
$response = $this->process();
|
||||
// check if response contain valid json
|
||||
|
||||
$validResponse = json_decode($response->getResponse());
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
|
||||
}
|
||||
|
||||
return $this->mapData(json_encode($validResponse), $response->getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getAfterMerge(array $modelPool)
|
||||
{
|
||||
if (empty($modelPool)) {
|
||||
throw new \Exception('Model pool can not be empty');
|
||||
}
|
||||
|
||||
$payload = [];
|
||||
$url = null;
|
||||
$apiVersion = null;
|
||||
$timeOut = null;
|
||||
$login = null;
|
||||
$password = null;
|
||||
$method = null;
|
||||
$requestToFunction = null;
|
||||
$config = Application::getInstance()->getConfig();
|
||||
$pathToMainData = null;
|
||||
$resultShouldTransformedToArray = false;
|
||||
$useNewMapper = false;
|
||||
$isJsonContainVariadicType = false;
|
||||
$pathsToVariadicTypesAndValue = [];
|
||||
$customFunctionForPaths = [];
|
||||
|
||||
foreach ($modelPool as $key => $model) {
|
||||
|
||||
// kostyl, all variable will be rewrite every iteration
|
||||
$url = $model->url;
|
||||
$apiVersion = $model->apiVersion;
|
||||
$timeOut = $model->timeOut;
|
||||
$login = $model->DFSLogin;
|
||||
$password = $model->DFSPassword;
|
||||
$method = $model->method;
|
||||
$requestToFunction = $model->requestToFunction;
|
||||
$pathToMainData = $model->pathToMainData;
|
||||
$resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
|
||||
$useNewMapper = $model->isUseNewMapper();
|
||||
$isJsonContainVariadicType = $model->isJsonContainVariadicType();
|
||||
$pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
|
||||
$customFunctionForPaths = $model->getCustomFunctionForPaths();
|
||||
|
||||
if ($model->isSupportedMerge) {
|
||||
if ($model->postId === null) {
|
||||
$payload['json'][] = $model->payload;
|
||||
}
|
||||
|
||||
if ($model->postId !== null) {
|
||||
$payload['json'][$model->postId] = $model->payload;
|
||||
}
|
||||
} else {
|
||||
throw new \Exception('Provided model '.get_class($model).' is not supported merge');
|
||||
}
|
||||
}
|
||||
|
||||
$payload['headers'] = $config['headers'];
|
||||
|
||||
$http = new HttpClient($url, $apiVersion, $timeOut, $login, $password);
|
||||
|
||||
$res = $http->sendSingleRequest($method, $requestToFunction, $payload);
|
||||
// check if response contain valid json
|
||||
|
||||
$validResponse = json_decode($res->getResponse());
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
|
||||
}
|
||||
|
||||
//get called class.
|
||||
$calledClassNameWithNapeSpace = get_called_class();
|
||||
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
|
||||
//for php 7.3 can be use array_last_key
|
||||
$classNameArray[count($classNameArray) - 1];
|
||||
|
||||
$mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $res->getStatus(), $pathToMainData);
|
||||
|
||||
if ($useNewMapper) {
|
||||
$paveDataOptions = new PaveDataOptions();
|
||||
$paveDataOptions->setJson(json_encode($validResponse));
|
||||
$paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
|
||||
$paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
|
||||
$paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);
|
||||
|
||||
$mappedModel = $mapper->paveDataNew($paveDataOptions);
|
||||
|
||||
return $mappedModel;
|
||||
}
|
||||
|
||||
$mappedModel = $mapper->paveData(json_encode($validResponse), null, $resultShouldTransformedToArray);
|
||||
|
||||
return $mappedModel;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|AbstractModel[] $modelPool
|
||||
* @return array|null
|
||||
*/
|
||||
public static function getAsync(array $modelPool, int $timeout = 100)
|
||||
{
|
||||
|
||||
if (count($modelPool) > 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestsPool = [];
|
||||
$results = [];
|
||||
|
||||
$payload = [];
|
||||
$url = null;
|
||||
$apiVersion = null;
|
||||
$timeOut = null;
|
||||
$login = null;
|
||||
$password = null;
|
||||
|
||||
$config = Application::getInstance()->getConfig();
|
||||
$pathToMainData = null;
|
||||
$resultShouldTransformedToArray = false;
|
||||
$useNewMapper = false;
|
||||
$isJsonContainVariadicType = false;
|
||||
$pathsToVariadicTypesAndValue = [];
|
||||
$customFunctionForPaths = [];
|
||||
$pathsToDictionary = [];
|
||||
|
||||
foreach ($modelPool as $key => $model) {
|
||||
|
||||
// kostyl, all variable will be rewrite every iteration
|
||||
$url = $model->url;
|
||||
$apiVersion = $model->apiVersion;
|
||||
$login = $model->DFSLogin;
|
||||
$password = $model->DFSPassword;
|
||||
|
||||
$pathToMainData = $model->pathToMainData;
|
||||
$resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
|
||||
$useNewMapper = $model->isUseNewMapper();
|
||||
$isJsonContainVariadicType = $model->isJsonContainVariadicType();
|
||||
$pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
|
||||
$customFunctionForPaths = $model->getCustomFunctionForPaths();
|
||||
$pathsToDictionary = $model->getPathsToDictionary();
|
||||
|
||||
$payload['json'][] = $model->getPayload();
|
||||
$payload['headers'] = $config['headers'];
|
||||
|
||||
$requestsPool[$key]['method'] = $model->getHttpMethod();
|
||||
$requestsPool[$key]['url'] = $model->getRequestToFunction();
|
||||
$requestsPool[$key]['params'] = $payload;
|
||||
$requestsPool[$key]['pathToMainData'] = $model->getPathToMainData();
|
||||
}
|
||||
|
||||
$http = new HttpClient($url, $apiVersion, $timeout, $login, $password);
|
||||
$responses = $http->sendAsyncRequests($requestsPool, null);
|
||||
|
||||
foreach ($responses as $response) {
|
||||
|
||||
/**
|
||||
* @var Responses $response
|
||||
*/
|
||||
|
||||
//get called class.
|
||||
$calledClassNameWithNapeSpace = get_called_class();
|
||||
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
|
||||
//for php 7.3 can be use array_last_key
|
||||
$classNameArray[count($classNameArray) - 1];
|
||||
|
||||
$mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $response->getStatus(), $pathToMainData);
|
||||
|
||||
if ($useNewMapper) {
|
||||
$paveDataOptions = new PaveDataOptions();
|
||||
$paveDataOptions->setJson($response->getResponse());
|
||||
$paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
|
||||
$paveDataOptions->setPathsToDictionary($pathsToDictionary);
|
||||
$paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
|
||||
$paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);
|
||||
|
||||
$mappedModel = $mapper->paveDataNew($paveDataOptions);
|
||||
|
||||
$results[] = $mappedModel;
|
||||
}
|
||||
|
||||
if (! $useNewMapper) {
|
||||
$results[] = $mapper->paveData($response->getResponse(), null, $resultShouldTransformedToArray);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DFSClientV3\Services\HttpClient\Handlers\Responses
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
$http = new HttpClient($this->url, $this->apiVersion, $this->timeOut, $this->DFSLogin, $this->DFSPassword);
|
||||
$payload = [];
|
||||
|
||||
if (! $this->application) {
|
||||
dd('DFSClient was not init, add to your code: $DFSClient = new DFSClient() ');
|
||||
}
|
||||
|
||||
if (! $this->requestToFunction) {
|
||||
dd('requestFunction can not be null, set this field in your model: '.get_called_class());
|
||||
}
|
||||
|
||||
if ($this->postId === null) {
|
||||
$payload['json'][0] = $this->payload;
|
||||
}
|
||||
|
||||
if ($this->postId != null) {
|
||||
$payload['json'][$this->postId] = $this->payload;
|
||||
}
|
||||
|
||||
$payload['headers'] = $this->config['headers'];
|
||||
|
||||
$res = $http->sendSingleRequest($this->method, $this->requestToFunction, $payload);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getAsJson()
|
||||
{
|
||||
$result = $this->process()->getResponse();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPathToMainData()
|
||||
{
|
||||
return $this->pathToMainData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCalledClass()
|
||||
{
|
||||
$calledClassNameWithNapeSpace = get_called_class();
|
||||
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
|
||||
|
||||
//for php 7.3 can be use array_last_key
|
||||
return $classNameArray[count($classNameArray) - 1];
|
||||
}
|
||||
|
||||
public function setOpt()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function setLogin(string $newLogin)
|
||||
{
|
||||
$this->DFSLogin = $newLogin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPassword(string $newPassword)
|
||||
{
|
||||
$this->DFSPassword = $newPassword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isSuccesful
|
||||
*
|
||||
* return mixed;
|
||||
*/
|
||||
protected function mapData(string $json, bool $isSuccesful = false)
|
||||
{
|
||||
|
||||
$mapper = new DataMapper($this->getCalledClass(), $isSuccesful, $this->pathToMainData);
|
||||
|
||||
if ($this->useNewMapper) {
|
||||
$paveDataOptions = new PaveDataOptions();
|
||||
$paveDataOptions->setJson($json);
|
||||
$paveDataOptions->setJsonContainVariadicType($this->isJsonContainVariadicType());
|
||||
$paveDataOptions->setPathsToVariadicTypesAndValue($this->getPathsToVariadicTypesAndValue());
|
||||
$paveDataOptions->setPathsToDictionary($this->getPathsToDictionary());
|
||||
$paveDataOptions->setCustomFunctionForPaths($this->getCustomFunctionForPaths());
|
||||
|
||||
$mappedModel = $mapper->paveDataNew($paveDataOptions);
|
||||
|
||||
return $mappedModel;
|
||||
}
|
||||
|
||||
$mappedModel = $mapper->paveData($json, null, $this->resultShouldBeTransformedToArray);
|
||||
|
||||
return $mappedModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function resultShouldBeTransformedToArray()
|
||||
{
|
||||
return $this->resultShouldBeTransformedToArray;
|
||||
}
|
||||
|
||||
public function useSandbox(string $url = null)
|
||||
{
|
||||
$this->url = $this->config['sandboxUrl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $timeOut int
|
||||
*/
|
||||
public function setTimeOut(int $timeOut)
|
||||
{
|
||||
$this->timeOut = $timeOut;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isJsonContainVariadicType(): bool
|
||||
{
|
||||
return $this->jsonContainVariadicType;
|
||||
}
|
||||
|
||||
public function getPathsToVariadicTypesAndValue(): array
|
||||
{
|
||||
return $this->pathsToVariadicTypesAndValue;
|
||||
}
|
||||
|
||||
public function getPathsToDictionary(): array
|
||||
{
|
||||
return $this->pathsToDictionary;
|
||||
}
|
||||
|
||||
public function addCustomFunctionForPath(array $customFunction)
|
||||
{
|
||||
$this->customFunctionForPaths = array_merge($this->customFunctionForPaths, $customFunction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomFunctionForPaths(): array
|
||||
{
|
||||
return $this->customFunctionForPaths;
|
||||
}
|
||||
|
||||
private function initDefaultMethods()
|
||||
{
|
||||
$this->addCustomFunctionForPath($this->initCustomFunctionForPaths());
|
||||
|
||||
}
|
||||
|
||||
protected function initCustomFunctionForPaths(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function isUseNewMapper(): bool
|
||||
{
|
||||
return $this->useNewMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHttpMethod()
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRequestToFunction()
|
||||
{
|
||||
return $this->requestToFunction;
|
||||
}
|
||||
|
||||
public function getPayload(): array
|
||||
{
|
||||
return $this->payload ?? [];
|
||||
}
|
||||
}
|
||||
203
app/Helpers/ThirdParty/DFS/SettingSerpLiveAdvanced.php
vendored
Normal file
203
app/Helpers/ThirdParty/DFS/SettingSerpLiveAdvanced.php
vendored
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\ThirdParty\DFS;
|
||||
|
||||
class SettingSerpLiveAdvanced extends AbstractModel
|
||||
{
|
||||
protected $method = 'POST';
|
||||
|
||||
protected $isSupportedMerge = true;
|
||||
|
||||
protected $pathToMainData = 'tasks->{$postID}->result';
|
||||
|
||||
protected $requestToFunction = 'serp/{$se}/{$seType}/live/advanced';
|
||||
|
||||
protected $resultShouldBeTransformedToArray = true;
|
||||
|
||||
protected $jsonContainVariadicType = true;
|
||||
|
||||
protected $pathsToVariadicTypesAndValue = ['tasks->(:number)->result->(:number)->items->(:number)' => 'type'];
|
||||
|
||||
protected $useNewMapper = true;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setUrl(string $url)
|
||||
{
|
||||
$this->payload['url'] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLanguageCode(string $langCode)
|
||||
{
|
||||
$this->payload['language_code'] = $langCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setKeyword(string $keyword)
|
||||
{
|
||||
$this->payload['keyword'] = $keyword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setPriority(string $priority)
|
||||
{
|
||||
$this->payload['priority'] = $priority;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocationName(string $locationName)
|
||||
{
|
||||
$this->payload['location_name'] = $locationName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocationCode(int $locationCode)
|
||||
{
|
||||
$this->payload['location_code'] = $locationCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocationCoordinate(string $locationCoordinate)
|
||||
{
|
||||
$this->payload['location_coordinate'] = $locationCoordinate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLanguageName(string $languageName)
|
||||
{
|
||||
$this->payload['language_name'] = $languageName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setDevice(string $device)
|
||||
{
|
||||
$this->payload['device'] = $device;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setOs(string $os)
|
||||
{
|
||||
$this->payload['os'] = $os;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSeDomain(string $seDomain)
|
||||
{
|
||||
$this->payload['se_domain'] = $seDomain;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setDepth(int $depth)
|
||||
{
|
||||
$this->payload['depth'] = $depth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSearchParam(string $searchParam)
|
||||
{
|
||||
$this->payload['search_param'] = $searchParam;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setTag(string $tag)
|
||||
{
|
||||
$this->payload['tag'] = $tag;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setSeType(string $seType)
|
||||
{
|
||||
if (! in_array($seType, $this->seTypes)) {
|
||||
throw new \Exception('Provided se type not allowed');
|
||||
}
|
||||
|
||||
$this->requestToFunction = str_replace('{$seType}', $seType, $this->requestToFunction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSe(string $seName)
|
||||
{
|
||||
$this->requestToFunction = str_replace('{$se}', $seName, $this->requestToFunction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get(): \DFSClientV3\Entity\Custom\SettingSerpLiveAdvancedEntityMain
|
||||
{
|
||||
return parent::get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getAfterMerge(array $modelPool)
|
||||
{
|
||||
return parent::getAfterMerge($modelPool); // TODO: Change the autogenerated stub
|
||||
}
|
||||
}
|
||||
89
app/Http/Controllers/Front/FrontHomeController.php
Normal file
89
app/Http/Controllers/Front/FrontHomeController.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Post;
|
||||
use Artesaos\SEOTools\Facades\SEOMeta;
|
||||
use Artesaos\SEOTools\Facades\SEOTools;
|
||||
use GrahamCampbell\Markdown\Facades\Markdown;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FrontHomeController extends Controller
|
||||
{
|
||||
public function home(Request $request)
|
||||
{
|
||||
$featured_post = Post::where('status', 'publish')->orderBy('published_at', 'desc')->first();
|
||||
$latest_posts = Post::where(function ($query) use ($featured_post) {
|
||||
$query->whereNotIn('id', [$featured_post->id]);
|
||||
})->where('status', 'publish')->orderBy('published_at', 'desc')->limit(5)->get();
|
||||
|
||||
return response(view('front.welcome', compact('featured_post', 'latest_posts')), 200);
|
||||
}
|
||||
|
||||
public function terms(Request $request)
|
||||
{
|
||||
$markdown = file_get_contents(resource_path('markdown/terms.md'));
|
||||
|
||||
$title = 'Terms of Service';
|
||||
$description = 'Our Terms of Service outline your rights, responsibilities, and the standards we uphold for a seamless experience.';
|
||||
|
||||
SEOTools::metatags();
|
||||
SEOTools::twitter();
|
||||
SEOTools::opengraph();
|
||||
SEOTools::jsonLd();
|
||||
SEOTools::setTitle($title);
|
||||
SEOTools::setDescription($description);
|
||||
SEOMeta::setRobots('noindex');
|
||||
|
||||
$content = Markdown::convert($markdown)->getContent();
|
||||
|
||||
//$content = $this->injectTailwindClasses($content);
|
||||
|
||||
return view('front.pages', compact('content', 'title', 'description'));
|
||||
}
|
||||
|
||||
public function privacy(Request $request)
|
||||
{
|
||||
$markdown = file_get_contents(resource_path('markdown/privacy.md'));
|
||||
|
||||
$title = 'Privacy Policy';
|
||||
$description = 'Our Privacy Policy provides clarity about the data we collect and how we use it, ensuring your peace of mind.';
|
||||
|
||||
SEOTools::metatags();
|
||||
SEOTools::twitter();
|
||||
SEOTools::opengraph();
|
||||
SEOTools::jsonLd();
|
||||
SEOTools::setTitle($title);
|
||||
SEOTools::setDescription($description);
|
||||
SEOMeta::setRobots('noindex');
|
||||
|
||||
$content = Markdown::convert($markdown)->getContent();
|
||||
|
||||
//$content = $this->injectTailwindClasses($content);
|
||||
|
||||
return view('front.pages', compact('content', 'title', 'description'));
|
||||
}
|
||||
|
||||
public function disclaimer(Request $request)
|
||||
{
|
||||
$markdown = file_get_contents(resource_path('markdown/disclaimer.md'));
|
||||
|
||||
$title = 'Disclaimer';
|
||||
$description = 'EchoScoop provides the content on this website purely for informational purposes and should not be interpreted as legal, financial, or medical guidance.';
|
||||
|
||||
SEOTools::metatags();
|
||||
SEOTools::twitter();
|
||||
SEOTools::opengraph();
|
||||
SEOTools::jsonLd();
|
||||
SEOTools::setTitle($title);
|
||||
SEOTools::setDescription($description);
|
||||
SEOMeta::setRobots('noindex');
|
||||
|
||||
$content = Markdown::convert($markdown)->getContent();
|
||||
|
||||
//$content = $this->injectTailwindClasses($content);
|
||||
|
||||
return view('front.pages', compact('content', 'title', 'description'));
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Front/FrontListController.php
Normal file
27
app/Http/Controllers/Front/FrontListController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FrontListController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$posts = Post::where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
||||
|
||||
return view('front.post_list', compact('posts'));
|
||||
}
|
||||
|
||||
public function category(Request $request, $category_slug)
|
||||
{
|
||||
$category = Category::where('slug', $category_slug)->first();
|
||||
|
||||
$posts = $category?->posts()->where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
||||
|
||||
return view('front.post_list', compact('category', 'posts'));
|
||||
}
|
||||
}
|
||||
132
app/Http/Controllers/Front/FrontPostController.php
Normal file
132
app/Http/Controllers/Front/FrontPostController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Post;
|
||||
use GrahamCampbell\Markdown\Facades\Markdown;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class FrontPostController extends Controller
|
||||
{
|
||||
public function index(Request $request, $slug)
|
||||
{
|
||||
$post = Post::where('slug', $slug)->where('status', 'publish')->first();
|
||||
|
||||
if (is_null($post)) {
|
||||
return abort(404);
|
||||
}
|
||||
|
||||
$content = Markdown::convert($post->body)->getContent();
|
||||
|
||||
//dd($content);
|
||||
$content = $this->injectBootstrapClasses($content);
|
||||
$content = $this->injectTableOfContents($content);
|
||||
$content = $this->injectFeaturedImage($post, $content);
|
||||
|
||||
return view('front.single_post', compact('post', 'content'));
|
||||
}
|
||||
|
||||
private function injectBootstrapClasses($content)
|
||||
{
|
||||
$crawler = new Crawler($content);
|
||||
|
||||
$crawler->filter('h1')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' display-6 fw-bolder mt-3 mb-4'));
|
||||
});
|
||||
|
||||
$crawler->filter('h2')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'h4 mb-3'));
|
||||
});
|
||||
|
||||
$crawler->filter('h3')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'h6 mb-2'));
|
||||
});
|
||||
|
||||
$crawler->filter('p')->each(function (Crawler $pNode) {
|
||||
$precedingHeaders = $pNode->previousAll()->filter('h2');
|
||||
|
||||
// If there are no preceding <h2> tags, just process the <p>
|
||||
if (! $precedingHeaders->count()) {
|
||||
$existingClasses = $pNode->attr('class');
|
||||
$newClasses = trim($existingClasses.' mt-2 mb-4');
|
||||
$pNode->getNode(0)->setAttribute('class', $newClasses);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$precedingHeader = $precedingHeaders->first();
|
||||
if (trim($precedingHeader->text()) !== 'FAQs') {
|
||||
$existingClasses = $pNode->attr('class');
|
||||
$newClasses = trim($existingClasses.' mt-2 mb-4');
|
||||
$pNode->getNode(0)->setAttribute('class', $newClasses);
|
||||
}
|
||||
|
||||
if (strpos($pNode->text(), 'Q:') === 0) {
|
||||
$currentClasses = $pNode->attr('class');
|
||||
$newClasses = trim($currentClasses.' fw-bold');
|
||||
$pNode->getNode(0)->setAttribute('class', $newClasses);
|
||||
}
|
||||
});
|
||||
|
||||
$crawler->filter('ul')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'py-2'));
|
||||
});
|
||||
|
||||
$crawler->filter('ol')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'py-2'));
|
||||
});
|
||||
|
||||
// Convert the modified DOM back to string
|
||||
$modifiedContent = '';
|
||||
foreach ($crawler as $domElement) {
|
||||
$modifiedContent .= $domElement->ownerDocument->saveHTML($domElement);
|
||||
}
|
||||
|
||||
return $modifiedContent;
|
||||
}
|
||||
|
||||
private function injectTableOfContents($html)
|
||||
{
|
||||
$crawler = new Crawler($html);
|
||||
|
||||
// Create the Table of Contents
|
||||
$toc = '<div class="p-3 rounded-3 bg-light mb-3"><ol>';
|
||||
$crawler->filter('h2')->each(function (Crawler $node, $i) use (&$toc) {
|
||||
$content = $node->text();
|
||||
$id = 'link-'.$i; // Creating a simple id based on the index
|
||||
$node->getNode(0)->setAttribute('id', $id); // Set the id to the h2 tag
|
||||
$toc .= "<li class=\"py-1\"><a class=\"text-decoration-none hover-text-decoration-underline\" href='#{$id}'>{$content}</a></li>";
|
||||
});
|
||||
$toc .= '</ol></div>';
|
||||
|
||||
// Insert TOC after h1
|
||||
$domDocument = $crawler->getNode(0)->ownerDocument;
|
||||
$fragment = $domDocument->createDocumentFragment();
|
||||
$fragment->appendXML($toc);
|
||||
$h1Node = $crawler->filter('h1')->getNode(0);
|
||||
$h1Node->parentNode->insertBefore($fragment, $h1Node->nextSibling);
|
||||
|
||||
// Get the updated HTML
|
||||
$updatedHtml = $crawler->filter('body')->html();
|
||||
|
||||
return $updatedHtml;
|
||||
|
||||
}
|
||||
|
||||
private function injectFeaturedImage($post, $content)
|
||||
{
|
||||
if (! is_empty($post->featured_image)) {
|
||||
$featured_image_alt = strtolower($post->short_title);
|
||||
$featured_image_alt_caps = strtoupper($post->short_title);
|
||||
$featured_image = "<div class=\"w-100 d-flex justify-content-center\"><figure class=\"text-center\"><img decoding=\"async\" class=\"img-fluid rounded mb-2 shadow\" src=\"{$post->featured_image_cdn}\" alt=\"{$featured_image_alt}\"><figcaption class=\"text-secondary small\">{$featured_image_alt_caps}</figcaption></figure></div>";
|
||||
|
||||
$content = preg_replace('/(<\/h1>)/', '$1'.$featured_image, $content, 1);
|
||||
|
||||
}
|
||||
|
||||
return $content;
|
||||
|
||||
}
|
||||
}
|
||||
37
app/Jobs/GenerateArticleFeaturedImageJob.php
Normal file
37
app/Jobs/GenerateArticleFeaturedImageJob.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Tasks\GenerateArticleFeaturedImageTask;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class GenerateArticleFeaturedImageJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $post;
|
||||
|
||||
public $timeout = 600;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($post)
|
||||
{
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if (! is_null($this->post)) {
|
||||
GenerateArticleFeaturedImageTask::handle($this->post);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Jobs/GenerateArticleJob.php
Normal file
37
app/Jobs/GenerateArticleJob.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Tasks\GenerateArticleTask;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class GenerateArticleJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $serp_url;
|
||||
|
||||
public $timeout = 600;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($serp_url)
|
||||
{
|
||||
$this->serp_url = $serp_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if (! is_null($this->serp_url)) {
|
||||
GenerateArticleTask::handle($this->serp_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
243
app/Jobs/Tasks/GenerateArticleFeaturedImageTask.php
Normal file
243
app/Jobs/Tasks/GenerateArticleFeaturedImageTask.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
|
||||
use App\Models\NewsSerpResult;
|
||||
use DFSClientV3\DFSClient;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Image;
|
||||
|
||||
class GenerateArticleFeaturedImageTask
|
||||
{
|
||||
public static function handle($post)
|
||||
{
|
||||
$keyword = $post->main_keyword;
|
||||
$title = $post->short_title;
|
||||
$article_type = $post->type;
|
||||
$country_iso = 'US';
|
||||
$country_name = get_country_name_by_iso($country_iso);
|
||||
|
||||
$images = [];
|
||||
|
||||
$client = new DFSClient(
|
||||
config('dataforseo.login'),
|
||||
config('dataforseo.password'),
|
||||
config('dataforseo.timeout'),
|
||||
config('dataforseo.api_version'),
|
||||
config('dataforseo.url'),
|
||||
);
|
||||
|
||||
// You will receive SERP data specific to the indicated keyword, search engine, and location parameters
|
||||
$serp_model = new SettingSerpLiveAdvanced();
|
||||
|
||||
$serp_model->setSe('google');
|
||||
$serp_model->setSeType('images');
|
||||
$serp_model->setKeyword($keyword);
|
||||
$serp_model->setLocationName($country_name);
|
||||
$serp_model->setDepth(100);
|
||||
$serp_model->setLanguageCode('en');
|
||||
$serp_res = $serp_model->getAsJson();
|
||||
|
||||
// try {
|
||||
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
//dd($serp_obj);
|
||||
|
||||
if ($serp_obj?->status_code == 20000) {
|
||||
$json_file_name = config('platform.dataset.news.images_serp.file_prefix').str_slug($keyword).'-'.epoch_now_timestamp().'.json';
|
||||
|
||||
$upload_status = OSSUploader::uploadJson(
|
||||
config('platform.dataset.news.images_serp.driver'),
|
||||
config('platform.dataset.news.images_serp.path'),
|
||||
$json_file_name,
|
||||
$serp_obj);
|
||||
|
||||
if ($upload_status) {
|
||||
$news_serp_result = new NewsSerpResult();
|
||||
$news_serp_result->serp_provider = 'dfs';
|
||||
$news_serp_result->serp_se = 'google';
|
||||
$news_serp_result->serp_se_type = 'images';
|
||||
$news_serp_result->serp_keyword = $keyword;
|
||||
$news_serp_result->serp_country_iso = strtoupper($country_iso);
|
||||
$news_serp_result->serp_cost = $serp_obj?->cost;
|
||||
$news_serp_result->result_count = $serp_obj?->tasks[0]?->result[0]?->items_count;
|
||||
$news_serp_result->filename = $json_file_name;
|
||||
$news_serp_result->status = 'initial';
|
||||
|
||||
if ($news_serp_result->save()) {
|
||||
|
||||
$serp_items = $serp_obj?->tasks[0]?->result[0]?->items;
|
||||
|
||||
//dd($serp_items);
|
||||
|
||||
foreach ($serp_items as $item) {
|
||||
if ($item->type == 'images_search') {
|
||||
//dd($item);
|
||||
$images[] = $item->source_url;
|
||||
|
||||
if (count($images) > 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//return $news_serp_result;
|
||||
} else {
|
||||
throw new Exception('Uploading failed', 1);
|
||||
}
|
||||
} else {
|
||||
throw new Exception('Data failed', 1);
|
||||
}
|
||||
// } catch (Exception $e) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
$numImagesInCanvas = 2;
|
||||
|
||||
if ($numImagesInCanvas > count($images)) {
|
||||
$numImagesInCanvas = count($images);
|
||||
}
|
||||
|
||||
$canvasWidth = 720;
|
||||
$canvasHeight = 405;
|
||||
|
||||
$canvas = Image::canvas($canvasWidth, $canvasHeight);
|
||||
|
||||
// Add Images
|
||||
$imageWidth = $canvasWidth / $numImagesInCanvas;
|
||||
|
||||
// Process and place each image
|
||||
$xOffset = 0; // Horizontal offset to place each image
|
||||
for ($i = 0; $i < count($images); $i++) {
|
||||
$url = $images[$i];
|
||||
|
||||
try {
|
||||
$imageResponse = Http::timeout(300)->withHeaders([
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
])->get($url);
|
||||
|
||||
$imageContent = $imageResponse->body();
|
||||
|
||||
$image = Image::make($imageContent)
|
||||
->resize(null, $canvasHeight, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
})
|
||||
->resizeCanvas($imageWidth, $canvasHeight, 'center', false, [255, 255, 255, 0]);
|
||||
//->blur(6)
|
||||
|
||||
$canvas->insert($image, 'top-left', $xOffset, 0);
|
||||
$xOffset += $imageWidth;
|
||||
} catch (Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
$fontSize = 28;
|
||||
$articleTypeFontSize = 24;
|
||||
$padding = 15;
|
||||
|
||||
$fontPath = resource_path('fonts/Inter/Inter-Black.ttf');
|
||||
|
||||
// Split title into words and reconstruct lines
|
||||
$words = explode(' ', $title);
|
||||
$lines = [''];
|
||||
$currentLineIndex = 0;
|
||||
|
||||
foreach ($words as $word) {
|
||||
$potentialLine = $lines[$currentLineIndex] ? $lines[$currentLineIndex].' '.$word : $word;
|
||||
|
||||
$box = imagettfbbox($fontSize, 0, $fontPath, $potentialLine);
|
||||
$textWidth = abs($box[2] - $box[0]);
|
||||
|
||||
if ($textWidth < $canvasWidth * 0.9) {
|
||||
$lines[$currentLineIndex] = $potentialLine;
|
||||
} else {
|
||||
$currentLineIndex++;
|
||||
$lines[$currentLineIndex] = $word;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the dimensions of the article_type text
|
||||
$articleTypeBox = imagettfbbox($articleTypeFontSize, 0, $fontPath, $article_type);
|
||||
$articleTypeWidth = abs($articleTypeBox[2] - $articleTypeBox[0]);
|
||||
$articleTypeHeight = abs($articleTypeBox[7] - $articleTypeBox[1]);
|
||||
|
||||
// Define the start Y position for the article type overlay
|
||||
$articleOverlayStartY = $canvasHeight - ($fontSize * count($lines)) - ($articleTypeHeight + 4 * $padding);
|
||||
|
||||
// Create the blue overlay for the article type
|
||||
$overlayWidth = $articleTypeWidth + 2 * $padding;
|
||||
$canvas->rectangle(20, $articleOverlayStartY, 10 + $overlayWidth, $articleOverlayStartY + $articleTypeHeight + 2 * $padding, function ($draw) {
|
||||
$draw->background([255, 255, 255, 0.8]);
|
||||
});
|
||||
|
||||
// Overlay the article_type text within its overlay, centered horizontally and vertically
|
||||
$textStartX = 20 + ($overlayWidth - $articleTypeWidth) / 2; // Center the text horizontally
|
||||
$canvas->text(strtoupper($article_type), $textStartX, $articleOverlayStartY + ($articleTypeHeight + 2 * $padding) / 2, function ($font) use ($articleTypeFontSize, $fontPath) {
|
||||
$font->file($fontPath);
|
||||
$font->size($articleTypeFontSize);
|
||||
$font->color('#0000FF');
|
||||
$font->align('left');
|
||||
$font->valign('middle'); // This ensures the text is vertically centered within the overlay
|
||||
});
|
||||
|
||||
// Create the blue overlay for the title
|
||||
$titleOverlayStartY = $articleOverlayStartY + $articleTypeHeight + 2 * $padding;
|
||||
|
||||
$canvas->rectangle(0, $titleOverlayStartY, $canvasWidth, $canvasHeight, function ($draw) {
|
||||
$draw->background([0, 0, 255, 0.5]);
|
||||
});
|
||||
|
||||
// Draw each line for the title
|
||||
$yPosition = $titleOverlayStartY + $padding;
|
||||
foreach ($lines as $line) {
|
||||
$canvas->text($line, $canvasWidth / 2, $yPosition, function ($font) use ($fontSize, $fontPath) {
|
||||
$font->file($fontPath);
|
||||
$font->size($fontSize);
|
||||
$font->color('#FFFFFF');
|
||||
$font->align('center');
|
||||
$font->valign('top');
|
||||
});
|
||||
$yPosition += $fontSize + $padding;
|
||||
}
|
||||
|
||||
$filename = $post->slug.'-'.epoch_now_timestamp().'.jpg';
|
||||
|
||||
$ok = OSSUploader::uploadFile('r2', 'post_images/', $filename, (string) $canvas->stream('jpeg'));
|
||||
|
||||
// LQIP
|
||||
// Clone the main image for LQIP version
|
||||
$lqipImage = clone $canvas;
|
||||
|
||||
// Create the LQIP version of the image
|
||||
$lqipImage->fit(10, 10, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
|
||||
$lqipImage->encode('jpg', 5);
|
||||
|
||||
// LQIP filename
|
||||
$lqip_filename = $post->slug.'-'.epoch_now_timestamp().'_lqip.jpg';
|
||||
|
||||
// Upload the LQIP version using OSSUploader
|
||||
$lqip_ok = OSSUploader::uploadFile('r2', 'post_images/', $lqip_filename, (string) $lqipImage->stream('jpeg'));
|
||||
|
||||
if ($ok && $lqip_ok) {
|
||||
|
||||
$post->featured_image = 'post_images/'.$filename;
|
||||
$post->status = 'publish';
|
||||
$post->save();
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
82
app/Jobs/Tasks/GenerateArticleTask.php
Normal file
82
app/Jobs/Tasks/GenerateArticleTask.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OpenAI\OpenAI;
|
||||
use App\Models\Author;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostCategory;
|
||||
use App\Models\SerpUrl;
|
||||
use Exception;
|
||||
|
||||
class GenerateArticleTask
|
||||
{
|
||||
public static function handle(SerpUrl $serp_url)
|
||||
{
|
||||
|
||||
$ai_titles = OpenAI::suggestArticleTitles($serp_url->title, $serp_url->description, 1);
|
||||
|
||||
if (is_null($ai_titles)) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -2);
|
||||
}
|
||||
|
||||
$suggestion = null;
|
||||
|
||||
// dump($ai_titles);
|
||||
|
||||
try {
|
||||
$random_key = array_rand($ai_titles?->suggestions, 1);
|
||||
|
||||
$suggestion = $ai_titles->suggestions[$random_key];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -1);
|
||||
}
|
||||
|
||||
if (is_null($suggestion)) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -3);
|
||||
}
|
||||
|
||||
$markdown = OpenAI::writeArticle($suggestion->title, $suggestion->description, $suggestion->article_type, 500, 800);
|
||||
|
||||
if (is_empty($markdown)) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -4);
|
||||
}
|
||||
|
||||
$post = new Post;
|
||||
$post->title = $suggestion->title;
|
||||
$post->type = $suggestion->article_type;
|
||||
$post->short_title = $ai_titles->short_title;
|
||||
$post->main_keyword = $ai_titles->main_keyword;
|
||||
$post->keywords = $suggestion->photo_keywords;
|
||||
$post->slug = str_slug($suggestion->title);
|
||||
$post->excerpt = $suggestion->description;
|
||||
$post->author_id = Author::find(1)->id;
|
||||
$post->featured = false;
|
||||
$post->featured_image = null;
|
||||
$post->body = $markdown;
|
||||
$post->status = 'draft';
|
||||
|
||||
if ($post->save()) {
|
||||
$post_category = new PostCategory;
|
||||
$post_category->post_id = $post->id;
|
||||
$post_category->category_id = $serp_url->category->id;
|
||||
|
||||
if ($post_category->save()) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, 1);
|
||||
} else {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -5);
|
||||
}
|
||||
}
|
||||
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -6);
|
||||
}
|
||||
|
||||
private static function saveAndReturnSerpProcessStatus($serp_url, $process_status)
|
||||
{
|
||||
$serp_url->process_status = $process_status;
|
||||
$serp_url->save();
|
||||
|
||||
return $serp_url->process_status;
|
||||
}
|
||||
}
|
||||
79
app/Jobs/Tasks/GetNewsSerpTask.php
Normal file
79
app/Jobs/Tasks/GetNewsSerpTask.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
|
||||
use App\Models\Category;
|
||||
use App\Models\NewsSerpResult;
|
||||
use DFSClientV3\DFSClient;
|
||||
use Exception;
|
||||
|
||||
class GetNewsSerpTask
|
||||
{
|
||||
public static function handle(Category $category, $country_iso)
|
||||
{
|
||||
$country_name = get_country_name_by_iso($country_iso);
|
||||
|
||||
$keyword = strtolower("{$category->name}");
|
||||
|
||||
$client = new DFSClient(
|
||||
config('dataforseo.login'),
|
||||
config('dataforseo.password'),
|
||||
config('dataforseo.timeout'),
|
||||
config('dataforseo.api_version'),
|
||||
config('dataforseo.url'),
|
||||
);
|
||||
|
||||
// You will receive SERP data specific to the indicated keyword, search engine, and location parameters
|
||||
$serp_model = new SettingSerpLiveAdvanced();
|
||||
|
||||
$serp_model->setSe('google');
|
||||
$serp_model->setSeType('news');
|
||||
$serp_model->setKeyword($keyword);
|
||||
$serp_model->setLocationName($country_name);
|
||||
$serp_model->setDepth(100);
|
||||
$serp_model->setLanguageCode('en');
|
||||
$serp_res = $serp_model->getAsJson();
|
||||
|
||||
try {
|
||||
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if ($serp_obj?->status_code == 20000) {
|
||||
$json_file_name = config('platform.dataset.news.news_serp.file_prefix').str_slug($category->name).'-'.epoch_now_timestamp().'.json';
|
||||
|
||||
$upload_status = OSSUploader::uploadJson(
|
||||
config('platform.dataset.news.news_serp.driver'),
|
||||
config('platform.dataset.news.news_serp.path'),
|
||||
$json_file_name,
|
||||
$serp_obj);
|
||||
|
||||
if ($upload_status) {
|
||||
$news_serp_result = new NewsSerpResult;
|
||||
$news_serp_result->category_id = $category->id;
|
||||
$news_serp_result->category_name = $category->name;
|
||||
$news_serp_result->serp_provider = 'dfs';
|
||||
$news_serp_result->serp_se = 'google';
|
||||
$news_serp_result->serp_se_type = 'news';
|
||||
$news_serp_result->serp_keyword = $keyword;
|
||||
$news_serp_result->serp_country_iso = strtoupper($country_iso);
|
||||
$news_serp_result->serp_cost = $serp_obj?->cost;
|
||||
$news_serp_result->result_count = $serp_obj?->tasks[0]?->result[0]?->items_count;
|
||||
$news_serp_result->filename = $json_file_name;
|
||||
$news_serp_result->status = 'initial';
|
||||
if ($news_serp_result->save()) {
|
||||
$category->serp_at = now();
|
||||
$category->save();
|
||||
}
|
||||
|
||||
return $news_serp_result;
|
||||
} else {
|
||||
throw new Exception('Uploading failed', 1);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
146
app/Jobs/Tasks/ParseNewsSerpDomainsTask.php
Normal file
146
app/Jobs/Tasks/ParseNewsSerpDomainsTask.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Models\Category;
|
||||
use App\Models\NewsSerpResult;
|
||||
use App\Models\SerpUrl;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
|
||||
class ParseNewsSerpDomainsTask
|
||||
{
|
||||
public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 1)
|
||||
{
|
||||
//dd($news_serp_result->category->serp_at);
|
||||
|
||||
$serp_results = null;
|
||||
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
|
||||
$serp_results = OSSUploader::readJson(
|
||||
config('platform.dataset.news.news_serp.driver'),
|
||||
config('platform.dataset.news.news_serp.path'),
|
||||
$news_serp_result->filename)?->tasks[0]?->result[0]?->items;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$serp_results = null;
|
||||
}
|
||||
|
||||
if (! is_null($serp_results)) {
|
||||
|
||||
$valid_serps = [];
|
||||
|
||||
foreach ($serp_results as $serp_item) {
|
||||
|
||||
$news_date = Carbon::parse($serp_item->timestamp);
|
||||
|
||||
if (is_empty($serp_item->url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if (!str_contains($serp_item->time_published, "hours"))
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
$serp_url = SerpUrl::where('url', $serp_item->url)->first();
|
||||
|
||||
if (! is_null($serp_url)) {
|
||||
if ($serp_url->status == 'blocked') {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (str_contains($serp_item->title, ':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valid_serps[] = $serp_item;
|
||||
|
||||
if (count($valid_serps) >= $serp_counts) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//dd($valid_serps);
|
||||
|
||||
foreach ($valid_serps as $serp_item) {
|
||||
|
||||
//dd($serp_item);
|
||||
|
||||
if (is_null($serp_url)) {
|
||||
$serp_url = new SerpUrl;
|
||||
$serp_url->category_id = $news_serp_result->category_id;
|
||||
$serp_url->category_name = $news_serp_result->category_name;
|
||||
$serp_url->news_serp_result_id = $news_serp_result->id;
|
||||
}
|
||||
|
||||
$serp_url->source = 'serp';
|
||||
$serp_url->url = self::normalizeUrl($serp_item->url);
|
||||
$serp_url->country_iso = $news_serp_result->serp_country_iso;
|
||||
|
||||
if (! is_empty($serp_item->title)) {
|
||||
$serp_url->title = $serp_item->title;
|
||||
}
|
||||
|
||||
if (! is_empty($serp_item->snippet)) {
|
||||
$serp_url->description = $serp_item->snippet;
|
||||
}
|
||||
|
||||
if ($serp_url->isDirty()) {
|
||||
$serp_url->serp_at = $news_serp_result->category->serp_at;
|
||||
}
|
||||
|
||||
if ($serp_url->save()) {
|
||||
$success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
private static function normalizeUrl($url)
|
||||
{
|
||||
try {
|
||||
$parsedUrl = parse_url($url);
|
||||
|
||||
// Force the scheme to https to avoid duplicate content issues
|
||||
$parsedUrl['scheme'] = 'https';
|
||||
|
||||
if (! isset($parsedUrl['host'])) {
|
||||
// If the host is not present, throw an exception
|
||||
throw new \Exception('Host not found in URL');
|
||||
}
|
||||
|
||||
// Check if the path is set and ends with a trailing slash, if so, remove it
|
||||
if (isset($parsedUrl['path']) && substr($parsedUrl['path'], -1) === '/') {
|
||||
$parsedUrl['path'] = rtrim($parsedUrl['path'], '/');
|
||||
}
|
||||
|
||||
// Remove query parameters
|
||||
unset($parsedUrl['query']);
|
||||
|
||||
$normalizedUrl = sprintf(
|
||||
'%s://%s%s',
|
||||
$parsedUrl['scheme'],
|
||||
$parsedUrl['host'],
|
||||
$parsedUrl['path'] ?? ''
|
||||
);
|
||||
|
||||
// Remove fragment if exists
|
||||
$normalizedUrl = preg_replace('/#.*$/', '', $normalizedUrl);
|
||||
|
||||
return $normalizedUrl;
|
||||
} catch (\Exception $e) {
|
||||
// In case of an exception, return the original URL
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
app/Models/Author.php
Normal file
47
app/Models/Author.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class Author
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $avatar
|
||||
* @property string $bio
|
||||
* @property bool $enabled
|
||||
* @property bool $public
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Collection|Post[] $posts
|
||||
*/
|
||||
class Author extends Model
|
||||
{
|
||||
protected $table = 'authors';
|
||||
|
||||
protected $casts = [
|
||||
'enabled' => 'bool',
|
||||
'public' => 'bool',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'avatar',
|
||||
'bio',
|
||||
'enabled',
|
||||
'public',
|
||||
];
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $short_name
|
||||
* @property string|null $slug
|
||||
* @property bool $enabled
|
||||
* @property Carbon|null $created_at
|
||||
@@ -39,6 +40,7 @@ class Category extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'short_name',
|
||||
'slug',
|
||||
'enabled',
|
||||
'_lft',
|
||||
@@ -64,4 +66,16 @@ public function saveQuietly(array $options = [])
|
||||
return $this->save($options);
|
||||
});
|
||||
}
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Post::class,
|
||||
PostCategory::class,
|
||||
'category_id', // Foreign key on PostCategory table
|
||||
'id', // Local key on Post table
|
||||
'id', // Local key on Category table
|
||||
'post_id' // Foreign key on PostCategory table
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
59
app/Models/NewsSerpResult.php
Normal file
59
app/Models/NewsSerpResult.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class NewsSerpResult
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $category_id
|
||||
* @property string $category_name
|
||||
* @property string $serp_provider
|
||||
* @property string $serp_se
|
||||
* @property string $serp_se_type
|
||||
* @property string $serp_keyword
|
||||
* @property string $serp_country_iso
|
||||
* @property float|null $serp_cost
|
||||
* @property int|null $result_count
|
||||
* @property string $filename
|
||||
* @property string $status
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Category $category
|
||||
*/
|
||||
class NewsSerpResult extends Model
|
||||
{
|
||||
protected $table = 'news_serp_results';
|
||||
|
||||
protected $casts = [
|
||||
'category_id' => 'int',
|
||||
'serp_cost' => 'float',
|
||||
'result_count' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'category_id',
|
||||
'category_name',
|
||||
'serp_provider',
|
||||
'serp_se',
|
||||
'serp_se_type',
|
||||
'serp_keyword',
|
||||
'serp_country_iso',
|
||||
'serp_cost',
|
||||
'result_count',
|
||||
'filename',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
123
app/Models/Post.php
Normal file
123
app/Models/Post.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Feed\Feedable;
|
||||
use Spatie\Feed\FeedItem;
|
||||
|
||||
/**
|
||||
* Class Post
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $title
|
||||
* @property string|null $slug
|
||||
* @property string|null $type
|
||||
* @property string|null $excerpt
|
||||
* @property int|null $author_id
|
||||
* @property bool $featured
|
||||
* @property string|null $featured_image
|
||||
* @property string|null $body
|
||||
* @property int $views_count
|
||||
* @property string $status
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Author|null $author
|
||||
* @property Collection|PostCategory[] $post_categories
|
||||
* @property Carbon $published_at
|
||||
*/
|
||||
class Post extends Model implements Feedable
|
||||
{
|
||||
protected $table = 'posts';
|
||||
|
||||
protected $casts = [
|
||||
'author_id' => 'int',
|
||||
'featured' => 'bool',
|
||||
'views_count' => 'int',
|
||||
'keywords' => 'array',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'short_title',
|
||||
'slug',
|
||||
'type',
|
||||
'excerpt',
|
||||
'author_id',
|
||||
'featured',
|
||||
'featured_image',
|
||||
'body',
|
||||
'views_count',
|
||||
'status',
|
||||
'main_keyword',
|
||||
'keywords',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
public function getFeaturedImageLqipCdnAttribute()
|
||||
{
|
||||
if (! is_empty($this->featured_image)) {
|
||||
// Get the extension of the original featured image
|
||||
$extension = pathinfo($this->featured_image, PATHINFO_EXTENSION);
|
||||
|
||||
// Append "_lqip" before the extension to create the LQIP image URL
|
||||
$lqipFeaturedImage = str_replace(".{$extension}", "_lqip.{$extension}", $this->featured_image);
|
||||
|
||||
return 'https://'.Storage::disk('r2')->url($lqipFeaturedImage);
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFeaturedImageCdnAttribute()
|
||||
{
|
||||
if (! is_empty($this->featured_image)) {
|
||||
return 'https://'.Storage::disk('r2')->url($this->featured_image);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->belongsTo(Author::class);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->hasOneThrough(
|
||||
Category::class, // The target model
|
||||
PostCategory::class, // The through model
|
||||
'post_id', // The foreign key on the through model
|
||||
'id', // The local key on the parent model (Post)
|
||||
'id', // The local key on the through model (PostCategory)
|
||||
'category_id' // The foreign key on the target model (Category)
|
||||
);
|
||||
}
|
||||
|
||||
public function toFeedItem(): FeedItem
|
||||
{
|
||||
return FeedItem::create([
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'summary' => $this->excerpt,
|
||||
'updated' => $this->updated_at,
|
||||
'link' => route('posts.show', $this->slug),
|
||||
'authorName' => optional($this->author)->name,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getFeedItems()
|
||||
{
|
||||
return self::where('status', 'published')->latest('published_at')->take(10)->get();
|
||||
}
|
||||
}
|
||||
46
app/Models/PostCategory.php
Normal file
46
app/Models/PostCategory.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class PostCategory
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $post_id
|
||||
* @property int $category_id
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Post $post
|
||||
* @property Category $category
|
||||
*/
|
||||
class PostCategory extends Model
|
||||
{
|
||||
protected $table = 'post_categories';
|
||||
|
||||
protected $casts = [
|
||||
'post_id' => 'int',
|
||||
'category_id' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'post_id',
|
||||
'category_id',
|
||||
];
|
||||
|
||||
public function post()
|
||||
{
|
||||
return $this->belongsTo(Post::class);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
65
app/Models/SerpUrl.php
Normal file
65
app/Models/SerpUrl.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class SerpUrl
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $news_serp_result_id
|
||||
* @property int $category_id
|
||||
* @property string $category_name
|
||||
* @property string $source
|
||||
* @property string $url
|
||||
* @property string $country_iso
|
||||
* @property string|null $title
|
||||
* @property string|null $description
|
||||
* @property Carbon|null $serp_at
|
||||
* @property string $status
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property NewsSerpResult $news_serp_result
|
||||
* @property Category $category
|
||||
*/
|
||||
class SerpUrl extends Model
|
||||
{
|
||||
protected $table = 'serp_urls';
|
||||
|
||||
protected $casts = [
|
||||
'news_serp_result_id' => 'int',
|
||||
'category_id' => 'int',
|
||||
'process_status' => 'int',
|
||||
'serp_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'news_serp_result_id',
|
||||
'category_id',
|
||||
'category_name',
|
||||
'source',
|
||||
'url',
|
||||
'country_iso',
|
||||
'title',
|
||||
'description',
|
||||
'process_status',
|
||||
'serp_at',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function news_serp_result()
|
||||
{
|
||||
return $this->belongsTo(NewsSerpResult::class);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@ public function boot(): void
|
||||
|
||||
Route::middleware('web')
|
||||
->group(base_path('routes/web.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->prefix('tests')
|
||||
->group(base_path('routes/tests.php'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Providers/ViewServiceProvider.php
Normal file
32
app/Providers/ViewServiceProvider.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Views\Composers\ParentCategoryComposer;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ViewServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
// Using class based composers...
|
||||
View::composer('front.layouts.partials.nav', ParentCategoryComposer::class);
|
||||
|
||||
}
|
||||
}
|
||||
14
app/Views/Composers/ParentCategoryComposer.php
Normal file
14
app/Views/Composers/ParentCategoryComposer.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Views\Composers;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ParentCategoryComposer
|
||||
{
|
||||
public function compose(View $view)
|
||||
{
|
||||
$view->with('parent_categories', Category::whereNull('parent_id')->get());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user