在 lumen 中设置日期与数据库时区

项目使用了 Laravel 的精简版 Lumen 来做 api 和 sso 的服务。今天在提交代码时发现,生成的 database migration 代码时间相差八个小时。看来是时区设置的问题,查找了一下,发现有好几个地方都有时区的设置,那就简单总结一下。

PHP 时区设置

PHP 里可以在 php.ini 中设置 date.timezone 选项来设置时区,也可以在脚本中动态制定时区,如使用 date_default_timezone_set() 来动态指定时区。使用脚本指定时会覆盖 php.ini 中的设置。时区列表可以使用 \DateTimeZone::listIdentifiers() 来获取。也可以到 timezonemap.h 来查看对应的可选项。

中国的时区设置可以使用以下选项:

1
2
3
4
5
6
7
8
9
{ "cst",   0,  28800, "Asia/Chongqing"                },
{ "cst", 0, 28800, "Asia/Chungking" },
{ "cst", 0, 28800, "Asia/Harbin" },
{ "cst", 0, 28800, "Asia/Macao" },
{ "cst", 0, 28800, "Asia/Macau" },
{ "cst", 0, 28800, "Asia/Shanghai" },
{ "cst", 0, 28800, "Asia/Taipei" },
{ "cst", 0, 28800, "PRC" },
{ "cst", 0, 28800, "ROC" },

lumen 时区设置

lumen 的时区设置有数据库操作的时区设置以及与使用时间相关的函数的时区设置。

日期相关设置

在 lumen 中应用程序的入口,会 new 一个 Application 实例,这个实例会读取 env 配置文件中的 APP_TIMEZONE 配置,若没有配置则会使用 UTC 时间。Application 位于 /path/to/project/vendor/laravel/lumen-framework/src/Application.php 。所以最好是在 .env 中设置时区 APP_TIMEZONE=PRC

数据库

数据库的时区设置可以在 config 的 database 文件中设置。database 文件分两个,一个是框架级别,一个是项目级别。框架级别的文件位于 /path/to/project/vendor/laravel/lumen-framework/config/database.php ,而项目级别的文件位于 /path/to/project/config/database.php

此配置是和 MySQL 相关的,所以关键字当然是去 MySQL 中找。在 MySQL 时区设置 文档中可以看到,有以下三种配置

1
2
3
1. SYSTEM 表示与系统时区相同
2. '+10:00' or '-6:00' 表示与 UTC 时间的一个偏移量
3. 'Europe/Helsinki', 'US/Eastern', or 'MET' 表示命名时区,命名时区必须在 mysql 库下的 time_zone_name 有注册

导入时区的命令为

1
shell> mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root --database=mysql -p

建议是在项目级别设置时区。在 .env 中指定 DB_TIMEZONE=+08:00 和在 database 中指定 'timezone' => env('DB_TIMEZONE', '+08:00') 均可。

总结

时区设置分为 PHP 时区设置和 MySQL 的时区设置。PHP 时区设置涉及到 php.ini 配置以及运行时配置,而在 lumen 中还涉及到环境变量的配置。lumen 中既可以在 config 目录下的文件中指定,也可以在 .env 文件中指定。建议既指定 PHP 的配置又指定环境变量的配置,环境变量的配置可供整个应用程序使用,是项目级别的共享配置。

反引号 (`) 与 单引号 (') 以及 双引号 ('') 的使用

反引号 ` (backtick) 与单引号 '(single quote) 以及双引号 " (double quote) 在 PHP 及 MySQL 中都有不同的含义,下面将分别说明并给出它们在 PHP 和 MySQL 结合使用时的一些注意事项。

在 PHP 中

` 在 PHP 中是 执行运算符,效果与调用 shell_exec() 相同 。

' 在 PHP 中用来输出字面量值,被单引号括起来的内容会被原样输出。

" 在 PHP 中可以用来解析变量。

在 MySQL 中

` 被用来在查询,告诉解析器反引号内的内容表示一个字面量,直接读取而不用做变量替换。

"' 用来均被用来解析 MySQL 字符串及 特殊字符 ,不过这与 MySQL 的 SQL mode 设置有关。当 ANSI_QUOTES 开启时," 将被当作标识符。

SQL mode

MySQL 中开启 ANSI mode 时将会使用 ' 来告诉解析器,' 内包裹的字符串字面量直接读取,无需解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> SET SESSION sql_mode ='ANSI_QUOTES';
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT 'hello', '"hello"', '""hello""', 'hel''lo','\'hello','\r';
+-------+---------+-----------+--------+--------+---+
| hello | "hello" | ""hello"" | hel'lo |'hello | |
+-------+---------+-----------+--------+--------+---+
|hello | "hello" | ""hello"" | hel'lo |'hello |
+-------+---------+-----------+--------+--------+---+
1 row in set (0.01 sec)

mysql> SELECT "hello", '"hello"', '""hello""', 'hel''lo','\'hello','\r';
ERROR 1054 (42S22): Unknown column 'hello' in 'field list'

mysql> SELECT hello, '"hello"', '""hello""', 'hel''lo','\'hello','\r';
ERROR 1054 (42S22): Unknown column 'hello' in 'field list'

mysql> SELECT `hello`, '"hello"', '""hello""', 'hel''lo','\'hello','\r';
ERROR 1054 (42S22): Unknown column 'hello' in 'field list'

PHP 执行 MySQL 查询

为了防止 SQL 注入,最好使用预处理语句及参数绑定来执行查询。建议使用 PDO 或者 MySQLi

  • PDO

    1
    2
    3
    4
    5
    <?php    
    $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
    $stmt = $dbh->prepare("INSERT INTO REGISTRY (name, value) VALUES (:name, :value)");
    $stmt->bindParam(':name', $name);
    $stmt->bindParam(':value', $value);
  • MySQLi

    1
    2
    3
    4
    <?php
    $mysqli = new mysqli("localhost", "user", "password", "database");
    $stmt = $mysqli->prepare("INSERT INTO `test`(`id`) VALUES (?)");
    $stmt->bind_param("i", $id)

实践

SQL 标准规定使用 ' 来包裹字符串,但不同的数据库有不同的实现,最好的方式,当然是使用 PDO ,这样就不用对 sql 语句单独进行处理,而且在转换数据库时业务逻辑中的 sql 也无需修改和转换。

最好的实践其实应该在代码设计上规避错误:

  1. 取名不与 MySQL 保留字 冲突
  2. 清楚 MySQL 服务器 SQL mode SELECT @@sql_mode
  3. 使用 ` 包裹表名及列名
  4. 使用 ' 包裹查询条件

解决 PHP "headers already sent" 错误

这篇文章是 stackoverflow 上的一个问题的回答,答案集中了其它答案的想法,并做了归纳,所以翻译过来,以后遇到 “headers already sent” 的错误就可以直接一一排查,或者干脆在代码动手及设计上避免这类问题了。

发送 header 前不要有任何输出

发送或者修改 HTTP 头信息的方法必须在任何输出被输出之前被调用。否则调用将会出错:

Warning: Cannot modify header information - headers already sent (output started at script:line)

这些方法可以修改(modify) HTTP 头信息:

输出(output)可以是:

  • 无意的:
    • <?php 之前或者 ?> 之后的空格
    • UTF-8 BOM
  • 有意的:
    • printecho 以及其他能产生输出的方法
    • <?php 前原始的 <html> 区块

为什么这个错误会产生

为了理解为什么 HTTP header 必须在输出之前发送出去,我们有必要了解看一下一个典型的 HTTP 相应。PHP 脚本主要用来生成 HTML ,但它也会发送一系列的 HTTP/CGI 头信息到 web 服务器:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Powered-By: PHP/5.3.7
Vary: Accept-Encoding
Content-Type: text/html; charset=utf-8

<html><head><title>PHP page output page</title></head>
<body><h1>Content</h1> <p>Some more output follows...</p>
and <a href="/"> <img src=internal-icon-delayed> </a>

页面或者输出总是紧跟在头信息后面。PHP 必须先把头信息发送给 web 服务器,并且它只能发送一次,在这之后就再也不能修改头信息了。

当 PHP 第一次接收到输出时(print ,echo,<html>) 它会清掉所有收集到的头信息。在此之后它能把输出所有想输出的内容,但是再想发送 HTTP 头信息就不可能了。

怎么找到到底是哪里提前产生了输出?

header() 头信息包含所有与问题产生相关的信息:

Warning: Cannot modify header information - headers already sent by (output started at /www/usr2345/htdocs/auth.php:52) in /www/usr2345/htdocs/index.php on line 100

在上面的警告中,line 100 指向调用 header() 失败的脚本行数。

圆括号里的 output started 这条信息更加重要。它指出了先于 header() 前的输出的源头。在这个例子中是 auth.php 的 第 52 行,这就是你要去找的过早的输出的地方。

典型的原因有这些:

  1. print,echo
    有意的 printecho 语句输出将会中断输出 HTTP 头信息的机会。应用程序流必须重组以避免这种行为,可以使用 function 和模版来重组,从而保证 header() 调用是在信息被写出之前。
    产生输出的方法包括:

    • print, echo, printf, vprintf
    • trigger_error, ob_flush, ob_end_flush, var_dump, print_r
    • readfile, passthru, flush, imagepng, imagejpeg

    以及其他用户自定义的方法。

  2. 原始的 HTML
    在一个 PHP 文件中未被解析的 HTML 区块也是输出。脚本中各种可能触发调用 header() 的条件都必须在任何 <html> 区块前声明。

  3. <?php 前的空格导致的 "script.php line 1" 警告
    如果警告指向第 1 行的输出,那么它很有可能指向的是在 <?php 之前的空格,文本或者 HTML 。

    1
    2
     <?php
    // 在 <?php 前有个空格

    同样它可能出现在附加的脚本或者脚本区块上:

    1
    2
    3
    ?>

    <?php

    PHP 确实在闭合标签后占据了一个换行符,但是它不会在上面的空白处插入换行符、制表符或者空格(也就是说这是我们自己造成的)。

  4. UTF-8 BOM
    换行符或者空格可能导致问题,但是不可见的字符序列同样可以。最著名的就是大多数文本编辑器并不会显示的 UTF-8 BOM 。它是在 UTF-8 编码的文档里可选甚至是多余的,被标示为 EF BB BF 的字节序列。但是 PHP 必须把它当作原始的输出来处理。它可能以  这样的符号输出(如果客户端以 Latin-1 来解释这个文档)或者其他这样的“非法输出”。
    以某种图形化的编辑器或者基于 JAVA 的 IDE 查看这类文件时,你可能察觉不到 UTF-8 BOM 的存在。它们没有把 UTF-8 BOM 形象化(受制于 Unicode 标准)。然而大多数程序编辑器和控制台编辑程序会这样处理:

    像这样就能简单地提早发现问题了。其他的编辑器在设置某些选项后也能纠正这样的问题(Windows 上的 Notepad++ 可以识别并且 纠正 BOM 问题 ),另一个发现 BOM 的方法就是借助十六进制的编辑器。在 *nix 系统上,大都提供了 hexdump ,如果没有的话,其他图形化的变种也可以用来简化审计这些问题的步骤:

    一个简单的修正方法就是将文本编辑器设置为 以 UTF-8 (no BOM) 保存文件save files as UTF-8 (no BOM))或者其他类似的设置。

    修正程序

    有很多自动化的工具可以检测并修改文本文件(sed / awk 或者 recode )。PHP 里有 phptags 。它可以把打开标签和关闭标签重写成长标签(<?php)或者短标签(<?)的形式。也可以轻松地解决前导或尾随的空格、Unicode 和 UTF-x BOM 问题:

    1
    phptags  --whitespace  *.php

    同样,你可以在某个目录或整个项目目录使用这个命令。

  5. ?> 后的空白
    如果错误代码在闭合标签 >? 这一行的前面,那么这就是 >? 后的空格或者原始文本输出导致的问题。PHP 的结束标记并不会在遇到闭合标签时终止执行脚本,任何 ?> 之后的文本或者空格字符都会被当作页面内容输出。
    通用的被鼓励的做法,特别是针对新手,是避免在 PHP 文件后加上闭合标签 ?> 。这样就能避免一部分产生这类问题的情况。

  6. 错误源提示:”Unknown on line 0”
    如果没有给出具体的错误源,那么这就是典型的 PHP 扩展或者 php.ini 设置的问题:

    • 偶尔是 gzip 编码设置或者是 ob_gzhandler
    • 也有可能是 php.ini 设置里模块加载了两次导致 PHP 产生了启动 / 警告信息
  7. 先前的错误导致输出了错误信息
    如果前面的 PHP 语句或者表达式造成了 warning 或者 notice 信息导致输出,这些输出也被认为是过早地输出。
    在这种情况下你需要避免错误,推后这些语句的执行,或者抑制这些信息的输出,可以使用 isset() 进行判断,或者使用抑制符 @,前提是它们不会阻止后续的调试。

没有错误信息输出

如果你禁用了 php.ini 里的 error_reporting 或者 display_errors 设置,那么将不会产生 warning 。但是忽略错误并不会让问题消失,头信息仍然不能在过早的输出输出之前发送出去。

所以当 header("Location: ...") 跳转静默地失败时,建议你去查看 warnings 。在脚本的最前面用下面的两条命令重新开启错误报告设置:

1
2
error_reporting(E_ALL);
ini_set("display_errors", 1);

或者如果其他的设置都失败了那就设置 set_error_handler("var_dump");

至于跳转的 header ,在执行至最后的代码时你应该遵循下面的这种风格:

1
exit(header("Location: /finished.html"));

最好是提供一个方法,特别是当 header() 执行失败时打印出用户信息。

变通方法:输出缓冲

PHP 的输出缓冲的方法是缓解这种问题的一种变通方法。它运行起来可靠,但是你绝不要使用它来替代你架构良好应用程序结构,从控制逻辑中分离输出。它的真实目的是用来减轻大块数据传输至服务器时的压力。

  1. output_buffering 设置php.ini 或者 .htaccess 或者甚至在最新的 FPM/FastCGI 的 .user.ini 中设置;
  2. 同样你可以在脚本的最前面使用 ob_start() 来设置,但是它并没那么可靠:
    • 即使 <?php ob_start(); ?> 在第一个脚本里,空格或者 BOM 也有可能在此之前被输出
    • 它可以隐藏 HTML 输出里的空格(将空格放到 buffer 中),但是只要应用程序逻辑企图发送二进制内容(比如生成的图片),缓冲里的无关的输出就会成为问题(这样 ob_clean() 方法就成为下一步的变通方法了)。
    • 缓冲有大小限制,并且在默认配置下很容易超出,并且这种情况并不少见,一旦发生也不太容易追踪。

因此这两个方法变得不可靠了,特别是当你需要更改开发环境或者生产环境的配置的时候。这就是为什么输出缓冲被认为只是一种蹩脚的变通方法。

建议参考官方手册里的基本 使用方法 ,以及它的优缺点:

但是在其他的服务器上是好的?

如果你之前没有收到过头信息的 warning ,那么 php.ini 里的 output_buffering 设置改变了。在现在的/不同的服务器上很有可能没有设置。

使用 headers_sent() 检查

你可以使用 headers_sent 来检查是否可以发送头信息。这种方法可以有效地检查以便输出一个错误信息或是应用其他的逻辑。
不错的回退变通方法有:

  • HTML<meta> tag
    如果你的应用程序很难在结构上解决这个问题,有个简单但显得不专业的做法是在 HTML 标签中来跳转网页。可以这样实现:

    1
    <meta http-equiv="Location" content="http://example.com/">

    或者加上一个延迟时间

    1
    <meta http-equiv="Refresh" content="2; url=../target.html">
  • JavaScript 跳转
    另一个可选的方法就是使用 JavaScript 跳转 来实现网页跳转:

    1
    <script> location.replace("target.html"); </script>

这种方式相比较 方法起来更兼容 HTML 标准,它只依赖于可以运行 JavaScript 的客户端。

这两种方式在 HTTP header() 调用失败时都提供了可以接受的回退方式。理想化的处理方式应该是将跳转与其它方式结合,给出对用户友好的辅助信息并且提供一个可点的链接以供后续操作。

为什么 setcookie()session_start() 都会被影响

setcookie()session_start() 都需要发送一个 set-cookie: 的 HTTP 头信息。这种情况就和前面输出 header() 的情况类似,所以同样会出现由于过早地输出错误信息导致的错误。

(当然它们受影响也有可能是因为客户端禁止了 cookie 导致的,设置可能是代理的问题。很明显,session 也取决去剩余磁盘空间大小或者 php.ini 里的其它设置)


原文链接:http://stackoverflow.com/questions/8028957/how-to-fix-headers-already-sent-error-in-php

理解 Exit Code 并学会如何在 Bash 脚本中使用

Exit Codes 是什么

在 Unix 和 Linux 系统中,程序可以在执行终止后传递值给其父进程。这个值被称为退出码(exit code)或退出状态(exit status)。在 POSIX 系统中,惯例做法是当程序成功执行时传递 0 ,当程序执行失败时传递 1 或比 1 大的值。

传递状态码为何重要?如果你在命令行脚本上下文中查看状态码,答案显而易见。任何有用的脚本,它将不可避免地要么被其他脚本所使用,要么被 bash 单行脚本包裹所使用。特别是脚本被用来与自动化工具 SaltStack 或者监测工具 Nagios 配合使用。这些工具会执行脚本并检查它的状态,来确定脚本是否执行成功。

其中最重要的原因是,即使你不定义状态码,它仍然存在于你的脚本中。如果你不定义恰当的退出码,执行失败的脚本可能会返回成功的状态,这样会导致问题,问题大小取决于你的脚本做了什么。

如果不指定退出码会发生什么

在 Linux 里,任何在命令行中执行的脚本都有退出码。在 Bash 脚本中,如果脚本里没有指定退出码,退出码将会是脚本最后一个命令执行后产生的状态码。为了更好地解释退出码,下面将给出一个脚本来说明。

脚本

1
2
3
#!/bin/bash
touch /root/test
echo created file

上面的脚本既会执行 touch 命令也会执行 echo 命令。当我们以非 root 用户执行这个脚本时 touch 命令将会执行失败,理想情况下,当我们执行 touch 命令失败时我们希望通过脚本的退出码来表明有命令执行失败。我们可以通过打印 Bash 的特殊变量 $? 来获取退出码。这个变量将会打印出脚本最后一个命令执行的退出码。

执行输出

1
2
3
4
5
$ ./tmp.sh 
touch: cannot touch '/root/test': Permission denied
created file
$ echo $?
0

你可以看到,当执行 ./tmp.sh 后退出码是 0 ,而 0 代表脚本执行成功,虽然 touch 命令执行失败了。上面的脚本执行了两个命令:touchecho。因为我们没有指定退出码所以脚本以最后一个命令执行后的状态码退出。在这个例子中,最后运行的是 echo 命令,这个命令确实执行成功了。

脚本

1
2
#!/bin/bash
touch /root/test

如果我们去掉脚本中的 echo 命令,我们将看到 touch 命令的退出码。

执行结果

1
2
3
4
$ ./tmp.sh 
touch: cannot touch '/root/test': Permission denied
$ echo $?
1

你可以看到,因为最后运行的命令是 touch ,所以脚本的退出码正确地反应了脚本的状态:执行失败。

在你的 Bash 脚本中使用退出码

当从我们的脚本中去掉 echo 命令后脚本跑通了并且返回了恰当的退出码。当我们想在 touch 命令执行成功执行一个操作,而在执行失败时执行另一个操作会发生什么。比如脚本执行成功时输出至 stdout ,执行失败时输出至 stderr 这种操作。

测试退出码

前面的代码中,我们使用了特殊变量 $? 来打印脚本的退出码。我们同样可以在脚本中使用它来测试 touch 命令是否执行成功。

脚本

1
2
3
4
5
6
7
8
#!/bin/bash
touch /root/test 2> /dev/null
if [$? -eq 0 ]
then
echo "Successfully created file"
else
echo "Could not create file" >&2
fi

在上面的修改版代码中,如果 touch 的退出码是 0 ,脚本将会输出成功的消息。如果退出码是除 0 以外的其他数字,这表示执行失败,脚本将会打印失败的消息到 stderr

1
2
$ ./tmp.sh
Could not create file

在程序中提供你自己的退出码

在上面的程序中,虽然 touch 命令执行失败时将会提供一个错误信息提示,但它仍给出了表示执行成功的状态码 0 。

1
2
3
4
$ ./tmp.sh
Could not create file
$ echo $?
0

既然脚本执行失败了,但它仍然传递执行成功的退出码给其他需要执行此脚本程序显然很不适合。为添加我们自己的退出码到这个程序里,我们可以简单地通过 exit 来实现。

脚本

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

touch /root/test 2> /dev/null

if [$? -eq 0 ]
then
echo "Successfully created file"
exit 0
else
echo "Could not create file" >&2
exit 1
fi

通过脚本里的 exit 命令,我们可以在 touch 命令执行成功时输出成功的消息并返回状态码 0 。当 touch 执行失败时我们将打印失败的消息至 stderr 并返回一个表示失败的状态码 1 。

执行结果

1
2
3
4
$ ./tmp.sh
Could not create file
$ echo $?
1

在命令行中使用退出码

现在我们的脚本既可以通知用户也能通知程序命令是否执行成功,我们可以使用这个脚本配合其他的管理工具,或者简单地通过 Bash 单行命令使用它。
Bash One Liner:

1
2
3
4
$ ./tmp.sh && echo "bam" || (sudo ./tmp.sh && echo "bam" || echo "fail")
Could not create file
Successfully created file
bam

上面的命令组使用了在 Bash 里称为 list constructs 的工具。它允许你通过 &&(代表 and) 和 || (代表 or) 将命令串到一起。上面的命令将会执行 ./tmp.sh 脚本,如果退出码是 0 命令 echo "bam" 将被执行。但如果 ./tmp.sh 的退出码为 1 ,圆括号里的命令将在之后被执行。圆括号里的命令通过 &&|| 被串到一起。

list constructs 使用退出码来知晓一个命令是否执行成功。如果脚本不恰当地使用退出码,其他使用更高阶的命令(比如 list constructs)调用此脚本的用户就可能得到与预期不符的结果。

更多关于退出码的信息

Bash 里的 exit 命令接受不了从 0 - 255 的整形数值,大多数情况下 01 就够用了,不过其他的数值也提供了,为更具体的错误信息做储备。The Linux Documentation Project 有一个不错的 保留状态码 列表,告诉大家这些状态码作何使用。


原文链接:http://bencane.com/2014/09/02/understanding-exit-codes-and-how-to-use-them-in-bash-scripts/

使用 Chrome 扩展程序 JSON Viewer 进行调试

PHP 调试时有很多种方法,其中最简单的无非是 echo 与 var_dump 、 print_r 以及 debug_backtrace 了。这篇文章要介绍的是平常用的比较多的,适合在调 HTTP 接口以及平常开发中需要输出变量内容的情况。其实在 IDE 中比较适合使用 XDebug 或者断点调试,这样能在运行时直接查看当前上下文的各个变量,不过这种情况不在此篇讨论之列,后续我也将介绍相关的方法,来完善这个系列。

现在步入正题。在平常和前端的配合中经常会有一些 RESTful 接口输出,而其中最常用的内容传输格式就是 JSON ,JSON 在 PHP 中可以很方便地转化为对象或者数组,在打印变量时使用 JSON 再配合浏览器的扩展进行解析,无疑能提高开发效率。

JSON

先用两张图来了解一下 JSON 的格式,这样就不会混淆其在 PHP 中所代表的意思。

对象是一个无序的“‘名称 / 值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称 / 值’ 对”之间使用“,”(逗号)分隔。

数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。

值(value)可以是双引号括起来的字符串(string)、数值 (number)、true、false、 null、对象(object)或者数组(array)。这些结构可以嵌套。

Chrome 扩展

JSON viewer 扩展的地址是 https://chrome.google.com/webstore/detail/json-viewer/gbmdgpbipfallnflgajpaliibnhdgobh

JSON view GitHub 官网介绍了其有如下 feature

  • 语法高亮
  • 23 个内置的主题
  • 节点可折叠
  • 可点击的 url
  • 在控制台「console」输入 json 回车中查看你的 JSON (通过访问 id 为 json 的 HTML 元素)
  • 显示行号选项
  • 在 url 中加入时间戳 header

等等。

PHP 配合 JSON viewer 进行输出调试

PHP 中用来处理 JSON 使用最多的是 json_encode 和 json_decode ,比如你有 array 有如下结构

1
2
3
4
5
6
7
8
$arr = array(
'key1' => 1,
1 => 'string',
'array' => array(
'value1',
'value2',
)
);

如果使用 var_dump 输出,那么长的是这样

1
2
3
4
5
6
7
8
9
10
11
Array
(
[key1] => 1
[1] => string
[array] => Array
(
[0] => value1
[1] => value2
)

)

当层级不够多,数组元素不够多,肉眼观察根本不是问题,但是一旦数据变得复杂,输出将会非常难看且不易查找,这时候一个美观的输出外带查看层级和数据复制功能会显得很方便,此时,你只需要 echo json_encode($arr);exit; ,扩展会自动解析 JSON 。

最终你得到的可能是这样的 UI:

好了,其实还有很多输出调试工具,比如 symfony 的 var-dumper 既能输出 HTML 的调试信息,又能在 cli 下输出带格式的调试信息。还有 debug helper Kint 、Yii 的 yii-debug-toolbar 以及强大的 XDebug 。


FaceBook 命令行 UI PathPicker

早先在逛 Facebook 后端开源项目时看到了一个命令行工具 PathPicker ,安装后试用了一下感觉还不错,恰好项目开发时在使用 Git ,发现结合 PathPicker 后免去了很多麻烦。

试想以下场景:

  1. 你完成一个功能会涉及到不少文件,你得小心翼翼地一个个文件选择,防止不必要的文件出现在同一个待提交列表中;

  2. 你改了很多文件,然后执行了 git add . ,这时你有两个选择,要么 git rest 不想在这次提交中包含的文件,要么一个个选择,然后 添加到 git commit 的后面。

于是 PathPicker 出现了,专为解决此种费力不讨好的事情,UI 选择器,命令行,一起上。

介绍

FaceBook PathPicker 是一个简单的命令行工具,用来处理选择 bash 文件输出时的问题。

PathPicker 做以下事情:

  • 通过管道处理输入中所有表现出文本特性的文件
  • 在一个方便选择的 UI 容器里展现管道输入
  • 然后你可以对输入做以下事情:

    • 用你最喜欢的编辑器处理选择的所有文件
    • 利用输入执行任意命令

下面通过一段视频来了解 PathPicker:

PathPicker 是怎么运行的

PathPicker 是 bash 脚本和一些 Python 模块 结合后的产物。 它主要有以下三个步骤:

  1. 首先在 bash 脚本中,它将所有的标准输入重定向到一个 Python 模块,然后通过解析取出其中所包含的文件名候选列表。每个候选文件名都将被用来与文件系统做比对确保其存在,再然后,会把处理结果保存至临时文件并终止 python 脚本的运行。
  2. 第二部,bash 脚本会切换至命令行输入模式,并且另一个 python 模块会读取出已保存的条目并使用 curses 在一个 UI 选择器展现它们。
  3. 最后,python 脚本会输出一条命令至 bash 文件,最终被原始的 bash 脚本解析执行。

使用示例

当改了很多文件需要提交提交时,查看当前状态 git status

git add before

将输出重定向至 PathPicker git stataus | fpp

git status|fpp

  • 已选中为绿色
  • 已选中且当前光标悬停显示为红色
  • 未选中且当前光标悬停显示为蓝色
  • 无法选中的光标无法跳到此项,显示为纯文本

提交选定的文件 git commit $F

git commit $F

PathPicker 官网见 http://facebook.github.io/PathPicker/


Git 常用操作


Git 忽略已追踪文件的修改

git update-index –assume-unchanged /path/to/file

Git 取消忽略文件的修改(再次追踪此文件)

git update-index --no-assume-unchanged /path/to/file

导入 Git 仓库至 Bitbucket

  1. create repository in Bitbucket
  2. do upstreaming in local
1
2
3
4
cd /path/to/my/repo
git remote add origin https://username@bitbucket.org/username/reponame.git
git push -u origin --all # pushes up the repo and its refs for the first time
git push -u origin --tags # pushes up any tags`

分支重命名

  1. 修改当前分支 git branch -m <newname>
  2. 未提交至 remote git branch -m <oldname> <newname>
  3. 已提交至 remote 需新加分支 git push origin <newname>:<newname> 并删除旧的远程分支 git push origin :<newname>

从某个分支上线

场景:当需要从某个分支的指定版本上线,又需要保留当前分支的修改

  1. checkout 到指定分支 git checkout [revision] [revision] 代表指定 commit 的 hash
  2. 以当前 commit 创建分支 git checkout -b [branchName] [branchName] 为分支名
  3. 推送至远程 git push origin branchName:branchName

添加多个远程分支

1
2
git remote set-url origin --push --add <a remote>
git remote set-url origin --push --add <another remote>

错误 merge 了某个分支后撤销

场景:当某个功能开发并自测完毕,需要 merge 到测试环境进行测试时,如果不小心 merge 错了分支,这时候需要撤销这个 merge 。如果是使用 Pull Request 进行分支合并的话,直接 close 就可以了,但如果是使用命令行进行操作的话,本地的分支实际上已经与待 merge 的分支进行合并了,需要回到 merge 前。

只需要两个一个操作:找到 merge 前的 commit hash ,并 reset –hard

1
git reset --hard <commit hash>

强制提交,覆盖错误的提交

场景:如果不小心将某个提交推送到远程,又不想这个提交留在历史里,就需要强制覆盖远程。

1
2
git checkout -b <branchName> <commit hash> # commit hash 为错误提交的前一个提交 hash
git push origin branchName -f # 强制提交

备注:对于个人开发或者比较私密的信息不小心提交了才建议这么做,否则还是直接 revert 错误的提交。

MySQL 单机多实例主从

environment

[root@iZ25qtxg0q6Z ~]# uname -a
Linux iZ25qtxg0q6Z 2.6.32-431.23.3.el6.x86_64 #1 SMP Thu Jul 31 17:20:51 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
[root@iZ25qtxg0q6Z ~]# cat /etc/redhat-release
CentOS release 6.7 (Final)
[root@iZ25qtxg0q6Z ~]# mysql –version
mysql Ver 14.14 Distrib 5.1.73, for redhat-linux-gnu (x86_64) using readline 5.1

set-up replication steps

  1. enable binary logging of master
  2. stop writing FLUSH TABLES WITH READ LOCK;
  3. obtain binary log file name and position for slave
  4. transfer data from master to slave (mysqldump 、 mysqlimport)
  5. on the master UNLOCK TABLES;
  6. Setting the Replication Slave Configuration

    1
    2
    3
    4
    5
    6
    CHANGE MASTER TO
    MASTER_HOST='master_host_name',
    MASTER_USER='replication_user_name',
    MASTER_PASSWORD='replication_password',
    MASTER_LOG_FILE='recorded_log_file_name',
    MASTER_LOG_POS=recorded_log_position;

  7. check master and slave status SHOW VARIABLES LIKE 'server_id'

  8. in slave START SLAVE

安装过程中的命令及问题解决方法

mysql_secure_installation

mysql_secure_installation 作用:

  • set root password,
  • disallowing root login remotely,
  • removing anonymous user accounts after first installation
  • removing test database which can be accessed by any users

但是 mysql_secure_installation 无法指定命令参数(5.7 可以指定参数以后可以直接指定,之前的版本无法指定),会使用默认的配置,导致新安装的实例安全配置无法更新
配置位置 /var/lib/mysql/mysql2.sock

有两种方法可以解决执行 mysql_secure_installation 时应用到指定实例:
[root@iZ25qtxg0q6Z ~]# which mysql_secure_installation /usr/bin/mysql_secure_installation

  1. 在配置文件中制定,修改为对应的实例配置
  2. 使用 软链

启动 mysql instanse

mysqld_safe --defaults-file=/etc/my2.cnf

主从数据保持一致(导出导入)

  1. 导出数据 mysqldump --all-databases --master-data --events &gt; dbdump.db -p
  2. 导入至指定实例 mysqlimport -uroot -P 3380 -h 127.0.0.1 dbdump.db -p

stop mysql instance by specifing sockct

mysqladmin -S /var/lib/mysql/mysql2.sock shutdown -p

  1. mysql 安全设置
  2. 使用软链
  3. kill process
  4. config slave error handlering
  5. restore mysqldump file
  6. reset mysql root password
  7. create-multiple-mysql-instance
  8. multiple-mysql-servers
  9. Setting Up Replication with New Master and Slaves

MySQL 索引类型


有必要对数据库的索引类型及其实现做个了解及总结。

Question:

  1. 书中所说索引分为单列索引,主键索引以及联合索引;
  2. 在数据库的 GUI 软件中却可以对一个表设置三种类型的索引:分别为 normal、unique、full text;
  3. 在 MySQL manual 中创建 index 的语法为
    1
    2
    3
    4
    5
    CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name
    [USING index_type]
    ON tbl_name (index_col_name,...)
    index_col_name:
    col_name [(length)] [ASC | DESC]

Answer:

疑惑解决: 第一种分法是按照列的数量进行分类,第二、三种是按照索引类型进行分类

  • normal index 为普通索引
  • primary index 为主键索引,索引列的值必须唯一且不为空
  • unique index 为唯一索引,索引列必须唯一但可以为空
  • fulltext index 为全文索引,只能对 CHAR,VARCHAR 和 TEXT 列编制索引,并且只能在 MyISAM 表中编制
  • spatial index 为空间索引,只能对空间列编制索引,并且只能在 MyISAM 表中编制

有时间再对其具体实现及背后使用的算法做了解及总结。
可能的关键字如下:

  1. HASH
  2. B-TREE
  3. B+TREE

为网站全站链接添加参数

需求

为网站链接添加参数,可以统计用户在网站中的流向,以及确定网站经常访问的部分。

解决方案

  1. 在程序生成链接的时候附带转向,这样需要改变所有生成链接的地方
  2. 使用 jQuery 在网站加载时动态改变网站页面内所有链接

利弊

使用第一种方案时当需要改变某一页面的来源时,所有与本页面有关的链接都需要改动,好处是页面输出到浏览器后即为用户最终所得,不存在链接参数错误的问题。
使用第二种方案好处是修改页面链接时只需在页面标签的父级标签添加某一属性(如例子给出的 from),即可定位相当精准,但弊端在于此方法必须在页面 DOM 树加载完成之后执行,假如中途 DOM 树出问题或者标签属性输出不全可能导致链接错误,另外当标签内的链接由 JavaScript 单独进行定义时需额外修改,这是两种方案都要面对的问题。

使用 jQuery 实现的版本

思路:

  1. 查找所有 a 节点
  2. 获取其父类,一级一级往上找,使用 closest()查找,获取其 from 参数值
  3. 判断 a 节点的 href 属性是否包含?,包含则直接添加 &from ,否则添加?&from。如果添加 href 为 javascript: 或者 javascript:void(0) 时则不改变其 href 属性
    前提:链接不是 javascript: 且链接父级标签含有属性 from
    如网页链接为 www.jayxhj.com
    标签层级为
    <p from="top"> <a href="/test"></a> </p>

则执行 addparam()之后变为

<p from="top"> <a href="/test?f=top"></a> </p>

样例:www.baidu.com 转换后为 www.baidu.com?f=top
www.baidu.com?r=test 转换后为 www.baidu.com?r=test&f=top

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function addparam() {
$("a").each(function () {
var realhref = $.trim($(this).attr('href'));
var href = '';
if (realhref.indexOf('?') === -1) {
href = realhref;
} else {
if (realhref.indexOf('&') === -1) {
if (realhref.indexOf('f=') === -1) {
href = realhref;
} else {
var hrefarr = realhref.split('&');
var length = hrefarr.length - 1;
for (var i = 0; i < length; i++) {
href += hrefarr[i] + '&';
}
href = href.substring(0, href.length - 1);
}
} else {
var hrefarr = realhref.split('&');
if (realhref.indexOf('f=') === -1) {
href = realhref;
} else {
var length = hrefarr.length - 1;
for (var i = 0; i < length; i++) {
href += hrefarr[i] + '&';
}
href = href.substring(0, href.length - 1);
}
}
}
if (!~href.indexOf('javascript:') && href !== '') {
var parent = $(this).parents();
for (var j = 0; j <= parent.length; j++) {
if (typeof ($(parent[j]).attr('from')) != 'undefined') {
$(this).attr('href', href + (href.indexOf('?') === -1 ? '?' : '&' ) + 'f=' + $(parent[j]).attr('from'));
break;
}
}
}
});
}

具体可参看 股票雷达网站