將 Closure 內化到自己的程式碼中

PHP 5.3 正式將 closure 帶入 PHP,到了 Laravel 5,我們看到了 Laravel 大量使用 closure,除了在配合 Laravel 的地方使用 closure 外,我們該如何將 closure 加以內化,進而活用在自己的程式碼中呢? 我們將實際探索 Laravel 原始碼,學習 Taylor Otwell 如何使用 closure。

Version


PHP 7.0.0
Laravel 5.2.31

Closure vs. Callback


一般人想到 closure,就會想到 callback,這是由我們寫 jQuery 與 Node.js 的所得到的經驗,將 closure 用在 event 或非同步的狀況下。1 1關於 PHP 的 closure 基本語法,詳細請參考 如何使用 Closure?

由於 PHP 是同步的,所以很多人不知道該怎麼在 PHP 使用 closure,實際探索 Laravel 原始碼後我們會發現,儘管在同步的 PHP,Laravel 內部仍有 3 個地方會使用 closure 實現 :

  1. 由使用者執行一段邏輯
  2. 由使用者決定一個布林
  3. 由使用者改變一個物件

我們將一一的詳細討論,學習 Laravel 怎麼活用 closure,再以實際範例重構成 closure。

由使用者執行一段邏輯


Schema::create()

學習 Laravel,大家第一個會碰到的 closure,大概會在 migration 的 Schema::create() :

database/migrations/create_users_table.php
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
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{

/**
* Run the migrations.
*
* @return void
*/

public function up()
{

Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/

public function down()
{

Schema::drop('users');
}
}

13 行

1
2
3
4
5
6
7
8
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});

我們發現 Schema::create() 的第 2 個參數要求我們傳入一個 closure,且 closure 還傳入了 Blueprint $table,為什麼會有這樣怪異的寫法呢?

若去 trace Laravel 原始碼,會發現 Schema::create() 是長這樣 :

database/migrations/create_users_table.php
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
namespace Illuminate\Database\Schema;

use Closure;
use Illuminate\Database\Connection;

class Builder
{

/**
* Create a new table on the schema.
*
* @param string $table
* @param \Closure $callback
* @return \Illuminate\Database\Schema\Blueprint
*/

public function create($table, Closure $callback)
{

$blueprint = $this->createBlueprint($table);

$blueprint->create();

$callback($blueprint);

$this->build($blueprint);
}
}

create() 共執行 4 個函式,其中 3 個函式為 Laravel 自己要執行的邏輯,只有 21 行的 $callback($blueprint) 是要執行使用者的邏輯。

其中 Blueprint $table 就是在此由 $blueprint 所代入的。

當函式內,中間有一段邏輯,必須由使用者決定,而非函式本身所決定,可要求使用者傳入 closure 並執行之。

我們來舉一個實務上的例子比較容易理解。

實際範例

測試案例

  • 顯示出 Post model 的所有文章。

單元測試
PostServiceTest.php 2 2GitHub Commit : 單元測試 : 顯示所有 Post()

tests/Unit/PostServiceTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use App\Services\PostService;

class PostServiceTest extends TestCase
{

/** @test */
public function 顯示所有Post()
{

/** arrange */
$expected = 10;
$target = App::make(PostService::class);

/** act */
$actual = $target->displayAllPosts();

/** assert */
$this->assertEquals($expected, $actual);
}
}

驗證傳回文章筆數是否為 10 筆。

PostService.php 3 3GitHub Commit : 新增 displayAllPost()

app/Services/PostService.php
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
namespace App\Services;

use App\Repositories\PostRepository;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllPosts()
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}
}

第 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;
}

PostRepository 依賴注入。

21 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @return int
*/

public function displayAllPosts()
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}

PostRepository->getAllPosts() 取得 $posts collection。

foreach 整個 collection,透過 echo() 顯示訊息。

最後回傳所有文章的筆數。

PostRepository.php 4 4GitHub Commit : 新增 getAllPosts()

app/Services/PostRepository.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Repositories;

use App\Post;
use Illuminate\Database\Eloquent\Collection;

class PostRepository
{

/**
* @return Collection|Post[]
*/

public function getAllPosts()
{

return Post::all();
}
}

傳回所有文章。

目前為止完全符合需求,會得到第 1 個 綠燈

使用 Closure 重構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @return int
*/

public function displayAllPosts()
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}

根據需求,我們發現只有第 9 行與第 10 行會根據使用者需求而異動,其他行數都不會異動,因此我們想將其他行數提煉出來。

PostService.php 5 5GitHub Commit : 重構 : 使用 Closure 重構成 getAllPosts()

app/Services/PostService.php
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
namespace App\Services;

use App\Post;
use App\Repositories\PostRepository;
use Closure;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllPosts()
{

return $this->getAllPosts(function (Post $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
});
}

private function getAllPosts(Closure $closure)
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$closure($post);
}

return $posts->count();
}
}

34 行

1
2
3
4
5
6
7
8
9
10
private function getAllPosts(Closure $closure)
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$closure($post);
}

return $posts->count();
}

提煉出 getAllPosts() 後,我們發現 foreach() 內關於顯示邏輯部分,會根據使用者需求而異動,根據之前的經驗 :

當函式內,中間有一段邏輯,必須由使用者決定,而非函式本身所決定,可要求使用者傳入 closure 並執行之。

第 6 行我們以 $closure 取代,因為顯示需要 Post model 資料, 因此傳入 $post

23 行

1
2
3
4
5
6
7
8
9
10
/**
* @return int
*/

public function displayAllPosts()
{

return $this->getAllPosts(function (Post $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
});
}

將原本由 echo() 顯示資料部分,改由 closure 傳進 getAllPosts()

重構後趕快執行測試,看看有沒有重構壞掉。

目前為止完全符合需求,會得到第 2 個 綠燈

使用 Collection 重構

為什麼要自己寫 getAllPosts() 呢? Laravel 的 collection 不是有自帶 each() 嗎?

沒錯,因為實務上太多在 collection 內執行其他 closure 的需求,Laravel 的 collection 已經幫我們準備了 each() 函式。

PostService.php 6 6GitHub Commit : 重構 : 使用 Collection->each()

app/Services/PostService.php
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
namespace App\Services;

use App\Post;
use App\Repositories\PostRepository;
use Closure;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllPosts()
{

return $this->postRepository->getAllPosts()
->each(function (Post $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
})->count();
}
}

由於 Eloquent 傳回的就是 collection,因此可以將 closure 傳入 each(),這樣我們連 foreach() 也不用寫。

由於 collection 的函式都支援 fluent API 風格,也是回傳 collection,因此可以繼續下 count() 加以回傳。

重構後趕快執行測試,看看有沒有重構壞掉。

目前為止完全符合需求,會得到第 3 個 綠燈

為什麼 each() 可以取代自己寫的 foreach() 呢?

Collection.php

Illuminate/Support/Collection.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{

/**
* Execute a callback over each item.
*
* @param callable $callback
* @return $this
*/

public function each(callable $callback)
{

foreach ($this->items as $key => $item) {
if ($callback($item, $key) === false) {
break;
}
}

return $this;
}
}

若進去 trace Laravel 原始碼,會發現 each() 內幫我們實現了 foreach(),而我們傳進去的 closure 正好在 foreach() 內執行,若 closure 傳回 false,還可以中斷 foreach()

其中 $callback() 剛好是藏在函式中,由使用者決定的邏輯,而 $callback 之外,都是函式自己的邏輯,也再次應證之前的結論 :

當函式內,中間有一段邏輯,必須由使用者決定,而非函式本身所決定,可要求使用者傳入 closure 並執行之。

由使用者決定一個布林


Collection->first()

有些 Laravel 函式,會希望我們傳進一個 closure,回傳 true 或 false,如 collection 的 first()

1
2
3
4
5
collect([1, 2, 3, 4])->first(function ($key, $value) {
return $value > 2;
});

// 3

first() 會傳回符合條件的第一個元素,但條件必須我們自己用 closure 傳進去,為什麼會有這樣怪異的寫法呢?

若去 trace Laravel 原始碼,會發現 first() 長這樣 :

Illuminate/Support/Arr.php
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
class Arr
{

/**
* Return the first element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
* @return mixed
*/

public static function first($array, callable $callback = null, $default = null)
{

if (is_null($callback)) {
return empty($array) ? value($default) : reset($array);
}

foreach ($array as $key => $value) {
if (call_user_func($callback, $key, $value)) {
return $value;
}
}

return value($default);
}
}

17 行

1
2
3
4
5
foreach ($array as $key => $value) {
if (call_user_func($callback, $key, $value)) {
return $value;
}
}

Laravel 會 foreach() 整個陣列,由 call_user_func() 去執行 closure,遇到第一個 closure 傳回 true 的條件,就 return 離開迴圈。

當函式內,中間有一個布林,必須由使用者決定,而非函式本身所決定,可要求使用者傳入 closure 並執行之。

我們來舉一個實務上的例子比較容易理解。

實際範例

測試案例

  • 顯示出 Post model 的所有奇數 ID 文章。

單元測試
PostServiceTest.php 7 7GitHub Commit : 單元測試 : 顯示所有奇數 ID 文章

tests/Unit/PostServiceTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use App\Services\PostService;

class PostServiceTest extends TestCase
{

/** @test */
public function 顯示奇數IDPost()
{

/** arrange */
$expected = 5;
$target = App::make(PostService::class);

/** act */
$actual = $target->displayAllOddPosts();

/** assert */
$this->assertEquals($expected, $actual);
}
}

驗證傳回文章筆數是否為 5 筆。

PostService.php 8 8GitHub Commit : 新增 displayAllOddPost()

app/Services/PostService.php
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
namespace App\Services;

use App\Repositories\PostRepository;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllOddPosts()
{

$posts = $this->postRepository->getAllPosts();
$cnt = 0;

foreach ($posts as $post) {
if ($post->id % 2) {
$cnt++;
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}
}

return $cnt;
}
}

21 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @return int
*/

public function displayAllOddPosts()
{

$posts = $this->postRepository->getAllPosts();
$cnt = 0;

foreach ($posts as $post) {
if ($post->id % 2) {
$cnt++;
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}
}

return $cnt;
}

PostRepository->getAllPosts() 取得 $posts collection。

foreach 整個 collection,由 if ($post->id % 2) 做判斷,若 ID 為奇數,則 $cnt++ 並顯示訊息。

最後回傳所有文章的筆數。

目前為止完全符合需求,會得到第 1 個 綠燈

使用 Closure 重構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @return int
*/

public function displayAllOddPosts()
{

$posts = $this->postRepository->getAllPosts();
$cnt = 0;

foreach ($posts as $post) {
if ($post->id % 2) {
$cnt++;
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}
}

return $cnt;
}

根據需求,我們發現只有第 10 行的 if ($post->id % 2) 會根據使用者需求而異動,其他行數都不會異動,因此我們想將其他行數提煉出來。

PostService.php 9 9GitHub Commit : 重構 : 使用 Closure 重構成 filterAllOddPosts()

app/Services/PostService.php
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
namespace App\Services;

use App\Post;
use App\Repositories\PostRepository;
use Closure;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllOddPosts()
{

return $this->filterAllOddPosts(function (Post $post) {
return ($post->id % 2);
});
}

/**
* @param Closure $closure
* @return int
*/

private function filterAllOddPosts(Closure $closure)
{

$posts = $this->postRepository->getAllPosts();
$cnt = 0;

foreach ($posts as $post) {
if ($closure($post)) {
$cnt++;
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}
}

return $cnt;
}
}

37 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param Closure $closure
* @return int
*/

private function filterAllOddPosts(Closure $closure)
{

$posts = $this->postRepository->getAllPosts();
$cnt = 0;

foreach ($posts as $post) {
if ($closure($post)) {
$cnt++;
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}
}

return $cnt;
}

提煉出 filterAllOddPosts() 後,我們發現 foreach() 內關於是否為奇數部分,會根據使用者需求而異動,根據之前的經驗 :

當函式內,中間有一個布林,必須由使用者決定,而非函式本身所決定,可要求使用者傳入 closure 並執行之。

第 11 行我們以 $closure 取代,因為判斷需要 Post model 資料, 因此傳入 $post

23 行

1
2
3
4
5
6
7
8
9
/**
* @return int
*/

public function displayAllOddPosts()
{

return $this->filterAllOddPosts(function (Post $post) {
return ($post->id % 2);
});
}

將原本由 if ($post->id % 2) 判斷奇數部分,改由 closure 傳進 filterAllOddPosts()

重構後趕快執行測試,看看有沒有重構壞掉。

目前為止完全符合需求,會得到第 2 個 綠燈

使用 Collection 重構

為什麼要自己寫 filterAllOddPosts() 呢? Laravel 的 collection 不是有自帶 filter() 嗎?

沒錯,因為實務上太多在 collection 內執行 filter 的需求,Laravel 的 collection 已經幫我們準備了 filter() 函式。

PostService.php 10 10GitHub Commit : 重構 : 使用 Collection->filter()

app/Services/PostService.php
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
namespace App\Services;

use App\Post;
use App\Repositories\PostRepository;
use Closure;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllOddPosts()
{

return $this->postRepository->getAllPosts()
->filter(function ($value) {
return ($value->id % 2);
})
->each(function (Post $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
})->count();
}
}

由於 Eloquent 傳回的就是 collection,因此可以將 closure 傳入 filter(),這樣我們連 foreach() 也不用寫。

由於 collection 的函式都支援 fluent API 風格,也是回傳 collection,因此可以繼續下 each() 顯示資訊。

最後再用 count() 回傳筆數。

重構後趕快執行測試,看看有沒有重構壞掉。

目前為止完全符合需求,會得到第 3 個 綠燈

為什麼 filter() 可以取代自己寫的 foreach() 呢?

Collection.php

Illuminate/Support/Collection.php
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
class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{

/**
* Run a filter over each of the items.
*
* @param callable|null $callback
* @return static
*/

public function filter(callable $callback = null)
{

if ($callback) {
$return = [];

foreach ($this->items as $key => $value) {
if ($callback($value, $key)) {
$return[$key] = $value;
}
}

return new static($return);
}

return new static(array_filter($this->items));
}
}

若進去 trace Laravel 原始碼,會發現 filter() 內幫我們實現了 foreach(),而我們傳進去的 closure 正好在 foreach() 內執行 if ($callback($value, $key))

其中 $callback() 剛好是藏在函式的 if 內,由使用者決定的邏輯,而 $callback 之外,都是函式自己的邏輯,也再次應證之前的結論 :

當函式內,中間有一個布林,必須由使用者決定,而非函式本身所決定,可要求使用者傳入 closure 並執行之。

由使用者改變一個物件


Where Nested Query

1
2
3
SELECT * 
FROM posts
WHERE (status = 0 or status = 1)

若要寫出以上的巢狀 query,在 Eloquent 我們必須這樣寫。11 11關於巢狀 query 寫法,詳細請參考 如何在 Eloquent 建立一個含 or 的 where 條件式?

1
2
3
4
Post::where(function ($query) {
$query->where('status', 0)
->orWhere('status', 1);
})->get();

Eloquent 要求我們在 where() 傳入一個 closure,在 closure 的 $query 加入巢狀 query,,為什麼會有這樣怪異的寫法呢?

若去 trace Laravel 原始碼,會發現 where() 長這樣 :

Illuminate/Database/Eloquent/Builder.php
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
class Builder
{

/**
* Add a basic where clause to the query.
*
* @param string $column
* @param string $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/

public function where($column, $operator = null, $value = null, $boolean = 'and')
{

if ($column instanceof Closure) {
$query = $this->model->newQueryWithoutScopes();

call_user_func($column, $query);

$this->query->addNestedWhereQuery($query->getQuery(), $boolean);
} else {
call_user_func_array([$this->query, 'where'], func_get_args());
}

return $this;
}
}

15 行

1
$query = $this->model->newQueryWithoutScopes();

建立一個新的 query builder 物件。

17 行

1
call_user_func($column, $query);

$query 物件傳進 closure,由使用者將巢狀 query 加到 $query 物件內。

19 行

1
$this->query->addNestedWhereQuery($query->getQuery(), $boolean);

再將使用者修改過的 $query 物件加到 addNestedWhereQuery() 函式內。

當函式內,中間有一個物件,必須由使用者修改,而非函式本身所能修改,可要求使用者傳入 closure 並執行之。

我們來舉一個實務上的例子比較容易理解。

實際範例

測試案例

  • Post model 的 title 全部改成 Laravel,並顯示之。

單元測試
PostServiceTest.php 12 12GitHub Commit : 單元測試 : title 全部改成 Laravel 並顯示

tests/Unit/PostServiceTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use App\Services\PostService;

class PostServiceTest extends TestCase
{

/** @test */
public function title全部改成Laravel()
{

/** arrange */
$expected = 10;
$target = App::make(PostService::class);

/** act */
$actual = $target->displayAllPostsWithLaravel();

/** assert */
$this->assertEquals($expected, $actual);
}
}

驗證傳回文章筆數是否為 10 筆。

PostService.php 13 13GitHub Commit : 新增 displayAllPostWithLaravel()

app/Services/PostService.php
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
namespace App\Services;

use App\Repositories\PostRepository;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllPostsWithLaravel()
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$post->title = 'Laravel';
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}
}

21 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @return int
*/

public function displayAllPostsWithLaravel()
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$post->title = 'Laravel';
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}

PostRepository->getAllPosts() 取得 $posts collection。

foreach 整個 collection,由 $post->title = 'Laravel'$post 變數做修改,並顯示訊息。

最後回傳所有文章的筆數。

目前為止完全符合需求,會得到第 1 個 綠燈

使用 Closure 重構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @return int
*/

public function displayAllPostsWithLaravel()
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$post->title = 'Laravel';
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}

根據需求,我們發現只有第 9 行的 $post->title = 'Laravel'會根據使用者需求而異動,其他行數都不會異動,因此我們想將其他行數提煉出來。

PostService.php 14 14GitHub Commit : 重構 : 使用 Closure 重構成 replaceAllOddPostsWithLaravel()

app/Services/PostService.php
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
namespace App\Services;

use App\Post;
use App\Repositories\PostRepository;
use Closure;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllPostsWithLaravel()
{

return $this->replaceAllPostsWithLaravel(function (Post $post) {
$post->title = 'Laravel';
});
}

/**
* @param Closure $closure
* @return int
*/

private function replaceAllPostsWithLaravel(Closure $closure)
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$closure($post);
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}
}

33 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @param Closure $closure
* @return int
*/

private function replaceAllPostsWithLaravel(Closure $closure)
{

$posts = $this->postRepository->getAllPosts();

foreach ($posts as $post) {
$closure($post);
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
}

return $posts->count();
}

提煉出 replaceAllPostsWithLaravel() 後,我們發現 foreach() 內修改 $post 物件部分,會根據使用者需求而異動,根據之前的經驗 :

當函式內,中間有一個物件,必須由使用者修改,而非函式本身所能修改,可要求使用者傳入 closure 並執行之。

第 10 行我們以 $closure 取代,因為需要修改 Post model 資料, 因此傳入 $post 物件。

23 行

1
2
3
4
5
6
7
8
9
/**
* @return int
*/

public function displayAllPostsWithLaravel()
{

return $this->replaceAllPostsWithLaravel(function (Post $post) {
$post->title = 'Laravel';
});
}

將原本由 $post->title = 'Laravel' 修改物件部分,改由 closure 傳進 replaceAllOddPostsWithLaravel()

重構後趕快執行測試,看看有沒有重構壞掉。

目前為止完全符合需求,會得到第 2 個 綠燈

使用 Collection 重構

為什麼要自己寫 replaceAllOddPostsWithLaravel() 呢? Laravel 的 collection 不是有自帶 each() 嗎?

沒錯,因為實務上太多在 collection 內修改物件的需求,Laravel 的 collection 已經幫我們準備了 each() 函式。

PostService.php 15 15GitHub Commit : 重構 : 使用 Collection->each()

app/Services/PostService.php
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
namespace App\Services;

use App\Post;
use App\Repositories\PostRepository;
use Closure;

class PostService
{

/**
* @var PostRepository
*/

private $postRepository;

/**
* PostService constructor.
* @param PostRepository $postRepository
*/

public function __construct(PostRepository $postRepository)
{

$this->postRepository = $postRepository;
}

/**
* @return int
*/

public function displayAllPostsWithLaravel()
{

return $this->postRepository->getAllPosts()
->each(function (Post $post) {
$post->title = 'Laravel';
})
->each(function (Post $post) {
$txt = "{$post->id} : {$post->title}" . PHP_EOL;
echo($txt);
})
->count();
}
}

由於 Eloquent 傳回的就是 collection,因此可以將 closure 傳入 each(),並將 $post物件傳入 closure,這樣我們連 foreach() 也不用寫。

由於 collection 的函式都支援 fluent API 風格,也是回傳 collection,因此可以繼續下 each() 顯示資訊。

最後再用 count() 回傳筆數。

重構後趕快執行測試,看看有沒有重構壞掉。

目前為止完全符合需求,會得到第 3 個 綠燈

為什麼 each() 可以修改 $post 物件呢?

雖然 PHP 是以 pass by value 傳遞變數,但別忘了對於物件,PHP 是 pass by reference,因此在 closure 內可以直接修改 $post 物件。

Collection 的 each() 原始碼,之前已經分析過,就不再贅述,不過不管是使用 each(),或是自己建立的 replaceAllOddPostsWithLaravel(),其心法都是相同的 :

當函式內,中間有一個物件,必須由使用者修改,而非函式本身所能修改,可要求使用者傳入 closure 並執行之。

Conclusion


  • 若重複的是一段邏輯,可將該邏輯提煉為函式;若重複的是一段邏輯以外的邏輯,可將以外的邏輯提煉為函式,而將該段邏輯成為 closure 傳入。
  • 若可以使用 collection 提供的函式,優先使用之,其次再自己提煉函式,傳入 closure。

Sample Code


完整的範例可以在我的 GitHub 上找到。

2016-04-30