This document outlines the strategy for coordinating data serialization between the frontend and the API backend, ensuring that data structures are consistent, schema-validated, and properly handled throughout the request lifecycle.
A critical issue was discovered where valid input data passes schema validation, but the data saved to disk becomes invalid due to PHP object serialization behavior. This document provides a comprehensive plan to:
- Fix the immediate serialization issue
- Establish patterns for future implementations
- Coordinate frontend/backend development for all entity types
When creating/updating calendar data via PUT/PATCH requests:
- Input: Frontend sends valid JSON data (e.g.,
{ "litcal": [...] }) - Validation: API validates input against JSON schema - PASSES
- Model Conversion: API converts to PHP model objects (
DiocesanData, etc.) - Serialization: API calls
json_encode($modelObject)to save - Output: PHP serializes internal object structure, producing INVALID JSON
Example of the mismatch:
// Expected (schema-compliant):
{
"litcal": [
{ "liturgical_event": {...}, "metadata": {...} }
]
}
// Actual (PHP serialized):
{
"litcal": {
"litcalItems": [
{ "liturgical_event": {...}, "metadata": {...} }
]
}
}The schema validation IS working correctly - it validates the input data from the frontend. The issue is:
- Validation happens before model conversion
- No validation happens after serialization (before saving)
- PHP's default
json_encode()on objects produces a different structure than the input
PHP model classes (DiocesanData, DiocesanLitCalItemCollection, etc.) do not implement JsonSerializable.
When json_encode() is called on these objects, PHP serializes all public properties, including internal
wrapper properties like $litcalItems, resulting in nested structures that don't match the schema.
During analysis, an additional issue was discovered: the @phpstan-type declarations in several model classes
were describing computed output data (with properties like missal, grade_lcl, common_lcl) instead of
raw source data (with { liturgical_event, metadata } structure).
Affected files (now fixed):
src/Models/LitCalItemCollection.php- IncorrectLiturgicalEventArray/Objecttypessrc/Models/RegionalData/DiocesanData/DiocesanLitCalItemCollection.php- Imported incorrect typessrc/Models/RegionalData/DiocesanData/DiocesanData.php- Imported incorrect typessrc/Models/RegionalData/WiderRegionData/WiderRegionData.php- Imported incorrect typessrc/Models/RegionalData/NationalData/NationalData.php- Had local incorrect types
Fix applied: Updated all type declarations to correctly reference the { liturgical_event, metadata }
structure defined in LitCalItem and DiocesanLitCalItem.
After further analysis, a simpler approach was identified that avoids the serialization issue entirely:
The current PUT/PATCH flow is:
- Raw JSON payload received
- Schema validation PASSES
- Convert to DTO:
DiocesanData::fromObject($payload)← Unnecessary for write operations json_encode($dto)to write to disk ← Produces INVALID structure
Since schema validation already ensures data integrity, the handler can write the validated raw payload directly:
// Current code (problematic):
if (RegionalDataHandler::validateDataAgainstSchema($payload, LitSchema::DIOCESAN->path())) {
$params['payload'] = DiocesanData::fromObject($payload); // ← Converts to DTO
}
// Later...
$calendarData = json_encode($payload, ...); // ← DTO serializes incorrectly
// Proposed fix:
if (RegionalDataHandler::validateDataAgainstSchema($payload, LitSchema::DIOCESAN->path())) {
// Keep raw payload for writing to disk
$params['rawPayload'] = $payload; // stdClass - validated against schema
// Only convert to DTO if we need to access typed properties (e.g., for metadata extraction)
$params['payload'] = DiocesanData::fromObject($payload);
}
// Later...
$calendarData = json_encode($params['rawPayload'], ...); // ← Write raw validated JSON- No new models needed - Avoids code duplication and bloat
- Schema validation ensures correctness - Already validated before conversion
- DTOs remain for their intended purpose - Reading and manipulating data programmatically
- Simple fix - Minimal code changes required
- Consistent with GET flow - GET already returns raw JSON from files
DTOs should still be used when:
- Extracting typed properties (e.g.,
$params['payload']->metadata->nation) - Iterating over items with type safety
- Applying translations or other business logic
An important validation rule was added: the i18n property is required for PUT/PATCH operations,
even though the JSON schema marks it as optional.
Why the difference?
- JSON Schema marks
i18nas optional because it reflects the stored file structure - i18n data is extracted and written to separate locale files, so it's not present in the calendar resource file after processing. - PUT/PATCH requests must include
i18nbecause without translations, the calendar data would be incomplete and unusable.
Implementation:
// In RegionalDataHandler - before processing PUT/PATCH
// Schema marks i18n as optional (for stored files), but it's required for PUT/PATCH
if (!property_exists($payload, 'i18n')) {
throw new UnprocessableContentException('The i18n property is required for PUT/PATCH operations');
}This explicit validation ensures that:
- Calendar data consistently has associated translations
- The API fails fast with a clear error message
- Schema validation alone isn't relied upon for business rules
| Entity Type | Schema File | Model Class | Frontend Form | Status |
|---|---|---|---|---|
| Diocesan Calendar | DiocesanCalendar.json |
DiocesanData |
extending.php?choice=diocesan |
PUT/PATCH/DELETE: ✅ Working (raw payload serialization) |
| National Calendar | NationalCalendar.json |
NationalData |
extending.php?choice=national |
PUT/PATCH/DELETE: ✅ Working (raw payload serialization) |
| Wider Region | WiderRegionCalendar.json |
WiderRegionData |
extending.php?choice=widerRegion |
PUT/PATCH/DELETE: ✅ Working (raw payload serialization) |
Note (2025-11): Audit logging has been added to all write operations (PUT/PATCH/DELETE). The serialization issue has been fixed - handlers now use the raw payload (
\stdClass) forjson_encode()instead of DTOs, preserving the schema-compliant JSON structure when saving to disk.
| Entity Type | Schema File | Model Class | Frontend Form | Status |
|---|---|---|---|---|
| Proprium de Sanctis | PropriumDeSanctis.json |
TBD | admin.php (partial) |
Partial frontend, API not implemented |
| Proprium de Tempore | PropriumDeTempore.json |
TBD | admin.php (partial) |
Partial frontend, API not implemented |
Note: There is partial support for handling missals data in the frontend
admin.php. However, this will need significant work and should be aligned with the same workflow patterns used for creating national, diocesan, and wider region calendar data. The goal is to have a consistent approach across all entity types for data serialization, validation, and API communication.
| Entity Type | Schema File | Model Class | Frontend Form | Status |
|---|---|---|---|---|
| Decrees | LitCalDecreesSource.json |
TBD | TBD | Not Implemented |
| Entity Type | Schema File | Model Class | Frontend Form | Status |
|---|---|---|---|---|
| Test Cases | LitCalTest.json |
TBD | TBD | Partial |
Instead of implementing JsonSerializable on all model classes (which is complex and error-prone),
the simpler approach is to write the validated raw payload directly to disk.
Changes required in RegionalDataHandler:
- Add
rawPayloadproperty toRegionalDataParams - Store the raw
\stdClasspayload alongside the DTO - Use raw payload when writing to files
Implementation (actual):
// In RegionalDataHandler::parsePayload() - after schema validation
$this->validateDataAgainstSchema($payload, LitSchema::DIOCESAN->path());
$params['rawPayload'] = $payload; // Keep raw stdClass for writing
$params['payload'] = DiocesanData::fromObject($payload); // DTO for typed property access
// In createDiocesanCalendar() - use raw payload for writing
// Remove i18n first (written separately)
unset($this->params->rawPayload->i18n);
$calendarData = json_encode($this->params->rawPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
file_put_contents($diocesanCalendarFile, $calendarData . PHP_EOL);Status (2025-11): This approach is now fully implemented for all calendar types (diocesan, national, wider region). The raw payload strategy avoids the complexity of implementing
JsonSerializableon all model classes.
The incorrect @phpstan-type declarations have been fixed. See "Additional Issue" section above.
For extra safety, validate the written file after saving:
// After writing to disk
$writtenData = Utilities::jsonFileToObject($diocesanCalendarFile);
if (!self::validateDataAgainstSchema($writtenData, LitSchema::DIOCESAN->path())) {
// Log error, rollback, or alert
throw new ImplementationException('Written data does not conform to schema');
}If the raw payload approach is not feasible for some use cases, JsonSerializable can be implemented
on model classes. This is more complex because:
- All nested classes must also implement
JsonSerializable - Enums must serialize to their
valueproperty, notname - Computed properties must be excluded
- Collection classes must serialize as arrays, not objects
If needed, affected classes would be:
AbstractJsonSrcData,AbstractJsonSrcDataArray(base classes)DiocesanData,DiocesanLitCalItemCollection,DiocesanLitCalItem, etc.NationalData,LitCalItemCollection,LitCalItem, etc.WiderRegionData,WiderRegionMetadata- All
LitCalItem*subclasses for different action types
Understanding how data flows from the frontend through the backend to storage is essential for coordinating this effort.
The frontend must produce a payload matching the JSON schema. For diocesan calendars (DiocesanCalendar.json):
{
"litcal": [
{
"liturgical_event": {
"event_key": "StExampleSaint",
"color": ["white"],
"grade": 3,
"common": ["Martyrs"],
"day": 15,
"month": 6
},
"metadata": {
"form_rownum": 0,
"since_year": 2020
}
}
],
"metadata": {
"diocese_id": "DIOCESE_ID",
"diocese_name": "Diocese Name",
"nation": "US",
"locales": ["en_US"],
"timezone": "America/New_York"
},
"settings": {
"epiphany": "SUNDAY_JAN2_JAN8",
"ascension": "SUNDAY",
"corpus_christi": "SUNDAY"
},
"i18n": {
"en_US": {
"StExampleSaint": "Saint Example"
}
}
}The backend handles the payload in two stages:
- Write
i18ndata to separate locale files in thei18n/folder - Write remaining data (
litcal,metadata,settings) to the calendar resource file
Frontend Payload
│
▼
┌──────────────────────────────────────┐
│ Backend receives payload │
│ - Validates against JSON schema │
│ - Converts to DTO (for property │
│ access like metadata.diocese_id) │
└──────────────────────────────────────┘
│
├──────────────────────────────────────────────────┐
▼ ▼
┌──────────────────────────────┐ ┌─────────────────────────────────────┐
│ Write i18n data │ │ Write calendar data │
│ │ │ │
│ For each locale in i18n: │ │ Remove i18n from payload │
│ - Write to │ │ Write to: │
│ i18n/{locale}.json │ │ {calendar_id}.json │
│ │ │ │
│ Example: │ │ Contains: │
│ i18n/en_US.json = │ │ - litcal (array) │
│ {"StExampleSaint": │ │ - metadata (object) │
│ "Saint Example"} │ │ - settings (object, optional) │
└──────────────────────────────┘ └─────────────────────────────────────┘
Problem 1: litcal serialization
// In createDiocesanCalendar()
$payload = $this->params->payload; // DiocesanData DTO
// ...
$calendarData = json_encode($payload, ...); // ← Serializes DTO incorrectly!The DiocesanData DTO has a $litcal property of type DiocesanLitCalItemCollection, which has a
$litcalItems property. Without JsonSerializable, this produces:
{ "litcal": { "litcalItems": [...] } } // WRONG!Instead of:
{ "litcal": [...] } // CorrectProblem 2: i18n serialization
foreach ($payload->i18n as $locale => $litCalEventsI18n) {
json_encode($litCalEventsI18n, ...); // ← TranslationMap with private properties
}TranslationMap has private properties ($translations, $keys), so json_encode() produces {}
(empty object) instead of the translation data.
The fix is straightforward - use the raw \stdClass payload for writing instead of the DTO:
Step 1: Store raw payload in RegionalDataParams
// In RegionalDataParams.php
public DiocesanData|NationalData|WiderRegionData $payload;
public \stdClass $rawPayload; // NEW: Keep raw payload for writingStep 2: Store raw payload during initialization
// In RegionalDataHandler::initParams()
if (RegionalDataHandler::validateDataAgainstSchema($payload, LitSchema::DIOCESAN->path())) {
$params['rawPayload'] = $payload; // Raw stdClass for writing
$params['payload'] = DiocesanData::fromObject($payload); // DTO for property access
$key = $params['payload']->metadata->diocese_id;
}Step 3: Use raw payload for writing
// In createDiocesanCalendar()
$rawPayload = $this->params->rawPayload;
// Write i18n from raw payload
foreach ($rawPayload->i18n as $locale => $litCalEventsI18n) {
$diocesanCalendarI18nFile = /* ... */;
file_put_contents(
$diocesanCalendarI18nFile,
json_encode($litCalEventsI18n, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL
);
}
// Remove i18n from raw payload before writing calendar file
unset($rawPayload->i18n);
// Write calendar data from raw payload
$calendarData = json_encode($rawPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
file_put_contents($diocesanCalendarFile, $calendarData . PHP_EOL);Generate TypeScript interfaces from JSON schemas to ensure frontend serialization matches backend expectations.
Location: liturgy-components-js/src/types/
// Generated from DiocesanCalendar.json
export interface DiocesanCalendar {
litcal: DiocesanLitCalItem[];
metadata: DiocesanMetadata;
settings?: DiocesanSettings;
i18n?: Record<string, Record<string, string>>;
}
export interface DiocesanLitCalItem {
liturgical_event: DiocesanLiturgicalEvent;
metadata: DiocesanItemMetadata;
}
// ... etc.Both frontend and backend should use the same validation approach:
Frontend (JavaScript):
import Ajv from 'ajv';
import diocesanSchema from './schemas/DiocesanCalendar.json';
const ajv = new Ajv();
const validate = ajv.compile(diocesanSchema);
function validateDiocesanCalendar(data) {
const valid = validate(data);
if (!valid) {
console.error('Validation errors:', validate.errors);
throw new Error('Data does not conform to schema');
}
return true;
}Backend (PHP):
// Already exists in RegionalDataHandler::validateDataAgainstSchema()
// Ensure it's called both on input AND before saving outputCreate comprehensive documentation of expected payload structures for each endpoint.
Location: docs/api/payloads/
diocesan-calendar-payload.mdnational-calendar-payload.mdwider-region-payload.mdmissals-payload.mddecrees-payload.md
Backend Tasks:
-
Implement(Using raw payload approach instead)JsonSerializableon all diocesan model classes - Add
rawPayloadproperty toRegionalDataParamsand use it for writing - Add post-serialization validation in
createDiocesanCalendar() - Add post-serialization validation in
updateDiocesanCalendar() - Implement
deleteDiocesanCalendar()fully - Add audit logging to write operations
- Write PHPUnit tests for serialization round-trip ✅ (
PayloadValidationTest.php)
Frontend Tasks:
- Review
saveDiocesanCalendar_btnClicked()inextending.js - Add client-side schema validation before submission
- Ensure
CalendarDatastructure matchesDiocesanCalendar.jsonschema - Add error handling for validation failures
Testing:
- Create integration test: submit from frontend → validate API response
- Create round-trip test: save → load → verify identical structure
- Test edge cases: empty litcal array, null settings, multiple locales
Backend Tasks:
-
Implement(Using raw payload approach instead)JsonSerializableon all national model classes - Use
rawPayloadfor writing increateNationalCalendar() - Add post-serialization validation in
createNationalCalendar() -
updateNationalCalendar()implementation exists -
deleteNationalCalendar()implementation exists - Add audit logging to write operations
- Handle complex litcal item types (makePatron, setProperty, moveEvent, createNew)
Frontend Tasks:
- Review
serializeNationalCalendarData()inextending.js - Ensure all action types serialize correctly
- Add client-side validation
Testing:
- Test each litcal action type individually
- Test combinations of action types
- Test i18n data handling
Backend Tasks:
-
Implement(Using raw payload approach instead)JsonSerializableon wider region model classes -
createWiderRegionCalendar()implemented with raw payload approach ✅ -
updateWiderRegionCalendar()uses raw payload for writing ✅ -
deleteWiderRegionCalendar()implementation exists (via genericdeleteCalendar()) - Add audit logging to write operations
Frontend Tasks:
- Review
serializeWiderRegionData()inextending.js - Ensure proper locale handling
- Add client-side validation
Important: The frontend
admin.phpalready has partial support for missals data management. The implementation should follow the same patterns established for calendar data (diocesan, national, wider region) to maintain consistency across the codebase.
Backend Tasks:
- Design model classes for Proprium de Sanctis (following established patterns)
- Design model classes for Proprium de Tempore (following established patterns)
- Implement PUT/PATCH/DELETE handlers in
MissalsHandler - Implement
JsonSerializableon all classes - Add post-serialization validation
Frontend Tasks:
- Review existing
admin.phpmissals functionality - Align serialization logic with patterns from
extending.js - Implement consistent form handling and validation
- Ensure authentication integration matches other protected endpoints
Alignment Goals:
- Use the same
CalendarData-style state management pattern - Implement the same validation flow (client-side then server-side)
- Use consistent error handling and user feedback patterns
- Follow the same authentication/authorization patterns
Backend Tasks:
- Design model classes for decrees
- Implement PUT/PATCH/DELETE handlers in
DecreesHandler - Implement
JsonSerializableon all classes
Frontend Tasks:
- Design UI for decrees management (if needed)
- Implement form and serialization logic following established patterns
For each model class that implements JsonSerializable:
public function testJsonSerializeProducesSchemaCompliantOutput(): void
{
$data = DiocesanData::fromObject($this->getValidTestData());
$serialized = json_encode($data);
$decoded = json_decode($serialized);
$this->assertTrue(
RegionalDataHandler::validateDataAgainstSchema($decoded, LitSchema::DIOCESAN->path())
);
}
public function testRoundTripPreservesData(): void
{
$original = $this->getValidTestData();
$model = DiocesanData::fromObject($original);
$serialized = json_encode($model);
$decoded = json_decode($serialized);
$this->assertEquals($original->litcal, $decoded->litcal);
$this->assertEquals($original->metadata, $decoded->metadata);
}public function testCreateDiocesanCalendarStoresValidData(): void
{
// Submit valid data via API
$response = $this->createDiocesanCalendar($validPayload);
$this->assertEquals(201, $response->getStatusCode());
// Read back the stored file
$storedData = file_get_contents($expectedFilePath);
$decoded = json_decode($storedData);
// Validate against schema
$this->assertTrue(
RegionalDataHandler::validateDataAgainstSchema($decoded, LitSchema::DIOCESAN->path())
);
}Using a test framework (e.g., Playwright, Cypress) to test the full flow:
- Fill out the diocesan calendar form in the frontend
- Submit via the Save button
- Verify API response is successful
- Load the calendar data back
- Verify all fields are correctly populated
Add✅ DonerawPayloadproperty toRegionalDataParamsModify✅ DoneRegionalDataHandlerto store raw\stdClasspayload alongside DTOUse raw payload for✅ Donejson_encode()in all create/update methods- Add post-serialization validation before returning success response
Write serialization round-trip tests to prevent regression✅ Done (PayloadValidationTest.php)
Complete PATCH implementation for diocesan calendars✅ DoneComplete DELETE implementation for diocesan calendars✅ DoneAdd audit logging to write operations✅ DoneImplement✅ DonecreateWiderRegionCalendar()- Add frontend validation
- Write comprehensive tests
- Design and implement missals model classes following established patterns
- Align
admin.phpmissals handling withextending.jspatterns - Complete all CRUD operations for missals
- Update frontend forms as needed
- Design and implement decrees model classes
- Implement CRUD handlers
- Design and implement frontend UI
- Comprehensive testing
Note (2025-11): The original plan to implement
JsonSerializableon all model classes has been superseded by the raw payload approach. The files below were modified to support the raw payload strategy instead.
src/Params/RegionalDataParams.php # ✅ Added rawPayload property
src/Handlers/RegionalDataHandler.php # ✅ Uses rawPayload for json_encode()
# ✅ Added writeI18nFiles() helper
# ✅ Added updateI18nFiles() helper
# ✅ Added audit logging
phpunit_tests/Schemas/PayloadValidationTest.php # ✅ Round-trip serialization tests
phpunit_tests/fixtures/payloads/ # ✅ Test fixtures for all calendar types
The following model classes do not need JsonSerializable implementation because
the raw payload approach writes the original \stdClass directly:
src/Models/RegionalData/DiocesanData/* # No changes needed (raw payload used)
src/Models/RegionalData/NationalData/* # No changes needed (raw payload used)
src/Models/RegionalData/WiderRegionData/* # No changes needed (raw payload used)
src/Handlers/MissalsHandler.php # TODO: Implement PUT/PATCH/DELETE with validation
LiturgicalCalendarFrontend/assets/js/extending.js
├── saveDiocesanCalendar_btnClicked() # Review serialization
├── serializeNationalCalendarData() # Review serialization
└── serializeWiderRegionData() # Review serialization
LiturgicalCalendarFrontend/admin.php # Align missals handling with calendar patterns
LiturgicalCalendarFrontend/assets/js/admin.js # (if exists) Align with extending.js patterns
docs/api/payloads/diocesan-calendar-payload.md
docs/api/payloads/national-calendar-payload.md
docs/api/payloads/wider-region-payload.md
docs/api/payloads/missals-payload.md
phpunit_tests/Models/DiocesanDataSerializationTest.php
phpunit_tests/Models/NationalDataSerializationTest.php
phpunit_tests/Models/WiderRegionDataSerializationTest.php
phpunit_tests/Models/MissalsDataSerializationTest.php
- Schema Compliance: All data saved to disk validates against its respective JSON schema
- Round-Trip Integrity: Data loaded from disk and re-serialized produces identical output
- Test Coverage: All serialization paths have unit tests
- Documentation: All payload formats are documented with examples
- Error Handling: Clear error messages when validation fails (both frontend and backend)
- Consistency: All entity types (calendars, missals, decrees) follow the same patterns
This roadmap addresses technical details for work tracked in the following GitHub issues:
-
LiturgicalCalendarAPI#265: "Refactor resource creation / updating via PUT/PATCH/DELETE requests"
This is the parent issue tracking all PUT/PATCH/DELETE implementation across:
- Roman Missal sanctorale data (
/missals) - National Calendar data (
/data/nation) - marked complete but has serialization bug - Diocesan Calendar data (
/data/diocese) - marked complete but has serialization bug - Decrees data (
/decrees) - Unit tests (
/tests)
Critical finding: The "complete" status for National and Diocesan calendar data needs revision. While the handlers exist, the serialization bug documented in this roadmap means saved data does not conform to the JSON schemas.
- Roman Missal sanctorale data (
-
LiturgicalCalendarFrontend#142: "Align
extendingfrontends with new path backends"This issue tracks frontend alignment with API changes including:
- Router implementation changes
- Data shape changes (snake_case properties)
- Extending frontend updates
The serialization coordination work in this roadmap directly supports this issue by ensuring frontend serialization produces data that the API can correctly process and store.
- Authentication Roadmap - JWT authentication implementation
- OpenAPI Evaluation Roadmap - API schema gaps and missing CRUD operations
- API Client Libraries Roadmap - Client library coordination
This section documents the design of the DTO (Data Transfer Object) type system, explaining how raw source data is transformed into computed properties and ensuring type coherence across the codebase.
The codebase distinguishes between two categories of data:
- Raw Source Data: JSON data stored in
jsondata/sourcedata/files - Computed Data: Properties derived from raw data plus i18n translations
The @phpstan-type declarations define the shape of raw source data (what's in the JSON files),
NOT the computed output data. This is intentional and correct.
Example - LitCalItemCreateNewFixed:
/**
* @phpstan-type LitCalItemCreateNewFixedObject \stdClass&object{
* event_key:string, // ✓ In raw JSON
* day:int, // ✓ In raw JSON
* month:int, // ✓ In raw JSON
* color:string[], // ✓ In raw JSON
* grade:int, // ✓ In raw JSON
* common:string[] // ✓ In raw JSON
* // NOTE: `name` is NOT here - it's computed from i18n
* }
*/The name property is a computed property that does not exist in raw source data. It is:
- Declared in
LiturgicalEventDatabase class:public string $name; - Not initialized in subclass constructors
- Populated via
setName()from i18n translation data
Data Flow:
Raw JSON Source Data DTO Creation Translation Step
──────────────────── ──────────────── ────────────────
jsondata/sourcedata/ LitCalItem::fromObject() NationalData::applyTranslations()
calendars/nations/US.json DiocesanData::fromObject() DiocesanData::applyTranslations()
WiderRegionData::fromObject() WiderRegionData::applyTranslations()
{ │ │
"litcal": [ │ │
{ ▼ ▼
"liturgical_event": { ┌─────────────────┐ ┌─────────────────┐
"event_key": "...", │ LitCalItem │ │ setName() │
"day": 15, │ - event_key ✓ │ ──► │ - name ✓ (set) │
"month": 6, │ - day ✓ │ │ │
"color": ["white"], │ - month ✓ │ │ Translation │
"grade": 3, │ - color ✓ │ │ from i18n/*.json│
"common": [...] │ - grade ✓ │ └─────────────────┘
}, │ - common ✓ │
"metadata": {...} │ - name ✗ │ ← Uninitialized until
} │ (uninitialized)│ applyTranslations()
] └─────────────────┘
}
Each regional data class provides methods to populate the name property:
| Class | Method | i18n Source |
|---|---|---|
NationalData |
applyTranslations() |
i18n/{nation}/{locale}.json |
NationalData |
setNames() |
External translation array |
DiocesanData |
applyTranslations() |
i18n/{nation}/{diocese}/{locale}.json |
WiderRegionData |
applyTranslations() |
i18n/{region}/{locale}.json |
Example from NationalData:
public function applyTranslations(string $locale): void
{
foreach ($this->litcal as $litcalItem) {
$translation = $this->i18n->getTranslation($litcalItem->getEventKey(), $locale);
if (null === $translation) {
throw new \ValueError('translation not found for event key: ' . $litcalItem->getEventKey());
}
$litcalItem->setName($translation); // ← Populates the computed `name` property
}
}In CalendarHandler, @var annotations are used to narrow union types after conditional checks.
These annotations reference the DTO classes (not the @phpstan-type declarations) and assume
name has been populated via translations.
Example type narrowing:
// Line 3383 - Union type before narrowing
/** @var LitCalItemCreateNewFixed|LitCalItemCreateNewMobile|LitCalItemMakePatron $liturgicalEvent */
$liturgicalEvent = $litEvent->liturgical_event;
// Line 3390 - Narrowed after property check
if (property_exists($liturgicalEvent, 'strtotime') && $liturgicalEvent->strtotime !== '') {
/** @var LitCalItemCreateNewMobile $liturgicalEvent */
// ... now PHPStan knows $liturgicalEvent has strtotime property
}| Pattern | Purpose | Example |
|---|---|---|
@phpstan-type |
Define raw JSON structure | LitCalItemCreateNewFixedObject |
@phpstan-import-type |
Reuse types across files | @phpstan-import-type LitCalItemArray from LitCalItem |
@var ClassName $var |
Narrow union types | /** @var LitCalItemMakePatron $liturgicalEvent */ |
@param TypeName $param |
Document parameter types | @param LitCalItemObject $data |
The type system has been verified to be coherent:
- PHPStan level 10: ✅ Passes with no errors
- All tests: ✅ 108 tests passing
- Raw vs computed separation: ✅ Correctly implemented
- i18n flow: ✅
nameproperly populated before use in CalendarHandler
@phpstan-type= Raw Input: Types describe JSON schema, not runtime object statenameis Computed: Never in source JSON, always from i18n lookup- Translations Before Processing:
applyTranslations()must be called before accessingname - Type Narrowing:
@varannotations help PHPStan understand conditional type refinement
| Schema | Path |
|---|---|
| Diocesan Calendar | jsondata/schemas/DiocesanCalendar.json |
| National Calendar | jsondata/schemas/NationalCalendar.json |
| Wider Region Calendar | jsondata/schemas/WiderRegionCalendar.json |
| Proprium de Sanctis | jsondata/schemas/PropriumDeSanctis.json |
| Proprium de Tempore | jsondata/schemas/PropriumDeTempore.json |
| Decrees Source | jsondata/schemas/LitCalDecreesSource.json |
| Unit Tests | jsondata/schemas/LitCalTest.json |
| Endpoint | Methods | Handler | Auth Required |
|---|---|---|---|
/data/diocese/{id} |
PUT, PATCH, DELETE | RegionalDataHandler |
Yes |
/data/nation/{id} |
PUT, PATCH, DELETE | RegionalDataHandler |
Yes |
/data/widerregion/{id} |
PUT, PATCH, DELETE | RegionalDataHandler |
Yes |
/missals/{id} |
PUT, PATCH, DELETE | MissalsHandler |
Yes (TBD) |
/decrees/{id} |
PUT, PATCH, DELETE | DecreesHandler |
Yes (TBD) |
/tests/{id} |
PUT, PATCH, DELETE | TestsHandler |
WARN: PUT without auth |
Security Note: The OpenAPI Evaluation Roadmap identified that
PUT /testscurrently lacks authentication. This should be fixed before production use. SeeOPENAPI_EVALUATION_ROADMAP.mdfor details.
| Entity Type | Frontend File | Main Function/Handler |
|---|---|---|
| Diocesan Calendar | extending.php?choice=diocesan |
saveDiocesanCalendar_btnClicked() |
| National Calendar | extending.php?choice=national |
serializeNationalCalendarData() |
| Wider Region | extending.php?choice=widerRegion |
serializeWiderRegionData() |
| Missals | admin.php |
TBD (needs alignment) |
| Decrees | TBD | TBD |
| Tests | UnitTestInterface/admin.php |
TBD (needs modernization) |
Note: The UnitTestInterface is a separate repository. See the API Client Libraries Roadmap for details on UnitTestInterface modernization needs.