title: 聊一聊 php 代碼提示
date: 2017-8-25 15:05:49
這次我們來聊一聊 php 的代碼提示, 不使用 IDE 的同學(xué)也可以瞧瞧看, PHP IDE 推薦 phpstorm.
phpstorm 使用代碼提示非常簡單, 只需要將代碼提示文件放到項目中就好, 我目前放到
vendor/目錄下
起源
- 最近開發(fā)的項目中, 有使用到
PHP 魔術(shù)方法和單例模式, 導(dǎo)致了需要代碼提示的問題 - 最近在嘗試用 swoole 寫 tcp server, 有需要用到
swoole IDE helper, swoole wiki首頁就有推薦
數(shù)據(jù)庫模型
在 laravel 中, 如果有一張數(shù)據(jù)表 lessons 如下:
CREATE TABLE `lessons` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`intro` text COLLATE utf8mb4_unicode_ci NOT NULL,
`image` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`published_at` timestamp NOT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
那么可以建立一個 Lesson 模型和他對應(yīng):
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Lesson extends Model
{
//
}
之后, 我們就可以直接使用下面的方法了:
$lesson = new Lesson();
$lesson->title = 'far'; // set
$lesson = Lession::find(1);
echo $lesson->title; // get
這樣寫是不是很舒服, 或者說很「優(yōu)雅」?
而實現(xiàn)起來, 非常簡單, __get() / __set() 就可以了:
// laravel 文件: Illuminate\Database\Eloquent\Model
/**
* Dynamically retrieve attributes on the model.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->getAttribute($key);
}
/**
* Dynamically set attributes on the model.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function __set($key, $value)
{
$this->setAttribute($key, $value);
}
在 laravel 中, 這樣的實現(xiàn)方式隨處可見, 比如:
// Illuminate\Http\Request $request
$request->abc; // 獲取 http 請求中的 abc 參數(shù), 無論是 get 請求還是 post 請求
$request->get('abc'); // 和上面等效
好了, 原理清楚了. 寫起來確實「舒服」了, 但是, 代碼提示呢? 難道要自己去數(shù)據(jù)庫查看字段么?
在我們的另一個使用 hyperframework框架的項目中, 我們使用了 代碼自動生成的方法:
// lib/Table/Trade.php 文件
<?php
namespace App\Table;
class Trade
{
public function getId() {
return $this->getColumn('id');
}
public function setId($id) {
$this->setColumn('id', $id);
}
...
}
// lib/Model/Trade.php 文件
<?php
namespace App\Model;
use App\Table\Trade as Base
class Trade extends BaseTable
{
...
}
// 這樣我們就可以愉快的使用下面代碼了
$trade = new Trade();
$trade->setId(1); // set
$trade = Trade::find(1);
$trade->getId(); // get
上面的 lib/Table/Trade.php 文件使用一個 php 腳本, 讀取 mysql 中 information_schema.COLUMNS 的記錄, 然后處理字符串生成的. 但是, 缺點也非常明顯:
- 多了一個腳本需要維護(hù)
- 字段修改了, 需要重新生成
- 代碼結(jié)構(gòu)中, 多了一層
Table層, 而這層其實就只干了get / set
雖然有了代碼提示了, 這樣做真的好么? 那好, 我們來按照上面的套路改造一下:
// lib/Models/BaseModel.php
<?php
namespace App\Models;
use Hyperframework\Db\DbActiveRecord;
class BaseModel extends DbActiveRecord
{
// 獲取 model 對應(yīng)的數(shù)據(jù)庫 table 名
public static function getTableName()
{
// 反射, 這個后面會講到
$class = new \ReflectionClass(static::class);
return strtolower(preg_replace('/((?<=[a-z])(?=[A-Z]))/', '_', $class->getShortName()));
}
public function __get($key) {
return $this->getColumn($key);
}
public function __set($key, $value) {
$this->setColumn($key, $value);
}
}
// lib/Models/User.php
<?php
namespace App\Models;
class User extends BaseModel
{
...
}
好了, 問題又來了, 代碼提示怎么辦? 這樣常見的問題, 當(dāng)然有成熟的解決方案:
laravel-ide-helper: laravel package, 用來生成 ide helper
上面 Lesson model 的問題, 就可以這樣解決了, 只要執(zhí)行 php artisan ide-helper:models, 就會幫我們生成這樣的文件:
<?php
namespace App{
/**
* App\Lesson
*
* @property int $id
* @property string $title
* @property string $intro
* @property string $image
* @property string $published_at
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|\App\Lesson whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Lesson whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Lesson whereImage($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Lesson whereIntro($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Lesson wherePublishedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Lesson whereTitle($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Lesson whereUpdatedAt($value)
* @mixin \Eloquent
*/
class Lesson extends \Eloquent {}
}
通過注釋, 我們的代碼提示, 又回來了!
Facade 設(shè)計模式 / 單例設(shè)計模式
了解 laravel 的話, 對 Facede 一定不陌生, 不熟悉的同學(xué), 可以通過這篇博客 設(shè)計模式(九)外觀模式Facade(結(jié)構(gòu)型) 了解一下.
現(xiàn)在來看看, 如果我們需要使用 redis, 在 laravel 中, 我們可以這樣寫:
Redis::get('foo');
Redis::set('foo', 'bar');
底層依舊是通過 ext-redis 擴(kuò)展來實現(xiàn), 而實際上, 我們使用 ext-redis, 需要這樣寫:
$cache = new \Redis();
$cache->connect('127.0.0.1', '6379');
$cache->auth('woshimima');
$redis->get('foo');
$redis->set('foo', 'bar');
2 個明顯的區(qū)別: 1. new 不見了(有時候會不會感覺 new 很煩人); 2. 一個是靜態(tài)方法, 一個是普通方法
如果稍微了解一點設(shè)計模式, 單例模式 肯定聽過了, 因為使用場景實在是太普遍了, 比如 db 連接, 而且實現(xiàn)也非常簡單:
// 簡單實現(xiàn)
class User {
private static $_instance = null; // 靜態(tài)變量保存全局實例
// 私有構(gòu)造函數(shù),防止外界實例化對象
private function __construct() {}
// 私有克隆函數(shù),防止外辦克隆對象
private function __clone() {}
//靜態(tài)方法,單例統(tǒng)一訪問入口
public static function getInstance() {
if (is_null ( self::$_instance ) || isset ( self::$_instance )) {
self::$_instance = new self ();
}
return self::$_instance;
}
}
// 使用
$user = User::getInstance();
好了, 關(guān)于 new 的問題解決了. 接下來再看看靜態(tài)方法. 在我們的另一個使用 hyperframework框架的項目中, 我們也實現(xiàn)了自己的 Redis service 類:
// lib/Services/Redis.php 文件
<?php
namespace App\Services;
use Hyperframework\Common\Config;
use Hyperframework\Common\Registry;
class Redis
{
/**
* 將 redis 注冊到 Hyperframework 的容器中
* 容器這個概念先留個坑, 下次講 laravel 核心的時候, 再一起好好講講
* 這里只要簡單理解我們已經(jīng)實現(xiàn)了 redis 的單例模式就好了
*/
public static function getEngine()
{
return Registry::get('services.redis', function () {
$redis = new \Redis();
$redis->connect(
Config::getString('redis.host'),
Config::getString('redis.port'),
Config::getString('redis.expire')
);
$redisPwd = Config::getString('redis.pwd');
if ($redisPwd !== null) {
$redis->auth($redisPwd);
}
return $redis;
});
}
// 重點來了
public static function __callStatic($name, $arguments)
{
return static::getEngine()->$name(...$arguments);
}
// k-v
public static function get($key)
{
return static::getEngine()->get($key);
}
}
拍黑板劃重點: __callStatic(), 就是這個魔術(shù)方法了. 另外再看看 ...$arguments, 知識點!
仔細(xì)看的話, 我們下面按照 ext-redis 中的方法, 再次實現(xiàn)了一次 $redis->get() 方法, 有 2 點理由:
- 魔術(shù)方法會有一定性能損失
- 我們又有代碼提示可以用了, 只是要用啥, 就要自己把 ext-redis 里的方法封裝一次
好了, 來看看我們的老朋友, laravel 是怎么實現(xiàn)的吧:
- laravel: Illuminate\Support\Facades\Facade
// 獲取 service 的單例
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
return static::$resolvedInstance[$name] = static::$app[$name];
}
// 魔術(shù)方法實現(xiàn)靜態(tài)函數(shù)調(diào)用
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
然后, 使用上面的 package, 執(zhí)行 php artisan ide-helper:generate, 就可以得到代碼提示了:
namespace Illuminate\Support\Facades {
...
class Redirect {
/**
* Create a new redirect response to the "home" route.
*
* @param int $status
* @return \Illuminate\Http\RedirectResponse
* @static
*/
public static function home($status = 302)
{
return \Illuminate\Routing\Redirector::home($status);
}
/**
* Create a new redirect response to the previous location.
*
* @param int $status
* @param array $headers
* @param mixed $fallback
* @return \Illuminate\Http\RedirectResponse
* @static
*/
public static function back($status = 302, $headers = array(), $fallback = false)
{
return \Illuminate\Routing\Redirector::back($status, $headers, $fallback);
}
...
}
...
}
通過反射實現(xiàn) swoole 代碼提示
通過反射實現(xiàn) swoole 代碼提示來自此項目 flyhope/php-reflection-code, 核心代碼其實很簡單, 如下
static public function showDoc($class_name) {
try {
// 初始化反射實例
$reflection = new ReflectionClass($class_name);
} catch(ReflectionException $e) {
return false;
}
// 之后都是字符串處理之類的工作了
// Class 定義
$doc_title = ucfirst($class_name) . " Document";
$result = self::showTitle($doc_title);
$result .= self::showClass($class_name, $reflection) . " {\n\n";
// 輸出常量
foreach ($reflection->getConstants() as $key => $value) {
$result .= "const {$key} = " . var_export($value, true) . ";\n";
}
// 輸出屬性
foreach ($reflection->getProperties() as $propertie) {
$result .= self::showPropertie($propertie) . "\n";
}
//輸出方法
$result .= "\n";
foreach($reflection->getmethods() as $value) {
$result .= self::showMethod($value) . "\n";
}
// 文件結(jié)尾
$result .= "}\n";
return $result;
}
再回到上面我們使用反射的例子:
// 獲取 model 對應(yīng)的數(shù)據(jù)庫 table 名
public static function getTableName()
{
// 反射, 這個后面會講到
$class = new \ReflectionClass(static::class);
return strtolower(preg_replace('/((?<=[a-z])(?=[A-Z]))/', '_', $class->getShortName()));
}
注意, 這里要使用 static, 如果你使用 self 得到的就是 BaseModel 了. 至于一個簡單的理解 static & self 的方式: static 是指當(dāng)前內(nèi)存中運(yùn)行的實例, 所以永遠(yuǎn)都是 所見即所得.
魔術(shù)方法的性能損失
本來我也想做一下 profile 的, 還折騰起了 xhprof 和 xdebug, 但是其實可以簡單的測試:
$start = microtime();
dosomething();
echo microtime() - $start; // 單位: 微秒
感謝這位仁兄做的測試 PHP 魔術(shù)方法性能測試, 實測結(jié)果下來性能損失在 10us 內(nèi), 這個數(shù)量級, 我個人認(rèn)為除非少數(shù)極端要求性能的場景, 完全是可以接受的.
最后, 補(bǔ)充一下 單例模式 的優(yōu)缺點:
優(yōu)點:
- 改進(jìn)系統(tǒng)的設(shè)計
- 是對全局變量的一種改進(jìn)
缺點:
- 難于調(diào)試
- 隱藏的依賴關(guān)系
- 無法用錯誤類型的數(shù)據(jù)覆寫一個單例