運行時可裝卸的Mixin的PHP實現

| Comments

PHP的Trait可以實現加載時(load time)的混入(mixin)。作爲元編程的一部分,運行時(run time)的混入擁有更大的靈活性。下面利用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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
/**
 * 支持混入的類
 */
class Component
{
    // ...

    // 所有混入的實例
    private $_behaviors = [];

    /**
     * 魔術方法
     * @param string $name 方法名
     * @param array $arguments 參數數組
     * @return mixed
     * @throws MethodNotFoundException
     */
    public function __call($name, $arguments) {
        foreach ($this->_behaviors as $behavior) {
            if (method_exists($behavior, $name)) {
                return call_user_func_array([$behavior, $name], $arguments);
            }
        }
        throw new MethodNotFoundException(get_class($this), $name);
    }

    /**
     * 魔術方法,從混入對象實例中取屬性值
     * @param string $attrName 屬性名
     * @return mixed
     * @throws AttrNotFoundException
     */
    public function __get($attrName)
    {
        foreach ($this->_behaviors as $behavior) {
            if (property_exists($behavior, $attrName)) {
                return $behavior->$attrName;
            }
        }
        throw new AttributeNotFoundException(get_class($this), $attrName);
    }

    /**
     * 附加混入對象實例
     * @param object $behavior 混入對象實例
     * @param string $name 混入對象實例名稱
     * @return void
     */
    public function attachBehavior($behavior, $name='') {
        if (empty($name))
            $this->_behaviors[] = $behavior;
        else
            $this->_behaviors[$name] = $behavior;
    }

    /**
     * 卸載混入對象實例
     * @param string $name 混入對象實例名稱
     * @return void
     */
    public function detachBehavior($name) {
        unset($this->_behaviors[$name]);
    }

    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
 * 混入類
 */
class Behavior
{
    // ...

    /**
     * 将本實例混入指定對象
     * @param object $object 支持混入的實例
     * @param string $name 目标對象存儲本混入對象實例的鍵值
     * @return void
     * @throws BehaviorNotAttachableException
     */
    public function mixin($object, $name='') {
        if (method_exists($object, 'attachBehavior')) {
            return call_user_func_array([$object, 'attachBehavior'], [$this, $name]);
        }
        throw new BehaviorNotAttachableException(get_class($object));
    }

    // ...
}

使用示例:

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
<?php
include_once 'component.php';
include_once 'behavior.php';

class TestBehavior extends Behavior
{
    public function test($what)
    {
        echo "say $what";
    }
}

$c = new Component();
$b = new TestBehavior();

$c->attachBehavior($b, 'test');
echo '<pre>'; var_dump($c); echo '</pre>';

$c->detachBehavior('test');
echo '<pre>'; var_dump($c); echo '</pre>';

$b->mixin($c, 'test');
echo '<pre>'; var_dump($c); echo '</pre>';

$c->detachBehavior('test');
$b->mixin($c);
echo '<pre>'; var_dump($c); echo '</pre>';

$c->test('hello world');

讀完《湘行散記》

| Comments

能讓我這個不喜歡讀散文的人堅持讀完的,是枕著一葉扁舟、聽著潺潺流水、忖著多年後的自己——身在何方、心念何人、情系何物……

Phalcon項目中PHPUnit的初始化

| Comments

參考官方文檔,稍作修改。

在項目下創建目錄unittests,進入目錄執行:

1
composer require phpunit/phpunit

創建tests目錄并在其中創建文件Bootstrap.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
<?php
use Phalcon\DI,
    Phalcon\DI\FactoryDefault;

ini_set('display_errors',1);
error_reporting(E_ALL);

define('ROOT_PATH', __DIR__);
define('PROJECT_DIR', '/home/taoqi/workspace');

set_include_path(
    ROOT_PATH . PATH_SEPARATOR . get_include_path()
);

// required for phalcon/incubator
include __DIR__ . "/../vendor/autoload.php";

// 加載項目文件
$config = require_once PROJECT_DIR.'/web/config/config.php';
require_once PROJECT_DIR.'/web/config/loader.php';
$loader->registerDirs(array(
    ROOT_PATH
), true);

// $di = new FactoryDefault();
DI::reset();

// add any needed services to the DI here
require_once PROJECT_DIR.'/web/config/services.php';

DI::setDefault($di);

安裝phalcon的phpunit輔助庫:

1
composer require phalcon/incubator

創建phpunit.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./Bootstrap.php"
        backupGlobals="false"
        backupStaticAttributes="false"
        verbose="true"
        colors="false"
        convertErrorsToExceptions="true"
        convertNoticesToExceptions="true"
        convertWarningsToExceptions="true"
        processIsolation="false"
        stopOnFailure="false"
        syntaxCheck="true">
    <testsuite name="Phalcon - Testsuite">
        <directory>./</directory>
    </testsuite>
</phpunit>

創建單元測試基類UnitTestCase.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
<?php
use Phalcon\DI,
    \Phalcon\Test\UnitTestCase as PhalconTestCase;

abstract class UnitTestCase extends PhalconTestCase {

    /**
     * @var \Voice\Cache
     */
    protected $_cache;

    /**
     * @var \Phalcon\Config
     */
    protected $_config;

    /**
     * @var bool
     */
    private $_loaded = false;

    public function setUp(Phalcon\DiInterface $di = NULL, Phalcon\Config $config = NULL) {

        // Load any additional services that might be required during testing
        $di = DI::getDefault();

        // get any DI components here. If you have a config, be sure to pass it to the parent
        parent::setUp($di);

        $this->_loaded = true;

    }

    /**
     * Check if the test case is setup properly
     * @throws \PHPUnit_Framework_IncompleteTestError;
     */
    public function __destruct() {
        if(!$this->_loaded) {
            throw new \PHPUnit_Framework_IncompleteTestError('Please run parent::setUp().');

        }

    }

}

創建單元測試類testsTestUnitTest.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace Test;

/**
 * Class UnitTest
 */
class UnitTests extends \UnitTestCase {

    public function testTestCase() {
        $post = \Post::find(33);
        $this->assertObjectHasAttribute('title', $post, 'where is title ?');
    }

}

在tests目錄下建立phpunit的軟連接并執行測試:

1
2
ln -sf ../vendor/bin/phpunit run
./run

:發現個詭異的問題,如果Model中不覆蓋getSource()方法,單元測試中會自動找用下劃線分隔的表名,即假如Model名爲FooBar,會去找foo_bar的表名,但正常執行程序時找的是foobar。在官方論壇問的問題還木有解決。phalcon坑挺多的。

用好代碼時光機

| Comments

今天看了篇文章,講幾種常見的、糟糕的注釋用法。其中之一是把廢棄的代碼注釋起來,而不是直接删掉,原因是擔心以後可能會用。

這個其實就是版本控制系統(VCS)要解決的問題之一。包括對于團隊協作的項目,經常需要看某段代碼是誰改的、什麽時間、什麽原因。都是可以用VCS很方便地解決的問題。

我以前是用二分法在提交列表裏找的。其實有更好的解決方法,思路是列出源碼在曆次提交中修改的内容,然後在其中查找要找的東西就行了。

git的解決方法:

1
git log -p abc.php

svn的解決方法:

1
svn log --diff --internal-diff abc.php

vim的輔助函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
" Show commit history of the current file under the given VCS in a new window
function! ShowCommitHistory(vcs)
    " Check parameter
    if a:vcs != 'svn' && a:vcs != 'git'
        echoerr 'Unknow VCS: '.a:vcs
        return
    endif

    " Do the dirty work
    let fileName = expand('%')
    if !empty(fileName)
        exe 'new'
        if a:vcs == 'svn'
            exe 'r !svn log --diff --internal-diff '.fileName
        elseif a:vcs == 'git'
            exe 'r !git log -p '.fileName
        endif
    else
        echo 'File not found.'
    endif
endfunction
nnoremap <leader>ssch :call ShowCommitHistory('svn')<CR>
nnoremap <leader>gsch :call ShowCommitHistory('git')<CR>

開始用在線Markdown編輯器記筆記

| Comments

開始用支持VIM模式的在線Markdown編輯器記筆記。既非鼠輩(鼠标依賴症不治者),始終忍受不了其它WYSIWYG(所見即所得)的在線筆記。

大部分在線筆記最傻X的一點是,必須點一下編輯按鈕才能開始寫東西。支持VIM模式的筆記就不用說了,絲般順滑,不是我大Vimer就不會懂個中騷柔。

組織上一向後知後覺。智能手機剛開始的時候,組織上很不感冒,理由是屏幕易碎、電池難支、馮唐易老、李廣難封。而今現在眼目下,一日不搞機,如隔三秋。在線Markdown編輯器出現也有段時間了,一直覺得這貨無非就是給Markdown加了個可笑的準WYSIWYG,現在看來,真是做筆記的不二神器,罪過罪過。

說說組織上跟Markdown的故事。很久很久以前,博客剛開始流行的時候,組織上開始寫博客,和大部分人一樣,注冊個賬号寫公開日記,很快就忍受不了WYSIWYG編輯器失控的格式,于是開始手寫HTML代碼,很快就發現這貨真TM反人類,從此恨烏及烏至今最讨厭的數據交換格式還是XML,于是自定幾種格式或标記,用Java寫了個解析工具,把文章源碼翻譯成HTML。突然有天發現這個星球上居然有種叫輕量型标記語言的東西,幹的就是這麽個事,隻不過我的那些标記和解析工具又醜又爛而已。試了幾種之後,最終選了Markdown,從此舉案齊眉雲雲。

Vim強制在PHP中使用HTML註釋的方法

| Comments

對PHP頁面模板中的HTML做註釋,NERDCommenter是根據文件類型處理的,所以必須臨時轉換文件類型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
" 强制使用HTML的注释
function! ForceHTMLComment(mode, type) range
    set ft=html
    if a:mode == "x"
        execute a:firstline.",".a:lastline."call NERDComment(\"x\", \"".a:type."\")"
    else
        if a:type == "Sexy"
            normal ,cs
        else
            normal ,cc
        endif
    endif
    set ft=php
endfunction
au FileType php nmap <buffer> <leader>fhcc :call ForceHTMLComment("n", "Comment")<CR>
au FileType php vmap <buffer> <leader>fhcc :call ForceHTMLComment("x", "Comment")<CR>
au FileType php nmap <buffer> <leader>fhcs :call ForceHTMLComment("n", "Sexy")<CR>
au FileType php vmap <buffer> <leader>fhcs :call ForceHTMLComment("x", "Sexy")<CR>
au FileType php nmap <buffer> <leader>fhcu :call ForceHTMLComment("n", "Uncomment")<CR>
au FileType php vmap <buffer> <leader>fhcu :call ForceHTMLComment("x", "Uncomment")<CR>

有日子沒寫vimscript了,手都生了。

ZSH下新安裝的程序無法自動補全的解決方法

| Comments

Zsh默認開啟了對PATH變量的緩存,這是導致新安裝的程序無法立即使用自動補全的原因。

其實只要PATH變量不太複雜,安裝的程序不太多,完全沒必要開啟緩存,實際上我把緩存關掉後完全沒有感覺到補全的速度有什麼變化。

方法如下,在.zshrc中增加一行:

1
zstyle ':completion:*' rehash true

也可以在必要的時間手工執行命令rehash,也是個臨時解決方法。

接管PHP致命錯誤的方法

| Comments

Yii 2.0引入了一項新特性,可以接管PHP的致命錯誤。在此之前,如果PHP源碼有語法錯誤,框架本身是不會處理的。

實現的思路如下:

  • 禁止顯示錯誤
  • 註冊自定義的shutdown回調函數
  • 在回調函數中獲取最近的錯誤
  • 若錯誤是致命錯誤,調用相應的處理邏輯

代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

// ...

ini_set('display_errors', false);
register_shutdown_function(function(){
    $error = error_get_last();
    if (isset($error['type']) && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING])) {
        ob_clean();
        echo '<pre>'; var_dump($error); echo '</pre>';
        exit(1);
    }
});

// ...

?>

讀完《黑客與畫家》

| Comments

身兼技術牛人和創業成功者兩個身份,自然有資格指點江山,有些觀點很自負,有些觀點對人有啟發。作者很推崇Lisp,很不屑Java。