Update documentation: Add Hook development section and Custom Entities overview
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# EspoCRM Best Practices & Entwicklungsrichtlinien
|
||||
|
||||
**Version:** 2.0
|
||||
**Version:** 2.1
|
||||
**Datum:** 9. März 2026
|
||||
**Zielgruppe:** AI Code Agents & Entwickler
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
3. [Entity-Entwicklung](#entity-entwicklung)
|
||||
4. [Relationship-Patterns](#relationship-patterns)
|
||||
5. [API-Entwicklung](#api-entwicklung)
|
||||
6. [Workflow-Management](#workflow-management)
|
||||
7. [Testing & Validierung](#testing--validierung)
|
||||
8. [Fehlerbehandlung](#fehlerbehandlung)
|
||||
9. [Deployment-Prozess](#deployment-prozess)
|
||||
10. [Troubleshooting](#troubleshooting)
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
@@ -67,6 +68,46 @@ client/custom/ # Frontend-Code
|
||||
└── 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
|
||||
@@ -123,7 +164,7 @@ client/custom/ # Frontend-Code
|
||||
- ✅ Folge PSR-12 Coding Standard
|
||||
|
||||
**DON'T:**
|
||||
- ❌ Keine komplexe Logik in Hooks
|
||||
- ❌ Keine komplexe Business-Logic in Hooks (nutze Services)
|
||||
- ❌ Keine direkten SQL-Queries (nutze EntityManager)
|
||||
- ❌ Keine hard-coded Werte (nutze Config)
|
||||
- ❌ Keine redundanten Includes
|
||||
@@ -669,6 +710,523 @@ curl -X GET "https://crm.example.com/api/v1/CMyEntity" \
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
<?php
|
||||
namespace Espo\Custom\Hooks\{EntityName};
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
use Espo\Core\Hook\Hook\BeforeSave;
|
||||
|
||||
class MyHook implements BeforeSave
|
||||
{
|
||||
public function __construct(
|
||||
private \Espo\ORM\EntityManager $entityManager,
|
||||
private \Espo\Core\Utils\Config $config
|
||||
) {}
|
||||
|
||||
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||
{
|
||||
// Hook-Logik hier
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Legacy Hooks (EspoCRM < 7.0, noch unterstützt):**
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Espo\Custom\Hooks\{EntityName};
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class MyHook extends \Espo\Core\Hooks\Base
|
||||
{
|
||||
public function beforeSave(Entity $entity, array $options = [])
|
||||
{
|
||||
// Hook-Logik hier
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
**Moderne Hooks nutzen Constructor Injection:**
|
||||
|
||||
```php
|
||||
public function __construct(
|
||||
private \Espo\ORM\EntityManager $entityManager,
|
||||
private \Espo\Core\Utils\Config $config,
|
||||
private \Espo\Core\Utils\Language $language,
|
||||
private \Espo\Core\ServiceFactory $serviceFactory,
|
||||
private \Espo\Core\Mail\EmailSender $emailSender
|
||||
) {}
|
||||
```
|
||||
|
||||
**Verfügbare Services:**
|
||||
- `EntityManager` - Datenbankzugriff
|
||||
- `Config` - System-Konfiguration
|
||||
- `Language` - i18n-Übersetzungen
|
||||
- `ServiceFactory` - Service-Instanzen
|
||||
- `EmailSender` - E-Mail-Versand
|
||||
- `Acl` - ACL-Prüfungen
|
||||
- `User` - Aktueller Benutzer
|
||||
|
||||
### Praxis-Beispiele aus dem Projekt
|
||||
|
||||
#### Beispiel 1: Daten-Validierung & Normalisierung (CBankverbindungen)
|
||||
|
||||
**Datei:** `custom/Espo/Custom/Hooks/CBankverbindungen/BankdatenValidation.php`
|
||||
|
||||
**Use Case:** IBAN/BIC-Validierung mit Modulo-97-Algorithmus
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Espo\Custom\Hooks\CBankverbindungen;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Utils\Language;
|
||||
|
||||
class BankdatenValidation
|
||||
{
|
||||
public function __construct(
|
||||
private Language $language
|
||||
) {}
|
||||
|
||||
public function beforeSave(Entity $entity, array $options): void
|
||||
{
|
||||
// IBAN-Normalisierung und Validierung
|
||||
$iban = $entity->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
|
||||
<?php
|
||||
namespace Espo\Custom\Hooks\CDokumente;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class CDokumente extends \Espo\Core\Hooks\Base
|
||||
{
|
||||
public function beforeSave(Entity $entity, array $options = [])
|
||||
{
|
||||
$dokument = $entity->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
|
||||
<?php
|
||||
namespace Espo\Custom\Hooks\CPuls;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Repository\Option\SaveOptions;
|
||||
use Espo\Core\Hook\Hook\BeforeSave;
|
||||
|
||||
class UpdateTeamStats implements BeforeSave
|
||||
{
|
||||
public function __construct(
|
||||
private \Espo\ORM\EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
public function beforeSave(Entity $entity, SaveOptions $options): void
|
||||
{
|
||||
// Dokumente zählen
|
||||
if ($entity->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
|
||||
@@ -991,6 +1549,59 @@ docker exec espocrm php -l custom/Espo/Custom/Controllers/MyController.php
|
||||
- [ ] 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
|
||||
|
||||
Reference in New Issue
Block a user