标签归档:PHP

Mac 开发环境搭建

进入Mac的默认Shell终端,安装Homebrew工具:

#https://brew.sh/index_zh-cn.html
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Homebrew是一个Ruby工具,类似Ubuntu下的apt-get和CentOS下的YUM。后面安装的软件就依靠它来完成。

关于Homebrew,还需要知道的:
通过brew安装的软件,默认总是安装/usr/local/Cellar这个目录中,其它方式的组织是通过软链接的方式链接到对应的文件。
通过brew安装的软件,一般可以把/usr/local看做跟目录,比如工具对应的配置和可执行文件,分别位于或对应于/usr/local/etc/xxx,/usr/local/bin/xxx,/usr/local/sbin/xxx;其中的可执行文件均为符合链接。
搜索软件使用brew search xxx, 安装使用brew install xxx,卸载使用brew uninstall xxx。

另一个需要注意的是:Mac当前登录的用户,是普通用户,如果某个软件需要以root权限启动,那么就需要首先打入sudo来临时切换到以root身份运行某个命令。

一 安装PHP

#搜索,会出现几个分之,比如PHP56 PHP71
brew search php
#过滤,只要71分支,提供了非常多扩展包
brew search php71
#安装(选择需要的扩展包)
brew install homebrew/php/php71 homebrew/php/php71-apcu homebrew/php/php71-redis homebrew/php/php71-mongodb homebrew/php/php71-opcache omebrew/php/php71-swoole

大部分PHP的模块,都包含在了homebrew/php/php71中,是编译到内核的(非动态模块),上面的apcu,redis,mogondb,swoole是动态模块,模块安装位置:/usr/local/opt/,比如:/usr/local/opt/php71-apcu/apcu.so。配置文件自然是/usr/local/etc/php/7.1/php.ini,扩展的配置放在/usr/local/etc/php/7.1/conf.d/*.ini。

php -v
php -m

编译到内核的模块确实是大而全,然后还需要调整一下php.ini的配置(才能符合开发环境要求):

#设置时区
date.timezone = Asia/Shanghai
 
#CGI相关参数,实际上建议修改的是force_redirect,其它均保留默认值
cgi.force_redirect = 0   #默认为1,改为0
cgi.fix_pathinfo = 1     #默认是1,保留
fastcgi.impersonate = 1  #默认是1,保留
cgi.rfc2616_headers = 0  #默认是0,保留

#其它参数调整,根据实际情况调整
upload_max_filesize = 64M
max_execution_time = 1200
max_input_time = 600
max_input_nesting_level = 128
max_input_vars = 2048
memory_limit = 1024M
 
post_max_size = 64M

如果要启动PHPFPM,FPM主配置文件/usr/local/etc/php/7.1/php-fpm.conf,池配置在/usr/local/etc/php/7.1/php-fpm.d中,需要注意的是,池配置中,默认的运行用户是和用户组均为_www,所以需要检查文件的权限,保证对_www具有读和执行(默认是符合的),如果要写入,那么还需要保证对应的文件夹有被写入的权限。

启动PHPFPM,由于php-fpm这个命令放入到了/usr/local/sbin中,默认shell并不搜索这个路径,所以要想添加环境变量:

#设置环境变量
export PATH="/usr/local/sbin:$PATH"  
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile

#确认命令能找到
which php-fpm
which php71-fpm

#手动启动,PHPFPM可以不使用root身份启动(user和group指令无用),会使用当前用户运行
sudo php71-fpm start
sudo php71-fpm stop

对于开发环境,PHPFPM可以不用启动,直接使用PHP内置的HTTP服务器也可以。

二 安装Nginx

brew install --with-http2 nginx  

如果要绑定到80端口,那么Nginx就必须以root身份运行。默认的server配置位于(可改):/usr/local/etc/nginx/servers。可以往里面方式配置:

server {
    listen 80;
    #listen 443 ssl http2;
    server_name test.app;
    root "/Users/xx/www/test/public";
 
    index index.html index.htm index.php;
 
    charset utf-8;
 
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
 
    access_log off;
 
    sendfile off;
 
    location ~ \.php$ {
        client_max_body_size 64M;
        fastcgi_intercept_errors off;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 64 32k;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
 
    location ~ /\.ht {
        deny all;
    }
 
    #ssl_certificate     /etc/nginx/ssl/test.app.crt;
    #ssl_certificate_key /etc/nginx/ssl/test.app.key;
}

Nginx的主配置文件设置的启动user一般应该和PHPFPM相同,或者需要保证Nginx对文件具备读和执行的权限。如果是文件上传,还需要确保Nginx对临时中间文件夹具备写入权限。

启动关闭等:

sudo nginx -t
sudo nginx -s start
sudo nginx -s stop

三 安装MySQL

#安装最新版本(5.7.xx)
brew install mysql

#确定搜索路径:
which mysqld
mysqld —verbose —help | grep -A 1 ‘Default options’

/etc/my.cnf  /etc/mysql/my.cnf  /usr/local/etc/my.cnf  ~/.my.cnf

#
mysql.server start
mysql_secure_installation

# 停止
mysql.server stop

MySQL不需要以root身份启动。

关于启动问题:
在Mac下,如果要开机启动,可以参考如下配置(一般不需要):

#Nginx
cp /usr/local/opt/nginx/homebrew.mxcl.nginx.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  

# PHP-FPM
cp /usr/local/opt/php70/homebrew.mxcl.php71.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  

# MySQL
cp /usr/local/opt/mysql/homebrew.mxcl.mysql.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

## 卸载
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

更加实际的方法:进入操作系统后,启动Nginx和PHPFPM(因为要sudo,需要输入密码),MySQL则在需要时启动,比如本地一般链接远程数据库。所以可以这个别名:(往.bash_profile中写入)

alias servers.start='sudo nginx && php-fpm --fpm-config /usr/local/etc/php/7.1/php-fpm.conf -D'
alias servers.stop='sudo bash -c "killall -9 php-fpm && nginx -s stop"'                       
alias nginx.logs='tail -f /usr/local/opt/nginx/access.log'
alias nginx.errors='tail -f /usr/local/opt/nginx/error.log'

遇到问题:
1 Nginx启动提示

nginx: [emerg] getgrnam("

提示大体就是找不到用户组的意思。在Nginx配置中,user如果只指定了用户名,默认会去寻找同名的用户组,在Mac中,用户不一定对应一个同名的用户组,所以出现这种情况就是需要明确指定存在的用户组,可以通过如下方式来确定用户和用户组:

#当前登录的用户名
whoami
www

#确认用户组(可见www的uid是502,对应的组id是20,名称是staff)
id
uid=502(www) gid=20(staff) groups=20(staff),12(everyone)

把www和staff对应填入,错误提示消失。

Hack PHP: 黑你没商量

PHP7和它之前的版本比较,有了巨大的性能提升。对于一门具有20多年历史的语言,还能有如此大的性能提升,确实不容易。不过这也间接说明PHP7之前的版本有点烂吧。PHP7之后的PHP7.1和PHP7.2,都没有加入JIT,说好的JIT在PHP7中被跳票了。PHP的历史包袱是很重的,需要多方面兼顾。说PHP7接近HHVM运行PHP的性能,这其实是需要打问号的。从原理上来说,一个没有JIT的运行引擎会比一个具备JIT的运行引擎更快应该不可能,否则PHP还搞什么JIT。

PHP是弱类型的,这个对JIT来说不太友好。在运行时,需要类型推断,而且需要推断正确才能发挥JIT的作用。对于强类型语言,JIT就好做的多,于是出现了Hack(Hack PHP一把的意思),它引入了类型系统,用Hack写的PHP,HHVM的JIT可以充分发挥,从这个角度来说,HHVM是向JVM看齐的(它的多线程架构也和JAVA类似)。

所以,当前的PHP7和HHVM下的PHP相比,差的何止一点点。PHP7.0引入了类型系统(默认关闭),PHP7.1引入了类型推断优化opcode,PHP7.2还是看不到JIT的影子。这个大概就是PHP自身坚持弱类型,但是又要打造一个实用的JIT之间的矛盾。如果没有HHVM的出现,估计PHP压根没有打造自己的JIT的打算,这事本身就极具悲剧色彩,说HHVM拯救了PHP不为过。

从纯计算的角度,C/C++比Java快,Java比Node.js快,Node.js比PHP快。Java比PHP快一个数量级,不奇怪。Node.js携带的V8引擎自带JIT,可能已经到达极限,瓶颈在弱类型。目前Node.js在吞噬PHP的市场,它的生态虽然很火爆,但是工程化比PHP还是差很多。Java虽然运行很快,但是由于其臃肿的体积,在Web领域,无法撼动PHP的市场。多语言并存,相互协作已是常态。

PHP引入JIT还是非常值得期待的,从现有思路来看,官方希望在opcode上进行透明操作,或者提供一个开关也是一个不错的做法。比如对应新的项目,开启类型系统,开启JIT。

PHP 拟合函数计算趋势线

    // 拟合算法(最小二乘法)
    // 输入: $values = [2, 3, 5, 4, 7, 8, 3, 2, 6];
    // 输出: $values = [3.578, 3.794, 4.011, 4.228, 4.444, 4.661, 4.878, 5.094, 5.311];
    // 斜率(正切值): k = y / x;   =>   5.311 - 3.578 / 9 = 0.1925,最终结果是负无穷 -> 0 -> 正无穷
    // k=0 说明无斜率,与X轴平行, k=1说明斜度为45度,大于1说明斜度大于45度(分正负)
    // atan(k)获取弧度角,弧度换算为角度:atan(k) * (180 / M_PI);
    public function trendLine(array $values)
    {
        $n = count($values);
        $sumX = 0;
        $sumY = 0.0;
        $sumXX = 0;
        $sumXY = 0.0;
        for($i = 1; $i <= $n; $i++) {
            $sumX += $i;
            $sumY += $values[$i - 1];
            $sumXX += $i * $i;
            $sumXY += $i * $values[$i - 1];
        }
        // 求a,b
        $b = ($n * $sumXY - $sumX * $sumY) / ($n * $sumXX - $sumX * $sumX);
        $a = ($sumY - $b * $sumX) / $n;
        // 返回趋势线y值
        $ys = [];
        for($i = 1; $i <= $n; $i++) {
            $ys[$i - 1] = round($b * $i + $a, 3);
        }
        // 斜率(正切值)
        $slope = ($ys[$n -1] - $ys[0]) / $n;
        // 弧度
        $radians = atan($slope);
        // 角度(1度 = 180 / M_PI = 57.297弧度)
        $degree = $radians * 57.297;

        return [
            'slope' => $slope,
            'radians' => $radians,
            'degree' => $degree,
            'values' => $ys
        ];
    }

至于最小二乘法的公式和原理可以搜索一下。根据这个计算的结果,可以得到一条直线的趋势线,通常我们顺便计算一下斜率以及夹角大小(弧度角和角度)。

Zephir用类似PHP的语法写PHP C扩展

Zephir是一个非常有意思的项目。它是专门针对编写PHP C扩展而创建的,是PHP C框架Phalcon的实现语言。Zephir提供了类似PHP的语法,然后转换成PHP C扩展代码,然后就是正常的C扩展编译和安装。

官网:http://zephir-lang.com/

Zephir依赖re2c来进行语法分析(PHP本身也使用re2c来进行语法分析),需要把Zephir代码转换为PHP C扩展的C代码,所以依赖PHP的头文件,然后需要编译,那么就必须有编译器(gcc):(https://docs.zephir-lang.com/en/latest/install.html)

# 安装PHP的头文件,如果是RPM安装,需要安装devel包
php70w-devel-7.0.13-1.w7.x86_64

# 安装re2c:http://re2c.org/
yum install re2c

# 安装编译器
yum install gcc make autoconf git

#安装
git clone https://github.com/phalcon/zephir
cd zehpir
./install -c   #拷贝zephir/bin/zephir 到  /usr/local/bin/zephir

zephir help

编译实例:

git clone --depth=1 https://github.com/phalcongelist/beanspeak.git
cd beanspeak
zephir build
# Or, for PHP 7 use
zephir build --backend=ZendEngine3

参考例子:

#https://github.com/phalcongelist/beanspeak/blob/master/beanspeak/client.zep
namespace Beanspeak;

/**
 * Beanspeak\Client
 *
 * Class to access the beanstalk queue service.
 *
 * Implements the beanstalk protocol spec 1.10.
 *
 * <code>
 * use Beanspeak\Client;
 *
 * // Each connection parameter is optional
 * $queue = new Client([
 *     'host'       => '127.0.0.1', // The beanstalk server hostname or IP address to connect to
 *     'port'       => 11300,       // The port of the server to connect to
 *     'timeout'    => 60,          // Timeout in seconds when establishing the connection
 *     'persistent' => true,        // Whether to make the connection persistent or not
 *     'wretries'   => 8,           // Write retries
 * ]);
 * </code>
 *
 * @link https://github.com/kr/beanstalkd/
 */
class Client
{
	/**
	 * The current socket connection.
	 * @var resource
	 */
	protected socket;

	/**
	 * The current connection options.
	 * @var array
	 */
	protected options = [];

	/**
	 * The current used tube.
	 * @var string
	 */
	protected usedTube = "default";

	/**
	 * The current watched tubes.
	 * @var array
	 */
	protected watchedTubes = [ "default" : true ];

	/**
	 * Beanspeak\Client constructor
	 */
	public function __construct(array options = null)
	{
		array defaults = [
			"host"       : "127.0.0.1",
			"port"       : "11300",
			"persistent" : true,
			"timeout"    : "60",
			"wretries"   : 8
		];

		if typeof options != "array" {
			let this->{"options"} = defaults;
		} else {
			let this->{"options"} = options + defaults;
		}
	}

	/**
	 * Makes a connection to the Beanstalk server.
	 *
	 * The resulting stream will not have any timeout set on it.
	 * Which means it can wait an unlimited amount of time until a packet
	 * becomes available.
	 *
	 * @throws Exception
	 */
	public function connect() -> resource
	{
		var e, options, socket, usedTube, tube;

		if this->isConnected() {
			this->disconnect();
		}

		let options = this->options;

		try {
			if options["persistent"] {
				let socket = pfsockopen(options["host"], options["port"], null, null, options["timeout"]);
			} else {
				let socket = fsockopen(options["host"], options["port"], null, null, options["timeout"]);
			}

			if typeof socket != "resource" {
				throw new Exception("Can't connect to Beanstalk server.");
			}
		} catch  \Exception, e {
			throw new Exception(e->getMessage());
		}

		stream_set_timeout(socket, -1, null);

		let this->{"socket"} = socket,
			usedTube = this->usedTube;

		if usedTube != "default" {
			this->useTube(usedTube);
		}

		for tube, _ in this->watchedTubes {
			if tube != "default" {
				unset(this->watchedTubes[tube]);
				this->watch(tube);
			}
		}

		if !isset this->watchedTubes["default"] {
			this->ignore("default");
		}

		return socket;
	}

	/**
	 * Closes the connection to the Beanstalk server.
	 *
	 * Will throw an exception if closing the connection fails, to allow
	 * handling the then undefined state.
	 *
	 * @throws Exception
	 */
	public function disconnect() -> boolean
	{
		var socket, status;

		if !this->isConnected() {
			return false;
		}

		let socket = this->socket,
			status = fclose(socket);

		if !status {
			throw new Exception("Failed to close connection.");
		}

		let this->{"socket"}       = null,
			this->{"usedTube"}     = "default",
			this->{"watchedTubes"} = [ "default" : true ];

		return true;
	}

	/**
	 * Whether the connection is established or not.
	 */
	public function isConnected() -> boolean
	{
		return typeof this->socket == "resource";
	}

	/**
	 * Inserts a job into the client's currently used tube.
	 *
	 * <code>
	 * $task = [
	 *     'recipient' => 'user@mail.com',
	 *     'subject'   => 'Welcome',
	 *     'content'   => $content,
	 * ];
	 *
	 * $put = $queue->pit($task, 999, 60 * 60, 3600);
	 * </code>
	 */
	public function put(var data, int priority = 1024, int delay = 0, int ttr = 86400) -> int|boolean
	{
		var status, response, serialized, length;

		// Data is automatically serialized before be sent to the server
		let serialized = serialize(data),
			length     = strlen(serialized);

		this->write("put " . priority . " " . delay . " " . ttr . " " . length . "\r\n" . serialized);

		let response = this->readStatus();

		if isset response[1] {
			let status = response[0];

			if status == "INSERTED" || status == "BURIED" {
				return intval(response[1]);
			}
		}

		return false;
	}

	/**
	 * Inserts a job into the desired tube.
	 *
	 * <code>
	 * $task = [
	 *     'recipient' => 'user@mail.com',
	 *     'subject'   => 'Welcome',
	 *     'content'   => $content,
	 * ];
	 *
	 * $queue->putInTube('tube-name', $task, 999, 60 * 60, 3600);
	 * </code>
	 */
	public function putInTube(string! tube, var data, int priority = 1024, int delay = 0, int ttr = 86400) -> boolean|int
	{
		var  response;

		let response = this->useTube(tube);
		if typeof response == "object" {
			return this->put(data, priority, delay, ttr);
		}

		return false;
	}

	/**
	 * Change the active tube.
	 *
	 * The "use" command is for producers. Subsequent put commands will put jobs
	 * into the tube specified by this command. If no use command has been issued,
	 * jobs will be put into the tube named "default".
	 *
	 * <code>
	 * $queue->useTube('mail-queue');
	 * </code>
	 *
	 * @throws Exception
	 */
	public function useTube(string! tube) -> <Client>
	{
		var response, status, used;

		let used = this->usedTube;
		if used == tube {
			return this;
		}

		this->write("use " . tube);

		let response = this->readStatus();

		if isset response[1] && response[0] == "USING" {
			let status = response[0];

			if status == "USING" {
				let this->{"usedTube"} = tube;
				return this;
			}
		}

		throw new Exception(
			"Unable to change the active tube. Server response: ". join(" ", response)
		);
	}

	/**
	 * Lets the client inspect a job in the system.
	 *
	 * <code>
	 * $peekJob = $queue->peek($jobId);
	 * </code>
	 */
	public function peekJob(int id) -> boolean|<Job>
	{
		var response;

		this->write("peek " . id);

		let response = this->readStatus();
		if isset response[2] && response[0] == "FOUND" {
			return new Job(this, response[1], unserialize(this->read(response[2])));
		}

		return false;
	}

	/**
	 * Return the delayed job with the shortest delay left.
	 *
	 * <code>
	 * $queue->peekDelayed();
	 * </code>
	 */
	public function peekDelayed() -> boolean|<Job>
	{
		var response;

		this->write("peek-delayed");

		let response = this->readStatus();
		if isset response[2] && response[0] == "FOUND" {
			return new Job(this, response[1], unserialize(this->read(response[2])));
		}

		return false;
	}

	/**
	 * Return the next job in the list of buried jobs.
	 *
	 * <code>
	 * $queue->peekBuried();
	 * </code>
	 */
	public function peekBuried() -> boolean|<Job>
	{
		var response;

		this->write("peek-buried");

		let response = this->readStatus();
		if isset response[2] && response[0] == "FOUND" {
			return new Job(this, response[1], unserialize(this->read(response[2])));
		}

		return false;
	}

	/**
	 * Inspect the next ready job.
	 *
	 * <code>
	 * $queue->peekReady();
	 * </code>
	 */
	public function peekReady() -> boolean|<Job>
	{
		var response;

		this->write("peek-ready");

		let response = this->readStatus();
		if isset response[2] && response[0] == "FOUND" {
			return new Job(this, response[1], unserialize(this->read(response[2])));
		}

		return false;
	}

	/**
	 * Moves jobs into the ready queue.
	 * The Kick command applies only to the currently used tube.
	 *
	 * <code>
	 * $queue->kick(3);
	 * </code>
	 */
	public function kick(int bound) -> boolean|int
	{
		var response;

		this->write("kick " . bound);

		let response = this->readStatus();
		if isset response[1] && response[0] == "KICKED" {
			return intval(response[1]);
		}

		return false;
	}

	/**
	 * Adds the named tube to the watch list, to reserve jobs from.
	 *
	 * <code>
	 * $count = $queue->watch($tube);
	 * </code>
	 *
	 * @throws Exception
	 */
	public function watch(string! tube) -> <Client>
	{
		var response, watchedTubes;

		let watchedTubes = this->watchedTubes;
		if !isset watchedTubes[tube] {
			this->write("watch " . tube);

			let response = this->readStatus();
			if !isset response[1] || response[0] != "WATCHING" {
				throw new Exception("Unhandled response: " . join(" ", response));
			}

			let this->watchedTubes[tube] = true;
		}

		return this;
	}

	/**
	* Adds the named tube to the watch list, to reserve jobs from, and
	* ignores any other tubes remaining on the watchlist.
	*
	* <code>
	* $count = $queue->watchOnly($tube);
	* </code>
	*
	* @throws Exception
	*/
	public function watchOnly(string! tube) -> <Client>
	{
		var watchedTubes, watchedTube;

		this->watch(tube);

		let watchedTubes = this->watchedTubes;
		for watchedTube, _ in watchedTubes {
			if watchedTube == tube {
				continue;
			}

			this->ignore(watchedTube);
		}

		return this;
	}

	/**
	 * Reserves/locks a ready job from the specified tube.
	 *
	 * <code>
	 * $job = $queue->reserve();
	 * </code>
	 *
	 * @throws Exception
	 */
	public function reserve(int timeout = -1) -> boolean|<Job>
	{
		var response;
		string command;

		if timeout >= 0 {
			let command = "reserve-with-timeout " . timeout;
		} else {
			let command = "reserve";
		}

		this->write(command);

		let response = this->readStatus();

		if response[0] != "RESERVED" || !isset response[2] {
			return false;
		}

		return new Job(this, response[1], unserialize(this->read(response[2])));
	}

	/**
	 * Reserves/locks a ready job from the specified tube.
	 *
	 * <code>
	 * $job = $queue->reserve();
	 * </code>
	 *
	 * @throws Exception
	 */
	public function reserveFromTube(string tube, int timeout = -1) -> boolean|<Job>
	{
		this->watch(tube);

		return this->reserve(timeout);
	}

	/**
	 * Removes the named tube from the watch list for the current connection.
	 *
	 * <code>
	 * $count = $queue->ignore('tube-name);
	 * </code>
	 *
	 * @throws Exception
	 */
	public function ignore(string! tube) -> <Client>
	{
		var response, watchedTubes;

		let watchedTubes = this->watchedTubes;
		if typeof watchedTubes != "array" {
			return this;
		}

		if isset watchedTubes[tube] {
			this->write("ignore " . tube);

			let response = this->readStatus();
			if response[0] == "NOT_IGNORED" {
				throw new Exception("Cannot ignore last tube in watchlist.");
			}

			if !isset response[1] || response[0] != "WATCHING" {
				throw new Exception("Unhandled response: " . join(" ", response));
			}

			unset(watchedTubes[tube]);
			let this->watchedTubes = watchedTubes;
		}

		return this;
	}

	/**
	 * Gives statistical information about the system as a whole.
	 *
	 * <code>
	 * $queue->stats();
	 * </code>
	 */
	public function stats() -> boolean|array
	{
		var response;

		this->write("stats");

		let response = this->readYaml();
		if response[0] != "OK" {
			return false;
		}

		return response[2];
	}

	/**
	 * Gives statistical information about the specified tube if it exists.
	 *
	 * <code>
	 * $stats = $queue->statsTube('process-bitcoin');
	 * </code>
	 */
	public function statsTube(string! tube) -> boolean|array
	{
		var response;

		this->write("stats-tube " . tube);

		let response = this->readYaml();
		if response[0] != "OK" {
			return false;
		}

		return response[2];
	}

	/**
	 * Returns a list of all existing tubes.
	 *
	 * <code>
	 * $tubes = $queue->listTubes();
	 * </code>
	 */
	public function listTubes() -> boolean|array
	{
		var response;

		this->write("list-tubes");

		let response = this->readYaml();
		if response[0] != "OK" {
			return false;
		}

		return response[2];
	}

	/**
	 * Returns the tube currently being used by the client.
	 *
	 * <code>
	 * $tube = $queue->listTubeUsed(); // local cache
	 * $tube = $queue->listTubeUsed(); // ask server
	 * </code>
	 *
	 * @throws Exception
	 */
	public function listTubeUsed(boolean ask = false) -> string
	{
		var response;

		if !ask {
			return this->usedTube;
		}

		this->write("list-tube-used");

		let response = this->readStatus();

		if isset response[1] && response[0] == "USING" {
			let this->{"usedTube"} = response[1];
			return response[1];
		}

		throw new Exception("Unhandled response form beanstalkd server: " . join(" ", response));
	}

	/**
	 * Returns a list tubes currently being watched by the client.
	 *
	 * <code>
	 * $tubes = $queue->listTubesWatched(); // local cache
	 * $tubes = $queue->listTubesWatched(true); // ask server
	 * </code>
	 *
	 * @throws Exception
	 */
	public function listTubesWatched(boolean ask = false) -> array
	{
		var response;

		if !ask {
			return array_keys(this->watchedTubes);
		}

		this->write("list-tubes-watched");

		let response = this->readYaml();
		if response[0] != "OK" {
			throw new Exception("Unhandled response form beanstalkd server: " . join(" ", response));
		}

		let this->{"watchedTubes"} = array_fill_keys(response[2], true);

		return this->watchedTubes;
	}

	/**
	 * Can delay any new job being reserved for a given time.
	 *
	 * <code>
	 * $queue->pauseTube('process-video', 60 * 60);
	 * </code>
	 */
	public function pauseTube(string! tube, int delay) -> boolean
	{
		var response;

		this->write("pause-tube " . tube . " " . delay);

		let response = this->readStatus();
		if !isset response[0] || response[0] != "PAUSED" {
			return false;
		}

		return true;
	}

	/**
	 * Resume the tube.
	 *
	 * <code>
	 * $queue->resumeTube('process-video');
	 * </code>
	 */
	public function resumeTube(string! tube) -> boolean
	{
		return this->pauseTube(tube, 0);
	}

	/**
	 * Simply closes the connection.
	 *
	 * <code>
	 * $queue->quit();
	 * </code>
	 */
	public function quit() -> boolean
	{
		this->write("quit");
		this->disconnect();

		return typeof this->socket != "resource";
	}

	/**
	 * Writes data to the socket.
	 * Performs a connection if none is available.
	 * @throws Exception
	 */
	public function write(string data) -> int
	{
		var socket, retries, written, step, length;

		if !this->isConnected() {
			this->connect();

			if !this->isConnected() {
				throw new Exception("Unable to establish connection with beanstalkd server.");
			}
		}

		let retries = this->options["wretries"],
			socket  = this->socket,
			data   .= "\r\n",
			step    = 0,
			written = 0;

		let length = strlen(data);

		while written < length {
			let step++;

			if step >= retries && !written {
				throw new Exception("Failed to write data to socket after " . retries . " tries.");
			}

			let written += fwrite(socket, substr(data, written));
		}

		return written;
	}

	/**
	 * Reads a packet from the socket.
	 * Performs a connection if none is available.
	 * @throws Exception
	 */
	public function read(int length = 0) -> boolean|string
	{
		var socket, data, meta;

		if !this->isConnected() {
			this->connect();

			if !this->isConnected() {
				return false;
			}
		}

		let socket = this->socket;

		if length {
			if feof(socket) {
				throw new Exception("Failed to read data from socket (EOF).");
			}

			let data = stream_get_line(socket, length + 2),
				meta = stream_get_meta_data(socket);

			if meta["timed_out"] {
				throw new Exception("Connection timed out upon attempt to read data from socket.");
			}

			if false === data {
				throw new Exception("Failed to read data from socket.");
			}

			let data = rtrim(data, "\r\n");
		} else {
			let data = stream_get_line(socket, 16384, "\r\n");
		}

		array errors = [
			"UNKNOWN_COMMAND" : "Unnown command.",
			"JOB_TOO_BIG"     : "Job data exceeds server-enforced limit.",
			"BAD_FORMAT"      : "Bad command format.",
			"OUT_OF_MEMORY"   : "Out of memory."
		];

		if isset errors[data] {
			throw new Exception(errors[data]);
		}

		return data;
	}

	/**
	 * Fetch a YAML payload from the Beanstalkd server.
	 */
	final public function readYaml() -> array
	{
		var response, status, data = [], bytes = 0;

		let response = this->readStatus();

		if isset response[0] {
			let status = response[0];
		} else {
			let status = "UNKNOWN";
		}

		if isset response[1] {
			let bytes = response[1],
				data  = this->yamlParse();
		}

		return [
			status,
			bytes,
			data
		];
	}

	/**
	 * Reads the latest status from the Beanstalkd server.
	 */
	final public function readStatus() -> array
	{
		var status;
		let status = this->read();
		if false === status {
			return [];
		}

		return explode(" ", status);
	}

	private function yamlParse() -> array
	{
		var data, lines, key, value, values, tmp, response = [];

		let data = this->read();

		if typeof data != "string" || empty(data) {
			return [];
		}

		if function_exists("yaml_parse") {
			let response = yaml_parse(data);

			return response;
		}

		let data  = rtrim(data),
			lines = preg_split("#[\r\n]+#", rtrim(data));

		if isset lines[0] && lines[0] == "---" {
			array_shift(lines);
		}

		if typeof lines != "array" || empty(lines) {
			trigger_error("YAML parse error.", E_USER_WARNING);
			return [];
		}

		for key, value in lines {
			if starts_with(value, "-") {
				let value = ltrim(value, "- ");
			} elseif strpos(value, ":") !== false {
				let values = explode(":", value);

				if !isset values[1] {
					trigger_error("YAML parse error for line: \"" . value . "\"", E_USER_WARNING);
				} else {
					let key   = values[0],
						value = ltrim(values[1], " ");
				}
			}

			if is_numeric(value) {
				let tmp = intval(value);

				if tmp == value {
					let value = tmp;
				} else {
					let value = doubleval(value);
				}
			}

			let response[key] = value;
		}

		return response;
	}
}

PHP7.0.x编译

PHP安装根据启用的PHP模块,对系统有不同的库文件依赖,一般情况如下:

gd gd-devel			--with-gd --enable-gd-native-ttf
libjpeg libjpeg-devel		--with-jpeg-dir
libpng libpng-devel		--with-png-dir
freetype freetype-devel		--with-freetype-dir

libxml2 libxml2-devel		--enable-xml

libmcrypt libmcrypt-devel	--with-mcrypt
libmhash libmhash-devel		--with-mhash

curl curl-devel			--with-curl

zlib zlib-devel			--with-zlib
openssl openssl-devel		--with-openssl
pcre pcre-devel			PHP的正则功能依赖

安装:

#基本	
yum install libtool autoconf automake gcc gcc-c++ make

#依赖库
yum install gd gd-devel libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel libmcrypt libmcrypt-devel libmhash libmhash-devel curl curl-devel zlib zlib-devel openssl openssl-devel pcre pcre-devel

#编译参考
./configure \
--prefix=/usr/local/php7 \
--exec-prefix=/usr/local/php7 \
--bindir=/usr/local/php7/bin \
--sbindir=/usr/local/php7/sbin \
--includedir=/usr/local/php7/include \
--libdir=/usr/local/php7/lib/php \
--mandir=/usr/local/php7/php/man \
--with-config-file-path=/usr/local/php7/etc \
--with-mysql-sock=/var/lib/mysql/mysql.sock \
--with-mcrypt=/usr/include \
--with-mhash \
--with-openssl \
--with-mysqli=shared,mysqlnd \
--with-pdo-mysql=shared,mysqlnd \
--with-gd \
--with-iconv \
--with-zlib \
--enable-zip \
--enable-inline-optimization \
--disable-debug \
--disable-rpath \
--enable-shared \
--enable-xml \
--enable-bcmath \
--enable-shmop \
--enable-sysvsem \
--enable-mbregex \
--enable-mbstring \
--enable-ftp \
--enable-gd-native-ttf \
--enable-pcntl \
--enable-sockets \
--with-xmlrpc \
--enable-soap \
--without-pear \
--with-gettext \
--enable-session \
--with-curl \
--with-jpeg-dir \
--with-freetype-dir \
--enable-opcache \
--enable-fpm \
--disable-cgi \
--with-fpm-user=www \
--with-fpm-group=www \
--without-gdbm \
--disable-fileinfo

安装:

Installing shared extensions:     /usr/local/php7/lib/php/extensions/no-debug-non-zts-20151012/
Installing PHP CLI binary:        /usr/local/php7/bin/
Installing PHP CLI man page:      /usr/local/php7/php/man/man1/
Installing PHP FPM binary:        /usr/local/php7/sbin/
Installing PHP FPM config:        /usr/local/php7/etc/
Installing PHP FPM man page:      /usr/local/php7/php/man/man8/
Installing PHP FPM status page:   /usr/local/php7/php/php/fpm/
Installing phpdbg binary:         /usr/local/php7/bin/
Installing phpdbg man page:       /usr/local/php7/php/man/man1/
Installing build environment:     /usr/local/php7/lib/php/build/
Installing header files:           /usr/local/php7/include/php/
Installing helper programs:       /usr/local/php7/bin/
  program: phpize
  program: php-config
Installing man pages:             /usr/local/php7/php/man/man1/
  page: phpize.1
  page: php-config.1
/usr/local/php7/src/build/shtool install -c ext/phar/phar.phar /usr/local/php7/bin
ln -s -f phar.phar /usr/local/php7/bin/phar
Installing PDO headers:           /usr/local/php7/include/php/ext/pdo/

Window 7 下部署Nginx PHP7环境

在Window下做开发,一般会选择使用集成安装包来安装环境,比如wamp, xamp。 由于仅仅是一个开发环境,又希望和生产环境保持一致(Nginx+PHP),在Windows下可以手动配置,实际上不会比一键安装包来得的更复杂。

#下载Nginx  http://nginx.org/
#下载PHP http://windows.php.net/download

如果下载的是PHP 7.x,需要安装VC14,下载地址:http://www.microsoft.com/zh-CN/download/details.aspx?id=48145。注意:这里提供的两个包,分别针对32为和64位的机器。

把下载的Nginx和PHP压缩释放到一个目录(E:/wnmp):

E:\wnmp\nginx
E:\wnmp\php

Nginx配置:

# 1
E:\wnmp\nginx\conf\nginx.conf

#user  nobody;
worker_processes  2;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    ##配置一个默认虚拟主机
    server {
        listen       80;
        server_name  localhost;

	root E:/var/www/default;
    	index index.html index.htm index.php;

        location ~ \.php$ {
            root           E:/var/www/default;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include        fastcgi_params;
        }
    }

    ##把其它的虚拟主机放入conf/vhosts中
    include vhosts/*.conf;
}


###############################################
#vhosts/*.conf模子
server {
    listen 80;

    server_name php.dev;

    root D:\wamp\www\php.dev\public;
    index index.html index.htm index.php;

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

    location / {
	root D:\wamp\www\php.dev\public;
	try_files $uri $uri/ /index.php?$query_string;
    }

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

    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;
   }
}
###############################################


# 2 测试配置文件正确
E:\wnmp\nginx\nginx.exe -t

# 3 启动
E:\wnmp\nginx\nginx.exe

Nginx启动关闭脚本:

# 启动
nginx_start.bat

@echo off
set NGINX_HOME=E:\wnmp\nginx
start /D %NGINX_HOME%\ %NGINX_HOME%\nginx.exe
pause

# 关闭
nginx_stop.bat

@echo off
set NGINX_HOME=E:\wnmp\nginx
cd %NGINX_HOME%
nginx.exe -s quit
pause

PHP配置:

# 对php.ini文件做一些修改
# 拷贝E:\wnmp\php\php.ini-development为php.ini
# 修改或确认如下参数:

#设置时区
date.timezone = Asia/Shanghai

#允许用户在运行时加载PHP扩展
enable_dl = On           #默认为Off 

#CGI相关参数,实际上建议修改的是force_redirect,其它均保留默认值
cgi.force_redirect = 0   #默认为1,改为0
cgi.fix_pathinfo = 1     #默认是1,保留
fastcgi.impersonate = 1  #默认是1,保留
cgi.rfc2616_headers = 0  #默认是0,保留

#扩展目录
extension_dir = "ext"

#加载扩展 根据需要调整
extension=php_bz2.dll
extension=php_curl.dll
extension=php_fileinfo.dll
extension=php_gd2.dll
extension=php_gettext.dll
extension=php_intl.dll
extension=php_mbstring.dll
extension=php_mysqli.dll
extension=php_openssl.dll
extension=php_pdo_mysql.dll

#扩展opcache是否加载,开发环境可以忽略
[opcache]
zend_extension=php_opcache.dll
opcache.enable=1
opcache.enable_cli=1

#其它参数调整,根据实际情况调整
upload_max_filesize = 64M
max_execution_time = 1200
max_input_time = 600
max_input_nesting_level = 128
max_input_vars = 2048
memory_limit = 1024M

post_max_size = 64M

PHP启动关闭脚本:

#启动x个php-cgi进行(注意,这里没有PHP-FPM管理器,无法动态管理CGI进程,开发环境就无所谓了)
php_start.vbs

createobject("wscript.shell").run "E:\wnmp\php\php-cgi.exe -b 127.0.0.1:9000",0,false
createobject("wscript.shell").run "E:\wnmp\php\php-cgi.exe -b 127.0.0.1:9000",0,false
createobject("wscript.shell").run "E:\wnmp\php\php-cgi.exe -b 127.0.0.1:9000",0,false

#关闭php-cgi进行
php_stop.bat

@echo off
taskkill /fi "imagename eq php-cgi.exe"
pause

启动时,点击nginx_start.bat和php_start.vbs,关闭时点击nginx_stop.bat和php_stop.bat。如果要切换到不同版本的PHP,很简单,直接替换就可以,或者建立多个启动脚本,比如:

start_php5.5.vbs
start_php5.6.vbs
start_php7.0.vbs
start_php7.1.vbs

关闭脚本就不需要建立多个了。

然后修改一下path环境变量:
winpath

phpenv

windowphp

在Linux下,PHP的fpm模块是可用的,但是在Windows下不支持,看起来也没有支持的计划。目前在Windows下也看到有类似实现的,不过一般是监控php-cgi进程数量,死掉重启,如此而已,一般认为没有什么价值。开发环境,直接运行一个脚本,启动几个php-cgi进程即可。

在widows下,可以配置为开启运行这些脚本,避免每次都要点击:
cmd-gpedit-msc

gpedit-window

这样设置就可以开机启动了。

另外,在开发环境下,如果觉得引入Nginx太麻烦,还有更加简便的方法,PHP 5.4以后,引入了一个内置的CLI Http Server,启动方式:

D:\wnmp\php-7.0.11\php.exe -S 127.0.0.1:6060 -t D:\var\www\xx\public

可以监听同一个端口,根据不同的名称来区分不同的项目。也使用IP地址,使用不同端口来区分不同的项目。(相同端口和相同主机名,可以启动多次的)
[sehll]
#基于端口,IP一样,端口不一样
D:\wnmp\php-7.0.11\php.exe -S 127.0.0.1:6060 -t D:\var\www\xx\public
D:\wnmp\php-7.0.11\php.exe -S 127.0.0.1:6061 -t D:\var\www\yy\public

#基于主机名(先做hosts绑定),端口一样,主机名不一样
D:\wnmp\php-7.0.11\php.exe -S vfeelit.local:6060 -t D:\var\www\xx\public
D:\wnmp\php-7.0.11\php.exe -S ifeeline.local:6060 -t D:\var\www\yy\public
[/shell]

为了方便,可以对不同项目编写独立的VBS文件,需要哪个点哪个,这也是一个不错的办法:

createobject("wscript.shell").run "D:\wnmp\php-7.0.11\php.exe -S ifeeline.local:6060 -t D:\var\www\yy\public",0,false

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