MySQL 的编码问题

解决字符集为 utf8_general_ci 的表里无法存储表情符号的问题。

前几天朋友的小程序遇到点问题,同样的代码,有些人就是注册不上,有些人就没问题。让我帮忙看。

看代码应该没问题,我自己试也没问题,很诡异。后来我发现,说注册不上的一个截图里,昵称里有一个雨滴的符号。我们知道,传统的编码是没有表情符号的,表情符号是 Unicode 后期才加进去的,那么会不会是数据库字段的问题?看了一眼,Laravel 默认创建的数据库,字符类型是 utf8mb4_unicode_ci,而他们数据库是 utf8_general_ci,Google 一下,找到下面这篇文章:

為什麼MYSQL要設定用UTF8MB4編碼 UTF8MB4_UNICODE_CI

里面提到:

當資料庫需要儲存或處理以下資料:emoji (手機端常用的表情字符)

应该使用 utf8mb4_unicode_ci,因为它会用更多的空间存储字符。基本锁定是字符集的问题。然后看到梦康大的一篇博文:直接使用 mysql utf8 存储 超过三个字节的 emoji 表情 ( 不使用 utf8mb4 ),决定参考他的方案,毕竟改库改表不是小事情。

不过时过境迁,梦康文中的 func_overload 已经可以用 mb_strlen($str, '8bit') 来替代,所以最后的代码大约是这样的:

// 替换
protected function encodeEmoji($input) {
  $length = mb_strlen($input, 'utf-8');
  $result = '';

  for ($i = 0; $i < $length; $i++) {
    $tmp = mb_substr($input, $i, 1, 'utf-8');
    if (mb_strlen($tmp, '8bit') >= 4) {
      $result .= '[[Emoji:' . rawurlencode($tmp) . ']]';
    } else {
      $result .= $tmp;
    }
  }
  return $result;
}

// 替换回
protected function decodeEmoji($nickname) {
  return preg_replace_callback('~\[\[Emoji:(.*?)\]\]~', function ($matches) {
    return rawurldecode($matches[1]);
  }, $nickname);
}

所以说,程序员心态要保持年轻,步调要跟年轻人保持一致,这样才更容易发现新问题,所以我的昵称已经改成“肉山🎩”了。

Laravel 开发笔记:环境搭建

记录 Laravel 的一些笔记,随时更新。

文档

英文官网:https://laravel.com/
英文文档:https://laravel.com/docs/5.5
中文文档:https://d.laravel-china.org/docs/5.5/packages

建议两边都打开,对比阅读。

配合 PHPStorm 使用

Laravel 里面定义了很多别名,直接放在 PHPStorm 里会有很多黄色曲线,看起来非常不爽。关键是影响代码补全,所以最好用一个库来搞一下。

安装 barryvdh/laravel-ide-helper:

composer require --dev barryvdh/laravel-ide-helper

注册服务。编辑 app/Providers/AppServiceProvider.php:

public function register() {
  if ($this->app->environment() !== 'production') {
    $this->app->register(IdeHelperServiceProvider::class);
  }
}

生成 meta 文件:

php artisan ide-helper:meta

使用 Satis 搭建私有仓库

这篇文章介绍如何使用 Satis 搭建私有源。Composer 是现在最常用的 PHP 依赖管理工具,然而 PHP 的特点决定了它对安全性的要求很高,把依赖放在开放仓库里可能会带来各种危险,所以建立私有源就显得非常必要。Satis 是官方提供的工具,简单好用。本文除了介绍文档中的搭建流程,还会分享我踩过的坑。

题外话

开始正题之前聊一些题外话。PHP 是个很好的语言,简洁高效,易学强大。但是出身不好,工程学上的高起点反而成为大家轻视它的原因。很多开发者也的确对自己要求不高,只写业务逻辑不考虑语言特性,使得代码难看难改难维护。所以我想多说两句回顾一下 PHP 本身的发展史。(以下以我个人经历为主)

上古时期

我们一个页面写一段 PHP,或者一个动作写一个 PHP,收集请求,做出处理,给出回应,完成。

好处:

  1. 简单,好上手
  2. 逻辑关系清晰,从前端可以直接找到目标程序
  3. 一个地方出错,多半只挂一个功能

坏处:

  1. 代码复用率低,不好维护
  2. 难以批量修改

古典社会

随着工程变大,需要大量的 PHP,分散碎片化的代码实在难以管理和维护。于是我们开始把一些共用代码抽出来,做成一个函数,叫 functions.php,其他所有页面都 include 它,这样公用的代码就不会这里一份那里一份了。

好处:

  1. 提高了代码复用性,减少开发量,提升效率,降低维护难度

坏处:

  1. 工程大的话,一个 functions.php 好几千行,可读性也没好到哪儿去
  2. 有时候我们需要对一个函数进行一些小修改,于是不仅函数库会膨胀,函数本身也在膨胀

中世纪

PHP 引入类的概念,并且提供了“魔术方法”来实现一些功能。有些程序员也意识到不能所有代码都自己手写,该引用的还得引用,于是从一些开源的库拷来代码开始用。这个时候连 Google Code 都不存在,下载代码多半在网上搜索 + Ctrl C/V,所以代码中各种混用。经常出现,我 include 一个文件,然后就挂了,原来是类被重复定义,或者全局环境下同名函数互相覆盖。开发乱象不断,形如黑暗的中世纪。

好处:

  1. 不考虑维护的话,开发速度还是可以的……

坏处:

  1. 越到后来坑越多,项目一大积重难返
  2. 内部执行环境不统一,a.php b.php 的内部环境都不一致,共享代码反而更加困难

文艺复兴

PHP 对类的支持已经十分完善了,大家也开始习惯用命名空间划分领域。通过使用设计模式、继承、接口,复写功能和代码管理的情况大大改善。同时,伴随 Google Code 和 GitHub 的出现和发展,大家有了一个托管代码和寻找代码的好地方。我们也开始用 SVN 管理代码,不会再搞出 action.php action.php.bak action_new.php action_new.20160102.php 这样的幺蛾子。开始学习开发规范,开始更多的的用类管理代码。

好处:

  1. 代码规范
  2. 版本管理后,更好追溯代码的变更记录
  3. 可以下载到新版本的代码

坏处:

  1. SVN 不方便进行多仓库的管理
  2. 测试还靠人工发掘问题

近代社会

Git 开始普及,我们可以更方便的管理代码了。GitHub 发展速度很快,从上面找好代码也很容易,凭借 Git 子仓库的概念,维护依赖也容易很多。MVC 框架开始普及,单入口开始流行,内部执行环境得到统一。开发者意识到测试的重要性,开始使用测试工具进行测试开发,代码的稳定性进一步提升。

好处:

  1. 内部执行环境统一,全局修改变得容易
  2. 开始写测试了

坏处:

  1. 学习成本开始增加,新入行的人开始搞不懂,为啥写一个脚本就能干的事,你们要搞这么复杂一套架构出来

现代社会

包管理工具成为标配。项目依赖不再通过复制代码或者子仓库来管理,而是直接使用包管理工具 Compposer。并且整合测试、部署脚本,方便我们更容易地完成整套开发流程。另一方面,前端之前已经崛起,PHP 可以更多的考虑后端业务逻辑,输出纯粹的数据接口。

好处:

  1. 大型项目稳定性可用性大大增加
  2. 专业分工加强,PHP 程序员可以更多考虑后端逻辑

坏处:

  1. 学习曲线更加陡峭,新人入行更难,甚至连有经验的老人都未必能适应新形态的开发。

然则历史的车轮不可阻挡,我们势必会走向学习成本更高、学习曲线更陡,但业务量更大、更稳定的未来。


正文开始

正如前面所说,现在我们更多使用 Composer 进行依赖管理。和其它语言的包管理工具一样,Composer 使用 GitHub 托管代码,可以根据配置文件管理依赖,也可以建立各种脚本,执行特定任务。总之好处很多。

实际工作中,我们可以把多个项目公用的逻辑抽出来,作为一个依赖,然后提交到 Packagist,就可以在其它项目中引用它了。但是,与 NPM 这种工具不同的是,PHP 程序多半会部署在服务器上,通过接口接受外部访问,对安全性的要求高很多。前端可以放开给大家随便观摩,后端最好还是放在别人轻易看不到的地方,万一哪个同事把密码、salt 写到代码里提交,被搜出来,结果可能就很危险。

此时我们就需要一个工具,能够搭建私有源,里面都是私有仓库,对内不对外。

Satis 就是 Composer 官方提供的建立私有源的工具。它的文档在这里 以及 这里

整体流程并不复杂,文档里都有,我简单复述一下,只包含我用过的部分,重点穿插我的经验。我假定读者已经了解 Composer 的基础使用,如有问题,请自行翻阅文档。

1. 建立项目

使用 Composer 自带的建项目功能,这个相当于 git clone + composer install + 运行 post-install 脚本。

composer create-project composer/satis my-satis --stability=dev --keep-vcs

2. 建立配置文件

/path/to/my-satis 目录下建立 satis.json 文件

{
  "name": "仓库名称",
  "homepage": "http://satis仓库地址",
  "repositories": [
    { "type": "vcs", "url": "https://github.com/mycompany/privaterepo" },
    { "type": "vcs", "url": "http://svn.example.org/private/repo" },
    { "type": "vcs", "url": "https://github.com/mycompany/privaterepo2" }
  ],
  "require-all": true
}

注意:仓库名称需要和仓库里 composer.jsonname 定义一致,和路径没什么关系,不然就会找不到。我当时被这个卡了好久……

因为加入私有源的仓库本身可能也有依赖,require-all 会把这些依赖的信息也抓进来。如果不需要的话,可以指定某个仓库,甚至某个版本:

{
  "name": "仓库名称",
  "homepage": "http://satis仓库地址/",
  "repositories": [
    { "type": "vcs", "url": "https://github.com/mycompany/privaterepo" },
    { "type": "vcs", "url": "http://svn.example.org/private/repo" },
    { "type": "vcs", "url": "https://github.com/mycompany/privaterepo2" }
  ],
  "require": {
    "company/package": "*",
    "company/package2": "*",
    "company/package3": "2.0.0"
  }
}

3. 生成仓库列表

执行

php bin/satis build satis.json ./web

就可以在 path/to/my-satis/web/ 里生成仓库列表了。

4. 在其它项目中使用私有源

只需要在项目的 composer.json 文件的根上添加

{
  "repositories": [
    {
      "type": "composer",
      "url": "http://satis仓库地址/"
    }
  ],
  "require": {
    "company/package": "1.2.0",
    "company/package2": "1.5.2",
    "company/package3": "dev-master"
  }
}

之后再通过 composer require 或者 composer install 想要的仓库就可以了。

注意:源里面只有“仓库列表”,并没有真的同步代码仓库过来,所以下载还要走托管代码的机器,比如 GitHub,内部 GitLab 等。所以需要确保相关的 ssh-key 已经添加,或者在配置文件中写上登录信息(不建议这么做)。

Tips: secure-http

satis 默认要求使用 https,不过 https 需要证书,不太好搞,比如前司运维就不愿意弄(当然,他们工作很忙,我十分理解)。此时我们可以设置 secure-httpfalse 强制 Composer 接受 http 的源。需要注意,secure-httpconfig 的属性之一,写在根上是没用的。

{
  "config": {
    "secure-http": false
  }
}

总结,Satis 私有源的搭建,对于使用 PHP 的开发团队来说是非常必要的。用 Composer 管理依赖效果也非常好,希望所有 PHP 开发者都好好学一学。我现在用的也比较浅,将来有心得继续补充。

使用 Phantomjs 导出 PDF

使用 Phantomjs 可以很方便的导出 PDF,并且几乎可以在导出前进行各种编辑调整,使其满足要求。

接到一个新需求,给用户导出电子协议,也就是 PDF 文档。因为我们后台使用 PHP,所以自然就去寻找 PHP 的解决方案。看了几个库,包括 Packagist 几万十几万下载的库,唉,不得不说,虽然 PHP 是世界上最好的语言,但是 PHP 开发者,审美水平真的,说好听点就是,没法看……

第二个问题是,PHP 很多都要“拼” PDF,一行一行,一个元素一个元素,组装起一个完整的文档。相当费力,而且样式不好控制。从 HTML 转换也有类似的问题。研究了一会儿觉得蛋越来越疼,唉,算了,还是换 Phantomjs 吧。

Phantomjs 是一个命令行 Webkit 工具,我们可以把它理解成不输出页面的浏览器,但它支持浏览器的各种功能,因为有 Webkit 嘛。所以渲染网页然后抓图就是小菜一碟了。

用 Phantomjs 输出 PDF 非常简单:

  • 首先,约定好宽高(为方便打印,我们的电子协议要分页,而且有页头页脚),完成页面模板。
  • 完成 Phantomjs 脚本。因为只用它生成文档,所以不需要 Web 服务。
  • 用 PHP 调用脚本,生成 PDF 文档,然后 readfile 给用户下载。

这样做的好处是我们随时可以预览效果,HTML 好读好改,PHP 替换其中的内容也很方便。而且代码非常简单,结合官方示例,很快就写出来了:

'use strict';

var page = require('webpage').create()
  , system = require('system')
  , args = system.args
  , url = args.length > 1 ? args[1] : 'http://www.dianjoy.com/'
  , filename = args.length > 2 ? args[2] : 'tmp';

page.viewportSize = {
  width: 800,
  height: 1100
};
url = decodeURIComponent(url);
page.open(url, function (status) {
  console.log(status);
  if (status === 'success') {
    page.render('/tmp/pdf/' + filename + '.pdf');
  }
  phantom.exit();
});

部署这段代码最大的问题反而是 GFW 导致 npm install phantomjs -g 失败,直接下载 zip 也不行(因为放在 Amazon S3 上)。于是继续给病魔加油,早日弄死方校长,及其它筑墙士。

第二个问题则是 PHP 执行脚本老不成功。只看文档很简单:

exec('/usr/local/phantomjs/bin/phantomjs pdf.js http://meathill.com/ meathill');

但实际上既没有生成文档,也没有任何返回,调试半天,我突然想起来前阵子用 Apktool 解析安装包的时候也遭遇过类似的问题,于是在末尾加上 2>&1 问题竟然就解决了。

Google 之,也不是很懂。回头再说吧。


其它参考:

shell_exec


图文无关。其实是一位大学友人今日喜得贵子,放张她的奇怪照片祝贺她。

解决 PHP 导出 CSV 的乱码问题

PHP 输出 CSV 文件时,如果是 UTF-8,需要在前面加上开始标记才能让 Windows + Office 正常识别。

项目当中遭遇一个奇怪的问题:

导出 CSV,文本编码使用 UTF-8,使用 Mac + Numbers,Windows + WPS 打开都正常,使用 Windows + Office 就乱码(Mac + Office 没有测试)。用记事本打开另存为,编码的确是 UTF-8。

后来发现,用 EditPlus++ 打开,然后另存为 “UTF-8 BOM”,就可以正常打开了。看来应该是这个 BOM 的问题。

于是乎参考 StackOverflow 这个答案,给输出的开头加上 "\xEF\xBB\xBF",果然解决了问题。

PHP匿名函数使用父作用域的变量

PHP的匿名函数可以使用 use ($var) 来使用父作用域的变量。

(自古图文不相关)

同事问为什么要用 array_maparray_filter之类的函数,用 foreach 不就好了?

答:这样写出来的代码语义更清晰,阅读更容易。

那么如何使用其它变量呢?global 么?

答:global 肯定不合适,不过怎么写我也不知道。待我查查。

在PHP中,不能像JS那样直接使用闭包里的其它变量,必须通过声明继承的语法,写出来是这样的:

    <?php

    $arr = [1, 2, 3];
    $split = 2; // 分界
    $arr = array_map(function ($value) use ($split) {
      return $value < $split ? 0 : $value;
    }, $arr);
    var_dump($arr);

    // 输出
    // 0, 2, 3

重点是那个 use ($split)


参考:

漏装php55w-mbstring导致中文邮件乱码

yum安装新版本php需要手动安装各种依赖。

朋友的WordPress发中文邮件总是乱码,喊我帮忙看看。很奇怪,后台、文章里的中文都能正常显示,看起来一切正常;我在我电脑上搭了一套,同样的代码,发邮件也没问题。

后来打开phpmailer的debug模式,发现什么都对,就是中文内容都是问号。

继续往上找到发邮件的函数,运气不错插件留了filter,遂修改模板,add_filter强制转换内容。

转换前先检查,不是UTF-8再转:

    if (!mb_detect_encoding($content)) {
      $content = iconv('ASCII', 'UTF-8', $content);
    }
    return $content;

结果代码传上去报错,说没有mb_detect_encoding,然后想起来yum安装php时确实默认不包含很多扩展,于是手动安装yum install php55w-mbstring。然后重启apache,发邮件测试,正常了。

我又想是不是缺少多字节文本模块(Multibyte String)导致原先无法发送中文呢?去掉filter,仍然正常,确实如此。

总结

yum安装新版php需要增加源,而新版的源默认不包含很多常用的库,使用的时候最好都装上。WordPress的翻译机制面对多字节文本时,编码不对不会报错,也需要小心。

HTML5跨域开发

HTML5中提供了跨域加载数据的方法,让我们得以从JSONP或者Flash中介等各种绕行方案中解脱出来,更加顺畅地与服务器交流。另一方面,因为PHP是最好的语言……所以在它与Node.js之间,我选择前者作为后端语言开发内容服务。这篇文章记录使用jQuery+PHP开发跨域应用时的小心得。

HTML5中提供了跨域加载数据的方法,让我们得以从JSONP或者Flash中介等各种绕行方案中解脱出来,更加顺畅地与服务器交流。另一方面,因为PHP是最好的语言……所以在它与Node.js之间,我选择前者作为后端语言开发内容服务。

这篇文章记录使用jQuery+PHP开发跨域应用时的小心得。

身份验证

做身份验证,最简单的办法就是使用PHP的SESSION保存用户信息,于是就要用到Cookie。默认情况下,跨域Ajax请求发起时候不包含Cookie,需要我们主动将XHRwithCredentials属性设为true才行。

jQuery会把XHR封装成jqXHR,并且不暴露真正的XHR(说实话这点有点难以理解,尤其是在做上传进度条的时候)。然后它提供一个给真正XHR赋值的接口xhrField,所以写成代码就是这样事儿的:

$.ajax(url, {
  xhrField: {
    withCredentials: true
  }
}

各种HTTP头

如果不需要验证用户身份,直接在HTTP头中输出Access-Control-Allow-Origin: *即可。

我的产品需要验证,那么首先,HTTP头中必须有Access-Control-Allow-Credentials: true;此时对域的限制也严格许多,不再允许像前面那样使用*放开给任何来源,必须指明哪个具体域可以接受。

关于Access-Control-Allow-Origin的值,规范中的说明是“域名列表或null”,然则接下来的“注意”有点诡异:“实际生产中,‘列表或null’要求更严格。你可以认为它实际只允许单一域名或null,而非空格分隔的域名列表。”——既然如此你干脆写个“域名或null”不就完了……

总之对于我们而言,返回的HTTP头中还要包含Access-Control-Allow-Origin: http://域名,指定允许作为来源的协议、域名、端口,并且只能有一个(组)。因为通常来说我们开发环境和生产环境不一样,所以这里的域名最好不要写在服务器配置里;使用PHP,通过$_SERVER['HTTP_ORIGIN']取出访问来源,与白名单比对,通过后再输出相应的头,更加合适。

调试

我选择JSON作为前后端交流的格式。为了方便浏览器解析(也是HTML5的要求),我还返回了Content-type: application/json头。

使用PHP少不了使用Xdebug。出现错误时,Xdebug会返回完整的栈,有利排查。但是为了方便阅读,Xdebug还会给返回信息套上<table>结构,这时Chrome的Network工具就会把它解析成奇怪的格式,所以Content-type一定要最后和数据一起返回。

与之相反的是前文说到的Access-Control-Allow-OriginAccess-Control-Allow-Credentials,这二位必须放在最前面。不然如果出现500错,响应头不包含这两个跨域标记,Chrome就会理所当然地不显示返回内容,也就无法看到错误描述,根本无法排查。

参考资料

  1. Using CORS
  2. Cross-Origin Resource Sharing
  3. HTTP access control (CORS)
  4. jQuery.ajax()

composer导致没有log的错误

composer没有autoload的类在引用时会报错,但是服务器log没有记录。

我一般使用git pull从Github更新代码。某次提交新版本后,服务器开始报500错误,但是看log什么都没有。反复回想好像上次拉完代码没有composer dump-autoload,而且这个版本确实增加了几个类。于是赶紧生成autoload,故障解除。

mustache.php在生产环境下一定要开cache

实测开与不开有3倍左右的性能差距,当然我们的模板并不复杂,理论上应该越复杂越能拉开差距。

mustache.php在生产环境下一定要开cache。

实测开与不开有3倍左右的性能差距,当然我们的模板并不复杂,理论上应该越复杂越能拉开差距。