Laravel 13 模板方法模式深度解析 模板方法模式是一种行为型设计模式,它在父类中定义算法的骨架,将某些步骤延迟到子类中实现。本文将深入探讨 Laravel 13 中模板方法模式的高级用法。
模板方法模式基础 什么是模板方法模式 模板方法模式定义了一个操作中的算法骨架,将一些步骤延迟到子类中,使得子类可以不改变算法结构即可重定义该算法的某些特定步骤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php namespace App \Contracts ;abstract class AbstractTemplate { final public function execute ( ): mixed { $this ->validate (); $this ->beforeExecute (); $result = $this ->doExecute (); $this ->afterExecute ($result ); return $result ; } abstract protected function validate ( ): void ; abstract protected function doExecute ( ): mixed ; protected function beforeExecute ( ): void { } protected function afterExecute (mixed $result ): void { } }
数据导出模板 导出模板基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <?php namespace App \Services \Export ;use Illuminate \Support \Collection ;abstract class DataExporter { protected Collection $data ; protected array $options = []; public function __construct (Collection $data , array $options = [] ) { $this ->data = $data ; $this ->options = $options ; } final public function export ( ): string { $this ->validateData (); $this ->prepareData (); $content = $this ->generateContent (); $this ->addMetadata ($content ); return $content ; } protected function validateData ( ): void { if ($this ->data->isEmpty ()) { throw new \InvalidArgumentException ('Data cannot be empty' ); } } protected function prepareData ( ): void { $this ->data = $this ->data->map (function ($item ) { return $this ->formatItem ($item ); }); } abstract protected function generateContent ( ): string ; protected function formatItem ($item ): array { return is_array ($item ) ? $item : $item ->toArray (); } protected function addMetadata (string &$content ): void { } abstract public function getMimeType ( ): string ; abstract public function getFileExtension ( ): string ; }
CSV 导出器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <?php namespace App \Services \Export ;class CsvExporter extends DataExporter { protected string $delimiter = ',' ; protected string $enclosure = '"' ; public function setDelimiter (string $delimiter ): self { $this ->delimiter = $delimiter ; return $this ; } protected function generateContent ( ): string { $output = fopen ('php://temp' , 'r+' ); $headers = $this ->getHeaders (); if ($headers ) { fputcsv ($output , $headers , $this ->delimiter, $this ->enclosure); } foreach ($this ->data as $row ) { fputcsv ($output , array_values ($row ), $this ->delimiter, $this ->enclosure); } rewind ($output ); $content = stream_get_contents ($output ); fclose ($output ); return $content ; } protected function getHeaders ( ): ?array { $firstItem = $this ->data->first (); return $firstItem ? array_keys ($firstItem ) : null ; } public function getMimeType ( ): string { return 'text/csv' ; } public function getFileExtension ( ): string { return 'csv' ; } }
JSON 导出器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php namespace App \Services \Export ;class JsonExporter extends DataExporter { protected int $jsonOptions = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE; public function setJsonOptions (int $options ): self { $this ->jsonOptions = $options ; return $this ; } protected function generateContent ( ): string { $structure = $this ->buildStructure (); return json_encode ($structure , $this ->jsonOptions); } protected function buildStructure ( ): array { return [ 'data' => $this ->data->values ()->toArray (), 'meta' => [ 'count' => $this ->data->count (), 'exported_at' => now ()->toIso8601String (), ], ]; } public function getMimeType ( ): string { return 'application/json' ; } public function getFileExtension ( ): string { return 'json' ; } }
Excel 导出器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 <?php namespace App \Services \Export ;use PhpOffice \PhpSpreadsheet \Spreadsheet ;use PhpOffice \PhpSpreadsheet \Writer \Xlsx ;class ExcelExporter extends DataExporter { protected string $sheetTitle = 'Sheet1' ; protected array $columnWidths = []; public function setSheetTitle (string $title ): self { $this ->sheetTitle = $title ; return $this ; } protected function generateContent ( ): string { $spreadsheet = new Spreadsheet (); $sheet = $spreadsheet ->getActiveSheet (); $sheet ->setTitle ($this ->sheetTitle); $this ->writeHeaders ($sheet ); $this ->writeData ($sheet ); $this ->applyStyles ($sheet ); $writer = new Xlsx ($spreadsheet ); $tempFile = tempnam (sys_get_temp_dir (), 'excel_' ); $writer ->save ($tempFile ); $content = file_get_contents ($tempFile ); unlink ($tempFile ); return $content ; } protected function writeHeaders ($sheet ): void { $headers = $this ->getHeaders (); if ($headers ) { $column = 1 ; foreach ($headers as $header ) { $sheet ->setCellValue ([$column , 1 ], $header ); $sheet ->getStyle ([$column , 1 ])->getFont ()->setBold (true ); $column ++; } } } protected function writeData ($sheet ): void { $row = 2 ; foreach ($this ->data as $item ) { $column = 1 ; foreach (array_values ($item ) as $value ) { $sheet ->setCellValue ([$column , $row ], $value ); $column ++; } $row ++; } } protected function applyStyles ($sheet ): void { foreach ($this ->columnWidths as $column => $width ) { $sheet ->getColumnDimensionByColumn ($column )->setWidth ($width ); } } protected function getHeaders ( ): ?array { $firstItem = $this ->data->first (); return $firstItem ? array_keys ($firstItem ) : null ; } public function getMimeType ( ): string { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ; } public function getFileExtension ( ): string { return 'xlsx' ; } }
报表生成模板 报表模板基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 <?php namespace App \Services \Reports ;use Illuminate \Support \Collection ;abstract class ReportGenerator { protected Collection $data ; protected array $parameters ; protected array $results = []; public function __construct (array $parameters = [] ) { $this ->parameters = $parameters ; } final public function generate ( ): array { $this ->validateParameters (); $this ->fetchData (); $this ->processData (); $this ->calculateMetrics (); $this ->formatResults (); return $this ->results; } protected function validateParameters ( ): void { foreach ($this ->getRequiredParameters () as $param ) { if (!isset ($this ->parameters[$param ])) { throw new \InvalidArgumentException ("Missing required parameter: {$param} " ); } } } abstract protected function getRequiredParameters ( ): array ; abstract protected function fetchData ( ): void ; protected function processData ( ): void { $this ->data = $this ->data->map (function ($item ) { return $this ->transformItem ($item ); }); } protected function transformItem ($item ) { return $item ; } abstract protected function calculateMetrics ( ): void ; protected function formatResults ( ): void { $this ->results = [ 'title' => $this ->getTitle (), 'generated_at' => now ()->toIso8601String (), 'data' => $this ->data->toArray (), 'metrics' => $this ->results['metrics' ] ?? [], 'summary' => $this ->results['summary' ] ?? '' , ]; } abstract protected function getTitle ( ): string ; }
销售报表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <?php namespace App \Services \Reports ;use App \Models \Order ;use Illuminate \Support \Facades \DB ;class SalesReportGenerator extends ReportGenerator { protected function getRequiredParameters ( ): array { return ['start_date' , 'end_date' ]; } protected function fetchData ( ): void { $this ->data = Order ::query () ->whereBetween ('created_at' , [ $this ->parameters['start_date' ], $this ->parameters['end_date' ], ]) ->with (['items' , 'customer' ]) ->get (); } protected function transformItem ($item ): array { return [ 'order_id' => $item ->id, 'order_number' => $item ->order_number, 'customer' => $item ->customer->name, 'total' => $item ->total, 'items_count' => $item ->items->count (), 'created_at' => $item ->created_at->toDateString (), ]; } protected function calculateMetrics ( ): void { $this ->results['metrics' ] = [ 'total_orders' => $this ->data->count (), 'total_revenue' => $this ->data->sum ('total' ), 'average_order_value' => $this ->data->avg ('total' ), 'total_items' => $this ->data->sum ('items_count' ), ]; } protected function getTitle ( ): string { return 'Sales Report' ; } }
用户活动报表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?php namespace App \Services \Reports ;use App \Models \UserActivity ;use Illuminate \Support \Facades \DB ;class UserActivityReportGenerator extends ReportGenerator { protected function getRequiredParameters ( ): array { return ['start_date' , 'end_date' ]; } protected function fetchData ( ): void { $this ->data = UserActivity ::query () ->whereBetween ('created_at' , [ $this ->parameters['start_date' ], $this ->parameters['end_date' ], ]) ->with ('user' ) ->get (); } protected function transformItem ($item ): array { return [ 'user_id' => $item ->user_id, 'user_name' => $item ->user->name, 'activity_type' => $item ->activity_type, 'description' => $item ->description, 'ip_address' => $item ->ip_address, 'created_at' => $item ->created_at->toIso8601String (), ]; } protected function calculateMetrics ( ): void { $this ->results['metrics' ] = [ 'total_activities' => $this ->data->count (), 'unique_users' => $this ->data->unique ('user_id' )->count (), 'activities_by_type' => $this ->data->groupBy ('activity_type' ) ->map (fn($items ) => $items ->count ()) ->toArray (), ]; } protected function getTitle ( ): string { return 'User Activity Report' ; } }
通知模板 通知模板基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 <?php namespace App \Services \Notifications ;abstract class NotificationTemplate { protected $recipient ; protected array $data ; protected array $channels = []; public function __construct ($recipient , array $data = [] ) { $this ->recipient = $recipient ; $this ->data = $data ; } final public function send ( ): array { $this ->validateRecipient (); $this ->prepareData (); $results = []; foreach ($this ->getChannels () as $channel ) { $results [$channel ] = $this ->sendViaChannel ($channel ); } $this ->afterSend ($results ); return $results ; } protected function validateRecipient ( ): void { if (!$this ->recipient) { throw new \InvalidArgumentException ('Recipient is required' ); } } protected function prepareData ( ): void { $this ->data = array_merge ($this ->getDefaultData (), $this ->data); } protected function getDefaultData ( ): array { return []; } protected function getChannels ( ): array { return $this ->channels; } protected function sendViaChannel (string $channel ): bool { return match ($channel ) { 'email' => $this ->sendEmail (), 'sms' => $this ->sendSms (), 'push' => $this ->sendPush (), 'database' => $this ->sendToDatabase (), default => false , }; } abstract protected function getSubject ( ): string ; abstract protected function getBody ( ): string ; protected function sendEmail ( ): bool { return \Mail ::to ($this ->recipient->email) ->send (new $this ->emailClass ($this ->getSubject (), $this ->getBody (), $this ->data)); } protected function sendSms ( ): bool { return false ; } protected function sendPush ( ): bool { return false ; } protected function sendToDatabase ( ): bool { return true ; } protected function afterSend (array $results ): void { } }
欢迎通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php namespace App \Services \Notifications ;class WelcomeNotification extends NotificationTemplate { protected array $channels = ['email' , 'database' ]; protected function getDefaultData ( ): array { return [ 'app_name' => config ('app.name' ), 'support_email' => config ('mail.support.address' ), ]; } protected function getSubject ( ): string { return "Welcome to {$this->data['app_name']} !" ; } protected function getBody ( ): string { return "Hello {$this->recipient->name} , welcome to {$this->data['app_name']} !" ; } protected function sendEmail ( ): bool { \Mail ::to ($this ->recipient->email)->send ( new \App\Mail\WelcomeEmail ($this ->recipient, $this ->data) ); return true ; } }
订单确认通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php namespace App \Services \Notifications ;class OrderConfirmationNotification extends NotificationTemplate { protected array $channels = ['email' , 'sms' , 'database' ]; protected function getDefaultData ( ): array { return [ 'order' => $this ->data['order' ] ?? null , ]; } protected function getSubject ( ): string { $order = $this ->data['order' ]; return "Order #{$order->order_number} Confirmed" ; } protected function getBody ( ): string { $order = $this ->data['order' ]; return "Your order #{$order->order_number} has been confirmed. Total: \${$order->total} " ; } protected function sendSms ( ): bool { $order = $this ->data['order' ]; if ($this ->recipient->phone) { \App\Services\SmsService ::send ( $this ->recipient->phone, $this ->getBody () ); return true ; } return false ; } }
数据导入模板 导入模板基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 <?php namespace App \Services \Import ;use Illuminate \Support \Collection ;abstract class DataImporter { protected string $filePath ; protected array $options ; protected Collection $data ; protected array $results = [ 'imported' => 0 , 'skipped' => 0 , 'errors' => [], ]; public function __construct (string $filePath , array $options = [] ) { $this ->filePath = $filePath ; $this ->options = $options ; } final public function import ( ): array { $this ->validateFile (); $this ->parseFile (); $this ->validateData (); $this ->processData (); return $this ->results; } protected function validateFile ( ): void { if (!file_exists ($this ->filePath)) { throw new \InvalidArgumentException ("File not found: {$this->filePath} " ); } } abstract protected function parseFile ( ): void ; protected function validateData ( ): void { $this ->data = $this ->data->filter (function ($item , $key ) { $errors = $this ->validateItem ($item ); if (!empty ($errors )) { $this ->results['errors' ][$key ] = $errors ; $this ->results['skipped' ]++; return false ; } return true ; }); } protected function validateItem (array $item ): array { return []; } protected function processData ( ): void { $this ->beforeProcess (); foreach ($this ->data as $key => $item ) { try { $this ->importItem ($item ); $this ->results['imported' ]++; } catch (\Exception $e ) { $this ->results['errors' ][$key ] = $e ->getMessage (); $this ->results['skipped' ]++; } } $this ->afterProcess (); } protected function beforeProcess ( ): void { } abstract protected function importItem (array $item ): void ; protected function afterProcess ( ): void { } }
CSV 导入器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?php namespace App \Services \Import ;class CsvImporter extends DataImporter { protected string $delimiter = ',' ; public function setDelimiter (string $delimiter ): self { $this ->delimiter = $delimiter ; return $this ; } protected function parseFile ( ): void { $handle = fopen ($this ->filePath, 'r' ); $headers = fgetcsv ($handle , 0 , $this ->delimiter); $rows = collect (); while (($row = fgetcsv ($handle , 0 , $this ->delimiter)) !== false ) { if (count ($row ) === count ($headers )) { $rows ->push (array_combine ($headers , $row )); } } fclose ($handle ); $this ->data = $rows ; } }
用户导入器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php namespace App \Services \Import ;use App \Models \User ;use Illuminate \Support \Facades \Hash ;class UserImporter extends CsvImporter { protected function validateItem (array $item ): array { $errors = []; if (empty ($item ['email' ])) { $errors [] = 'Email is required' ; } elseif (User ::where ('email' , $item ['email' ])->exists ()) { $errors [] = 'Email already exists' ; } if (empty ($item ['name' ])) { $errors [] = 'Name is required' ; } return $errors ; } protected function importItem (array $item ): void { User ::create ([ 'name' => $item ['name' ], 'email' => $item ['email' ], 'password' => Hash ::make ($item ['password' ] ?? 'password' ), ]); } }
测试模板方法模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php namespace Tests \Unit \Services ;use Tests \TestCase ;use App \Services \Export \CsvExporter ;use App \Services \Export \JsonExporter ;use Illuminate \Support \Collection ;class TemplateMethodTest extends TestCase { public function test_csv_exporter ( ): void { $data = collect ([ ['name' => 'John' , 'email' => 'john@example.com' ], ['name' => 'Jane' , 'email' => 'jane@example.com' ], ]); $exporter = new CsvExporter ($data ); $content = $exporter ->export (); $this ->assertStringContainsString ('name,email' , $content ); $this ->assertStringContainsString ('John,john@example.com' , $content ); $this ->assertEquals ('text/csv' , $exporter ->getMimeType ()); } public function test_json_exporter ( ): void { $data = collect ([ ['name' => 'John' , 'email' => 'john@example.com' ], ]); $exporter = new JsonExporter ($data ); $content = $exporter ->export (); $decoded = json_decode ($content , true ); $this ->assertArrayHasKey ('data' , $decoded ); $this ->assertArrayHasKey ('meta' , $decoded ); $this ->assertEquals (1 , $decoded ['meta' ]['count' ]); } }
最佳实践 1. 使用 final 关键字 1 2 3 4 5 6 7 8 9 10 11 <?php abstract class BaseTemplate { final public function execute ( ): mixed { return $this ->doExecute (); } abstract protected function doExecute ( ): mixed ; }
2. 提供钩子方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php abstract class TemplateWithHooks { final public function process ( ): void { $this ->before (); $this ->doProcess (); $this ->after (); } protected function before ( ): void { } abstract protected function doProcess ( ): void ; protected function after ( ): void { } }
3. 使用 protected 方法 1 2 3 4 5 6 7 8 9 10 11 <?php abstract class SecureTemplate { public function run ( ): mixed { return $this ->executeInternal (); } protected abstract function executeInternal ( ): mixed ; }
总结 Laravel 13 的模板方法模式提供了一种优雅的方式来定义算法骨架。通过合理使用模板方法模式,可以复用代码结构,同时允许子类自定义特定步骤的实现。