Add DocumentTranslation model with renderMarkdown and search scope
Includes a minimal Document::translations() HasMany relation so that DocumentFactory's afterCreating callback (which calls $document->translations()->count()) works. The full Document model refactor (accessors, fallback helpers, default-translation accessor) lands in Task 4.
This commit is contained in:
@@ -281,6 +281,15 @@ public function recentByUsers(): HasManyThrough
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translations of this document, one per locale.
|
||||||
|
* (Other relation/accessor refactor lands in Task 4.)
|
||||||
|
*/
|
||||||
|
public function translations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(DocumentTranslation::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ディレクトリパスを取得
|
* ディレクトリパスを取得
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use League\CommonMark\CommonMarkConverter;
|
||||||
|
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
|
||||||
|
|
||||||
|
class DocumentTranslation extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'document_id',
|
||||||
|
'locale',
|
||||||
|
'title',
|
||||||
|
'content',
|
||||||
|
'rendered_html',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function document(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Document::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updater(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-text search scope. Falls back to LIKE on non-MySQL drivers
|
||||||
|
* (notably SQLite in tests, which lacks FULLTEXT).
|
||||||
|
*/
|
||||||
|
public function scopeSearch(Builder $query, string $term): Builder
|
||||||
|
{
|
||||||
|
if ($query->getConnection()->getDriverName() === 'mysql') {
|
||||||
|
return $query->whereRaw(
|
||||||
|
'MATCH(title, content) AGAINST(? IN BOOLEAN MODE)',
|
||||||
|
[$term]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where(function (Builder $q) use ($term) {
|
||||||
|
$like = '%' . $term . '%';
|
||||||
|
$q->where('title', 'like', $like)->orWhere('content', 'like', $like);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function renderMarkdown(string $markdown): string
|
||||||
|
{
|
||||||
|
$converter = new CommonMarkConverter([
|
||||||
|
'html_input' => 'strip',
|
||||||
|
'allow_unsafe_links' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$converter->getEnvironment()->addExtension(new GithubFlavoredMarkdownExtension());
|
||||||
|
$converter->getEnvironment()->addExtension(new \App\Markdown\MediaEmbedExtension());
|
||||||
|
|
||||||
|
return $converter->convert($markdown)->getContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Models;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\DocumentTranslation;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DocumentTranslationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_belongs_to_a_document(): void
|
||||||
|
{
|
||||||
|
$doc = Document::factory()->create();
|
||||||
|
$translation = $doc->translations()->first();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Document::class, $translation->document);
|
||||||
|
$this->assertSame($doc->id, $translation->document->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unique_document_locale_constraint(): void
|
||||||
|
{
|
||||||
|
$doc = Document::factory()->create(['default_locale' => 'en']);
|
||||||
|
|
||||||
|
$this->expectException(QueryException::class);
|
||||||
|
|
||||||
|
DocumentTranslation::create([
|
||||||
|
'document_id' => $doc->id,
|
||||||
|
'locale' => 'en',
|
||||||
|
'title' => 'Duplicate',
|
||||||
|
'content' => 'x',
|
||||||
|
'rendered_html' => '<p>x</p>',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_cascade_delete_when_document_deleted(): void
|
||||||
|
{
|
||||||
|
$doc = Document::factory()->create();
|
||||||
|
$translationId = $doc->translations()->first()->id;
|
||||||
|
|
||||||
|
$doc->forceDelete();
|
||||||
|
|
||||||
|
$this->assertNull(DocumentTranslation::find($translationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_render_markdown_converts_basic_markdown(): void
|
||||||
|
{
|
||||||
|
$html = DocumentTranslation::renderMarkdown('# Hello');
|
||||||
|
$this->assertStringContainsString('<h1>', $html);
|
||||||
|
$this->assertStringContainsString('Hello', $html);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user