Files
michaelschiemer/docs/whatsapp-notification-channel.md
Michael Schiemer 3b623e7afb feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- Create AnsibleDeployStage using framework's Process module for secure command execution
- Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments
- Add force_deploy flag support in Ansible playbook to override stale locks
- Use PHP deployment module as orchestrator (php console.php deploy:production)
- Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal

Architecture:
- BuildStage → AnsibleDeployStage → HealthCheckStage for production
- Process module provides timeout, error handling, and output capture
- Ansible playbook supports rollback via rollback-git-based.yml
- Zero-downtime deployments with health checks
2025-10-26 14:08:07 +01:00

12 KiB

WhatsApp Notification Channel

Dokumentation für den WhatsApp Business API Notification Channel im Custom PHP Framework.

Übersicht

Der WhatsApp Channel ermöglicht das Versenden von Notifications über die WhatsApp Business API. Es werden sowohl Textnachrichten als auch Template-basierte Nachrichten unterstützt.

Features

Text Messages: Einfache Textnachrichten mit Markdown-Formatierung Template Messages: WhatsApp-approved Message Templates mit Parametern Action Buttons: Support für Action URLs und Labels Type Safety: Framework-konforme Value Objects für alle Identifier HttpClient Integration: Nutzung des Framework's HttpClient Moduls Error Handling: Umfassende Exception-Behandlung mit WhatsAppApiException

Architektur

WhatsAppChannel (NotificationChannelInterface)
    ↓
WhatsAppClient (API Communication)
    ↓
HttpClient (Framework's HTTP Module)
    ↓
WhatsApp Business API

Installation & Setup

1. WhatsApp Business Account einrichten

  1. Erstelle einen WhatsApp Business Account bei Facebook
  2. Registriere deine Business Phone Number
  3. Generiere einen Access Token
  4. Notiere deine Phone Number ID und Business Account ID

URLs:

2. Konfiguration

Die Konfiguration erfolgt aktuell hardcoded in WhatsAppConfig::createDefault():

use App\Framework\Notification\Channels\WhatsApp\WhatsAppConfig;

$config = WhatsAppConfig::createDefault();
// Oder manuell:
$config = new WhatsAppConfig(
    accessToken: 'YOUR_ACCESS_TOKEN',
    phoneNumberId: 'YOUR_PHONE_NUMBER_ID',
    businessAccountId: WhatsAppBusinessAccountId::fromString('YOUR_BUSINESS_ACCOUNT_ID'),
    apiVersion: 'v18.0'
);

Verwendung

Basic Text Message

use App\Framework\Core\ValueObjects\PhoneNumber;
use App\Framework\Notification\Notification;
use App\Framework\Notification\ValueObjects\NotificationChannel;
use App\Framework\Notification\ValueObjects\SystemNotificationType;

// Create notification
$notification = Notification::create(
    recipientId: 'user_123',
    type: SystemNotificationType::SYSTEM_ALERT(),
    title: 'Important Update',
    body: 'Your order has been shipped!',
    NotificationChannel::WHATSAPP
);

// Send via dispatcher
$result = $notificationDispatcher->send($notification);

if ($result->isSuccessful()) {
    echo "WhatsApp message sent: {$result->getMetadata()['message_id']}";
}

Template Message

WhatsApp erfordert pre-approved Templates für Marketing und Notifications.

$notification = Notification::create(
    recipientId: 'user_123',
    type: SystemNotificationType::ORDER_CONFIRMATION(),
    title: 'Order Confirmation',
    body: 'Template will be used',
    NotificationChannel::WHATSAPP
)->withData([
    'whatsapp_template_id' => 'order_confirmation',
    'whatsapp_language' => 'en_US',
    'whatsapp_template_params' => [
        'John Doe',           // Customer name
        'ORD-12345',         // Order number
        '€99.99'             // Total amount
    ]
]);

$result = $notificationDispatcher->send($notification);

With Action Button

$notification = Notification::create(
    recipientId: 'user_123',
    type: SystemNotificationType::PAYMENT_REMINDER(),
    title: 'Payment Due',
    body: 'Your invoice is ready for payment.',
    NotificationChannel::WHATSAPP
)->withAction(
    url: 'https://example.com/invoices/123',
    label: 'View Invoice'
);

// Message will include: "👉 View Invoice: https://example.com/invoices/123"

Phone Number Resolver

Implementiere UserPhoneNumberResolver für deine Anwendung:

use App\Framework\Core\ValueObjects\PhoneNumber;
use App\Framework\Notification\Channels\WhatsApp\UserPhoneNumberResolver;

final readonly class DatabaseUserPhoneNumberResolver implements UserPhoneNumberResolver
{
    public function __construct(
        private UserRepository $userRepository
    ) {}

    public function resolvePhoneNumber(string $userId): ?PhoneNumber
    {
        $user = $this->userRepository->find($userId);

        if ($user === null || $user->phoneNumber === null) {
            return null;
        }

        try {
            return PhoneNumber::fromString($user->phoneNumber);
        } catch (\InvalidArgumentException $e) {
            // Invalid phone number format
            return null;
        }
    }
}

Value Objects

PhoneNumber

use App\Framework\Core\ValueObjects\PhoneNumber;

// E.164 format required: +[country code][number]
$phone = PhoneNumber::fromString('+4917612345678');

// Or from parts
$phone = PhoneNumber::fromInternational('49', '17612345678');

// Methods
$phone->toString();                  // +4917612345678
$phone->toDisplayFormat();           // +49 176 126 456 78
$phone->getCountryCode();            // 49
$phone->getSubscriberNumber();       // 17612345678

WhatsAppTemplateId

use App\Framework\Notification\Channels\WhatsApp\ValueObjects\WhatsAppTemplateId;

// Template names must be lowercase alphanumeric with underscores
$templateId = WhatsAppTemplateId::fromString('order_confirmation');
$templateId = WhatsAppTemplateId::fromString('hello_world');

// ❌ Invalid
$templateId = WhatsAppTemplateId::fromString('OrderConfirmation'); // Uppercase not allowed
$templateId = WhatsAppTemplateId::fromString('order-confirmation'); // Hyphen not allowed

WhatsAppBusinessAccountId

use App\Framework\Notification\Channels\WhatsApp\ValueObjects\WhatsAppBusinessAccountId;

$accountId = WhatsAppBusinessAccountId::fromString('123456789012345');

WhatsAppMessageId

use App\Framework\Notification\Channels\WhatsApp\ValueObjects\WhatsAppMessageId;

// Returned from API after sending
$messageId = WhatsAppMessageId::fromString('wamid.HBgNNDkxNzYxMjM0NTY3OBUCABIYFjNFQjBDNzE4RjAzMEE1NzQxODZEMDIA');

WhatsApp Templates

Template Erstellen

  1. Gehe zu WhatsApp Business Manager
  2. Navigiere zu "Message Templates"
  3. Erstelle ein neues Template
  4. Warte auf Approval (kann 24-48h dauern)

Template Beispiel

Template Name: order_confirmation Language: English (US) Category: Transactional Body:

Hello {{1}},

Your order {{2}} has been confirmed!
Total amount: {{3}}

Thank you for your purchase.

Usage:

$notification->withData([
    'whatsapp_template_id' => 'order_confirmation',
    'whatsapp_language' => 'en_US',
    'whatsapp_template_params' => [
        'John Doe',      // {{1}}
        'ORD-12345',     // {{2}}
        '€99.99'         // {{3}}
    ]
]);

Testing

Manual Test Script

docker exec php php tests/debug/test-whatsapp-notification.php

Wichtig: Ersetze die Test-Telefonnummer in der Datei mit deiner eigenen WhatsApp-Nummer!

Unit Test

use App\Framework\Notification\Channels\WhatsAppChannel;
use App\Framework\Notification\Notification;

it('sends WhatsApp notification successfully', function () {
    $mockClient = Mockery::mock(WhatsAppClient::class);
    $mockResolver = Mockery::mock(UserPhoneNumberResolver::class);

    $mockResolver->shouldReceive('resolvePhoneNumber')
        ->with('user_123')
        ->andReturn(PhoneNumber::fromString('+4917612345678'));

    $mockClient->shouldReceive('sendTextMessage')
        ->once()
        ->andReturn(new WhatsAppResponse(
            success: true,
            messageId: WhatsAppMessageId::fromString('wamid_test_123')
        ));

    $channel = new WhatsAppChannel($mockClient, $mockResolver);

    $notification = Notification::create(
        recipientId: 'user_123',
        type: SystemNotificationType::SYSTEM_ALERT(),
        title: 'Test',
        body: 'Test message',
        NotificationChannel::WHATSAPP
    );

    $result = $channel->send($notification);

    expect($result->isSuccessful())->toBeTrue();
});

Error Handling

WhatsAppApiException

use App\Framework\Notification\Channels\WhatsApp\WhatsAppApiException;

try {
    $response = $whatsappClient->sendTextMessage($phoneNumber, $message);
} catch (WhatsAppApiException $e) {
    // API returned an error
    $httpCode = $e->getHttpStatusCode();
    $message = $e->getMessage();

    // Log or handle error
    $logger->error('WhatsApp API error', [
        'http_code' => $httpCode,
        'message' => $message
    ]);
}

Common Errors

Error Code Beschreibung Lösung
100 Invalid parameter Prüfe Parameter (phone number format, template ID)
131009 Parameter value not valid Template parameters stimmen nicht mit Template überein
131026 Message undeliverable Empfänger hat WhatsApp nicht oder blockiert Business Account
131047 Re-engagement message User muss zuerst Business Account kontaktieren
190 Access token expired Generiere neuen Access Token

Best Practices

1. Phone Number Validation

// ✅ Validate before using
try {
    $phone = PhoneNumber::fromString($userInput);
} catch (\InvalidArgumentException $e) {
    // Handle invalid phone number
    return 'Invalid phone number format';
}

// ❌ Don't assume format
$phone = PhoneNumber::fromString($_POST['phone']); // Can throw exception

2. Template Usage

// ✅ Use templates for marketing/promotional content
$notification->withData([
    'whatsapp_template_id' => 'weekly_newsletter',
    'whatsapp_language' => 'en_US'
]);

// ✅ Use text messages for immediate transactional updates
$notification = Notification::create(
    recipientId: 'user_123',
    type: SystemNotificationType::SYSTEM_ALERT(),
    title: 'Server Alert',
    body: 'Critical: Database connection lost!',
    NotificationChannel::WHATSAPP
);

3. Rate Limiting

WhatsApp hat Rate Limits pro Business Account:

  • Tier 1 (default): 1,000 unique contacts/24h
  • Tier 2: 10,000 unique contacts/24h
  • Tier 3: 100,000 unique contacts/24h
// Implement rate limiting
if ($this->rateLimiter->tooManyAttempts("whatsapp:{$userId}", 5, 3600)) {
    throw new RateLimitException('Too many WhatsApp messages sent');
}

4. Opt-In Requirement

WhatsApp erfordert explicit Opt-In von Usern:

// Check user consent before sending
if (!$user->hasWhatsAppOptIn()) {
    return ChannelResult::failure(
        channel: NotificationChannel::WHATSAPP,
        errorMessage: 'User has not opted in to WhatsApp notifications'
    );
}

Troubleshooting

Message nicht zugestellt

Checklist:

  • Phone Number ist in E.164 Format (+4917612345678)
  • Empfänger hat WhatsApp installiert
  • Empfänger hat Business Account nicht blockiert
  • Access Token ist gültig
  • Template ist approved (für Template Messages)
  • Rate Limits nicht überschritten

Template Errors

Problem: "Parameter value not valid" Lösung: Anzahl der Parameter muss exakt mit Template übereinstimmen

// Template hat 3 placeholders: {{1}}, {{2}}, {{3}}
// ✅ Correct
'whatsapp_template_params' => ['Param1', 'Param2', 'Param3']

// ❌ Wrong - zu wenige Parameter
'whatsapp_template_params' => ['Param1', 'Param2']

Access Token Expired

Problem: Error 190 - Access token has expired Lösung: Generiere neuen Access Token im Facebook Business Manager

Weiterführende Ressourcen

Framework Integration

Der WhatsApp Channel folgt allen Framework-Patterns:

Readonly Classes: Alle VOs und Configs sind final readonly Value Objects: Keine Primitive Obsession (PhoneNumber, TemplateId, etc.) No Inheritance: Composition über Inheritance Type Safety: Strikte Typisierung für alle Parameter Framework Compliance: Integration mit HttpClient, Notification System Explicit Dependencies: Constructor Injection, keine Service Locators