分类 PHP 下的文章

php中nbsp的trim

non-breaking space

此处所指的nbsp是实体,而不是nbsp四个字符

$str = " abc"; 
$converted = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)));
var_dump($converted); // 这儿才是要处理的字符串,上面的都是准备工作

在处理此问题的时候,经历了一番波折,先是var_dump出来,但是和正常的字符串无异。
后来又使用escape尝试输出,发现是\xa0,于是就想trim($converted, "\xa0"),无果。

最后在php手册的trim函数下的第一个评论发现了解决方案。

var_dump(trim($converted, chr(0xc2) . chr(0xa0)));

[Laravel4] Eloquent with的错觉

$categories = \Category::with(array('posts' => function ($query) {
    return $query->orderBy('created_at', 'desc')->take(10);
}))->get();

这样的相对语义化的写法很容易让人错误的以为会对每个分类下取10篇文章,而事实上是总共取10篇文章

QQ20140601-1.png

[Laravel4] 创建一个 占位图片 服务

演示图片地址:http://usr.im/100x100

使用Composer安装intervention/image库

composer require intervention/image:dev-master

编码

这里并未对生成的图片进程缓存处理,需要的可以自行加上。

// vim app/routes.php
<?php
Route::pattern('width', '\d+');
Route::pattern('height', '\d+');
Route::get('{width}x{height}', 'ImageHolderController@getIndex');
// vim app/controllers/ImageHolderController.php
<?php

class ImageHolderController extends BaseController {
    public function getIndex($width, $height)
    {
        $width = intval($width);
        $height = intval($height);
        if ($width > 1900 || $height > 900)
            App::abort(404);
        $fontSize = min(max(intval($width / 5), 12), 38);
        $image = Image::canvas($width, $height, '#CCCCCC')
                ->line('#B5B5B5', 0, 0, $width, $height)
                ->line('#B5B5B5', $width, 0, 0, $height)
                ->text($width . 'x' . $height, $width / 2, $height / 2, function ($font) use ($fontSize) {
                    $font->file(public_path('font/Georgia.ttf'));
                    $font->align('center');
                    $font->valign('middle');
                    $font->size($fontSize);
                    $font->color('#666666');
                });
        // 2014-07-19 17:46 修复图片格式不正确的问题,->encode('png')
        return Response::make($image->encode('png'), 200, array('Content-Type' => 'image/png'));
    }
}

PHP unpack VS Python unpack

unpack 对二进制数据解包。

php unpack的结果 数组的索引是从 1 开始的
python unpack的结果是 元祖,索引从 0 开始

php unpack可以传大于需要解包长度的二进制串
python unpack只能传入需要的长度的二进制串

php unpack 和 python unpack 的解包格式不一样,例如:

// php
unpack('V6', $bin);

# python
import struct
struct.unpack('<6L', bin[0:24])

理解Laravel Eloquent的cache

Eloquent::remember

// 从数据库中取出前5个用户,并缓存结果。
User::remember(10)->take(5)->get();
// 下面这句会缓存posts么?
User::with(array('posts'))->remember(10)->take(5)->get();

先找一下remember在哪定义的!

remember 方法的定义

# grep -nC1 "Eloquent" app/config/app.php
150-        'DB'              => 'Illuminate\Support\Facades\DB',
151:        'Eloquent'        => 'Illuminate\Database\Eloquent\Model',
152-        'Event'           => 'Illuminate\Support\Facades\Event',
# grep -nC3 "function remember(" vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php
1086-     * @param  string  $key
1087-     * @return \Illuminate\Database\Query\Builder|static
1088-     */
1089:    public function remember($minutes, $key = null)
1090-    {
1091-        list($this->cacheMinutes, $this->cacheKey) = array($minutes, $key);
1092-

这里EloquentModel的一个别名,而Model中并没有定义remember方法,而是用了装饰者设计模式通过__call魔术方法调用了Builder的方法。其实他是在内部实例化Builder的,有点“反向代理”的意思。

Builder在调用remember的时候做了个标记,在get的时候对这个标记作了判断,对查询结果做了缓存的写入和读取。这就不难理解remember方法只会对单个query查询结果进行缓存。

回头看

User::with(array('posts'))->remember(10)->take(5)->get();

使用with只是将1 + 5 * 1次query,优化成了1 + 1次;而不是想象中的一次缓存该条语句产生的所有query的结果。

缓存关联Model

User::with(array('posts' => function ($query)
    {
        return $query->remember(10);
    }))->remember(10)->take(5)->get();

当然,在定义model relation的时候也可以将 remember() 跟在后面,但我不认为这是一个好的方法。

function posts()
{
    return $this->hasMany('Post')->remember(10);
}

[Laravel4] 增加自定义的function

vendor/laravel/framework/src/Illuminate/Support/helpers.php Laravel4这个自带的function很不错,但是有的时候想扩展自己的方法;问题是该在哪边require进去。

建立一个文件app/Service/helpers.php,所有的自定义方法就写在这个里面了;然后修改composer.json,在autoload里面加上该文件,再执行composer du就可以了

{
    "autoload": {
        // ...
        "files": ["app/Service/helpers.php"]
    }
}

[Laravel4] 使用command执行任务

场景

  1. 比如使用redis作为计数器,然后在每天的凌晨将计数保存到MySQL

  2. 比如定时执行一些批量的任务

扩展artisan的command

例子:批量将文字题目生成为图片

artisan command:make QuestionCommand
# app/command/QuestionCommand.php

生成php文件之后,首先需要修改name为你的命令

protected $name = 'question:gen';

getArguments, getOptions 分别为获取参数和选项的值

getArguments // command arg1 arg2
getOptions // command --opt1=val1 --opt2=val2
fire // 执行命令的时候具体要做的事情

较为完整的例子

<?php

use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;

class QuestionCommand extends Command {

    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'question:gen';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '生成question图片';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return void
     */
    public function fire()
    {
        $questions = $this->option('all') ? Question::all() : Question::whereThumb(0)->get();
        // 初始化markdown解析引擎
        $ciconia = new Ciconia\Ciconia();
        $ciconia->addExtension(new Ciconia\Extension\Gfm\FencedCodeBlockExtension());
        $ciconia->addExtension(new Ciconia\Extension\Gfm\TaskListExtension());
        $ciconia->addExtension(new Ciconia\Extension\Gfm\InlineStyleExtension());
        $ciconia->addExtension(new Ciconia\Extension\Gfm\WhiteSpaceExtension());
        $ciconia->addExtension(new Ciconia\Extension\Gfm\TableExtension());
        foreach ($questions as $question) {
            $item = array(
                'id' => $question->id,
                'absPath' => public_path('upload/question/' . $question->id . '.png'),
                'relPath' => 'question/' . $question->id . '.png',
                'question' => $ciconia->render($question->question),
            );
            Queue::push('code2png', $item, 'code2png');
            $this->info('Question ' . $question->id . ' has been added to the queue.');
        }
    }

    /**
     * Get the console command arguments.
     *
     * @return array
     */
    protected function getArguments()
    {
        return array(
            // array('example', InputArgument::REQUIRED, 'An example argument.'),
        );
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions()
    {
        return array(
            array('all', null, InputOption::VALUE_OPTIONAL, '全部重新生成', null),
        );
    }

}

需要在app/start/artisan.php里面添加下面一行才会生效

// 4.0
$artisan->add(new QuestionCommand);
// 4.1
Artisan::add(new QuestionCommand);

命令行中这样执行

artisan question:gen --all=1

如果要在Controller里面调用,可以这样

Artisan::call('question:gen', array('--all' => 1));

[Laravel4] 从数据库读取配置

再次验证时发现下面的代码有问题,有待修复

场景

很多时候我们的网站的配置都是存在数据库里面的,这样后台也方便配置修改。而Laravel4默认的是将数组直接写在配置文件里面的。

解决

Get Config from database 这个还不错,但是有个问题,就是不支持这样的$register = Config::get('site.register'); var_dump($register['invite']);类似于多级数组的意思

数据库里面直接这样写:

|       key       | value |
|-----------------|-------|
| register.invite |   1   |

我们改造一下

<?php
// app/config/site.php
class DBconfiguratorObject implements ArrayAccess, Serializable {
    protected $config = array();
    protected $table = null;

    private static $_instance = null;

    public static function instance($tableName = 'site'){
        if(self::$_instance === null){
            self::$_instance = new self($tableName);
        }
        return self::$_instance;
    }

    private function __construct($tableName = 'site'){
        $this->table = DB::table($tableName);
        $config = $this->table->lists('value', 'key');
        $this->config = array();
        foreach ($config as $key => $value) {
            array_set($this->config, $key, $value);
        }
    }

    public function offsetGet($key){
        // return $this->config[$key];
        return array_get($this->config, $key);
    }

    public function offsetSet($key, $value){
        if($this->offsetExists($key)){
            $this->table->where('key', $key)->update(array(
                'value' => $value
            ));
        } else {
            $this->table->insert(array(
                'key' => $key,
                'value' => $value
            ));
        }
        $this->config[$key] = $value;
    }

    public function offsetExists($key){
        return isset($this->config[$key]);
    }

    public function offsetUnset($key){
        unset($this->config[$key]);
        $this->table->where('key', $key)->delete();
    }

    public function serialize(){
        return serialize($this->config);
    }

    public function unserialize($serialized){
        $config = unserialize($serialized);
        foreach($config as $key => $value){
            $this[$key] = $value;
        }
    }

    public function toJson(){
        return json_encode($this->config);
    }
}

return DBconfiguratorObject::instance();

php正则匹配出错的问题

发现

因为用了laravel4的

Response::json(array())->setCallback('callback')

然后始终报

The callback name is not valid.

然后单独写了个test.php文件测试该语句在不同的机器上测下来的结果不一样。

var_dump(preg_match('/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*+$/u', 'sad'));

解决

本以为是系统哪边的设置的问题,于是将envphp -i的结果都比对了一遍,都一样。后来想到看一下pcre的版本是否一致pcre-config --version,结果都是6.6;最后没办法rmp -qa | grep pcre,好家伙最后的小版本不一样;再然后你懂的,升级了一下pcre就正常了。

附注

  • 有问题的版本: pcre-6.6-2.el5_1.7

  • 升级后的版本: pcre-6.6-9.el5

在CENTOS上编译pidgin-lwqq,配置qq机器人

首先,yum安装各种需要的东西,比如pidgin,以及后面需要用到的依赖包,其中dbus是用来通信的

yum install pidgin-devel finch screen dbus dbus-devel dbus-x11 libev-devel zlib-devel glibc-devel cmake sqlite-devel libcurl-devel

其次,通过git clone pidgin-lwqq来进行编译,详细的见https://github.com/xiehuc/pidgin-lwqq,不过这里需要注意两点:

  1. 切换到dev分支,不然没法编译,master分支需要依赖一个mozjs的东西,centos上可以搞,但是相当麻烦

  2. 因为是在一个没有图形界面的系统(比如VPS)上编译,需要将验证码输出到一个web可访问的路径去,否则登录不了。修改方法大致如下:

diff --git a/lib/login.c b/lib/login.c
index 8fe25c0..a1ae11b 100644
--- a/lib/login.c
+++ b/lib/login.c
@@ -141,6 +141,7 @@ static LwqqAsyncEvent* check_need_verify(LwqqClient *lc,const char* appid)
 static int request_captcha_back(LwqqHttpRequest* req,LwqqVerifyCode* code)
 {
     int err = 0;
+       FILE *captcha_fp;
     if(req->http_code!=200){
         err = -1;
         goto done;
@@ -148,6 +149,9 @@ static int request_captcha_back(LwqqHttpRequest* req,LwqqVerifyCode* code)
     LwqqClient* lc = req->lc;
     code->data = req->response;
     code->size = req->resp_len;
+       captcha_fp = fopen("/path/to/qqcaptcha.png", "wb");
+       fwrite(code->data, code->size, 1, captcha_fp);
+       fclose(captcha_fp);
     req->response = NULL;
     lwqq_call_action(lc,need_verify2)(lc,code);
 done:

然后,编译好之后,就可以先试试登录了,直接敲入finch试试,目测就能看到登录界面了
1.png

选择webqq进行登录,使用快捷键还是很方便的,不会使用可以man一下

下面就是机器人部分了,pidgin支持dbus通信,于是我们可以用利用dbus的接口来监听qq的消息,并作出回应,抑或是主动发出消息。要使用dbus,那么finch需要运行在screen下,这也是前面安装screen的原因,简单点,在命令行里敲入

dbus-launch screen

即可启动dbus,并进入screen,然后在screen下运行finch即可。友情提示,某些terminal里,screen下的finch排版混乱,至今没找到解决方案。另外,screen的使用方法可以man一下,你的通信脚本也必须运行在screen里。

dbus通信可以用多个语言实现,c、python、php等等都可以,个人熟悉php,因此使用php进行编写机器人脚本。首先安装php扩展dbus:

pecl install dbus-0.1.1

装完记得在php.ini里加入dbus的配置

[dbus]
extension=dbus.so

网上能找到很多示例代码,比如PHP官网就有很多example:传送门在此

下面贴个简单自动重复消息的机器人代码

<?php

Robot::getInstance()->run(200);
/**
 * class Robot
 * @author Baiqiang Dong<qiyuuu@gmail.com>
 */
class Robot {

    protected $interface = 'im.pidgin.purple.PurpleInterface';
    protected $signals = array(
        'ReceivedImMsg',
        'ReceivedChatMsg',
    );

    private static $_instance;
    private $_dbus;
    private $_proxy;

    public static function getInstance() {
        if (self::$_instance === null) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    private function __construct() {
        //connect to dbus
        $this->_dbus = new IDBus(DBus::BUS_SESSION);
        $this->_proxy = $this->_dbus->createProxy(
            'im.pidgin.purple.PurpleService',
            '/im/pidgin/purple/PurpleObject',
            'im.pidgin.purple.PurpleInterface'
        );
        //listen to specified signals
        foreach ($this->signals as $signal) {
            $this->_dbus->addSignalReceiver(array($this, lcfirst($signal)), $this->interface, $signal);
        }
    }

    public function receivedImMsg($signal) {
        $this->responseMessage($signal);
    }

    public function receivedChatMsg($signal) {
        $this->responseMessage($signal);
    }

    public function responseMessage($signal) {
        list($receiver, $sender, $message, $conversation, $flags) = $signal->getData()->getData();
        //send what you received
        $this->sendMessage($conversation, $message, $sender);
    }

    public function sendMessage($conversation, $message) {
        $proxy = $this->_proxy;
        try {
            $type = $proxy->PurpleConversationGetType($conversation);
            switch ($type) {
                //im
                case 1:
                    $im = $proxy->PurpleConvIm($conversation);
                    $proxy->PurpleConvImSend($im, $message);
                    break;
                //chat
                case 2:
                    $chat = $proxy->PurpleConvChat($conversation);
                    $proxy->PurpleConvChatSend($chat, $message);
                    break;
                
                default:
                    # code...
                    break;
            }
        } catch (Exception $e) {
            echo $e->getMessage(), "\n";
            return false;
        }
        return true;
    }

    public function run($time = 1000) {
        $this->_dbus->mainLoop($time);
    }

}

/**
 * extend the DBus class to implement methods like mainLoop and addSignalReceiver
 */
class IDBus extends DBus {
    private $_signalReceivers = array();

    public function __construct($type) {
        parent::__construct($type);
    }

    public function addSignalReceiver($callback, $interface, $signal) {
        if (!is_callable($callback)) {
            return;
        }
        if (!isset($this->_signalReceivers[$interface])) {
            $this->addWatch($interface);
        }
        $this->_signalReceivers[$interface][$signal] = $callback;
    }

    public function mainLoop($time) {
        while (true) {
            $signal = parent::waitLoop($time);
            $called = false;
            if ($signal instanceof DbusSignal) {
                foreach ($this->_signalReceivers as $interface=>$callbacks) {
                    foreach ($callbacks as $method=>$callback) {
                        if ($signal->matches($interface, $method) && is_callable($callback)) {
                            $called = call_user_func($callback, $signal);
                            //break to main loop if callback return true
                            if ($called) {
                                break 2;
                            }
                        }
                    }
                }
            }
        }
        return $signal;
    }
}

php重启apache服务

这里有个矛盾的地方,就是 php是作为Apache的一个模块来运行的,所以一旦重启Apache,在Apache停掉的时候,php调用的shell命令就不会继续执行了;也就是说,只能关闭Apache,不能启动Apache。

我想到的办法就是将命令交给其他程序去运行,于是就想到了at命令。man at只看到at -f [file] now从文件读取,但是我就一个命令,不想再生成一个文件,然后就想到了管道,试一下,成功了。

echo 'sudo service httpd restart' | at now

注:at命令,apache用户需要有登录权限(/etc/passwd)

<time datetime="2013-11-03">2013-11-03 修改</time>

不知为何用apache去启动的服务,在apache服务退出的时候会接管apache的端口

php opcode的反解

查看opcode的扩展安装方法:《编译php vld扩展》

手动反解步骤

opcode的对照表 opcode的对照表
换行的ascii是10,换算成16进制就是A
1.png
2.png

反解出的文件和源文件的对比

<?php
$a = 1;
echo $a + $a++;
echo "\n";
$a = 1;
echo $a + $a + $a++;
echo "\n";</code></pre><p><a href="http://type.so/usr/uploads/2013/10/1563209402.png" title="3.png"><img src="http://type.so/usr/uploads/2013/10/1563209402.png" alt="3.png" /></a></p>

编译php vld扩展

wget http://pecl.php.net/get/vld-0.12.0.tgz
tar zxf vld-0.12.0.tgz
phpize
./configure
make && make install

# vim /usr/local/php/etc/php.ini
# extension=vld.so

# 查看扩展是否已经加载
php -m | grep vld

# 试一试
php -dvld.active=1 test.php