标签归档:PHP

CGI – FastCGI – FPM PHP-FPM

CGI – Common Gateway Interface的简写,一般叫做通用网关接口。CGI程序就是指可以使用某某解释器解释的脚本程序,比如Perl、PHP、Shell脚本等。工作流程大体是这样:HTTP服务器根据请求调用相应的CGI脚本,这个时候会从HTTP服务器fork一个进程,在这个进程内用脚本声明的解释器执行脚本,HTTP服务器接收标准输出,然后CGI进程退出。当有新的请求过来,继续这样的流程。在Apache Httpd服务器中,提供了CGI模块,为了加深认识,这里配置一下:

yum install httpd

vi /etc/httpd/conf/httpd.conf
#修改下端口,避免冲突
Listen *:8080
#明确指定服务器名称
ServerName localhost:8080

#修改cgi-bin目录下的文件都是cgi脚本,都可以执行
<Directory "/var/www/cgi-bin">
    AllowOverride All
    Options +ExecCGI
    #AddHandler cgi-script .cgi
    SetHandler cgi-script
    Require all granted
</Directory>

#由于是演示,少开几个进程
<IfModule mpm_prefork_module>
    StartServers 2
    MinSpareServers 2
    MaxSpareServers 6
    ServerLimit 200
    MaxClients 150
    MaxRequestsPerChild 30
</IfModule>

#启动服务器器
systemctl start httpd.service
# - or -
/usr/sbin/httpd

然后分别建立三种类型的脚本(shell, perl, php)

cd /var/www/cgi-bin

## 1 Shell 
vi cgi.sh
 
#!/bin/sh
set -f
echo "Content-type: text/plain; charset=utf-8"
echo
echo CGI Shell Test----------------------------:
echo
echo argc is $#. argv is "$*".
echo
echo SERVER_SOFTWARE = $SERVER_SOFTWARE
echo SERVER_NAME = $SERVER_NAME
echo GATEWAY_INTERFACE = $GATEWAY_INTERFACE
echo SERVER_PROTOCOL = $SERVER_PROTOCOL
echo SERVER_PORT = $SERVER_PORT
echo REQUEST_METHOD = $REQUEST_METHOD
echo HTTP_ACCEPT = "$HTTP_ACCEPT"
echo PATH_INFO = "$PATH_INFO"
echo PATH_TRANSLATED = "$PATH_TRANSLATED"
echo SCRIPT_NAME = "$SCRIPT_NAME"
echo QUERY_STRING = "$QUERY_STRING"
echo REMOTE_HOST = $REMOTE_HOST
echo REMOTE_ADDR = $REMOTE_ADDR
echo REMOTE_USER = $REMOTE_USER
echo AUTH_TYPE = $AUTH_TYPE
echo CONTENT_TYPE = $CONTENT_TYPE
echo CONTENT_LENGTH = $CONTENT_LENGTH

## 2 Perl
vi cgi.perl

#!/usr/bin/perl

print "Content-type: text/plain; charset=utf-8\n\n";

foreach $var (sort(keys(%ENV))) {
    $val = $ENV{$var};
    $val =~ s|\n|\\n|g;
    $val =~ s|"|\\"|g;
    print "${var}=\"${val}\"\n";
}

## 3 PHP
#!/usr/bin/php
<?php
echo "Content-type: text/plain; charset=utf-8\n\n";

print_r($_SERVER);

确保这些程序是可以执行的(chmod 777 *), 然后通过浏览器访问:
1 Shell的输出
cgi-sh

2 Perl的输出
cgi-perl

3 PHP的输出
cig-php

看到以上的输出,应该对CGI会有一个比较直观的了解。 然后把cgi.php修改一下:

#!/usr/bin/php
<?php
echo "Content-type: text/plain; charset=utf-8\n\n";

$i = 20;
while($i > 0) {
	echo $i." sleep...<br />";
	sleep(2);
	$i--;
}

在进程没有退出前,查看进程:
cgi-ps
很明显,从httpd派生了一个进程,然后把脚本传递给php解释器。

HTTP服务器会把环境变量(比如请求参数等)传递给CGI解释器,比如PHP解释器的$_SERVER输出就能取到传递过来的参数。而CGI程序需要输出一个MIME类型的头(”Content-type: text/plain; charset=utf-8\n\n”),之后跟一个空行(Shell中无法解释转义,需要明确输出空行),这些几乎就是大部分内容了。

FastCGI是为了解决CGI的缺陷而引入的。CGI的缺陷是非常明显的,每一个请求都启动一个进程,用完了后退出。FastCGI要解决的问题是进程启动,转入解释器,解释脚本,完成后不退出,继续等待其它脚本的投递。比如启动10个FastCGI进程,形成一个进程池。PHP中提供了php-cgi(php-cgi.exe),可以非常便利实现这个进程池:

#启动三个php-cgi
D:\wnmp\php-7.0.11\php-cgi.exe -b 127.0.0.1:9000
D:\wnmp\php-7.0.11\php-cgi.exe -b 127.0.0.1:9000
D:\wnmp\php-7.0.11\php-cgi.exe -b 127.0.0.1:9000

这样在Nginx中,可以直接使用:

    location ~ \.php$ {
        client_max_body_size    100M;
        
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout    300;
        fastcgi_read_timeout    300;
        fastcgi_buffer_size     32k;
        fastcgi_buffers     256 32k;
 
        fastcgi_pass        127.0.0.1:9000;
        fastcgi_index       index.php;
 
        fastcgi_param       HTTPS $https if_not_empty;
 
        fastcgi_param       SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include             fastcgi_params;
   }

指令fastcgi_param定义的参数,会传递给FastCGI进程。在Window下为了使用Nginx+PHP组合,就必须这么做了。

现在这个模式解决了CGI资源消耗问题,但是又有了新的问题:一次性启动的进程,如果因为某种原因异常退出了,如何重启未解决;多个进程监听同一个端口,那么Nginx投递请求过来时,必定引起进程争抢请求现象(这个是资源浪费);如果请求量加大,无法动态加大进程池的数量(PHP是单进程堵塞模式,每次只能处理一个请求)。

看起来,这个进程池里面的进程,需要一个主管。它负责把请求交给谁处理,监控进程池的进程是否异常退出然后重启,根据请求情况动态增加进程池进程数量等等。这个就是进程管理器的概念。一般称为Fast CGI Process Manager,简称就是FPM,而PHP实现的FPM就叫PHP-FPM。PHP中的PHP-FPM主进程目前应该使用的是Reactor模型,它负责hold住请求,socket监听绑定(没有FPM时是各个进程同时监听和绑定,被唤醒时存在争抢),FPM可以均衡控制子进程处理的请求数量,在到达一定数量后,可以重启该进程(防止内存泄露问题),监控进程异常退出,动态加大进程池等。总之,PHP-FPM是一个非常不错的实现(Window下没有这个FPM)。

接下来具体看看PHP-FPM模型:

安装了php-fpm模块后,会对应一个服务启动脚本:

cat /usr/lib/systemd/system/php-fpm.service
[Unit]
Description=The PHP FastCGI Process Manager
After=syslog.target network.target

[Service]
Type=notify
PIDFile=/var/run/php-fpm/php-fpm.pid
EnvironmentFile=/etc/sysconfig/php-fpm
ExecStart=/usr/sbin/php-fpm --nodaemonize --fpm-config /etc/php-fpm.conf
ExecReload=/bin/kill -USR2 $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

可以看到,/usr/sbin/php-fpm就是进程管理器,它接收配置文件,根据配置启动进程池。进程池里面的进程不是调用php-cgi而来的,而是从php-fpm fork出来的,php-fpm进程是常驻内存的,解释器(也可以认为是zend引擎)在每个进程中都独立存在一份,它的所谓opcode缓存也是在进程没有退出前有效的,由于PHP的特点,脚本执行完毕,脚本相关的一切变量都会被销毁(可以看做是在一个沙箱中执行代码,完了后把整个沙箱干掉),基于这样的事实,什么垃圾回收之类的都是浮云,垃圾回收也只能在脚本运行中回收。另外,当脚本中大量include文件进来时,这些文件编译后可以被缓存起来(opcode),但是缓存也是有大小限制的,当新的请求进来时,如果include了相同的文件,那么就可以从缓存中取出来执行,如果没有就从文件系统读入文件,编译成字节码,放入缓存,执行字节码。

注:这里无法确定的是当有缓存时,是否需要搬动。

nginx-php-fpm

root       25711     1  nginx: master process /usr/sbin/nginx
www      25712 25711  nginx: worker process
www      25713 25711  nginx: worker process

root       25726     1  php-fpm: master process (/etc/php-fpm.conf)
www      25727 25726  php-fpm: pool www
www      25728 25726  php-fpm: pool www

/etc/php-fpm.conf

#################################
#全局配置
#################################
[global]
pid = /var/run/php-fpm/php-fpm.pid
error_log = /var/log/php-fpm/error.log
;syslog.facility = daemon
;syslog.ident = php-fpm
;log_level = notice
;emergency_restart_threshold = 0
;emergency_restart_interval = 0
;process_control_timeout = 0
;process.max = 128
;process.priority = -19
daemonize = yes
;rlimit_files = 1024
;rlimit_core = 0
;events.mechanism = epoll
;systemd_interval = 10
include=/etc/php-fpm.d/*.conf

/etc/php-fpm.d/www.conf

#################################
#针对池的配置
#################################
[www]
listen = 127.0.0.1:9000
;listen.backlog = -1
listen.allowed_clients = 127.0.0.1

;listen.owner = nobody
;listen.group = nobody
;listen.mode = 0666

user = www
group = www

pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
;pm.max_requests = 500

;pm.status_path = /status
;ping.path = /ping
;ping.response = pong
;request_terminate_timeout = 0
;request_slowlog_timeout = 0
slowlog = /var/log/php-fpm/www-slow.log
 
rlimit_files = 10240
;rlimit_core = 0
;chroot = 
;chdir = /var/www
;catch_workers_output = yes
;security.limit_extensions = .php .php3 .php4 .php5

;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin
;env[TMP] = /tmp
;env[TMPDIR] = /tmp
;env[TEMP] = /tmp

;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
;php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 10240M

; Set session path to a directory owned by process user
php_value[session.save_handler] = files
php_value[session.save_path]    = /var/lib/php/session
php_value[soap.wsdl_cache_dir]  = /var/lib/php/wsdlcache

当配置了多个池时,如果日志都写入/var/log/php-fpm/,那么需要确保不同池的用户都可以往这个文件夹写入内容。同样的/var/lib/php也是如此。

错误日志:
如果应用程序由捕捉异常,相关的日志信息由程序的自定义程序处理。如果没有就会上交到PHP,如果PHP中配置了记录日志的位置,那么日志就会被记录到这个文件中,如果没有就会上交给HTTP服务器,比如Nginx,在HTTP服务器这个级别,如果对不同域做了配置,那么就记录到这个域下的配置中,如果没有则统一记录到error_log指令指向的文件。

参考:http://blog.ifeeline.com/1901.html

MySQL 插入数据之replace和ignore

MySQL中处理常规的insert into之外,也提供了replace insert和insert ignore into语句。

当需要插入不重复的值(有唯一索引),常规的插入就必须先判断是否存在,然后再进行插入,这个在需要批量插入数据时,需要循环查询判断,如果使用replace insert和insert ignore into语句就不需要这样做,insert ignore into很好理解,对比唯一索引,如果存在直接跳过,而replace insert是指存在时,对存在的数据行进行更新,准确来说应该是对存在的数据行进行删除,然后插入新的。

所以使用replace insert要特别小心,它是先删除,再插入,比如插入一个已经存在的行,它会返回受影响的行是2,如果新的行没有包含原来的全部数据,那么这部分数据将丢失,如果设置了id为自动增长的,就可以看到,id将不会连续(先删除后插入的缘故)。

以下是一个trait,用来扩展Laravel ORM模型以支持insertReplace和insertIgnore这样的语法:

<?php

namespace Ebt\ModelExtend;

trait InsertReplaceable 
{
    public static function insertReplace(array $attributes = []) 
    {
        return static::executeQuery ( 'replace', $attributes );
    }
    
    public static function insertIgnore(array $attributes = []) 
    {
        return static::executeQuery ( 'insert ignore', $attributes );
    }
    
    protected static function executeQuery($command, array $attributes) 
    {
        $prefix = \DB::connection()->getTablePrefix();
        if (! count ( $attributes )) {
            return true;
        }
        $model = new static ();
        if ($model->fireModelEvent ( 'saving' ) === false) {
            return false;
        }
        $attributes = collect ( $attributes );
        $first = $attributes->first ();
        if (! is_array ( $first )) {
            $attributes = collect ( [ 
                $attributes->toArray () 
            ] );
        }
        $keys = collect ( $attributes->first () )->keys ()->transform ( function ($key) {
            return "`" . $key . "`";
        } );
        $bindings = [ ];
        $query = $command . " into " .  $prefix . $model->getTable () . " (" . $keys->implode ( "," ) . ") values ";
        $inserts = [ ];
        foreach ( $attributes as $data ) {
            $qs = [ ];
            foreach ( $data as $value ) {
                $qs [] = '?';
                $bindings [] = $value;
            }
            $inserts [] = '(' . implode ( ",", $qs ) . ')';
        }
        $query .= implode ( ",", $inserts );
        \DB::connection ( $model->getConnectionName () )->insert ( $query, $bindings );
        $model->fireModelEvent ( 'saved', false );
    }
}

用法:

$data = [
    [
        "name" => 'ifeeline',
        "note" => "xx"
    ],
    [
        "name" => 'ifeeline2',
        "note" => "yy"
    ],
];
//\App\TestTest::insertReplace($data);
\App\TestTest::insertIgnore($data);

PHP时间验证范例

$dt = "2016-02-30 01:04:44";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);        
print_r($e);

// 2月份肯定没有30号,发出警告
Array
(
    [year] => 2016
    [month] => 2
    [day] => 30
    [hour] => 1
    [minute] => 4
    [second] => 44
    [fraction] =>
    [warning_count] => 1
    [warnings] => Array
        (
            [19] => The parsed date was invalid
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

$dt = "2016-02-22 01:04:60";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);        
print_r($e);
// 时间没有60秒
Array
(
    [year] => 2016
    [month] => 2
    [day] => 22
    [hour] => 1
    [minute] => 4
    [second] => 60
    [fraction] =>
    [warning_count] => 1
    [warnings] => Array
        (
            [19] => The parsed time was invalid
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

$dt = "2016-2-2 01:04:20";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);        
print_r($e);
// 合法,不过好像不是很满意,月份和日期望是两位数
Array
(
    [year] => 2016
    [month] => 2
    [day] => 2
    [hour] => 1
    [minute] => 4
    [second] => 20
    [fraction] =>
    [warning_count] => 0
    [warnings] => Array
        (
        )

    [error_count] => 0
    [errors] => Array
        (
        )

    [is_localtime] =>
)

从输出可以看到,要保证给定时间合法,输出的数组warning_count和error_count都应该等于0,从输出来看,它只是根据给定的格式来解析时间,所以2和02都是合法的月份。那么,在验证通过后,可以对时间来一次格式化:

$dt = "2016-2-2 01:04:20";
$e=date_parse_from_format("Y-m-d H:i:s",$dt);  
if(($e['warning_count'] === 0) && ($e['error_count'] === 0)) {
    $dt = date("Y-m-d H:i:s", strtotime($dt));
    echo $dt;
}

看起来,这样做法是可以的。另外,strtotime()在转换一个字符串为时间戳时,如果不成功就返回false,看如下例子:

        // 输出2016-03-02 01:04:20 
        $dt = "2016-2-31 01:04:20";
        $fdt = strtotime($dt);
        if(false !== $fdt) {
            echo date("Y-m-d H:i:s", $fdt)."\n";
        } else {
            echo $dt." xxx\n";
        }
               
        // 无法转换
        $dt = "2016-2-2 01:04:61";
        $fdt = strtotime($dt);
        if(false !== $fdt) {
            echo date("Y-m-d H:i:s", $fdt)."\n";
        } else {
            echo $dt." xxx\n";
        }

可以看到,2月31号,它认为是合法的,实际会给你变成3月2号,但是61秒就无法向上进一变成05分02秒,很明显,这个不是我们想要的,不符合预期。

PHP Laravel Excel

PHP Laravel Excel官方网址:http://www.maatwebsite.nl/laravel-excel/docs;

GitHub: https://github.com/Maatwebsite/Laravel-Excel;
Packagist: https://packagist.org/packages/maatwebsite/excel

这个程序包依赖PHPOffice/PHPExcel,实际就是在这个工具包上做了一些易于用户操作的封装。需要说明的是,目录依赖的PHPOffice/PHPExcel版本是1.8.*(也是当前的稳定版),在1.9.*和2.0.*中开始引入PHP的命名空间,意味着PHP版本至少要5.3以上,这两个分支还在开发中,看起来这个包的作者非常的保守。

安装就按照Laravel套路来就好:

#往composer.json中添加"maatwebsite/excel": "~2.1.0",然后update

#添加ServiceProvider
vi config/app.php
'Maatwebsite\Excel\ExcelServiceProvider',

#添加Facade(可选)
'Excel' => 'Maatwebsite\Excel\Facades\Excel',

#配置(会添加excel.php配置文件)
php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider"

#获取excel实例
$excel = App::make('excel');

Maatwebsite/excel本身有一个默认的配置文件,如果应用级别有配置文件,那么应用配置将覆盖默认配置配置,具体实现是在ExcelServiceProvider中:

    public function boot()
    {
        $this->publishes([
            __DIR__ . '/../../config/excel.php' => config_path('excel.php'),
        ]);

        $this->mergeConfigFrom(
            __DIR__ . '/../../config/excel.php', 'excel'
        );

        //Set the autosizing settings
        $this->setAutoSizingSettings();
    }

表格操作,主要涉及输入和输出。

Excel::load('file.xls', function($reader) {

    // Getting all results
    $results = $reader->get();

    // ->all() is a wrapper for ->get() and will work the same
    $results = $reader->all();

});

使用get和all方法获取结果,默认,如果表格只有一个sheet,那么直接返回行集合,如果有多个sheet,那么返回sheet的集合,每个sheet又是行的集合。为了统一操作,可以设置配置文件中的force_sheets_collection设置为true,这样都会返回sheet的集合。

表格sheet的第一行是头部,默认会被转换成slug(别名),可以设置import.heading为false表示不使用文件头,可用值true|false|slugged|slugged_with_count|ascii|numeric|hashed|trans|original,设置为original表示使用字面值作为key,这个比较常见。

Sheet/行/单元格都是集合,使用了get()之后,就可以使用集合的方法。

#
$reader->get()->groupBy('firstname');

#依赖force_sheets_collection,可能返回第一个sheet或第一个行
$reader->first();
// Get workbook title
$workbookTitle = $reader->getTitle();

foreach($reader as $sheet)
{
    // get sheet title
    $sheetTitle = $sheet->getTitle();
}

// You can either use ->take()
$reader->take(10);

// Or ->limit()
$reader->limit(10);

// Skip 10 results
$reader->skip(10);

// Skip 10 results with limit, but return all other rows
$reader->limit(false, 10);

// Skip and take
$reader->skip(10)->take(10);

// Limit with skip and take
$reader->($skip, $take);

$reader->toArray();

$reader->toObject();

// Dump the results
$reader->dump();

// Dump results and die
$reader->dd();

#也可以使用foreach
// Loop through all sheets
$reader->each(function($sheet) {

    // Loop through all rows
    $sheet->each(function($row) {

    });

});

选择Sheet和列

#仅加载sheet1
Excel::selectSheets('sheet1')->load("xx.xls", function($reader) {});
Excel::selectSheets('sheet1', 'sheet2')->load();

#通过下标选择比较靠谱
// First sheet
Excel::selectSheetsByIndex(0)->load();

// First and second sheet
Excel::selectSheetsByIndex(0, 1)->load();

#选择列,很熟悉的用法?
#All get methods (like all(), first(), dump(), toArray(), ...) accept an array of columns.
// Select
$reader->select(array('firstname', 'lastname'))->get();

// Or
$reader->get(array('firstname', 'lastname'));

日期:
By default the dates will be parsed as a Carbon object.

分批导入:

Excel::filter('chunk')->load('file.csv')->chunk(250, function($results)
{
        foreach($results as $row)
        {
            // do stuff
        }
});

每次读入250行,处理完毕在导入250行??

批量导入:

Excel::batch('app/storage/uploads', function($rows, $file) {

    // Explain the reader how it should interpret each row,
    // for every file inside the batch
    $rows->each(function($row) {

        // Example: dump the firstname
        dd($row->firstname);

    });

});

$files = array(
    'file1.xls',
    'file2.xls'
);

Excel::batch($files, function($rows, $file) {

});

Excel::batch('app/storage/uploads', function($sheets, $file) {

    $sheets->each(function($sheet) {

    });

});

导出也有很多定制化的操作,参考:http://www.maatwebsite.nl/laravel-excel/docs/export

例子:

// 关联数组,输出表头
$excel_array = [
    [
        "表头1" => "xxxx",
        "表头2" => "yyyy"
    ],
    [
        "表头1" => "xxxx2",
        "表头2" => "yyyy3"
    ]
];
// 直接数据输入
$excel_array2 = [
    [
        "表头1", "表头2"
    ],
    [
        "xxxx", "yyyy"
    ],
    [
        "xxxx2", "yyyy3"
    ]
];

        \Excel::create("test1", function ($excel) use($excel_array) {
            $excel->sheet('sheet1', function ($sheet) use($excel_array) {
                $sheet->fromArray($excel_array);
            });
        })->store("xls","d:/");
        
        \Excel::create("test2", function ($excel) use($excel_array2) {
            $excel->sheet('sheet1', function ($sheet) use($excel_array2) {
                $sheet->fromArray($excel_array2, null, 'A1', false, false);
            });
        })->save("xls");

默认,如果不指定导入的路径,会保存到storage_path(‘exports’),即app/storage/exports。可以修改配置文件export.store.path的值。

导出的sheet,默认第一行总是头部,这个可以修改配置文件的export.generate_heading_by_indices为false取消这个默认值。也可以指定fromArray的第5参数为false达到同样目的。

store()的第一参数是导入文件的类型,第二参数是路径(不需要包含文件名),第三参数控制是否返回保存文件的信息(比如保存的路径,扩展名等)。

PHP GuzzleHttp使用文档

内容来自:http://docs.guzzlephp.org/en/latest/,这个Http库实现上可以用完美来形容,之前使用过Zend\Http库,感觉已经很不错了,不过看起来,GuzzleHttp更胜一筹。

Guzzlehttp/guzzle当前最新分支6.x,5.x是维护阶段,4.x之前已经终结。

6.x需要PHP5.5以上,依赖guzzlehttp下的psr7和promises包。它提供两种驱动支持,一是PHP的stream扩展,需要在php.ini中启用allow_url_fopen,二是cURL扩展。如果cURL没有安装,那么就使用PHP的stream扩展,也可以指定自己的驱动。

Composer安装:

 {
   "require": {
      "guzzlehttp/guzzle": "~6.0"
   }
}

开始开始:

use GuzzleHttp\Client;
// 客户端
$client = new Client([
    'base_uri' => 'https://foo.com/api/',
    'timeout'  => 2.0,
]);
// 发起请求
$response = $client->request('GET', 'test');
$response = $client->request('GET', '/root');

看明白这里的请求URL是关键,第一个发起的URL是https://foo.com/api/test,第二发起的URL是https://foo.com/root。(RFC 3986规范)

客户端Client构造函数接受的参数是base_uri,handler和任何请求参数(可以传递到request对象的参数)。这里的参数除了handler,都是可以覆盖的。handler参数的解释:
“(callable) Function that transfers HTTP requests over thewire. The function is called with a Psr7\Http\Message\RequestInterface and array of transfer options, and must return a GuzzleHttp\Promise\PromiseInterface that is fulfilled with a Psr7\Http\Message\ResponseInterface on success. “handler” is a constructor only option that cannot be overridden in per/request options. If no handler is provided, a default handler will be created that enables all of the request options below by attaching all of the default middleware to the handler.”

要理解这段话并不容易。大体上是说这个handler被Psr7\Http\Message\RequestInterface对象调用返回一个被Psr7\Http\Message\ResponseInterface填充的GuzzleHttp\Promise\PromiseInterface。一般我们理解应该是返回Response,这里返回一个Promise,引入了一个中间层,实际是为了可以产生异步调用而准备的。Promise可以是同步的,也可以是异步的。

看这个例子:

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;

$handler = new CurlHandler();
$stack = HandlerStack::create($handler); // Wrap w/ middleware
$client = new Client(['handler' => $stack]);

可见,hanlder就是底层实际传输http内容的工具,比如curl,stream。(默认的hanlder就是优先使用curl,如果要强制使用curl,可以参考这个例子),可以给halder添加中间件。

发起请求:

$response = $client->get('http://httpbin.org/get');
$response = $client->delete('http://httpbin.org/delete');
$response = $client->head('http://httpbin.org/get');
$response = $client->options('http://httpbin.org/get');
$response = $client->patch('http://httpbin.org/patch');
$response = $client->post('http://httpbin.org/post');
$response = $client->put('http://httpbin.org/put');

#替代
$response = $client->request('GET',"");

也可以先创建一个请求对象,然后通过Client的send的方法发起请求:

use GuzzleHttp\Psr7\Request;

$request = new Request('PUT', 'http://httpbin.org/put');
$response = $client->send($request, ['timeout' => 2]);

对应,可以使用sendAsync()和requestAsync()发起异步请求:

use GuzzleHttp\Psr7\Request;

// Create a PSR-7 request object to send
$headers = ['X-Foo' => 'Bar'];
$body = 'Hello!';
$request = new Request('HEAD', 'http://httpbin.org/head', $headers, $body);

// Or, if you don't need to pass in a request instance:
$promise = $client->requestAsync('GET', 'http://httpbin.org/get');

如果是异步请求,可以使用then方法接收响应,或者异常:

use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;

$promise = $client->requestAsync('GET', 'http://httpbin.org/get');
$promise->then(
    function (ResponseInterface $res) {
        echo $res->getStatusCode() . "\n";
    },
    function (RequestException $e) {
        echo $e->getMessage() . "\n";
        echo $e->getRequest()->getMethod();
    }
);

有了异步的实现,那么就可以并行发起一批请求:

use GuzzleHttp\Client;
use GuzzleHttp\Promise;

$client = new Client(['base_uri' => 'http://httpbin.org/']);

// Initiate each request but do not block
$promises = [
    'image' => $client->getAsync('/image'),
    'png'   => $client->getAsync('/image/png'),
    'jpeg'  => $client->getAsync('/image/jpeg'),
    'webp'  => $client->getAsync('/image/webp')
];

// Wait on all of the requests to complete. Throws a ConnectException
// if any of the requests fail
$results = Promise\unwrap($promises);

// Wait for the requests to complete, even if some of them fail
$results = Promise\settle($promises)->wait();

// You can access each result using the key provided to the unwrap
// function.
echo $results['image']->getHeader('Content-Length');
echo $results['png']->getHeader('Content-Length');

或者使用Pool来进行并发请求:

use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;

$client = new Client();

$requests = function ($total) {
    $uri = 'http://127.0.0.1:8126/guzzle-server/perf';
    for ($i = 0; $i < $total; $i++) {
        yield new Request('GET', $uri);
    }
};

$pool = new Pool($client, $requests(100), [
    'concurrency' => 5,
    'fulfilled' => function ($response, $index) {
        // this is delivered each successful response
    },
    'rejected' => function ($reason, $index) {
        // this is delivered each failed request
    },
]);

// Initiate the transfers and create a promise
$promise = $pool->promise();

// Force the pool of requests to complete.
$promise->wait();

使用响应:

$code = $response->getStatusCode(); // 200
$reason = $response->getReasonPhrase(); // OK

// Check if a header exists.
if ($response->hasHeader('Content-Length')) {
    echo "It exists";
}

// Get a header from the response.
echo $response->getHeader('Content-Length');

// Get all of the response headers.
foreach ($response->getHeaders() as $name => $values) {
    echo $name . ': ' . implode(', ', $values) . "\r\n";
}

$body = $response->getBody();
// Implicitly cast the body to a string and echo it
echo $body;
// Explicitly cast the body to a string
$stringBody = (string) $body;
// Read 10 bytes from the body
$tenBytes = $body->read(10);
// Read the remaining contents of the body as a string
$remainingBytes = $body->getContents();

查询参数:

$response = $client->request('GET', 'http://httpbin.org?foo=bar');

$client->request('GET', 'http://httpbin.org', [
    'query' => ['foo' => 'bar']
]);

$client->request('GET', 'http://httpbin.org', ['query' => 'foo=bar']);

上传数据(数据直接作为body体):

// Provide the body as a string.
$r = $client->request('POST', 'http://httpbin.org/post', [
    'body' => 'raw data'
]);

// Provide an fopen resource.
$body = fopen('/path/to/file', 'r');
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => $body]);

// Use the stream_for() function to create a PSR-7 stream.
$body = \GuzzleHttp\Psr7\stream_for('hello!');
$r = $client->request('POST', 'http://httpbin.org/post', ['body' => $body]);

// 传送JSON数据
$r = $client->request('PUT', 'http://httpbin.org/put', [
    'json' => ['foo' => 'bar']
]);

POST表单请求(如果是GET的表单,就是简单的查询字符串,不是这里讨论的内容)

$response = $client->request('POST', 'http://httpbin.org/post', [
    'form_params' => [
        'field_name' => 'abc',
        'other_field' => '123',
        'nested_field' => [	// 嵌套,checkbox多选
            'nested' => 'hello'
        ]
    ]
]);

// 上传文件
$response = $client->request('POST', 'http://httpbin.org/post', [
    'multipart' => [
        [
            'name'     => 'field_name',
            'contents' => 'abc'
        ],
        [
            'name'     => 'file_name',
            'contents' => fopen('/path/to/file', 'r')
        ],
        [
            'name'     => 'other_file',
            'contents' => 'hello',
            'filename' => 'filename.txt',
            'headers'  => [
                'X-Foo' => 'this is an extra header to include'
            ]
        ]
    ]
]);

Cookies

// Use a specific cookie jar
$jar = new \GuzzleHttp\Cookie\CookieJar;
$r = $client->request('GET', 'http://httpbin.org/cookies', [
    'cookies' => $jar
]);

// 如果要对所有请求使用cookies,可以在Client指定使用cookies
// Use a shared client cookie jar
$client = new \GuzzleHttp\Client(['cookies' => true]);
$r = $client->request('GET', 'http://httpbin.org/cookies');

重定向(GET请求和POST请求的重定向需要注意)
Guzzle自动跟踪重定向,可以明确关闭:

$response = $client->request('GET', 'http://github.com', [
    'allow_redirects' => false
]);
echo $response->getStatusCode();
// 301

异常:

use GuzzleHttp\Exception\ClientException;

try {
    $client->request('GET', 'https://github.com/_abc_123_404');
} catch (ClientException $e) {
    echo $e->getRequest();
    echo $e->getResponse();
}

请求对象可选项:

##############
allow_redirects
默认:
[
    'max'             => 5,
    'strict'          => false,
    'referer'         => true,
    'protocols'       => ['http', 'https'],
    'track_redirects' => false
]

#指定false关闭
$res = $client->request('GET', '/redirect/3', ['allow_redirects' => false]);
echo $res->getStatusCode();

##############
auth
$client->request('GET', '/get', ['auth' => ['username', 'password']]);
$client->request('GET', '/get', [
    'auth' => ['username', 'password', 'digest']
]);

##############
body 请求体内容

##############
cert

##############
cookies 可以在Client设置cookie为true以设置所有请求使用cookie

$jar = new \GuzzleHttp\Cookie\CookieJar();
$client->request('GET', '/get', ['cookies' => $jar]);

##############
connect_timeout
默认为0,表示不超时(无限等待)

##############
debug 可以输出调试信息,或把调试信息写入文件(fopen)

##############
decode_content
默认为ture,表示解码服务端回送的压缩的内容

##############
delay

##############
expect

##############
form_params

##############
headers

$client->request('GET', '/get', [
    'headers' => [
        'User-Agent' => 'testing/1.0',
        'Accept'     => 'application/json',
        'X-Foo'      => ['Bar', 'Baz']
    ]
]);

##############
http_errors
默认为true,表示出错时抛出异常

##############
json

##############
multipart

##############
on_headers

##############
query
$client->request('GET', '/get?abc=123', ['query' => ['foo' => 'bar']]);

##############
sink
保存请求体
$resource = fopen('/path/to/file', 'w');
$client->request('GET', '/stream/20', ['sink' => $resource]);

$resource = fopen('/path/to/file', 'w');
$stream = GuzzleHttp\Psr7\stream_for($resource);
$client->request('GET', '/stream/20', ['save_to' => $stream]);

##############
verify
默认为true,验证SSL证书

##############
timeout
默认为0,不超时。(请求超时)

当使用curl时(默认优先使用,如果指定curl参数,最后可以明确指定使用curl作为hanlder,否则无效):

$client->request('GET', '/', [
    'curl' => [
        CURLOPT_INTERFACE => 'xxx.xxx.xxx.xxx'
    ]
]);

HTTP BASIC认证操作实践

HTTP BASIC认证是HTTP层次内容,以下使用PHP来实现这个过程:

vi basic.php

<?php
$users = [
    "admin" => "xxxxxx"
];
$needAuth = true;
if(!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
    // 用户名和密码
    $user = trim($_SERVER['PHP_AUTH_USER']);
    $pwd = trim($_SERVER['PHP_AUTH_PW']);

    if(isset($users[$user]) && ($users[$user] === $pwd)) {
        $needAuth = false;
    }
}

if($needAuth) {
    header("Content-type:text/html;charset=utf-8");
    header('WWW-Authenticate: Basic realm="admin xxxxxx"');
    header('HTTP/1.0 401 Unauthorized');
    echo date("Y-m-d H:i:s")." -> Need Auth...";
    exit;
}

echo date("Y-m-d H:i:s")." -> Auth...";

http_basic_auth
对于一个小应用程序,如果需要做隐藏保护,这个方式会非常便利。不过这个认证方式更多见于API访问中。

查看发送的HTTP请求头:
http_basic_header
用户名和密码通过HTTP的一个请求头Authorization来传输的,内容是Basic YWRtaW46eHh4eHh4,第一个字符串Basic为认证方式,第二个字符串是用户名和密码冒号分隔的字符串的base64编码(admin:xxxxxx -> YWRtaW46eHh4eHh4)。

这个用户名和密码传递到服务器端,对于Nginx(Apache等),它可以首先处理,也可以继续转发到PHP,让PHP来处理(这里就是这个情况)。PHP接收这两个变量使用:

$_SERVER['PHP_AUTH_USER']
$_SERVER['PHP_AUTH_PW']

这两个变量是经过了base64解码之后得到的,这个解码应该是HTTP服务进行的,把得到的变量传递给PHP。注意,这里的base64编码目的不是在加密,而是方便传输。所有如果直接通过HTTP传输是不安全的(其它的一般用户名密码登录也一样),所以,对于API设计,为了安全,一般通过HTTPS传送数据。

BASCIC认证是HTTP层次的内容,所以对于Nginx这样的HTTP服务器软件,当然可以配置其进行BASIC认证,这样就不需要由PHP来处理。Nginx配置参考:

server {
    listen       80;
    server_name  xx.xx.xx.xx;
    root /var/www/xxx/public;
    index index.php

    error_page 404 /index.php;
    
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/conf.d/htpasswd;

    if (!-e $request_filename) {
        rewrite ^/(.+)$ /index.php last;
        break;
    }

    location / {
	root /var/www/xxx/public;
	try_files $uri $uri/ /index.php?$query_string;
    }

    location ~* ^.+\.(css|js|jpeg|jpg|gif|png|ico|eot|ttf|woff|svg) {
        expires 30d;
    }

    location ~* \.(eot|ttf|woff|svg|html)$ {
        add_header Access-Control-Allow-Origin *;
    }

    location ~ .(php|php5)?$ {
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 256 32k;

        fastcgi_pass   127.0.0.1:9000;
	fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
   }
}

主要是添加auth_basic指令和auth_basic_user_file指令,auth_basic和PHP中的如下两行设置类似:

header('WWW-Authenticate: Basic realm="admin xxxxxx"');
header('HTTP/1.0 401 Unauthorized');

指令auth_basic的值直接对应Basic realm=”xxx”中的xxx值,表示是BASIC认证,认证的用户名和密码是auth_basic_user_file指定的密码文件,这个文件中保存的用户名密码可不是用base64编码的,它使用的是Hash算法,格式:

vfeelit:CQArTEgiT84So:注释

匹配过程大体应该是这样:获取HTTP的Authorization请求头,从中获取经过base64编码的字符串,解码获取用户名和密码,然后匹配用户名,再通过Hash算法得到密码的Hash值,最后和保存的Hash值进行比较。

这个密码文件产生(htpasswd或者openssl):

# printf "vfeelit:$(openssl passwd -crypt 123456)\n" >> conf.d/htpasswd
# cat conf.d/htpasswd 
vfeelit:DmZ3GXV9zFegY

Laravel 队列详解

配置
config/queue.php
QUEUE_DRIVER现在改为在.env中设置,最简单的方式是使用数据库,改为database。然后建立队列数据表:

php artisan queue:table

建立一个数据迁移文件,运行一下:

//composer dump-autoload
php artisan migrate

然后就在数据库中建立jobs表。

能够进队列的工作都放在App\Jobs目录下(以前叫Commands,5.1中也兼容Commands,实际上就该了个名字而已),如下建立一个可以放入队列的命令:

php artisan make:job SendEmail --queued
//php artisan make:command SendEmail --queued

注意–queued参数,表示这个命令是可以压入队列的,通常意味着是后台执行了。工作类中有一个handle方法,工作运行时的方法。

要把一个工作放入队列执行可以使用:

Queue::push(new SendEmail($message));

一些细节:————————————————–
Queue是一个Facade,对应关系Queue – queue – Illuminate\Queue\QueueManager,正常流程,在容器中看不到queue这个key。查看流程中的deferredServices数组:

#deferredServices: array:84 [
    "queue" => "Illuminate\Queue\QueueServiceProvider"
    "queue.worker" => "Illuminate\Queue\QueueServiceProvider"
    "queue.listener" => "Illuminate\Queue\QueueServiceProvider"
    "queue.failer" => "Illuminate\Queue\QueueServiceProvider"
]

可以知道,Illuminate\Queue\QueueServiceProvider是一个deferred服务,从实用的角度,定义一个服务提供者时,只要定义protected $defer = true(默认为false),就会标记为延时服务。框架的服务在app.conf的providers数组中(Illuminate\Queue\QueueServiceProvider::class, 它的$defer为true),这些服务会在Illuminate\Foundation\Bootstrap\RegisterProviders的bootstrap()方法中进入(这个是框架bootstrap阶段其中的一个步骤),方法中运行$app->registerConfiguredProviders(),见名知意,就是注册配置的服务,具体的规则准守以上的规则(细节就不再跟踪了)。从deferredServices的输出可以猜测,需要queue或queue.work等时,这个QueueServiceProvider就会被启动,这个应该跟一般的服务启动是一样的,这里是延迟到了真需要时才启动(这种对于不是每次请求都需要,或者只是某些请求才需要的服务,会非常有用)。使用当使用Queue这个facade时,对应的服务启动,这个服务会注册一个叫queue的Illuminate\Queue\QueueManager的实例。通过它来进行队列任务管理。

一个最简单的场景,压一个工作进入具体的队列,那么这个管理器需要提供push方法,不过在push之前,要先链接上具体的队列链接上,那么它应该有一个connection的概念,可能有多个队列,那么总是有默认的吧,等等,很多的Manager都类似。也就是说,Manager通常充当一个生产工厂和一个监视器。(这里只是队列里面的生产者,还有消费者,当然它们都是需要先链上队列软件的)

    public function __call($method, $parameters)
    {
        $callable = [$this->connection(), $method];

        return call_user_func_array($callable, $parameters);
    }

它的push方法就是这样来的。用如下图了描述这个组件:
queue
Connectors是在Service中填充的,它决定哪些Connectors可用,通过调用Connector产生具体的Connection,它是队列的抽象,实际上它才是主战场,比如它有push,pop方法,对应入队和出队。平时说的队列客户端,就是这么个东西。
—————————————————————————————

可以具体的控制器代码中调用:

//控制器中的使用
$job = (new SendEmail($message))->onQueue('emails');
$this->dispatch($job);

// 类似
Queue::pushOn('emails', new SendEmail($message));

这样命令发送到emails队列执行。注意,命名的队列必须启用。

一些细节:—————————————————–
App\Http\Controllers\Controller中use了DispatchesJobs这个trail,里面定义了:

    protected function dispatch($job)
    {
        return app('Illuminate\Contracts\Bus\Dispatcher')->dispatch($job);
    }

这个Illuminate\Contracts\Bus\Dispatcher实际会启动一个延后的服务叫Illuminate\Bus\BusServiceProvider,这个服务的register方法:

    public function register()
    {
        $this->app->singleton('Illuminate\Bus\Dispatcher', function ($app) {
            return new Dispatcher($app, function () use ($app) {
                return $app['Illuminate\Contracts\Queue\Queue'];
            });
        });

        $this->app->alias(
            'Illuminate\Bus\Dispatcher', 'Illuminate\Contracts\Bus\Dispatcher'
        );

        $this->app->alias(
            'Illuminate\Bus\Dispatcher', 'Illuminate\Contracts\Bus\QueueingDispatcher'
        );
    }

Illuminate\Bus\Dispatcher内部最终会启动$app[‘Illuminate\Contracts\Queue\Queue’],它也对应一个延后服务,不过它被闭包函数包围了,在调用Illuminate\Bus\Dispatcher实例的dispatch()方法前还不会启动queue,跟踪一下这个方法:

    public function dispatch($command, Closure $afterResolving = null)
    {
        if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
            return $this->dispatchToQueue($command);
        } else {
            return $this->dispatchNow($command, $afterResolving);
        }
    }

这里的$this->queueResolver就是Illuminate\Bus\Dispatcher实例生成时传递的那个闭包函数(用来启动queue),判断$command是否可入队,如果可以,就调用dispatchToQueue($command)负责这个事情,否则就是dispatchNow():

    public function dispatchToQueue($command)
    {
        // 这里启动queue
        $queue = call_user_func($this->queueResolver);
        
        if (! $queue instanceof Queue) {
            throw new RuntimeException('Queue resolver did not return a Queue implementation.');
        }
        // 如果有queue方法
        if (method_exists($command, 'queue')) {
            return $command->queue($queue, $command);
        } else {
            return $this->pushCommandToQueue($queue, $command);
        }
    }

更多细节就不跟踪了,具体用法会有例子。不过这里需要说明,能入队的,必须是实现了Illuminate\Contracts\Queue\ShouldQueue接口的对象(实际它啥也没有,只是为了区分)。另外,可以dispatch的,不仅是入队的对象,也可以是马上要执行的对象(dispatchNow负责),它可以把控制器中的大逻辑或者可重用的逻辑提取出来作为一个工作,这对分离大控制器和提取重用逻辑非常有用。

控制器中的dispatch()获取app(‘Illuminate\Contracts\Bus\Dispatcher’),返回一个Bus分发器,在任何时候,我们都可以直接这样干(我们的代码一般都是在控制器中,所以不需要那么生硬的调用),获取到Bus分发器后就可以分发工作了。使用Bus的好处是既可以分发可以入队的工作,也可以不是。使用Queue就只能分入队的工作。

对于能入队的工作,仅仅需要知道这么一个流程,就足够了:一个工作对象会被序列化后入队,出队时反序列化,工作对象的数据自然需要通过它的构造函数带入(出队时才能有对应数据),反序列化后会去执行工作的handle方法。

关于入队,现在知道的,应该已经足够多了。不过还有一个出队问题。我们需要一个监听器,连续不停地扫描队列,取出任务执行,所以最简单的方法是拉起一个主进程,其中循环,不管是否取到工作,都拉起一个子进程,有则执行,无则空跑,然后就退出。Laravel本身提供的php artisan queue:listen就是这种方式。这个方式要说优点的话,就实现简单。缺点非常明显,每次拉起一个子进程,载入框架,完了释放,进程号增长非常快,存在内存回收问题(你要觉得PHP进程退出了内存就释放然后就可以回收了,我表示无语)。从测试来看,这个方法CPU占用过多,资源浪费严重。简直可以用操蛋来形容。

Laravel提供的第二种监听队列的方法,是令人满意的。就是php artisan queue:work –daemon的方式,这个方式也是循环(要实现不间断监听,只能做循环),不过它不会每次拉起一个进程来载入整个框架,而是只会载入一次,然后内部循环监听队列,取到任务就执行任务,取不到就继续循环。这个方式消费资源较少。不过可能存在内存泄露问题,可以模仿PHP-FPM管理器,每个PHP进程在处理了预定次数的脚本后自杀。另外,框架只载入一次,那么在框架初始化过程中打开的文件,数据库链接等,是不会长时间等着的,所以,必须在内循环每次对需要用到的资源再次初始化,就算空跑也是如此,Laravel的work模式就是这样的,所以,如果开启了20个work守护进程,没有设置sleep参数,假设框架开启数据库链接,那么你会看到大量的到数据库的链接,这个情况可以加入sleep参数来缓解,实际可以改为如果取到空任务,可以不需要再次初始化资源,直接返回。这个可能会改进吧….
—————————————————————————————

执行监听

#connection代表队列链接,比如使用数据库为队列
#connection就是database
php artisan queue:listen connection

php artisan queue:listen

php artisan queue:listen --timeout=60

php artisan queue:listen --sleep=5

php artisan queue:work

实际上queue:listen和queue:work效果是差不多的,区别在于queue:listen本身是一个监听器(内部循环),而queue:work是一个工作,执行完就退出(最多取一个,如果队列为空,马上退出),可以用queue:work –daemon让其变成一个监听器。如果不指定监听哪个队列,那么就是在监听名为default的队列,可以通过–queue指定要监听的队列,–queue可以接多个队列名,分别代表优先级(最先的优先级最高)。两种监听方法,测试来看,当抛出异常时,queue:work进程会退出(需要配合进程监视器来监控),而queue:listen则不会。

所以,最常用的做法,无非这样:

#这个用法建议不用
php artisan queue:listen --queue=emails --sleep=3 --tries=3
#效果差不多
php artisan queue:work --queue=emails --sleep=3 --tries=3 --daemon

监听名字为emails的队列,最多尝试3次。

已失败的工作

php artisan queue:failed-table
php artisan migrate

队列失败时自动放入。可以在命令中定义:

public function failed()
{
    // 当工作失败的时候会被调用……
}

这样就可以处理失败作业了,比如发送邮件等?

补充:
目前的队列使用数据库来进行模拟,看起来工作良好,不过数据库并不擅长干这个。实际使用上,当开启多个队列,每个队列启动多个监听进程时,很容易出现死锁,这个锁是来自数据库InnoDB的,相互在等待锁释放,这应该是个Bug。所以,目前比较理想的是使用beanstalk来作为队列,说起来,这个东西使用起来异常简单。

1 安装,参考http://blog.ifeeline.com/1268.html

#主要命令
mkdir -p /var/log/beanstalkd
/usr/local/bin/beanstalkd -b /var/log/beanstalkd -l 127.0.0.1

选项-b指定一个目录,用来存放队列数据。

2 设置supervistor监控(http://blog.ifeeline.com/2082.html)

vi beanstalkd.conf 

[program:beanstalkd]
command=/usr/local/bin/beanstalkd -b /var/log/beanstalkd -l 127.0.0.1
autostart=true
autorestart=true
user=root
redirect_stderr=true
stdout_logfile=/mnt/www/ebt/storage/logs/beanstalkd.log

3 Laravel中的设置

#安装依赖 Laravel依赖这个包链接beanstalkd
php composer.phar require pda/pheanstalk

#修改.env
QUEUE_DRIVER=beanstalkd

#修改config/queue.conf
'beanstalkd' => [
            'driver' => 'beanstalkd',
            'host'   => '192.168.1.168',
            'queue'  => 'default',
            'ttr'    => 60,
        ],

就这样,设置全部完成。这里把database改为beanstalkd后,对于失败的Job,框架仍然会把其放入failed_job表中。

——————————————————————————————————–
为了更加高效的使用Beanstalk,可以使用https://github.com/phalcongelist/beanspeak提供的C扩展驱动。不过需要按照约定重新封装一下:

# 替换系统的服务提供者
<?php
namespace Vfeelit\Queue;

use Illuminate\Queue\QueueServiceProvider as SystemQueueServiceProvider;
use Vfeelit\Queue\Connectors\BeanstalkdConnector;

class QueueServiceProvider extends SystemQueueServiceProvider
{
    protected function registerBeanstalkdConnector($manager)
    {
        if (extension_loaded('beanspeak')) {
            $manager->addConnector('beanstalkd', function () {
                return new BeanstalkdConnector;
            });
        } else {
            parent::registerBeanstalkdConnector($manager);
        }  
    }
}

#客户端封装
<?php
namespace Vfeelit\Queue;

use Illuminate\Queue\Queue;
use Illuminate\Contracts\Queue\Queue as QueueContract;
use Vfeelit\Queue\Jobs\BeanstalkdJob;

class BeanstalkdQueue extends Queue implements QueueContract
{
    /**
     * The Beanspeak\Client instance.
     *
     * @var Beanspeak\Client
     */
    protected $beanspeak;

    /**
     * The name of the default tube.
     *
     * @var string
     */
    protected $default;

    /**
     * The "time to run" for all pushed jobs.
     *
     * @var int
     */
    protected $timeToRun;

    /**
     * Create a new Beanspeak\Client queue instance.
     *
     * @param  \Beanspeak\Client  $beanspeak
     * @param  string  $default
     * @param  int  $timeToRun
     * @return void
     */
    public function __construct(\Beanspeak\Client $beanspeak, $default, $timeToRun)
    {
        $this->default = $default;
        $this->timeToRun = $timeToRun;
        $this->beanspeak = $beanspeak;
    }

    /**
     * Push a new job onto the queue.
     *
     * @param  string  $job
     * @param  mixed   $data
     * @param  string  $queue
     * @return mixed
     */
    public function push($job, $data = '', $queue = null)
    {
        return $this->pushRaw($this->createPayload($job, $data), $queue);
    }

    /**
     * Push a raw payload onto the queue.
     *
     * @param  string  $payload
     * @param  string  $queue
     * @param  array   $options
     * @return mixed
     */
    public function pushRaw($payload, $queue = null, array $options = [])
    {
        return $this->beanspeak->useTube($this->getQueue($queue))->put(
            $payload, 1024, 0, $this->timeToRun
        );
    }

    /**
     * Push a new job onto the queue after a delay.
     *
     * @param  \DateTime|int  $delay
     * @param  string  $job
     * @param  mixed   $data
     * @param  string  $queue
     * @return mixed
     */
    public function later($delay, $job, $data = '', $queue = null)
    {
        $payload = $this->createPayload($job, $data);

        $beanspeak = $this->beanspeak->useTube($this->getQueue($queue));

        return $beanspeak->put($payload, 1024, $this->getSeconds($delay), $this->timeToRun);
    }

    /**
     * Pop the next job off of the queue.
     *
     * @param  string  $queue
     * @return \Illuminate\Contracts\Queue\Job|null
     */
    public function pop($queue = null)
    {
        $queue = $this->getQueue($queue);

        $job = $this->beanspeak->watchOnly($queue)->reserve(0);

        if ($job instanceof \Beanspeak\Job) {
            return new BeanstalkdJob($this->container, $this->beanspeak, $job, $queue);
        }
    }

    /**
     * Delete a message from the Beanstalk queue.
     *
     * @param  string  $queue
     * @param  string  $id
     * @return void
     */
    public function deleteMessage($queue, $id)
    {
        $job = $this->beanspeak->useTube($this->getQueue($queue))->peekJob($id);
        
        if ($job instanceof \Beanspeak\Job) {
            $job->delete($id);
        }
    }

    /**
     * Get the queue or return the default.
     *
     * @param  string|null  $queue
     * @return string
     */
    public function getQueue($queue)
    {
        return $queue ?: $this->default;
    }

    /**
     * Get the underlying \Beanspeak\Client instance.
     *
     * @return \Beanspeak\Client
     */
    public function getBeanspeak()
    {
        return $this->beanspeak;
    }	
}

#连接器封装
<?php
namespace Vfeelit\Queue\Connectors;

use Illuminate\Queue\Connectors\ConnectorInterface;
use Illuminate\Support\Arr;
use Vfeelit\Queue\BeanstalkdQueue;

class BeanstalkdConnector implements ConnectorInterface
{
    /**
     * Establish a queue connection.
     *
     * @param  array  $config
     * @return \Illuminate\Contracts\Queue\Queue
     */
    public function connect(array $config)
    {
	
		$client = new \Beanspeak\Client([
			'host' => Arr::get($config, 'host', '127.0.0.1'),
			'port' => Arr::get($config, 'port', 11300),
			'timeout' => Arr::get($config, 'timeout', 60),
			'persistent' => Arr::get($config, 'persistent', true),
			'wretries' => Arr::get($config, 'wretries', 8)
		]);
		
		$client->connect();

        return new BeanstalkdQueue(
            $client, $config['queue'], Arr::get($config, 'ttr', 60)
        );
    }
}

# Job封装
<?php

namespace Vfeelit\Queue\Jobs;

use Illuminate\Queue\Jobs\Job;
use Illuminate\Contracts\Queue\Job as JobContract;
use Illuminate\Container\Container;

class BeanstalkdJob extends Job implements JobContract
{
    /**
     * The Beanspeak\Client instance.
     *
     * @var \Beanspeak\Client
     */
    protected $beanspeak;

    /**
     * The Beanspeak\Job job instance.
     *
     * @var \Beanspeak\Job
     */
    protected $job;

    /**
     * Create a new job instance.
     *
     * @param  \Illuminate\Container\Container  $container
     * @param  \Beanspeak\Client  $beanspeak
     * @param  \Beanspeak\Job  $job
     * @param  string  $queue
     * @return void
     */
    public function __construct(Container $container,
                                \Beanspeak\Client $beanspeak,
                                \Beanspeak\Job $job,
                                $queue)
    {
        $this->job = $job;
        $this->queue = $queue;
        $this->container = $container;
        $this->beanspeak = $beanspeak;
    }

    /**
     * Fire the job.
     *
     * @return void
     */
    public function fire()
    {
        $this->resolveAndFire(json_decode($this->getRawBody(), true));
    }

    /**
     * Get the raw body string for the job.
     *
     * @return string
     */
    public function getRawBody()
    {
        return $this->job->getBody();
    }

    /**
     * Delete the job from the queue.
     *
     * @return void
     */
    public function delete()
    {
        parent::delete();

        $this->job->delete();
    }

    /**
     * Release the job back into the queue.
     *
     * @param  int   $delay
     * @return void
     */
    public function release($delay = 0)
    {
        parent::release($delay);

        $priority = 1024;

        $this->job->release($priority, $delay);
    }

    /**
     * Bury the job in the queue.
     *
     * @return void
     */
    public function bury()
    {
        parent::release();

        $this->job->bury();
    }

    /**
     * Get the number of times the job has been attempted.
     *
     * @return int
     */
    public function attempts()
    {
        $stats = $this->job->stats();

		$reserves = 0;
		if (!empty($stats['reserves'])) {
			$reserves = (int) $stats['reserves'];
		}

        return $reserves;
    }

    /**
     * Get the job identifier.
     *
     * @return string
     */
    public function getJobId()
    {
        return $this->job->getId();
    }

    /**
     * Get the IoC container instance.
     *
     * @return \Illuminate\Container\Container
     */
    public function getContainer()
    {
        return $this->container;
    }

    /**
     * Get the underlying Beanspeak\Client instance.
     *
     * @return \Beanspeak\Client
     */
    public function getBeanspeak()
    {
        return $this->beanspeak;
    }

    /**
     * Get the underlying Beanspeak\Job job.
     *
     * @return \Pheanstalk\Job
     */
    public function getPheanstalkJob()
    {
        return $this->job;
    }
}

服务提供者中对于没有启用beanspeak扩展的情况做了处理,没有则走原有逻辑。

Zend\Authentication 应用实例(Yaf框架)

#用户表SQL
SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) NOT NULL,
  `email` varchar(128) NOT NULL,
  `password` varchar(1024) NOT NULL,
  `active` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `email_idx` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'vfeelit', 'vfeelit@qq.com', '4cbfa3a5874c68e0593c7a7c5ec7d4fc6c823235b71fc7fb96db51eceb2073d5db55e20b0a22098c8440b665c462141cc7dcfe1b25ff7d2be717aacaf8578d882b869ea8d7cba9ab2f82', '0');
INSERT INTO `user` VALUES ('2', 'ifeeline', 'ifeeline@qq.com', '4cbfa3a5874c68e0593c7a7c5ec7d4fc6c823235b71fc7fb96db51eceb2073d5db55e20b0a22098c8440b665c462141cc7dcfe1b25ff7d2be717aacaf8578d882b869ea8d7cba9ab2f82', '1');


################################
<?php
use Ifeeline\Registry;
use Ifeeline\Password;

use Zend\Authentication\AuthenticationService;
use Zend\Authentication\Storage\Session as SessionStorage;
use Zend\Authentication\Result;
use Zend\Authentication\Adapter\DbTable\CredentialTreatmentAdapter;

use Table\UserModel;

class AuthPlugin extends Yaf\Plugin_Abstract 
{
    public function routerStartup(Yaf\Request_Abstract $request, Yaf\Response_Abstract $response)
    {
    }
     
    // 命令行方式不会经过这里
    public function routerShutdown(Yaf\Request_Abstract $request, Yaf\Response_Abstract $response )
    {
        // 路由之后才能获取这三个值
        $module = strtolower($request->getModuleName());
        $controller = strtolower($request->getControllerName());
        $action = strtolower($request->getActionName());
         
        $default = new Zend\Session\Container();
        if(!$request->isPost()){
            $default->offsetSet('securityToken',md5(uniqid(rand(),true)));
        }
         
        // 可以传入Zend\Authentication\Storage\Session对象,实际关联一个SESSION容器
        $auth = new AuthenticationService();
        if($auth->hasIdentity()) {
            $storage = $auth->getStorage();
            $storageData = $storage->read();
             
            $access_time = 0;
            if(!empty($storageData->access_time)) {
                $access_time = (int)$storageData->access_time;
            }
             
            // 已经半小时没有活动了 实际SESSION可能并没有清除
            if((time() - $access_time) > 1800) {
                $auth->clearIdentity();
                $response->clearBody()->setRedirect("/login/login");
                exit;
            } else {
                $storageData->access_time = time();
                $storage->write($storageData);
            }
             
            if(($controller === "login")) {
                if($action === "logout") {
                    $auth->clearIdentity();
                    $response->clearBody()->setRedirect("/login/login");
                    exit;
                }
                if($action === "login") {
                    $response->clearBody()->setRedirect("/");
                    exit;
                }
            }
        } else if($request->isPost()) {
            // 验证token
            if(!isset($_POST['securityToken']) || ($_POST['securityToken'] !== $default->offsetGet('securityToken'))) {
                $response->clearBody()->setRedirect("/login/login");
                exit;
            }
            
            // 需要验证的数据
            $email = trim($_POST['email']);
            $password = trim($_POST['password']);
            if(empty($email) || empty($password)) {
                $default->offsetSet("freshMessage", "邮件地址或密码不能为空");
                $response->clearBody()->setRedirect("/login/login");
                exit;
            }
            
            // 匹配邮件地址 和 密码
            $user = new Table\UserModel();
            $userRow = $user->getUserByEmail($email);
            if(!empty($userRow)) {
                // 查看是否已经被禁用
                if((int)$userRow['active'] < 1) {
                    $default->offsetSet("freshMessage", "账户已经禁用.");
                    $response->clearBody()->setRedirect("/login/login");
                    exit;
                }
                
                $hashPassword = trim($userRow['password']);
                $salt = Ifeeline\Password::getPasswordSaltByHash($hashPassword);
                $nowPassword = Ifeeline\Password::getPasswordHash($salt, $password);
                
                if($nowPassword !== $hashPassword) {
                    $default->offsetSet("freshMessage", "密码不正确");
                    $response->clearBody()->setRedirect("/login/login");
                    exit;
                }
            } else {
                $default->offsetSet("freshMessage", "邮件地址不存在");
                $response->clearBody()->setRedirect("/login/login");
                exit;
            }            
            
            // 实际上,以上的密码比较已经结束  这里使用它的会话持久化功能
            $dbAdapter = Registry::get('db');
            $authAdapter = new CredentialTreatmentAdapter($dbAdapter);
            $authAdapter
                ->setTableName('user')
                ->setIdentityColumn('email')
                ->setCredentialColumn('password');
         
            // 这里应该使用自定义的密码哈希算法,然后再传递进行比较
            $authAdapter
                ->setIdentity($email)
                ->setCredential($nowPassword);
              
            $result = $auth->authenticate($authAdapter);
         
            // 这个IF应该永不会进入
            if (!$result->isValid()) {
                switch ($result->getCode()) {
                    case Result::FAILURE_IDENTITY_NOT_FOUND:
                        //break;
                    case Result::FAILURE_CREDENTIAL_INVALID:
                        //break;
                    //case Result::SUCCESS:
                    //    break;
                    default:
                        //$result->getMessages()
                        $default->offsetSet("freshMessage", "用户名或密码不正确.");
                        break;
                }
                 
                $response->clearBody()->setRedirect("/login/login");
                exit;
            } else {                
                $row = $authAdapter->getResultRowObject(null, array('password'));
                // 账户被禁用(这不会执行)
                if((int)$row->active < 1) {
                    // 清楚认证信息
                    $auth->clearIdentity();
                     
                    $default->offsetSet("freshMessage", "用户名已经被禁用.");
                     
                    $response->clearBody()->setRedirect("/login/login");
                    exit;
                } else {
                    $row->access_time = time();
                     
                    $storage = $auth->getStorage();
                    $storage->write($row);
                     
                    // 成功登录
                    $response->clearBody()->setRedirect("/");
                    exit;
                }
            }
        } else {
            if(($controller !== "login") || (($controller === "login") && ($action !== "login"))) {
                $response->clearBody()->setRedirect("/login/login");
                exit;
            }
        }
    }
     
    public function preDispatch(Yaf\Request_Abstract $request, Yaf\Response_Abstract $response)
    {
    }
     
    public function postDispatch(Yaf\Request_Abstract $request, Yaf\Response_Abstract $response)
    {
    }
}

##对应控制器 模型 和 模板
<?php
use Ifeeline\Registry;
use Ifeeline\BaseController;
 
class LoginController extends BaseController
{
    public function loginAction()
    {
        // 取回登录失败信息
        $default = new Zend\Session\Container();
        if($default->offsetExists("freshMessage")){
            $this->_view->freshMessage = $default->offsetGet("freshMessage");
            $default->offsetUnset("freshMessage");
        }
        $this->_view->securityToken = $default->offsetGet("securityToken");
         
        $this->render("login/login.phtml");
    }
     
    public function logoutAction()
    {
        return false;
    }
}
###################################
<?php
namespace Table;

use Ifeeline\Registry;
use Exception;
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Sql\Sql;
use Zend\Db\Sql\Select;

class UserModel extends TableGateway {
	protected $table = 'user';
	public function __construct(AdapterInterface $adapter = null, $features = null, ResultSetInterface $resultSetPrototype = null, Sql $sql = null){
		if($adapter instanceof Adapter){
			parent::__construct($this->table, $adapter, $features, $resultSetPrototype, $sql);
		}else{
			$adapter = Registry::get('db');
			if($adapter instanceof Adapter){
				parent::__construct($this->table, $adapter);
			}else{
				throw new Exception("Need an Zend\Db\Adapter object.");
			}
		}
	}
	
	// 根据邮件地址返回一行
	public function getUserByEmail($email=null)
	{
	    if(!empty($email) && is_string($email)) {
	        $current = $this->select(array('email'=>$email))->current();
	        if(!empty($current)) {
	            return $current->getArrayCopy();
	        }
	    }
	    return array();
	}
}
###################################
<div>
<?php 
if($this->freshMessage){
    print_r($this->freshMessage);
}
?>
</div>
<form action="/login/login" method="post">
<table style="width:500px;">
    <tr>
        <td style="width:150px; text-align:right">邮件地址</td>
        <td><input type="input" name="email" value="" /></td>
    </tr>
    <tr>
        <td style="width:150px; text-align:right">密码</td>
        <td><input type="password" name="password" value="" /></td>
    </tr>
    </tr>
        <td>
        <input type="hidden" name="securityToken" value="<?php echo $this->securityToken;?>" />
        </td>
        <td><input type="submit" value="提交" /></td>
    </tr>
</table>
</form>

// 同时贴上Bootstrap配置
<?php
use Yaf\Application;
use Yaf\Bootstrap_Abstract as BootstrapAbstract;
use Yaf\Dispatcher;
use Yaf\Route\Regex;

use Ifeeline\Registry;

use Zend\Db\Adapter\Adapter;
use Zend\Mail\Transport\Smtp as SmtpTransport;
use Zend\Mail\Transport\SmtpOptions;
use Zend\Session\Config\SessionConfig;
use Zend\Session\SessionManager;
use Zend\Session\Validator\HttpUserAgent;

class Bootstrap extends BootstrapAbstract {
	private $_config;

	public function _init(Dispatcher $dispatcher) {
	    // 引入Composer,Yaf扩展的配置项yaf.use_spl_autoload务必设置为1
	    if(file_exists(ROOT_PATH.'/vendor/autoload.php')){
	        $loader = include ROOT_PATH.'/vendor/autoload.php';
	        // 可以手工载入一批第三方库
	        // 明确指定命名空间对应的路径,有利于提升性能
	        $loader->add("",ROOT_PATH.'/library');
	        $loader->addPsr4("Zend\\",ROOT_PATH.'/library/Zend');
	        
	        Registry::set('loader', $loader);
	    }
	    
	    // 禁止自动渲染
	    $dispatcher->autoRender(FALSE);
	    
	    // 保存配置
		$this->_config = Application::app()->getConfig();
		Registry::set('config', $this->_config);

		// 报错设置
		if($this->_config->global->showError){
			error_reporting (-1);
			ini_set('display_errors','On');
		}
		
		// 命令行方式,跳过SESSION
		if(!defined("SKIP_SESSION")) {
		    // SESSION
		    $config = new SessionConfig();
		    
		    $sessionConfig = $this->_config->session->toArray();
		    if(isset($sessionConfig['save_path'])) {
		        @mkdir($sessionConfig['save_path'],0777,true);
		    }
		    
		    $config->setOptions($sessionConfig);
		    $manager = new SessionManager($config);
		    $manager->getValidatorChain()->attach('session.validate', array(new HttpUserAgent(), 'isValid'));
		    $manager->start();
		    if(!$manager->isValid()) {
		        $manager->destroy();
		        throw new \Exception("会话验证失败");
		    }
		    Registry::set('session', $manager);
		}

		// 数据库
		Registry::set('db',function(){
			$mysqlMasterConfig = $this->_config->mysql->master->toArray();
			$adapter = new Adapter($mysqlMasterConfig);
			return $adapter;
		});
		
        //
		Registry::set('job',function(){
            $jobConfig = $this->_config->mysql->job->toArray();
            
            //$jobConfig['driver'] = 'mysqli';
            // or
            unset($jobConfig['charset']);
            $jobConfig['driver'] = 'pdo_mysql';
            $jobConfig['driver_options']['1002'] = "SET NAMES UTF8;";

		    $jobAdapter = new Adapter($jobConfig);
		    return $jobAdapter;
        });
		
		// 邮件
		Registry::set('mail',function() {
		    $options   = new SmtpOptions($this->_config->smtp->toArray());
		    $mail = new SmtpTransport();
		    $mail->setOptions($options);
		    
		    return $mail;
		});
		
		// 日志
		Registry::set('logger', function() {
		    $logger = new Zend\Log\Logger;
		    $writer = new Zend\Log\Writer\Stream($this->_config->log->path.'/'.date("Ymd").".log");
		    
		    $logger->addWriter($writer);
		    return $logger;
		});
	}
	
	public function _initRoutes() {
		//Dispatcher::getInstance()->getRouter()->addRoute("xxx", new Regex(,,));
	}
	
	public function _initPlugin(Dispatcher $dispatcher) {
		$authPlugin = new AuthPlugin();
		$dispatcher->registerPlugin($authPlugin);
	}
}

Yaf实现简单布局 与 实例(分页)

在一个视图输出中,套入一个布局是很常见的。Yaf中提供了一个简单的视图实现,只要稍微封装,就可以实现一般的布局。

<?php
namespace Ifeeline;

use Yaf\Controller_Abstract as ControllerAbstract;

class BaseController extends ControllerAbstract
{
    // 实现简单布局
    public function render($tpl, array $parameters = NULL)
    {
        if(!empty($tpl) && is_string($tpl)) {
            if(!empty($parameters) && is_array($parameters)){
                $this->_view->assign($parameters);
            }
            $content = $this->_view->render($tpl);
            // 总是启用布局,除非明确禁止
            if($this->_view->layout !== false) {
                // 确定布局文件
                $layout = $this->_view->layoutTemplate;
                if(empty($layout) || !is_string($layout)) {
                    $layout = "main.phtml";
                }
                
                // 确定布局路径
                $layoutPath = '';
                $config = Registry::get('config');
                if(isset($config->global->layoutPath)) {
                    $layoutPath = $config->global->layoutPath;
                }
                if(empty($layoutPath)) {
                    if(defined('APPLICATION_PATH')) {
                        $layoutPath = APPLICATION_PATH."/layouts";
                    }
                }
                
                // 布局文件存在
                if(!empty($layoutPath) && file_exists($layoutPath."/".$layout)) {
                    $this->_view->setScriptPath(APPLICATION_PATH."/layouts");
                    $this->_view->assign("content", $content);
                    echo $this->_view->render($layout);
                    return;
                }
            }
            echo $content;
        }
    }
}

// 所有控制器类继承自Ifeeline\BaseController
<?php
use Ifeeline\BaseController;

class TestViewController extends BaseController
{
}

Yaf\Controller_Abstract本身实现了render()方法(实际是视图render()的封装),这里覆盖掉这个方法,把视图输出套入布局中。

Yaf中的View非常的简单,这个对象的数据可以通过$this->_view->xxxx=vvvv设置,也可以通过$this->_view->assign(array())设置,还可以在调用$this->_view->render(‘index.phtml’,array())传入,这些传入的数据都存储在_tpl_var中,所以多次渲染都会共用这个变量池,故而可以把子视图输出,套入布局中,在布局中进行输出,从而实现二步视图。

以下贴一个实例代码(分页实现):

#控制器
use Ifeeline\Registry;

use Ifeeline\BaseController;
use Zend\Db\Adapter\Adapter;
use Table\PlatformModel;
use Zend\Db\TableGateway\TableGateway;
use Zend\Paginator\Adapter\DbTableGateway;
use Zend\Paginator\Paginator;

class TestViewController extends BaseController
{   
    public function indexAction()
    {
        $dbAdapter = Registry::get('job');
        $tableGateway = new TableGateway("datatables_demo", $dbAdapter);
        $tableGatewayAdapter = new DbTableGateway($tableGateway,null,"id ASC");
        $paginator = new Paginator($tableGatewayAdapter);

        // 总页数
        $totalPage = count($paginator);
        
        // 获取当前页码
        $page = !empty($_GET['page'])?(int)$_GET['page']:1;
        if($page < 1) {
            $page = 1;
        } else if($page > $totalPage) {
            $page = $totalPage;
        }
        $paginator->setCurrentPageNumber($page);
        
        
        //
        $paginator->setPageRange(10);
        
        ///////////////////////////////////////
        // 视图输出
        $this->_view->assign(array(
            // 布局参数
            'title' => "页面标题设置",
            // 视图数据
            'paginator' => $paginator
        ));
        $this->render("testview/index.phtml");
    }
}

# 布局 layout.phtml
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title><?php echo isset($title)?$title:'默认标题';?></title>
</head>
<body>
<?php
// 页头
echo $this->render("header.phtml");

// 主体
if(isset($content)){
    echo $content;
}

// 页脚
echo $this->render("footer.phtml");
?>
</body>
</html>

# testview/index.phtml
<table style="width:500px;">
    <tr style="backgroup-color:#ccc">
        <td style="width:150px; text-align:left;">名称</td>
        <td style="width:150px; text-align:left;">值</td>
        <td>操作</td>
    </tr>
<?php
if(!empty($paginator)) {
    $datas = $paginator->getCurrentItems();
}
if(!empty($datas)) {
    foreach($datas as $row) {
?>
    <tr>
        <td style="text-align:left;"><?php echo $row['first_name'];?></td>
        <td style="text-align:left;"><?php echo $row['last_name'];?></td>
        <td style="text-align:left;">编辑 更新 删除</td>
    </tr>
<?php 
    }
}
?>
</table>

<br /><br />
<?php 
if(!empty($paginator)) {
    echo $this->render("paginator.phtml");
}

# paginator.phtml 分页
<?php 
// 取回请求的Uri
$request = \Yaf\Dispatcher::getInstance()->getRequest();
$baseURL = $request->getRequestUri();
// 分页控制
$pageCtrl = $paginator->getPages();
// 查询数据
$query = $_GET;
unset($query['x'],$query['y']);

if ($pageCtrl->pageCount): ?>
<div class="paginationControl">
    <?php echo $pageCtrl->firstItemNumber;?> - <?php echo $pageCtrl->lastItemNumber;?> of <?php echo $pageCtrl->totalItemCount;?>

    <!-- First page link -->
    <?php 
    if (isset($pageCtrl->previous)): 
        $query['page'] = $pageCtrl->first;
    ?>
    <a href="<?php echo $baseURL."?".http_build_query($query);?>">首页</a> |
    <?php else: ?>
    <span class="disabled">首页</span> |
    <?php endif; ?>

    <!-- Previous page link -->
    <?php if (isset($pageCtrl->previous)): 
        $query['page'] = $pageCtrl->previous;
    ?>
      <a href="<?php echo $baseURL."?".http_build_query($query);?>">
                     前一页
      </a> |
    <?php else: ?>
      <span class="disabled">前一页</span> |
    <?php endif; ?>

    <!-- Numbered page links -->
    <?php foreach ($pageCtrl->pagesInRange as $pageIndex): ?>
      <?php if ($pageIndex != $pageCtrl->current): 
            $query['page'] = $pageIndex;
      ?>
        <a href="<?php echo $baseURL."?".http_build_query($query);?>">
            <?php echo $pageIndex; ?>
        </a> |
      <?php else: ?>
        <?php echo $pageIndex; ?> |
      <?php endif; ?>
    <?php endforeach; ?>
    
    <!-- Next page link -->
    <?php if (isset($pageCtrl->next)): 
        $query['page'] = $pageCtrl->next;
    ?>
      <a href="<?php echo $baseURL."?".http_build_query($query);?>">
                     下一页
      </a> |
    <?php else: ?>
      <span class="disabled">下一页</span> |
    <?php endif; ?>
    
    <!-- Last page link -->
    <?php if (isset($pageCtrl->next)): 
        $query['page'] = $pageCtrl->last;
    ?>
      <a href="<?php echo $baseURL."?".http_build_query($query);?>">
                    末页
      </a>
    <?php else: ?>
      <span class="disabled">末页</span>
    <?php endif; ?>
</div>
<?php endif; ?>

yaf-layout

PHP ZIP解压缩工具 与 范例

针对ZIP压缩包的操作,PHP中提供了ZIP扩展。具体来说,它提供了一个叫ZipArchive的类,和一系列ZIP函数。ZipArchive类提供了大多操作ZIP压缩包的方法,比如创建压缩包(addFile),获取压缩包的文件名(索引),读取包内的内容等(写和读两个内容展开),对于读取压缩包也可以使用ZIP函数。(ZIP的一系列函数是读取ZIP内文件的工具,在PHP中存在的事件非常长,ZipArchive大概是后期提供的,因为仅仅读取ZIP是远不够的,所以它完整提供了读写功能)

## 创建压缩包addFile

        //生成压缩包下载
        $filename = "./" . date ( 'Ymd' )."_".time() . ".zip";
        // 生成文件
        $zip = new ZipArchive(); 
        if($zip->open($filename, ZIPARCHIVE::CREATE ) !== TRUE) {
            exit('无法打开文件,或者文件创建失败');
        }
        
        foreach($fileNameArr as $val) {
            $zip->addFile($val);
        }
        $zip->close();

New一个ZipArchive,调用其的open方法打开一个zip文件(文件不存在就是创建一个zip),然后非常简单的调用addFile()就可以把文件添加这个压缩包中,最后调用close方法,玛尼压缩包就生成了。So easy。

## 遍历压缩包,获取文件名
需要知道,New一个ZipArchive,那就有一个叫numFiles的属性,它记录了这个压缩包有多少个文件:

#test.php压缩包结构
22222
    4444.txt
33333
fffff.txt

$zip = new ZipArchive();
if ($zip->open($zipFile) == TRUE) {
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $filename = $zip->getNameIndex($i);
        echo $filename."\n";
    }
    $zip->close();
}

输出:
22222/4444.txt
33333/
fffff.txt
22222/

目录是一个文件,实际的文件带目录前缀。

##获取压缩包内的文件

$zip = new ZipArchive();
if ($zip->open($zipFile) == TRUE) {
    $file = $zip->getStream("22222/excel.xls");
    file_put_contents("/var/data/excel.xls", $file);

    $zip->close();
}

这里的getStream()接收一个字符串文件名,就是通过getNameIndex()获取到的文件名,可以首先遍历,判断预期文件是否存在,然后就打开这个文件(getStream()就是打开这个文件),获得一个文件流指针,然后跟操作文件没有什么不一样了。

##获取压缩包内的文件的稍特别的例子 使用copy函数

$zip = new ZipArchive;
if ($zip->open($path) === true) {
    for($i = 0; $i < $zip->numFiles; $i++) {
        $filename = $zip->getNameIndex($i);
        $fileinfo = pathinfo($filename);
        copy("zip://".$path."#".$filename, "/your/new/destination/".$fileinfo['basename']);
    }
    $zip->close();
}

如果要读取压缩包内容,还有如下方法(ZIP系列函数):

            $zip = zip_open($tempZipFile);
            if(is_resource($zip)) {
                $needEntry = '';
                // 取期望的文件
                while($zipf = zip_read($zip)) {
                    $zipfname = zip_entry_name($zipf); 
                    if(iconv("UTF-8","GBK","xxx.xls") == $zipfname) {
                        $needEntry = $zipf;
                    }
                }
                
                if(!empty($needEntry)) {
                    // 打开内文件
                    zip_entry_open($zip, $needEntry, "r");
                    // 在用zip_entry_read()读取内容,务必使用zip_entry_filesize()带上文件大小
                    file_put_contents($savaPath, zip_entry_read($needEntry,zip_entry_filesize($needEntry)));
                    
                    zip_entry_close($needEntry);
                } else {
                    // 不存在预定文件
                }
                zip_close($zip);
            }

这个方法较为繁琐,一般建议不要再使用了。在实际中遇到zip_entry_name()乱码情况(文件名编码实际为GBK),可能跟PHP版本有关(测试环境为PHP 5.6, 而PHP 5.5正确工作)。