标签归档:PHP

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正确工作)。

Zend Framework 2.x 之 Zend\Mail

Introduction to Zend\Mail

Getting started
Zend\Mail provides generalized普遍 functionality to compose组成 and send both text and MIME-compliant multipart email messages. Mail can be sent with Zend\Mail via the Mail\Transport\Sendmail, Mail\Transport\Smtp or the Mail\Transport\File transport. Of course, you can also implement your own transport by implementing the Mail\Transport\TransportInterface.(邮件传送出去使用Sendmail或Smtp很容易理解,但是File就有点困惑)

Simple email with Zend\Mail
A simple email consists of one or more recipients(一个或多个接收人), a subject, a body and a sender. To send such a mail using Zend\Mail\Transport\Sendmail, do the following:

use Zend\Mail;

$mail = new Mail\Message();
$mail->setBody('This is the text of the email.');
$mail->setFrom('Freeaqingme@example.org', 'Sender\'s name');
$mail->addTo('Matthew@example.com', 'Name of recipient');
$mail->setSubject('TestSubject');

$transport = new Mail\Transport\Sendmail();
$transport->send($mail);

总体上,就是构建一个消息,然后使用一个Transport发送出去。

Note
Minimum definitions
In order to send an email using Zend\Mail you have to specify at least one recipient as well as a message body. Please note that each Transport may require additional parameters to be set.

For most mail attributes there are “get” methods to read the information stored in the message object. for further details, please refer to the API documentation.

You also can use most methods of the Mail\Message object with a convenient fluent interface.(说可以链式构建消息)

use Zend\Mail;

$mail = new Mail\Message();
$mail->setBody('This is the text of the mail.')
     ->setFrom('somebody@example.com', 'Some Sender')
     ->addTo('somebody_else@example.com', 'Some Recipient')
     ->setSubject('TestSubject');

Configuring the default sendmail transport

The most simple to use transport is the Mail\Transport\Sendmail transport class. It is essentially a wrapper to the PHP mail() function. If you wish to pass additional parameters to the mail() function, simply create a new transport instance and pass your parameters to the constructor.

Passing additional parameters
This example shows how to change the Return-Path of the mail() function.

use Zend\Mail;

$mail = new Mail\Message();
$mail->setBody('This is the text of the email.');
$mail->setFrom('Freeaqingme@example.org', 'Dolf');
$mail->addTo('matthew@example.com', 'Matthew');
$mail->setSubject('TestSubject');

$transport = new Mail\Transport\Sendmail('-freturn_to_me@example.com');
$transport->send($mail);

Note
Safe mode restrictions
Supplying additional parameters to the transport will cause the mail() function to fail if PHP is running in safe mode.(启动安全模式可能使得发邮件失败)

Note
Choosing your transport wisely聪明地
Although the sendmail transport is the transport that requires only minimal configuration, it may not be suitable for your production environment. This is because emails sent using the sendmail transport will be more often delivered to SPAM-boxes. This can partly be remedied by using the SMTP Transport combined with an SMTP server that has an overall good reputation. Additionally, techniques such as SPF and DKIM may be employed to ensure even more email messages are delivered as should.(使用Sendmail发送多层垃圾,使用SMTP)

Zend\Mail\Message

Overview
The Message class encapsulates a single email message as described in RFCs 822 and 2822. It acts basically as a value object for setting mail headers and content.

If desired, multi-part email messages may also be created. This is as trivial平常的 as creating the message body using the Zend\Mime component, assigning it to the mail message body.

The Message class is simply a value object. It is not capable能胜任的 of sending or storing itself; for those purposes, you will need to use, respectively, a Transport adapter or Storage adapter.

Quick Start
Creating a Message is simple: simply instantiate例示 it.

use Zend\Mail\Message;

$message = new Message();

Once you have your Message instance, you can start adding content or headers. Let’s set who the mail is from, who it’s addressed to, a subject, and some content:

$message->addFrom("matthew@zend.com", "Matthew Weier O'Phinney")
        ->addTo("foobar@example.com")
        ->setSubject("Sending an email from Zend\Mail!");
$message->setBody("This is the message body.");

You can also add recipients to carbon-copy (“Cc:”) or blind carbon-copy (“Bcc:”). 抄送 和 密送

$message->addCc("ralph.schindler@zend.com")
        ->addBcc("enrico.z@zend.com");

If you want to specify an alternate address to which replies may be sent, that can be done, too.

$message->addReplyTo("matthew@weierophinney.net", "Matthew");

Interestingly, RFC822 allows for multiple “From:” addresses. When you do this, the first one will be used as the sender, unless you specify a “Sender:” header. The Message class allows for this.

/*
 * Mail headers created:
 * From: Ralph Schindler <ralph.schindler@zend.com>, Enrico Zimuel <enrico.z@zend.com>
 * Sender: Matthew Weier O'Phinney <matthew@zend.com></matthew>
 */
$message->addFrom("ralph.schindler@zend.com", "Ralph Schindler")
        ->addFrom("enrico.z@zend.com", "Enrico Zimuel")
        ->setSender("matthew@zend.com", "Matthew Weier O'Phinney");

可以有多个from,默认第一个为sender,可用setSender明确设置sender。

By default, the Message class assumes ASCII encoding for your email. If you wish to use another encoding, you can do so; setting this will ensure all headers and body content are properly encoded using quoted-printable encoding. 默认ASCII编码

$message->setEncoding("UTF-8");

If you wish to set other headers, you can do that as well. 添加邮件头信息

/*
 * Mail headers created:
 * X-API-Key: FOO-BAR-BAZ-BAT
 */
$message->getHeaders()->addHeaderLine('X-API-Key', 'FOO-BAR-BAZ-BAT');

Sometimes you may want to provide HTML content, or multi-part content. To do that, you’ll first create a MIME message object, and then set it as the body of your mail message object. When you do so, the Message class will automatically set a “MIME-Version” header, as well as an appropriate “Content-Type” header.

In addition you can check how to add attachment to your message E-mail Attachments.

use Zend\Mail\Message;
use Zend\Mime\Message as MimeMessage;
use Zend\Mime\Part as MimePart;

$text = new MimePart($textContent);
$text->type = "text/plain";

$html = new MimePart($htmlMarkup);
$html->type = "text/html";

$image = new MimePart(fopen($pathToImage, 'r'));
$image->type = "image/jpeg";

$body = new MimeMessage();
$body->setParts(array($text, $html, $image));

$message = new Message();
$message->setBody($body);

If you want a string representation of your email, you can get that:

echo $message->toString();

Finally, you can fully introspect内现 the message – including getting all addresses of recipients and senders, all headers, and the message body.

// Headers
// Note: this will also grab all headers for which accessors/mutators exist in
// the Message object itself.
foreach ($message->getHeaders() as $header) {
    echo $header->toString();
    // or grab values: $header->getFieldName(), $header->getFieldValue()
}

// The logic below also works for the methods cc(), bcc(), to(), and replyTo()
foreach ($message->getFrom() as $address) {
    printf("%s: %s\n", $address->getEmail(), $address->getName());
}

// Sender
$address = $message->getSender();
if(!is_null($address)) {
   printf("%s: %s\n", $address->getEmail(), $address->getName());
}

// Subject
echo "Subject: ", $message->getSubject(), "\n";

// Encoding
echo "Encoding: ", $message->getEncoding(), "\n";

// Message body:
echo $message->getBody();     // raw body, or MIME object
echo $message->getBodyText(); // body as it will be sent

Once your message is shaped to your liking, pass it to a mail transport in order to send it!

$transport->send($message);

消息准备好,传递给Transport的send方法。

Zend\Mail\Transport
Overview
Transports take care of the actual delivery of mail. Typically, you only need to worry about two possibilities: using PHP’s native mail() functionality, which uses system resources to deliver mail, or using the SMTP protocol for delivering mail via a remote server. Zend Framework also includes a “File” transport, which creates a mail file for each message sent; these can later be introspected as logs or consumed for the purposes of sending via an alternate transport mechanism later.(提供了FileTransport,用来日志等)

The Zend\Mail\Transport interface defines exactly one method, send(). This method accepts a Zend\Mail\Message instance, which it then introspects and serializes in order to send. 只有一个send()方法

Quick Start
Using a mail transport is typically as simple as instantiating it, optionally configuring it, and then passing a message to it. 实例化 配置 传递一个消息给它

Sendmail Transport Usage
SMTP Transport Usage

use Zend\Mail\Message;
use Zend\Mail\Transport\Smtp as SmtpTransport;
use Zend\Mail\Transport\SmtpOptions;

$message = new Message();
$message->addTo('matthew@zend.com')
        ->addFrom('ralph.schindler@zend.com')
        ->setSubject('Greetings and Salutations!')
        ->setBody("Sorry, I'm going to be late today!");

// Setup SMTP transport using LOGIN authentication
$transport = new SmtpTransport();
$options   = new SmtpOptions(array(
    'name'              => 'localhost.localdomain',
    'host'              => '127.0.0.1',
    'connection_class'  => 'login',
    'connection_config' => array(
        'username' => 'user',
        'password' => 'pass',
    ),
));
$transport->setOptions($options);
$transport->send($message);

File Transport Usage

use Zend\Mail\Message;
use Zend\Mail\Transport\File as FileTransport;
use Zend\Mail\Transport\FileOptions;

$message = new Message();
$message->addTo('matthew@zend.com')
        ->addFrom('ralph.schindler@zend.com')
        ->setSubject('Greetings and Salutations!')
        ->setBody("Sorry, I'm going to be late today!");

// Setup File transport
$transport = new FileTransport();
$options   = new FileOptions(array(
    'path'              => 'data/mail/',
    'callback'  => function (FileTransport $transport) {
        return 'Message_' . microtime(true) . '_' . mt_rand() . '.txt';
    },
));
$transport->setOptions($options);
$transport->send($message);

InMemory Transport Usage

use Zend\Mail\Message;
use Zend\Mail\Transport\InMemory as InMemoryTransport;

$message = new Message();
$message->addTo('matthew@zend.com')
        ->addFrom('ralph.schindler@zend.com')
        ->setSubject('Greetings and Salutations!')
        ->setBody("Sorry, I'm going to be late today!");

// Setup InMemory transport
$transport = new InMemoryTransport();
$transport->send($message);

// Verify the message:
$received = $transport->getLastMessage();

The InMemory transport is primarily of interest when in development or when testing.(InMemoryTransport就是一个垃圾桶。)

Zend\Mail\Transport\SmtpOptions
Zend\Mail\Transport\FileOptions

Zend\Mail 使用范本 使用Mime发送附件(图片 Excel Zip等)

首先需要知道,Zend\Mime\Part抽象了文件(可以是文本文件 HTML文件 图片 Excel zip等),一个Message就是有一个或多个Part组成(一个或多个文件组成),这些文件之间的分隔需要使用Zend\Mime\Mime来进行,所以一个Message还有一个Mime对象。

每个Part,就是文件,根据不同类型,实际可以用的设置会不同,但是接口统一。比如有setType()用来设置Mime类型(就是标识文件的类型),setFilenme()设置文件名(需要带后缀名,可能会根据后缀名识别类型,QQ邮箱就是如此),setEncoding()设置编码(一般针对二进制文件,对二进制流进行编码后传输,所以一般是针对图片 Zip包这些文件,对应文本文件就没有实际作用),setChartset()设置文件编码(这个主要针对文本文件,涉及其中的字符编码,对二进制文件无效),setDisposition()设置是内联还是附件形式(用于邮件)。

另外,Part的getContent()可以获取使用设置的编码编码之后的输出,比如要还原一张图片,反向解码就是二进制流。setType()常见有”text/plain”,”text/html”, “text/xml”, “text/css”, “image/jpeg”, “application/json”, “text/javascript”。对应于HTTP响应,这些值将填充头Content-Type,客户端根据这个值来识别文件类型(决定如何处理)。

<?php
use Yaf\Controller_Abstract as ControllerAbstract;
use Zend\Db\Adapter\Adapter;
use Thousand\Tongtool\Session as TongtoolSession;
use Thousand\Services;
use Zend\Mail;
use Zend\Mail\Message;
use Zend\Mail\Transport\File as FileTransport;
use Zend\Mail\Transport\FileOptions;
use Zend\Mail\Transport\Smtp as SmtpTransport;
use Zend\Mail\Transport\SmtpOptions;
use Zend\Mail\Transport\InMemory as InMemoryTransport;
use Zend\Mime\Message as MimeMessage;
use Zend\Mime\Part as MimePart;

class TestController extends ControllerAbstract
{

    public function helloAction()
    {
        $adapter = Services::get('adapter');
         
        $jobs = $adapter->query("SELECT * FROM job", Adapter::QUERY_MODE_EXECUTE);
         
        print_r($jobs->toArray());
    }
    
    public function sendAction()
    {
        $text = new MimePart('Hello World');
        $text->type = "text/plain";
        
        // 邮件主体,覆盖了纯文本设置
        $html = new MimePart('<span style="color:red">Hi, Vfeelit...<a href="http://blog.ifeeline.com">this is my blog</a');
        $html->type = "text/html";

        // 作为附件 但是被QQ邮箱识别为bin文件
        $image = new MimePart(file_get_contents("D:/mailtest.jpg"));
        $image->type = "image/jpeg";
        
        // 设置MimeMessage
        $body = new MimeMessage();
        $body->setParts(array($text, $html, $image));
        
        
        // 1 设置信息
        $message = new Message();
        $message->addTo('jinsheng.sha@1000shores.com')
                ->addFrom('vfeelit@qq.com')
                ->setSubject('I want you.')
                //->setBody("I want you.");
                ->setBody($body);
        
        /*
                        完整参考
        $message->setTo('xxx')
                ->addTo('ifeeline@qq.com')      // 可以To多个人
                ->setFrom('xxx')
                ->addFrom('vfeelit@qq.com')     // 可以设置多个From
                ->setSender()                   // No addSender,默认是第一个From
                ->setCc()
                ->addCc()                       // 抄送
                ->setBcc()  
                ->addBcc()                      // 密送
                ->setReplyTo()                  // 回复给哪些人,应该是To对应,也可以多加
                ->addReplyTo()                  // 可以回复多个人
                ->setHeaders()                  // No addHeader(s)
                ->setSubject('I want you.')     // No addSubject
                ->setBody("I want you.");       // No addBody
        
        $message->getXxx()                      // 一系列的get方法
        
        $message->setEncoding("UTF-8")          // 默认为ACSSII
                ->fromString()                  // 用原始字符串构建
                ->toString()                    // 消息转换成字符串
                ->isValid()                     // 验证消息,返回布尔值
                
        // 邮件头设置
        $message->getHeaders()->addHeaderLine('X-API-Key', 'FOO-BAR-BAZ-BAT');
        $message->setHeaders() 需要传递一个Zend\Mail\Headers对象,每个单元是一个Zend\Mail\Header
        
        // 关于setBody,如果要发送HTML或者图片,就需要构建一个Zend\Mime\Message,一个Zend\Mime\Message由多个
        // Zend\Mime\Part组成,一个Zend\Mime\Part可以是一段纯文本,一个HTML片段,一张图片
        // 然后调用Zend\Mime\Message的setParts(array()),最后把这个Zend\Mime\Message传递给邮件消息的setBody
        $text = new MimePart($textContent);
        $text->type = "text/plain";
        
        $html = new MimePart($htmlMarkup);
        $html->type = "text/html";
        
        // 如果要成功发送一个图片附件,就必须设置如下几个参数,其中编码保守改为base64,默认是8bit
        // 经测试有些服务商无法读取这个内容
        $image = new MimePart(fopen($pathToImage, 'r'));
        $image->setType("image/jpeg");
        $image->setEncoding(Zend\Mime\Mime::ENCODING_BASE64);
        $image->setDisposition(Zend\Mime\Mime::DISPOSITION_ATTACHMENT);
        $image->setFileName('t.jpg');
        
        // 注意,作为一个附件,仅设置type是不够的,还需要设置文件名,根据文件后缀名了识别类型
        // 至少QQ邮箱是如此,一个Message有一个或多个Part,有一个Zend\Mime\Mime(啥毛?)
        // 多个Part作为内容输出时,需要合并在一起,这时Zend\Mime\Mime就用来参数分割线的了
        $excel = new MimePart(file_get_contents("D:/excel.xls"));
        $excel->setType("application/x-xls"); 
        $excel->setEncoding(Zend\Mime\Mime::ENCODING_BASE64);
        $excel->setDisposition(Zend\Mime\Mime::DISPOSITION_INLINE);
        $excel->setFileName('e.xls');
        $excel->setDescription("这是一个表格");
        $excel->setCharset("UTF-8");

        $body = new MimeMessage();
        $body->setParts(array($text, $html, $image));
        
        $message = new Message();
        $message->setBody($body); 
        */
       
        /* 文件transport
        $transport = new FileTransport();
        $options   = new FileOptions(array(
            // 只有两个参数
            'path'              => ROOT_PATH.'/mail/',
            'callback'  => function (FileTransport $transport) {
                return 'Message_' . microtime(true) . '_' . mt_rand() . '.txt';
            },
        ));
        $transport->setOptions($options);
        */
        
        /* 内存transport 无配置参数
        $transport = new InMemoryTransport();
        */
        
        // 2 启动  SmtpTransport
        $transport = new SmtpTransport();
        // 3 配置
        $options   = new SmtpOptions(array(
            'name'              => 'localhost',
            'host'              => 'smtp.qq.com',
            'port'              => 25,
            //默认是smtp, 测试plain, login都可以发信,但是smtp不行
            'connection_class'  => 'login', 
            'connection_config' => array(
                'username' => 'vfeelit@qq.com',
                'password' => 'xxxx',
                // “ssl” => “tls” and port 587 for TLS or “ssl” => “ssl” and port 465 for SSL.
                'ssl'    => 'tls'
            ),
        ));
        
        /* 
        // 普通配置
        $options   = new SmtpOptions(array(
            'name'              => 'localhost',
            'host'              => 'smtp.qq.com',
            'port'              => 25,
            'connection_class'  => 'plain',
            'connection_config' => array(
                'username' => 'vfeelit@qq.com',
                'password' => 'xxxxx'
            ),
        ));
         
        // 以下是一份ssl配置
        $options   = new SmtpOptions(array(
            'name'              => 'localhost',
            'host'              => 'smtp.qq.com',
            'port'              => 587,
            'connection_class'  => 'plain',
            'connection_config' => array(
                'username' => 'vfeelit@qq.com',
                'password' => 'xxxxx',
                'ssl'      => 'tls'
            ),
        ));
        */
        
        // 4 设置配置
        $transport->setOptions($options);

        // 5 发信
        $transport->send($message);
        
    }
}

Yaf框架快速搭建

文件下载:Yaf样板

编译模块,添加:

[yaf]
;yaf.environ=product
;yaf.library=NULL
;yaf.cache_config=0
;yaf.name_suffix=1
;yaf.name_separator=""
;yaf.forward_limit=5
yaf.use_namespace=1
yaf.use_spl_autoload=1

建立public目录,建立两个文件:

//.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* index.php

//index.php
<?php
define('ROOT_PATH', dirname(__DIR__));
define('APPLICATION_PATH', ROOT_PATH . "/application");

$application = new Yaf\Application(require ROOT_PATH . "/conf/global.php");
$application->bootstrap()->run();

建立conf目录,键一个文件:

// global.php
<?php
return array(
    'application' => array(
        'directory' => realpath(__DIR__.'/../application'), // 应用目录,可改名
        //'ext' => "php",
        //'bootstrap' => "Bootstrapplication.php",
        //'library' => application.directory + "/library",
        //'baseUri' => NULL,
        'dispatcher' => array(
            //'defaultModule' => "index",
            'throwException' => FALSE,              // 默认TRUE
            //'catchException' => False,
            //'defaultController' => "index",
            //'defaultAction' => "index",
        ),
        //'view' => array(
        //    'ext' => "phtml",
        //),
        //'modules' => 'Index',
        //'system' => array(),
    ),
    'mysql' => array(
        'master' => array(
            'driver' => "pdo_mysql",
            'driver_options' => array(
                "1002" => "SET NAMES UTF8;"
            ),
            'hostname' => "localhost",
            'database' => 'test',
            'username' => 'root',
            'password' => '',
            'port' => 3306
        ),
        'slave' => array(
            'driver' => "pdo_mysql",
            'driver_options' => array(
                "1002" => "SET NAMES UTF8;"
            ),
            'hostname' => "localhost",
            'database' => 'test',
            'username' => 'root',
            'password' => '',
            'port' => 3306
        ),
        'mysqli' => array(
            'driver' => "mysqli",
            'charset' => 'utf8',
            'host' => "localhost",
            'dbname' => 'test',
            'username' => 'root',
            'password' => '',
            'port' => 3306
        )
    )
);

建立application文件夹,在其下建立controllers layouts library models plugins views文件夹和Bootstrap.php文件:

<?php
use Yaf\Application;
use Yaf\Bootstrap_Abstract as BootstrapAbstract;
use Yaf\Dispatcher;
use Yaf\Route\Regex;
use Zend\Db\Adapter\Adapter;
use Ifeeline\Registry;

class Bootstrap extends BootstrapAbstract 
{
	private $_config;

	public function _init(Dispatcher $dispatcher) 
	{
	    // 配置
		$this->_config = Application::app()->getConfig();
		Registry::set('config', $this->_config);

		// 是否显示错误
		if($this->_config->application->showError){
			error_reporting (-1);
			ini_set('display_errors','On');
		}
		
        // 引入Composer,Yaf扩展的配置项yaf.use_spl_autoload务必设置为1
        if(file_exists(ROOT_PATH.'/vendor/autoload.php')){
	       $loader = include ROOT_PATH.'/vendor/autoload.php';
	       
	       // 可以手工载入一批第三方库
	       $loader->setPsr4("Zend\\",ROOT_PATH.'/library/Zend');
	       
	       Registry::set('loader', $loader);
		}
		
		// 禁止自动渲染视图
		$dispatcher->autoRender(FALSE);
		// 一样?
		//$dispatcher->disableView();
		
		// 数据适配器
		Registry::set('db',function(){
			$mysqlMasterConfig = $this->_config->mysql->master->toArray();
			$adapter = new Adapter($mysqlMasterConfig);
			return $adapter;
		});
	}
	
	public function _initRoutes(Dispatcher $dispatcher)
	{
		//Dispatcher::getInstance()->getRouter()->addRoute("xxx", new Regex(,,));
	}
	
	// 注册插件
	public function _initPlugin(Dispatcher $dispatcher) 
	{
		$authPlugin = new AuthPlugin();
		$dispatcher->registerPlugin($authPlugin);
	}
	
	// 初始化视图布局
	public function _initLayout() 
	{
	}
}

在application/library下建立:

##Ifeeline/Registry.php
<?php
namespace Ifeeline;

class Registry
{
	public static $instance = array();

	public static function set($name,$callBack)
	{
		if(!isset(self::$instance[$name])) {
			self::$instance[$name] = $callBack;
		}
	}

	public static function get($name)
	{
		if(isset(self::$instance[$name])) {
			if(is_callable(self::$instance[$name])) {
				$func = self::$instance[$name];
				self::$instance[$name] = $func();
			}
			return self::$instance[$name];
		}
		return false;
	}
	
	public static function has($name)
	{
		if(isset(self::$instance[$name]))
		{
			return true;
		}
		return false;
	}
	
	public static function del($name)
	{
		if(self::has($name)) {
			unset(self::$instance[$name]);
		}
	}
}

在application/plugins下建立一个插件类样板:

##Auth.php 文件名不带Plugin,但是类名要带
<?php

class AuthPlugin extends Yaf\Plugin_Abstract 
{

    public function preDispatch(Yaf\Request_Abstract $request, Yaf\Response_Abstract $response)
    {
//      $module = strtolower($request->getModuleName());
//     	$controller = strtolower($request->getControllerName());
//     	$action = strtolower($request->getActionName());
    }
    
    public function routerShutdown(Yaf\Request_Abstract $request, Yaf\Response_Abstract $response ){
//         print_r($request);
    }
}

在application的models目录下建立Table文件夹,在其下建立一个模型样板:

##Platform.php  类名需要包含Model后缀
<?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;

class PlatformModel extends TableGateway {
	protected $table = 'platform';
	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 getAll(){
		return $this->select("id > 0")->toArray();
	}
}

在application的controllers中建立个控制器:

##Test.php  文件名不要带Controller,但是类名需要
<?php
use Yaf\Controller_Abstract as ControllerAbstract;
use Ifeeline\Registry;
use Zend\Db\Adapter\Adapter;

class TestController extends ControllerAbstract
{
    public function init()
    {
        $db = Registry::get('db');
        $platforms = $db->query("SELECT * FROM platform",Adapter::QUERY_MODE_EXECUTE);
        
        // 
        $this->_view->assign(array("platforms"=>$platforms->toArray()));
    }
    
    public function indexAction()
    {
        echo $this->_view->render("test/index.phtml");
    }

    public function helloAction() 
    {
        $db = Registry::get('db'); 
        //$db = new Zend\Db\Adapter\Adapter($driver);
        $platforms = $db->query("SELECT * FROM platform",Adapter::QUERY_MODE_EXECUTE);
        
        echo $this->_view->render("test/hello.phtml",array("platforms"=>$platforms->toArray()));
    }
    
    public function methodAction()
    {
        $request = $this->getRequest();
        
        // 请求类型
        if($request->isGet()) { 
        }      
        if($request->isPost()) {
        }
        if($request->isHead()) {
        }
        if($request->isPut()) {
        }
        if($request->isCli()) {
        }
        if($request->isXmlHttpRequest()) {
        }
        
        // 获取当前请求的类型, 可能的返回值为GET,POST,HEAD,PUT,CLI等
        // 用来批判的是否是命令行
        if ($request->getMethod() == "CLI") {
            echo "running in cli mode";
        }
        
        // 获取路由参数 不是获取$_GET
        // http://yaf.vfeelit.com/index/test/method/aaa/1111
        $v = $request->getParam("aaa");
        if($v) {
            echo 'aaa->'.$v;
        }
        $request->setParam("fire",'nono');
        $vs = $request->getParams();
        print_r($vs);
    }

    //使用模型
    public function modeAction()
    {
        $platform = new PlatformModel();
        print_r($platform->getAll());
    }
}

整个MVC齐全了。

使用Zend\Db批量插入数据范例

<?php
$rootPath = dirname(__DIR__);
$library = $rootPath."/library";

$includePath = array(
    $library,
    get_include_path()
);
set_include_path(implode(PATH_SEPARATOR, $includePath));

chdir($rootPath);
if(file_exists('vendor/autoload.php')){
    $loader = include '/vendor/autoload.php';
}else{
    exit("Autoload Failed. ");
}
//$loader->setUseIncludePath(true);
$loader->setPsr4("Zend\\",$library.'/zf2_psr4');

$adapter = new Zend\Db\Adapter\Adapter(array(
    'hostname' => 'localhost',
    'database' => 'test',
    'username' => 'root',
    'password' => '',
    
    'driver' => 'mysqli',
    'charset' =>'utf8'
    
    //'driver' => 'pdo_mysql',
    //'driver_options' => array(
    //  PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
    //),
));

header("Content-type:text/html;charset=utf-8");

$connection = $adapter->getDriver()->getConnection();
$resource = $connection->getResource();

$valuss = [];
for($i = 0; $i < 100000; $i++) {
    $row   = [];
    $row[] = "'".$resource->real_escape_string('\'s vfeelit\\')."'";
    $row[] = "'".$resource->real_escape_string("sku".$i)."'";
    $row[] = "'".$resource->real_escape_string(round(($i+1)/3,2))."'";
    $row[] = "'".$resource->real_escape_string("仓库? 'd".$i)."'";
    $row[] = "'".$resource->real_escape_string("供应:商".$i)."'";
    $row[] = "'".$resource->real_escape_string("链接\\".$i.'\\')."'";
    $row[] = "'".$resource->real_escape_string("采购员".$i.'dd')."'";
    $valuess[] = "(".implode(",",$row).")";
}

$s = microtime(true);
$rs = $adapter->query("INSERT INTO `product`(`name`,`sku`,`purchase_price`,`warehouse_name`,`supplier_name`,`purchase_link`,`who_purchase`) values".implode(",",$valuess),Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
$e = microtime(true);
echo "100000 Record into DB:".(float)($e-$s)."\n";

批量插入数据数据是非常快的。不过可能遇到MySQL server has gone away异常,这种异常要么就是数据库链接异常断了,要么就是发送的包太大,这里就是数据包太大导致无法解析。

show global variables like 'max_allowed_packet';
set global max_allowed_packet=1024*1024*16; #改大

补注:
在新版本的Zend Framework中(2.4.8),Zend\Db\Adapter\Platform\Mysql的quoteValue改为如下:

    public function quoteValue($value)
    {
        if ($this->resource instanceof DriverInterface) {
            $this->resource = $this->resource->getConnection()->getResource();
        }
        if ($this->resource instanceof \mysqli) {
            return '\'' . $this->resource->real_escape_string($value) . '\'';
        }
        if ($this->resource instanceof \PDO) {
            return $this->resource->quote($value);
        }
        return parent::quoteValue($value);
    }

填补了之前的Bug。现在是直接调用Resource的具体方法。