书写技术成长之路

php docker compose

用docker compose 构建PHP开发环境


version: '2' services: web: image: nginx:latest ports: - "8080:80" volumes: - ./code:/code - ./site.conf:/etc/nginx/conf.d/default.conf networks: - code-network php: image: php:fpm volumes: - ./code:/code - ./php.ini:/usr/local/etc/php/php.ini networks: - code-network command: > bash -c "apt-get update -y && apt-get install -y libfreetype6-dev && apt-get install -y libjpeg62-turbo-dev && apt-get install -y libpng12-dev && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && docker-php-ext-install gd" composer: image: composer/composer:php7 restart: 'no' command: install working_dir: /code volumes: - ./code:/code networks: - code-network mysql: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=docker - MYSQL_USER=docker-container - MYSQL_PASSWORD=YqukX1!m!eBeP networks: - code-network redis: image: redis:5.0-alpine ports: - "16379:6379" networks: code-network: driver: bridge

Mac PHP7.1环境搭建问题

正在使用的PHP7.1环境突然报出了如下错误,

屏幕快照 2018-01-28 下午3.35.38.png

这个错误还没有见过dyld: Library not loaded: /usr/local/opt/jpeg/lib/libjpeg.9.dylib, Google了一番最终解决了。

解决方案:

  1. 先安装libjpeg, brew install libjpeg。以下是截图 屏幕快照 2018-01-28 下午4.01.36.png

  2. 根据命令提示切换

The currently linked version is 8d
You can use `brew switch jpeg 9b` to link this version.`
  1. 运行 brew switch jpeg 9b

  2. 再运行php -v就可以看到php的输出信息了。

github和stackoverflow上有关这个的讨论我试了一番都不行,这次解决也是不断尝试出来的。

参考链接

php72 install libjpeg.8.dylib not found

https://stackoverflow.com/questions/32703296/dyld-library-not-loaded-usr-local-lib-libjpeg-8-dylib-homebrew-php

https://stackoverflow.com/questions/32703296/dyld-library-not-loaded-usr-local-lib-libjpeg-8-dylib-homebrew-php

https://stackoverflow.com/questions/38331388/how-to-install-libjpeg-on-osx

安装PHP

安装PEAR

安装oracle扩展

PHP上线遇到版本问题解决方案

昨天项目上线,本地开发环境和测试环境运行都没问题,到上线运行composer install --no-dev --optimize-autoloader就报了错,原因是composer依赖包要求的版本是PHP 7.1.3, 而生产环境用的PHP版本是7.1.2,所以就报错了。

WechatIMG230.jpeg

产生问题的原因: 由于本地开发用的PHP版本是7.1.3,所以在用composer update安装依赖包的时候composer就直接按照我本地的php版本来安装,这样就导致一个人机器上运行好好的,换到别人机器上就不能运行了。

解决方案: 通常有三种

  • 降低你本地的PHP版本,使你的版本小于线上的版本。
  • 升级服务器的PHP版本。
  • 在composer.json中添加指定平台的PHP版本, 重新运行composer update来更新composer.lock, platform指令就是让你可以模拟生产环境或者你指定的平台

WechatIMG231.jpeg

此三人说的已经很清楚了

Different php versions for development and production · Issue  5163 · composer composer · GitHub.png

参考

https://stackoverflow.com/questions/26277151/force-composer-to-require-php-version-between-version-x-and-version-y

https://getcomposer.org/doc/06-config.md#platform

https://github.com/composer/composer/issues/5163

https://github.com/laravel/homestead/issues/638

PHP Hello World 扩展

  1. 从git下载php源码 git clone http://git.php.net/repository/php-src.git
  2. cd php-src
  3. 切换到指定版本的PHP分支, 例如 git checkout PHP-7.1
  4. 构建 ./buildconf
  5. 指定配置 ./configure --prefix=/etc/php7 --enable-debug --enable-maintainer-zts
  6. make
  7. make install
  8. 复制php.ini文件 'cp php.ini-development /etc/php55/lib/php.ini' 到此,PHP已经编译安装好了,下面开始编写扩展

首先生成扩展的基本框架, 进入到php-src的ext目录,执行 ./ext_skel --extname=hello, 如果执行成功会看到一些说明信息。 然后修改hello/config.m4文件,写入自己的函数逻辑

去掉注释PHP_ARG_ENABLE, 结果如下

PHP_ARG_ENABLE(hello, whether to enable hello support,
Make sure that the comment is aligned:
[  --enable-hello           Enable hello support])

然后再注释掉 AC_DEFINE

AC_DEFINE(HAVE_HELLOLIB,1,[ ])

在ext目录下执行phpize会生成一些配置信息,然后指定配置 ./configure --enable-hello

再次修改helo.c文件 vim hello.c, 添加以下代码

/* {{{ proto string hello_world(string arg)
   Say Hello World to everyone */
PHP_FUNCTION(hello_world)
{
    RETURN_STRING("Hello world");
}
/* }}} */

并更新zend_function_entry函数体

const zend_function_entry hello_functions[] = {
    PHP_FE(confirm_hello_compiled,  NULL)       /* For testing, remove later. */
    PHP_FE(hello_world, NULL)
    PHP_FE_END  /* Must be the last line in hello_functions[] */
};

编译 make

运行 php -i | grep hello查看是否有该扩展,也可以直接修改php.ini文件,在最后面加入extension=hello.so并把编译生成的hello.so复制到PHP扩展目录下就好了, cp /php-src/ext/hello/modules/hello.so /etc/php55/lib/php/extensions/no-debug-non-zts-20121212

执行 php -r 'echo hello_world();'就可以看到hello world了。

参考

http://ahungry.com/blog/2016-09-29-Creating-a-php-7-extension.html

PHP编译安装

从git仓库下载代码,这样可以随时切换不同的PHP版本

1. git clone http://git.php.net/repository/php-src.git
2. cd php-src
3. git checkout PHP-5.5
4. 安装编译所需要的各种开发工具 sudo yum groupinstall 'Development Tools' 或者 yum install gcc gcc-c++ make openssl-devel libxml2 libxml2-devel
5. 构建 ./buildconf --force
6. 编译 './configigure --prefix=/etc/php55 --enable-debug --enable-maintainer-zts' prefix指定PHP的安装路径,可改为你自己的, 开启调试模式和下次线程安全
7. make
8. make install
9. 更改环境变量 vim /etc/profile 加入 PATH=$PATH:/etc/php55/bin; SOURCE PATH;
10. 执行'php --ini'查看是否成功, 发现没有ini配置文件,执行'cp php.ini-development /etc/php55/lib/php.ini'

在Redhat 7上编译安装PHP的时候,遇到了不支持bison的问题

Error Message: checking for bison... bison -y
checking for bison version... invalid
configure: WARNING: bison versions supported for regeneration of the Zend/PHP parsers: 1.28 1.35 1.75 1.875 2.0 2.1 2.2 2.3 2.4 2.4.1 2.4.2 2.4.3 2.5 2.5.1 2.6 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7 (found 3.0).

bugs.php.net找到了答案,最后发现只有PHP7才开始支持bison 3.0版本,以下是官网的解释

When building directly from Git sources or after custom modifications you might also need:

autoconf: 2.13+ (for PHP < 5.4.0), 2.59+ (for PHP >= 5.4.0)
automake: 1.4+
libtool: 1.4.x+ (except 1.4.2)
re2c: Version 0.13.4 or newer
flex: Version 2.5.4 (for PHP <= 5.2)
bison:
PHP 5.4: 1.28, 1.35, 1.75, 1.875, 2.0, 2.1, 2.2, 2.3, 2.4, 2.4.1, 2.4.2, 2.4.3, 2.5, 2.5.1, 2.6, 2.6.1, 2.6.2, 2.6.4
PHP 5.5: 2.4, 2.4.1, 2.4.2, 2.4.3, 2.5, 2.5.1, 2.6, 2.6.1, 2.6.2, 2.6.3, 2.6.4, 2.6.5, 2.7
PHP 5.6: >= 2.4, < 3.0
PHP 7.0: 2.4 or later (including Bison 3.x)
解决方案
  1. 卸载yum安装的bison, sudo yum remove bison
  2. 下载2.6.4版本并编译安装
wget http://ftp.gnu.org/gnu/bison/bison-2.6.4.tar.gz
tar -xvzf bison-2.6.4.tar.gz
cd bison-2.6.4
./configure
make && make install
  1. 更改环境变量 vim /etc/environment
PATH=$PATH:/usr/local/bin
export PATH
  1. 使环境变量生效: source /etc/environment, 运行bison -V查看bison版本
参考

http://www.phpinternalsbook.com/build_system/building_php.html#why-not-use-packages

https://bugs.php.net/bug.php?id=69055

http://php.net/manual/en/install.unix.php

https://github.com/php-build/php-build

php-fpm优化配置

一直以来很少关注PHP-FPM的配置,偶然一天看到一篇技术文章,讲述的是PHP-FPM占用的大量的内存和CPU,如何优化的问题。

/etc/php-fpm.conf.default中 PHP-FPM的默认配置是pm = dynamic, 也就是动态的分配php-fpm的子进程,这就导致了 一些分配的进程没有被使用,只是等在那里,所以占用了内存。

文章里提到的最重要的就是改变这种动态分配子进程的方式,改用在需要时才分配,大大节约了内存和CPU

# 优化前的配置
pm = dynamic
pm.max_children = 75
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

# 优化后的配置
pm = ondemand
pm.max_children = 75
pm.process_idle_timeout = 10s
pm.max_requests = 500

参考

How to reduce PHP-FPM (php5-fpm) RAM usage by about 50%

A better way to run PHP-FPM

大话PHP SESSION

说起session,作为一名后台开发程序猿,无人不知无人不晓。其实开发以来,只知道个大概原理,这次要详细记录下来。

session的由来

说起session的由来就必须先从HTTP说起了,由于HTTP协议是无状态的,所以并不能识别这个请求每次是谁发送的, 比如你这次发请求往购物车里添加商品,然后你又添加一个商品,由于HTTP的无状态,所以根本不能识别还是你添加的, 它把每次请求都视为不同的,这样就不能记录用户相关的信息了,更别说现在做什么智能推荐等等了。

为了能记录这些状态,一些先驱,前辈便发明了session和cookie这对兄弟。当你第一次请求服务器的时候,就给你分配一个唯一的session_id, 在服务端保存这个session_id,并在你的浏览器设置cookie,比如这个cookie名叫SESSIONID。当你下次发送请求的时候浏览器就会自动带上这个cookie发送到服务器端,服务器端查找这个SESSIONID,如果找到就可以识别这个就是你了。比如你不登录也能往购物车里添加商品,每次就是根据这个SESSIONID来维护购物车的状态,当你登录了就自动把购物车里商品信息记录到你的用户下。还有你不登录的时候一些APP的智能推荐新闻等,即使你未登录,也会在客户端和服务端有一个唯一标识你信息的ID。

如何设置30天自动登录呢?

30天自动登录,以前想的就是你登录成功的时候,就用setcookie把过期时间设置为30天的时间。但是这个只是设置cookie的有效期,跟服务端的session有效期是两个独立的概念。比如你手动把浏览器的cookie清除了,就登录不上去了,这只是你浏览器的SESSIONID不存在才导致不能登录,并不是SESSION也失效了,你看到的未必是你看到的~。如果你能伪造之前的COOKIE并发送到服务器端,还是可以登录的,因为这个时候服务端的SESSION还是有效的,并没有被回收。

那么SESSION的30天有效期怎么设置呢?

首先不是setcookie那么简单的就解决了,当然setcookie设置30天的有效期是必要的,但是还要设置服务端session的有效期。 stackoverflow的大神是这么说的

  • session.gc_maxlifetime should be at least equal to the lifetime of this custom expiration handler (1800 in this example);

  • if you want to expire the session after 30 minutes of activity instead of after 30 minutes since start, you'll also need to use setcookie with an expire of time()+60*30 to keep the session cookie active.

大意是说你要用session.gc_maxlifetime来设置session的最大生命周期,超过设个时间就会被垃圾回收, 然后在你的程序里,session_start的时候要设置一个last_activity值,记录最后更新的时间,如果这个值还存在,并且距离当前小于30天的 就表面还有效,就更新最后的更新时间,否则就销毁这个sesion,以此来实现30天自动登录的功能。

闲扯

现在更多的使用JWT(Json Web Token)来做状态标识了,因为一下几点使得JWT更适合当今的开发

  • SESSION默认是使用文件存储的,当访问量大和数据量大的时候,这会成为性能瓶颈
  • SESSION在负载均衡的时候会比较麻烦,当然可以使用Redis,Memcache等来解决(也能解决上面的问题)
  • SESSION在前后端分离模式下就显得更不适用了
  • JWT直接保存相关用户信息,还可以使用签名来保证安全,不用服务端保存这些信息,节省了空间,也解决了SESSION的性能瓶颈问题

参考地址

https://stackoverflow.com/questions/520237/how-do-i-expire-a-php-session-after-30-minutes

http://www.laruence.com/2012/01/10/2469.html

array_combine 函数的误区

情景是这样的,需要记录用户的身份证和姓名信息,前端以表单的方式提交过来

前端提交的数据格式

$id_cards[] = '3306821978072033829';
$id_cards[] = '371581199202123681';

$names[] = '小李';
$names[] = '小王';

后端接收到的数据是这样处理的

$names = ['小李', '小王'];
$id_cards = ['3306821978072033829', '371581199202123681'];

$inputs = array_combine($id_cards, $names);

foreach ($inputs as $key => $value) {
        $info['id_card'] = $key;
        $info['name'] = $value;
        $result[] = $info;
}

json_encode($result);

发现最后输出结果是这样的

{
    "id_card": 3306821978072033100,
    "name": "小李"
},
{
    "id_card": 371581199202123100,
    "name": "小王"
}

怎么后面变成100了呢?不细心看还真发觉不出来这个bug。 通过调试发现,数组$id_cards全是数字,PHP就把它当成整型来处理,这时候array_combine是用身份证号作为数组的键,然后本来以为$id_cards 中的元素是字符串,在用array_combine的时候就被隐式转换为整型了,这就可能会发生超出INT的最大值而导致意想不到的结果。

最后的解决方案就是为$id_cards的每个元素添加个字符串前缀, 然后再用array_combine

array_walk($id_cards, function(&$val) {
    $val = "E".$val;
    return $val;
});

PHP数组在指定位置处插入元素

<?php

/**
 *
 * array_splice函数是删除数组中的一部分并返回
 * 这里用它来截取要插入的元素的前面的元素,这样就可以获得要插入元素前面的数组了
 * 这时候$arr也被截取为两部分,前半部分是待插入元素前面的数组,后半部分是待插入元素后面的数组
 * 然后利用array_merge就可以实现在任意指定位置插入元素了
 */
function insert_to_array($arr, $position, $element)
{
    $first_array = array_splice($arr, 0, $position);
    $result = array_merge($first_array, $element, $arr);
    return $result;
}

$arr = ['第1条' => 'aaa', '第3条' => 'ccc', '第4条' => 'ddd'];
$insert_arr = ['第2条' => 'bbb'];
$result_arr = insert_to_array($arr, 1, $insert_arr);
print_r($result_arr);

PHP 常见误区

foreach 循环中的变量引用

foreach中的变量引用可以用来更改数组中的元素,例如

$arr = array(1, 2, 3, 4);
foreach ($arr as &$value) {
    $value = $value * 2;
} 
// $arr foreach后的值为(2, 4, 6, 8)

但是变量的引用也会导致一些意想不到的bug,比如下面的例子

$array = [1, 2, 3];
foreach ($array as &$value) {} // 通过引用的方式, $value是数组$array元素的引用
echo implode(',', $array), "\n";

foreach ($array as $value) {} // 通过赋值的方式
echo implode(',', $array), "\n";

// 结果是
1,2,3
1,2,2

后面再循环$array的结果却不是1,2,3而是1,2,2 这是因为在每一次循环过后,$array中的元素都会赋给$value, 因为$value是引用的方式,在循环结束后,$value仍然引用数组的最后一个元素,也就是$array[2], 这时候在第二次foreach循环的时候,因为$value仍然是引用$array[2], 当把数组中的每一个元素赋给$value的时候,$array[2]的值也跟着改变,在第二次foreach的时候

  1. 把$array[0]赋值给$value,因为$value是引用$array[2], 所以这个时候$array[2]的值也变为1
  2. 把$array[1]赋值给$value的时候,因为$value是引用$array[2], 所以$array[2]的值变为2
  3. 这个时候需要把$array[2]的值赋给$value, 因为$array[2]=2,所以$value的值也是2 这样,foreach之后,$array的元素就变为[1, 2, 2]了。 所以在foreach修改元素值的时候,为了避免引入bug,就需要删除引用
$arr = array(1, 2, 3);
foreach ($arr as &$value) {
    $value = $value * 2;
}
unset($value);       // 删除$value和$arr中最后一个元素的引用

isset的误区

isset检查变量是否设置的时候并不可靠

$arr = [];
$arr['name'] = null;

var_dump(isset($arr['name'])); // result is false

// 如果要检测变量是否存在并不为空
if ($arr['name']) {
   // key存在并不为空
} else {

}

isset在变量的值是null的时候依然会返回false, 所以如果要检测变量存在并且不为空要使用if来判断,最主要的是取决于你期望获得的结果是什么。

$_POST误区

$_POST超级全局变量并不总是能获得POST方法提交来的数据

$.ajax({
    url: 'http://my.site/some/path',
    method: 'post',
    data: JSON.stringify({a: 'a', b: 'b'}),
    contentType: 'application/json'
});

上面这段通过ajax请求并且请求方法是post,不过它的contentType是application/json,这种方式在当前开发中很常见, 但是打印$_POST输出的却是空数组,这是为什么呢? 这是因为在POST方法请求PHP执行时,PHP只能处理content type是application/x-www-form-urlencode或者multipart/form-data格式的, 所以上面这种application/json Content Type不在$_POST变量中,那么我们该怎么处理这种格式呢

// 通过这种方式来获得content type是`application/json`的数据
$data = json_decode(file_get_contents('php://input'), true);

参考

https://www.toptal.com/php/10-most-common-mistakes-php-programmers-make