Laravel 13 建造者模式深度解析

建造者模式是一种创建型设计模式,它允许你分步骤创建复杂对象,使相同的构建过程可以创建不同的表示。本文将深入探讨 Laravel 13 中建造者模式的高级用法。

建造者模式基础

什么是建造者模式

建造者模式将复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。

1
2
3
4
5
6
7
8
<?php

namespace App\Contracts;

interface BuilderInterface
{
public function build(): mixed;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Builders;

use App\Contracts\BuilderInterface;

abstract class AbstractBuilder implements BuilderInterface
{
protected mixed $result;

abstract public function reset(): void;

public function build(): mixed
{
$result = $this->result;
$this->reset();
return $result;
}
}

查询构建器

数据库查询构建器

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<?php

namespace App\Builders;

use Illuminate\Database\Eloquent\Builder;

class QueryBuilder
{
protected Builder $query;
protected array $filters = [];
protected array $sorts = [];
protected array $includes = [];
protected int $perPage = 15;

public function __construct(Builder $query)
{
$this->query = $query;
}

public function filter(array $filters): self
{
$this->filters = array_merge($this->filters, $filters);
return $this;
}

public function where(string $field, mixed $value): self
{
$this->filters[$field] = $value;
return $this;
}

public function whereIn(string $field, array $values): self
{
$this->filters["{$field}_in"] = $values;
return $this;
}

public function whereBetween(string $field, array $range): self
{
$this->filters["{$field}_between"] = $range;
return $this;
}

public function search(string $query, array $fields): self
{
$this->filters['_search'] = ['query' => $query, 'fields' => $fields];
return $this;
}

public function sort(string $field, string $direction = 'asc'): self
{
$this->sorts[$field] = $direction;
return $this;
}

public function include(array $relations): self
{
$this->includes = array_merge($this->includes, $relations);
return $this;
}

public function perPage(int $count): self
{
$this->perPage = $count;
return $this;
}

public function build(): Builder
{
$this->applyFilters();
$this->applySorts();
$this->applyIncludes();

return $this->query;
}

public function paginate()
{
return $this->build()->paginate($this->perPage);
}

public function get()
{
return $this->build()->get();
}

public function first()
{
return $this->build()->first();
}

protected function applyFilters(): void
{
foreach ($this->filters as $field => $value) {
if (str_ends_with($field, '_in')) {
$actualField = str_replace('_in', '', $field);
$this->query->whereIn($actualField, $value);
} elseif (str_ends_with($field, '_between')) {
$actualField = str_replace('_between', '', $field);
$this->query->whereBetween($actualField, $value);
} elseif ($field === '_search') {
$this->applySearch($value['query'], $value['fields']);
} else {
$this->query->where($field, $value);
}
}
}

protected function applySearch(string $query, array $fields): void
{
$this->query->where(function ($q) use ($query, $fields) {
foreach ($fields as $field) {
$q->orWhere($field, 'like', "%{$query}%");
}
});
}

protected function applySorts(): void
{
foreach ($this->sorts as $field => $direction) {
$this->query->orderBy($field, $direction);
}
}

protected function applyIncludes(): void
{
if (!empty($this->includes)) {
$this->query->with($this->includes);
}
}
}

使用查询构建器

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
<?php

namespace App\Services;

use App\Models\Product;
use App\Builders\QueryBuilder;

class ProductSearchService
{
public function search(array $params)
{
$builder = new QueryBuilder(Product::query());

return $builder
->filter($params['filter'] ?? [])
->where('is_active', true)
->whereIn('category_id', $params['categories'] ?? [])
->whereBetween('price', [$params['min_price'] ?? 0, $params['max_price'] ?? 999999])
->search($params['q'] ?? '', ['name', 'description'])
->sort($params['sort'] ?? 'created_at', $params['order'] ?? 'desc')
->include(['category', 'brand', 'images'])
->perPage($params['per_page'] ?? 15)
->paginate();
}
}

HTTP 请求构建器

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php

namespace App\Builders\Http;

class HttpRequestBuilder
{
protected string $url = '';
protected string $method = 'GET';
protected array $headers = [];
protected array $query = [];
protected array $body = [];
protected array $options = [];
protected int $timeout = 30;
protected bool $verifySsl = true;

public function url(string $url): self
{
$this->url = $url;
return $this;
}

public function method(string $method): self
{
$this->method = strtoupper($method);
return $this;
}

public function get(): self
{
return $this->method('GET');
}

public function post(): self
{
return $this->method('POST');
}

public function put(): self
{
return $this->method('PUT');
}

public function delete(): self
{
return $this->method('DELETE');
}

public function header(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}

public function headers(array $headers): self
{
$this->headers = array_merge($this->headers, $headers);
return $this;
}

public function acceptJson(): self
{
return $this->header('Accept', 'application/json');
}

public function bearerToken(string $token): self
{
return $this->header('Authorization', "Bearer {$token}");
}

public function query(array $params): self
{
$this->query = array_merge($this->query, $params);
return $this;
}

public function body(array $data): self
{
$this->body = array_merge($this->body, $data);
return $this;
}

public function json(array $data): self
{
$this->body = $data;
return $this->header('Content-Type', 'application/json');
}

public function timeout(int $seconds): self
{
$this->timeout = $seconds;
return $this;
}

public function withoutSslVerification(): self
{
$this->verifySsl = false;
return $this;
}

public function options(array $options): self
{
$this->options = array_merge($this->options, $options);
return $this;
}

public function build(): array
{
return [
'url' => $this->buildUrl(),
'method' => $this->method,
'headers' => $this->headers,
'body' => $this->shouldIncludeBody() ? $this->body : null,
'options' => array_merge($this->options, [
'timeout' => $this->timeout,
'verify' => $this->verifySsl,
]),
];
}

public function send(): mixed
{
$config = $this->build();

return Http::withHeaders($config['headers'])
->timeout($config['options']['timeout'])
->withoutVerifying(!$config['options']['verify'])
->send($config['method'], $config['url'], $config['body'] ? ['json' => $config['body']] : []);
}

protected function buildUrl(): string
{
if (empty($this->query)) {
return $this->url;
}

return $this->url . '?' . http_build_query($this->query);
}

protected function shouldIncludeBody(): bool
{
return in_array($this->method, ['POST', 'PUT', 'PATCH']);
}
}

邮件构建器

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<?php

namespace App\Builders\Mail;

use Illuminate\Mail\Mailable;

class EmailBuilder
{
protected string $to = '';
protected string $subject = '';
protected string $body = '';
protected string $view = '';
protected array $viewData = [];
protected array $cc = [];
protected array $bcc = [];
protected array $attachments = [];
protected ?string $from = null;
protected ?string $fromName = null;
protected ?string $replyTo = null;
protected int $priority = 3;

public function to(string $email, string $name = ''): self
{
$this->to = $email;
return $this;
}

public function subject(string $subject): self
{
$this->subject = $subject;
return $this;
}

public function body(string $body): self
{
$this->body = $body;
return $this;
}

public function view(string $view, array $data = []): self
{
$this->view = $view;
$this->viewData = $data;
return $this;
}

public function cc(array $emails): self
{
$this->cc = array_merge($this->cc, $emails);
return $this;
}

public function bcc(array $emails): self
{
$this->bcc = array_merge($this->bcc, $emails);
return $this;
}

public function from(string $email, string $name = ''): self
{
$this->from = $email;
$this->fromName = $name;
return $this;
}

public function replyTo(string $email): self
{
$this->replyTo = $email;
return $this;
}

public function attach(string $path, array $options = []): self
{
$this->attachments[] = ['path' => $path, 'options' => $options];
return $this;
}

public function attachData(string $data, string $name, array $options = []): self
{
$this->attachments[] = ['data' => $data, 'name' => $name, 'options' => $options];
return $this;
}

public function priority(int $priority): self
{
$this->priority = $priority;
return $this;
}

public function build(): Mailable
{
$mail = new class extends Mailable {
public function __construct(
public string $bodyContent = '',
public string $viewTemplate = '',
public array $viewData = []
) {}

public function build(): self
{
if ($this->viewTemplate) {
return $this->view($this->viewTemplate, $this->viewData);
}

return $this->html($this->bodyContent);
}
};

$mail->to($this->to);

if (!empty($this->cc)) {
$mail->cc($this->cc);
}

if (!empty($this->bcc)) {
$mail->bcc($this->bcc);
}

if ($this->from) {
$mail->from($this->from, $this->fromName);
}

if ($this->replyTo) {
$mail->replyTo($this->replyTo);
}

$mail->subject($this->subject);
$mail->priority($this->priority);

foreach ($this->attachments as $attachment) {
if (isset($attachment['path'])) {
$mail->attach($attachment['path'], $attachment['options']);
} else {
$mail->attachData($attachment['data'], $attachment['name'], $attachment['options']);
}
}

$mail->bodyContent = $this->body;
$mail->viewTemplate = $this->view;
$mail->viewData = $this->viewData;

return $mail;
}

public function send(): void
{
Mail::send($this->build());
}

public function queue(): void
{
Mail::queue($this->build());
}
}

PDF 文档构建器

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php

namespace App\Builders\Pdf;

class PdfBuilder
{
protected string $title = '';
protected string $content = '';
protected string $template = '';
protected array $data = [];
protected string $orientation = 'portrait';
protected string $pageSize = 'A4';
protected array $margins = [10, 10, 10, 10];
protected array $header = [];
protected array $footer = [];
protected string $watermark = '';
protected bool $encrypt = false;
protected ?string $password = null;

public function title(string $title): self
{
$this->title = $title;
return $this;
}

public function content(string $content): self
{
$this->content = $content;
return $this;
}

public function template(string $template, array $data = []): self
{
$this->template = $template;
$this->data = $data;
return $this;
}

public function landscape(): self
{
$this->orientation = 'landscape';
return $this;
}

public function portrait(): self
{
$this->orientation = 'portrait';
return $this;
}

public function pageSize(string $size): self
{
$this->pageSize = $size;
return $this;
}

public function margins(int $top, int $right, int $bottom, int $left): self
{
$this->margins = [$top, $right, $bottom, $left];
return $this;
}

public function header(callable $callback): self
{
$this->header = ['callback' => $callback];
return $this;
}

public function footer(callable $callback): self
{
$this->footer = ['callback' => $callback];
return $this;
}

public function watermark(string $text): self
{
$this->watermark = $text;
return $this;
}

public function encrypt(string $password): self
{
$this->encrypt = true;
$this->password = $password;
return $this;
}

public function build(): string
{
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadHTML($this->renderContent());

$pdf->setPaper($this->pageSize, $this->orientation);
$pdf->setOptions([
'margin_top' => $this->margins[0],
'margin_right' => $this->margins[1],
'margin_bottom' => $this->margins[2],
'margin_left' => $this->margins[3],
]);

if ($this->watermark) {
$pdf->setOption('watermark', $this->watermark);
}

if ($this->encrypt && $this->password) {
$pdf->setEncryption($this->password);
}

return $pdf->output();
}

public function download(string $filename): \Symfony\Component\HttpFoundation\Response
{
return response($this->build())
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', "attachment; filename=\"{$filename}\"");
}

public function stream(): \Symfony\Component\HttpFoundation\Response
{
return response($this->build())
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'inline');
}

protected function renderContent(): string
{
if ($this->template) {
return view($this->template, array_merge($this->data, [
'title' => $this->title,
]))->render();
}

return $this->content;
}
}

报表构建器

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<?php

namespace App\Builders\Reports;

class ReportBuilder
{
protected string $name = '';
protected string $description = '';
protected array $columns = [];
protected array $data = [];
protected array $filters = [];
protected array $aggregations = [];
protected array $groupings = [];
protected string $format = 'table';
protected array $styles = [];

public function name(string $name): self
{
$this->name = $name;
return $this;
}

public function description(string $description): self
{
$this->description = $description;
return $this;
}

public function column(string $name, string $label = null, callable $formatter = null): self
{
$this->columns[] = [
'name' => $name,
'label' => $label ?? $name,
'formatter' => $formatter,
];
return $this;
}

public function columns(array $columns): self
{
foreach ($columns as $name => $label) {
$this->column($name, $label);
}
return $this;
}

public function data(array $data): self
{
$this->data = $data;
return $this;
}

public function filter(string $field, $value): self
{
$this->filters[$field] = $value;
return $this;
}

public function aggregation(string $type, string $field, string $alias = null): self
{
$this->aggregations[] = [
'type' => $type,
'field' => $field,
'alias' => $alias ?? "{$type}_{$field}",
];
return $this;
}

public function sum(string $field, string $alias = null): self
{
return $this->aggregation('sum', $field, $alias);
}

public function avg(string $field, string $alias = null): self
{
return $this->aggregation('avg', $field, $alias);
}

public function count(string $field, string $alias = null): self
{
return $this->aggregation('count', $field, $alias);
}

public function groupBy(string $field): self
{
$this->groupings[] = $field;
return $this;
}

public function format(string $format): self
{
$this->format = $format;
return $this;
}

public function asTable(): self
{
return $this->format('table');
}

public function asChart(string $type = 'bar'): self
{
return $this->format("chart:{$type}");
}

public function style(string $element, array $styles): self
{
$this->styles[$element] = $styles;
return $this;
}

public function build(): array
{
$processedData = $this->processData();

return [
'name' => $this->name,
'description' => $this->description,
'columns' => $this->columns,
'data' => $processedData,
'aggregations' => $this->calculateAggregations($processedData),
'format' => $this->format,
'styles' => $this->styles,
];
}

protected function processData(): array
{
$data = $this->data;

foreach ($this->filters as $field => $value) {
$data = array_filter($data, fn($row) => ($row[$field] ?? null) === $value);
}

if (!empty($this->groupings)) {
$data = $this->groupData($data);
}

return array_values($data);
}

protected function groupData(array $data): array
{
$grouped = [];

foreach ($data as $row) {
$key = implode('_', array_map(fn($field) => $row[$field] ?? '', $this->groupings));

if (!isset($grouped[$key])) {
$grouped[$key] = [];
}

$grouped[$key][] = $row;
}

return $grouped;
}

protected function calculateAggregations(array $data): array
{
$results = [];

foreach ($this->aggregations as $agg) {
$values = array_column($data, $agg['field']);

$results[$agg['alias']] = match($agg['type']) {
'sum' => array_sum($values),
'avg' => count($values) > 0 ? array_sum($values) / count($values) : 0,
'count' => count($values),
'min' => min($values),
'max' => max($values),
default => null,
};
}

return $results;
}
}

测试建造者

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
<?php

namespace Tests\Unit\Builders;

use Tests\TestCase;
use App\Builders\Http\HttpRequestBuilder;
use App\Builders\Mail\EmailBuilder;
use App\Builders\Reports\ReportBuilder;

class BuilderTest extends TestCase
{
public function test_http_request_builder(): void
{
$config = (new HttpRequestBuilder())
->url('https://api.example.com/users')
->get()
->acceptJson()
->bearerToken('test-token')
->query(['page' => 1, 'per_page' => 10])
->timeout(60)
->build();

$this->assertEquals('https://api.example.com/users?page=1&per_page=10', $config['url']);
$this->assertEquals('GET', $config['method']);
$this->assertEquals('application/json', $config['headers']['Accept']);
$this->assertEquals('Bearer test-token', $config['headers']['Authorization']);
$this->assertEquals(60, $config['options']['timeout']);
}

public function test_email_builder(): void
{
$mail = (new EmailBuilder())
->to('test@example.com')
->subject('Test Subject')
->view('emails.test', ['name' => 'John'])
->cc(['admin@example.com'])
->priority(1)
->build();

$this->assertEquals('Test Subject', $mail->subject);
$this->assertEquals('test@example.com', $mail->to[0]['address']);
}

public function test_report_builder(): void
{
$report = (new ReportBuilder())
->name('Sales Report')
->columns(['date' => 'Date', 'amount' => 'Amount', 'status' => 'Status'])
->data([
['date' => '2024-01-01', 'amount' => 100, 'status' => 'completed'],
['date' => '2024-01-02', 'amount' => 200, 'status' => 'completed'],
['date' => '2024-01-03', 'amount' => 150, 'status' => 'pending'],
])
->filter('status', 'completed')
->sum('amount', 'total_amount')
->asTable()
->build();

$this->assertEquals('Sales Report', $report['name']);
$this->assertCount(2, $report['data']);
$this->assertEquals(300, $report['aggregations']['total_amount']);
}
}

最佳实践

1. 流式接口

1
2
3
4
5
6
7
<?php

$result = (new QueryBuilder())
->where('status', 'active')
->sort('created_at', 'desc')
->include(['user', 'items'])
->paginate();

2. 不可变构建器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Builders;

class ImmutableBuilder
{
protected array $data;

public function with(string $key, mixed $value): self
{
$clone = clone $this;
$clone->data[$key] = $value;
return $clone;
}

public function build(): array
{
return $this->data;
}
}

3. 验证构建状态

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
<?php

namespace App\Builders;

class ValidatedBuilder
{
protected string $url = '';
protected string $method = '';

public function build(): array
{
$this->validate();

return [
'url' => $this->url,
'method' => $this->method,
];
}

protected function validate(): void
{
if (empty($this->url)) {
throw new \InvalidArgumentException('URL is required');
}

if (empty($this->method)) {
throw new \InvalidArgumentException('Method is required');
}
}
}

总结

Laravel 13 的建造者模式提供了一种优雅的方式来创建复杂对象。通过合理使用建造者模式,可以使代码更加清晰、可读,并支持灵活的对象构建过程。