# EspoCRM Best Practices & Entwicklungsrichtlinien **Version:** 2.1 **Datum:** 9. März 2026 **Zielgruppe:** AI Code Agents & Entwickler --- ## 📋 Inhaltsverzeichnis 1. [Projekt-Übersicht](#projekt-übersicht) 2. [Architektur-Prinzipien](#architektur-prinzipien) 3. [Entity-Entwicklung](#entity-entwicklung) 4. [Relationship-Patterns](#relationship-patterns) 5. [API-Entwicklung](#api-entwicklung) 6. [Hook-Entwicklung](#hook-entwicklung) 7. [Workflow-Management](#workflow-management) 8. [Testing & Validierung](#testing--validierung) 9. [Fehlerbehandlung](#fehlerbehandlung) 10. [Deployment-Prozess](#deployment-prozess) 11. [Troubleshooting](#troubleshooting) --- ## Projekt-Übersicht ### System-Architektur ``` EspoCRM 9.3.2 ├── PHP 8.2.30 ├── MariaDB 12.2.2 ├── Docker Container: espocrm, espocrm-db └── Workspace: /var/lib/docker/volumes/vmh-espocrm_espocrm/_data ``` ### Verzeichnisstruktur ``` custom/ ├── Espo/Custom/ # Backend-Code │ ├── Controllers/ # REST API Endpoints │ ├── Services/ # Business Logic │ ├── Repositories/ # Data Access Layer │ ├── Hooks/ # Entity Lifecycle Hooks │ └── Resources/ │ ├── metadata/ # Entity & Field Definitionen │ │ ├── entityDefs/ # Entity-Konfiguration │ │ ├── clientDefs/ # Frontend-Konfiguration │ │ ├── scopes/ # Entity-Scopes │ │ └── formula/ # Formula Scripts │ ├── layouts/ # UI-Layouts │ └── i18n/ # Übersetzungen (de_DE, en_US) ├── scripts/ # Entwicklungs-Tools │ ├── validate_and_rebuild.py # Haupt-Validierungs-Tool │ ├── e2e_tests.py # End-to-End Tests │ ├── ki_project_overview.py # Projekt-Analyse für AI │ └── junctiontabletests/ # Junction Table Tests ├── docs/ # Dokumentation (NEU) │ ├── ESPOCRM_BEST_PRACTICES.md # Dieses Dokument │ ├── tools/ # Tool-Dokumentation │ └── workflows/ # Workflow-Dokumentation └── workflows/ # Workflow JSON-Definitions client/custom/ # Frontend-Code ├── src/ # JavaScript Modules ├── css/ # Custom Styles └── res/ # Resources ``` ### Custom Entities Übersicht **19 Custom Entities implementiert (Stand: März 2026):** | Entity | Beschreibung | Hooks | Typ | |--------|--------------|-------|-----| | `CAdressen` | Adressen-Verwaltung | - | Base | | `CAICollections` | AI-Dokumenten-Sammlungen | - | Base | | `CAICollectionCDokumente` | Junction: Collections ↔ Dokumente | - | Junction | | `CBankverbindungen` | Bankdaten (IBAN/BIC) | ✅ Validierung | Base | | `CBeteiligte` | Beteiligte Personen | - | Base | | `CCallQueues` | Call-Warteschlangen | - | Base | | `CDokumente` | Dokumenten-Management | ✅ Hash-Berechnung | Base | | `CKuendigung` | Kündigungen | - | Base | | `CMietinkasso` | Mietinkasso-Fälle | - | Base | | `CMietobjekt` | Mietobjekte | - | Base | | `CPuls` | Posteingangs-System | ✅ Statistik | Base | | `CPulsTeamZuordnung` | Puls-Team-Zuordnungen | - | Base | | `CVMHBeteiligte` | VMH-spezifische Beteiligte | - | Base | | `CVmhErstgespraech` | Erstgespräche | - | Base | | `CVmhMietverhltnis` | Mietverhältnisse | - | Base | | `CVmhRumungsklage` | Räumungsklagen | - | Base | | `CVmhVermieter` | Vermieter | - | Base | **Standard-Entities erweitert:** - `Contact` - Erweiterterte Kontakt-Felder - `Call` - Custom Call-Felder - `User` - User-Erweiterungen - `Meeting` - Meeting-Erweiterungen - `Email` - E-Mail-Anpassungen - `Task` - Task-Anpassungen - `PhoneNumber` - Telefonnummern-Erweiterungen - `Team` - Team-Anpassungen - `BpmnUserTask` - Workflow-Task-Erweiterungen **Implementierte Hooks:** 1. **CBankverbindungen/BankdatenValidation** - IBAN/BIC-Validierung mit Modulo-97 2. **CDokumente/CDokumente** - MD5/SHA256-Hash-Berechnung für Uploads 3. **CPuls/UpdateTeamStats** - Automatische Statistik-Berechnung --- ## Architektur-Prinzipien ### 1. Separation of Concerns **EspoCRM = Data Layer** - Speichert Entities - Stellt UI bereit - Validiert Daten - Bietet REST API **Middleware = Business Logic** - KI-Analyse - Team-Zuweisung - Komplexe Workflows - Externe Integrationen ### 2. Drei-Schichten-Architektur ``` ┌─────────────────────────────────────────┐ │ FRONTEND (clientDefs, Layouts) │ │ • User Interface │ │ • JavaScript Actions │ └────────────────┬────────────────────────┘ │ AJAX/REST ┌────────────────▼────────────────────────┐ │ CONTROLLER (Controllers/) │ │ • Request Validation │ │ • ACL Checks │ └────────────────┬────────────────────────┘ │ Service Call ┌────────────────▼────────────────────────┐ │ SERVICE (Services/) │ │ • Business Logic │ │ • Entity Manager │ └────────────────┬────────────────────────┘ │ Repository ┌────────────────▼────────────────────────┐ │ REPOSITORY (Repositories/) │ │ • Data Access │ │ • Relationships │ └─────────────────────────────────────────┘ ``` ### 3. Clean Code Principles **DO:** - ✅ Nutze sprechende Variablennamen - ✅ Schreibe kleine, fokussierte Funktionen - ✅ Kommentiere komplexe Business-Logik - ✅ Verwende Type Hints (PHP 8.2+) - ✅ Folge PSR-12 Coding Standard **DON'T:** - ❌ Keine komplexe Business-Logic in Hooks (nutze Services) - ❌ Keine direkten SQL-Queries (nutze EntityManager) - ❌ Keine hard-coded Werte (nutze Config) - ❌ Keine redundanten Includes - ❌ Keine ungenutzten Imports --- ## Entity-Entwicklung ### Entity-Naming Convention **Pattern:** `C{EntityName}` für Custom Entities **Beispiele:** - `CMietobjekt` - Mietobjekte - `CVmhMietverhltnis` - Mietverhältnisse (VMH = Vermieter Helden) - `CKuendigung` - Kündigungen - `CAICollections` - AI Collections ### Entity Definition Template **Datei:** `custom/Espo/Custom/Resources/metadata/entityDefs/{EntityName}.json` ```json { "fields": { "name": { "type": "varchar", "required": true, "maxLength": 255, "trim": true, "isCustom": true, "tooltip": true }, "status": { "type": "enum", "options": ["Neu", "In Bearbeitung", "Abgeschlossen"], "default": "Neu", "required": true, "isCustom": true, "style": { "Neu": "primary", "In Bearbeitung": "warning", "Abgeschlossen": "success" } }, "description": { "type": "text", "rows": 10, "isCustom": true, "tooltip": true }, "amount": { "type": "currency", "isCustom": true, "audited": true }, "dueDate": { "type": "date", "isCustom": true, "audited": true } }, "links": { "parent": { "type": "belongsToParent", "entityList": ["CVmhRumungsklage", "CMietinkasso"] }, "createdBy": { "type": "belongsTo", "entity": "User" }, "modifiedBy": { "type": "belongsTo", "entity": "User" } } } ``` ### Scope Definition **Datei:** `custom/Espo/Custom/Resources/metadata/scopes/{EntityName}.json` ```json { "entity": true, "type": "Base", "module": "Custom", "object": true, "isCustom": true, "tab": true, "acl": true, "stream": true, "disabled": false, "customizable": true, "importable": true, "notifications": true, "calendar": false } ``` **Wichtige Flags:** - `tab: true` - Zeigt Entity in Navigation - `acl: true` - ACL-System aktiv - `stream: true` - Stream/Activity Feed - `calendar: true` - Für Entities mit Datum-Feldern ### i18n (Internationalisierung) **KRITISCH:** Immer BEIDE Sprachen pflegen! **Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/{EntityName}.json` ```json { "labels": { "Create {EntityName}": "{EntityName} erstellen", "{EntityName}": "{EntityName}", "name": "Name", "status": "Status", "description": "Beschreibung" }, "fields": { "name": "Name", "status": "Status", "description": "Beschreibung", "amount": "Betrag", "dueDate": "Fälligkeitsdatum" }, "links": { "parent": "Übergeordnet", "relatedEntity": "Verknüpfte Entity" }, "options": { "status": { "Neu": "Neu", "In Bearbeitung": "In Bearbeitung", "Abgeschlossen": "Abgeschlossen" } }, "tooltips": { "name": "Eindeutiger Name des Datensatzes", "description": "Detaillierte Beschreibung" } } ``` **Datei:** `custom/Espo/Custom/Resources/i18n/en_US/{EntityName}.json` ```json { "labels": { "Create {EntityName}": "Create {EntityName}", "{EntityName}": "{EntityName}" }, "fields": { "name": "Name", "status": "Status", "description": "Description", "amount": "Amount", "dueDate": "Due Date" }, "links": { "parent": "Parent", "relatedEntity": "Related Entity" }, "options": { "status": { "Neu": "New", "In Bearbeitung": "In Progress", "Abgeschlossen": "Completed" } } } ``` --- ## Relationship-Patterns ### 1. One-to-Many (hasMany / belongsTo) **Beispiel:** Ein Mietobjekt hat viele Mietverhältnisse **Parent Entity (CMietobjekt):** ```json { "links": { "mietverhltnisse": { "type": "hasMany", "entity": "CVmhMietverhltnis", "foreign": "mietobjekt" } } } ``` **Child Entity (CVmhMietverhltnis):** ```json { "fields": { "mietobjektId": { "type": "varchar", "len": 17 }, "mietobjektName": { "type": "varchar" } }, "links": { "mietobjekt": { "type": "belongsTo", "entity": "CMietobjekt", "foreign": "mietverhltnisse" } } } ``` ### 2. Many-to-Many (hasMany mit relationName) **Beispiel:** Dokumente ↔ AI Collections **Entity 1 (CDokumente):** ```json { "links": { "cAICollections": { "type": "hasMany", "entity": "CAICollections", "foreign": "cDokumente", "relationName": "cAICollectionCDokumente" } } } ``` **Entity 2 (CAICollections):** ```json { "links": { "cDokumente": { "type": "hasMany", "entity": "CDokumente", "foreign": "cAICollections", "relationName": "cAICollectionCDokumente" } } } ``` **Wichtig:** `relationName` muss identisch sein! ### 3. Many-to-Many mit additionalColumns (Junction Entity) **Seit EspoCRM 6.0:** Junction-Tabellen werden automatisch als Entities verfügbar! **Entity Definition:** ```json { "links": { "cDokumente": { "type": "hasMany", "entity": "CDokumente", "foreign": "cAICollections", "relationName": "cAICollectionCDokumente", "additionalColumns": { "syncId": { "type": "varchar", "len": 255 } } } } } ``` **Junction Entity (CAICollectionCDokumente):** **entityDefs/CAICollectionCDokumente.json:** ```json { "fields": { "id": { "type": "id", "dbType": "bigint", "autoincrement": true }, "cAICollections": { "type": "link" }, "cAICollectionsId": { "type": "varchar", "len": 17, "index": true }, "cDokumente": { "type": "link" }, "cDokumenteId": { "type": "varchar", "len": 17, "index": true }, "syncId": { "type": "varchar", "len": 255, "isCustom": true }, "deleted": { "type": "bool", "default": false } }, "links": { "cAICollections": { "type": "belongsTo", "entity": "CAICollections" }, "cDokumente": { "type": "belongsTo", "entity": "CDokumente" } } } ``` **scopes/CAICollectionCDokumente.json:** ```json { "entity": true, "type": "Base", "module": "Custom", "object": true, "isCustom": true, "tab": false, "acl": true, "disabled": false } ``` **Controller & Service:** ```php getParsedBody(); $id = $data->id ?? null; if (!$id) { throw new BadRequest('ID is required'); } $result = $this->getRecordService()->doSomething($id, $data); return [ 'success' => true, 'data' => $result ]; } /** * Custom GET Action: GET /api/v1/CMyEntity/{id}/customData */ public function getActionCustomData(Request $request): array { $id = $request->getRouteParam('id'); $data = $this->getRecordService()->getCustomData($id); return [ 'data' => $data ]; } } ``` **2. Service Logic:** **Datei:** `custom/Espo/Custom/Services/{EntityName}.php` ```php getAcl()->checkEntityEdit($this->entityType)) { throw new Forbidden(); } // Load Entity $entity = $this->getEntityManager()->getEntity($this->entityType, $id); if (!$entity) { throw new NotFound(); } // Business Logic $entity->set('status', 'In Bearbeitung'); $this->getEntityManager()->saveEntity($entity); // Return Result return [ 'id' => $entity->getId(), 'status' => $entity->get('status') ]; } public function getCustomData(string $id): array { $entity = $this->getEntityManager()->getEntity($this->entityType, $id); if (!$entity) { throw new NotFound(); } // Complex data aggregation $relatedData = $this->getRelatedData($entity); return [ 'entity' => $entity->getValueMap(), 'related' => $relatedData ]; } } ``` ### API Authentication **API Key Header:** ```bash curl -X GET "https://crm.example.com/api/v1/CMyEntity" \ -H "X-Api-Key: your-api-key-here" ``` **Test API Keys:** - `marvin`: `e53def10eea27b92a6cd00f40a3e09a4` - `dev-test`: `2b0747ca34d15032aa233ae043cc61bc` --- ## Hook-Entwicklung ### Überblick **Hooks** sind Event-Handler, die bei Entity-Lifecycle-Events ausgeführt werden. Sie ermöglichen automatische Validierung, Berechnung und Synchronisation ohne Frontend-Änderungen. **Verzeichnis:** `custom/Espo/Custom/Hooks/{EntityName}/` **Hook-Typen (EspoCRM 9.x Interface-basiert):** | Interface | Trigger | Verwendung | |-----------|---------|------------| | `BeforeSave` | Vor dem Speichern | Validierung, Feld-Berechnung, Normalisierung | | `AfterSave` | Nach dem Speichern | Notifications, externe API-Calls, Statistik-Updates | | `BeforeRemove` | Vor dem Löschen | Validierung, Cascade-Prüfungen | | `AfterRemove` | Nach dem Löschen | Cleanup, externe System-Updates | | `AfterRelate` | Nach Relationship-Link | Statistik-Updates, Synchronisation | | `AfterUnrelate` | Nach Relationship-Unlink | Statistik-Updates, Cleanup | ### Hook-Pattern (EspoCRM 9.x) **Moderne Interface-basierte Hooks (PHP 8.2+):** ```php get('iban'); if ($iban !== null && $iban !== '') { // Normalisieren: Leerzeichen entfernen, Großbuchstaben $ibanClean = strtoupper(str_replace(' ', '', $iban)); $entity->set('iban', $ibanClean); // Mathematische IBAN-Prüfung mit Modulo-97 if (!$this->validateIban($ibanClean)) { $message = $this->language->translateLabel( 'invalidIbanChecksum', 'messages', 'CBankverbindungen' ); throw new BadRequest($message); } } // BIC-Normalisierung und Validierung $bic = $entity->get('bic'); if ($bic !== null && $bic !== '') { $bicClean = strtoupper(str_replace(' ', '', $bic)); $entity->set('bic', $bicClean); // BIC-Format: 8 oder 11 Zeichen $bicLength = strlen($bicClean); if ($bicLength !== 8 && $bicLength !== 11) { $message = $this->language->translateLabel( 'invalidBicLength', 'messages', 'CBankverbindungen' ); throw new BadRequest($message); } // BIC-Regex: AAAA BB CC DDD if (!preg_match('/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/', $bicClean)) { $message = $this->language->translateLabel( 'invalidBicFormat', 'messages', 'CBankverbindungen' ); throw new BadRequest($message); } } } private function validateIban(string $iban): bool { if (strlen($iban) < 15) { return false; } // IBAN umstellen: erste 4 Zeichen ans Ende $rearranged = substr($iban, 4) . substr($iban, 0, 4); // Buchstaben in Zahlen umwandeln (A=10, B=11, ..., Z=35) $numeric = ''; for ($i = 0; $i < strlen($rearranged); $i++) { $char = $rearranged[$i]; if (ctype_alpha($char)) { $numeric .= (string)(ord($char) - ord('A') + 10); } else { $numeric .= $char; } } // Modulo-97-Prüfung: Ergebnis muss 1 sein return $this->bcmod($numeric, '97') === '1'; } private function bcmod(string $number, string $modulus): string { // Modulo für sehr große Zahlen in Schritten $take = 9; $mod = ''; do { $a = (int)($mod . substr($number, 0, $take)); $number = substr($number, $take); $mod = (string)($a % (int)$modulus); } while (strlen($number) > 0); return $mod; } } ``` **i18n-Messages (erforderlich):** ```json // custom/Espo/Custom/Resources/i18n/de_DE/CBankverbindungen.json { "messages": { "invalidIbanChecksum": "IBAN-Prüfsumme ungültig (Modulo-97-Fehler)", "invalidBicLength": "BIC muss 8 oder 11 Zeichen lang sein", "invalidBicFormat": "BIC-Format ungültig (erwarte: AAAAAA BB CC DDD)" } } ``` **Best Practice:** - ✅ Normalisierung VOR Validierung - ✅ i18n für Fehlermeldungen - ✅ BadRequest mit Fehlertext werfen - ✅ Mathematisch korrekte Algorithmen (Modulo-97) --- #### Beispiel 2: Hash-Berechnung (CDokumente) **Datei:** `custom/Espo/Custom/Hooks/CDokumente/CDokumente.php` **Use Case:** Automatische MD5/SHA256-Hash-Berechnung für Datei-Uploads ```php get('dokument'); if (!$dokument) { return; } // Attachment laden if (is_object($dokument)) { $attachment = $dokument; } else { $attachment = $this->getEntityManager() ->getEntity('Attachment', $dokument); } if (!$attachment) { return; } // Dateipfad prüfen $filePath = 'data/upload/' . $attachment->get('id'); if (!file_exists($filePath)) { return; } // Hash-Berechnung $newMd5 = hash_file('md5', $filePath); $newSha256 = hash_file('sha256', $filePath); $entity->set('md5sum', $newMd5); $entity->set('sha256', $newSha256); // Status-Erkennung if ($entity->isNew()) { $entity->set('fileStatus', 'new'); } else { $oldMd5 = $entity->getFetched('md5sum'); $oldSha256 = $entity->getFetched('sha256'); if ($oldMd5 !== $newMd5 || $oldSha256 !== $newSha256) { $entity->set('fileStatus', 'changed'); } else { $entity->set('fileStatus', 'synced'); } } } } ``` **Hinweis:** EspoCRM markiert Datei-Uploads nicht als Feldänderung (`isAttributeChanged('dokument')` = false). Daher läuft der Hook bei jedem Save mit Dokument-Feld. **Best Practice:** - ✅ File-Existence-Check vor Hash-Berechnung - ✅ Unterstütze sowohl Object als auch ID - ✅ Nutze `isNew()` für Status-Logik - ✅ Dokumentiere API-Limitationen als Kommentar --- #### Beispiel 3: Statistik-Berechnung (CPuls) **Datei:** `custom/Espo/Custom/Hooks/CPuls/UpdateTeamStats.php` **Use Case:** Automatische Berechnung von Zählern für verwandte Entities ```php isNew() || $entity->isAttributeChanged('id')) { $dokumenteCount = $this->entityManager ->getRDBRepository('CDokumente') ->where(['pulsId' => $entity->getId()]) ->count(); $entity->set('anzahlDokumente', $dokumenteCount); } // Team-Zuordnungen analysieren $zuordnungen = $this->entityManager ->getRDBRepository('CPulsTeamZuordnung') ->where(['pulsId' => $entity->getId()]) ->find(); $aktiv = 0; $abgeschlossen = 0; foreach ($zuordnungen as $z) { if ($z->get('aktiv')) { $aktiv++; if ($z->get('abgeschlossen')) { $abgeschlossen++; } } } $entity->set('anzahlTeamsAktiv', $aktiv); $entity->set('anzahlTeamsAbgeschlossen', $abgeschlossen); } } ``` **Best Practice:** - ✅ Moderne Interface-basierte Hook-Klasse - ✅ Constructor Injection für EntityManager - ✅ Private Typed Properties (PHP 8.2+) - ✅ Bedingte Berechnung (`isNew()`, `isAttributeChanged()`) - ✅ Repository queries statt direktes SQL --- ### Best Practices für Hooks #### ✅ DO 1. **Nutze Interface-basierte Hooks (EspoCRM 9.x)** ```php class MyHook implements BeforeSave { } ``` 2. **Constructor Injection für Dependencies** ```php public function __construct( private EntityManager $entityManager ) {} ``` 3. **Validierung in beforeSave, Notifications in afterSave** - `beforeSave`: Synchron, blockiert Transaction - `afterSave`: Transaction bereits committed 4. **Exception werfen bei Validierungsfehlern** ```php throw new BadRequest('Error message'); throw new Forbidden('Access denied'); ``` 5. **i18n für Fehlermeldungen** ```php $this->language->translateLabel('key', 'messages', 'EntityName'); ``` 6. **Performance-Optimierung mit Conditions** ```php if ($entity->isNew() || $entity->isAttributeChanged('field')) { // Nur bei Änderung ausführen } ``` #### ❌ DON'T 1. **Keine komplexe Business-Logic in Hooks** → Nutze Services stattdessen 2. **Keine direkten SQL-Queries** → Nutze EntityManager/Repositories 3. **Keine externe API-Calls in beforeSave** → Kann Transaction blockieren, nutze afterSave oder Queue 4. **Keine Circular Dependencies** → Hook A speichert Entity B, Hook B speichert Entity A = Endlosschleife 5. **Keine Hooks für UI-Logic** → Nutze Frontend-Controller ### Hook-Reihenfolge **Entity Save Lifecycle:** ``` 1. beforeSave Hook 2. Entity Validation 3. Database Transaction START 4. INSERT/UPDATE Query 5. Transaction COMMIT 6. afterSave Hook 7. Stream/Notification ``` **Wichtig:** - `beforeSave`: Änderungen im Entity werden gespeichert - `afterSave`: Entity ist bereits committed, Änderungen erfordern separates `saveEntity()` ### Debugging Hooks **Log-Output:** ```php $GLOBALS['log']->debug('MyHook: ' . json_encode([ 'entity' => $entity->getEntityType(), 'id' => $entity->getId(), 'isNew' => $entity->isNew(), 'changed' => $entity->get('field') ])); ``` **Log-File:** ```bash tail -f data/logs/espo-$(date +%Y-%m-%d).log | grep MyHook ``` **Fehlersuche:** 1. **Hook wird nicht ausgeführt** - Clear Cache: `php clear_cache.php` - Rebuild: `php rebuild.php` - Prüfe Namespace/Klassennamen 2. **Exception in Hook** - Prüfe Log: `data/logs/espo-{date}.log` - Prüfe Type Hints (PHP 8.2 strict types) - Validiere Constructor Injection 3. **Hook läuft mehrfach** - Prüfe auf Circular Dependencies - Nutze Conditions (`isAttributeChanged()`) ### Troubleshooting **Problem: Hook läuft nicht** ```bash # Cache clearen php clear_cache.php # Rebuild php rebuild.php # Hook-Datei prüfen php -l custom/Espo/Custom/Hooks/{Entity}/{HookName}.php ``` **Problem: Circular Dependency** ```php // ❌ FALSCH: Endlosschleife class HookA implements BeforeSave { public function beforeSave(Entity $entity, SaveOptions $options): void { $entityB = $this->entityManager->getEntity('EntityB', 'id'); $entityB->set('field', 'value'); $this->entityManager->saveEntity($entityB); // triggert HookB } } class HookB implements BeforeSave { public function beforeSave(Entity $entity, SaveOptions $options): void { $entityA = $this->entityManager->getEntity('EntityA', 'id'); $entityA->set('field', 'value'); $this->entityManager->saveEntity($entityA); // triggert HookA → LOOP! } } // ✅ RICHTIG: Mit Flag class HookA implements BeforeSave { public function beforeSave(Entity $entity, SaveOptions $options): void { if ($options->get('skipHooks')) { return; } $entityB = $this->entityManager->getEntity('EntityB', 'id'); $entityB->set('field', 'value'); $this->entityManager->saveEntity($entityB, [ 'skipHooks' => true ]); } } ``` --- ## Workflow-Management ### Workflow-Dateien **Verzeichnis:** `custom/workflows/` **Format:** JSON (Simple Workflow oder BPM Flowchart) ### Simple Workflow Beispiel ```json { "type": "simple", "name": "auto-assign-new-entity", "entity_type": "CMyEntity", "trigger_type": "afterRecordCreated", "is_active": true, "description": "Auto-assign new records to team", "conditions_all": [ { "type": "isEmpty", "attribute": "assignedUserId" } ], "actions": [ { "type": "applyAssignmentRule", "targetTeamId": "team-id-here" }, { "type": "sendEmail", "to": "assignedUser", "emailTemplateId": "template-id" } ] } ``` ### Workflow Import/Export ```bash # Alle Workflows exportieren php custom/scripts/workflow_manager.php export # Workflow importieren php custom/scripts/workflow_manager.php import custom/workflows/my-workflow.json # Workflows auflisten php custom/scripts/workflow_manager.php list ``` --- ## Testing & Validierung ### Validierungs-Tool **Haupt-Tool:** `custom/scripts/validate_and_rebuild.py` ```bash # Vollständige Validierung + Rebuild python3 custom/scripts/validate_and_rebuild.py # Nur Validierung (kein Rebuild) python3 custom/scripts/validate_and_rebuild.py --dry-run # Mit E2E Tests überspringen python3 custom/scripts/validate_and_rebuild.py --skip-e2e ``` **Das Tool prüft:** 1. ✅ JSON-Syntax aller Custom-Dateien 2. ✅ Relationship-Konsistenz (bidirektionale Links) 3. ✅ Formula-Script Platzierung 4. ✅ i18n-Vollständigkeit (de_DE + en_US) 5. ✅ Layout-Struktur (bottomPanelsDetail, detail.json) 6. ✅ Dateirechte (www-data:www-data) 7. ✅ CSS-Validierung (csslint) 8. ✅ JavaScript-Validierung (jshint) 9. ✅ PHP-Syntax (php -l) 10. ✅ EspoCRM Rebuild 11. ✅ E2E-Tests (CRUD-Operationen) **Bei Fehlern:** Automatische Fehlerlog-Analyse der letzten 50 Log-Zeilen! ### End-to-End Tests **Tool:** `custom/scripts/e2e_tests.py` ```bash # E2E Tests ausführen python3 custom/scripts/e2e_tests.py ``` **Tests:** - CRUD für alle Custom Entities - Relationship-Verknüpfungen - ACL-Prüfungen ### Manuelle Tests **Checkliste:** - [ ] Entity in UI sichtbar? - [ ] Felder editierbar? - [ ] Relationships funktionieren? - [ ] Formulas triggern korrekt? - [ ] Workflows aktiv? - [ ] API-Endpoints erreichbar? - [ ] ACL-Regeln greifen? --- ## Fehlerbehandlung ### Log-Files **Verzeichnis:** `data/logs/` **Haupt-Logfile:** `espo-{YYYY-MM-DD}.log` ```bash # Letzte Fehler anzeigen tail -50 data/logs/espo-$(date +%Y-%m-%d).log | grep -i error # Live-Monitoring tail -f data/logs/espo-$(date +%Y-%m-%d).log ``` ### Häufige Fehler #### 1. Layout-Fehler: "false" statt "{}" **Problem:** EspoCRM 7.x+ erfordert `{}` statt `false` als Platzhalter **Falsch:** ```json { "rows": [ [ {"name": "field1"}, false ] ] } ``` **Richtig:** ```json { "rows": [ [ {"name": "field1"}, {} ] ] } ``` #### 2. Relationship nicht bidirektional **Problem:** `foreign` zeigt nicht zurück **Falsch:** ```json // Entity A "links": { "entityB": { "type": "hasMany", "entity": "EntityB", "foreign": "wrongName" // ❌ } } // Entity B "links": { "entityA": { "type": "belongsTo", "entity": "EntityA", "foreign": "entityB" } } ``` **Richtig:** ```json // Entity A "links": { "entityB": { "type": "hasMany", "entity": "EntityB", "foreign": "entityA" // ✅ Zeigt auf Link-Namen in B } } // Entity B "links": { "entityA": { "type": "belongsTo", "entity": "EntityA", "foreign": "entityB" // ✅ Zeigt auf Link-Namen in A } } ``` #### 3. i18n fehlt für en_US **Problem:** Nur de_DE vorhanden, en_US fehlt **Lösung:** IMMER beide Sprachen pflegen! en_US ist Fallback. #### 4. Dateirechte falsch **Problem:** Files gehören root statt www-data **Lösung:** Automatisch via validate_and_rebuild.py oder manuell: ```bash sudo chown -R www-data:www-data custom/ sudo find custom/ -type f -exec chmod 664 {} \; sudo find custom/ -type d -exec chmod 775 {} \; ``` #### 5. ACL: 403 Forbidden **Problem:** Role hat keine Rechte auf Entity **Lösung:** ACL in Admin UI oder via SQL: ```sql UPDATE role SET data = JSON_SET(data, '$.table.CMyEntity', JSON_OBJECT('create', 'yes', 'read', 'all', 'edit', 'all', 'delete', 'all') ) WHERE name = 'RoleName'; ``` --- ## Deployment-Prozess ### Standard-Workflow ```bash # 1. Code-Änderungen durchführen vim custom/Espo/Custom/Resources/metadata/entityDefs/CMyEntity.json # 2. Validierung + Rebuild python3 custom/scripts/validate_and_rebuild.py # 3. Bei Erfolg: Commit git add custom/ git commit -m "feat: Add CMyEntity with custom fields" git push ``` ### Quick Rebuild (nach kleinen Änderungen) ```bash docker exec espocrm php command.php clear-cache docker exec espocrm php command.php rebuild ``` ### Nach Änderungen an Relationships **IMMER:** 1. Cache löschen 2. Rebuild ausführen 3. Browser-Cache löschen (Ctrl+F5) --- ## Troubleshooting ### Rebuild schlägt fehl **1. Logs prüfen:** ```bash python3 custom/scripts/validate_and_rebuild.py # → Zeigt automatisch Fehlerlog-Analyse ``` **2. Manuell Logs checken:** ```bash tail -100 data/logs/espo-$(date +%Y-%m-%d).log ``` **3. PHP-Fehler:** ```bash docker exec espocrm php -l custom/Espo/Custom/Controllers/MyController.php ``` ### Entity nicht sichtbar **Checklist:** - [ ] `tab: true` in scopes? - [ ] `disabled: false` in scopes? - [ ] ACL-Rechte für Role? - [ ] Cache gelöscht? - [ ] Rebuild durchgeführt? ### Relationship funktioniert nicht **Checklist:** - [ ] Bidirektional konfiguriert? - [ ] `foreign` zeigt korrekt zurück? - [ ] `relationName` identisch (bei M2M)? - [ ] Rebuild durchgeführt? ### API gibt 404 **Checklist:** - [ ] Controller existiert? - [ ] Service existiert? - [ ] Action-Methode korrekt benannt? (postAction..., getAction...) - [ ] ACL-Rechte? ### Formula triggert nicht **Checklist:** - [ ] In `metadata/formula/` statt entityDefs? - [ ] Syntax korrekt? - [ ] Rebuild durchgeführt? ### Bekannte i18n-Warnungen (nicht kritisch) **Stand: März 2026** Die folgenden i18n-Link-Labels fehlen aktuell (funktional keine Auswirkung): ``` ⚠ CDokumente (en_US): Link 'cAICollections' fehlt in i18n ⚠ CAICollections (de_DE): Link 'meetings' fehlt in i18n ⚠ CAICollections (de_DE): Link 'cDokumente' fehlt in i18n ⚠ CAICollections (en_US): Link 'cDokumente' fehlt in i18n ``` **Behebung (optional):** **Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/CDokumente.json` ```json { "links": { "cAICollections": "AI Collections" } } ``` **Datei:** `custom/Espo/Custom/Resources/i18n/en_US/CDokumente.json` ```json { "links": { "cAICollections": "AI Collections" } } ``` **Datei:** `custom/Espo/Custom/Resources/i18n/de_DE/CAICollections.json` ```json { "links": { "cDokumente": "Dokumente", "meetings": "Meetings" } } ``` **Datei:** `custom/Espo/Custom/Resources/i18n/en_US/CAICollections.json` ```json { "links": { "cDokumente": "Documents", "meetings": "Meetings" } } ``` --- ## Projekt-spezifische Entities ### Übersicht 1. **CMietobjekt** - Mietobjekte (Wohnungen/Häuser) 2. **CVmhMietverhltnis** - Mietverhältnisse 3. **CKuendigung** - Kündigungen 4. **CBeteiligte** - Beteiligte Personen 5. **CMietinkasso** - Mietinkasso-Verfahren 6. **CVmhRumungsklage** - Räumungsklagen 7. **CDokumente** - Dokumente 8. **CPuls** - Puls-System (Entwicklungen) 9. **CAICollections** - AI Collections ### Entity-Graph ``` CMietobjekt ├── CVmhMietverhltnis (hasMany) │ ├── CKuendigung (hasMany) │ │ └── CVmhRumungsklage (hasOne) │ ├── CMietinkasso (hasMany) │ └── CBeteiligte (hasMany) └── Contact (hasMany) CDokumente ├── parent → [CVmhRumungsklage, CMietinkasso, CKuendigung] └── CAICollections (hasMany via Junction) └── CPuls (hasMany) ``` --- ## Tools & Scripts ### Übersicht | Tool | Zweck | Ausführung | |------|-------|-----------| | validate_and_rebuild.py | Validierung + Rebuild | `python3 custom/scripts/validate_and_rebuild.py` | | e2e_tests.py | End-to-End Tests | `python3 custom/scripts/e2e_tests.py` | | ki_project_overview.py | Projekt-Analyse für AI | `python3 custom/scripts/ki_project_overview.py` | | workflow_manager.php | Workflow-Verwaltung | `php custom/scripts/workflow_manager.php list` | ### KI-Projekt-Übersicht **Für AI Code Agents:** ```bash python3 custom/scripts/ki_project_overview.py > /tmp/project-overview.txt # → Gibt vollständigen Projekt-Status für AI aus ``` --- ## Ressourcen ### Dokumentation - **EspoCRM Docs:** https://docs.espocrm.com/ - **API Reference:** https://docs.espocrm.com/development/api/ - **Formula Functions:** https://docs.espocrm.com/administration/formula/ ### Projekt-Dokumentation - `custom/docs/ESPOCRM_BEST_PRACTICES.md` - Dieses Dokument - `custom/scripts/QUICKSTART.md` - Quick Start Guide - `custom/scripts/VALIDATION_TOOLS.md` - Validierungs-Tools - `custom/scripts/E2E_TESTS_README.md` - E2E Tests - `custom/README.md` - Custom Actions Blueprint - `custom/TESTERGEBNISSE_JUNCTION_TABLE.md` - Junction Table Implementation --- ## Glossar **ACL** - Access Control List (Zugriffsrechte) **Entity** - Datenmodell (z.B. CMietobjekt) **Link** - Relationship zwischen Entities **Junction Table** - Verbindungstabelle für Many-to-Many **Formula** - Berechnete Felder oder Automation-Scripts **Scope** - Entity-Konfiguration (Tab, ACL, etc.) **Stream** - Activity Feed einer Entity **Hook** - Lifecycle-Event-Handler **Service** - Business Logic Layer **Controller** - API Request Handler **Repository** - Data Access Layer --- **Ende der Best Practices Dokumentation** Für spezifische Fragen oder Updates: Siehe `/custom/docs/` Verzeichnis.