Files
portal-api/app/Http/Services/DocsWordpressSyncService.php
T
baodev0 520f21cb11 feat: add GeneratesUniqueSlug class for unique slug generation
feat: create PostCategoryFactory for generating post category data

feat: create PostFactory for generating post data

feat: add retry_requested_at column to services table

feat: add vcenter provisioning fields to products table

feat: add module_host_id to os table

feat: create email_notification_templates table for email templates

feat: add scenario and format fields to template table

feat: add admin fields to transactions table

fix: update ticket table columns and types

feat: add uuid to ticket table and backfill existing records

fix: make ticket_reply user_id nullable

feat: create notifications table for user notifications

fix: make ticket uuid nullable

feat: create post_categories table for blog categories

feat: create posts table for blog posts

feat: add invoice email template for PDF generation
2026-05-25 15:39:42 +07:00

155 lines
5.0 KiB
PHP

<?php
namespace App\Http\Services;
use App\Models\Post;
use App\Models\PostCategory;
use App\Support\GeneratesUniqueSlug;
use Illuminate\Support\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class DocsWordpressSyncService
{
/**
* @return array{created:int,updated:int,skipped:int,total:int}
*/
public function sync(?string $sourceUrl = null, int $perPage = 20, ?int $maxPages = null): array
{
$url = $sourceUrl ?: config('services.docs.wordpress_posts_url');
if (!$url) {
throw new \RuntimeException('Chưa cấu hình nguồn đồng bộ WordPress.');
}
$created = 0;
$updated = 0;
$skipped = 0;
$total = 0;
$page = 1;
while (true) {
if ($maxPages !== null && $page > max(1, $maxPages)) {
break;
}
$response = Http::timeout(20)
->retry(2, 300)
->acceptJson()
->get($url, [
'per_page' => min(max($perPage, 1), 100),
'page' => $page,
'_embed' => 1,
'status' => 'publish',
]);
$response->throw();
/** @var array<int, array<string, mixed>> $items */
$items = $response->json() ?? [];
if (empty($items)) {
break;
}
foreach ($items as $item) {
$total++;
$result = $this->syncItem($item);
match ($result) {
'created' => $created++,
'updated' => $updated++,
default => $skipped++,
};
}
if (count($items) < $perPage) {
break;
}
$page++;
}
return compact('created', 'updated', 'skipped', 'total');
}
private function syncItem(array $item): string
{
$title = trim((string) data_get($item, 'title.rendered', ''));
$content = (string) data_get($item, 'content.rendered', '');
$slug = trim((string) data_get($item, 'slug', ''));
if ($title === '' || $content === '') {
return 'skipped';
}
$category = $this->resolveCategory($item);
$existing = Post::withTrashed()->where('slug', $slug)->first();
$thumbnail = $this->resolveFeaturedImage($item);
$excerpt = trim(strip_tags((string) data_get($item, 'excerpt.rendered', '')));
$publishedAt = data_get($item, 'date_gmt') ?: data_get($item, 'date');
$payload = [
'post_category_id' => $category->id,
'title' => html_entity_decode(strip_tags($title), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'excerpt' => $excerpt !== '' ? html_entity_decode($excerpt, ENT_QUOTES | ENT_HTML5, 'UTF-8') : null,
'content' => $content,
'thumbnail' => $thumbnail,
'status' => 'published',
'is_featured' => false,
'reading_time' => $this->estimateReadingTime($content),
'meta_title' => html_entity_decode(strip_tags($title), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'meta_description' => $excerpt !== '' ? html_entity_decode($excerpt, ENT_QUOTES | ENT_HTML5, 'UTF-8') : null,
'published_at' => $publishedAt ? Carbon::parse($publishedAt) : now(),
];
if ($existing) {
$existing->restore();
$existing->update($payload);
return 'updated';
}
Post::create($payload + [
'slug' => GeneratesUniqueSlug::forModel(Post::class, $slug !== '' ? $slug : $title),
'view_count' => 0,
]);
return 'created';
}
private function resolveCategory(array $item): PostCategory
{
$termGroups = (array) data_get($item, '_embedded.wp:term', []);
$terms = is_array($termGroups[0] ?? null) ? $termGroups[0] : [];
$firstTerm = Arr::first($terms);
$categoryName = trim((string) data_get($firstTerm, 'name', 'Imported Docs'));
$categorySlug = trim((string) data_get($firstTerm, 'slug', 'imported-docs'));
return PostCategory::firstOrCreate(
['slug' => $categorySlug !== '' ? $categorySlug : 'imported-docs'],
[
'name' => $categoryName !== '' ? $categoryName : 'Imported Docs',
'description' => 'Danh mục đồng bộ từ WordPress Docs.',
'status' => 'active',
'sort_order' => 999,
]
);
}
private function resolveFeaturedImage(array $item): ?string
{
return data_get($item, '_embedded.wp:featuredmedia.0.source_url')
?: data_get($item, '_embedded.wp:featuredmedia.0.media_details.sizes.full.source_url')
?: null;
}
private function estimateReadingTime(string $html): int
{
$wordCount = str_word_count(Str::of(strip_tags($html))->squish()->value());
return max(1, (int) ceil($wordCount / 200));
}
}