- Hinzufügen der DependencyInjection-Konfiguration für das Billing-Modul. - Erstellen der Invoice-Entity mit API-Ressourcen und Berechtigungen. - Konfigurieren der Services in services.yaml für das Billing-Modul. - Implementieren von CLI-Commands zur Verwaltung von Modul-Lizenzen und zur Auflistung installierter Module. - Erstellen eines API-Controllers zur Verwaltung von Modulen und Lizenzen. - Hinzufügen eines EventListeners für das Booten von Modulen. - Definieren von Interfaces für Lizenzvalidierung und Modul-Plugins. - Implementieren der ModuleRegistry zur Verwaltung und Booten von Modulen. - Erstellen eines LicenseValidator-Services zur Validierung und Registrierung von Lizenzen.
624 lines
14 KiB
Markdown
624 lines
14 KiB
Markdown
# Plugin-System für lizenzierbare Module
|
|
|
|
## Übersicht
|
|
|
|
Das myCRM Plugin-System ermöglicht die Installation und Verwaltung von optionalen, lizenzierbaren Modulen als separate Composer-Packages. Module können unabhängig vom Core entwickelt, installiert und lizenziert werden.
|
|
|
|
## Architektur-Prinzipien
|
|
|
|
### 1. **Lose Kopplung**
|
|
- Core kennt nur Interfaces, keine konkreten Module
|
|
- Module registrieren sich selbst über Symfony's Service-Tagging
|
|
- Keine Hard-Dependencies im Core-Code
|
|
|
|
### 2. **Lizenzbasierte Aktivierung**
|
|
- Module werden nur gebootet, wenn eine gültige Lizenz vorhanden ist
|
|
- Online-Validierung mit Offline-Fallback (Grace Period: 7 Tage)
|
|
- Caching zur Performance-Optimierung
|
|
|
|
### 3. **Plugin als Symfony Bundle**
|
|
- Jedes Modul ist ein vollständiges Symfony Bundle
|
|
- Eigene Entities, Controller, Services, Routes
|
|
- Optionale Frontend-Komponenten (Vue.js)
|
|
|
|
### 4. **Composer-basierte Installation**
|
|
- Module sind private Composer-Packages
|
|
- `composer require mycrm/modul-name` installiert das Modul
|
|
- Automatische Service-Registrierung über Symfony Flex
|
|
|
|
## Komponenten
|
|
|
|
### Core-Komponenten (im Hauptprojekt)
|
|
|
|
```
|
|
src/Plugin/
|
|
├── ModulePluginInterface.php # Interface für alle Module
|
|
├── LicenseValidatorInterface.php # Interface für Lizenzvalidierung
|
|
├── ModuleRegistry.php # Registry für alle installierten Module
|
|
|
|
src/Service/
|
|
└── LicenseValidator.php # Standard-Implementierung
|
|
|
|
src/EventListener/
|
|
└── ModuleBootListener.php # Bootet Module beim Request
|
|
|
|
src/Command/
|
|
├── ModuleListCommand.php # CLI: Module auflisten
|
|
└── ModuleLicenseCommand.php # CLI: Lizenzen verwalten
|
|
|
|
src/Controller/Api/
|
|
└── ModuleManagementController.php # API für Module-Verwaltung
|
|
|
|
config/
|
|
└── services_plugin.yaml # Service-Konfiguration
|
|
```
|
|
|
|
### Modul-Struktur (externes Package)
|
|
|
|
```
|
|
mycrm-modulname/
|
|
├── composer.json
|
|
├── src/
|
|
│ ├── ModulnameModulePlugin.php # Implementiert ModulePluginInterface
|
|
│ ├── ModulnameBundle.php # Symfony Bundle
|
|
│ ├── DependencyInjection/
|
|
│ │ ├── ModulnameExtension.php
|
|
│ │ └── Configuration.php
|
|
│ ├── Entity/ # Modul-spezifische Entities
|
|
│ ├── Controller/ # API-Controller
|
|
│ ├── Service/ # Business Logic
|
|
│ ├── Security/Voter/ # Permission-Voter
|
|
│ └── EventListener/
|
|
├── config/
|
|
│ ├── services.yaml
|
|
│ ├── routes.yaml
|
|
│ └── packages/
|
|
│ └── doctrine.yaml
|
|
├── assets/ # Frontend (optional)
|
|
│ └── js/
|
|
│ └── components/
|
|
└── migrations/ # Modul-spezifische Migrations
|
|
```
|
|
|
|
## Installation und Konfiguration
|
|
|
|
### 1. Core-System vorbereiten
|
|
|
|
Die Core-Komponenten sind bereits implementiert. Konfiguration aktivieren:
|
|
|
|
```bash
|
|
# services_plugin.yaml in services.yaml importieren
|
|
# config/services.yaml
|
|
imports:
|
|
- { resource: services_plugin.yaml }
|
|
```
|
|
|
|
Environment-Variablen setzen:
|
|
|
|
```bash
|
|
# .env.local
|
|
LICENSE_SERVER_URL=https://license.mycrm.local
|
|
INSTANCE_ID=unique-instance-identifier-here
|
|
|
|
# Beispiel-Lizenz für ein Modul
|
|
LICENSE_BILLING=your-license-key-here
|
|
```
|
|
|
|
### 2. Modul installieren
|
|
|
|
```bash
|
|
# Private Repository konfigurieren
|
|
composer config repositories.mycrm-billing vcs https://github.com/your-org/mycrm-billing-module
|
|
|
|
# Modul installieren
|
|
composer require mycrm/billing-module
|
|
|
|
# Bundle registrieren (falls nicht automatisch via Flex)
|
|
# config/bundles.php
|
|
return [
|
|
// ...
|
|
MyCRM\BillingModule\BillingBundle::class => ['all' => true],
|
|
];
|
|
|
|
# Migrations ausführen
|
|
php bin/console doctrine:migrations:migrate
|
|
```
|
|
|
|
### 3. Lizenz registrieren
|
|
|
|
**Via CLI:**
|
|
|
|
```bash
|
|
# Interaktiv
|
|
php bin/console app:module:license billing
|
|
|
|
# Direkt mit Key
|
|
php bin/console app:module:license billing YOUR_LICENSE_KEY
|
|
|
|
# Lizenz validieren
|
|
php bin/console app:module:license billing --validate
|
|
|
|
# Lizenz widerrufen
|
|
php bin/console app:module:license billing --revoke
|
|
|
|
# Alle Module auflisten
|
|
php bin/console app:module:list
|
|
```
|
|
|
|
**Via API (für Admin-UI):**
|
|
|
|
```bash
|
|
# Module auflisten
|
|
curl -X GET http://localhost:8000/api/modules \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Lizenz registrieren
|
|
curl -X POST http://localhost:8000/api/modules/billing/license \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{"license_key": "YOUR_LICENSE_KEY"}'
|
|
|
|
# Lizenz widerrufen
|
|
curl -X DELETE http://localhost:8000/api/modules/billing/license \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
```
|
|
|
|
**Via Frontend (Vue.js):**
|
|
|
|
Die Komponente `ModuleManagement.vue` bietet eine Admin-Oberfläche zur Verwaltung:
|
|
|
|
```javascript
|
|
// In router.js hinzufügen
|
|
{
|
|
path: '/admin/modules',
|
|
name: 'module-management',
|
|
component: () => import('@/views/ModuleManagement.vue'),
|
|
meta: {
|
|
requiresAuth: true,
|
|
requiresRole: 'ROLE_ADMIN'
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Cache leeren und testen
|
|
|
|
```bash
|
|
php bin/console cache:clear
|
|
php bin/console app:module:list
|
|
```
|
|
|
|
## Modul entwickeln
|
|
|
|
### Schritt 1: Projekt-Struktur erstellen
|
|
|
|
```bash
|
|
mkdir mycrm-mymodule
|
|
cd mycrm-mymodule
|
|
composer init
|
|
```
|
|
|
|
### Schritt 2: composer.json konfigurieren
|
|
|
|
```json
|
|
{
|
|
"name": "mycrm/mymodule",
|
|
"description": "Mein Custom CRM-Modul",
|
|
"type": "symfony-bundle",
|
|
"require": {
|
|
"php": ">=8.3",
|
|
"symfony/framework-bundle": "^7.1"
|
|
},
|
|
"autoload": {
|
|
"psr-4": {
|
|
"MyCRM\\MyModule\\": "src/"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Schritt 3: Plugin-Klasse implementieren
|
|
|
|
```php
|
|
<?php
|
|
// src/MyModulePlugin.php
|
|
|
|
namespace MyCRM\MyModule;
|
|
|
|
use App\Plugin\LicenseValidatorInterface;
|
|
use App\Plugin\ModulePluginInterface;
|
|
|
|
class MyModulePlugin implements ModulePluginInterface
|
|
{
|
|
public function __construct(
|
|
private readonly LicenseValidatorInterface $licenseValidator
|
|
) {}
|
|
|
|
public function getIdentifier(): string
|
|
{
|
|
return 'mymodule';
|
|
}
|
|
|
|
public function getDisplayName(): string
|
|
{
|
|
return 'Mein Modul';
|
|
}
|
|
|
|
public function getVersion(): string
|
|
{
|
|
return '1.0.0';
|
|
}
|
|
|
|
public function getDescription(): string
|
|
{
|
|
return 'Beschreibung meines Moduls';
|
|
}
|
|
|
|
public function isLicensed(): bool
|
|
{
|
|
$info = $this->getLicenseInfo();
|
|
return $info['valid'];
|
|
}
|
|
|
|
public function getLicenseInfo(): array
|
|
{
|
|
return $this->licenseValidator->validate($this->getIdentifier());
|
|
}
|
|
|
|
public function boot(): void
|
|
{
|
|
if (!$this->isLicensed()) {
|
|
throw new \RuntimeException('Modul nicht lizenziert');
|
|
}
|
|
|
|
// Modul-spezifische Boot-Logik
|
|
}
|
|
|
|
public function getPermissionModules(): array
|
|
{
|
|
return ['mymodule'];
|
|
}
|
|
|
|
public function canInstall(): array
|
|
{
|
|
return ['success' => true, 'errors' => []];
|
|
}
|
|
}
|
|
```
|
|
|
|
### Schritt 4: Bundle-Klasse erstellen
|
|
|
|
```php
|
|
<?php
|
|
// src/MyModuleBundle.php
|
|
|
|
namespace MyCRM\MyModule;
|
|
|
|
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
|
|
|
class MyModuleBundle extends Bundle
|
|
{
|
|
public function getPath(): string
|
|
{
|
|
return \dirname(__DIR__);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Schritt 5: Services konfigurieren
|
|
|
|
```yaml
|
|
# config/services.yaml
|
|
services:
|
|
_defaults:
|
|
autowire: true
|
|
autoconfigure: true
|
|
|
|
# Plugin registrieren
|
|
MyCRM\MyModule\MyModulePlugin:
|
|
tags: ['app.module_plugin']
|
|
|
|
# Weitere Services...
|
|
MyCRM\MyModule\Controller\:
|
|
resource: '../src/Controller/'
|
|
tags: ['controller.service_arguments']
|
|
```
|
|
|
|
### Schritt 6: Entities mit ModuleAwareInterface
|
|
|
|
```php
|
|
<?php
|
|
// src/Entity/MyEntity.php
|
|
|
|
namespace MyCRM\MyModule\Entity;
|
|
|
|
use ApiPlatform\Metadata\ApiResource;
|
|
use App\Entity\ModuleAwareInterface;
|
|
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
#[ORM\Entity]
|
|
#[ApiResource(
|
|
security: "is_granted('VIEW', 'mymodule')"
|
|
)]
|
|
class MyEntity implements ModuleAwareInterface
|
|
{
|
|
#[ORM\Id]
|
|
#[ORM\GeneratedValue]
|
|
#[ORM\Column]
|
|
private ?int $id = null;
|
|
|
|
// ... weitere Properties
|
|
|
|
public function getModuleName(): string
|
|
{
|
|
return 'mymodule';
|
|
}
|
|
}
|
|
```
|
|
|
|
### Schritt 7: Voter für Permissions (optional)
|
|
|
|
```php
|
|
<?php
|
|
// src/Security/Voter/MyEntityVoter.php
|
|
|
|
namespace MyCRM\MyModule\Security\Voter;
|
|
|
|
use App\Security\Voter\ModuleVoter;
|
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
|
|
|
class MyEntityVoter extends Voter
|
|
{
|
|
public function __construct(
|
|
private readonly ModuleVoter $moduleVoter
|
|
) {}
|
|
|
|
protected function supports(string $attribute, mixed $subject): bool
|
|
{
|
|
return in_array($attribute, ['VIEW', 'EDIT', 'DELETE'])
|
|
&& ($subject instanceof MyEntity || $subject === 'mymodule');
|
|
}
|
|
|
|
protected function voteOnAttribute(
|
|
string $attribute,
|
|
mixed $subject,
|
|
TokenInterface $token
|
|
): bool {
|
|
// An ModuleVoter delegieren
|
|
return $this->moduleVoter->vote($token, $subject, [$attribute]) === Voter::ACCESS_GRANTED;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Lizenzserver-Integration
|
|
|
|
### Lizenzserver-Endpunkt
|
|
|
|
Der Lizenzserver muss folgenden Endpunkt bereitstellen:
|
|
|
|
```
|
|
POST /api/validate
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"license_key": "YOUR-LICENSE-KEY",
|
|
"module": "billing",
|
|
"instance_id": "unique-instance-id",
|
|
"version": "1.0.0"
|
|
}
|
|
|
|
Response (200 OK):
|
|
{
|
|
"valid": true,
|
|
"expires_at": "2026-12-31T23:59:59Z",
|
|
"licensed_to": "Firma GmbH",
|
|
"features": ["feature1", "feature2"],
|
|
"message": "Lizenz gültig"
|
|
}
|
|
|
|
Response (400/403):
|
|
{
|
|
"valid": false,
|
|
"message": "Lizenz abgelaufen / ungültig / etc."
|
|
}
|
|
```
|
|
|
|
### Lizenzschlüssel-Format (JWT)
|
|
|
|
Empfohlenes Format als JWT mit folgenden Claims:
|
|
|
|
```json
|
|
{
|
|
"sub": "billing",
|
|
"customer_id": "CUST-12345",
|
|
"licensed_to": "Firma GmbH",
|
|
"expires_at": "2026-12-31T23:59:59Z",
|
|
"features": ["invoicing", "recurring_billing"],
|
|
"iat": 1704278400,
|
|
"exp": 1767350399
|
|
}
|
|
```
|
|
|
|
## Permission-System Integration
|
|
|
|
### Automatische Modul-Registrierung
|
|
|
|
Module, die `getPermissionModules()` implementieren, werden automatisch im Permission-System verfügbar:
|
|
|
|
```php
|
|
public function getPermissionModules(): array
|
|
{
|
|
return [
|
|
'billing', // Hauptmodul
|
|
'invoices', // Sub-Modul
|
|
'payments', // Weiteres Sub-Modul
|
|
];
|
|
}
|
|
```
|
|
|
|
Diese Module können dann in der Role-Verwaltung zugewiesen werden.
|
|
|
|
### Permission-Checks in Modul-Code
|
|
|
|
```php
|
|
// In Controller
|
|
$this->denyAccessUnlessGranted('VIEW', 'billing');
|
|
|
|
// In Voter
|
|
$user->hasModulePermission('billing', 'edit');
|
|
|
|
// In API Platform
|
|
#[ApiResource(
|
|
security: "is_granted('VIEW', 'billing')"
|
|
)]
|
|
```
|
|
|
|
## Frontend-Integration
|
|
|
|
### Modul-Status im Frontend verfügbar machen
|
|
|
|
```javascript
|
|
// In Symfony Twig-Template (oder API-Endpunkt)
|
|
<script>
|
|
window.myCRM = {
|
|
modules: {{ modules_status|json_encode|raw }}
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Bedingte Route-Registrierung
|
|
|
|
```javascript
|
|
// router.js
|
|
import { moduleRegistry } from './moduleRegistry'
|
|
|
|
const routes = [
|
|
// Core-Routes...
|
|
]
|
|
|
|
// Modul-Routes nur hinzufügen, wenn aktiv
|
|
if (moduleRegistry.isActive('billing')) {
|
|
routes.push({
|
|
path: '/billing',
|
|
component: () => import('@/views/BillingDashboard.vue')
|
|
})
|
|
}
|
|
```
|
|
|
|
### Modul-Komponenten lazy-loaden
|
|
|
|
```javascript
|
|
// In App.vue oder Layout
|
|
import { computed } from 'vue'
|
|
import { useModules } from '@/composables/useModules'
|
|
|
|
const { isModuleActive } = useModules()
|
|
|
|
const showBillingMenu = computed(() => isModuleActive('billing'))
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Unit-Tests für Plugin
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace MyCRM\MyModule\Tests;
|
|
|
|
use MyCRM\MyModule\MyModulePlugin;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class MyModulePluginTest extends TestCase
|
|
{
|
|
public function testGetIdentifier(): void
|
|
{
|
|
$licenseValidator = $this->createMock(LicenseValidatorInterface::class);
|
|
$plugin = new MyModulePlugin($licenseValidator);
|
|
|
|
$this->assertSame('mymodule', $plugin->getIdentifier());
|
|
}
|
|
|
|
public function testIsLicensedReturnsTrueWithValidLicense(): void
|
|
{
|
|
$licenseValidator = $this->createMock(LicenseValidatorInterface::class);
|
|
$licenseValidator->method('validate')
|
|
->willReturn(['valid' => true]);
|
|
|
|
$plugin = new MyModulePlugin($licenseValidator);
|
|
|
|
$this->assertTrue($plugin->isLicensed());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Modul wird nicht erkannt
|
|
|
|
1. Bundle in `config/bundles.php` registriert?
|
|
2. Composer-Autoloading aktualisiert? (`composer dump-autoload`)
|
|
3. Cache geleert? (`php bin/console cache:clear`)
|
|
4. Service-Tag `app.module_plugin` gesetzt?
|
|
|
|
### Lizenz wird nicht validiert
|
|
|
|
1. Environment-Variablen korrekt gesetzt?
|
|
2. Lizenzserver erreichbar? (Netzwerk, Firewall)
|
|
3. Cache-Problem? Cache löschen und neu validieren
|
|
4. Logs prüfen: `tail -f var/log/dev.log | grep -i license`
|
|
|
|
### Modul bootet nicht
|
|
|
|
1. Lizenz gültig? (`php bin/console app:module:license mymodule --validate`)
|
|
2. Abhängigkeiten erfüllt? (`php bin/console app:module:list`)
|
|
3. Doctrine-Mappings korrekt? (`php bin/console doctrine:schema:validate`)
|
|
4. Fehler in `boot()`-Methode? Logs prüfen
|
|
|
|
## Best Practices
|
|
|
|
### 1. Versionierung
|
|
- Semantic Versioning verwenden (Major.Minor.Patch)
|
|
- Breaking Changes nur in Major-Versionen
|
|
- Migrations mit Rollback-Fähigkeit
|
|
|
|
### 2. Fehlerbehandlung
|
|
- Keine Exceptions in `isLicensed()` werfen
|
|
- Detaillierte Fehlermeldungen in `getLicenseInfo()`
|
|
- Graceful Degradation bei Netzwerkfehlern
|
|
|
|
### 3. Performance
|
|
- Lizenz-Status cachen (bereits implementiert)
|
|
- Lazy-Loading für Frontend-Komponenten
|
|
- Doctrine-Queries optimieren
|
|
|
|
### 4. Sicherheit
|
|
- Nie Lizenzschlüssel im Frontend exponieren
|
|
- API-Endpunkte mit `ROLE_ADMIN` schützen
|
|
- Input-Validierung in allen Controllern
|
|
|
|
### 5. Dokumentation
|
|
- README.md mit Installation-Guide
|
|
- API-Dokumentation (OpenAPI/Swagger)
|
|
- Changelog pflegen
|
|
|
|
## Beispiel: Komplettes Mini-Modul
|
|
|
|
Ein vollständiges Mini-Beispiel findet sich in:
|
|
- `/docs/example-module/` - Backend-Code
|
|
- `/docs/EXAMPLE_MODULE_STRUCTURE.md` - Struktur-Übersicht
|
|
|
|
## Support und Weiterentwicklung
|
|
|
|
### Geplante Features
|
|
|
|
- [ ] Multi-Tenancy-Support (verschiedene Instanzen, eine Lizenz)
|
|
- [ ] Offline-Lizenzierung (Air-Gapped Systeme)
|
|
- [ ] Automatische Updates über Composer
|
|
- [ ] Modul-Marketplace Integration
|
|
- [ ] Rollback-Mechanismus bei fehlgeschlagenen Installations
|
|
|
|
### Beitragen
|
|
|
|
Weitere Module können nach diesem Pattern entwickelt werden. Core-Interfaces sind stabil und abwärtskompatibel.
|