Laravel 13 状态模式深度解析

状态模式是一种行为型设计模式,它允许对象在其内部状态改变时改变其行为。本文将深入探讨 Laravel 13 中状态模式的高级用法。

状态模式基础

什么是状态模式

状态模式将对象的行为封装在不同的状态类中,让对象在其内部状态改变时看起来像是改变了其类。

1
2
3
4
5
6
7
8
9
10
<?php

namespace App\Contracts;

interface StateInterface
{
public function handle(): void;

public function getName(): string;
}

订单状态管理

订单状态接口

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

namespace App\Contracts\States;

interface OrderStateInterface
{
public function process(): void;

public function cancel(): void;

public function ship(): void;

public function deliver(): void;

public function refund(): void;

public function getStatusName(): string;

public function getAvailableTransitions(): array;
}

基础订单状态

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\States\Order;

use App\Models\Order;
use App\Contracts\States\OrderStateInterface;

abstract class BaseOrderState implements OrderStateInterface
{
protected Order $order;

public function __construct(Order $order)
{
$this->order = $order;
}

public function process(): void
{
throw new \InvalidArgumentException(
"Cannot process order in {$this->getStatusName()} state"
);
}

public function cancel(): void
{
throw new \InvalidArgumentException(
"Cannot cancel order in {$this->getStatusName()} state"
);
}

public function ship(): void
{
throw new \InvalidArgumentException(
"Cannot ship order in {$this->getStatusName()} state"
);
}

public function deliver(): void
{
throw new \InvalidArgumentException(
"Cannot deliver order in {$this->getStatusName()} state"
);
}

public function refund(): void
{
throw new \InvalidArgumentException(
"Cannot refund order in {$this->getStatusName()} state"
);
}

public function getAvailableTransitions(): array
{
return [];
}
}

具体状态实现

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

namespace App\States\Order;

class PendingState extends BaseOrderState
{
public function process(): void
{
$this->order->update([
'status' => 'processing',
'processed_at' => now(),
]);

event(new \App\Events\OrderProcessed($this->order));
}

public function cancel(): void
{
$this->order->update([
'status' => 'cancelled',
'cancelled_at' => now(),
]);

event(new \App\Events\OrderCancelled($this->order));
}

public function getStatusName(): string
{
return 'pending';
}

public function getAvailableTransitions(): array
{
return ['processing', 'cancelled'];
}
}
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
<?php

namespace App\States\Order;

class ProcessingState extends BaseOrderState
{
public function ship(): void
{
$this->order->update([
'status' => 'shipped',
'shipped_at' => now(),
]);

event(new \App\Events\OrderShipped($this->order));
}

public function cancel(): void
{
$this->order->update([
'status' => 'cancelled',
'cancelled_at' => now(),
]);

$this->order->payment->refund();

event(new \App\Events\OrderCancelled($this->order));
}

public function getStatusName(): string
{
return 'processing';
}

public function getAvailableTransitions(): array
{
return ['shipped', 'cancelled'];
}
}
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
<?php

namespace App\States\Order;

class ShippedState extends BaseOrderState
{
public function deliver(): void
{
$this->order->update([
'status' => 'delivered',
'delivered_at' => now(),
]);

event(new \App\Events\OrderDelivered($this->order));
}

public function getStatusName(): string
{
return 'shipped';
}

public function getAvailableTransitions(): array
{
return ['delivered'];
}
}
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\States\Order;

class DeliveredState extends BaseOrderState
{
public function refund(): void
{
$this->order->update([
'status' => 'refunded',
'refunded_at' => now(),
]);

$this->order->payment->refund();

event(new \App\Events\OrderRefunded($this->order));
}

public function getStatusName(): string
{
return 'delivered';
}

public function getAvailableTransitions(): array
{
return ['refunded'];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\States\Order;

class CancelledState extends BaseOrderState
{
public function getStatusName(): string
{
return 'cancelled';
}

public function getAvailableTransitions(): array
{
return [];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\States\Order;

class RefundedState extends BaseOrderState
{
public function getStatusName(): string
{
return 'refunded';
}

public function getAvailableTransitions(): array
{
return [];
}
}

状态工厂

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

namespace App\Factories;

use App\Models\Order;
use App\States\Order\{PendingState, ProcessingState, ShippedState, DeliveredState, CancelledState, RefundedState};
use InvalidArgumentException;

class OrderStateFactory
{
public static function create(Order $order): OrderStateInterface
{
return match ($order->status) {
'pending' => new PendingState($order),
'processing' => new ProcessingState($order),
'shipped' => new ShippedState($order),
'delivered' => new DeliveredState($order),
'cancelled' => new CancelledState($order),
'refunded' => new RefundedState($order),
default => throw new InvalidArgumentException("Unknown order status: {$order->status}"),
};
}
}

订单上下文

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

namespace App\Models;

use App\Contracts\States\OrderStateInterface;
use App\Factories\OrderStateFactory;

class Order extends Model
{
protected ?OrderStateInterface $state = null;

public function getState(): OrderStateInterface
{
if ($this->state === null) {
$this->state = OrderStateFactory::create($this);
}

return $this->state;
}

public function setState(OrderStateInterface $state): void
{
$this->state = $state;
}

public function process(): void
{
$this->getState()->process();
$this->refreshState();
}

public function cancel(): void
{
$this->getState()->cancel();
$this->refreshState();
}

public function ship(): void
{
$this->getState()->ship();
$this->refreshState();
}

public function deliver(): void
{
$this->getState()->deliver();
$this->refreshState();
}

public function refund(): void
{
$this->getState()->refund();
$this->refreshState();
}

protected function refreshState(): void
{
$this->refresh();
$this->state = null;
}
}

文档审批状态

文档状态接口

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

namespace App\Contracts\States;

interface DocumentStateInterface
{
public function submit(): void;

public function approve(): void;

public function reject(): void;

public function publish(): void;

public function archive(): void;

public function getStatus(): 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
<?php

namespace App\States\Document;

use App\Models\Document;
use App\Contracts\States\DocumentStateInterface;

class DraftState implements DocumentStateInterface
{
protected Document $document;

public function __construct(Document $document)
{
$this->document = $document;
}

public function submit(): void
{
$this->document->update([
'status' => 'pending_review',
'submitted_at' => now(),
]);
}

public function approve(): void
{
throw new \Exception('Cannot approve a draft document');
}

public function reject(): void
{
throw new \Exception('Cannot reject a draft document');
}

public function publish(): void
{
throw new \Exception('Cannot publish a draft document');
}

public function archive(): void
{
$this->document->update(['status' => 'archived']);
}

public function getStatus(): string
{
return 'draft';
}
}
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
<?php

namespace App\States\Document;

use App\Models\Document;
use App\Contracts\States\DocumentStateInterface;

class PendingReviewState implements DocumentStateInterface
{
protected Document $document;

public function __construct(Document $document)
{
$this->document = $document;
}

public function submit(): void
{
throw new \Exception('Document is already submitted');
}

public function approve(): void
{
$this->document->update([
'status' => 'approved',
'approved_at' => now(),
'approved_by' => auth()->id(),
]);
}

public function reject(): void
{
$this->document->update([
'status' => 'rejected',
'rejected_at' => now(),
'rejected_by' => auth()->id(),
]);
}

public function publish(): void
{
throw new \Exception('Cannot publish a document pending review');
}

public function archive(): void
{
$this->document->update(['status' => 'archived']);
}

public function getStatus(): string
{
return 'pending_review';
}
}
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
<?php

namespace App\States\Document;

use App\Models\Document;
use App\Contracts\States\DocumentStateInterface;

class ApprovedState implements DocumentStateInterface
{
protected Document $document;

public function __construct(Document $document)
{
$this->document = $document;
}

public function submit(): void
{
throw new \Exception('Document is already approved');
}

public function approve(): void
{
throw new \Exception('Document is already approved');
}

public function reject(): void
{
throw new \Exception('Cannot reject an approved document');
}

public function publish(): void
{
$this->document->update([
'status' => 'published',
'published_at' => now(),
]);
}

public function archive(): void
{
$this->document->update(['status' => 'archived']);
}

public function getStatus(): string
{
return 'approved';
}
}
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
<?php

namespace App\States\Document;

use App\Models\Document;
use App\Contracts\States\DocumentStateInterface;

class PublishedState implements DocumentStateInterface
{
protected Document $document;

public function __construct(Document $document)
{
$this->document = $document;
}

public function submit(): void
{
throw new \Exception('Document is already published');
}

public function approve(): void
{
throw new \Exception('Document is already published');
}

public function reject(): void
{
throw new \Exception('Cannot reject a published document');
}

public function publish(): void
{
throw new \Exception('Document is already published');
}

public function archive(): void
{
$this->document->update([
'status' => 'archived',
'archived_at' => now(),
]);
}

public function getStatus(): string
{
return 'published';
}
}

用户账户状态

账户状态接口

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

namespace App\Contracts\States;

interface AccountStateInterface
{
public function activate(): void;

public function suspend(): void;

public function ban(): void;

public function close(): void;

public function canLogin(): bool;

public function canPerformAction(string $action): bool;

public function getStatus(): 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
53
54
55
56
57
58
59
<?php

namespace App\States\Account;

use App\Models\User;
use App\Contracts\States\AccountStateInterface;

class ActiveState implements AccountStateInterface
{
protected User $user;

public function __construct(User $user)
{
$this->user = $user;
}

public function activate(): void
{
}

public function suspend(): void
{
$this->user->update([
'status' => 'suspended',
'suspended_at' => now(),
]);
}

public function ban(): void
{
$this->user->update([
'status' => 'banned',
'banned_at' => now(),
]);
}

public function close(): void
{
$this->user->update([
'status' => 'closed',
'closed_at' => now(),
]);
}

public function canLogin(): bool
{
return true;
}

public function canPerformAction(string $action): bool
{
return true;
}

public function getStatus(): string
{
return 'active';
}
}
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
<?php

namespace App\States\Account;

use App\Models\User;
use App\Contracts\States\AccountStateInterface;

class SuspendedState implements AccountStateInterface
{
protected User $user;

public function __construct(User $user)
{
$this->user = $user;
}

public function activate(): void
{
$this->user->update([
'status' => 'active',
'suspended_at' => null,
]);
}

public function suspend(): void
{
}

public function ban(): void
{
$this->user->update([
'status' => 'banned',
'banned_at' => now(),
]);
}

public function close(): void
{
$this->user->update([
'status' => 'closed',
'closed_at' => now(),
]);
}

public function canLogin(): bool
{
return false;
}

public function canPerformAction(string $action): bool
{
return false;
}

public function getStatus(): string
{
return 'suspended';
}
}
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\States\Account;

use App\Models\User;
use App\Contracts\States\AccountStateInterface;

class BannedState implements AccountStateInterface
{
protected User $user;

public function __construct(User $user)
{
$this->user = $user;
}

public function activate(): void
{
throw new \Exception('Cannot activate a banned account');
}

public function suspend(): void
{
throw new \Exception('Cannot suspend a banned account');
}

public function ban(): void
{
}

public function close(): void
{
$this->user->update([
'status' => 'closed',
'closed_at' => now(),
]);
}

public function canLogin(): bool
{
return false;
}

public function canPerformAction(string $action): bool
{
return false;
}

public function getStatus(): string
{
return 'banned';
}
}

支付状态

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\States\Payment;

use App\Models\Payment;
use App\Contracts\States\PaymentStateInterface;

class PendingPaymentState implements PaymentStateInterface
{
protected Payment $payment;

public function __construct(Payment $payment)
{
$this->payment = $payment;
}

public function complete(): void
{
$this->payment->update([
'status' => 'completed',
'completed_at' => now(),
]);
}

public function fail(string $reason = null): void
{
$this->payment->update([
'status' => 'failed',
'failed_at' => now(),
'failure_reason' => $reason,
]);
}

public function cancel(): void
{
$this->payment->update([
'status' => 'cancelled',
'cancelled_at' => now(),
]);
}

public function refund(): void
{
throw new \Exception('Cannot refund a pending payment');
}

public function getStatus(): string
{
return 'pending';
}
}
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
<?php

namespace App\States\Payment;

use App\Models\Payment;
use App\Contracts\States\PaymentStateInterface;

class CompletedPaymentState implements PaymentStateInterface
{
protected Payment $payment;

public function __construct(Payment $payment)
{
$this->payment = $payment;
}

public function complete(): void
{
throw new \Exception('Payment is already completed');
}

public function fail(string $reason = null): void
{
throw new \Exception('Cannot fail a completed payment');
}

public function cancel(): void
{
throw new \Exception('Cannot cancel a completed payment');
}

public function refund(): void
{
$this->payment->update([
'status' => 'refunded',
'refunded_at' => now(),
]);

$this->payment->processRefund();
}

public function getStatus(): string
{
return 'completed';
}
}

测试状态模式

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

namespace Tests\Unit\States;

use Tests\TestCase;
use App\Models\Order;
use App\States\Order\{PendingState, ProcessingState};
use App\Factories\OrderStateFactory;

class StateTest extends TestCase
{
public function test_order_pending_state(): void
{
$order = Order::factory()->create(['status' => 'pending']);
$state = OrderStateFactory::create($order);

$this->assertInstanceOf(PendingState::class, $state);
$this->assertEquals('pending', $state->getStatusName());
$this->assertEquals(['processing', 'cancelled'], $state->getAvailableTransitions());
}

public function test_order_state_transition(): void
{
$order = Order::factory()->create(['status' => 'pending']);

$order->process();

$this->assertEquals('processing', $order->fresh()->status);
$this->assertInstanceOf(ProcessingState::class, $order->getState());
}

public function test_invalid_state_transition(): void
{
$order = Order::factory()->create(['status' => 'pending']);

$this->expectException(\InvalidArgumentException::class);
$order->ship();
}
}

最佳实践

1. 状态类应该无状态

1
2
3
4
5
6
7
8
<?php

class StatelessState implements StateInterface
{
public function handle(): void
{
}
}

2. 使用枚举定义状态

1
2
3
4
5
6
7
8
9
10
<?php

enum OrderStatus: string
{
case PENDING = 'pending';
case PROCESSING = 'processing';
case SHIPPED = 'shipped';
case DELIVERED = 'delivered';
case CANCELLED = 'cancelled';
}

3. 状态转换验证

1
2
3
4
5
6
7
8
9
10
11
<?php

trait ValidatesTransitions
{
protected array $allowedTransitions = [];

public function canTransitionTo(string $state): bool
{
return in_array($state, $this->allowedTransitions);
}
}

总结

Laravel 13 的状态模式提供了一种优雅的方式来管理对象的不同状态。通过合理使用状态模式,可以消除大量的条件判断,使代码更加清晰、可维护和可扩展。