170 lines
5.5 KiB
PHP
170 lines
5.5 KiB
PHP
<?php
|
||
|
||
namespace Database\Seeders;
|
||
|
||
use App\Models\Category;
|
||
use App\Models\Course;
|
||
use App\Models\CourseBlock;
|
||
use Illuminate\Database\Eloquent\Collection;
|
||
use Illuminate\Database\Seeder;
|
||
use Illuminate\Support\Str;
|
||
|
||
class CourseContentSeeder extends Seeder
|
||
{
|
||
/**
|
||
* Seed course blocks (rich content) from parsed docx articles.
|
||
* Updates long_desc/requirements/scope on existing courses if empty.
|
||
* Creates new courses from articles that don't match existing ones.
|
||
*
|
||
* Source: storage/app/course_blocks_data.json
|
||
* Run AFTER CourseSeeder. Existing CourseSeeder is untouched.
|
||
*/
|
||
public function run(): void
|
||
{
|
||
$jsonPath = storage_path('app/course_blocks_data.json');
|
||
|
||
if (! file_exists($jsonPath)) {
|
||
$this->command->error('course_blocks_data.json bulunamadı. Önce Python parse script çalıştırın.');
|
||
|
||
return;
|
||
}
|
||
|
||
/** @var array<int, array<string, mixed>> $data */
|
||
$data = json_decode(file_get_contents($jsonPath), true);
|
||
$cats = Category::pluck('id', 'slug');
|
||
$allCourses = Course::all();
|
||
$matched = 0;
|
||
$created = 0;
|
||
$newCourses = 0;
|
||
$blockCount = 0;
|
||
|
||
foreach ($data as $item) {
|
||
$course = $this->findCourse($allCourses, $item);
|
||
|
||
if (! $course) {
|
||
// Create new course from article data
|
||
$categoryId = $cats[$item['category_slug']] ?? null;
|
||
if (! $categoryId) {
|
||
$this->command->warn("Kategori bulunamadı: {$item['category_slug']} -> {$item['title_short']}");
|
||
|
||
continue;
|
||
}
|
||
|
||
$course = Course::create([
|
||
'category_id' => $categoryId,
|
||
'slug' => Str::slug($item['title_short']),
|
||
'title' => $item['title_short'],
|
||
'desc' => Str::limit($item['long_desc'], 200),
|
||
'long_desc' => $item['long_desc'],
|
||
'duration' => '',
|
||
'requirements' => $item['requirements'],
|
||
'scope' => $item['scope'],
|
||
'standard' => 'STCW / IMO Uyumlu',
|
||
'language' => 'Türkçe',
|
||
'location' => 'Kadıköy, İstanbul',
|
||
'meta_title' => $item['title_short'].' | Boğaziçi Denizcilik',
|
||
'meta_description' => Str::limit($item['long_desc'], 155),
|
||
]);
|
||
$newCourses++;
|
||
$allCourses->push($course);
|
||
} else {
|
||
$matched++;
|
||
$this->updateCourseFields($course, $item);
|
||
}
|
||
|
||
// Sync blocks
|
||
$count = $this->syncBlocks($course, $item['blocks']);
|
||
$blockCount += $count;
|
||
}
|
||
|
||
$total = count($data);
|
||
$this->command->info("Toplam: {$total} makale, {$matched} eşleşme, {$newCourses} yeni kurs, {$blockCount} blok.");
|
||
}
|
||
|
||
/**
|
||
* @param Collection<int, Course> $allCourses
|
||
* @param array<string, mixed> $item
|
||
*/
|
||
private function findCourse($allCourses, array $item): ?Course
|
||
{
|
||
$articleSlug = Str::slug($item['title_short']);
|
||
$titleNorm = $this->normalize($item['title_short']);
|
||
|
||
// 1. Exact slug match
|
||
$found = $allCourses->first(fn (Course $c) => $c->slug === $articleSlug);
|
||
if ($found) {
|
||
return $found;
|
||
}
|
||
|
||
// 2. Normalized title contains match
|
||
$found = $allCourses->first(fn (Course $c) => $this->normalize($c->title) === $titleNorm);
|
||
if ($found) {
|
||
return $found;
|
||
}
|
||
|
||
// 3. Fuzzy — first 25 chars of normalized title
|
||
$prefix = mb_substr($titleNorm, 0, 25);
|
||
|
||
return $allCourses->first(fn (Course $c) => str_starts_with($this->normalize($c->title), $prefix));
|
||
}
|
||
|
||
/**
|
||
* Normalize text: lowercase, strip accents, collapse whitespace.
|
||
*/
|
||
private function normalize(string $text): string
|
||
{
|
||
$text = mb_strtolower($text);
|
||
// Turkish specific replacements
|
||
$text = str_replace(
|
||
['ç', 'ğ', 'ı', 'ö', 'ş', 'ü', 'â', 'î', 'û'],
|
||
['c', 'g', 'i', 'o', 's', 'u', 'a', 'i', 'u'],
|
||
$text,
|
||
);
|
||
$text = preg_replace('/[^a-z0-9\s]/', '', $text);
|
||
|
||
return preg_replace('/\s+/', ' ', trim($text));
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $item
|
||
*/
|
||
private function updateCourseFields(Course $course, array $item): void
|
||
{
|
||
$updates = [];
|
||
|
||
if (empty($course->long_desc) && ! empty($item['long_desc'])) {
|
||
$updates['long_desc'] = $item['long_desc'];
|
||
}
|
||
|
||
if ((empty($course->requirements) || $course->requirements === []) && ! empty($item['requirements'])) {
|
||
$updates['requirements'] = $item['requirements'];
|
||
}
|
||
|
||
if ((empty($course->scope) || $course->scope === []) && ! empty($item['scope'])) {
|
||
$updates['scope'] = $item['scope'];
|
||
}
|
||
|
||
if (! empty($updates)) {
|
||
$course->update($updates);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param array<int, array<string, mixed>> $blocks
|
||
*/
|
||
private function syncBlocks(Course $course, array $blocks): int
|
||
{
|
||
$count = 0;
|
||
|
||
foreach ($blocks as $block) {
|
||
CourseBlock::updateOrCreate(
|
||
['course_id' => $course->id, 'order_index' => $block['order_index']],
|
||
['type' => $block['type'], 'content' => $block['content']],
|
||
);
|
||
$count++;
|
||
}
|
||
|
||
return $count;
|
||
}
|
||
}
|