4.9 KiB
4.9 KiB
Learnings — edit-page-restructure
2026-03-29 — Session start
Code Conventions
- Migrations: anonymous class
return new class extends Migration {; methodsup(): void,down(): void - Models:
$fillablearray;casts()method (not$castsproperty); promoted properties in constructor - SoftDeletes: used on Song and Slide models;
whereNull('deleted_at')in manual queries - PHP string concat: no spaces around
.operator - Return types always present; union types for multiple returns
- Relationships: typed return types (
HasMany,BelongsTo) - Error messages: German Du-form; flash
->with('success', '...'); JSON'message'key
Codebase Facts
service_songstable stores ONLY songs from CTS agenda (viagetSongs())EventAgendaItem.typeactual values: UNKNOWN — Task 5 will discoverEventAgendaItem.getItems()returns ALL agenda items (not just songs)isBeforeEvent=trueitems should be excluded from visible agenda- Settings stored as plain strings in
settingstable (key-value) - Settings controller uses
MACRO_KEYSconstant array for validation - SlideUploader sends
type,service_id,expire_datein FormData - PlaylistExportService builds: info slides → matched songs → moderation → sermon (OLD ORDER)
- Backward compat needed: old services without agenda items fall back to block-based export
Test Patterns
- Pest v4 function-style:
test('description', function() { ... }) - DB reset:
use RefreshDatabase;in class-based, or implicit in Pest - Inertia assertions:
$response->assertInertia(fn ($page) => $page->component('...')->has('...')) - Mocked API: injectable closures in ChurchToolsService constructor
- Storage:
Storage::fake('public')in beforeEach for file tests - Carbon:
Carbon::setTestNow(...)for deterministic dates
2026-03-29 — ServiceAgendaItem DB schema
Created
- Migration:
2026_03_29_100001_create_service_agenda_items_table.php— contains all EventAgendaItem fields from CTS - Migration:
2026_03_29_100002_add_service_agenda_item_id_to_slides_table.php— links slides to agenda items - Model:
ServiceAgendaItemwithscopeVisible(), relationships (service, serviceSong, slides) - Factory:
ServiceAgendaItemFactorywithwithSong()andnonSong()states - Tests:
ServiceAgendaItemTest— 14 tests covering schema, relationships, cascades, scopes
Key Fields
cts_agenda_item_id(nullable, indexed): CTS API item ID for syncingposition(string): CTS agenda item position (e.g. "1", "2", "1.1")type(string): CTS type value ("Song", "Default", "Header", etc.)is_before_event(boolean): filters withscopeVisible()scoperesponsible(json → array cast): responsible personsservice_song_id(nullable FK): links agenda item to song (nullOnDelete)sort_order(unsignedInt): unique per service withunique(['service_id', 'sort_order'])
Relationships
service()BelongsTo Service (cascadeOnDelete)serviceSong()BelongsTo ServiceSong (nullable, nullOnDelete)slides()HasMany Slide (new FK added to slides table)
Test Patterns
- Factory states:
->withSong(song)for song items,->nonSong()for non-song - Scope:
ServiceAgendaItem::visible()filtersis_before_event=false - Migrations now always use anonymous class
return new class extends Migration {} - Unique constraint throws
QueryExceptionwhen violated (tested)
Slide Model Update
- Added
service_agenda_item_idto fillable - Added
serviceAgendaItem()BelongsTo relationship (nullOnDelete)
2026-03-29 — AgendaMatcherService
Implementation
- Created
AgendaMatcherServicewith 4 public methods for glob-style wildcard matching - Uses PHP's native
fnmatch($pattern, $title, FNM_CASEFOLD)for * wildcard support and case-insensitive matching - Key methods:
matches(string $itemTitle, string $pattern): bool— single pattern matchmatchesAny(string $itemTitle, array $patterns): bool— match against arrayfindFirstMatch(array $agendaItems, string $patterns): ?ServiceAgendaItem— comma-separated patterns, returns first match or nullfilterBetween(array $items, ?string $startPattern, ?string $endPattern): array— filters items between boundaries (boundaries excluded)
Test Coverage
- 17 tests in Pest function-style covering all methods
- Patterns tested: exact match, suffix wildcard, prefix wildcard, both sides, case-insensitive, no-match
filterBetweenedge cases: both boundaries, start-only, neither, no-matching-start, empty arrays- Pattern parsing: comma-separated patterns with whitespace trimming
- All tests pass; Pint clean
Code Patterns
- Service class structure matches
SongMatchingService(constructor, public methods, no state) - Public method documentation via docstrings (necessary for API clarity)
- Test organization via section headers (necessary for readability)
- No regex used — just
fnmatch()for simplicity and correctness