分类目录归档:PHP

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

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

HTML编辑器 – UEditor

UEditor 是一个功能比较齐全的HTML编辑器,百度出品,官方网站为:http://ueditor.baidu.com/website/。这个编辑器对于文件上传方面提供了强大支持,比如可以直接粘贴(CTRL + V)图片,上传涂鸦文件(客户端画图),多文件上传等。

其中有一个叫UMeditor的产品,简称UM,它是UEditor(简称UE)的功能缩减版本。也可以使用其提供的Ubuilder来构建一个自定义的版本(主要指功能模块)。

一般直接使用UEditor这个全功能版本即可:
ueditor

目录结构:
ueditor-struct
目录dialogs对应各种弹框,目录lang对应语言包(前端展示),目录themes对应皮肤,third-party是第三方插件。文件ueditor.all.js是编辑器实现代码,可见未压缩有1M以上,min压缩后也有接近400K,问津ueditor.config.js是前端的主要配置。

前台主要配置都在ueditor.config.js中,这个文件几乎不用改变,一般需要修改的地方就是serverUrl参数,当有图片或文件需要上传时,就提交到这个指定的地址。其它配置项在这个文件中都有详细的注释(比如皮肤,语言等):

(function () {
    var URL = window.UEDITOR_HOME_URL || getUEBasePath();
    var origin = getOrigin();

    /**
     * 配置项主体。注意,此处所有涉及到路径的配置别遗漏URL变量。
     */
    window.UEDITOR_CONFIG = {

        //为编辑器实例添加一个路径,这个不能被注释
        UEDITOR_HOME_URL: URL

        // 服务器统一请求接口路径
        , serverUrl: origin + "/ueditor/server"
    };
    // ...
    // ...
    function getOrigin() {
        if (typeof location.origin === 'undefined') {
            location.origin = location.protocol + '//' + location.host;
        }
        return location.origin;
    }

    window.UE = {
        getUEBasePath: getUEBasePath,
        getOrigin: getOrigin
    };
})();

其中getOrigin()原始文件是没有的,这个用来解决原来的函数获取基本路径不准确的问题。

以上配置指定了/ueditor/serve,那么文件上传时都会POST到这个地址。所以后端需要处理文件上传的逻辑,原本下载包已经提供了相关的实现,不过为了真正可用,需要做一些改造,比如需要验证:

// 抽象类,Upload, 处理文件上传逻辑,有上个子类:UploadCatch.php 、 UploadFile.php 、 UploadScrawl.php
// 分别实现fire()方法,这个方法实际是读取配置,处理上传逻辑
<?php
namespace UEditor;

abstract class Upload
{
    //文件域名
    protected $fileField;
    //文件上传对象
    protected $file;
    //文件上传对象
    protected $base64;
    //配置信息
    protected $config;
    //原始文件名
    protected $oriName;
    //新文件名
    protected $fileName;
    //完整文件名,即从当前配置目录开始的URL
    protected $fullName;
    //完整文件名,即从当前配置目录开始的URL
    protected $filePath;
    //文件大小
    protected $fileSize;
    //文件类型
    protected $fileType;
    //上传状态信息,
    protected $stateInfo;
    //上传状态映射表,国际化用户需考虑此处数据的国际化
    protected $stateMap = array(
        //上传成功标记,在UEditor中内不可改变,否则flash判断会出错
        "SUCCESS",
        "文件大小超出 upload_max_filesize 限制",
        "文件大小超出 MAX_FILE_SIZE 限制",
        "文件未被完整上传",
        "没有文件被上传",
        "上传文件为空",
        "ERROR_TMP_FILE" => "临时文件错误",
        "ERROR_TMP_FILE_NOT_FOUND" => "找不到临时文件",
        "ERROR_SIZE_EXCEED" => "文件大小超出网站限制",
        "ERROR_TYPE_NOT_ALLOWED" => "文件类型不允许",
        "ERROR_CREATE_DIR" => "目录创建失败",
        "ERROR_DIR_NOT_WRITEABLE" => "目录没有写权限",
        "ERROR_FILE_MOVE" => "文件保存时出错",
        "ERROR_FILE_NOT_FOUND" => "找不到上传文件",
        "ERROR_WRITE_CONTENT" => "写入文件内容错误",
        "ERROR_UNKNOWN" => "未知错误",
        "ERROR_DEAD_LINK" => "链接不可用",
        "ERROR_HTTP_LINK" => "链接不是http链接",
        "ERROR_HTTP_CONTENTTYPE" => "链接contentType不正确",
        "INVALID_URL" => "非法 URL",
        "INVALID_IP" => "非法 IP"
    );

    public function __construct(array $config, $request)
    {
        $this->config = $config;
        $this->request = $request;
        $this->fileField = $this->config['fieldName'];
        if (isset($config['allowFiles'])) {
            $this->allowFiles = $config['allowFiles'];
        } else {
            $this->allowFiles = [];
        }
    }

    abstract function fire();

    public function handle()
    {
        $this->fire();
        return $this->getFileInfo();
    }

    /**
     * 上传错误检查
     * @param $errCode
     * @return string
     */
    protected function getStateInfo($errCode)
    {
        return !$this->stateMap[$errCode] ? $this->stateMap["ERROR_UNKNOWN"] : $this->stateMap[$errCode];
    }

    /**
     * 文件大小检测
     * @return bool
     */
    protected function checkSize()
    {
        return $this->fileSize <= ($this->config["maxSize"]);
    }

    /**
     * 获取文件扩展名
     * @return string
     */
    protected function getFileExt()
    {
        return '.' . $this->file->guessExtension();
    }

    /**
     * 重命名文件
     * @return string
     */
    protected function getFullName()
    {
        //替换日期事件
        $t = time();
        $d = explode('-', date("Y-y-m-d-H-i-s"));
        $format = $this->config["pathFormat"];
        $format = str_replace("{yyyy}", $d[0], $format);
        $format = str_replace("{yy}", $d[1], $format);
        $format = str_replace("{mm}", $d[2], $format);
        $format = str_replace("{dd}", $d[3], $format);
        $format = str_replace("{hh}", $d[4], $format);
        $format = str_replace("{ii}", $d[5], $format);
        $format = str_replace("{ss}", $d[6], $format);
        $format = str_replace("{time}", $t, $format);

        //过滤文件名的非法自负,并替换文件名
        $oriName = substr($this->oriName, 0, strrpos($this->oriName, '.'));
        $oriName = preg_replace("/[\|\?\"\<\>\/\*\\\\]+/", '', $oriName);
        $format = str_replace("{filename}", $oriName, $format);

        //替换随机字符串
        $randNum = rand(1, 10000000000) . rand(1, 10000000000);
        if (preg_match("/\{rand\:([\d]*)\}/i", $format, $matches)) {
            $format = preg_replace("/\{rand\:[\d]*\}/i", substr($randNum, 0, $matches[1]), $format);
        }

        $ext = $this->getFileExt();
        return $format . $ext;
    }

    /**
     * 获取文件完整路径
     * @return string
     */
    protected function getFilePath()
    {
        $fullName = $this->fullName;
        $rootPath = public_path();
        $fullName = ltrim($fullName, '/');

        return $rootPath . '/' . $fullName;
    }

    /**
     * 文件类型检测
     * @return bool
     */
    protected function checkType()
    {
        return in_array($this->getFileExt(), $this->config["allowFiles"]);
    }

    /**
     * 获取当前上传成功文件的各项信息
     * @return array
     */
    public function getFileInfo()
    {
        return array(
            "state" => $this->stateInfo,
            "url" => $this->fullName,
            "title" => $this->fileName,
            "original" => $this->oriName,
            "type" => $this->fileType,
            "size" => $this->fileSize
        );
    }
}


// 文件上传
class UploadFile extends Upload
{
    public function fire()
    {
        $file = $this->request->file($this->fileField);
        if (empty($file)) {
            $this->stateInfo = $this->getStateInfo("ERROR_FILE_NOT_FOUND");
            return false;
        }
        if (!$file->isValid()) {
            $this->stateInfo = $this->getStateInfo($file->getError());
            return false;
        }

        $this->file = $file;
        $this->oriName = $this->file->getClientOriginalName();
        $this->fileSize = $this->file->getSize();
        $this->fileType = $this->getFileExt();
        $this->fullName = $this->getFullName();
        $this->filePath = $this->getFilePath();
        $this->fileName = basename($this->filePath);

        //检查文件大小是否超出限制
        if (!$this->checkSize()) {
            $this->stateInfo = $this->getStateInfo("ERROR_SIZE_EXCEED");
            return false;
        }
        //检查是否不允许的文件格式
        if (!$this->checkType()) {
            $this->stateInfo = $this->getStateInfo("ERROR_TYPE_NOT_ALLOWED");
            return false;
        }

        if (config('ueditor.drivers.default') == 'local') {
            try {
                $this->file->move(dirname($this->filePath), $this->fileName);
                $this->stateInfo = $this->stateMap[0];
            } catch (\Symfony\Component\HttpFoundation\File\Exception\FileException $exception) {
                $this->stateInfo = $this->getStateInfo("ERROR_WRITE_CONTENT");
                return false;
            }
        } else {
            $this->stateInfo = $this->getStateInfo("ERROR_UNKNOWN");
            return false;
        }
        return true;
    }
}

要真正可用,还需要结合具体使用的框架做配置,比如在Laravel框架中,经过一些改造,就可以继承到项目中,如下是实际使用部署:
ueditor.zip

至于使用,则非常简单,需要编辑器的地方引入:

<script type="text/javascript" charset="utf-8" src="/ueditor/ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" src="/ueditor/ueditor.all.min.js"> </script>
<script type="text/javascript" charset="utf-8" src="/ueditor/lang/zh-cn/zh-cn.js"></script>

然后插入类似如下模板:

<div>
    <script id="editor" type="text/plain" style="width:1024px;height:500px;"></script>
</div>

<script type="text/javascript">

    //实例化编辑器
    //建议使用工厂方法getEditor创建和引用编辑器实例,如果在某个闭包下引用该编辑器,直接调用UE.getEditor('editor')就能拿到相关的实例
    var ue = UE.getEditor('editor');
</script>

具体的配置和使用,可以参考:http://fex.baidu.com/ueditor/

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 DOM文档操作

PHP中的DOM扩展(默认启用,可用–disable-dom关闭)依赖libxml(默认启用),这个扩展提供了操作DOM文档的全部方法,不过这个这些操作方法相对还是原始了一点,这个类似于在JavaScript中操作使用原生的函数操作DOM,相比之下,我们可能更加喜欢使用JQuery来代替这个原始的操作,同样,在PHP中我们也喜欢有类似Jquery这样的工具,可以非常方便的操作DOM文档。

目前在PHP的第三方包中,phpQuery几乎实现了JQuery大部分通用的方法,不过这个包已经很久未更新了。另外一个我觉得不错的是Symfony的组件包DomCrawler,虽然它没有提供JQuery那么多的方法,也没有一一对应它的方法,但是使用它来替代phpQuery,完全是可以的。

官方文档参考:https://symfony.com/doc/current/components/dom_crawler.html,从这个包的composer.json中可以知道,它依赖CssSelector,这个也是Symfony的组件包之一,DomCrawler用它来把CSS选择器转换成xpath。

DomCrawler实际是PHP DOM扩展的包装器而已。在DOM中, DOMNode是基本类,其它的DOMElement和DOMDocument都是继承DOMNode的,可以理解为它们是特殊一点的DOMNode。以下是对DomCrawler的一些使用例子(来自官方文档)。

        $html = <<<'HTML'
<!DOCTYPE html>
<html>
    <body>
        <p class="message">Hello World!</p>
        <p>Hello Crawler!</p>
    </body>
</html>
HTML;
        $crawler = new Crawler($html);
        
        foreach ($crawler as $domElement) {
            var_dump($domElement->nodeName);
        }
        //$crawler = $crawler->filterXPath('descendant-or-self::body/p');
        
        $crawler = $crawler->filter('body > p');
        $crawler->each(function($c, $i){
            echo $c->html();
        });
        
        // 原生DOMElement
        foreach($crawler as $cr) {
            $cro = new Crawler($cr);
            
            echo $cro->html();
        }

这里的两种遍历的方法,第一个是$crawler对象的each方法,看一下这个方法源代码:

    public function each(\Closure $closure)
    {
        $data = array();
        foreach ($this as $i => $node) {
            $data[] = $closure($this->createSubCrawler($node), $i);
        }

        return $data;
    }

把自身对象保存的$node进行遍历,放入createSubCrawler()方法,这个方法接收的参数类型是\DOMElement|\DOMElement[]|\DOMNodeList|null,这个说明$crawler对象里面保存的$node就是原生的DOMNode,在each的时候又使用DomCrawler来包装这个DOMNode让其是一个DomCrawler对象,这样就可以直接操作DomCrawler的方法,我们几乎可以把$crawler对象看做是JQuery中的美元符($)。

不过这里的createSubCrawler()方法是私有的,不能在外部使用,那么如果我有一个DOMElement(DOMNode),应该如何让它变成一个Crawler对象呢,很简单,就是以上的第二个例子,直接new一个Crawler,把DOMNode传入构造函数,这个构造函数实际内部会调用add()方法,这个方法负责实例化Crawler,查看add()方法可以知道,它除了接受DOMNode和DOMNodeList外(包括DOMElement等)还接受一个数组和字符串,数组自然应该是add()方法可以接受的参数类型,如果还是数组就会递归。

明白这个之后,操作就很简单了。直接来一些例子就好了:

$crawler->filter('body > p')->eq(0);
$crawler->filter('body > p')->first();
$crawler->filter('body > p')->last();
$crawler->filter('body > p')->siblings();
$crawler->filter('body > p')->nextAll();
$crawler->filter('body > p')->previousAll();
$crawler->filter('body')->children();
$crawler->filter('body > p')->parents();

$crawler = new Crawler('<html><body /></html>');

$crawler->addHtmlContent('<html><body /></html>');
$crawler->addXmlContent('<root><node /></root>');

$crawler->addContent('<html><body /></html>');
$crawler->addContent('<root><node /></root>', 'text/xml');

$crawler->add('<html><body /></html>');
$crawler->add('<root><node /></root>');

/////////////////////////////////////////
// 节点值
$tag = $crawler->filterXPath('//body/*')->nodeName();
$message = $crawler->filterXPath('//body/p')->text();
$class = $crawler->filterXPath('//body/p')->attr('class');
$attributes = $crawler
    ->filterXpath('//body/p')
    ->extract(array('_text', 'class'))

/////////////////////////////////////////
$document = new \DOMDocument();
$document->loadXml('<root><node /><node /></root>');
$nodeList = $document->getElementsByTagName('node');
$node = $document->getElementsByTagName('node')->item(0);

$crawler->addDocument($document);
$crawler->addNodeList($nodeList);
$crawler->addNodes(array($node));
$crawler->addNode($node);
$crawler->add($document);

/////////////////////////////////////
$html = '';

foreach ($crawler as $domElement) {
    $html .= $domElement->ownerDocument->saveHTML($domElement);
}

$html = $crawler->html();

这个包还提供提供了针对链接和表单的特定操作,用来快速操作它们。

在filter中可以使用的选择器语法,几乎和JQuery是一样的,这个包中实际是把CSS选择器转换成xpath来执行,它是由CssSelector来驱动的。

向Composer发布代码

Composer可以很方便管理应用的包与包依赖,自定义的包也可以发布到到packagist.org,然后通过Composer拉回来,已达到快速部署。当然,如果自定义的包非开源的,就不要这么干了。

首先,在github.com上创建账户,并把项目推送到github.com,这样会得到一个链接。
然后到packagist.org,点击使用Github账户登录,接着点击submit,这时候会要求输入github.com项目地址,接下来就是检查,这个检查会通知你当前可能重复的其它包,然后输入你包的名称(会从composer.json中自动检出),然后确认,这样包就发布出去了。

由于代码托管在github上,当向github推送代码时,packagist.org默认并不会同步更新,所以为了让packagist.org可以同步,需要在github中设置,参考:https://packagist.org/about#how-to-update-packages

To do so you can:

Go to your GitHub repository
Click the “Settings” button
Click “Integrations & services”
Add a “Packagist” service, and configure it with your API token, plus your Packagist username
Check the “Active” box and submit the form
You can then hit the “Test Service” button to trigger it and check if Packagist removes the warning about the package not being auto-updated.

简单来说这是一个事件通知服务,在github中设置一个钩子,当由事件触发时,通过packagist提供的API通知packagist,packagist就会去和github做同步操作。

接下来就是运行:

composer require ifeeline/wkhtmltox:dev-master

如果要拉开发分支,在包后加冒号接dev-master即可。

以下是一个例子:
ifeeline_whhtmltox