聊一聊 php 程式碼提示
title: 聊一聊 php 程式碼提示
date: 2017-8-25 15:05:49
這次我們來聊一聊 php 的程式碼提示, 不使用 IDE 的同學也可以瞧瞧看, PHP IDE 推薦 phpstorm.
phpstorm 使用程式碼提示非常簡單, 只需要將程式碼提示檔案放到專案中就好, 我目前放到
vendor/
目錄下
起源
- 最近開發的專案中, 有使用到
PHP 魔術方法
和單例模式
, 導致了需要程式碼提示的問題 - 最近在嘗試用 swoole 寫 tcp server, 有需要用到
swoole IDE helper
, swoole wiki首頁就有推薦
資料庫模型
在 laravel 中, 如果有一張資料表 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 模型和他對應:
<?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
這樣寫是不是很舒服, 或者說很「優雅」?
而實現起來, 非常簡單, __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 中, 這樣的實現方式隨處可見, 比如:
// Illuminate\Http\Request $request
$request->abc; // 獲取 http 請求中的 abc 引數, 無論是 get 請求還是 post 請求
$request->get('abc'); // 和上面等效
好了, 原理清楚了. 寫起來確實「舒服」了, 但是, 程式碼提示呢? 難道要自己去資料庫檢視欄位麼?
在我們的另一個使用 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
的記錄, 然後處理字串生成的. 但是, 缺點也非常明顯:
- 多了一個指令碼需要維護
- 欄位修改了, 需要重新生成
- 程式碼結構中, 多了一層
Table
層, 而這層其實就只幹了get / set
雖然有了程式碼提示了, 這樣做真的好麼? 那好, 我們來按照上面的套路改造一下:
// lib/Models/BaseModel.php
<?php
namespace App\Models;
use Hyperframework\Db\DbActiveRecord;
class BaseModel extends DbActiveRecord
{
// 獲取 model 對應的資料庫 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
{
...
}
好了, 問題又來了, 程式碼提示怎麼辦? 這樣常見的問題, 當然有成熟的解決方案:
laravel-ide-helper: laravel package, 用來生成 ide helper
上面 Lesson model 的問題, 就可以這樣解決了, 只要執行 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 設計模式 / 單例設計模式
瞭解 laravel 的話, 對 Facede 一定不陌生, 不熟悉的同學, 可以通過這篇部落格 設計模式(九)外觀模式Facade(結構型) 瞭解一下.
現在來看看, 如果我們需要使用 redis, 在 laravel 中, 我們可以這樣寫:
Redis::get('foo');
Redis::set('foo', 'bar');
底層依舊是通過 ext-redis 擴充套件來實現, 而實際上, 我們使用 ext-redis, 需要這樣寫:
$cache = new \Redis();
$cache->connect('127.0.0.1', '6379');
$cache->auth('woshimima');
$redis->get('foo');
$redis->set('foo', 'bar');
2 個明顯的區別: 1. new 不見了(有時候會不會感覺 new 很煩人); 2. 一個是靜態方法, 一個是普通方法
如果稍微瞭解一點設計模式, 單例模式 肯定聽過了, 因為使用場景實在是太普遍了, 比如 db 連線, 而且實現也非常簡單:
// 簡單實現
class User {
private static $_instance = null; // 靜態變數儲存全域性例項
// 私有建構函式,防止外界例項化物件
private function __construct() {}
// 私有克隆函式,防止外辦克隆物件
private function __clone() {}
//靜態方法,單例統一訪問入口
public static function getInstance() {
if (is_null ( self::$_instance ) || isset ( self::$_instance )) {
self::$_instance = new self ();
}
return self::$_instance;
}
}
// 使用
$user = User::getInstance();
好了, 關於 new 的問題解決了. 接下來再看看靜態方法. 在我們的另一個使用 hyperframework框架的專案中, 我們也實現了自己的 Redis service 類:
// lib/Services/Redis.php 檔案
<?php
namespace App\Services;
use Hyperframework\Common\Config;
use Hyperframework\Common\Registry;
class Redis
{
/**
* 將 redis 註冊到 Hyperframework 的容器中
* 容器這個概念先留個坑, 下次講 laravel 核心的時候, 再一起好好講講
* 這裡只要簡單理解我們已經實現了 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()
, 就是這個魔術方法了. 另外再看看 ...$arguments
, 知識點!
仔細看的話, 我們下面按照 ext-redis 中的方法, 再次實現了一次 $redis->get()
方法, 有 2 點理由:
- 魔術方法會有一定效能損失
- 我們又有程式碼提示可以用了, 只是要用啥, 就要自己把 ext-redis 裡的方法封裝一次
好了, 來看看我們的老朋友, laravel 是怎麼實現的吧:
- 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];
}
// 魔術方法實現靜態函式呼叫
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, 執行 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);
}
...
}
...
}
通過反射實現 swoole 程式碼提示
通過反射實現 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";
}
// 檔案結尾
$result .= "}\n";
return $result;
}
再回到上面我們使用反射的例子:
// 獲取 model 對應的資料庫 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 是指當前記憶體中執行的例項, 所以永遠都是 所見即所得.
魔術方法的效能損失
本來我也想做一下 profile 的, 還折騰起了 xhprof 和 xdebug, 但是其實可以簡單的測試:
$start = microtime();
dosomething();
echo microtime() - $start; // 單位: 微秒
感謝這位仁兄做的測試 PHP 魔術方法效能測試, 實測結果下來效能損失在 10us 內, 這個數量級, 我個人認為除非少數極端要求效能的場景, 完全是可以接受的.
最後, 補充一下 單例模式 的優缺點:
優點:
- 改進系統的設計
- 是對全域性變數的一種改進
缺點:
- 難於除錯
- 隱藏的依賴關係
- 無法用錯誤型別的資料覆寫一個單例
相關文章
- 從原始碼來聊一聊hashmap原始碼HashMap
- 無聊程式碼一段
- 聊一聊 RestTemplateREST
- 聊一聊 cookieCookie
- 聊一聊編碼與亂碼的區別
- 聊一聊 TLS/SSLTLS
- 聊一聊Java8 Optional,讓你的程式碼更加優雅Java
- 聊一聊Oracle的Tablespace(一)Oracle
- 聊一聊 EventBus 原始碼和設計之禪原始碼
- 聊一聊前端換膚前端
- 聊一聊 JVM 的 GCJVMGC
- 聊一聊Greenplum與PostgreSQLSQL
- 聊一聊模板方法模式模式
- 聊一聊測試流程
- 聊一聊session和cookieSessionCookie
- 聊一聊JWT與sessionJWTSession
- 無聊的html程式碼HTML
- 聊一聊隨機數安全隨機
- 聊一聊遊戲的壓測遊戲
- 聊一聊 Javascript 中的 ASTJavaScriptAST
- 面試官(7): 聊一聊 Babel?面試Babel
- 聊一聊前端業務開發前端
- 面試官:聊一聊索引吧面試索引
- 和手遊開發者聊一聊 iPhoneiPhone
- 聊一聊責任鏈模式模式
- 聊一聊介面卡模式模式
- 聊一聊裝飾者模式模式
- 聊一聊遊戲版本運營遊戲
- 聊一聊系統重構
- 簡單聊一聊ThreadPoolExecutorthread
- 來,聊一聊效能優化優化
- Nginx-01-聊一聊 nginxNginx
- 聊一聊SQL最佳化SQL
- 智慧小程式檔案館 —— 聊一聊 web-view 元件WebView元件
- 聊一聊程式設計師人群的認知偏見程式設計師
- 聊一聊Java的列舉enumJava
- 聊一聊Redis的離線分析Redis
- 聊一聊MySQL的字符集MySql