Skip to content

依赖注入

12-10月-18

出自维基百科 Wikipedia 中的 依赖注入

依赖注入是一种允许我们从硬编码的依赖中解耦出来,从而在运行时或者编译时能够修改的软件设计模式。

我们可以用一个简单的例子来说明依赖注入的概念。

下面的代码中有一个 Database 的类,它需要一个适配器来与数据库交互。我们在构造函数里实例化了适配器,从而产生了耦合。这会使测试变得很困难,而且 Database 类和适配器耦合的很紧密。

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct()
    {
        $this->adapter = new MySqlAdapter;
    }
}

class MysqlAdapter {}

这段代码可以用依赖注入重构,从而解耦。

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct(MySqlAdapter $adapter)
    {
        $this->adapter = $adapter;
    }
}

class MysqlAdapter {}

现在,我们以参数的形式向 Database 类传递其依赖的对象,而不是让它自己产生所依赖的对象。我们甚至可以创建一个方法(method),让这个方法可以接受依赖对象作为参数并在内部设置依赖关系,或者,如果 $adapter 属性本身是 public 的,我们可以直接给它赋值。

控制反转

你可能见过 “控制反转”(Inversion of Control) 或者 “依赖反转准则”(Dependency Inversion Principle)这种说法。顾名思义,一个系统通过组织控制和对象的完全分离来实现 “控制反转”。对于依赖注入,这就意味着通过在系统的其他地方控制和实例化依赖对象,从而实现了解耦。

一些 PHP 框架很早以前就已经实现控制反转了,但是问题是,应该反转哪部分以及到什么程度?比如,MVC 框架通常会提供超类或者基本的控制器类以便其他控制器可以通过继承来获得相应的依赖。这就是控制反转的例子,但是这种方法是直接移除了依赖而不是减轻了依赖。

依赖注入允许我们通过按需注入的方式更加优雅地解决这个问题,完全不需要任何耦合。

依赖反转准则

依赖反转准则是面向对象设计准则 S.O.L.I.D 中的 “D”,倡导 “依赖于抽象而不是具体”。简单来说就是依赖应该是接口/约定或者抽象类,而不是具体的实现。我们能很容易重构前面的例子,使之遵循这个准则。

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct(AdapterInterface $adapter)
    {
        $this->adapter = $adapter;
    }
}

interface AdapterInterface {}

class MysqlAdapter implements AdapterInterface {}

现在 Database 类依赖于接口,相比依赖于具体实现有更多的优势。

假设你工作的团队中,一位同事负责设计适配器。在第一个例子中,我们需要等待适配器设计完之后才能单元测试。现在由于依赖是一个接口/约定,我们能轻松地模拟接口测试,因为我们知道同事会基于约定实现那个适配器

这种方法的一个更大的好处是代码扩展性变得更高。如果一年之后我们决定要迁移到一种不同的数据库,我们只需要写一个实现相应接口的适配器并且注入进去,由于适配器遵循接口的约定,我们不需要额外的重构。

容器

你应该明白的第一件事是依赖注入容器和依赖注入不是相同的概念。容器是帮助我们更方便地实现依赖注入的工具,但是他们通常被误用来实现反模式设计 Service Location 。把一个依赖注入容器作为 Service Locator 注入进类中隐式地建立了对于容器的依赖,而不是真正需要替换的依赖,而且还会让你的代码更不透明,最终变得更难测试。

大多数现代的框架都有自己的依赖注入容器,允许你通过配置将依赖绑定在一起。这实际上意味着你能写出和框架层同样干净、解耦的应用层代码。

注:本文非原创,原文出自 PHP 编程之道之依赖注入

一个面向对象留言本的实例

04-10月-18

用面向对象的思想完成一个简单的留言本模型,这个模型不涉及实际的数据库操作以及界面显示,只是一个 demo,用来演示面向对象的一些思维。

在面向过程的思维里,要设计一个留言本,一切都将以留言本为核心,抓到什么是什么,按流程走下来,即按用户填写信息→留言→展示的流程进行。

在面向对象的世界,会想尽办法把肉眼能看见的以及看不见的,但实际存在的物或者流程抽象出来。既然是留言本,那么就存在留言内容这个实体,这个留言实体应该包括留言者的姓名、E-mail、留言内容等要素,如下所示(留言实体类 message.php)。

<?php
class message 
{
    public $name;    // 留言者姓名
    public $email;   // 留言者联系方式
    public $content; // 留言内容    

    public function __set($name, $value)
    {
        $this->$name = $value;
    }

    public function __get($name)
    {
        if (! isset($this->$name)) {
            $this->$name = null;
        }
    }       
}

然后,需要一个留言本模型,这个留言本模型包括留言本的基本属性和基本操作,如下所示(留言本模型 gbookModel.php)。

<?php
/**
 * 留言本模型,负责管理留言本
 * @author asus
 *
 */
class gbookModel
{
    private $bookPath; // 留言本文件
    private $data;     // 留言数据

    public function setBookPath($bookpath)
    {
        $this->bookPath = $bookpath;
    }

    public function getBookPath()
    {
        return $this->bookPath;
    }

    public function open(){}

    public function close(){}

    public function read()
    {
        return file_get_contents($this->bookPath);
    }

    // 写入留言
    public function write($data)
    {
        $this->data = self::safe($data)->name . '&' . self::safe($data)->email . '\r\nsaid:\r\n' . self::safe($data)->content;
        return file_put_contents($this->bookPath, $this->data, FILE_APPEND); // 第三个参数代表,如果文件 filename 已经存在,追加数据而不是覆盖
    }

    // 模拟数据的安全处理,先拆包再打包
    public static function safe($data)
    {
        $reflect = new ReflectionObject($data);
        $props = $reflect->getProperties();
        $messagebox = new stdClass();
        foreach ($props as $prop) {
            $ivar = $prop->getName();
            $messagebox->$ivar = trim($prop->getValue($data));
        }
        return $messagebox;
    }

    public function delete()
    {
        file_put_contents($this->bookPath, '');
    }
}

实际留言的过程可能会更复杂,可能还包括一系列准备工作以及 Log 处理,所以应定义一个类负责数据的逻辑处理,如下所示(留言本业务逻辑处理 leaveModel.php)。

<?php
class leaveModel
{
    public function write(gbookModel $gb, $data)
    {
        $book = $gb->getBookPath();
        $gb->write($data);
        // 记录日志
    }
}

最后,通过一个控制器,负责对各种操作的封装,这个控制器是直接面向用户的,所以包括留言本查看、删除、留言等功能。可以形象理解为这个控制器就是留言本所提供的直接面向使用者的功能,封装了操作细节,只需要调用控制器的相应方法即可,如下所示(前端控制部分代码)。

<?php
class authorControl
{
    // 在留言本上留言
    public function message(leaveModel $l, gbookModel $g, message $data)
    {
        $l->write($g, $data);
    }

    // 查看留言本内容
    public function view(gbookModel $g)
    {
        return $g->read();
    }

    public function delete(gbookModel $g)
    {
        $g->delete();
    }
}

测试代码如下:

<?php   
$message = new message();
$message->name = 'phper';
$message->email = 'phper@php.net';
$message->content = 'a crazy phper love php so much.';
// 新建一个留言相关的控制器
$gb = new authorControl();
// 拿出笔
$pen = new leaveModel();
// 翻出笔记本
$book = new gbookModel();
$book->setBookPath("./a.txt");
$gb->message($pen, $book, $message);
echo $gb->view($book);

Crontab 定时任务命令

03-10月-18

检查 crontab 工具是否安装:crontab -l

检查 crond 服务是否启动:service crond status

命令基本形式:分 小时 日 月 星期 命令

// 每晚的 21:30 重启 apache

30 21 * * * service httpd restart

// 每月 1、10、22 日的 4:45 重启 apache

45 4 1,10,22 * * service httpd restart

// 每月 1 到 10 日的 4:45 重启 apache

45 4 1-10 * * service httpd restart

// 每隔两分钟重启 apache 

*/2 * * * * service httpd restart

// 每隔两小时重启 apache 

0 */2 * * * service httpd restart

// 晚上 11 点到早上 7 点之间,每隔一小时重启 apache

0 23-7/1 * * * service httpd restart

// 每天 18:00 至 23:00 之间每隔 30 分钟重启 apache

0,30 18-23 * * * service httpd restart

0-59/30 18-23 * * * service httpd restart

// crontab 的日志
cd /var/log
ls -l cron*
tail -f cron    // cron 代表当天执行的定时任务日志

Git 常用命令

30-9月-18

Git 和其它版本控制系统(包括 Subversion 和近似工具)的主要差别在于 Git 对待数据的方法。 概念上来区分,其它大部分系统以文件变更列表的方式存储信息。 这类系统(CVS、Subversion、Perforce、Bazaar 等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。

Git 更像是一个小型的文件系统,提供了许多以此为基础构建的超强工具,而不只是一个简单的 VCS。

// 列出所有配置
git config --list
// 设置用户名称与邮件地址
git config --global user.name "John Doe"
git config --global user.email johndoe@example.com

// 新建一个分支并同时切换到那个分支
git checkout -b branch_name
// 删除本地分支
git branch -d branch_name
// 删除远程分支
git push origin --delete branch_name

// 删除本地标签
git tag -d tagname
// 删除远程标签
git push origin :refs/tags/tagname

// 取消暂存
git reset HEAD 123.php
// 从暂存区域移除,但仍然希望保留在当前工作目录中
git rm --cached 123.php

// 查看提交历史
git log -p -2   // 选项 -p,用来显示每次提交的内容差异。 -2 显示最近两次提交

// 尝试重新提交
git commit --amend

// 查看哪些分支已经合并到当前分支
git branch --merged
// 查看尚未合并到当前分支的分支
git branch --no-merged

// 储藏修改
git stash 
// 查看储藏的东西
git stash list
// 将最近的储藏工作重新应用
git stash apply

// 清理工作目录(移除没有忽略的未跟踪文件)
git clean -n    // 使用 -n 选项来运行命令,这意味着 “做一次演习然后告诉你将要移除什么”

// 查看尚未暂存的文件更新了哪些部分
git diff
// 查看已经暂存起来的变化
git diff --cached
// 查看系统支持哪些 Git Diff 插件
git difftool --tool-help 

// 查看远程仓库 origin
git remote show origin
// 使用远程分支覆盖本地工作目录
git reset --hard origin/dev

// 示例操作 
victor.zhu@VICTOR-ZHU MINGW64 /e/shejiben (master)
$ git fetch --tag   
$ git merge victor-opt
$ git tag | tail -2
$ git tag -a shejiben-6.5.2.0-201805311128-victor -m "小程序落地页"   
$ git push origin master --tag

PHP 内核中的 HashTable 分析

04-7月-18

HashTable 是 PHP 的灵魂,因为在 Zend 引擎中大量地使用了 HashTable,如变量表、常量表、函数表等,这些都是使用 HashTable 保存的,另外 PHP 的数组也是使用 HashTable 实现的。

PHP 内核 HashTable 的数据结构

PHP 中的 HashTable 实现代码保存在 Zend/zend_hash.hZend/zend_hash.c 这两个文件中,首先看 HashTable 的数据结构定义:

// Zend/zend_hash.h
#include <stdio.h>

typedef struct bucket {
    ulong h;                    // 保存经过 hash 函数处理之后的 hash 值
    uint nKeyLength;            // 保存索引(key)的长度
    void *pData;                // 指向要保存的内存块地址
    void *pDataPtr;             // 保存指针数据
    struct bucket *pListNext;   // 指向双向链表的下一个元素
    struct bucket *pListLast;   // 指向双向链表的上一个元素
    struct bucket *pNext;       // 指向具有同一个 hash 值的下一个元素
    struct bucket *pLast;       // 指向具有同一个 hash 值的上一个元素
    char arKey[1];              // 保存索引(key),而且必须为最后一个成员

} Bucket;

typedef struct _hashtable {
    uint nTableSize;            // 记录 Bucket 数组的大小
    uint nTableMask;            
    uint nNumOfElements;        // 记录 HashTable 中保存元素的个数
    ulong nNextFreeElement;     // 下一个可用 Bucket 位置
    Bucket *pInternalPointer;   // 遍历 HashTable 元素
    Bucket *pListHead;          // 双向链表表头
    Bucket *pListTail;          // 双向链表表尾
    Bucket **arBuckets;         // Bucket 数组
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
} HashTable;

在上面的数据结构定义中看到,Bucket 和 HashTable 这两个结构体构成一个完整的 HashTable

Bucket 结构体分析

PHP 的 HashTable 同时维护一个双向链表,用于有序遍历 HashTable 里的元素

在这些成员变量中,pData 和 pDataPtr 比较容易混淆。pData 指向的是想要保存的内存块地址,一般是通过 malloc 之类的系统调用分配出来。但是有时候只想保存一个指针,如果这样也去调用 malloc 分配内存的话就会造成很多细小的内存块,从而导致产生内存碎片。这种情况 PHP 内核是不能容忍的,所以出现了 pDataPtr 指针。pDataPtr 的作用就是当想要保存的数据是一个指针类型的数据时,就直接保存到 pDataPtr 成员变量中,而不用调用 malloc 分配内存,从而防止内存碎片的产生,然后 pData 直接指向 pDataPtr。当要保存的数据不是指针时,pDataPtr 被设置成 NULL。

HashTable 结构体分析

一个 Bucket 只能保存一个数据,而 HashTable 的目的就是通过索引(key)把每个元素分散到一个唯一的位置(当没有冲突时),这是怎么做到的呢?答案是 通过 hash 算法把索引处理成一个 int 的数,然后定位到一个 Bucket 数组的其中一个元素中,如图所示。

HashTable 原理

字符串索引经过 hash 函数处理后,返回一个值为 1 的 int 索引,然后从 Bucket 数组中取得索引为 1 的元素(第二个元素),这就是 HashTable 的原理和实现方法。

PHP 内核通过 HashTable 结构管理 Bucket 数组。arBuckets 中保存的就是上图所示的 Bucket 数组,而 nTableSize 记录 arBuckets 数组的大小。

// PHP 内核是怎样通过一个字符串索引定位到 Bucket 数组(arBuckets)中的一个元素的,代码如下:
h = hash(key);
pos = h % nTableSize;
bucket = arBuckets[pos]

int hash(char *key) {
    int h = 0;
    char *p = key;
    while (*p) {
        h += *p;
        p++;
    }
    return h;
}

// 上面的 hash 算法只展示了 hash 函数是怎么把一个字符串处理成一个整数,实际上 PHP 内核的 hash 算法会更复杂。

值得注意的是 nNumOfElements 与 nTableSize 的区别。

Bucket 存储数据示意图

pListHead 和 pListTail 分别是双向链表的表头和表尾指针。传统的 HashTable 是不会同时维护双向链表的。下图展示了这个双向链表。

HashTable 中的双向链表

MySQL 索引知识点

17-6月-18
  1. 存储引擎负责 MySQL 中数据的存储和提取。

  2. 索引是存储引擎用于快速找到记录的一种数据结构。

  3. 索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能高效地使用索引的最左前缀列。

  4. 有时候需要索引很长的字符列,这会让索引变得大且慢。对于 BLOBTEXT 或者很长的 VARCHAR 类型的列,必须使用前缀索引,因为 MySQL 不允许索引这些列的完整长度。

    mysql> ALTER TABLE sakila.city_demo ADD INDEX (city(7));
    
  5. 索引合并策略 type: index_merge 有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟糕:

    • 当出现服务器对多个索引做相交操作时 Extra: Using intersect(film_id,actor_id)(通常有多个 AND 条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。

    • 当服务器需要对多个索引做联合操作时 Extra: Using union(film_id,actor_id)(通常有多个 OR 条件),通常需要耗费大量 CPU 和内存资源在算法的缓存、排序和合并操作上。

  6. 如果一个索引包含所有需要查询的字段的值,我们就称之为 覆盖索引 Extra: Using index

  7. MySQL 可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务。如果 EXPLAIN 出来的 type: index,则说明 MySQL 使用了索引扫描来做排序。

    • 只有当索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向都一样时,MySQL 才能够使用索引来对结果做排序。

    • ORDER BY 子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL 都需要执行排序操作,而无法利用索引排序。有一种情况下 ORDER BY 子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。

      # 例如,KEY rental_date(rental_date,inventory_id,customer_id)
      
      # 可以使用 rental_date 索引做排序的查询示例:
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, customer_id;
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
      ... WHERE rental_date > '2005-05-25' ORDER BY rental_date, inventory_id; // ORDER BY 使用的两列就是索引的最左前缀
      
      # 不可以使用 rental_date 索引做排序的查询示例:
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC; // 使用了两种不同的排序方向
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id; // 引用了一个不在索引中的列
      ... WHERE rental_date = '2005-05-25' ORDER BY customer_id; // 无法组合成索引的最左前缀
      ... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id; // 索引列的第一列上是范围条件,所以 MySQL 无法使用索引的其余列       
      
  8. 理想情况下扫描的行数和返回的行数应该是相同的。

  9. EXPLAIN 语句中的 type 列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。

  10. 查询优化、索引优化、库表结构优化需要齐头并进,一个不落。

MySQL 查询执行过程

03-6月-18

当希望 MySQL 能够以更高的性能运行查询时,最好的办法就是弄清楚 MySQL 是如何优化和执行查询的。

当向 MySQL 发送一个请求的时候,MySQL 到底做了些什么:

  1. 客户端发送一条查询给服务器。

  2. 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。

  3. 服务器端进行 SQL 解析、预处理,再由优化器生成对应的执行计划。

    • 首先,MySQL 通过关键字将 SQL 语句进行解析,并生成一颗对应的“解析树”。MySQL 解析器将使用 MySQL 语法规则验证和解析查询。例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确匹配。
    • 预处理器则根据一些 MySQL 规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。下一步预处理器会验证权限。
    • 现在语法树被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很多种执行计划,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
  4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询。

  5. 将结果返回给客户端。如果查询可以被缓存,那么 MySQL 在这个阶段也会将结果存放到查询缓存中。

扩展:

1. 分解关联查询的好处?

  • 对 MySQL 的查询缓存来说,如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。
  • 将查询分解后,执行单个查询可以减少锁的竞争。

2. 优化 LIMIT 分页。

  • 在偏移量非常大的时候(翻页到非常靠后的页面),例如可能是 LIMIT 10000,20 这样的查询,这时 MySQL 需要查询 10020 条记录然后只返回最后 20 条,前面 10000 条记录都将被抛弃。
  • LIMIT 和 OFFSET 的问题,其实是 OFFSET 的问题,它会导致 MySQL 扫描大量不需要的行然后再抛弃掉。如果可以记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用 OFFSET。

    mysql> SELECT * FROM sakila.rental ORDER BY rental_id DESC LIMIT 20;
    
    // 假设上面的查询返回的是主键为 16049 到 16030 的记录,那么下一页查询就可以从 16030 这个点开始:        
    mysql> SELECT * FROM sakila.rental WHERE rental_id < 16030 ORDER BY rental_id DESC LIMIT 20;
    
    * 该技术的好处是无论翻页到多么后面,其性能都会很好。 
    

PHP 内核中的变量

28-5月-18

PHP 变量在内核中的存储方式

在 Zend 引擎中是怎么可以做到一个变量保存任何的数据类型呢?打开 Zend/zend.h 头文件,会发现以下一些结构体:

typedef struct _zval_struct zval;

typedef union _zvalue_value {
    long lval;              // long, bool, resource
    double dval;            // double
    struct {                // string(len 保存字符串长度,val 保存字符串的值)
        char *val;   
        int len;
    } str;

    HashTable *ht;          // array
    zend_object_value obj;  // object            
} zvalue_value;

struct _zval_struct {
    zvalue_value value;     // 变量的值
    zend_uint refcount;     // 变量引用数
    zend_uchar type;        // 变量的类型
    zend_uchar is_ref;      // 变量是否被引用
};

zval 结构体就是通常用到的 PHP 变量在内核中的表示方式。在 zval 结构体中,可以看到 4 个成员变量。

zval 结构体的 value 成员变量是一个 zvalue_value 联合体,从 zvalue_value 联合体的成员变量中可以看出,不同的类型会保存到不同的成员变量中,这样就实现了 PHP 变量可以存储任何数据类型。

引用计数器与写时复制

zval 结构中有以下两个成员变量用于引用计数器:

  • is_ref: BOOL 值,标识变量是否是引用集合。
  • refcount: 计算指向引用集合的变量个数。

一个 zval 结构的实体称为 zval 容器。在 PHP 语言层创建一个变量就会相应地在 PHP 内核中创建一个 zval 容器。

<?php
$a = "this is variable";
xdebug_debug_zval('a');
// 输出 --> a: (refcount=1, is_ref=0),string 'this is variable' (length=16)

$b = $a;    // 此时 $a 和 $b 指向同一内存块
xdebug_debug_zval('a');
// 输出 --> a: (refcount=2, is_ref=0),string 'this is variable' (length=16)

$a = "changed value";
xdebug_debug_zval('a');
// 输出 --> a: (refcount=1, is_ref=0),string 'changed value' (length=13)

当将变量 $a 的值赋给变量 $b 时,变量 $a 的 refcount 增加 1,所以这时候变量 $a 和变量 $b 是指向同一内存块的。当改变变量 $a 的值时,发现 refcount 的值变回 1,所以这时候变量 $a 和变量 $b 指向不同的内存块,这就是 写时复制机制。就是两个指向同一内存块的变量,当其中一个变量的值发生变化,才会另外创建一个内存块去保存新的值。

<?php
$a = 1;
xdebug_debug_zval('a');
// 输出 --> a: (refcount=1,is_ref=0),int 1

$b = &$a;
xdebug_debug_zval('a');
// 输出 --> a: (refcount=2,is_ref=1),int 1

$b += 5;
xdebug_debug_zval('a');
// 输出 --> a: (refcount=2,is_ref=1),int 6

当显式地让一个变量引用另一个变量时,变量的 is_ref 字段会设置为 1,表示此变量被引用,另外引用计数器(refcount)也相应地加 1。

PHP 新特性

22-5月-18

现代的 PHP 语言有很多令人兴奋的新特性,这些新特性让 PHP 语言变成了一个强大的平台,为构建 Web 应用和命令行工具提供了愉快的体验。

这些特性中有些不是必不可少的,不过能让我们的开发工作更轻松。而有些特性则非常重要,例如命名空间,这是现代 PHP 标准的基础,能让现代的 PHP 开发者顺其自然地使用一些开发实践(例如自动加载)。

1. 性状

性状是 PHP 5.4.0 引入的新概念,是类的部分实现,可以混入一个或多个现有的 PHP 类中。

性状有两个作用:表明类可以做什么(像是接口);提供模块化实现(像是类)。

<?php
// 定义性状
trait MyTrait {
    // 这里是性状的实现
}

// 使用性状
class MyClass
{
    use MyTrait;

    // 这里是类的实现
}

2. 生成器

PHP 生成器(generator)是 PHP 5.5.0 引入的功能。生成器是简单的迭代器,与标准的 PHP 迭代器不同,PHP 生成器不要求类实现 Iterator 接口,从而减轻了类的负担。

<?php
// 一个简单的生成器
function myGenerator() {
    yield 'value1';
    yield 'value2';
    yield 'value3';
}

// 调用并迭代生成器
foreach (myGenerator() as $yieldedValue) {
    echo $yieldedValue, PHP_EOL;
}

上述代码的输出如下:      
    value1
    value2
    value3

3. 闭包

闭包和匿名函数在 PHP 5.3.0 中引入。闭包是指在创建时封装周围状态的函数。匿名函数其实就是没有名称的函数。

注意:理论上讲,闭包和匿名函数是不同的概念。不过,PHP 将其视作相同的概念。

<?php
// 创建闭包
$closure = function($name) {
    return sprintf('Hello %s', $name);
};

echo $closure("Josh");
// 输出 --> "Hello Josh"

// 把状态附加到 PHP 闭包上
function enclosePerson($name) {
    return function($doCommand) use ($name) {
        return sprintf('%s, %s', $name, $doCommand);
    };
}

// 把字符串 "Clay" 封装在闭包中
$clay = enclosePerson('Clay');

// 传入参数,调用闭包
echo $clay('get me sweet tea!');    
// 输出 --> "Clay, get me sweet tea!"

具名函数 enclosePerson() 有个名为 $name 的参数,这个函数返回一个闭包对象,而且这个闭包封装了 $name 参数。

4. 内置的 HTTP 服务器

从 PHP 5.4.0 起,PHP 内置了 Web 服务器。

// 打开终端,进入项目根目录,本地启动一个监听端口为 4000 的 PHP Web 服务器
php -S localhost:4000

// 使用指定的初始化文件
php -S localhost:4000 -c app/config/php.ini

// 查明使用的是否为内置的服务器
if (php_sapi_name() === 'cli-server') {
    // PHP 内置的 Web 服务器
} else {
    // 其他 Web 服务器
}

Linux 常见服务配置

28-4月-18

https 配置

编辑 /etc/sysconfig/iptables 文件,开放 443 端口

-A INPUT -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT

修改 nginx 配置文件

server
{
    listen 443;
    server_name www.163100.com;
    ssl on;
    ssl_certificate /alidata/server/ssl/1_www.163100.com_bundle.crt;
    ssl_certificate_key /alidata/server/ssl/2_www.163100.com.key;

    root                /alidata/www/www.163100.com;
    charset             utf-8;
    index               index.html index.htm index.php;

    ssl_session_timeout 5m;
    ssl_protocols SSLv2 SSLv3 TLSv1;
    #ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    ssl_ciphers HIGH:!aNULL:!MD5:!EXPORT56:!EXP;
    ssl_prefer_server_ciphers   on;


    location ~ .*\.(php)?$
    {
        fastcgi_pass  127.0.0.1:9000;
        fastcgi_index   index.php;
        include fastcgi.conf;
        fastcgi_param HTTPS on;
    }

    #access_log  /alidata/log/nginx/access/www.163100.com.log;
    access_log  off;
    error_log  /alidata/log/nginx/error/www.163100.com.log notice;
    include /alidata/server/nginx/conf/rewrite/default.conf;
}

http 自动跳转到 https

server 
{
    listen       80;
    server_name  www.163100.com;
    rewrite ^(.*)$  https://$host$1 permanent;
}

不带 www 的域名跳转到带 www 上(301 重定向)

server_name  www.163100.com 163100.com;
if ($host != 'www.163100.com')
{
    rewrite ^/(.*)$ http://www.163100.com/$1 permanent;
}