Files
michaelschiemer/src/Domain/Cms/README.md
Michael Schiemer 2d53270056 feat(cms,asset): add comprehensive test suite and finalize modules
- Add comprehensive test suite for CMS and Asset modules using Pest Framework
- Implement ContentTypeService::delete() protection against deletion of in-use content types
- Add CannotDeleteContentTypeInUseException for better error handling
- Fix DerivatPipelineRegistry::getAllPipelines() to handle object uniqueness correctly
- Fix VariantName::getScale() to correctly parse scales with file extensions
- Update CMS module documentation with new features, exceptions, and test coverage
- Add CmsTestHelpers and AssetTestHelpers for test data factories
- Fix BlockTypeRegistry to be immutable after construction
- Update ContentTypeService to check for associated content before deletion
- Improve BlockRendererRegistry initialization

Test coverage:
- Value Objects: All CMS and Asset value objects
- Services: ContentService, ContentTypeService, SlugGenerator, BlockValidator, ContentLocalizationService, AssetService, DeduplicationService, MetadataExtractor
- Repositories: All database repositories with mocked connections
- Rendering: Block renderers and ContentRenderer
- Controllers: API endpoints for both modules

254 tests passing, 38 remaining (mostly image processing pipeline tests)
2025-11-10 02:12:28 +01:00

363 lines
10 KiB
Markdown

# Headless CMS Modul
Ein Block-basiertes Headless CMS System für das Custom PHP Framework.
## Übersicht
Das CMS Modul ermöglicht die Verwaltung von Content durch ein flexibles Block-System, ähnlich WordPress Blocks, Storyblok oder Contentful. Content besteht aus einer geordneten Liste von Content-Blöcken, die als JSON in der Datenbank gespeichert werden.
## Architektur
### Block-basiertes System
Content besteht aus einer **geordneten Liste von Content-Blöcken**. Jeder Block hat:
- **Block Type**: HERO, TEXT, IMAGE, GALLERY, CTA, VIDEO, etc.
- **Block Data**: Typed Value Object mit block-spezifischen Daten
- **Block ID**: Eindeutige ID innerhalb des Contents
- **Block Settings**: Optionale Konfiguration (Styling, Visibility)
### Beispiel: Landing Page
```php
use App\Domain\Cms\Entities\Content;
use App\Domain\Cms\Enums\BlockType;
use App\Domain\Cms\ValueObjects\*;
$content = Content::create(
clock: $clock,
contentTypeId: ContentTypeId::fromString('landing_page'),
slug: ContentSlug::fromString('homepage'),
title: 'Welcome to Our Product',
blocks: ContentBlocks::fromArray([
ContentBlock::create(
blockType: BlockType::HERO,
blockId: 'hero-1',
data: BlockData::fromArray([
'title' => 'Revolutionary Product',
'subtitle' => 'Transform your workflow',
'backgroundImage' => 'img-123', // MediaId als String
'ctaText' => 'Get Started',
'ctaLink' => '/signup'
])
),
ContentBlock::create(
blockType: BlockType::TEXT,
blockId: 'text-1',
data: BlockData::fromArray([
'content' => '<p>Our product is...</p>',
'alignment' => 'center'
])
)
])
);
```
## Datenbank-Schema
### content_types
Speichert Content Type Definitionen:
- `id` (VARCHAR) - Content Type Identifier
- `name` (VARCHAR) - Display Name
- `slug` (VARCHAR) - URL-freundlicher Slug
- `description` (TEXT) - Beschreibung
- `is_system` (BOOLEAN) - System Content Type
### contents
Speichert Content Instanzen:
- `id` (VARCHAR) - ULID Identifier
- `content_type_id` (VARCHAR) - Foreign Key zu content_types
- `slug` (VARCHAR) - URL-freundlicher Slug (unique)
- `title` (VARCHAR) - Content Titel
- `blocks` (JSON) - Array von Block-Objekten
- `meta_data` (JSON) - SEO, Open Graph Metadaten
- `status` (ENUM) - draft, published, archived
- `author_id` (VARCHAR) - Author User ID
- `published_at` (TIMESTAMP) - Veröffentlichungsdatum
- `created_at`, `updated_at` (TIMESTAMP)
## Verwendung
### ContentService
```php
use App\Domain\Cms\Services\ContentService;
use App\Domain\Cms\ValueObjects\*;
// Content erstellen
$content = $contentService->create(
contentTypeId: ContentTypeId::fromString('landing_page'),
title: 'Welcome Page',
blocks: ContentBlocks::fromArray([...]),
slug: ContentSlug::fromString('homepage')
);
// Content finden
$content = $contentService->findBySlug(ContentSlug::fromString('homepage'));
// Content veröffentlichen
$published = $contentService->publish($content->id);
// Content aktualisieren
$updated = $contentService->updateBlocks(
$content->id,
ContentBlocks::fromArray([...])
);
```
### ContentTypeService
```php
use App\Domain\Cms\Services\ContentTypeService;
use App\Domain\Cms\ValueObjects\ContentTypeId;
// Content Type erstellen
$contentType = $contentTypeService->create(
name: 'Landing Page',
slug: 'landing_page',
description: 'Landing pages for marketing campaigns',
isSystem: false
);
// Content Type aktualisieren
$updated = $contentTypeService->update(
id: ContentTypeId::fromString('landing_page'),
name: 'Updated Name',
description: 'New description'
);
// Content Type löschen
// Wirft CannotDeleteContentTypeInUseException wenn noch Content existiert
// Wirft CannotDeleteSystemContentTypeException für System-Types
try {
$contentTypeService->delete(ContentTypeId::fromString('landing_page'));
} catch (CannotDeleteContentTypeInUseException $e) {
// Content Type wird noch von Content verwendet
}
```
## REST API
### Endpoints
- `GET /api/v1/cms/contents` - Liste aller Contents
- `GET /api/v1/cms/contents/{id}` - Content Details
- `POST /api/v1/cms/contents` - Content erstellen
- `PUT /api/v1/cms/contents/{id}` - Content aktualisieren
- `DELETE /api/v1/cms/contents/{id}` - Content löschen
- `POST /api/v1/cms/contents/{id}/publish` - Content veröffentlichen
- `POST /api/v1/cms/contents/{id}/unpublish` - Content zurückziehen
### Beispiel Request
```json
POST /api/v1/cms/contents
{
"content_type_id": "landing_page",
"slug": "homepage",
"title": "Welcome Page",
"blocks": [
{
"id": "hero-1",
"type": "hero",
"data": {
"title": "Revolutionary Product",
"subtitle": "Transform your workflow",
"backgroundImage": "img-123",
"ctaText": "Get Started",
"ctaLink": "/signup"
},
"settings": {
"fullWidth": true
}
}
],
"status": "draft"
}
```
## Block Types
- `HERO` - Hero Section mit Titel, Bild, CTA
- `TEXT` - Rich Text Block
- `IMAGE` - Einzelnes Bild mit Caption
- `GALLERY` - Bildergalerie
- `CTA` - Call-to-Action Block
- `VIDEO` - Video Embed
- `FORM` - Formular Integration
- `COLUMNS` - Multi-Column Layout
- `QUOTE` - Zitat Block
- `SEPARATOR` - Trenner/Divider
## Integration mit anderen Domains
### Media Domain
Block Data kann MediaId als String enthalten:
```php
BlockData::fromArray([
'imageId' => 'img-123', // MediaId als String
'caption' => 'Product Screenshot'
])
```
### Meta Domain
SEO-Metadaten können in `meta_data` gespeichert werden:
```php
$content = $content->withMetaData(BlockData::fromArray([
'title' => 'SEO Title',
'description' => 'SEO Description',
'og_title' => 'OG Title',
'og_image' => 'og-image.jpg'
]));
```
## Migration
```bash
php console.php db:migrate
```
Die Migrations erstellen die Tabellen `content_types` und `contents` mit JSON-Spalten für Blocks.
## Exceptions
Das CMS Modul definiert folgende Domain-Exceptions:
### Content Exceptions
- **`ContentNotFoundException`** - Wird geworfen wenn Content nicht gefunden wird
```php
throw ContentNotFoundException::forId($contentId);
throw ContentNotFoundException::forSlug($slug);
```
- **`DuplicateSlugException`** - Wird geworfen wenn ein Slug bereits existiert
```php
throw DuplicateSlugException::forSlug($slug);
```
- **`InvalidContentStatusException`** - Wird geworfen bei ungültigen Status-Übergängen
```php
throw InvalidContentStatusException::invalidTransition($currentStatus, $targetStatus);
```
- **`InvalidBlockException`** - Wird geworfen bei ungültigen Block-Daten
```php
throw InvalidBlockException::missingRequiredField($blockId, $field);
throw InvalidBlockException::invalidFieldType($blockId, $field, $expectedType);
```
### ContentType Exceptions
- **`ContentTypeNotFoundException`** - Wird geworfen wenn ContentType nicht gefunden wird
```php
throw ContentTypeNotFoundException::forId($id);
```
- **`DuplicateContentTypeSlugException`** - Wird geworfen wenn ein ContentType-Slug bereits existiert
```php
throw DuplicateContentTypeSlugException::forSlug($slug);
```
- **`CannotDeleteSystemContentTypeException`** - Wird geworfen beim Versuch, einen System-ContentType zu löschen
```php
throw CannotDeleteSystemContentTypeException::forId($id);
```
- **`CannotDeleteContentTypeInUseException`** - Wird geworfen beim Versuch, einen ContentType zu löschen, der noch von Content verwendet wird
```php
throw CannotDeleteContentTypeInUseException::forId($id, $contentCount);
```
## ContentTypeService::delete() - Schutz vor versehentlichem Löschen
Die `ContentTypeService::delete()` Methode implementiert eine Schutzfunktion, die verhindert, dass ContentTypes gelöscht werden, die noch von Content-Instanzen verwendet werden:
```php
public function delete(ContentTypeId $id): void
{
$contentType = $this->contentTypeRepository->findById($id);
if ($contentType === null) {
throw ContentTypeNotFoundException::forId($id);
}
// System ContentTypes können nicht gelöscht werden
if ($contentType->isSystem) {
throw CannotDeleteSystemContentTypeException::forId($id);
}
// Prüfe ob ContentType noch von Content verwendet wird
$contents = $this->contentRepository->findByType($id);
if (count($contents) > 0) {
throw CannotDeleteContentTypeInUseException::forId($id, count($contents));
}
$this->contentTypeRepository->delete($id);
}
```
Dies verhindert Datenverlust und stellt sicher, dass keine verwaisten Content-Instanzen entstehen.
## Tests
Das CMS Modul verfügt über eine umfassende Test-Suite mit Pest Framework:
### Test-Struktur
```
tests/
├── Unit/
│ └── Domain/
│ └── Cms/
│ ├── ValueObjects/ # Value Object Tests
│ ├── Services/ # Service Tests
│ ├── Repositories/ # Repository Tests
│ └── Rendering/ # Rendering Tests
├── Feature/
│ └── Application/
│ └── Cms/ # Controller Tests
└── Support/
└── CmsTestHelpers.php # Test Helper Functions
```
### Test-Abdeckung
- **Value Objects**: Alle Value Objects (ContentId, ContentSlug, BlockType, etc.)
- **Services**: ContentService, ContentTypeService, SlugGenerator, BlockValidator, ContentLocalizationService
- **Repositories**: DatabaseContentRepository, DatabaseContentTypeRepository, DatabaseContentTranslationRepository
- **Rendering**: BlockRendererRegistry, HeroBlockRenderer, TextBlockRenderer, ImageBlockRenderer, DefaultBlockRenderer, ContentRenderer
- **Controllers**: ContentsController, ContentTypesController
### Tests ausführen
```bash
# Alle CMS Tests
./vendor/bin/pest tests/Unit/Domain/Cms tests/Feature/Application/Cms
# Spezifische Test-Datei
./vendor/bin/pest tests/Unit/Domain/Cms/Services/ContentServiceTest.php
```
### Test Helpers
Die `CmsTestHelpers` Klasse bietet Factory-Methoden für Test-Daten:
```php
use Tests\Support\CmsTestHelpers;
$content = CmsTestHelpers::createContent($clock, [
'title' => 'Test Content',
'blocks' => $blocks,
'slug' => ContentSlug::fromString('test-slug')
]);
$contentType = CmsTestHelpers::createContentType([
'name' => 'Test Type',
'slug' => 'test_type'
]);
```