Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

System & Infrastructure/Code Architecture

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

  1. Controller Architecture
  2. Service Architecture
  3. Database Query Optimization
  4. Code Maintenance
  5. Design Patterns Used

Controller Architecture

Controller Size Guidelines

Controllers should remain focused on HTTP concerns. The codebase follows these principles:

ResponsibilityControllerService
Validate inputYes (Form Requests)No
Authorize actionsYesNo
Call business logicYes (delegate)Yes (implement)
Return responsesYesNo
Complex calculationsNoYes
External API callsNoYes

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:

ServiceBeforeAfterPattern
DrillDownService787 lines174 lines maxComposition + Strategy
FilePreviewService482 lines148 lines maxStrategy
AnalyticsExportService456 lines201 lines maxStrategy
WebhookTemplateService476 lines201 lines maxComposition
CustomReportBuilder468 lines177 lines maxComposition

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

TableIndexPurpose
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:

MethodDescription
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:

ScopePurpose
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:

MethodPurpose
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

AreaBeforeAfter
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:

  1. TODO Comments Removed

    • Removed stale TODO comments and commented-out code
  2. Placeholder Implementations Documented

    • Updated docblocks to explain intentionally unimplemented stubs
  3. Defensive class_exists Checks Removed

    • Removed unnecessary checks for models that now exist

Files Modified:

  • SharedReportController.php - Removed TODO and commented notification code
  • AdminSearchService.php - Updated docblock for placeholder
  • CacheMonitorService.php - Converted inline comment to docblock
  • PptxExporter.php - Added stub documentation
  • AdminToolsController.php - Removed class_exists check
  • ComplianceDashboardController.php - Removed ternary check
  • CacheWarmerService.php - Removed if wrapper
  • SecurityAuditReport.php - Removed multiple class_exists checks
  • DashboardDataService.php - Simplified returns

Legitimate Dynamic Class Resolution

Some class_exists usages are legitimate for dynamic resolution:

FilePurpose
ReindexCommand.phpDynamic search model resolution
WorkflowExecutionController.phpUser-provided model validation
IndexingService.phpDynamic indexable model validation
WebhookService.phpDynamic resource class resolution

Design Patterns Used

Strategy Pattern

Used for pluggable implementations with common interface:

ServiceStrategy Use
FilePreviewServiceEach file type has dedicated generator
AnalyticsExportServiceEach export format has dedicated exporter
DrillDownServiceEach 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:

ServiceComponents
WebhookTemplateServiceBuiltInTemplates, TemplateValidator, PayloadTransformer
CustomReportBuilderDataSourceRegistry, ReportDataProcessor, ReportQueryBuilder
DrillDownServiceDrillDownQueryBuilder, 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