Architecture & Design Decisions
This project follows a clear hierarchy of principles:
- Laravel conventions - We follow Laravel's established patterns and best practices
- PHP standards - We adhere to PHP-FIG standards (PSR-4, PSR-12)
- Clean code principles - SOLID principles applied pragmatically, avoiding over-engineering
🌙 Laravel Conventions We Follow
Directory Structure
We follow the standard Laravel directory structure:
app/Http/Controllers/
- HTTP controllersapp/Services/
- Business logic servicesroutes/api.php
- API route definitionsconfig/
- Configuration filestests/
- Unit and feature tests
Service Container & Dependency Injection
We use Laravel's Service Container for dependency injection:
public function __construct(
private TelegramChannelService $telegramService,
private MessageService $messageService,
) {}
Service Providers
Custom services are registered in Service Providers:
// app/Providers/TelegramServiceProvider.php
$this->app->singleton(TelegramApiInterface::class, function ($app) {
return new MadelineProtoApiClient();
});
Route Model Binding
We use implicit binding in routes:
Route::get('/channels/{channel}/messages/last-id', ...)
Caching
We use Laravel's Cache facade:
Cache::put($cacheKey, $data, $ttl);
$cachedData = Cache::get($cacheKey);
⚙️ PHP Standards & Best Practices
PSR Standards
- PSR-4: Autoloading -
App\
namespace maps toapp/
directory - PSR-12: Coding style - Enforced by Laravel Pint
- PSR-7: HTTP messages - Used implicitly through Laravel
Type Declarations
We use PHP 8+ type declarations everywhere:
public function getLastMessageId(string $channel): ?int
{
// Return type is nullable int
}
Constructor Property Promotion
Using PHP 8's constructor property promotion for cleaner code:
public function __construct(
private TelegramChannelService $telegramService,
private MessageService $messageService,
) {}
✨ Our Design Decisions
Request/Response Objects Pattern
Instead of using arrays or stdClass, we use dedicated Request and Response objects:
// Custom Request Objects
app/Http/Requests/Api/V2/GetLastMessageRequest.php
app/Http/Requests/Api/V2/GetStatisticsRequest.php
// Custom Response Objects
app/Http/Responses/Api/V2/LastMessageResponse.php
app/Http/Responses/Api/V2/ErrorResponse.php
Why? Type safety, IDE autocompletion, and cleaner controllers. Inspired by DTO pattern.
JSON:API Specification
We implement JSON:API v1.1 for v2 endpoints:
{
"data": {
"type": "channel-message",
"id": "channelname",
"attributes": {
"last_message_id": 12345
}
},
"meta": {
"timestamp": "2025-01-10T12:00:00Z",
"api_version": "v2"
},
"jsonapi": {
"version": "1.1"
}
}
Why? Standardized format, better for API consumers, supports relationships and includes.
Service Layer Pattern
Business logic is extracted into service classes:
MessageService
- Handles message fetching and cachingStatisticsService
- Processes channel statisticsStatisticsCalculator
- Complex statistics calculations
Why? Follows Service Layer pattern, keeps controllers thin, improves testability.
Minimal Interface Usage
We only use interfaces for external dependencies that might change:
// Only for the Telegram client that could be swapped
interface TelegramApiInterface {
public function getChannelInfo(string $channel): ?array;
public function getMessagesHistory(string $channel, array $params): array;
}
Why? Avoids over-engineering. Internal services don't need interfaces unless there's a real need for multiple implementations.
Controller Separation
Each controller has a single responsibility:
MessageController
- Message-related endpointsStatisticsController
- Statistics endpointsChannelInfoController
- Channel information
Why? Follows Single Responsibility Principle, easier to maintain and test.
🚫 What We Avoid (No Over-Engineering)
No Unnecessary Abstractions
- No repository pattern for simple cache operations
- No interfaces for services that have only one implementation
- No abstract classes unless there's real shared behavior
- No design patterns just for the sake of using them
YAGNI Principle
"You Aren't Gonna Need It" - We don't add features or abstractions for hypothetical future needs:
- No multi-database support (just use Laravel's config)
- No plugin system
- No event sourcing or CQRS
- No microservices architecture for a simple API
Pragmatic SOLID
We apply SOLID principles where they add value, not dogmatically:
// Good: Service handles one clear responsibility
class MessageService {
public function getLastMessageId(string $channel): ?int
// Overkill: Interface for a service with one implementation
interface MessageServiceInterface // ❌ We don't need this
🔬 Testing Strategy
Unit Tests
Service classes are tested in isolation using Mockery:
$this->apiClient = Mockery::mock(TelegramApiInterface::class);
$this->apiClient->shouldReceive('getChannelInfo')
->with('@' . $channelUsername)
->once()
->andReturn(['type' => 'channel']);
Feature Tests
API endpoints are tested using Laravel's HTTP tests:
$response = $this->getJson("/api/v2/telegram/channels/{$channel}/messages/last-id");
$response->assertStatus(200)
->assertJsonStructure(['data', 'meta', 'jsonapi']);
⚡ Performance Optimizations
Caching Strategy
- Message IDs: 5 minutes cache
- Statistics: 1 hour cache
- Cache keys include parameters for granular invalidation
Rate Limiting
Different limits for different endpoints:
$channelMiddleware = ['throttle:60,1']; // 60 requests per minute
$statsMiddleware = ['throttle:10,60']; // 10 requests per hour