Code Architecture & Optimization Guide
Last Updated: 2026-01-17 Status: Implemented Plan Reference: 103-controller-size-optimization.md, 104-database-query-optimization.md, 107-n-plus-one-query-optimization.md, 108-controller-decomposition.md, 109-service-consolidation.md, 110-dead-code-cleanup.md, 111-large-service-refactoring.md
Overview
This guide documents the code architecture patterns, optimizations, and refactoring approaches used in the client portal application. It serves as a reference for maintaining code quality and performance standards.
Table of Contents
- Controller Architecture
- Service Architecture
- Database Query Optimization
- Code Maintenance
- Design Patterns Used
Controller Architecture
Controller Size Guidelines
Controllers should remain focused on HTTP concerns. The codebase follows these principles:
| Responsibility | Controller | Service |
|---|---|---|
| Validate input | Yes (Form Requests) | No |
| Authorize actions | Yes | No |
| Call business logic | Yes (delegate) | Yes (implement) |
| Return responses | Yes | No |
| Complex calculations | No | Yes |
| External API calls | No | Yes |
Controller Decomposition Pattern
Large controllers are split into specialized controllers by domain concern:
Example: FileController Split
FileController (241 lines) → Core file operations
FileVersionController (new) → Version management
FileAnnotationController (new) → Annotations
FilePreviewController (new) → Preview generation
Example: WorkflowController Split
WorkflowController (323 lines) → CRUD, management
WorkflowExecutionController (new) → Test, manual trigger, execution list
WorkflowDebugController (new) → Execution debugging
WorkflowAnalyticsController (new) → Analytics dashboard
WorkflowTemplateController (new) → Template management
Form Request Extraction
Validation rules are extracted from controllers to Form Request classes:
Location: app/Http/Requests/
// Before: Inline validation in controller
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
// ... 17 more rules
]);
}
// After: Form Request class
public function store(StoreWorkflowRequest $request): RedirectResponse
{
$validated = $request->validated();
}
Benefits:
- Single source of truth for validation rules
- Authorization check in form request
- Custom error messages
- Reusable validation logic
Code Generation Service
API code example generation extracted from controller to service:
Location: app/Services/Api/CodeGeneratorService.php
class CodeGeneratorService
{
public function generateAll(string $method, string $path, string $baseUrl): array;
public function generateCurl(string $method, string $path, string $baseUrl): string;
public function generateJavaScript(string $method, string $path, string $baseUrl): string;
public function generatePhp(string $method, string $path, string $baseUrl): string;
public function generatePython(string $method, string $path, string $baseUrl): string;
}
Service Architecture
Service Consolidation
The codebase uses a shared statistics service to eliminate duplicate calculations:
Location: app/Services/Statistics/StatsCalculatorService.php
class StatsCalculatorService
{
public function invoiceStats(?Collection $clientIds = null): array;
public function projectStats(?Collection $clientIds = null): array;
public function monthlyRevenue(?Collection $clientIds = null, ?int $month = null, ?int $year = null): float;
public function revenueForPeriod(string $startDate, string $endDate, ?Collection $clientIds = null): float;
}
Usage:
// Admin dashboard (all clients)
$stats = $this->statsCalculator->invoiceStats();
// Portal dashboard (user's clients only)
$clientIds = $user->clients->pluck('id');
$stats = $this->statsCalculator->invoiceStats($clientIds);
Date Range Scopes Trait
Location: app/Models/Concerns/HasDateRangeScopes.php
trait HasDateRangeScopes
{
public function scopeThisMonth(Builder $query, string $column = 'created_at'): Builder;
public function scopeThisYear(Builder $query, string $column = 'created_at'): Builder;
public function scopeLastDays(Builder $query, int $days, string $column = 'created_at'): Builder;
public function scopeDateRange(Builder $query, string $start, string $end, string $column = 'created_at'): Builder;
public function scopeForMonth(Builder $query, int $month, int $year, string $column = 'created_at'): Builder;
}
Usage:
Invoice::thisMonth('paid_at')->sum('total');
ActivityLog::lastDays(30)->count();
Project::forMonth(1, 2026)->get();
Large Service Refactoring
Five large service classes were refactored using Strategy and Composition patterns:
| Service | Before | After | Pattern |
|---|---|---|---|
| DrillDownService | 787 lines | 174 lines max | Composition + Strategy |
| FilePreviewService | 482 lines | 148 lines max | Strategy |
| AnalyticsExportService | 456 lines | 201 lines max | Strategy |
| WebhookTemplateService | 476 lines | 201 lines max | Composition |
| CustomReportBuilder | 468 lines | 177 lines max | Composition |
DrillDown Service Architecture
Location: app/Services/Analytics/DrillDown/
DrillDownService.php → Coordinator (123 lines)
DrillDownQueryBuilder.php → Query building (115 lines)
DrillDownFormatter.php → Output formatting (53 lines)
HierarchyDrillDownInterface.php → Interface contract
Hierarchies/
├── RevenueDrillDown.php → Revenue hierarchy
├── ClientDrillDown.php → Client hierarchy
├── ProjectDrillDown.php → Project hierarchy
├── GeographyDrillDown.php → Geography hierarchy
└── TimeDrillDown.php → Time hierarchy
File Preview Service Architecture
Location: app/Services/Files/Preview/
FilePreviewService.php → Coordinator
PreviewGeneratorFactory.php → Factory
PreviewGeneratorInterface.php → Interface
Generators/
├── AudioPreviewGenerator.php
├── CodePreviewGenerator.php
├── DocumentPreviewGenerator.php
├── ImagePreviewGenerator.php
├── PdfPreviewGenerator.php
├── SpreadsheetPreviewGenerator.php
├── TextPreviewGenerator.php
└── VideoPreviewGenerator.php
Analytics Export Service Architecture
Location: app/Services/Analytics/Export/
AnalyticsExportService.php → Coordinator
ExporterFactory.php → Factory
ExporterInterface.php → Interface
Exporters/
├── CsvExporter.php
├── ExcelExporter.php
├── JsonExporter.php
├── PdfExporter.php
├── PngExporter.php
├── PptxExporter.php (stub)
└── SvgExporter.php
Backward Compatibility
Original service files converted to facades that delegate to refactored implementations:
// app/Services/FilePreviewService.php (facade)
class FilePreviewService
{
public function __construct(
private \App\Services\Files\Preview\FilePreviewService $previewService
) {}
public function generatePreview(ProjectFile $file): array
{
return $this->previewService->generatePreview($file);
}
}
Database Query Optimization
Performance Indexes
Migration: database/migrations/2026_01_14_000001_add_performance_indexes.php
| Table | Index | Purpose |
|---|---|---|
project_files | [project_id, created_at] | Sorting files by date |
notifications | [notifiable_type, notifiable_id, read_at] | Unread notification queries |
activity_logs | [user_id, created_at] | User activity filtering |
invoices | [due_at, status] | Overdue invoice queries |
projects | [client_id, status] | Client project listings |
messages | [user_id, read_at] | Unread message counts |
generated_reports | [generated_by, created_at] | Report history queries |
N+1 Query Detection Trait
Location: tests/Concerns/DetectsNPlusOneQueries.php
use Tests\Concerns\DetectsNPlusOneQueries;
class ClientControllerTest extends TestCase
{
use DetectsNPlusOneQueries;
public function test_index_avoids_n_plus_one(): void
{
Client::factory()->count(20)->create();
$this->assertMaxQueries(10, function () {
return $this->actingAs($this->admin)
->get(route('admin.clients.index'));
});
}
}
Available Methods:
| Method | Description |
|---|---|
startQueryLog() | Begin recording queries |
stopQueryLog() | Stop recording queries |
assertMaxQueries($count, $callback) | Assert query limit |
assertQueryCount($expected) | Verify exact count |
getDuplicateQueries() | Find repeated queries (N+1 indicator) |
assertNoDuplicateQueries() | Strict N+1 check |
getSlowQueries($threshold) | Find slow queries |
assertNoSlowQueries($threshold) | Assert performance |
dumpQueries() | Debug helper |
Optimized Queries Trait
Location: app/Models/Concerns/OptimizedQueries.php
use App\Models\Concerns\OptimizedQueries;
class Client extends Model
{
use OptimizedQueries;
public function scopeWithIndexRelations(Builder $query): void
{
$query->withCount(['projects', 'invoices'])
->with(['primaryContact:id,name,email']);
}
}
Usage in Controller:
$clients = Client::withIndexRelations()->paginate(20);
Scopes Available:
| Scope | Purpose |
|---|---|
withIndexRelations() | Eager load for list pages |
withShowRelations() | Eager load for detail pages |
withEditRelations() | Eager load for edit forms |
forSelectOptions() | Minimal columns for dropdowns |
optimizedList() | Combined list optimization |
Static Helpers:
| Method | Purpose |
|---|---|
pluckForSelect() | Efficient dropdown data |
existsById() | Check existence without loading |
getColumnById() | Get single column value |
Bulk Operation Optimization
Before:
// N individual updates
foreach ($invoices as $invoice) {
$invoice->update($updateData);
}
After:
// Single batch update
Invoice::whereIn('id', $ids)->update($updateData);
// Chunked activity logging
$invoices->chunk(100)->each(function ($chunk) use ($updateData) {
foreach ($chunk as $invoice) {
$this->activityService->logStatusChanged(...);
}
});
N+1 Fix Pattern
Before:
$clients = Client::whereIn('id', $ids)->get();
foreach ($clients as $client) {
if ($client->projects()->exists() || $client->invoices()->exists()) {
// N+1: Two queries per client
}
}
After:
$clients = Client::whereIn('id', $ids)
->withCount(['projects', 'invoices'])
->get();
$protectedIds = $clients
->filter(fn($c) => $c->projects_count > 0 || $c->invoices_count > 0)
->pluck('id')
->toArray();
Query Reduction Results
| Area | Before | After |
|---|---|---|
| Bulk delete 50 clients | ~102 queries | ~3 queries |
| Bulk update 100 invoices | ~101 queries | ~2 queries |
| Compliance dashboard | ~8 queries | ~4 queries |
| Portal dashboard | ~12 queries | ~8 queries |
Slow Query Logging
Configuration: config/caching.php
'query_logging' => [
'enabled' => env('QUERY_LOGGING_ENABLED', false),
'slow_threshold_ms' => env('QUERY_SLOW_THRESHOLD_MS', 100),
],
Enable in .env:
QUERY_LOGGING_ENABLED=true
QUERY_SLOW_THRESHOLD_MS=50
Code Maintenance
Dead Code Cleanup
The following code maintenance was performed:
-
TODO Comments Removed
- Removed stale TODO comments and commented-out code
-
Placeholder Implementations Documented
- Updated docblocks to explain intentionally unimplemented stubs
-
Defensive
class_existsChecks Removed- Removed unnecessary checks for models that now exist
Files Modified:
SharedReportController.php- Removed TODO and commented notification codeAdminSearchService.php- Updated docblock for placeholderCacheMonitorService.php- Converted inline comment to docblockPptxExporter.php- Added stub documentationAdminToolsController.php- Removed class_exists checkComplianceDashboardController.php- Removed ternary checkCacheWarmerService.php- Removed if wrapperSecurityAuditReport.php- Removed multiple class_exists checksDashboardDataService.php- Simplified returns
Legitimate Dynamic Class Resolution
Some class_exists usages are legitimate for dynamic resolution:
| File | Purpose |
|---|---|
ReindexCommand.php | Dynamic search model resolution |
WorkflowExecutionController.php | User-provided model validation |
IndexingService.php | Dynamic indexable model validation |
WebhookService.php | Dynamic resource class resolution |
Design Patterns Used
Strategy Pattern
Used for pluggable implementations with common interface:
| Service | Strategy Use |
|---|---|
| FilePreviewService | Each file type has dedicated generator |
| AnalyticsExportService | Each export format has dedicated exporter |
| DrillDownService | Each hierarchy implements interface |
Example:
interface PreviewGeneratorInterface
{
public function supports(string $mimeType): bool;
public function generate(ProjectFile $file): array;
}
class ImagePreviewGenerator implements PreviewGeneratorInterface
{
public function supports(string $mimeType): bool
{
return str_starts_with($mimeType, 'image/');
}
}
Composition Pattern
Used for separating concerns into focused components:
| Service | Components |
|---|---|
| WebhookTemplateService | BuiltInTemplates, TemplateValidator, PayloadTransformer |
| CustomReportBuilder | DataSourceRegistry, ReportDataProcessor, ReportQueryBuilder |
| DrillDownService | DrillDownQueryBuilder, DrillDownFormatter, Hierarchies |
Facade Pattern
Used for backward compatibility when refactoring:
// Original class becomes facade
class FilePreviewService
{
public function __construct(
private \App\Services\Files\Preview\FilePreviewService $previewService
) {}
// Delegates to refactored implementation
public function generatePreview(ProjectFile $file): array
{
return $this->previewService->generatePreview($file);
}
}
Testing
Query Optimization Tests
Location: tests/Feature/QueryOptimizationTest.php
Tests for:
- Client index N+1 prevention
- Project index N+1 prevention
- Invoice index N+1 prevention
- Dashboard efficiency
- Client show eager loading
- Activity log optimization
- Query count scaling with pagination
Running Tests
# Run query optimization tests
php artisan test tests/Feature/QueryOptimizationTest.php
# Run with coverage
php artisan test --coverage tests/Feature/QueryOptimizationTest.php
Best Practices
When to Split Controllers
- Different authorization requirements between endpoint groups
- Distinct functional domains becoming mixed
- Complex business logic appearing in controller methods
- Controller exceeds 400+ lines with embedded logic
When to Use Services
- Logic involves multiple models
- Logic is reused across controllers
- Complex business rules need encapsulation
- External API calls are involved
- Logic requires transaction handling
When to Add N+1 Tests
- Index/list pages with relationships
- Dashboard pages with aggregations
- Bulk operation endpoints
- Pages with nested relationship display