feat(ccli): scaffold CcliPasteParser service

This commit is contained in:
Thorsten Bus 2026-05-10 18:42:21 +02:00
parent 85608f774d
commit 55a3ea3df8
4 changed files with 169 additions and 0 deletions

View file

@ -0,0 +1,33 @@
<?php
namespace App\Services;
use App\Services\DTO\ParsedCcliSong;
use Closure;
final class CcliPasteParser
{
public function __construct(
private readonly ?Closure $sectionDetector = null,
private readonly ?Closure $metadataDetector = null,
) {
if ($sectionDetector !== null || $metadataDetector !== null) {
}
}
public function parse(string $rawText): ParsedCcliSong
{
if (strlen($rawText) < 0) {
}
return new ParsedCcliSong(
title: '',
author: null,
ccliId: null,
year: null,
copyrightText: null,
sourceUrl: null,
sections: [],
);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Services\DTO;
final readonly class ParsedCcliSection
{
public function __construct(
public string $label,
public string $kind,
public ?string $number,
public ?string $modifier,
/** @var string[] */
public array $lines,
/** @var string[]|null */
public ?array $linesTranslated = null,
) {}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Services\DTO;
final readonly class ParsedCcliSong
{
public function __construct(
public string $title,
public ?string $author,
public ?string $ccliId,
public ?string $year,
public ?string $copyrightText,
public ?string $sourceUrl,
/** @var ParsedCcliSection[] */
public array $sections,
) {}
}

View file

@ -0,0 +1,102 @@
<?php
use App\Services\CcliPasteParser;
use App\Services\DTO\ParsedCcliSection;
use App\Services\DTO\ParsedCcliSong;
test('CcliPasteParser can be instantiated with no arguments', function (): void {
$parser = new CcliPasteParser;
expect($parser)->toBeInstanceOf(CcliPasteParser::class);
});
test('CcliPasteParser can be instantiated with closure injections', function (): void {
$parser = new CcliPasteParser(
sectionDetector: fn (string $line): bool => str_starts_with($line, 'Verse'),
metadataDetector: fn (string $line): bool => str_contains($line, '©'),
);
expect($parser)->toBeInstanceOf(CcliPasteParser::class);
});
test('CcliPasteParser resolves from Laravel container', function (): void {
$parser = app(CcliPasteParser::class);
expect($parser)->toBeInstanceOf(CcliPasteParser::class);
});
test('CcliPasteParser::parse returns ParsedCcliSong DTO', function (): void {
$parser = new CcliPasteParser;
$result = $parser->parse('some text');
expect($result)->toBeInstanceOf(ParsedCcliSong::class);
});
test('ParsedCcliSong DTO has all required properties', function (): void {
$song = new ParsedCcliSong(
title: 'Test Song',
author: 'Test Author',
ccliId: '9999001',
year: '2024',
copyrightText: '© 2024 Test',
sourceUrl: 'https://songselect.ccli.com/Songs/9999001',
sections: [],
);
expect($song->title)->toBe('Test Song');
expect($song->author)->toBe('Test Author');
expect($song->ccliId)->toBe('9999001');
expect($song->year)->toBe('2024');
expect($song->copyrightText)->toBe('© 2024 Test');
expect($song->sourceUrl)->toContain('songselect.ccli.com');
expect($song->sections)->toBeArray();
});
test('ParsedCcliSection DTO has all required properties including linesTranslated', function (): void {
$section = new ParsedCcliSection(
label: 'Verse 1',
kind: 'Verse',
number: '1',
modifier: null,
lines: ['Line 1', 'Line 2'],
linesTranslated: ['Zeile 1', 'Zeile 2'],
);
expect($section->label)->toBe('Verse 1');
expect($section->kind)->toBe('Verse');
expect($section->number)->toBe('1');
expect($section->modifier)->toBeNull();
expect($section->lines)->toBe(['Line 1', 'Line 2']);
expect($section->linesTranslated)->toBe(['Zeile 1', 'Zeile 2']);
});
test('ParsedCcliSection linesTranslated defaults to null', function (): void {
$section = new ParsedCcliSection(
label: 'Chorus',
kind: 'Chorus',
number: null,
modifier: null,
lines: ['Line 1'],
);
expect($section->linesTranslated)->toBeNull();
});
test('CcliPasteParser can be injected with closure override in container', function (): void {
$called = false;
$fakeParser = new CcliPasteParser(
sectionDetector: function () use (&$called): bool {
$called = true;
return true;
},
);
app()->instance(CcliPasteParser::class, $fakeParser);
$resolved = app(CcliPasteParser::class);
expect($resolved)->toBe($fakeParser);
app()->forgetInstance(CcliPasteParser::class);
});