2016年9月2日 星期五

如何在PhpStorm活用PHPDoc?

初學者使用PhpStorm開啟Laravel專案時,馬上會發現原來的程式碼出現一堆反白警告,事實上這些都是有意義的,只是初學者常常不知道怎麼處理,而忽略這些反白警告,除此之外,PhpStorm有強大的語法提示功能,讓你不用去死記物件有哪些field與method,只要用選的即可,可大幅增加開發效率,也不用擔心typo打錯。而PhpStorm威力的背後,就是基於PHPDoc

Version


PHP 7.0.0
Laravel 5.2.22
PhpStorm 10.0.3
Laravel IDE Helper 2.1.4

使用PhpStorm開啟Laravel


第一次使用PhpStorm開啟Laravel時,原來平靜的程式碼在很多地方都出現了許多反白 :

Route

PhpStorm抱怨RouteUndefined class Route

Validator

AuthController中,PhpStorm抱怨ValidatorUndefined class Validator

Schema Builder

在user的migration中,PhpStorm抱怨SchemaUndefined class Schema
還抱怨了unique()Method not found
之所以會如此,root cause有兩個 :
  1. RouteValidatorSchema使用了Laravel特有的Facade機制,導致PhpStorm無法解析。
  2. unique()使用了PHP獨有的Overloading機制,可以動態產生property與method,也因為是動態產生,所以PhpStorm無法解析。11PHP的Overloading與一般物件導向語言所謂的Overloading不同,詳細請參考PHP與C#語法快速導覽之Overloading

Laravel IDE Helper


有了問題就要解決,我們先來解決第一個問題 : Laravel特有的Facade機制。
Laravel IDE Helper讓PhpStorm看得懂Laravel Facade,還增加了許多其他的支援。22Laravel IDE Helper作者Barry vd. Heuvel的另一個大作Laravel Debugbar,詳細請參考如何使用Laravel Debugbar?

安裝

1
oomusou@mac:~/MyProject$ composer require barryvdh/laravel-ide-helper --dev
使用composer安裝Laravel IDE Helper,因為此套件只會在開發使用,可以加上--dev參數。33關於--dev參數,詳細請參考如何使用Laravel Debugbar#使用Composer安裝
1
oomusou@mac:~/MyProject$ composer require doctrine/dbal --dev
Laravel IDE Helper會透過doctrine/dbal去抓table的schema,替model加上欄位註解。

Service Provider

1
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
config/app.php中加入IdeHelperServiceProvider

設定檔

1
oomusou@mac:~/MyProject$ php artisan vendor:publish --provider="Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider" --tag=config
產生Laravel IDE Helper自己的設定檔,位在config/ide-helper.php
建議將include_helpers設定為true,讓Laravel IDE Helper幫我們建立helper function的註解。
Laravel 5預設將model放在app目錄下,若你有自己的model目錄,請修改此設定。

資料庫連線

有些Facade與資料庫有關,先確定專案已經與資料庫順利連線。

Facade

1
oomusou@mac:~/MyProject$ php artisan ide-helper:generate
建立Laravel Facade的PHPDoc,產生了_ide_helper.php
可以發現在專案根目錄多了_ide_helper.php,我們找到了Route class與get(),發現多加了PHPDoc註解,讓PhpStorm知道get()的參數資訊與回傳型別,這提供了PhpStorm幫我們做語法檢查的根據。
Route不再反白。
Validator也不再反白。
Schema也不再反白,不過unique()還是反白,稍後會解決。
 若將來透過composer update更新Laravel,是否還要重新產生_ide_helper.php呢?

composer.json

1
2
3
4
5
6
7
"scripts":{
    "post-update-cmd": [
        "php artisan clear-compiled",
        "php artisan ide-helper:generate",
        "php artisan optimize"
    ]
},
只要在composer.jsonpost-update-cmd改成如上圖所示,以後只要composer update更新Laravel,就會自動重新建立_ide_helper.php

Model

資料庫欄位名稱是開發過程的另外一個痛,傳統都要另外一個視窗開著phpMyAdmin或Sequel Pro,一邊查詢資料庫欄位一邊寫程式,若PhpStorm能替我們對model的欄位名稱做語法提示,讓我們用選的,那就太好了。
1
oomusou@mac:~/MyProject$ php artisan ide-helper:models
Laravel IDE Helper提供兩種方式幫你建立model的PHPDoc,預設是產生一個_ide_helper_models.php,也可以直接將PHPDoc寫在原本的model檔內,我們打yes,選擇直接在model內建立PHPDoc。44理論上選擇預設的_ide_helper_models.php也不是問題,不過因為在_ide_helper_models.php也定義了User class,所以在repository內use User時,會出現Multiple definitions for class User的警告,所以才選擇將PHPDoc直接建立在model內。
Laravel IDE Helper幫我們替User model建立了PHPDoc : 
  1. 所有的資料庫欄位名稱都加上了@property註解。
  2. 所有的資料庫欄位名稱的where都加上了@method註解。55這些method都是Eloquent根據資料庫欄位,使用Overloading機制動態產生的method,因此PhpStorm無法自動抓到,必須手動寫PHPDoc的@method
app/Repositories/UserRepository.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
namespace App\Repositories;

use App\User;

class UserRepository
{
    /** @var User */
    private $user;

    /**
     * UserRepository constructor.
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * 回傳第一位User
     *
     * @return User
     */
    public function getFirstUser() : User
    {
        return $this->user->all()->first();
    }
}
19行
1
2
3
4
5
6
7
8
9
/**
 * 回傳第一位User
 *
 * @return User
 */
public function getFirstUser() : User
{
    return $this->user->all()->first();
}
我們傳回第一筆User model。
app/Services/UserService.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
namespace App\Services;

use App\Repositories\UserRepository;

class UserService
{
    /** @var UserRepository */
    private $userRepository;

    /**
     * UserService constructor.
     * @param UserRepository $userRepository
     */
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * 顯示第一筆user的姓名
     */
    public function showFirstUser()
    {
        $user = $this->userRepository->getFirstUser();
        echo($user->name);
    }
}
19行
輸入$user後,只要輸入->,就會出現資料庫欄位名稱讓你挑選,再也不用死記或靠其他工具查詢資料庫欄位名稱了。

Service Container

當我們使用service container,利用App::make()建立物件時,由於傳進去的是字串,因此PhpStorm根本不知道我們建立了什麼物件,但透過PhpStorm另外擴充的PhpStorm Advanced Metadata機制,讓我們在使用service container時,也能享受語法提示功能。66關於PhpStorm Advanced Meta,請參考PhpStorm官網的PhpStorm Advanced Metadata
不過這裡不用擔心,不需要會寫PhpStorm Advanced Metadata,因為Laravel IDE Helper幫大家寫好了。
Strategy Pattern
routes.php7 7GitHub Commit : 修改routes.php
app/Http/routes.php
1
Route::get('/show', 'UserController@show');
在routes.php加上URI與其對應的controller action。
UserController.php8 8GitHub Commit : 建立UserController.php
app/Http/Controllers/UserController.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
namespace App\Http\Controllers;

use App\Http\Requests;
use App\Services\UserService;

class UserController extends Controller
{
    /** @var UserService */
    private $userService;

    /**
     * UserController constructor.
     * @param UserService $userService
     */
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function show()
    {
        $this->userService->show('admin');
    }
}
第8行
1
2
3
4
5
6
7
8
9
10
11
/** @var UserService */
private $userService;

/**
 * UserController constructor.
 * @param UserService $userService
 */
public function __construct(UserService $userService)
{
    $this->userService = $userService;
}
注入UserService
20行
1
2
3
4
public function show()
{
    $this->userService->show('admin');
}
show()呼叫$this->userServiceshow(),並將admin變數傳入。
AbstractUser.php99GitHub Commit : 建立AbstractUser.php
app/Services/User/AbstractUser.php
1
2
3
4
5
6
namespace App\Services\User;

abstract class AbstractUser
{
    abstract public function show();
}
abstract class定義show(),如此PhpStorm就能幫我們做語法提示與語法檢查了。
Admin.php1010GitHub Commit : 建立Admin.php
app/Services/User/Admin.php
1
2
3
4
5
6
7
8
9
namespace App\Services\User;

class Admin extends AbstractUser
{
    public function show()
    {
        echo('I am a admin');
    }
}
Admin繼承AbstractUser,因為之前定義了show() abstract method,所以必須在此實作show()
Customer.php1111GitHub Commit : 建立Customer.php
app/Services/User/Customer.php
1
2
3
4
5
6
7
8
9
namespace App\Services\User;

class Customer extends AbstractUser
{
    public function show()
    {
        echo('I am a customer');
    }
}
Customer繼承AbstractUser,因為之前定義了show() abstract method,所以必須在此實作show()
UserService.php12 12GitHub Commit : 建立UserService.php
app/Services/UserService.php
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace App\Services;

use App;
use App\Services\User\AbstractUser;

class UserService
{
    public function show(string $type)
    {
        App::bind(AbstractUser::class, 'App\Services\User\\' . ucfirst($type));
        App::make(AbstractUser::class)->show();
    }
}
第10行
透過App::make()產生的物件,PhpStorm並無法提供語法提示。
產生PhpStorm Advanced Metadata1313GitHub Commit : 建立.phpstorm.meta.php
1
oomusou@mac:~/MyProject$ php artisan ide-helper:meta
Laravel IDE Helper幫我們建立了PhpStorm Advanced Metadata,檔名為.phpstorm.meta.php
重新啟動PhpStorm
有了.phpstorm.meta.phpApp::make()就會參考此檔,自動顯示語法提示。1414.phpstorm.meta.php中定義了3種方式會啟動語法提示 : ArrayAccess styleApp::make()app(),其中Laravel IDE Helper在該檔中幫我們建立了很多class與interaface的別名,讓我們可以用更簡短的名稱使用service container。

Laravel Plugin


我們還需要安裝Laravel Plugin,它包含了一些Laravel IDE Helper所沒有提供的語法提示功能。

安裝

PhpStorm -> Preferences -> Plugins
輸入Laravel,按Browse
選擇Laravel Plugin,按Install安裝。
安裝完,按Restart PhpStorm重新啟動。
PhpStorm -> Preferences -> Other Settings -> Laravel Plugin
Enable plugin for this projectUse AutoPopop for completion打勾。
再次重新啟動PhpStorm。
 這一步非常重要,很多人安裝完Laravel Plugin後,因為沒有Enable,導致Laravel Plugin從來沒有啟動過。
 Laravel Plugin只要安裝一次即可,不過每次若開新Laravel專案,必須重新enable一次,否則Laravel Plugin不會啟動,這點很容易忽略。

Controller

routes.php中,已經可以選擇controller與action。

Route

在blade使用route()時,已經可以抓到在routes.php所定義的route別名。

View

在controller回傳view時,已經可以抓到在resources/views目錄下所定義的view。
除此之外,在blade中如@include也可以抓到其他blade。

config::get()

config::get()已經可以抓到array的key值了。

Why PHPDoc?


Laravel IDE Helper幫我們做了很多事情,讓PHP在PhpStorm可以如強型別語言一樣使用語法提示與語法檢查,但其黑魔法在哪裡呢?
如C#這種強型別語言,Visual Studio之所以能即時提供語法提示與語法檢查,因為當你在Visual Studio寫程式時,C# compiler就在背景默默地編譯,因此可以及時提供語法提示,且及時顯示語法檢查的警告,也因此Visual Studio需要更高檔的硬體支援。
但PHP沒有compiler,必須執行了才知道結果,所以PhpStorm所有的語法檢查與語法提示資訊都來自於PHPDoc,甚至可以說,PhpStorm是在檢查你的PHPDoc,而不是在檢查PHP
Laravel IDE Helper幫我們做的,就是將Laravel部分的PHPDoc補齊。
接下來要談的,是你自己寫程式的部分,也就是Laravel IDE Helper沒有辦法幫你的部分,必須自己寫PHPDoc。1515若你對更多的PHPDoc指令有興趣,詳細請參考如何使用PHPDoc寫註解?

自己寫的class


手動建立PHPDoc

  1. 在PhpStorm輸入/**,然後按下↩,PhpStorm會自動依據當時的游標的位置產生適當的PHPDoc blocks。
  2. 按熱鍵⌘ + N,會產生Generate選單,選擇PHPDoc Blocks
  3. 適當時機按熱鍵⌥ + ↩,會出現Generate PHPDoc for ...。如剛建立完class, property或method時。

Fields

語法
1
/** @var 型別 [變數名稱] [註解] */
  • 型別可以是PHP原生型別,class,interface或trait。
  • 假如下一行就是該變數,可以省略變數名稱
  • 可選擇性對該變數加上註解
自動建立PHPDoc
實務上建立field會有2種方式 :
  1. 由constructor injection建立field。(如注入service, repository)
  2. 由setter與getter建立field。(如strategy, state, adpater,decorator pattern..設定物件)
PhpStorm都提供了快速的方式自動建立PHPDoc。
由Constructor Injection建立Field
新建立了PostService,按熱鍵⌃ + N,顯示Generate視窗,選擇Constructor...
PhpStorm替我們自動產生了constructor框架,我們想藉由constructor注入UserService,輸入UserSer就可以發現PhpStorm的語法提示已經出現了UserService,按↩選擇之。
在constructor注入了$userService
$userService之後按熱鍵⌥ + ↩,顯示Show Intention Actions視窗,選擇Update PHPDoc Comment
由於在constructor的parameter已經有了type hint,因此產生的PHPDoc也自動加上了UserService型別。
$userService之後按熱鍵⌥ + ↩,顯示Show Intention Actions視窗,選擇Initialize fields
選擇要建立field的constructor parameter。
PhpStorm不只幫我們在constructor內補上code,還一併幫我們將field建好,而且PHPDoc也一併建立完成,還自動加上了型別。
 藉由PhpStorm這種流程建立field,不僅完全不需要打字,而且連PHPDoc也一併自動寫好,非常方便。
由Setter與Getter建立Field
還有另外一類field是使用setter/getter建立,這種field就必須先手動建立field。
新建立了PostService,按熱鍵⌃ + N,顯示Generate視窗,選擇PHPDoc Blocks...
選擇要建立PHPDoc的field。
PhpStorm自動幫我們產生了@var
輸入Pay就可以發現PhpStorm的語法提示已經出現了PaymentInterface,按↩選擇之。
加上了PaymentInterface型別。
按熱鍵⌃ + N,顯示Generate視窗,選擇Getters and Setters...
選擇要建立getter與setter的field。
PhpStorm自動幫我們建立了$paymentService的getter與setter。
 藉由PhpStorm這種流程建立field,僅需手動建立field,補上型別後,剩下的getter與setter都可自動建立,非常方便。
Adapter Pattern
實務上在接金流時,由於各家SDK所開的API都不一樣,導致我們處理上的困難,因此我們會使用adapter pattern,將各家API抽象化成相同的API,方便service處理。
PayPalSDK.php16 16GitHub Commit : 新增PayPalSDK.php
app/Services/Payment/PayPalSDK.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Services\Payment;

class PayPalSDK
{
    /**
     * 付款
     *
     * @param int $amount
     */
    public function pay(int $amount)
    {
        echo('PayPal pay ' . $amount);
    }
}
在此為了講解方便,我們使用PayPalSDK模擬PayPal的付款API,其API為pay()
AliPaySDK.php17 17GitHub Commit : 新增AliPaySDK.php
app/Services/Payment/AliPaySDK.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Services\Payment;

class AliPaySDK
{
    /**
     * 付款
     *
     * @param $amount
     */
    public function bill($amount)
    {
        echo('AliPay bill ' . $amount);
    }
}
在此為了講解方便,我們使用AliPalSDK模擬支付寶的付款API,其API為bill()
app/Services/Payment/PaymentService.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
namespace App\Services;

use App;

class PaymentService
{
    /**
     * 設定第三方支付機構
     *
     * @param string $paymentName
     */
    public function setPayment(string $paymentName)
    {
    }

    /**
     * 付款
     *
     * @param int $amount
     */
    public function checkout(int $amount)
    {
    }
}
但我們原本PaymentService,API為setPayment()checkout(),其中setPayment()為設定第三方支付機構,而checkout()為實際付款。
可以發現我們service定義的checkout()與PayPal的pay()與支付寶的bill()都不合,因此我們需要adapter pattern做一個轉接動作。
PaymentInterface.php18 18GitHub Commit : 新增PaymentInterface.php
app/Services/Payment/PaymentInterface.php
1
2
3
4
5
6
7
8
9
10
11
12
namespace App\Services\Payment;

interface PaymentInterface
{
    /**
     * 使用金流付款
     *
     * @param int $amount
     * @return void
     */
    public function checkout(int $amount);
}
定義了PaymentInterface,為我們原本PaymentService所用的checkout()
PayPal.php19 19GitHub Commit : 新增PayPal.php
app/Services/Payment/PayPal.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
namespace App\Services\Payment;

class PayPal implements PaymentInterface
{
    /** @var PayPalSDK */
    private $payPalSDK;

    /**
     * PayPal constructor.
     * @param PayPalSDK $payPalSDK
     */
    public function __construct(PayPalSDK $payPalSDK)
    {
        $this->payPalSDK = $payPalSDK;
    }

    /**
     * 使用金流付款
     *
     * @param int $amount
     * @return void
     */
    public function checkout(int $amount)
    {
        $this->payPalSDK->pay($amount);
    }
}
PayPal扮演adapter的角色,所以必須實現PaymentInterfacecheckout()
將扮演adaptee角色的PayPalSDK注入進來。
17行
1
2
3
4
5
6
7
8
9
10
/**
 * 使用金流付款
 *
 * @param int $amount
 * @return void
 */
public function checkout(int $amount)
{
    $this->payPalSDK->pay($amount);
}
checkout()轉換成PayPalSDKpay()
AliPay.php20 20GitHub Commit : 新增AliPay.php
app/Services/Payment/PayPal.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
namespace App\Services\Payment;

class AliPay implements PaymentInterface
{
    /** @var AliPaySDK */
    private $aliPaySDK;

    /**
     * AliPay constructor.
     * @param AliPaySDK $aliPaySDK
     */
    public function __construct(AliPaySDK $aliPaySDK)
    {
        $this->aliPaySDK = $aliPaySDK;
    }

    /**
     * 使用金流付款
     *
     * @param int $amount
     * @return void
     */
    public function checkout(int $amount)
    {
        $this->aliPaySDK->bill($amount);
    }
}
AliPay扮演adapter的角色,所以必須實現PaymentInterfacecheckout()
將扮演adaptee角色的AliPaySDK注入進來。
17行
1
2
3
4
5
6
7
8
9
10
/**
 * 使用金流付款
 *
 * @param int $amount
 * @return void
 */
public function checkout(int $amount)
{
    $this->aliPaySDK->bill($amount);
}
checkout()轉換成AliPaySDKpay()
PaymentEnum.php21 21GitHub Commit : 新增PaymentEnum.php
app/Services/Payment/PaymentEnum.php
1
2
3
4
5
6
7
namespace App\Services\Payment;

abstract class PaymentEnum
{
    const PayPal = 'PayPal';
    const AliPay = 'AliPay';
}
PaymentServicesetPayment()要求我們傳字串,但由於將來會將此字串直接做App::bind(),為了減少人為typo,我們希望能提供類似強型別語言的enum,在傳入字串時只要用選的就好,不需直接打字。
不過由於PHP沒有提供enum,我們只能使用abstract class + const模擬類似enum的機制。
 PHP模擬的enum,與強型別語言enum的差別在於 : 強型別語言可以在輸入字串使用enum當型別,但PHP還是只能使用string當型別,因此無法如強型別語言透過enum幫你檢查所輸入的資料是否型別正確,不過最少在輸入字串時可以避免typo。
UserController.php22 22GitHub Commit : 修改UserController.php
app/Http/Controllers/UserController.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
namespace App\Http\Controllers;

use App\Http\Requests;
use App\Services\Payment\PaymentEnum;
use App\Services\PaymentService;
use App\Services\UserService;

class UserController extends Controller
{
    /** @var UserService  */
    private $userService;
    /** @var PaymentService */
    private $paymentService;

    /**
     * UserController constructor.
     * @param UserService $userService
     * @param PaymentService $paymentService
     */
    public function __construct(UserService $userService, PaymentService $paymentService)
    {
        $this->userService = $userService;
        $this->paymentService = $paymentService;
    }

    public function show()
    {
        $this->userService->show('Admin');
        $this->paymentService->setPayment(PaymentEnum::AliPay);
        $this->paymentService->checkout(1000);
    }
}
PaymentService也注入進來。
26行
1
2
3
4
5
6
public function show()
{
    $this->userService->show('Admin');
    $this->paymentService->setPayment(PaymentEnum::AliPay);
    $this->paymentService->checkout(1000);
}
使用$this->paymentService->setPayment()設定要用什麼第三方支付機構,這裡使用了PaymentEnum來輸入字串,可以避免人為typo,且程式可讀性也更佳。
無論使用任何第三方支付,都使用相同的$this->paymentService->checkout(),若將來有新的第三方支付方式,只需新增class實現PaymentInterface即可,也不用修改UserController,,達到開放封閉原則的要求。
PaymentService.php23 23GitHub Commit : 新增PaymentService.php
app/Services/PaymentService.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
namespace App\Services;

use App;
use App\Services\Payment\PaymentInterface;

class PaymentService
{
    /** @var PaymentInterface */
    private $payment;

    /**
     * 設定第三方支付機構
     *
     * @param string $paymentName
     */
    public function setPayment(string $paymentName)
    {
        App::bind(PaymentInterface::class, 'App\Services\Payment\\' . $paymentName);
        $this->payment = App::make(PaymentInterface::class);
    }

    /**
     * 付款
     *
     * @param int $amount
     */
    public function checkout(int $amount)
    {
        $this->payment->checkout($amount);
    }
}
11行
1
2
3
4
5
6
7
8
9
10
/**
 * 設定第三方支付機構
 *
 * @param string $paymentName
 */
public function setPayment(string $paymentName)
{
    App::bind(PaymentInterface::class, 'App\Services\Payment\\' . $paymentName);
    $this->payment = App::make(PaymentInterface::class);
}
將傳進的$paymentName字串,直接做App::bind()
使用App::make()將剛剛bind的PaymentInterface建立成物件。
22行
1
2
3
4
5
6
7
8
9
/**
 * 付款
 *
 * @param int $amount
 */
public function checkout(int $amount)
{
    $this->payment->checkout($amount);
}
由於都實現PaymentInterface,所以只要使用統一個checkout()即可,不用擔心是什麼SDK,就算將來有新的第三方支付SDK,在PaymentService也不用修改,達到開放封閉原則的要求。
回到本文PHPDoc的重點,$this->payment因為有出現checkout()的語法提示,是因為第10行替private $payment加了@var註解,描述了$payment的型別為PaymentInterface,引此才能出現checkout()的語法提示。
若將private $payment的PHPDoc拿掉,我們發現PhpStorm將不再出現checkout()語法提示,因為PhpStorm不知道$payment的型別,因此無從顯示語法提示。
 這個範例除了示範adpater pattern外,也告訴我們使用@var替field描述型別的重要性,使用PHPDoc去描述field型別後,PhpStorm就能幫我們替field顯示語法提示,避免typo,也增加開發效率。
 目前PHP 7的field還是沒有type hint,所以field的@var是唯一讓PhpStorm得知field型別的管道,非常重要。

Method

語法
1
2
3
4
5
/** 
 * @param 型別 變數名稱 [註解] 
 * @return 型別 [註解]
 * @throws 型別 [註解]
 */
  • @param為傳入參數,@return為回傳值,@throws為exception。
  • 型別可以是PHP原生型別,class,interface或trait。
  • @param一定要加上變數名稱
  • 可選擇性加上註解
  • 若不傳回值,為@return void
自動建立PHPDoc
實務上建立method會有3種方式 :
  1. 自行由pubf建立method。
  2. 由熱鍵⌃ + I去實踐abstract classinterface的method。
  3. extend abstract classimplements interface建立method。
自行由pubf建立method
輸入pubf,按⇥。
產生public function框架。
自行輸入method名稱,輸入參數型別與名稱,與回傳型別,最後按熱鍵⌥ + ↩,顯示Generate PHPDoc for function,按↩繼續。
PhpStorm會自動幫你加上PHPDoc,包含@param@return
在PHPDoc第一行加上人看得懂的method註解,描述此method的主要功能,中英文皆可。
由熱鍵⌃ + I去實踐abstract class或interface的method
使用extends繼承abstract class2424這裡OrderService去繼承AbstractUser完全不合理,純粹是為了demo方便。
按熱鍵⌃ + I,選擇要實作的method。
要將Add PhHPDocCopy from base class打勾。
PhpStorm除了會幫我們建立method框架外,連PHPDoc也幫我複製過來了。
由extend abstract class或implements interface建立method
使用implements去實現interface2525這裡OrderService去實現PaymentInterface完全不合理,純粹是為了demo方便。
按熱鍵⌥ + ↩,顯示Add method stubs,按↩繼續。
PhpStorm除了會幫我們建立method框架外,連PHPDoc也幫我複製過來了。
 在duck type時代,@param@return是唯一讓PhpStorm得知method參數與回傳值型別的管道,非常重要,不過在PHP有type hint與PHP 7有return type之後,@param@return沒之前重要,目前PhpStorm已經可以自行透過type hint與return type得知變數型別。

Overloading


Property

語法
1
2
3
/** 
 * @property 型別 變數名稱 [註解] 
 */
若你有使用__get()__set()動態產生property話,由於是動態產生,PhpStorm無從得知property名稱與其型別,必須依賴@property描述。2626關於property overloading更詳細的描述,請參考PHP與C#語法快速導覽#Property Overloading
app/User.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
namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;

/**
 * App\User
 *
 * @property integer $id
 * @property string $name
 * @property string $email
 * @property string $password
 * @property string $remember_token
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @method static \Illuminate\Database\Query\Builder|\App\User whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereRememberToken($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\User whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class User extends Authenticatable
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}
典型的應用就是Eloquent的model,會根據資料庫欄位動態產生property,Laravel IDE Helper就是利用@property幫我們描述欄位與型別。2727GitHub Commit : 在User.php加入@propery與@method註解

Method

語法
1
2
3
/** 
 * @method 回傳型別 函式名稱 ([參數型別] 參數名稱)
 */
若你有使用__call()__callStatic()動態產生method話,由於是動態產生,PhpStorm無從得知method名稱、參數與回傳型別,必須依賴@method描述。2828關於method overloading更詳細的描述,請參考PHP與C#語法快速導覽#Method Overloading
之前的User model也看到了@method的使用。
在之前講migration之處,我們還留了一個未解的反白unique(),其實不只有unique(),一些常用的如nullable()unsigned()index()都會反白。
原因就是Laravel使用了method overloading的機制寫這些fluent method,所以PhpStorm無法得知,且目前Laravel IDE Helper也沒幫我們處理,必須自己解決。
_migration_helper.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Illuminate\Support;

/**
 * @method Fluent first()
 * @method Fluent after($column)
 * @method Fluent change()
 * @method Fluent nullable()
 * @method Fluent unsigned()
 * @method Fluent unique()
 * @method Fluent index()
 * @method Fluent primary()
 * @method Fluent default($value)
 * @method Fluent onUpdate($value)
 * @method Fluent onDelete($value)
 * @method Fluent references($value)
 * @method Fluent on($value)
 */
class Fluent {}
自己建立_migration_helper.php放在專案的跟目錄下,使用@method描述這些Laravel IDE Helper沒描述的method。2929GitHub Commit : 新增_migration_helper.php
unique()就不再反白了,以後nullable()unsigned()index()在PhpStorm也都有了語法提示。

Collection


Collection是我在Laravel又愛又恨的東西,愛的是collection所提供的method遠比PHP原生array優雅強大,我幾乎完全使用collection取代PHP原生array,恨的是collection與array一樣,我無法得知collection內每個element的型別,因此在foreach()時,PhpStorm無法對collection內物件的property與method做語法提示。
app\Repositories\UserRepository.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
namespace App\Repositories;

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

class UserRepository
{
    /** @var User */
    private $user;

    /**
     * UserRepository constructor.
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * 傳回所有User
     *
     * @return Collection
     */
    public function getAllUsers() : Collection
    {
        return $this->user->all();
    }
}
一個典型的repository應用,repository負責資料庫邏輯,在getAllUsers()傳回Collection3030GitHub Commit : 新增UserRepository.php
一個典型的service應用,注入repository後,從UserRepositorygetAllUsers()獲得collection,要foreach()時,發現PhpStorm無法對User model的資料庫欄位名稱做語法提示。
之前辛辛苦苦使用Laravel IDE Helper替User model加了PHPDoc,結果在使用collection之後,竟然完全用不上。
很多人的做法,是在foreach()之內補一行/** @var User $user*/描述$user型別,這樣雖然可以另$user出現資料庫欄位的語法提示,但缺點是這種inline PHPDoc很醜,且每次foreach()都要加一次很麻煩。
比較好的方式是將PHPDoc加在UserRepository的@return內。
app\Repositories\UserRepository.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
namespace App\Repositories;

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

class UserRepository
{
    /** @var User */
    private $user;

    /**
     * UserRepository constructor.
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * 傳回所有User
     *
     * @return Collection|User[]
     */
    public function getAllUsers() : Collection
    {
        return $this->user->all();
    }
}
20行
1
2
3
4
5
6
7
8
9
 /**
  * 傳回所有User
  *
  * @return Collection|User[]
  */
public function getAllUsers() : Collection
{
    return $this->user->all();
}
@return內除了Collection外,還加上了User[],這是模仿Java array的宣告方式,目的是告訴PhpStorm這個物件除了是collection,其每個item內的型別是User,因為描述了兩種型別資訊,中間要加上|符號。3131GitHub Commit : UserRepostory.php加上User[]註解
再也不用每個foreach()都補上inline PHPDoc,就可以讓PhpStorm對collection內的model做資料庫欄位名稱的語法提示。3232GitHub Commit : 新增UserService.php

Conclusion


  • Laravel IDE Helper + Laravel Plugin幫我們補上了大部分Laravel部分的PHPDoc,但自己寫的class,則有賴自己使用PHPDoc。
  • 隨著PHP 7對type hint的支援更加完整,PHPDoc的重要性沒以往重要,不過對於field,collection與overloading,目前還是得依賴PHPDoc,PhpStorm才能達到較滿意的語法提示與語法檢查功能。

Sample Code



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

from : http://oomusou.io/phpstorm/phpstorm-ide-helper/

沒有留言:

wibiya widget