🛸 Telegram Analytics API

Architecture & Design Decisions

This project follows a clear hierarchy of principles:

  1. Laravel conventions - We follow Laravel's established patterns and best practices
  2. PHP standards - We adhere to PHP-FIG standards (PSR-4, PSR-12)
  3. 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 controllers
  • app/Services/ - Business logic services
  • routes/api.php - API route definitions
  • config/ - Configuration files
  • tests/ - 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 to app/ 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 caching
  • StatisticsService - Processes channel statistics
  • StatisticsCalculator - 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 endpoints
  • StatisticsController - Statistics endpoints
  • ChannelInfoController - 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

🌠 Further Reading