-
-
Notifications
You must be signed in to change notification settings - Fork 1
Best Practices
Development, deployment, and maintenance best practices for php-chatbot.
- Development Best Practices
- Security Best Practices
- Performance Best Practices
- Deployment Guidelines
- Monitoring and Maintenance
- Code Quality
- Testing Strategies
Development, deployment, and maintenance best practices for php-chatbot.
- Development Best Practices
- Security Best Practices
- Performance Best Practices
- Deployment Guidelines
- Monitoring and Maintenance
- Code Quality
- Testing Strategies
Organize your chatbot implementation for maintainability and scalability:
app/
├── Chatbot/
│ ├── Controllers/
│ │ ├── ChatbotController.php
│ │ └── AdminChatbotController.php
│ ├── Services/
│ │ ├── ChatbotService.php
│ │ ├── ConversationManager.php
│ │ └── MessageProcessor.php
│ ├── Models/
│ │ ├── Conversation.php
│ │ ├── Message.php
│ │ └── ChatbotLog.php
│ ├── Middleware/
│ │ ├── RateLimitMiddleware.php
│ │ ├── ContentFilterMiddleware.php
│ │ └── AuthenticationMiddleware.php
│ ├── Jobs/
│ │ ├── ProcessChatMessage.php
│ │ └── AnalyzeChatInteraction.php
│ ├── Events/
│ │ ├── MessageSent.php
│ │ └── ConversationStarted.php
│ └── Providers/
│ └── ChatbotServiceProvider.phpUse environment-specific configurations:
// config/chatbot.php
return [
'default_model' => env('CHATBOT_DEFAULT_MODEL', 'openai'),
'models' => [
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'model' => env('OPENAI_MODEL', 'gpt-4'),
'temperature' => env('OPENAI_TEMPERATURE', 0.7),
'max_tokens' => env('OPENAI_MAX_TOKENS', 150),
'timeout' => env('OPENAI_TIMEOUT', 30),
],
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'model' => env('ANTHROPIC_MODEL', 'claude-3-sonnet-20240229'),
'temperature' => env('ANTHROPIC_TEMPERATURE', 0.7),
'max_tokens' => env('ANTHROPIC_MAX_TOKENS', 150),
],
],
'features' => [
'conversation_memory' => env('CHATBOT_CONVERSATION_MEMORY', true),
'content_filtering' => env('CHATBOT_CONTENT_FILTERING', true),
'rate_limiting' => env('CHATBOT_RATE_LIMITING', true),
'logging' => env('CHATBOT_LOGGING', true),
],
'security' => [
'max_message_length' => env('CHATBOT_MAX_MESSAGE_LENGTH', 2000),
'rate_limit' => [
'requests' => env('CHATBOT_RATE_LIMIT_REQUESTS', 20),
'per_minutes' => env('CHATBOT_RATE_LIMIT_MINUTES', 1),
],
],
];Environment file (.env):
# Chatbot Configuration
CHATBOT_DEFAULT_MODEL=openai
CHATBOT_CONVERSATION_MEMORY=true
CHATBOT_CONTENT_FILTERING=true
CHATBOT_RATE_LIMITING=true
CHATBOT_LOGGING=true
# OpenAI Settings
OPENAI_API_KEY=sk-your-openai-key
OPENAI_MODEL=gpt-4
OPENAI_TEMPERATURE=0.7
OPENAI_MAX_TOKENS=150
OPENAI_TIMEOUT=30
# Anthropic Settings
ANTHROPIC_API_KEY=your-anthropic-key
ANTHROPIC_MODEL=claude-3-sonnet-20240229
# Security Settings
CHATBOT_MAX_MESSAGE_LENGTH=2000
CHATBOT_RATE_LIMIT_REQUESTS=20
CHATBOT_RATE_LIMIT_MINUTES=1Create dedicated services for different concerns:
<?php
namespace App\Chatbot\Services;
class ChatbotService
{
public function __construct(
private ConversationManager $conversationManager,
private MessageProcessor $messageProcessor,
private SecurityService $securityService,
private LoggingService $loggingService
) {}
public function processMessage(string $message, array $context = []): array
{
// 1. Security validation
$this->securityService->validateMessage($message, $context);
// 2. Process the message
$response = $this->messageProcessor->process($message, $context);
// 3. Manage conversation state
$this->conversationManager->updateConversation($response, $context);
// 4. Log the interaction
$this->loggingService->logInteraction($message, $response, $context);
return $response;
}
}
class ConversationManager
{
public function getConversationHistory(string $conversationId, int $limit = 10): array
{
return Conversation::where('id', $conversationId)
->with(['messages' => function ($query) use ($limit) {
$query->latest()->take($limit);
}])
->first()
?->messages
?->reverse()
?->values()
?->toArray() ?? [];
}
public function updateConversation(array $response, array $context): void
{
if (!isset($context['conversation_id'])) {
return;
}
$conversation = Conversation::firstOrCreate([
'id' => $context['conversation_id'],
], [
'user_id' => $context['user_id'] ?? null,
'started_at' => now(),
]);
$conversation->messages()->create([
'content' => $response['reply'],
'role' => 'assistant',
'metadata' => [
'model' => $response['model'] ?? null,
'tokens' => $response['tokens'] ?? null,
],
]);
$conversation->touch();
}
}Implement comprehensive error handling:
<?php
namespace App\Chatbot\Services;
use App\Chatbot\Exceptions\ChatbotException;
use App\Chatbot\Exceptions\RateLimitException;
use App\Chatbot\Exceptions\ContentFilterException;
class MessageProcessor
{
public function process(string $message, array $context = []): array
{
try {
return $this->performProcessing($message, $context);
} catch (RateLimitException $e) {
return $this->handleRateLimitError($e);
} catch (ContentFilterException $e) {
return $this->handleContentFilterError($e);
} catch (ChatbotException $e) {
return $this->handleChatbotError($e);
} catch (\Exception $e) {
\Log::error('Unexpected chatbot error', [
'message' => $message,
'context' => $context,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->handleGenericError($e);
}
}
private function handleRateLimitError(RateLimitException $e): array
{
return [
'reply' => 'You\'re sending messages too quickly. Please wait a moment and try again.',
'error' => true,
'error_type' => 'rate_limit',
'retry_after' => $e->getRetryAfter(),
];
}
private function handleContentFilterError(ContentFilterException $e): array
{
return [
'reply' => 'I cannot process that message due to content restrictions. Please rephrase your request.',
'error' => true,
'error_type' => 'content_filter',
];
}
private function handleGenericError(\Exception $e): array
{
return [
'reply' => 'I apologize, but I encountered an error processing your message. Please try again later.',
'error' => true,
'error_type' => 'internal_error',
];
}
}Use dependency injection for testability:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use RumenX\PhpChatbot\PhpChatbot;
use App\Chatbot\Services\ChatbotService;
class ChatbotServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PhpChatbot::class, function ($app) {
return new PhpChatbot([
'model' => config('chatbot.default_model'),
'api_key' => config("chatbot.models.{config('chatbot.default_model')}.api_key"),
'temperature' => config("chatbot.models.{config('chatbot.default_model')}.temperature"),
]);
});
$this->app->singleton(ChatbotService::class, function ($app) {
return new ChatbotService(
$app->make(ConversationManager::class),
$app->make(MessageProcessor::class),
$app->make(SecurityService::class),
$app->make(LoggingService::class)
);
});
}
}Always validate and sanitize user input:
<?php
namespace App\Chatbot\Middleware;
class InputValidationMiddleware
{
public function handle(string $message, \Closure $next): mixed
{
// Length validation
if (strlen($message) > config('chatbot.security.max_message_length', 2000)) {
throw new ContentFilterException('Message too long');
}
// Character validation
if (!mb_check_encoding($message, 'UTF-8')) {
throw new ContentFilterException('Invalid character encoding');
}
// HTML/Script tag detection
if (preg_match('/<script|<iframe|javascript:/i', $message)) {
throw new ContentFilterException('Potentially malicious content detected');
}
// Clean the message
$cleanMessage = $this->sanitizeMessage($message);
return $next($cleanMessage);
}
private function sanitizeMessage(string $message): string
{
// Remove potential XSS attempts
$message = strip_tags($message);
// Normalize whitespace
$message = preg_replace('/\s+/', ' ', trim($message));
// Remove null bytes
$message = str_replace("\0", '', $message);
return $message;
}
}<?php
namespace App\Chatbot\Middleware;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
class RateLimitMiddleware
{
public function handle(string $message, \Closure $next, array $context = []): mixed
{
$identifier = $this->getIdentifier($context);
$key = "chatbot_rate_limit:{$identifier}";
if (RateLimiter::tooManyAttempts($key, $this->maxAttempts())) {
$seconds = RateLimiter::availableIn($key);
throw new RateLimitException("Rate limit exceeded. Try again in {$seconds} seconds.", $seconds);
}
RateLimiter::hit($key, $this->decayMinutes() * 60);
$response = $next($message);
// Clear rate limit on successful response
if (!($response['error'] ?? false)) {
RateLimiter::clear($key);
}
return $response;
}
private function getIdentifier(array $context): string
{
return $context['user_id']
?? $context['ip_address']
?? $context['session_id']
?? 'anonymous';
}
private function maxAttempts(): int
{
return config('chatbot.security.rate_limit.requests', 20);
}
private function decayMinutes(): int
{
return config('chatbot.security.rate_limit.per_minutes', 1);
}
}Secure API key handling:
<?php
namespace App\Chatbot\Services;
use Illuminate\Encryption\Encrypter;
class ApiKeyManager
{
private Encrypter $encrypter;
public function __construct()
{
$this->encrypter = new Encrypter(config('app.key'), 'AES-256-CBC');
}
public function getApiKey(string $provider): string
{
$encryptedKey = config("chatbot.models.{$provider}.api_key");
if (!$encryptedKey) {
throw new \InvalidArgumentException("API key not configured for provider: {$provider}");
}
// In production, decrypt from secure storage
return env("chatbot.api_keys.{$provider}") ?? $encryptedKey;
}
public function rotateApiKey(string $provider, string $newKey): void
{
// Encrypt and store new key
$encryptedKey = $this->encrypter->encrypt($newKey);
// Update configuration
config()->set("chatbot.models.{$provider}.api_key", $encryptedKey);
// Log key rotation
\Log::info("API key rotated for provider: {$provider}");
}
public function validateApiKey(string $provider, string $key): bool
{
try {
// Test the API key with a simple request
$testChatbot = new PhpChatbot([
'model' => $provider,
'api_key' => $key,
]);
$testChatbot->sendMessage('test', ['max_tokens' => 1]);
return true;
} catch (\Exception $e) {
\Log::warning("API key validation failed for {$provider}: {$e->getMessage()}");
return false;
}
}
}<?php
namespace App\Chatbot\Services;
class ContentFilterService
{
private array $blockedWords;
private array $patterns;
public function __construct()
{
$this->loadFilterRules();
}
public function filter(string $message): string
{
// Check for blocked words
if ($this->containsBlockedWords($message)) {
throw new ContentFilterException('Message contains inappropriate content');
}
// Check for suspicious patterns
if ($this->containsSuspiciousPatterns($message)) {
throw new ContentFilterException('Message contains suspicious patterns');
}
// Check for PII
$message = $this->filterPersonalInfo($message);
return $message;
}
private function containsBlockedWords(string $message): bool
{
$messageLower = strtolower($message);
foreach ($this->blockedWords as $word) {
if (strpos($messageLower, strtolower($word)) !== false) {
\Log::warning('Blocked word detected', ['word' => $word, 'message_hash' => md5($message)]);
return true;
}
}
return false;
}
private function containsSuspiciousPatterns(string $message): bool
{
foreach ($this->patterns as $pattern) {
if (preg_match($pattern, $message)) {
\Log::warning('Suspicious pattern detected', ['pattern' => $pattern, 'message_hash' => md5($message)]);
return true;
}
}
return false;
}
private function filterPersonalInfo(string $message): string
{
// Remove email addresses
$message = preg_replace('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', '[EMAIL]', $message);
// Remove phone numbers
$message = preg_replace('/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/', '[PHONE]', $message);
// Remove credit card numbers
$message = preg_replace('/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/', '[CREDIT_CARD]', $message);
// Remove SSN
$message = preg_replace('/\b\d{3}[-]?\d{2}[-]?\d{4}\b/', '[SSN]', $message);
return $message;
}
private function loadFilterRules(): void
{
$this->blockedWords = config('chatbot.content_filter.blocked_words', []);
$this->patterns = config('chatbot.content_filter.patterns', []);
}
}Implement multi-layer caching:
<?php
namespace App\Chatbot\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class CacheService
{
private const CACHE_PREFIX = 'chatbot:';
private const DEFAULT_TTL = 3600; // 1 hour
public function getCachedResponse(string $message, array $context = []): ?array
{
$cacheKey = $this->generateCacheKey($message, $context);
// Try L1 cache (memory/Redis)
if ($cached = $this->getFromL1Cache($cacheKey)) {
return $cached;
}
// Try L2 cache (database)
if ($cached = $this->getFromL2Cache($cacheKey)) {
// Store in L1 cache for next time
$this->storeInL1Cache($cacheKey, $cached, self::DEFAULT_TTL / 2);
return $cached;
}
return null;
}
public function cacheResponse(string $message, array $response, array $context = []): void
{
$cacheKey = $this->generateCacheKey($message, $context);
// Don't cache error responses
if ($response['error'] ?? false) {
return;
}
// Don't cache responses with user-specific data
if ($this->containsUserSpecificData($response)) {
return;
}
// Store in both cache layers
$this->storeInL1Cache($cacheKey, $response, self::DEFAULT_TTL);
$this->storeInL2Cache($cacheKey, $response, self::DEFAULT_TTL * 24); // 24 hours in DB
}
private function generateCacheKey(string $message, array $context): string
{
$keyData = [
'message' => $this->normalizeMessage($message),
'model' => $context['model'] ?? 'default',
'temperature' => $context['temperature'] ?? 0.7,
'language' => $context['language'] ?? 'en',
];
return self::CACHE_PREFIX . md5(json_encode($keyData));
}
private function normalizeMessage(string $message): string
{
// Remove user-specific information for better cache hits
$normalized = strtolower(trim($message));
// Replace common variations
$normalized = preg_replace('/\b(hi|hello|hey)\b/', 'greeting', $normalized);
$normalized = preg_replace('/\b(thanks?|thank you)\b/', 'thanks', $normalized);
return $normalized;
}
private function getFromL1Cache(string $key): ?array
{
return Cache::get($key);
}
private function storeInL1Cache(string $key, array $data, int $ttl): void
{
Cache::put($key, $data, $ttl);
}
private function getFromL2Cache(string $key): ?array
{
return \DB::table('chatbot_cache')
->where('cache_key', $key)
->where('expires_at', '>', now())
->value('data');
}
private function storeInL2Cache(string $key, array $data, int $ttl): void
{
\DB::table('chatbot_cache')->updateOrInsert(
['cache_key' => $key],
[
'data' => json_encode($data),
'expires_at' => now()->addSeconds($ttl),
'updated_at' => now(),
]
);
}
}Optimize database queries and structure:
-- Conversations table
CREATE TABLE conversations (
id VARCHAR(36) PRIMARY KEY,
user_id INT UNSIGNED NULL,
started_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
INDEX idx_user_updated (user_id, updated_at),
INDEX idx_started_at (started_at)
);
-- Messages table
CREATE TABLE messages (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
conversation_id VARCHAR(36) NOT NULL,
role ENUM('user', 'assistant') NOT NULL,
content TEXT NOT NULL,
metadata JSON NULL,
created_at TIMESTAMP NOT NULL,
INDEX idx_conversation_created (conversation_id, created_at),
INDEX idx_role_created (role, created_at),
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);
-- Chatbot cache table
CREATE TABLE chatbot_cache (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
cache_key VARCHAR(255) UNIQUE NOT NULL,
data JSON NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
INDEX idx_cache_key_expires (cache_key, expires_at),
INDEX idx_expires_at (expires_at)
);
-- Analytics table
CREATE TABLE chatbot_analytics (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
conversation_id VARCHAR(36) NOT NULL,
model_used VARCHAR(50) NOT NULL,
tokens_used INT UNSIGNED NULL,
response_time_ms INT UNSIGNED NULL,
user_satisfaction TINYINT NULL,
created_at TIMESTAMP NOT NULL,
INDEX idx_conversation_created (conversation_id, created_at),
INDEX idx_model_created (model_used, created_at),
INDEX idx_created_at (created_at)
);Optimize queue processing:
<?php
namespace App\Chatbot\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessChatMessage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Job configuration
public int $timeout = 60; // 60 seconds timeout
public int $tries = 3; // Retry failed jobs 3 times
public int $backoff = 10; // Wait 10 seconds between retries
public function __construct(
private string $message,
private array $context
) {
// Use different queues based on priority
$this->onQueue($this->determineQueue($context));
}
public function handle(ChatbotService $chatbotService): void
{
$startTime = microtime(true);
try {
$response = $chatbotService->processMessage($this->message, $this->context);
// Broadcast response
$this->broadcastResponse($response);
// Record metrics
$this->recordMetrics($response, microtime(true) - $startTime);
} catch (\Exception $e) {
// Log error and increment failure count
\Log::error('Chat message processing failed', [
'message' => $this->message,
'context' => $this->context,
'attempt' => $this->attempts(),
'error' => $e->getMessage(),
]);
// Broadcast error if final attempt
if ($this->attempts() >= $this->tries) {
$this->broadcastError($e->getMessage());
}
throw $e; // Re-throw to trigger retry
}
}
private function determineQueue(array $context): string
{
// Premium users get priority queue
if ($context['user_tier'] === 'premium') {
return 'chatbot-priority';
}
// Support requests get dedicated queue
if ($context['type'] === 'support') {
return 'chatbot-support';
}
return 'chatbot-default';
}
}Production environment setup:
# .env.production
APP_ENV=production
APP_DEBUG=false
# Chatbot Configuration
CHATBOT_DEFAULT_MODEL=openai
CHATBOT_CACHE_DRIVER=redis
CHATBOT_QUEUE_CONNECTION=redis
# Rate Limiting
CHATBOT_RATE_LIMIT_REQUESTS=50
CHATBOT_RATE_LIMIT_MINUTES=1
# Security
CHATBOT_CONTENT_FILTERING=true
CHATBOT_MAX_MESSAGE_LENGTH=2000
# Performance
CHATBOT_RESPONSE_CACHE_TTL=3600
CHATBOT_CONVERSATION_MEMORY=true
CHATBOT_MAX_CONVERSATION_HISTORY=50
# Monitoring
CHATBOT_LOGGING=true
CHATBOT_ANALYTICS=true
CHATBOT_ERROR_REPORTING=true# Dockerfile
FROM php:8.2-fpm-alpine
# Install dependencies
RUN apk add --no-cache \
nginx \
supervisor \
redis \
curl \
&& docker-php-ext-install \
pdo_mysql \
bcmath \
opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Configure PHP
COPY docker/php.ini /usr/local/etc/php/conf.d/99-app.ini
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# Configure Nginx
COPY docker/nginx.conf /etc/nginx/nginx.conf
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Copy application
WORKDIR /var/www/html
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Set permissions
RUN chown -R www-data:www-data /var/www/html
EXPOSE 80
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "80:80"
environment:
- APP_ENV=production
- DB_HOST=database
- REDIS_HOST=redis
depends_on:
- database
- redis
volumes:
- ./storage:/var/www/html/storage
database:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: chatbot
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
queue:
build: .
command: php artisan queue:work --queue=chatbot-priority,chatbot-support,chatbot-default
depends_on:
- database
- redis
environment:
- APP_ENV=production
- DB_HOST=database
- REDIS_HOST=redis
volumes:
mysql_data:
redis_data:Nginx load balancer configuration:
upstream chatbot_backend {
least_conn;
server app1:80 max_fails=3 fail_timeout=30s;
server app2:80 max_fails=3 fail_timeout=30s;
server app3:80 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name api.chatbot.com;
location /api/chatbot {
proxy_pass http://chatbot_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeout settings
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Rate limiting
limit_req zone=chatbot_api burst=20 nodelay;
}
}
# Rate limiting configuration
http {
limit_req_zone $binary_remote_addr zone=chatbot_api:10m rate=10r/s;
}<?php
namespace App\Chatbot\Services;
use Illuminate\Support\Facades\Log;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
class LoggingService
{
private Logger $chatbotLogger;
public function __construct()
{
$this->chatbotLogger = new Logger('chatbot');
$this->chatbotLogger->pushHandler(
new RotatingFileHandler(storage_path('logs/chatbot.log'), 30)
);
}
public function logInteraction(string $message, array $response, array $context): void
{
$this->chatbotLogger->info('Chat interaction', [
'message_hash' => md5($message),
'message_length' => strlen($message),
'response_length' => strlen($response['reply'] ?? ''),
'model_used' => $response['model'] ?? null,
'tokens_used' => $response['tokens'] ?? null,
'response_time' => $response['response_time'] ?? null,
'conversation_id' => $context['conversation_id'] ?? null,
'user_id' => $context['user_id'] ?? null,
'user_agent' => $context['user_agent'] ?? null,
'ip_address' => $context['ip_address'] ?? null,
]);
}
public function logError(string $message, \Exception $exception, array $context): void
{
$this->chatbotLogger->error('Chat error', [
'message_hash' => md5($message),
'error_message' => $exception->getMessage(),
'error_class' => get_class($exception),
'error_file' => $exception->getFile(),
'error_line' => $exception->getLine(),
'context' => $context,
'trace' => $exception->getTraceAsString(),
]);
}
public function logPerformance(array $metrics): void
{
$this->chatbotLogger->info('Performance metrics', $metrics);
}
}<?php
namespace App\Http\Controllers;
use App\Chatbot\Services\ChatbotService;
use Illuminate\Http\JsonResponse;
class HealthController extends Controller
{
public function chatbotHealth(ChatbotService $chatbotService): JsonResponse
{
$checks = [
'database' => $this->checkDatabase(),
'redis' => $this->checkRedis(),
'api_keys' => $this->checkApiKeys(),
'queue' => $this->checkQueue(),
'ai_providers' => $this->checkAiProviders($chatbotService),
];
$healthy = !in_array(false, array_values($checks));
return response()->json([
'status' => $healthy ? 'healthy' : 'unhealthy',
'checks' => $checks,
'timestamp' => now()->toISOString(),
], $healthy ? 200 : 503);
}
private function checkDatabase(): bool
{
try {
\DB::connection()->getPdo();
return true;
} catch (\Exception $e) {
return false;
}
}
private function checkRedis(): bool
{
try {
\Redis::ping();
return true;
} catch (\Exception $e) {
return false;
}
}
private function checkApiKeys(): bool
{
$providers = config('chatbot.models');
foreach ($providers as $provider => $config) {
if (empty($config['api_key'])) {
return false;
}
}
return true;
}
private function checkQueue(): bool
{
try {
$size = \Queue::size('chatbot-default');
return $size !== false;
} catch (\Exception $e) {
return false;
}
}
private function checkAiProviders(ChatbotService $chatbotService): array
{
$providers = ['openai', 'anthropic'];
$results = [];
foreach ($providers as $provider) {
try {
// Test with a simple request
$startTime = microtime(true);
$response = $chatbotService->testProvider($provider);
$responseTime = (microtime(true) - $startTime) * 1000;
$results[$provider] = [
'status' => 'healthy',
'response_time_ms' => round($responseTime, 2),
];
} catch (\Exception $e) {
$results[$provider] = [
'status' => 'unhealthy',
'error' => $e->getMessage(),
];
}
}
return $results;
}
}<?php
namespace App\Chatbot\Services;
use Illuminate\Support\Facades\DB;
class MetricsService
{
public function recordInteraction(array $data): void
{
DB::table('chatbot_analytics')->insert([
'conversation_id' => $data['conversation_id'],
'model_used' => $data['model'],
'tokens_used' => $data['tokens'] ?? null,
'response_time_ms' => $data['response_time_ms'] ?? null,
'user_satisfaction' => $data['satisfaction'] ?? null,
'created_at' => now(),
]);
}
public function getDailyMetrics(\DateTime $date): array
{
$startOfDay = $date->format('Y-m-d 00:00:00');
$endOfDay = $date->format('Y-m-d 23:59:59');
return [
'total_interactions' => $this->getTotalInteractions($startOfDay, $endOfDay),
'unique_conversations' => $this->getUniqueConversations($startOfDay, $endOfDay),
'average_response_time' => $this->getAverageResponseTime($startOfDay, $endOfDay),
'model_usage' => $this->getModelUsage($startOfDay, $endOfDay),
'error_rate' => $this->getErrorRate($startOfDay, $endOfDay),
'user_satisfaction' => $this->getAverageSatisfaction($startOfDay, $endOfDay),
];
}
private function getTotalInteractions(string $start, string $end): int
{
return DB::table('chatbot_analytics')
->whereBetween('created_at', [$start, $end])
->count();
}
private function getUniqueConversations(string $start, string $end): int
{
return DB::table('chatbot_analytics')
->whereBetween('created_at', [$start, $end])
->distinct('conversation_id')
->count();
}
private function getAverageResponseTime(string $start, string $end): float
{
return DB::table('chatbot_analytics')
->whereBetween('created_at', [$start, $end])
->whereNotNull('response_time_ms')
->avg('response_time_ms') ?: 0;
}
private function getModelUsage(string $start, string $end): array
{
return DB::table('chatbot_analytics')
->whereBetween('created_at', [$start, $end])
->groupBy('model_used')
->selectRaw('model_used, count(*) as usage_count')
->pluck('usage_count', 'model_used')
->toArray();
}
}PHPStan configuration (phpstan.neon):
parameters:
level: 8
paths:
- app/Chatbot
excludePaths:
- app/Chatbot/vendor/*
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
- '#Call to an undefined method Illuminate\\Database\\Query\\Builder::#'PHP CS Fixer configuration (.php-cs-fixer.php):
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/app/Chatbot')
->name('*.php')
->notName('*.blade.php');
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
])
->setFinder($finder);<?php
namespace App\Chatbot\Services;
/**
* ChatbotService handles the core chatbot functionality including
* message processing, conversation management, and response generation.
*
* @package App\Chatbot\Services
*/
class ChatbotService
{
/**
* Process a user message and generate an appropriate response.
*
* This method handles the complete flow of message processing including:
* - Security validation and content filtering
* - Message preprocessing and context building
* - AI model interaction
* - Response post-processing
* - Conversation state management
* - Logging and analytics
*
* @param string $message The user's input message
* @param array<string, mixed> $context Additional context data including:
* - user_id: ID of the user sending the message
* - conversation_id: Unique conversation identifier
* - ip_address: User's IP address for rate limiting
* - user_agent: Browser/client information
* - language: Preferred language code
* - model: AI model to use (optional)
*
* @return array{
* reply: string,
* conversation_id: string,
* model: string,
* tokens?: int,
* response_time_ms?: float,
* cached?: bool,
* error?: bool,
* error_type?: string
* } The processed response with metadata
*
* @throws RateLimitException When rate limits are exceeded
* @throws ContentFilterException When message violates content policy
* @throws ChatbotException When processing fails
*
* @example
* ```php
* $response = $chatbotService->processMessage(
* 'Hello, how can you help me?',
* [
* 'user_id' => 123,
* 'conversation_id' => 'conv_abc123',
* 'language' => 'en'
* ]
* );
* echo $response['reply']; // "Hello! I'm here to help..."
* ```
*/
public function processMessage(string $message, array $context = []): array
{
// Implementation...
}
}<?php
namespace Tests\Unit\Chatbot;
use Tests\TestCase;
use App\Chatbot\Services\ChatbotService;
use App\Chatbot\Services\MessageProcessor;
use App\Chatbot\Exceptions\RateLimitException;
use Mockery\MockInterface;
class ChatbotServiceTest extends TestCase
{
private ChatbotService $chatbotService;
private MockInterface $messageProcessor;
protected function setUp(): void
{
parent::setUp();
$this->messageProcessor = $this->mock(MessageProcessor::class);
$this->chatbotService = new ChatbotService(
$this->messageProcessor,
$this->mock(ConversationManager::class),
$this->mock(SecurityService::class),
$this->mock(LoggingService::class)
);
}
/** @test */
public function it_processes_a_simple_message(): void
{
$message = 'Hello, world!';
$expectedResponse = [
'reply' => 'Hello! How can I help you?',
'conversation_id' => 'conv_123',
];
$this->messageProcessor
->shouldReceive('process')
->once()
->with($message, [])
->andReturn($expectedResponse);
$response = $this->chatbotService->processMessage($message);
$this->assertEquals($expectedResponse, $response);
}
/** @test */
public function it_handles_rate_limit_exceptions(): void
{
$message = 'Test message';
$this->messageProcessor
->shouldReceive('process')
->once()
->andThrow(new RateLimitException('Rate limit exceeded', 60));
$response = $this->chatbotService->processMessage($message);
$this->assertTrue($response['error']);
$this->assertEquals('rate_limit', $response['error_type']);
$this->assertStringContains('too quickly', $response['reply']);
}
/** @test */
public function it_validates_message_length(): void
{
$longMessage = str_repeat('a', 3000); // Exceeds max length
$this->expectException(ContentFilterException::class);
$this->chatbotService->processMessage($longMessage);
}
}<?php
namespace Tests\Integration\Chatbot;
use Tests\TestCase;
use App\Chatbot\Services\ChatbotService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
class ChatbotIntegrationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_processes_a_complete_conversation_flow(): void
{
// Mock external API calls
Http::fake([
'api.openai.com/*' => Http::response([
'choices' => [
['message' => ['content' => 'Hello! How can I help you today?']]
],
'usage' => ['total_tokens' => 25]
])
]);
$chatbotService = app(ChatbotService::class);
// First message
$response1 = $chatbotService->processMessage('Hello', [
'user_id' => 1,
'conversation_id' => 'conv_test_123'
]);
$this->assertArrayHasKey('reply', $response1);
$this->assertArrayHasKey('conversation_id', $response1);
$this->assertEquals('conv_test_123', $response1['conversation_id']);
// Second message in same conversation
$response2 = $chatbotService->processMessage('How are you?', [
'user_id' => 1,
'conversation_id' => 'conv_test_123'
]);
$this->assertArrayHasKey('reply', $response2);
$this->assertEquals('conv_test_123', $response2['conversation_id']);
// Verify conversation is stored in database
$this->assertDatabaseHas('conversations', [
'id' => 'conv_test_123',
'user_id' => 1
]);
$this->assertDatabaseCount('messages', 4); // 2 user + 2 assistant messages
}
/** @test */
public function it_respects_rate_limits(): void
{
$chatbotService = app(ChatbotService::class);
$userId = 1;
$context = ['user_id' => $userId];
// Send messages up to rate limit
for ($i = 0; $i < config('chatbot.security.rate_limit.requests'); $i++) {
$response = $chatbotService->processMessage("Message {$i}", $context);
$this->assertFalse($response['error'] ?? false);
}
// Next message should trigger rate limit
$response = $chatbotService->processMessage('Rate limited message', $context);
$this->assertTrue($response['error']);
$this->assertEquals('rate_limit', $response['error_type']);
}
}<?php
namespace Tests\Feature\Chatbot;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
class ChatbotApiTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_accepts_chat_messages_via_api(): void
{
Queue::fake();
$response = $this->postJson('/api/chatbot/message', [
'message' => 'Hello, chatbot!',
'conversation_id' => 'conv_api_test'
]);
$response->assertStatus(200)
->assertJsonStructure([
'reply',
'conversation_id',
'timestamp'
]);
Queue::assertPushed(ProcessChatMessage::class);
}
/** @test */
public function it_validates_api_input(): void
{
$response = $this->postJson('/api/chatbot/message', [
'message' => '', // Empty message
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['message']);
}
/** @test */
public function it_handles_authentication_when_required(): void
{
config(['chatbot.require_auth' => true]);
$response = $this->postJson('/api/chatbot/message', [
'message' => 'Hello'
]);
$response->assertStatus(401);
// Test with authenticated user
$user = \App\Models\User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/chatbot/message', [
'message' => 'Hello'
]);
$response->assertStatus(200);
}
}These best practices provide a comprehensive guide for building, deploying, and maintaining production-ready chatbot applications with php-chatbot. Following these guidelines will help ensure your implementation is secure, performant, and maintainable.
Next recommended reading: Contributing to learn how to contribute to the project.