Laravel广告统计组件OOP架构优化咨询(入门阶段)
Hey Ivan, great question—building maintainable, extensible OOP components in Laravel (especially for cron/CLI tasks) is tricky, but there’s a clear path to structure this properly. Let’s break down how to refactor your ad stats component for flexibility, testability, and future reuse:
First, strip your command class down to just a entry point—it shouldn’t contain calculation or data access logic. Move all core functionality to a dedicated service class, so the same logic can be reused in HTTP controllers, queue jobs, or any other context later.
For example, your CalculateAdStatsCommand should only handle:
- Parsing CLI arguments/options
- Calling the core service
- Outputting success/error messages
Since you need to support multiple stats types (CPM, EPC, and future ones) with varying calculation rules (SQL vs. PHP), the strategy pattern is perfect here. It enforces consistency while letting you add new logic without modifying existing code.
Step 2.1: Define a Strategy Interface
Create a contract for all calculation strategies:
// app/Contracts/AdStatsCalculationStrategy.php namespace App\Contracts; use App\Repositories\AdStatsRepositoryInterface; use App\DataTransferObjects\StatsContext; interface AdStatsCalculationStrategy { /** * Calculate stats using either SQL or PHP logic * @return array Calculated results ready for storage */ public function calculate(AdStatsRepositoryInterface $repository, StatsContext $context): array; }
Step 2.2: Implement Concrete Strategies
Build separate classes for each stats type. Prioritize SQL calculations where possible, and handle PHP-based logic in the strategy for edge cases:
// app/Strategies/CpmCalculationStrategy.php namespace App\Strategies; use App\Contracts\AdStatsCalculationStrategy; use App\Repositories\AdStatsRepositoryInterface; use App\DataTransferObjects\StatsContext; class CpmCalculationStrategy implements AdStatsCalculationStrategy { public function calculate(AdStatsRepositoryInterface $repository, StatsContext $context): array { // Use SQL for efficient calculation return $repository->computeCpm($context->getDateRange()); } }
// app/Strategies/EpcCalculationStrategy.php namespace App\Strategies; use App\Contracts\AdStatsCalculationStrategy; use App\Repositories\AdStatsRepositoryInterface; use App\DataTransferObjects\StatsContext; class EpcCalculationStrategy implements AdStatsCalculationStrategy { public function calculate(AdStatsRepositoryInterface $repository, StatsContext $context): array { // Fallback to PHP calculation if SQL isn't feasible $rawData = $repository->getRawConversionData($context->getDateRange()); return $this->computeEpcFromRawData($rawData); } private function computeEpcFromRawData(array $rawData): array { // Your PHP-based EPC calculation logic here } }
Step 2.3: Add a Strategy Factory
Create a factory to resolve the correct strategy based on the stats type—this keeps your service class clean and avoids conditional logic:
// app/Factories/AdStatsStrategyFactory.php namespace App\Factories; use App\Contracts\AdStatsCalculationStrategy; use App\Strategies\CpmCalculationStrategy; use App\Strategies\EpcCalculationStrategy; use InvalidArgumentException; class AdStatsStrategyFactory { public function make(string $statsType): AdStatsCalculationStrategy { return match ($statsType) { 'cpm' => new CpmCalculationStrategy(), 'epc' => new EpcCalculationStrategy(), default => throw new InvalidArgumentException("Unsupported stats type: {$statsType}"), }; } }
Wrap all database operations (fetching raw data, running SQL calculations, storing results) in a repository with an interface. This decouples your core logic from Eloquent, making testing easier and allowing you to swap data sources later.
Step 3.1: Define Repository Interface
// app/Repositories/AdStatsRepositoryInterface.php namespace App\Repositories; use App\DataTransferObjects\StatsContext; interface AdStatsRepositoryInterface { public function computeCpm(string $dateRange): array; public function getRawConversionData(string $dateRange): array; public function storeResults(string $statsType, array $results, StatsContext $context): void; // Add other data methods as needed }
Step 3.2: Implement Eloquent Repository
// app/Repositories/EloquentAdStatsRepository.php namespace App\Repositories; use App\Models\AdImpression; use App\Models\AdConversion; use App\Models\AdStats; use App\DataTransferObjects\StatsContext; class EloquentAdStatsRepository implements AdStatsRepositoryInterface { public function computeCpm(string $dateRange): array { // Efficient SQL query to calculate CPM return AdImpression::selectRaw('ad_id, (SUM(impressions) / 1000) * revenue AS cpm') ->whereDate('created_at', $dateRange) ->groupBy('ad_id') ->get() ->toArray(); } public function getRawConversionData(string $dateRange): array { return AdConversion::whereDate('created_at', $dateRange)->get()->toArray(); } public function storeResults(string $statsType, array $results, StatsContext $context): void { // Bulk insert or update stats records foreach ($results as $result) { AdStats::updateOrCreate( ['ad_id' => $result['ad_id'], 'stats_type' => $statsType, 'date' => $context->getDateRange()], ['value' => $result[$statsType]] ); } } }
Use a Data Transfer Object (DTO) to pass context like date ranges, ad filters, or storage options around. This keeps method signatures clean and ensures all components have consistent access to runtime parameters:
// app/DataTransferObjects/StatsContext.php namespace App\DataTransferObjects; class StatsContext { private string $dateRange; private ?array $adFilters = null; public function getDateRange(): string { return $this->dateRange; } public function setDateRange(string $dateRange): self { $this->dateRange = $dateRange; return $this; } // Add getter/setter for ad filters or other context params }
This class ties everything together—it uses the factory to get the right strategy, leverages the repository for data operations, and orchestrates the full stats workflow:
// app/Services/AdStatsService.php namespace App\Services; use App\Contracts\AdStatsCalculationStrategy; use App\Repositories\AdStatsRepositoryInterface; use App\Factories\AdStatsStrategyFactory; use App\DataTransferObjects\StatsContext; class AdStatsService { public function __construct( private AdStatsRepositoryInterface $repository, private AdStatsStrategyFactory $strategyFactory ) {} public function calculateAndStore(string $statsType, StatsContext $context): void { $strategy = $this->strategyFactory->make($statsType); $results = $strategy->calculate($this->repository, $context); $this->repository->storeResults($statsType, $results, $context); } }
Now your command class is lightweight and focused on CLI-specific concerns:
// app/Console/Commands/CalculateAdStatsCommand.php namespace App\Console\Commands; use Illuminate\Console\Command; use App\Services\AdStatsService; use App\DataTransferObjects\StatsContext; class CalculateAdStatsCommand extends Command { protected $signature = 'ad-stats:calculate {type : Stats type to compute (cpm, epc)} {--date= : Date range (YYYY-MM-DD)}'; protected $description = 'Calculate and store ad stats (CPM, EPC, etc.)'; public function handle(AdStatsService $service): int { $context = (new StatsContext()) ->setDateRange($this->option('date') ?? now()->subDay()->toDateString()); try { $service->calculateAndStore($this->argument('type'), $context); $this->info("Successfully computed and stored {$this->argument('type')} stats."); return Command::SUCCESS; } catch (\Exception $e) { $this->error("Failed to compute stats: {$e->getMessage()}"); return Command::FAILURE; } } }
- Reusability: The
AdStatsServicecan be injected anywhere (controllers, queue jobs, etc.)—no CLI-specific lock-in. - Extensibility: Add a new stats type by creating a new strategy class and updating the factory—no changes to existing code (follows the Open/Closed Principle).
- Testability: Mock the repository and strategy interfaces to write fast, isolated unit tests without hitting the database.
- Maintainability: Clear separation of concerns makes it easy to debug or modify individual parts (e.g., change how CPM is calculated without touching EPC logic).
内容的提问来源于stack exchange,提问作者Ivan Pavel

