Menu

tlanyan

十里平湖霜满天,寸寸青丝愁华年

PHP回顾之发送邮件

转载请注明文章出处: https://tlanyan.me/php-review-send-email/

PHP回顾系列目录

发送邮件是网站的常用功能,用户激活、找回密码等场景常需要发送邮件到用户邮箱。本文先回顾发送邮件的相关概念,再给出使用PHP发送邮件的示例代码。

发送短信

从功能上看,短信和邮件类似,用途常是通知和安全校验。发送短信(基本上)需要向供应商付费,所以短信供应商有动力提供清晰的文档,易用的接口方便用户接入。一般而言,发送短信的是:

  1. 寻找供应商,例如阿里大鱼、聚合数据等;
  2. 注册账户,获取appid和appkey;
  3. 申请模板;
  4. 查看接口文档,集成到应用中;
  5. 调用API发送短信。

流程简单易懂,接入和使用也十分便捷,基本上一两小时内就能对接和测试好。用户无需考虑讯息在通讯过程中的编码、寻址下发等细节,缺点是要付费。

邮件一般是免费服务,相关支持没那么到位,这也要理解。各种编程语言发送邮件的类库不少,从信源角度看基本可以分成两类:从本机发送和从第三方邮件服务商发送。为了理解邮件发送的流程,先介绍一些相关概念。

相关概念

大部分接触到互联网的人都有使用邮件的经验,但基本上限于邮件客户端、网页端和提供商这几个概念。作为一个开发,理解本节中的以下概念能更好的帮你掌握邮件通讯中的细节。

MUA : Mail User Agent,邮件用户代理。用户代理是开发中经常接触到的词,主要指 理解人的意图并代表用户向资源方请求的工具。例如浏览器是最常用的用户代理,以HTTP/HTTPS协议格式向web服务器发送请求,并解析响应,渲染后呈现给用户。邮件用户代理,常见的是Foxmail、Outlook这类工具,人们写好邮件后,按格式封装邮件内容与邮件服务器通讯。

MTA : Mail Transfer Agent,邮件传输代理,帮用户收发邮件的程序。常说的邮件服务器指的就是MTA,开源的程序有sendmail,postfix,QMail等。

MRA : Mail Retrieval Agent,邮件收取代理,将用户的邮件从邮件服务器取回本地。邮件客户端是常见的MRA。

SMTP : Simple Mail Transfer Protocol,简单邮件传输协议。用户与邮件服务器、邮件服务器互相传递邮件均使用该协议(默认明文,可使用SSL\TLS加密)。

POP3/IMAP : Post Office Protocol version 3/Internet Message Access Protocol,邮局协议版本3或网络信息获取协议,客户端从服务端获取邮件时使用的协议。

用户A(163邮箱)向用户B(Gmail邮箱)发信,用户B获取信件的过程涉及到上述的概念。流程和概念关系可用如下简图表示:

注:上图给出的是邮件发送的大体流程,其他MSA、MDA、ESMTP、SMTPS等可能会出现在整个流程中,但不影响邮件收发的理解。下文中会提到的缩写和概念会注明,其他请自行查询。

postfix

Linux下发送邮件的软件主要是sendmail和postfix,它们在系统中充当上文概念中的MTA/MDA(Mail Delivery Agent,邮件投递代理)角色。它帮助用户向外发送邮件,接收邮件投递到用户信箱(默认位置/var/spool/mail/用户名)。

sendmail是老牌的邮件软件,知名度非常高。但是Wietse(Wietse Zweitze Venema)用的不爽,于是有了postfix。postfix命令(几乎)兼容于sendmail,但更高效和安全(后缀fix的由来),是目前大部分Linux发行版的默认邮件收发软件,推荐使用postfix而非sendmail(本博客多年前有篇文章写如何配置sendmail,那时年少无知见识少,打算抽空把那篇文章改一下)。

postfix的主要配置文件是/etc/postfix/main.cf,配置文件的注释非常全,选项基本是自解释的。最重要的几个配置是:myhostnamemyorigininet_interfacesinet_protocols以及mydestination(如果你打算收外网来信的话)。需要注意inet_interfaces配置为localhost时,inet_protocols的值应为ipv4,否则可能会出现类似postfix: fatal: parameter inet_interfaces: no local interface found for ::1的错误提示。

与邮件相关的几个常用postfix命令是:

  1. mailmailx,发送邮件。tlanyan用户向root发送邮件:mail -s "Greetings" root@localhost -r tlanyan@localhost,接着终端中输入A nice day!,然后回车,按ctrl+D结束正文编辑,邮件就已经发送出去。登录到root账号,会提示在/var/spool/mail/root中有新邮件。用tail或者其他命令可查看邮件的详细信息。

  2. postquque,查看邮件发送队列。postqueue -p可取代sendmail中的mailq命令,postqueue -f刷新队列(强制尝试发送队列中的邮件)。

  3. postcat,查看未发送邮件的信息。例如postcat -q xxxx(xxxx是postqueue或者mailq显示的未发送队列ID)可查看邮件的详细信息,postcat -b -q xxxxx只查看邮件正文。

  4. postsuper,超级用户才可使用的邮件管理程序。postsuper -d xxxx,删除队列ID为xxxxx的邮件;postsuper -h xxxxx,暂停队列ID为xxxx的邮件发送,等。

以上介绍对于发送邮件基本已足够。注意,mail命令发送的邮件能投递的前提是postfix正在运行(ps aux | grep postfix | grep -v grep输出不为空)。

有了postfix,配置好后可以对外发送邮件,也能收取外网发送过来的邮件,但限于命令行操作。想用foxmail等客户端收发邮件,需要让服务器支持POP3/IMAP协议。开源的dovecot可以实现这个功能。dovecot服务于收邮件而非发送,了解其对开发中的帮助不大。如果想搭建一套完整的邮件系统(包括网页端支持、垃圾邮件过滤、病毒查杀、传输加密等),建议参考或使用国产开源的 EwoMail

了解postfix对开发中发送邮件帮助有多大?说实话,几乎没有帮助。原因是为了防止垃圾邮件泛滥,各大云服务器厂商屏蔽了25端口(Google Cloud连465都干掉了)。亚马逊云通过申请还有放行的可能(但有速率和每日额度限制),其他厂商几乎不会让你使用自己的域名从本机直接发送邮件。封禁25端口,必须使用第三方的邮件服务,几乎是业界的标准做法。

聪明的人可能想到,使用465加密端口(基于SMTPS,SMTP over SSL协议)或587端口(SMTP over STARTTLS协议)发送邮件,是不是就能绕开限制了?阿里云/腾讯云等厂商并不封禁465端口,发送邮件可以使用该端口而无需申请。但注意465和587端口是客户端和邮件服务器通讯使用的端口,邮件服务器之间通讯使用25端口。你可以通过465端口连接到Gmail邮箱对外发送邮件,但无法让postfix使用465端口投递邮件到hotmail邮件服务器。

总结来说,sendmail/postfix作为垃圾和欺诈邮件泛滥前的邮件服务器软件,对业界贡献很大。随着云服务器的盛行,几乎无法以指向本机的域名向外发送邮件,sendmail/postfix除了在本机内发送提醒邮件,用处已然不大。要对外发送邮件,要么自建机房,要么使用第三方邮件系统。

PHP的mail函数

作为PHP开发中,了解sendmail/postfix还是有点用处。mail函数默认使用sendmail/postfix发送邮件,了解相关配置,就能知道为啥能工作/为啥不能工作。

简单来说,要让PHP自带的mail函数正常工作,需要做以下事情:

  1. 申请域名,在DNS解析中设置MX记录,指向本机(非合法主机(FQDN, Fully Qualified Domain Name)发送的邮件都会被当做垃圾邮件直接丢弃);
  2. 安装sendmail/postfix,配置软件并运行;
  3. 配置防火墙、安全组,放行端口。

发送效率低、非面向对象的调用方式,配置麻烦以及云服务器厂商的封锁,是使用mail函数的最大阻碍。所以做PHP以来,本人并未直接用过mail函数。

PHP发送邮件

发个邮件要了解这么多,会让人觉得很心累。说好的PHP是最好的语言呢?

PHP发送邮件也可以很简单,推荐方式就是使用Swift MailerPHPMailer等类库。引入这些类库后,注册第三方邮箱(比如Gmail、QQ等),填好用户名密码,配置好STMP地址和端口,就能像发送短信一样轻松发送邮件。当然这些类库也支持使用sendmail/postfix发送邮件,但我想你不会再这样做了。

Swift Mailer为例,直接上代码说明使用PHP发送邮件也是一个非常简单的事情!

首先,在项目中引入Swift Mailer

然后准备好邮件内容(以文本文件为例,不带附件):

接着,设置好邮件传输方式(使用Gmail邮箱):

或者使用sendmail/postfix的方式(不推荐):

最后,使用transport构造mailer实例,发送邮件:

老板再也不用担心发送邮件收不到了,So easy!

总结

本文先回顾了发送邮件的相关概念,说明不推荐使用内置的mail函数原因,最后给出了使用第三方类库发送邮件的代码示例。

感谢阅读,欢迎评论指正!

参考

  1. http://cn.linux.vbird.org/linux_server/0380mail.php
  2. http://doc.ewomail.com/ewomail/285649
  3. http://php.net/manual/en/function.mail.php
  4. https://swiftmailer.symfony.com

PHP回顾之创建自己的Composer包

转载请注明文章出处:https://tlanyan.me/php-review-create-self-composer-package/

PHP回顾系列目录

前文 PHP回顾之Composer 简要介绍了Composer的相关概念和简要用法,应付日常开发已无大碍。想要更好的利用Composer协同工作,学会创建自己的Composer包是一项必不可少的技能。本文先讲解Composer仓库的概念,再给出创建和发布Composer包的步骤。

仓库(Repository)

仓库是软件开发中常见的概念,与源(sources)意义相近,主要指托管资源的场所。许多软件都有仓库的概念,例如yum、npm、maven、Git,以及本文的主角Composer。仓库以中心化的方式托管资源,为软件的正常工作提供保障。

Packagist 是Composer默认的中央仓库,PHP社区的绝大部分Composer包都托管在该网站上。Packagist提供公开的、免费的托管服务,任何人均可注册、自由发布包,无需审核。Packagist由Private Packagist提供托管和维护,两者的主要区别为:Packagist的官网是https://packagist.org,托管开源代码,面向公众提供免费包托管服务;Private Packagist的官网是https://packagist.com,托管的代码无需开源,仓库服务器可位于内网,提供更快、更高效的包代码托管服务。

可以配置多个仓库,Composer会自动找出最适合项目的依赖包。搜索包的流程如下:首先检查当前项目是否配置额外仓库,有则优先在额外仓库中检索;无结果向上到全局配置中的额外仓库检索;未配置或搜索无结果的情况下,回退到默认的Packagist中央仓库检索。除非禁用了默认的仓库,Packagist中的包总会被检索到。因为这个原因,Composer推荐PHP开发人员将包托管在Packagist网站上,方便他人检索和引用。

配置仓库

有两种方法对Composer的仓库进行配置:命令行和编辑配置文件。composer config是Composer配置的命令,可以用来配置项目或全局的仓库信息,例如:

第二种方法是编辑配置文件。编辑项目的composer.json~/.config/composer/config.json,增加repositories一项配置,例如:

以上配置使用 **Packagist中国全量镜像 ** 网站作为默认中央仓库。在大陆地区部署PHP项目,建议使用该仓库目录,能加速依赖包的下载。

仓库配置最重要的两个参数是typeurltype指明仓库的类型,url则指向具体网址。根据仓库的位置,常用的type可选值有:

  1. composer,Composer包托管仓库,例如 Packagist中国全量镜像
  2. vcs,版本控制管理系统,例如Github上的项目地址;
  3. pear,PEAR上的包;
  4. package,位于互联网上包;
  5. artifact,代码包zip包合集;
  6. path,指向代码具体位置。

互联网上的仓库,type的常见值是composervcs;本地的项目,常见值是artifactpath。具体用例,可参考Composer官方文档。

掌握了仓库的概念和其配置,接下来我们创建自己的包。

创建自己的Composer包

创建一个Composer包只需两步:1. 填写包描述信息;2. 写代码。本文创建一个hello-composer的包来演示创建过程。该包功能只有一个:输出字符串“Hello, Composer!”。

Composer包的描述信息存放在composer.json文件中,可直接新建(或从其他项目拷贝)composer.json文件,手动填充必要的字段信息;也可以用composer init命令,交互式的输入包信息,生成composer.json文件后再补全其他字段信息。我们采取直接编辑文件的方式,在composer.json中输入如下内容:

以上内容基本上是一个Composer包的必备字段。其他字段可参考Composer官网的composer.json说明。需注意标记为root-only的字段,root-only表示当前包为主项目时才生效。例如require-dev字段,在当前项目中开发,字段内的包会下载放到vendor文件夹内;如果该项目被其他项目引用,则该字段的值被忽略,引用的包不会被下载。

接下来编写代码。在src目录下新建HelloComposer.php

代码风格建议参考PSR-2规范,文件命名和路径规范建议参考PSR-4规范。另外需注意文件的路径需与composer.jsonautoload的配置一致。

通过简单两步,我们创建的自己的Composer包。接下来在其他项目中引用该包。

引用Composer包

新建一个test项目,引用上文创建的包并查看效果,步骤如下:

  1. 新建test文件夹,拷贝或者新建composer.json文件,配置如下:

配置文件需要注意两点: 1. 如果hello-composer的composer.json文件没有version字段(或不是稳定版),minimum-stability值要是dev(默认是stable),否则无法安装; 2. 需添加自定义仓库,type值为path

  1. 执行composer install -vvv安装依赖包,安装完成后vendor目录下生成tlanyan/hello-composer目录。

  2. 在test中新建Test.php文件,引用HelloComposer类:

  1. 执行Test.php:php Test.php,输出Hello, Composer!

通过配置Composer仓库,我们成功引用了创建的hello-composer包。测试没问题后,就可以发布到网上供其他人使用。下面简要说是发布流程。

发布Composer包

将Composer包发布到互联网的方式有几种:

  1. 打包成zip,上传到任意一个可公开访问的网站;
  2. 通过版本控制软件,上传到代码仓库;
  3. 提交到PEAR社区;
  4. 提交到私有的Composer仓库;
  5. 提交到Packagist。

前四种方式,需要用户配置仓库信息才能检索到包(PEAR社区几乎已死,可以忽略)。如果代码开源,建议提交到Packagist,方便全世界的PHP开发者检索和使用,为Composer生态做贡献。

提交包到Packagist,要经历以下过程:

  1. Github创建项目并提交代码;
  2. Packagist输入项目地址提交包;
  3. 在Github配置项目,触发Packagist自动更新。

前两步是必须的,第三步可选。本着为提交的包负责的态度,强烈建议完成第三步操作。

提交包的过程涉及到Github和Packagist两个站点,Github和Packagist之间的关系为:Github托管实际的代码和文件;Packagist托管包的作者、包名、版本号、下载量等元数据保。简要说Packagist是索引,Github是内容提供方。

详细步骤可参考官网指引或网上教程,网上相关内容太多,本文不再重复。

总结

本文介绍了Composer仓库的概念,创建了一个完整的Composer包,并给出提交包到Packagist的指引。用户掌握相关概念和运行机制后,可提交代码为社区做贡献,也可跳出Packagist自由的引用和安装依赖包。

参考

  1. https://getcomposer.org/doc/
  2. https://packagist.org/about
  3. https://www.phpcomposer.com/

PHP回顾之Composer

转载请注明文章出处:https://tlanyan.me/php-review-composer/

PHP回顾系列目录

Composer是PHP社区推荐的依赖管理工具。Composer之于PHP犹如npm之于Node,几乎是做现代化PHP开发的必备技能。本文简要回顾相关概念和Composer用法。

拓展和包

与之相关的概念是框架和库,关于框架和库的区别,可以查看本人之前写的这篇文章

拓展和包是两个非常相近的概念。在PHP世界里,一般可以这样理解和区分两者:拓展(extension)和模块(module)等价,是用C语言写的功能合集;包(package)和库(library)等价,主要是用PHP实现的功能合集;拓展以动态链接库(dll或so)的形式加载,包则是通过require/include方式加载。绝大部分时候,两者混用不会造成理解上的困难。

常见的拓展包括GD、ZIP、XML、MySQLi、OPCache等,常见的包包括PHPMailer、PHPOffice、HTMLPurifier等。

PEAR和PECL

在Composer流行之前,PEAR和PECL是更为PHP开发者所知的两个工具(社区)。PEAR是PHP拓展和应用仓库(PHP Extension and Application Repository)的缩写,官网http://pear.php.net;PECL是PHP拓展社区库(PHP Extension Community Library)的缩写,官网http://pecl.php.net

两者的区别可用拓展和包来区分:PECL托管拓展,源代码多为C文件,例如APC、AMPQ等;PEAR托管包,功能用PHP实现,如PHP CodeSniffer、HTTP Request等;PEAR对应pear命令,PECL对应pecl命令,可用这两个命令安装和管理拓展和包(pear的build/pickle子命令也可以编译PECL中的拓展)。两者互为补充,官网以姐妹(sisters)形容两者的关系。

PECL是官方拓展的补充,目前仍处于活跃状态,一些优秀的拓展有成为官方拓展的潜质。韩天峰大神的swoole拓展也托管在PECL中,国内名气非常高。相比之下PEAR已是明日黄花。PEAR2和Pyrus(下一代的PEAR包安装工具,基于PHP5.3+构建,官网http://pear2.php.net)的出现也未能挽救PEAR。PEAR没落伴随着本文主角Composer的兴起。

PEAR的定位是“提供可复用的PHP组件”,以中心化的方式为开发者提供功能包。中心化发布的方式保证了代码的质量,同时带来维护上的不便:通过评审的包才能发布,包过时现象严重。PEAR安装的包是全局的,不能为单独项目安装依赖包,非特权用户不能自行安装依赖包。其他缺点还包括糟糕的依赖管理。随着Github的流行和Composer的出现,包管理进入Composer时代。PEAR已经完成其历史使命,可以安心的去了。

Composer

严格来说,Composer的定位是依赖管理工具而非包管理器。Composer中文网对Composer工作介绍如下:

Composer 将这样为你解决问题:

a) 你有一个项目依赖于若干个库。

b) 其中一些库依赖于其他库。

c) 你声明你所依赖的东西。

d) Composer 会找出哪个版本的包需要安装,并安装它们(将它们下载到你的项目中)。

PEAR能做的事情,Composer都能做(包括安装PECL拓展),部分还能做得更好。Composer默认把包安装在项目目录下,普通用户就能正常使用(Composer官方建议不要以root身份执行composer命令);鼓励遵循最佳实践(即大名鼎鼎的PSR规范,详情见PHP-FIG官网https://www.php-fig.org),极大的推动PHP社区编码风格的规范化;Composer是去中心化的平台,任何人均可发布代码包;发布包无需评审,包的质量由用户投票决定…作为PEAR的继任者,Composer的表现经受住了社区的考验,并成为事实上的依赖管理标准工具。

Composer目前已经形成庞大的生态,在数量上,Composer的包远超PEAR。由于任何人均可自由发布包且无需评审,Composer生态中的包可能存在代码质量参差不齐、代码风格各异、后门漏洞等隐忧。另外Composer的依赖管理以项目为单位,一台机器上可能多次安装同一个包。但瑕不掩瑜,总体而言,Composer极大的改变了PHP的开发生态,促进了代码交流和社区发展。

Composer用法

Composer为管理的项目的依赖而生,项目中的composer.json文件是其工作的依据。该文件中最重要的部分是require部分,该部分告诉Composer期望安装的包及其版本,例如:

然后运行composer install命令,Composer会自动分析依赖,安装最合适的包到vendor目录下。加-v(-vv, -vvv)选项会打印命令执行过程中的详细信息。安装完毕后,vendor目录下会生成autoload.php文件。在项目的入口文件中包含此文件: require __DIR__ . "/vendor/autoload.php";,接下来便可在项目的任何地方引用依赖包中的接口和类。

install命令,Composer提供了许多其他命令管理依赖。常用的命令场景包括:查找依赖、引入依赖、安装依赖、更新依赖。分别对应的命令是:

  1. composer search: 根据关键字查找依赖包,例如查找本人发布的包:composer search tlanyan。该命令等同于上https://packagist.org进行包查找;

  2. composer require: 引入依赖,声明项目或者全局(global,用户名全局,非系统全局)依赖某个包, 例如声明需要swiftmailer包: composer require [global] "swiftmailer/swiftmailer:dev-master";该命令更新composer.json文件,并默认立即安装依赖(–no-update选项可阻止默认安装);效果等同于编辑composer.json文件,然后执行install命令;

  3. composer install:安装composer.json声明的依赖包,最终安装的依赖包版本可能取决于有无composer.lock文件;

  4. composer update: 更新依赖到最新版本,相当于删除composer.lock文件后执行composer install

以上四条命令涵盖使用Composer的大部分场景。以下是几个常用的辅助命令,与依赖分析相关:

  1. composer info: 查看安装的依赖包信息,与composer show等价;

  2. composer dumpautoload: 加-o选项可导出优化的加载器;

  3. composer why(-not): 查看(不)安装某个包的原因。

总结

从拷贝第三方代码到项目中(1994),到PEAR安装依赖包(1999),再到Composer兴起(2012),PHP社区经历了将近20年的探索。PHP这门古老的语言,也在不断的发展更新,在web领域一直发光发热。Composer作为目前PHP包依赖管理的最佳工具,值得每一位PHP开发人员掌握。

参考

  1. https://benramsey.com/blog/2013/11/the-fall-of-pear-and-the-rise-of-composer/
  2. http://fabien.potencier.org/the-rise-of-composer-and-the-fall-of-pear.html
  3. http://docs.phpcomposer.com

字符串、unicode和UTF8编码之间的互相转换

转载请注明文章出处:https://tlanyan.me/string-unicode-utf8-converter/

想知道某个字符串的UTF8编码,图方便打算使用在线工具。坑爹的是,号称“UTF8汉字互转”的网页几乎全是字符串和unicode码点互转,并不提供与UTF8编码互转功能。没搞懂unicode码点(code point)和UTF8编码的关系,还大言不惭的说UTF8编码,真让人无语。

字符、字节和字节序,unicode和UTF8编码,是理解字符编码重要的概念,详情可查看本人之前博文文件和字符编码。本文讨论unicode和UTF8之间的转换,先简要介绍两个概念:unicode是将字符与码点(code point,一个整数)一一对应的编码方案;码点通常用\uXXXX或者U+XXXX的方式表示,XXXX是码点的十六进制;UTF8是unicode的一个具体编码方案,规定字符存储的方式;UTF8编码字节数可变,不存在大小端问题,互联网通信中常采用此种编码方式。

回顾一下那成片的“汉字UTF8编码互转”网页所做的事情。以“中国”为例,两个汉字码点分别为20013和22269,十六进制表示4E2D 56FD,UTF8编码E4 B8 AD E5 9B BD。随手打开搜索结果中的http://www.ip138.com/utf8/,输入“中国”,点击“转换UTF-8”按钮,下方出现码点的十六进制编码4E2D 56FD(&#x可以理解为十六进制的前缀0x),并非UTF8编码。

JavaScript的String对象有charCodeAtcodePointAt(兼容性不如charCodeAt)方法。根据这个函数,网页的转换工作可用如下代码实现:

调用encode(“中国”)或者其他字符串,将得到每个字符对应码点的十六进制。但这不是字符串的UTF8编码!

要拿到UTF8编码,需要在码点的基础上多走一步。先回顾unicode码点与UTF8的转换关系:

unicode码点(十六进制)UTF-8(二进制)
0000 0000-0000 007F0xxxxxxx
0000 0080-0000 07FF110xxxxx 10xxxxxx
0000 0800-0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

根据这个表,结合charCodeAt函数,可以写出字符串到UTF8编码的函数:

调用函数utf8,这才是JavaScript得到字符串的UTF8编码的正确姿势!从UTF8编码转换到字符串,做相反工作即可。

请激活越狱状态,并安装pp助手越狱版

用pp助手越狱后,电脑程序显示设备已越狱并激活。但安装本地ipa时总是失败,提示“请激活越狱状态,并安装pp助手越狱版”。

出现这个问题,可能的原因有两个:一是设备曾经越狱过,目前已失效(pp助手电脑版提示已越狱应该是服务端缓存),需要重新越狱;第二个原因是设备确实已经越狱,但未完成最后一步。

解决办法如下:如果设备上没有pp越狱助手,下载电脑版越狱助手点击越狱,此步骤会安装越狱助手app到设备上;打开设备上的越狱助手app,点击“热门插件下载”。首次点击会闪屏然后回到锁屏界面,输入密码后设备才越狱完全,可正常安装自签名ipa。以后再次打开越狱助手app并点击“热门插件下载”,不会再出现闪屏界面。

CLI模式下Yii2的log问题追踪

转载请注明出处:https://tlanyan.me/trace-log-problem-of-yii2-in-cli/

命令行下运行长时间任务,发现Yii2的log组件不能正常输出日志。空闲之余逐步追踪问题,终于发现原因,以下是问题追踪记录。

问题复现

为了复现问题,预先准备了log组件的配置:

以及测试用例:

命令行下执行./yii test/short,日志正常输出到指定的文件中;执行./yii test/long,使用tailf或者tail -f命令查看日志文件,未发现输出;按ctrl+c终止脚本,日志没有出现新信息。

问题分析

仔细分析,运行上述代码有两个问题:1. log组件中flushInterval参数设置每10条信息合并输出一次,实际中一直没有输出;2. 按ctrl+c终止脚本,缓冲的信息没有输出。

由于之前已经向Yii开发团队提交过一个log组件的bug(issue链接:https://github.com/yiisoft/yii2/issues/8036)。本次发现的问题暂不确定是Yii2的bug还是使用方法不当,毕竟(应该)很少人会使用命令行运行长时间任务。

查找问题

根据log组件的架构,可能出现问题的三个点事:

  1. Logger类,Yii2默认的log组件类,对外暴露打印日志的log/info/warning/error等方法;
  2. Dispatcher类,对消息进行分类,根据配置分发到具体负责输出的输出类
  3. Target的具体实现子类,这些子类实现消息的具体输出,例如保存到文件/数据库,或者发送邮件等。

根据这些线索,可以比较清晰的查看调用链条。

首先是Yii::info函数的调用,定义在BaseYii中,具体实现委托给Logger类的log方法:

log方法功能是格式化消息,然后调用flush方法输出日志。接着看flush方法的实现:

flush方法委托Dispatcher将消息分发到具体的目标。继续跟踪前,先看看Logger类的init方法:

logger组件初始化时,注册register_shutdown_function回调函数,确保脚本执行完毕时消息被正确打印。这也是在脚本中无论何时出现exit/die,或者调用Yii::$app->end()都会让日志正常输出的秘密。接下来看Dispatcher类的dispatch方法:

dispatch让具体负责输出的target类收集信息,如果期间出现异常,关闭该渠道,将信息以warning级别输出。接下来跟踪target的collect方法,该方法定义在抽象类Target中:

collect方法的作用:1. 调用filterMessages方法过滤日志信息; 2. 判断是否达到输出条件,然后调用子类的export方法实现日志的具体输出。从collect方法,可以看到第一问题的原因:target类有exportInterval参数,默认是1000,示例代码要运行非常长的时间才会收集到如此多的消息,如果需要及时查看,可将配置代码改成如下:

接着再看中断脚本日志不输出的问题。前面已经提到,日志输出的技巧是注册了register_shutdown_function回调。先看看这个函数的官方解释:

注册一个 callback ,它会在脚本执行完成或者 exit() 后被调用。

可以多次调用 register_shutdown_function() ,这些被注册的回调会按照他们注册时的顺序被依次调用。 如果你在注册的方法内部调用 exit(), 那么所有处理会被中止,并且其他注册的中止回调也不会再被调用。

Note:

如果进程被信号SIGTERM或SIGKILL杀死,那么中止函数将不会被调用。尽管你无法中断SIGKILL,但你可以通过pcntl_signal() 来捕获SIGTERM,通过在其中调用exit()来进行一个正常的中止。

根据解释,register_shutdown_function注册的函数将在脚本执行完成或者exit后被调用。但是SIGTERM/SIGKILL等信号,不会回调注册的函数,并且经过测试ctrl+c(发出SIGINT信号)也不会触发回调。

脚本未正常执行完被终止,该如何处理?根据文档提示,需要使用pcntl_signal函数捕捉信号,再调用exit函数让脚本正常退出,此时register_shutdown_function注册的函数会被正常回调。

将示例函数的方法改成:

然后在脚本执行过程中,按下ctrl_+c,或者通过kill命令发送信号,日志都正常输出,表明register_shutdown_function中的回调函数被正常调用。

总结

发现的两个问题,第一个并非Yii2的bug,而是未全面理解文档导致(Logger类的flushInterval和具体Target类的exportInterval都需要设置一个合适的值,才能及时查看消息);第二个问题有点蛋疼,应该算PHP的坑。好在非命令行情况下,pcntl拓展不可用,在web开发中不会出现类似问题。

参考

  1. http://www.yiiframework.com/doc-2.0/guide-runtime-logging.html
  2. http://php.net/manual/zh/function.register-shutdown-function.php
  3. http://php.net/manual/zh/function.pcntl-signal.php
  4. https://stackoverflow.com/questions/3909798/phps-register-shutdown-function-to-fire-when-a-script-is-killed-from-the-comman

关于操作系统的三篇好文分享

转载请注明出处:https://tlanyan.me/share-three-posts-on-os/

我承认,对操作系统的底层运行原理一直是好奇的。但是市面上的大部分教材,千篇一律的从操作系统管理的各个部分大谈设计之道,风格沉闷乏味。对于我这种工作在应用层,缺乏操作系统基础(尤其是硬件操作)的人来说,一篇深入浅出讲解某个点,启发你独立思考的博文会是非常棒的学习资料。

昨天在朋友圈看到某位朋友分享的关于操作系统的文章,感觉十分入味。顺着原链找到原作者的博客,发现此篇文章写于2014年,近期才被翻译成中文。接着查看作者其他的文章,找到了另外两篇都关于操作系统的好文。本来有翻译的想法,保险起见先查查是否已有中文版,发现都已经翻译成中文(最近一篇是2月1日翻译完的)。

前人已经打好基础,我等只需好好学习。先说一下作者信息:三篇博文的作者都是Gustavo Duarte,其个人博客地址是https://manybutfinite.com(原博客链接http://duartes.org会自动跳转到新网站)。

本文是对这篇博文的分享,希望对需要的人有帮助。

第一篇: 操作系统何时运行?原文链接:https://manybutfinite.com/post/when-does-your-os-run/,写于2014年10月28日,中文版链接:https://linux.cn/article-9095-1.html

第二篇: 当CPU空闲时它都在做什么?原文链接:https://manybutfinite.com/post/what-does-an-idle-cpu-do/,写于2014年10月29日,中文版链接:https://linux.cn/article-9303-1.html

第三篇: 系统调用让这个世界运转。原文链接: https://manybutfinite.com/post/system-calls/,写于2014年11月6日,中文版:http://blog.csdn.net/monkeynote/article/details/45771121

php://output和php://stdout的区别

转载请注明文章出处:https://tlanyan.me/php-output-vs-stdout/

PHP包含了以php://开头的一系列输出输出流,如php://stdin, php://stdout等。今天查看代码时,忽然想到一个问题:php://output和php://stdout有什么区别?

从PHP的官方文献中找答案,对输入流php://stdin和php://input的解释分别如下(输出流的解释过于简略):

php://stdin

php://stdin, php://stdout and php://stderr allow direct access to the corresponding input or output stream of the PHP process. The stream references a duplicate file descriptor, so if you open php://stdin and later close it, you close only your copy of the descriptor-the actual stream referenced by STDIN is unaffected. Note that PHP exhibited buggy behavior in this regard until PHP 5.2.1. It is recommended that you simply use the constants STDIN, STDOUT and STDERR instead of manually opening streams using these wrappers.

php://stdin is read-only, whereas php://stdout and php://stderr are write-only.

php://input

php://input is a read-only stream that allows you to read raw data from the request body. In the case of POST requests, it is preferable to use php://input instead of $HTTP_RAW_POST_DATA as it does not depend on special php.ini directives. Moreover, for those cases where $HTTP_RAW_POST_DATA is not populated by default, it is a potentially less memory intensive alternative to activating always_populate_raw_post_data. php://input is not available with enctype=”multipart/form-data”.

文档并未直接阐述两者的区别,仔细对比可得出以下信息:1. 均是只读流; 2. php://stdin是PHP进程的标准输入,php://input用来读取请求正文的原始数据。通过这些信息,该如何正确认识两者的本质区别?

顺着php://stdin进程输入的提示,联想PHP进程的执行过程,再结合SAPI的差异,可以得到两者主要区别:php://stdin是PHP进程的输入流,执行生命周期内均可能有数据流入(例如CLI下的交互式输入);php://input是PHP执行时的外部输入流,一般数据只能读一次(具体看SAPI的实现)。同理可得到php://stdout和php://output的区别:php://stdout是PHP进程的标准输出流,php://output是返回的结果数据流。

下面用代码验证结论:

命令行执行文件,输出如下:

浏览器端请求,输出如下:

在命令行下,PHP进程的标准输出流和结果输出流均指向终端,所有消息都打印出来。在浏览器端,PHP进程的输出流被忽略,只有结果数据流被发送到web服务器。同时,print和echo调用的信息都作为执行结果发往结果输出流,所以都正常显示。

最后再感慨一下PHP内置函数的简洁实用,一个file_put_contents函数就搞定流写入操作,换Java需要stream/writer一堆代码,也省去C风格的fopen/fwrite/fclose的繁琐。

参考

  1. http://php.net/manual/en/wrappers.php.php

使用PHPExcel读写excel

转载请注明文章出处:https://tlanyan.me/use-phpexcel-to-read-and-write-excel/

PHPOffice出品的PHPExcel是PHP读取和生成Excel的极佳工具。本文参考官方文档,对PHPExcel进行简要总结,希望对使用PHPExcel操作Excel的同行有帮助。

PHPExcel介绍

PHPExcel是用PHP实现的电子表格文档读写类库,其支持的文档类型包括:Excel(.xls)后缀,Excel 2007(.xlsx后缀),CSV(.csv后缀),LibreOffice Calc(.ods后缀),PDF和HTML等格式(某些格式只能读)。PHPExcel运行环境为PHP 5.2+,需要开启php_zip、php_xml和php_gd2拓展。

细心的读者可能看到PHPOffice有另外一款作品:PHPSpreadsheet。PHPSpreadsheet也是一个Excel读写类库,与PHPExcel主要区别是:

  1. PHPSpreadsheet是PHPExcel的重构版,基于PHP的新特性进行了重写。PHPSpreadsheet要求PHP 5.6+,使用了名字空间、PSR2编码规范、最新的PHP语言新特性;
  2. 对PHP版本的要求加强。官方的PHP版本支持结束后,PHPSpreadsheet对该版本至多额外支持6个月(意味肯定不支持PHP 5.5及以下版本,PHP5.6的支持也即将终止)。对比之下,PHPExcel依然支持PHP 5.2.0;
  3. 开发组已将所有资源转移到PHPSpreadsheet,PHPExcel的维护已经停止。

PHPSpreadsheet已经放出1.0.0稳定版,官方不再建议使用PHPExcel。本文内容主要讲解PHPExcel,掌握透彻后再转换到PHPSpreadsheet也是很容易的。

PHPExcel架构

理解PHPExcel的架构,可以先从理解Excel文件的结构开始。一个Excel文件包含多个表单,每个表单包含多个单元;文件、表单和单元都可以单独设置属性。这些概念对应到PHPExcel中的类,关系如下:

  • PHPExcel类 < -> Excel文件
  • PHPExcel_Worksheet类 < -> 表单
  • PHPExcel_Cell < -> 单元
  • PHPExcel_DocumentProperties < -> 文件属性
  • PHPExcel_Style_* < -> 格式设置类

下面开始介绍PHPExcel的常用操作。

使用PHPExcel

根据上面介绍的关系,分excel文件、表单、单元、格式设置四个部分分别介绍PHPExcel的使用方法。

excel文件

一个PHPExcel类的实例代表一个excel文件。新生成的PHPExcel对象,经常需要保存为文件;反之excel文件常需要导入为PHPExcel实例。保存和导入的行为分别由writer和reader负责。为了正确导入和保存数据,reader和writer需要知道具体的文件格式。PHPExcel提供了工厂类PHPExcel_IOFactory简化reader和writer的创建。读写文件的示例代码如下:

如果知道具体格式,可以使用具体的类操作:

可用的reader和writer类可以参考下图:

enter image description here

建议使用工厂方法读取文件,它能自动探测文件格式并加载。这在读取用户上传不同格式的文件时很有用,避免了格式与后缀名不符可能导致的错误。

注意不要混淆PHPExcel和writer/reader对象:PHPExcel持有数据,writer和reader是对其进行序列化和反序列化的辅助类。

表单操作

一个excel文件可以包含多个表单,常用操作包括读取、新建、复制和删除表单。表单从属于excel文件,一般需要挂载到具体的PHPExcel对象上。

获取表单的方式有多种,如获取当前表单、获取指定顺序表单、根据名字获取表单。以下是示例代码:

创建表单分为直接excel文件对象直接创建,也可以先创建表单实例,后续再关联。对应方法为:

PHPExcel也支持复制表单(包括复制其他PHPExcel对象中的表单):

删除表单的API比较简单,只提供了removeSheetByIndex一个方法:

单元操作

单元是承载内容的主体,其上操作比较复杂,大部分的类和API都与单元相关。单元隶属于具体的表单,使用上和表单类互动最多。

常用操作的包括定位、取值/赋值、格式化等。下面是一些代码示例:

文件属性

设置excel文件的属性,包括常见的作者、标题、创建时间、描述等。该功能由PHPExcel中类型为DocumentProperties的成员变量负责:

其他

上述介绍了常见的概念和操作,实际中可能会用到的概念还包括:

  • 缓存和性能
  • 图像、图表、超链接等富文本
  • 日期、货币等格式化和本地化
  • 公式设置
  • 打印属性设置
  • 内容对其、边距设置等
  • 文件密码安全设置

这些冷门或高级功能可以参照API文档。

PHPExcel官方文档可能稍有繁杂,网络上的二手资料在深入方面常有欠缺。要用好PHPExcel,一个基本功是搞清楚操作的对象,以及和其他类/对象的关系(这也是面向对象编程的基本功)。本文中提到的PHPExcel->PHPExcel_WorkSheet->PHPExcel_Cell继承体系,是使用过程中操作最为频繁的对象,希望以上说明和示例能加深读者对PHPExcel类库的理解。

参考

  1. https://github.com/PHPOffice/PHPExcel

Yii2中的事务

转载请注明来源:https://tlanyan.me/yii2-transactions/

今天运行程序时发现有条数据不完整。出现问题的数据属于某个事务,按道理要么逻辑走完数据提交,要么回滚。出现预料外问题,第一个反应是ActiveRecord中内嵌事务会单独提交到数据库中?为了验证这个问题,抽空写了一个测试用例验证。

准备工作

先建立两个表 foo1foo2

创建相应的ActiveRecord类,并定义好规则:

编写测试用例

为了彻底搞清楚Yii2中事务的执行情况,总共编写了六个例子。六个示例的作用分别是:

  1. 非事务保存、数据校验不通过
  2. 事务保存、数据校验不通过
  3. 校验通过、多模型数据保存
  4. 某条数据校验不通过
  5. 某条数据插入冲突
  6. 事务执行中exit/return

测试例子的代码如下:

执行结果

依次执行上述测试用例,结果如下:

  • case1: 输出”transaction committed”,数据未插入;
  • case2: 输出”transaction committed”,数据未插入;
  • case3: 输出”insert data error:SQLSTATE[23000]: Integrity constraint violation:1062 Duplicate entry ‘12345678’ for key ‘data1’
    The SQL being executed was: INSERT INTO foo1 (data1, value) VALUES (‘12345678’, ‘1245677553’)”,数据未插入;
  • case4: 输出”transaction committed”,foo1中的数据成功插入;
  • case5: 输出”insert data error:SQLSTATE[23000]: Integrity constraint violation:1062 Duplicate entry ‘12345678’ for key ‘data2’
    The SQL being executed was: INSERT INTO foo2 (data2, value) VALUES (‘12345678’, ‘1245677553’)”,数据未插入;
  • case6: 输出”exit now”,数据未插入。

分析

大部分示例的结果在预料之中,震惊的是case2和case4的结果。之前一直以为只要包裹在事务中,并且在transactions方法中声明了所在场景启用事务,数据保存出错就会抛异常,数据回滚。这个测试彻底颠覆了我的认知。

为了搞清楚执行机制,开始跟踪Yii2执行数据保存的源码。首先查看ActiveRecord基类BaseActiveRecord中的save方法:

save方法根据是否新数据,走插入或更新流程。继续跟踪insert方法(定义在yii\db\ActiveRecord中):

insert方法的实现代码解决了我的疑问:数据的规则验证不通过,直接返回false,不会抛异常。

再看保存过程:如果当前场景未声明事务,常规保存;事务保存第一步还是尝试常规保存,如果失败,回滚并抛出异常;如果事务保存成功,提交事务。

到这一步,Yii中事务处理已经比较清晰了。剩下的问题是:嵌套事务如何处理?继续跟踪yii\db\Transaction中的commit方法:

代码中出现事务的层级(level),结合begin方法,每嵌套一层事务,level加一并创建savepoint。事务提交时,如果是最外层事务,直接提交到数据库;如果是内嵌事务,释放savepoint或什么都不做。所以嵌套事务的疑问也解决了:内嵌事务不会单独提交。

总结

通过这次测试和源码跟踪阅读,对Yii的事务了解又深入一步。最大的收获是:事务开始前调用validate方法先校验数据,无错误时再通过事务中调用save(false)方法插入数据,此时出错才会抛出异常。

Scroll Up