如何使用 PhpStorm 重構 Namespace ?
在 TDD 開發流程,為了第一個 綠燈,一開始可能在同一個 namespace 下只有一個 class,但隨著重構的進行,可能重構出更多的 class 與 interface,為了更加的高內聚,低耦合,我們可能會將更相關的 class 與 interface 重構到其他 namespace,導致相依的 class 也必須修改,在重構 namepsace 時,PhpStorm 可以幫我們將相依的 class 一併修改,非常方便。
Version
PHP 7.0.0
Laravel 5.2.37
實際案例
我們將以經典的 service + repository 模式為例,以 PostService
處理商業邏輯,以 PostRepository
處理資料庫邏輯,將全部 post 顯示在網頁上。
最後使用 PhpStorm 重構 PostService
與 PostRepository
。
單元測試
以 TDD 方式開發,因此必須先寫單元測試。
PostServiceTest.php1 1GitHub Commit : 單元測試 : 建立 PostServiceTest.php1
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
29use App\Post;
use App\Services\PostService;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class PostServiceTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function 顯示所有文章()
{
/** arrange */
$expected = [
['title' => 'title1', 'description' => 'desc1', 'content' => 'content1'],
['title' => 'title2', 'description' => 'desc2', 'content' => 'content2'],
['title' => 'title3', 'description' => 'desc3', 'content' => 'content3'],
];
collect($expected)->each(function ($value) {
Post::create($value);
});
/** act */
$actual = app(PostService::class)->displayAllPosts()->toArray();
/** assert */
$this->assertArraySubset($expected, $actual);
}
}
13 行1
2
3
4
5
6
7
8
9
10/** arrange */
$expected = [
['title' => 'title1', 'description' => 'desc1', 'content' => 'content1'],
['title' => 'title2', 'description' => 'desc2', 'content' => 'content2'],
['title' => 'title3', 'description' => 'desc3', 'content' => 'content3'],
];
collect($expected)->each(function ($value) {
Post::create($value);
});
由於單元測試是使用 SQLite in Memory 為資料庫,只要測試一結束,記憶體就會釋放,因此每次測試都要重新新增資料。
使用 Collection->each()
將 $expected
中的資料透過 Post::create()
新增。
23 行1
2/** act */
$actual = app(PostService::class)->displayAllPosts()->toArray();
測試 PostService->displayAllPosts()
。2 2此時 PostService
與 displayAllPost()
都還沒建立,TDD 會等待測試亮 紅燈 時,才去新增 PostService
與 displayAllPost()
。
displayAllPosts()
回傳的是 Collection
,但 PHPUnit 無法對 Collection
做 assertion,必須先轉成 array。
26 行1
2/** assert */
$this->assertArraySubset($expected, $actual);
這裡不能使用 assertEquals()
,因為 posts
table 還包含 created_at
與 updated_at
兩個欄位,若使用 assertEquals()
一定失敗,必須改用 assertArraySubset()
。
PostService
實際跑測試,會得到第 1 個 紅燈,PHPUnit 抱怨 PostService
與 displayAllPosts()
尚未建立,須趕快補上。
PostService.php3 3GitHub Commit : 建立 PostService1
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
29namespace App\Services;
use App\Repositories\PostRepository;
use Illuminate\Database\Eloquent\Collection;
class PostService
{
/**
* @var PostRepository
*/
private $postRepository;
/**
* PostService constructor.
* @param PostRepository $postRepository
*/
public function __construct(PostRepository $postRepository)
{
$this->postRepository = $postRepository;
}
/**
* @return Collection
*/
public function displayAllPosts() : Collection
{
return $this->postRepository->getAllPosts();
}
}
第 8 行1
2
3
4
5
6
7
8
9
10
11
12
13/**
* @var PostRepository
*/
private $postRepository;
/**
* PostService constructor.
* @param PostRepository $postRepository
*/
public function __construct(PostRepository $postRepository)
{
$this->postRepository = $postRepository;
}
因為 PostService
須使用到 PostRepository
,使用 constructor injection 注入 PostRepository
。
22 行1
2
3
4
5
6
7/**
* @return Collection
*/
public function displayAllPosts() : Collection
{
return $this->postRepository->getAllPosts();
}
呼叫 PostRepository
的 getAllPosts()
, 回傳 Collection
。4 4此時 PostRepository
與 getAllPosts()
都還沒建立,TDD 會等待測試亮 紅燈 時,才去新增 PostRepository
與 getAllPosts()
。
實務上 PostService
除了呼叫 PostRepository
外,還會有自己的商業邏輯要寫,本文因為重點在 namespace 重構,所以簡化了 PostService
,關於 Service 模式,詳細請參考如何使用 Service 模式?
PostRepository
實際跑測試,會得到第 2 個 紅燈,PHPUnit 抱怨 PostRepository
與 getAllPosts()
尚未建立,須趕快補上。
PostRepository.php5 5GitHub Commit : 建立 PostRepository1
2
3
4
5
6
7
8
9
10
11
12
13
14
15namespace App\Repositories;
use App\Post;
use Illuminate\Database\Eloquent\Collection;
class PostRepository
{
/**
* @return Collection
*/
public function getAllPosts() : Collection
{
return Post::all();
}
}
為簡化起見,回傳 post
table 所有資料。
得到第 1 個 綠燈,完成 PostService
與 PostRepository
。
整合測試
單元測試目的是寫出 service 與 repository,我們要繼續寫整合測試,將 route、controller 與 view 補上。
PostApplicationTest.php6 6GitHub Commit : 整合測試 : 建立 PostApplicationTest1
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
32use App\Post;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class PostApplicationTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function 顯示所有文章()
{
/** arrange */
$expected = [
['title' => 'title1', 'description' => 'desc1', 'content' => 'content1'],
['title' => 'title2', 'description' => 'desc2', 'content' => 'content2'],
['title' => 'title3', 'description' => 'desc3', 'content' => 'content3'],
];
collect($expected)->each(function ($value) {
Post::create($value);
});
/** act */
$this->visit('/post');
/** assert */
collect($expected)->each(function ($value) {
$this->see($value['title']);
$this->see($value['description']);
$this->see($value['content']);
});
}
}
第 9 行1
2
3
4
5
6
7
8
9
10/** arrange */
$expected = [
['title' => 'title1', 'description' => 'desc1', 'content' => 'content1'],
['title' => 'title2', 'description' => 'desc2', 'content' => 'content2'],
['title' => 'title3', 'description' => 'desc3', 'content' => 'content3'],
];
collect($expected)->each(function ($value) {
Post::create($value);
});
由於單元測試是使用 SQLite in Memory 為資料庫,只要測試一結束,記憶體就會釋放,因此每次測試都要重新新增資料。
使用 Collection->each()
將 $expected
中的資料透過 Post::create()
新增。
22 行1
2/** act */
$this->visit('/post');
實際測試 /post
URI。7 7此時 route 都還沒建立,TDD 會等待測試亮 紅燈 時,才去新增 route。
27 行1
2
3
4
5
6/** assert */
collect($expected)->each(function ($value) {
$this->see($value['title']);
$this->see($value['description']);
$this->see($value['content']);
});
期望在網頁上看到 title
、description
與 content
等資料。
使用 Collection->each()
將 $expected
中的資料透過 $this->see()
做 assertion。8 8此時 view 都還沒建立,TDD 會等待測試亮 紅燈 時,才去新增 view。
Routes
實際跑測試,會得到第 1 個 紅燈,PHPUnit 抱怨找不到 http://localhost/post
,因為 route 尚未建立,須趕快補上。
routes.php9 9GitHub Commit : 建立 routes1
2
3
4
5
6
7
8Route::get('/', function () {
return view('welcome');
});
Route::get('/post', [
'as' => 'post',
'uses' => 'PostController@index'
]);
新增 route /post
,並指定其 controller 為 PostController@index
。10 10此時 PostController
都還沒建立,TDD 會等待測試亮 紅燈 時,才去新增 PostController
。
PostController
實際跑測試,會得到第 2 個 紅燈,PHPUnit 抱怨 PostController
尚未建立,須趕快補上。
PostController.php11 11GitHub Commit : 建立 PostController1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18namespace App\Http\Controllers;
use App\Http\Requests;
use App\Services\PostService;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$data['posts'] = app(PostService::class)->displayAllPosts();
return view('post.index', $data);
}
}
使用 app()
建立 PostService
,並呼叫其 displayAllPosts()
。
回傳 post.index
view。12 12此時 post.index
view 都還沒建立,TDD 會等待測試亮 紅燈 時,才去新增 post.index
view。
Post.Index Blade
實際跑測試,會得到第 3 個 紅燈,PHPUnit 抱怨 post.index
view 尚未建立,須趕快補上。
index.blade.php13 13GitHub Commit : 建立 post.index view1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!DOCTYPE html>
<html>
<head>
<title>Posts</title>
</head>
<body>
@foreach($posts as $post)
<div>
<h2>{{ $post->title }}</h2>
<h2>{{ $post->description }}</h2>
<h2>{{ $post->content }}</h2>
</div>
<hr>
@endforeach
</body>
</html>
簡單使用 @foreach
與 binding 將資料顯示。
得到第 1 個 綠燈,完成 routes
與 PostController
與 post.index
view。
重構 PostService
到目前為止,已經 綠燈 達成需求,但發現將 PostService
放在 app/Services
下似乎不妥,想將 PostServie
重構放在 app/Services/Post
目錄下。
根據 PSR-4
,PHP 的 namespace 必須與目錄相同,也就是說除了將 PostService
放到 app/Services/Post
目錄下外,以下程式碼必須修改 :
PostService
的 namespace 必須修改。- 單元測試的
PostServiceTest
的use
必須修改。 - Controller 的
PostController
的use
必須修改。
本文只是很簡單的範例,已經要改 3 個地方,實務上會更複雜,要改的地方會更多,還可能沒改到或改錯。
這時候就要使用 PhpStorm 的重構了。
將滑鼠游標放在要重構的 PostService
的 class 名稱上,按熱鍵 ⌃ + T,選擇 Move
。
顯示 Move Class
對話框。
- 在
Move Class PostService to namespace
填入新的 namespace :App\Services\Post
,PhpStorm 會自動在Target destination directory
加上Post
目錄。 - 將
Search in comments and strings
與Search for text occurrences
都打勾。 - 按下
Preview
可以先看一下 PhpStorm 將做哪些重構,按Refactor
則直接重構。
PhpStorm 預告將對 PostController
、PostServiceTest
與 PostService
做重構,與我們的預期相同。
若發現 PhpStorm 失去水準判斷錯誤,可以將其選擇按右鍵將其 Excluded
或 Remove
掉。
最後按 Do Refactor
開始重構。
重構完趕快跑單元測試與整合測試,確認 PhpStorm 沒有改壞。
重構 PostRepository
前一個例子,是將 PostService
單一 class 重構其 namespace,實務上還有另外一種應用,是將一個目錄下所有 class 重構成另外一個 namespace。
目前想將 app/Repositories
下所有的 class 重構到 app/Repositories/Post
目錄下。
根據 PSR-4
,PHP 的 namespace 必須與目錄相同,也就是說除了將 PostRepository
放到 app/Repositories/Post
目錄下外,以下程式碼必須修改 :
PostRepository
的 namespace 必須修改。- Service 的
PostService
的use
必須修改。
若目錄下有很多 class,要改的地方會更多,還可能沒改到或改錯。
這時候就要使用 PhpStorm 的重構了。
選擇要重構目錄下的其中一個檔案開啟,本例 app/Repositories
目錄下只有 PostRepository
。
將滑鼠游標放在要重構的 PostRepository
的 namespace 名稱上,按熱鍵 ⌃ + T,選擇 Move
。
顯示 Move Namespace
對話框。
- 在
New Namespace name
填入新的 namespace :App\Repositories\Post
,PhpStorm 會自動在Target destination directory
加上Post
目錄。 - 將
Search in comments and strings
與Search for text occurrences
都打勾。 - 按下
Preview
可以先看一下 PhpStorm 將做哪些重構,按Refactor
則直接重構。
PhpStorm 預告將對 PostRepository
與 PostService
做重構,與我們的預期相同。
若發現 PhpStorm 失去水準判斷錯誤,可以將其選擇按右鍵將其 Excluded
或 Remove
掉。
最後按 Do Refactor
開始重構。
重構完趕快跑單元測試與整合測試,確認 PhpStorm 沒有改壞。
Conclusion
- 重構單一 class,是將游標放在 class 名稱上。
- 重構一目錄下所有 class,是將游標放在 namespace 名稱上。
- Laravel 5 大量使用 namespace 後,只要改 namespace 就是大家永遠的痛,透過 PhpStorm 的重構,與自己寫的單元測試與整合測試保護後,再也不用害怕改 namespace 了。
Sample Code
完整的範例可以在我的 GitHub 上找到。