聊一聊 php 代碼提示

title: 聊一聊 php 代碼提示
date: 2017-8-25 15:05:49

這次我們來聊一聊 php 的代碼提示, 不使用 IDE 的同學(xué)也可以瞧瞧看, PHP IDE 推薦 phpstorm.

phpstorm 使用代碼提示非常簡單, 只需要將代碼提示文件放到項目中就好, 我目前放到 vendor/ 目錄下

起源

  1. 最近開發(fā)的項目中, 有使用到 PHP 魔術(shù)方法單例模式, 導(dǎo)致了需要代碼提示的問題
  2. 最近在嘗試用 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)點:

  1. 改進(jìn)系統(tǒng)的設(shè)計
  2. 是對全局變量的一種改進(jìn)

缺點:

  1. 難于調(diào)試
  2. 隱藏的依賴關(guān)系
  3. 無法用錯誤類型的數(shù)據(jù)覆寫一個單例
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容