Docker快速搭建一套PHP、Nginx、MySQL、Redis、Xdebug、Memcached 开发环境并演进

又来一个新项目,又要配置一套新环境,什么你继续用原来的,不怕互相干扰么。来看看这,一步拉起一套全新的Docker,PHP,Nginx,Redis,MySQL,Memcached开发环境

痛点

如何简单迅速地初始化一个全新的PHP开发环境呢?

笔者在尝试起一个新web项目时,往往会陷入重新建立一套Docker环境的繁琐事当中。我想大家在开始做一个新项目时,或者快速为了和以前的项目做完全的隔离,也会起一套新docker环境。

从这篇文章你会得到

采用docker-compose容器编排技术,一步启动全部服务。php容器在官方的基础上还打包了一些常用拓展(附带build源码,动手能力强可以自己打包),php,nginx,mysql都附带有自定义配置文件(便于开发测试的配置调整),都供下载。

架构

本片文章搭建出来的环境如上图。

环境说明

运行环境

  • Mac OS 10.13.6
  • Virtual Box 5.2.14
  • Ubuntu 16.04.4
  • Docker 17.07.0-ce

容器

  • Nginx 1.12.1-alpine
  • PHP-fpm 5.6
  • MySQL 5.7
  • Redis 3.2
  • Memcached 1.4.27

这里说明一下笔者运行环境,采用了在Mac机上安装vbox虚拟机,在虚拟机里面的debian环境安装了Docker环境,文件放在Mac环境,通过文件挂载方式,达到Mac环境编辑,Docker环境实时更新效果。你要问为啥不在Mac下直接装Docker,历史原因~

运行材料

.
├── build                                   ## docker 镜像build 材料
│   ├── php5.6                              ## php 5.6
│   │   ├── Dockerfile
│   │   ├── imagick-3.4.3.tgz
│   │   ├── memcached-2.2.0.tgz
│   │   ├── redis-4.0.2.tgz
│   │   ├── sources.list
│   │   └── xdebug-XDEBUG_2_5_5.tar.gz
│   └── php-cli                             ## php cli 的自建镜像
│       ├── amqp-1.9.3.tgz
│       ├── Dockerfile
│       ├── Dockerfile-php-swoole
│       ├── memcached-3.0.4.tgz
│       ├── redis-4.1.1.tgz
│       ├── sources.list.jessie
│       ├── swoole-4.0.4.tgz
│       ├── swoole-4.2.6.tgz
│       ├── xdebug-2.6.1.tgz
│       └── yaf-3.0.7.tgz
├── config                                  ## 配置文件
│   ├── apt                                 ## 由于使用的debian系列,所以命名apt
│   │   └── sources.list                    ## 更新为国内源
│   ├── memcached                           ## memchached的配置
│   │   └── memcached.conf
│   ├── mysql                            ## mysql 的配置
│   │   ├── conf.d
│   │   │   └── mysqld_safe_syslog.cnf
│   │   └── my.cnf
│   ├── nginx                            ## nginx 的配置
│   │   ├── conf.d
│   │   │   ├── default
│   │   │   └── xiuno
│   │   ├── mime.types
│   │   ├── nginx.conf
│   │   └── sites-enabled
│   ├── php                            ## php的配置,分cli,和fpm配置
│   │   ├── cli
│   │   │   ├── conf.d
│   │   │   │   ├── 05-opcache.ini
│   │   │   │   ├── 10-pdo.ini
│   │   │   │   ├── 20-curl.ini
│   │   │   │   ├── 20-gd.ini
│   │   │   │   ├── 20-imagick.ini
│   │   │   │   ├── 20-json.ini
│   │   │   │   ├── 20-memcache.ini
│   │   │   │   ├── 20-mysqli.ini
│   │   │   │   ├── 20-mysql.ini
│   │   │   │   ├── 20-pdo_mysql.ini
│   │   │   │   ├── 20-readline.ini
│   │   │   │   ├── 20-redis.ini
│   │   │   │   ├── 20-xdebug.ini
│   │   │   │   └── swoole.ini
│   │   │   └── php.ini
│   │   ├── fpm
│   │   │   ├── conf.d
│   │   │   │   ├── docker.conf
│   │   │   │   ├── www.conf
│   │   │   │   └── zz-docker.conf
│   │   │   ├── docker-php-fpm.conf
│   │   │   ├── php-fpm.conf
│   │   │   ├── php.ini
│   │   │   └── pool.d
│   │   │       └── www.conf
│   │   └── mods-available
│   │       ├── curl.ini
│   │       ├── gd.ini
│   │       ├── imagick.ini
│   │       ├── json.ini
│   │       ├── memcache.ini
│   │       ├── mysqli.ini
│   │       ├── mysql.ini
│   │       ├── opcache.ini
│   │       ├── pdo.ini
│   │       ├── pdo_mysql.ini
│   │       ├── readline.ini
│   │       ├── redis.ini
│   │       └── xdebug.ini
│   └── redis                            ## redis 配置
│       ├── redis.conf
│       ├── redis-server.sh
│       └── sentinel.conf
├── data                                ## 为了数据持久化
│   ├── mysql                            ## mysql 文件挂载
│   └── redis                            ## redis 持久化数据文件夹
├── docker-compose.yaml                 ## 容器编排 !!!
├── log                                 ## 各个容器产生的日志
│   ├── mysql                            ## mysql日志
│   │   ├── error.log
│   │   └── mysql.log
│   ├── nginx                            ## nginx 日志
│   │   ├── access.log
│   │   ├── access_xiuno.log
│   │   ├── error.log
│   │   └── error_xiuno.log
│   └── php
├── php                                    ## php代码存放目录
│   └── xiunobbs                            ## php项目
└── tool                                ## php 一些工具
    ├── composer.phar
    └── phpunit-4.8.36.phar

配置文件下载

git clone https://gitee.com/xupaul/docker_fast_init

配置说明

目前笔者还没有写相关自动化脚本自动适配用户的使用环境,所以这里需要先调整一下下载后的配置文件。

docker-compose.yml 文件调整

这个文件有大量的文件挂载配置,这块就需要调整,拿一个举例。

version: "2"

services:

    http:
        image: nginx:1.12.1-alpine
        volumes:
            - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:rw
        ports:
            - "8080:80"
        environment: 
            TZ : "Asia/Shanghai"
        networks:
            tasker_net:
                ipv4_address: "192.63.0.11"

在例子中最后volumes的下一行,是将宿主机的nginx.conf配置文件挂载并覆盖容器内的nginx.conf文件,./文件路径就是clone git项目后的目录。

如果你也使用了vbox虚拟机,那么你需要./要替换为docker的运行环境的路径!

ports这里,http服务为了不和本机的80端口冲突,以及sudo权限才能申请的小于1024的端口,这里我以8080作为默认值。其他容器的端口配置也遵循以上的规则,为的是能一个命令就能启动起来。

为每个容器都配置了时区。

网络ip这部分,为了便于配置就手动分配ip了。

Nginx 配置

配置好了php-fpm之间tcp通讯,以及php路由的rewrite。配置了静态资源缓存。

MySQL 配置

开启记录了一般性的查询日志,便于debug程序。注意这里挂载的配置文件读写权限要配置只读!

PHP 配置

    php:
        image: paulxu/php:5.6-fpm-jessis-pdo-xdebug-mysqli-gd-mb-zip-2
        volumes:
            - ./config/php/cli/php.ini:/usr/local/etc/php/php.ini/:rw
        ports:
            - "9000"
        environment: 
            PHP_IDE_CONFIG: "serverName=bs"
            XDEBUG_CONFIG: "remote_host=10.0.2.2 remote_port=9000"

我为php一起打包了一些常用拓展,其中xdebug需要调整一下remote_host的的ip(docker-compose.yaml, ~/config/php/cli/20-xdebug.ini 两个文件内的都需要调整). server_name 需要结合开发IDE一起调整,如PHPStorm里Preferences | Languages & Frameworks | PHP | Servers配置路径下有什么可用的记录,那么这里填什么。

Redis 配置

没有特殊配置

Memecached 配置

没有特殊配置

镜像构建

php镜像

可以通过如下命令拉取:

docker pull paulxu/php:5.6-fpm-jessis-pdo-xdebug-mysqli-gd-mb-zip-2

或者在到 ~/build/php5.6 下自行打包:

docker build -t paulxu/php:5.6-fpm-jessis-pdo-xdebug-mysqli-gd-mb-zip-2 -f dockerfile .

如果自定义tag name,需要一并调整docker-compose.yml

启动

  1. 从gitee下载配置
  2. 根据自己的环境调整配置
  3. 准备好镜像,镜像也可直接拉,我已上传了官方了。
  4. 启动

启动命令

## 当前线程运行,要停止时,键入`Ctrl+C`
docker-comose -f ./docker-compose.yaml up

## 后台运行
docker-comose -f ./docker-compose.yaml up -d

摧毁

docker-comose -f ./docker-compose.yaml down

常见问题

MySQL 挂载的配置文件被忽略,不起作用

MySQL 要求配置文件的权限为只能读,你需要已只读方式挂载

不知道remote_host怎么办

禁用xdebug,搭建好环境并启动,在IDE运行的环境(因为xdebug需要和IDE通讯)访问以http(在浏览器访问,注意端口映射)的方式访问php容器内代码,在Nginxaccess.log里查看访问的ip就是remote_host.

docker build 时报错导致打包镜像失败

遇到这种情况,如果时一些不要紧的报错,可以选择忽略他们。可以在命令后面追加 exit 0 来屏蔽错误

最后

祝愿大家能一步到位,有问题留言,遇到疑难杂可以试试本地起虚拟机里跑docker容器的那种方案。

Mac环境,安装OpenCV,VScode断点调试C++程序

Mac环境下,配置OpenCV的C/C++环境,并且能够通过VSCode断点调试。

背景

最近在研究图像识别相关代码——OpenCV,为了便于调试,就要在Mac上搭建一个调试c++程序的调试环境。我这跑通了,分享给大家。

环境

  • Mac OS 10.14.5
  • xcode-select v2354
  • Visual Studio code 1.35.1
  • OpenCV 3.4.5
  • Clang 1001.0.46.4
  • CMake 3.14.5

步骤说明

  1. 编译环境准备
  2. 安装OpenCV
  3. 安装VScode 插件
  4. VScode 项目配置
  5. OpenCV HelloWorld

环境准备

Mac 自带有C/C++环境,就不再赘述。

Xcode Command Line Tools是必须的,可通过一下命令行触发安装程序,或者下载安装。

命令安装 xcode-select
 sudo xcode-select --install
下载 xcode-select 安装

Apple Developer Download

选择下载文件注意这里提示的版本号!
命令行安装速度很慢,且失败率很高,建议通过下载dmg文件安装

CMake 准备

编译OpenCV时要用到cmake。同样可以通过homebrew或者下载安装。
下载方式安装的CMake,cmake可执行程序在/Applications/CMake.app/Contents/bin/cmake.

如果你用的homebrew方式安装opencv那么CMake就不是必须的.

pkg-config 安装

编译引用有opencv库的c/c++代码需要附带很多参数来告知include path,libs等,用pkg-config就能精简编译参数。

brew install pkg-config

安装OpenCV

有两种方式安装。

brew install
## 一个命令安装好opencv3
brew install opencv3

安装好之后最好配置上这几个环境变量。(一般命令行会提示你)

export PATH="/usr/local/opt/opencv@3/bin:$PATH"

export LDFLAGS="-L/usr/local/opt/opencv@3/lib"
export CPPFLAGS="-I/usr/local/opt/opencv@3/include"

export PKG_CONFIG_PATH="/usr/local/opt/opencv@3/lib/pkgconfig"

用homebrew安装很方便,就是安装时间很长(我用了一个大白天)。额外还会安装python-opencv。

编译安装

点击下载源代码。这里我选择的是Sources 3.4.5

解压后,进入目录

cd <opencv 解压后目录>
mkdir release
cd release
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local 
make
sudo make install

这里创建的release目录会放cmake命令产生的成果。CMAKE_INSTALL_PREFIX配置的目录会存make命令产生的成果。

到这里如果都没报错就算安装成功了。

一般都不会出问题,在虚拟环境不一定能成功。

参考自 编译安装opencv

写个C++ Hello World

先写一个只引用了标准库的C++代码试试看。

“`C++
/// ./main.cpp
#include <stdio.h>
#include

int main(int argc, const char * argv[]) {
std::cout << “Hello, World!\n”;
return 0;
}


结果如下。 ![](http://cdn.itjsz.com/20200710162408.png) 没问题就继续尝试在代码引入`OpenCV`库。 #### pkg-config 环境配置 安装好后,配置一个环境变量就行了。 ```sh PKG_CONFIG_PATH="~/opencv解压目录/release/unix-install"

配置好之后在执行命令:

pkg-config opencv --libs --cflags opencv

显示内容大致如下,这块的内容就是opencv.pc里面的配置。

该环境变量就是为了让pkg-config能够找到opencv.pc这个文件。

写个OpenCV Demo

好,重点来了,大部分人会卡在这里

写个OpenCV HellowWorld来试试,打开一个空的VScode项目,创建
文件test.cpp

#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <stdlib.h>
#include <stdio.h>

using namespace std;
using namespace cv;

Mat src, dst, src_gray;

char* window_name = "Demo";

/**
 * @主函数
 */
int main( int argc, char** argv )
{

    /// 读取一副图片,不改变图片本身的颜色类型(该读取方式为DOS运行模式)
    src = imread(argv[1], 1 );

    /// 将图片转换成灰度图片
    cvtColor(src, src_gray, CV_RGB2GRAY);

    namedWindow(window_name, CV_WINDOW_AUTOSIZE);

    imshow(window_name, src_gray);

    while(true) {
        int c;
        c = waitKey(20);
        if (27 == (char) c) {
            break;
        }
    }
}

进入到命令行环境

## 编译测试程序
g++ `pkg-config opencv --libs --cflags opencv` ./test.cpp -o ./test.o
## 运行测试程序
./test.o ~/Downloads/opencv-logo.png

pkg-config 这条命令是为编译opencv测试代码引入类库。

能看到如下运行结果,恭喜你成功了。

可参考该链接最后: 编译自测程序

实际调试代码不停编译运行会敲很多命令,我们尝试把这些工作交给VScode。先为VScode安装一些插件

  • C/C++
  • C++ Intellisense
  • C++ Clang Command Adapter

在根目录下创建一个.vocode文件夹,再创建三个文件。

  • .vscode/launch.json
  • .vscode/tasks.json
  • .vscode/c_cpp_properties.json

文件.vscode/tasks.json

{
    "tasks": [
        {
            "type": "shell",
            "label": "g++ build active file",
            "command": "/usr/bin/g++",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}.o",
                "`pkg-config",
                "--libs",
                "--cflags",
                "opencv`"
            ],
            "options": {
                "cwd": "/usr/bin"
            },
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": []
        }
    ],
    "version": "2.0.0"
}

文件.vscode/launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "g++ build and debug active file",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}/${fileBasenameNoExtension}.o",
            "args": [
                "~/Downloads/opencv-logo.png"
            ],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment":  [
                {"name": "PKG_CONFIG_PATH", "value": "opencv解压目录/release/unix-install"},   // 这是opencv解压码后创建的release目录下的unix-install, 要保证该目录下下有opencv.pc文件
                {"name": "DYLD_LIBRARY_PATH", "value": "/usr/local/opencv/build/lib"}   // 这个是你在编译时,opencv make时`CMAKE_INSTALL_PREFIX`指定的目录
                // {"DYLD_LIBRARY_PATH": "/usr/local/opencv/build/lib"}    // 这样的配置是错误的!
            ],
            "externalConsole": true,         // 这个开关打开表示,新开一个命令行窗口并运行程序。
            "MIMode": "lldb",
            "preLaunchTask": "g++ build active file"         // 这一行的内容要和上一个文件`.vscode/tasks.json`的`tasks`->`label`字段的内容要一模一样!
        }
    ]
}

文件.vscode/c_cpp_properties.json

{
    "configurations": [
        {
            "name": "Mac",
            "includePath": [
                "/usr/local/include",
                "/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include",
                "/usr/local/opencv/include",        // 这个是你在编译时,opencv make时`CMAKE_INSTALL_PREFIX`指定的目录
                "${workspaceFolder}"
            ],
            "browse": {
                "limitSymbolsToIncludedHeaders": true,
                "databaseFilename": "",
                "path": [
                    "/usr/local/include",
                    "/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include",
                    "/usr/local/opencv/include",     // 这个是你在编译时,opencv make时`CMAKE_INSTALL_PREFIX`指定的目录
                    "${workspaceFolder}"
                ]
            },
            "intelliSenseMode": "clang-x64",
            "macFrameworkPath": [
                "/System/Library/Frameworks",
                "/Library/Frameworks"
            ],
            "compilerPath": "/usr/bin/g++",
            "cStandard": "c11",
            "cppStandard": "c++17"
        }
    ],
    "version": 4
}

点击VScode菜单栏Debug > Start Debugging运行结果如下:

为了便于调试,我把launch.jsonexternalConsole设置为true,这样会在vscode之外新开一个终端运行程序,可能会弹框要授权,同意即可。

常见问题

  1. pkg-config 配置不正确导致的报错
Package opencv was not found in the pkg-config search path.
Perhaps you should add the directory containing `opencv.pc'
to the PKG_CONFIG_PATH environment variable
No package 'opencv' found
test.cpp:1:10: fatal error: 'opencv2/imgproc/imgproc.hpp' file not found
#include "opencv2/imgproc/imgproc.hpp"
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
[Finished in 0.4s with exit code 1]

解决方案:

运行一下命令添加一条path

export PKG_CONFIG_PATH="/opencv解压目录/release/unix-install":"$PKG_CONFIG_PATH"
  1. dyld 包依赖问题
dyld: Library not loaded: @rpath/libopencv_dnn.3.4.dylib
  Referenced from: ~/App/test
  Reason: image not found
bash: line 1: 18912 Abort trap: 6           ~/App/test
[Finished in 1.0s with exit code 134]

解决方案:

if you want to solve error on terminal you can use export DYLD_LIBRARY_PATH=your/lib:$DYLD_LIBRARY_PATH if you want to solve error on Xcode in build page , go to "Runpath Search Paths" add you lib path

运行一下命令添加一条path

export DYLD_LIBRARY_PATH=/usr/include/opencv/build/lib:$DYLD_LIBRARY_PATH

遇到问题,欢迎留言

原文链接。不支持转载!

PHP Session 并发写 覆盖 问题

问题:如题

php session 默认采用文件保存的session,带来的问题是并发锁,并且不能让PHP多点部署。

这个时候一般就是注册PHP sessionhandler,比如用redis,memcache保存session,这样就不会有并发锁了。(当然你可以在设计sessionhandler时,把并发写考虑进去,但我不推荐这样做),没有锁,带来的另一个问题就是并发写,
——导致写覆盖。如下图:

就是说在 并发时,先来了 耗时操作,又来了一个不耗时的操作,两个请求都读取并改动了seesion,那么耗时操作的写入的数据就会覆盖之前写入。

这个问题在其他Web服务(不论是Java,C#,Python)都会出现。

解决否

已解决

方案

我的方案:首先,PHP文件session又并发锁,是不推荐使用的。再一个,用户运行时数据,不应该放到php session 里保存,session 应该只保存用户的登陆信息,也就是登陆的那一次请求写一次session,后续的操作都应只有读,不能再写。因为当你用php代码修改了session的值,session并不是立马写到磁盘(或者刷到redis上),而是等到该http请求结束时,再一并写入。

所以,建议就是session只能在登录时写一次,登出时删除。如果应用又存用户运行时数据的需求,建议使用redis,memcached,mysql 实时储存,也就是又编辑请求时,立马发出操作数据库的请求,而不是等到http请求结束时在做,避免并发写覆盖问题。使用redis,memcached保存数据的key可借用session id作为该用户的数据key。

网友的方案:https://blog.csdn.net/wuxing26jiayou/article/details/78961830

ThinkPHP5 引入 Go AOP PHP AOP编程

ThinkPHP5,PHP7,要用AOP切面编程,没有现成的,一步到位的命令,需要怎么配置呢?什么你不知道什么是AOP,那还不来看看

项目背景

目前开发的WEB软件里有这一个功能,PHP访问API操作数据仓库,刚开始数据仓库小,没发现问题,随着数据越来越多,调用API时常超时(60s)。于是决定采用异步请求,改为60s能返回数据则返回,不能则返回一个异步ID,然后轮询是否完成统计任务。由于项目紧,人手不足,必须以最小的代价解决当前问题。

方案选择

  1. 重新分析需求,并改进代码
  2. 采用AOP方式改动程序
    从新做需求分析,以及详细设计,并改动代码,需要产品,架构,前端,后端的支持。会惊动的人过多,在资源紧张的情况下是不推荐的。
    采用AOP方式,不改动原有代码逻辑,只需要后端就能完成大部分任务了。后端用AOP切入请求API的方法,通过监听API返回的结果来控制是否让其继续运行原有的逻辑(API在60s返回了数据),或者是进入离线任务功能(API报告统计任务不能在60s内完成)。

之前用过AOP-PHP拓展,上手很简单,不过后来在某一个大项目中引入该拓展后,直接爆了out of memory,然后就研究其源码发现,它改变了语法树,并Hook了每个被调用的方法,也就是每个方法被调用是都会去询问AOP-PHP,这个方法有没有切面方法。所以效率损失是比较大的。而且这个项目距离现在已经有8年没更新了。所以不推荐该解决方案。

实际环境

Debian,php-fpm-7.0,ThinkPHP-5.10。

引入AOP

作为一门zui好的语言,PHP是不自带AOP的。那就得安装AOP-PHP拓展,当我打开pecl要下载时,傻眼了,全是bate版,没有显示说明支持php7。但我还是抱着侥幸心理,找到了git,发现4-5年没更新了,要不要等一波更新,哦,作者在issue里说了有时间就开始兼容php7。
好吧,狠话不多说,下一个方案:Go!AOP.看了下git,作者是个穿白体恤,喜欢山峰的大帅哥,基本每个issue都会很热心回复。

composer require goaop/framework

ThinkPHP5 对composer兼容挺不错的哦,(到后面,我真想揍ThinkPHP5作者)这就装好了,怎么用啊,git上的提示了简单用法。我也就照着写了个去切入controller。

<?PHP
namespace app\tests\controller;

use think\Controller;

class Test1 extends Controller
{
    public function test1()
    {
        echo $this->aspectAction();
    }

    public function aspectAction()
    {
        return 'hello';
    }
}

定义aspect

<?PHP
namespace app\tests\aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;

use app\tests\controller\Test1;

class MonitorAspect implements Aspect
{

    /**
     * Method that will be called before real method
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("execution(public|protected app\tests\controller\Test1->aspectAction(*))")
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        $obj = $invocation->getThis();
        echo 'Calling Before Interceptor for method: ',
             is_object($obj) ? get_class($obj) : $obj,
             $invocation->getMethod()->isStatic() ? '::' : '->',
             $invocation->getMethod()->getName(),
             '()',
             ' with arguments: ',
             json_encode($invocation->getArguments()),
             "<br>\n";
    }
}

启用aspect

<?PHP
// file: ./application/tests/service/ApplicationAspectKernel.php

namespace app\tests\service;

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

use app\tests\aspect\MonitorAspect;

/**
 * Application Aspect Kernel
 *
 * Class ApplicationAspectKernel
 * @package app\tests\service
 */
class ApplicationAspectKernel extends AspectKernel
{

    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     *
     * @return void
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new MonitorAspect());
    }
}

go-aop 核心服务配置

<?PHP
// file: ./application/tests/behavior/Bootstrap.php
namespace app\tests\behavior;

use think\Exception;
use Composer\Autoload\ClassLoader;
use Go\Instrument\Transformer\FilterInjectorTransformer;
use Go\Instrument\ClassLoading\AopComposerLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;

use app\tests\service\ApplicationAspectKernel;
use app\tests\ThinkPhpLoaderWrapper;

class Bootstrap
{
    public function moduleInit(&$params)
    {
        $applicationAspectKernel = ApplicationAspectKernel::getInstance();
        $applicationAspectKernel->init([
            'debug' =>  true,
            'appDir'    =>  __DIR__ . './../../../',
                'cacheDir'  =>  __DIR__ . './../../../runtime/aop_cache',
                'includePaths'  =>  [
                    __DIR__ . './../../tests/controller',
                    __DIR__ . './../../../thinkphp/library/think/model'
                ],
                'excludePaths'  =>  [
                    __DIR__ . './../../aspect',
                ]
            ]);
        return $params;
    }
}

配置模块init钩子,让其启动 go-aop

<?PHP
// file: ./application/tests/tags.php
// 由于是thinkphp5.10 没有容器,所有需要在module下的tags.php文件里配置调用他

return [
    // 应用初始化
    'app_init'     => [],
    // 应用开始
    'app_begin'    => [],
    // 模块初始化
    'module_init'  => [
        'app\\tests\\behavior\\Bootstrap'
    ],
    // 操作开始执行
    'action_begin' => [],
    // 视图内容过滤
    'view_filter'  => [],
    // 日志写入
    'log_write'    => [],
    // 应用结束
    'app_end'      => [],
];

兼容测试

好了,访问 http://127.0.0.1/tests/test1/test1 显示:

hello

这不是预期的效果,在aspect定义了,访问该方法前,会输出方法的更多信息信息。
像如下内容才是预期

Calling Before Interceptor for method: app\tests\controller\Test1->aspectAction() with arguments: []

上他官方Doc看看,是一些更高级的用法。没有讲go-aop的运行机制。
上git上也没看到类似issue,额,发现作者经常在issue里回复:试一试demo。也许我该试试demo。

Run Demos

我采用的是LNMP技术栈。

  1. 假设这里有台Ubuntu你已经配置好了LNMP环境
  2. 下载代码
  3. 配置nginx
# file: /usr/share/etc/nginx/conf.d/go-aop-test.conf
server {
    listen 8008;
#    listen 443 ssl;
    server_name 0.0.0.0;
    root "/usr/share/nginx/html/app/vendor/lisachenko/go-aop-php/demos";
    index index.html index.htm index.php;
    charset utf-8;

    access_log /var/log/nginx/go-aop-access.log;
    error_log  /var/log/nginx/go-aop-error.log notice;

    sendfile off;
    client_max_body_size 100m;

    location ~ \.php(.*)$ {
        include                         fastcgi_params;
        fastcgi_pass                    127.0.0.1:9000;
        fastcgi_index                   index.php;

        fastcgi_param                   PATH_INFO   $fastcgi_path_info;
#        fastcgi_param                   SCRIPT_FILENAME /var/www/html/app/vendor/lisachenko/go-aop-php/demos$fastcgi_script_name;  #docker的配置
        fastcgi_param                   SCRIPT_FILENAME /usr/share/nginx/html/api/vendor/lisachenko/go-aop-php/demos$fastcgi_script_name;
        fastcgi_param                   PATH_TRANSLATED $document_root$fastcgi_path_info;
        fastcgi_split_path_info         ((?U).+\.php)(/?.+)$;
    }
}

接下来要调整下代码

  1. 访问 http://127.0.0.1:8008 试试,(估计大家都遇到了这个)

  1. 这个报错信息提示找不到这个类。来到报错的文件里。这文件使用了use找不到类,就是autoload出问题了,看到 vendor/lisachenko/go-aop-php/demos/autoload.php 这个文件。
<?PHP
···
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
    /** @var Composer\Autoload\ClassLoader $loader */
    $loader = include __DIR__ . '/../vendor/autoload.php';
    $loader->add('Demo', __DIR__);
}

可以看到这个代码第一行没找到vendor下的autoload。我们做如下调整

<?PHP
$re = __DIR__ . '/../../../vendor/autoload.php';
if (file_exists(__DIR__ . '/../../../autoload.php')) {
    /** @var Composer\Autoload\ClassLoader $loader */
    $loader = include __DIR__ . '/../../../autoload.php';
    $loader->add('Demo', __DIR__);
}

再试试,demo运行起来了。


尝试了下,运行成功


通过以上的输出,可以得出demo里是对方法运行前成功捕获。为什么在thinkphp的controller里运行就不成功呢。我决定采用断点进行调试。

通过断点我发现了这个文件

<?PHP
// file: ./vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/AopComposerLoader.php

public function loadClass($class)
{
    if ($file = $this->original->findFile($class)) {
        $isInternal = false;
        foreach ($this->internalNamespaces as $ns) {
            if (strpos($class, $ns) === 0) {
                $isInternal = true;
                break;
            }
        }

        include ($isInternal ? $file : FilterInjectorTransformer::rewrite($file));
    }
}

这是一个autoload,每个类的载入都会经过它,并且会对其判断是否为内部类,不是的都会进入后续的操作。通过断点进入 FilterInjectorTransformer,发现会对load的文件进行语法解析,并根据注册的annotation对相关的类生成proxy类。说道这,大家就明白了go-aop是如何做到切入你的程序了吧,生成的proxy类,可以在你配置的cache-dir(我配置的是./runtime/aop_cache/)里看到。

同时./runtime/aop_cache/ 文件夹下也生成了很多东西,通过查看aop_cache文件内产生了与Test1文件名相同的文件,打开文件,发现它代理了原有的Test1控制器。这一系列信息,可以得出,Go!AOP 通过”劫持” composer autoload 让每个类都进过它,根据aspect的定义来决定是否为其创建一个代理类,并植入advice。
额,ThinkPHP5是把composer autoload里的东西copy出来,放到自己autoload里,然后就没composer啥事了。然后go-aop一直等不到composer autoload下发的命令,自然就不能起作用了,so,下一步

改进ThinkPHP5

在ThinkPHP5里,默认有且只会注册一个TP5内部的 Loader,并不会把include请求下发给composer的autoload。所以,为其让go-aop起作用,那么必须让让include class的请求经过 AopComposerLoad.
我们看看这个文件

<?PHP
// ./vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/AopComposerLoader.php:57

public static function init()
{
    $loaders = spl_autoload_functions();

    foreach ($loaders as &$loader) {
        $loaderToUnregister = $loader;
        if (is_array($loader) && ($loader[0] instanceof ClassLoader)) {
            $originalLoader = $loader[0];

            // Configure library loader for doctrine annotation loader
            AnnotationRegistry::registerLoader(function ($class) use ($originalLoader) {
                $originalLoader->loadClass($class);

                return class_exists($class, false);
            });
            $loader[0] = new AopComposerLoader($loader[0]);
        }
        spl_autoload_unregister($loaderToUnregister);
    }
    unset($loader);

    foreach ($loaders as $loader) {
        spl_autoload_register($loader);
    }
}

这个文件里有个类型检测,检测autoload callback是否为Classloader类型,然而ThinkPHP5不是,通过断点你会发现ThinkPHP5是一个字符串数组,so,这里也就无法把go-aop注册到class loader的callback当中了。

这里就要提一下PHP autoload机制了,这是现代PHP非常重要的一个功能,它让我们在用到一个类时,通过名字能自动加载文件。我们通过定义一定的类名规则与文件结构目录,再加上能实现以上规则的函数就能实现自动加载了。在通过 spl_autoload_register 函数的第三个参数 prepend 设置为true,就能让其排在在TP5的loader前面,先一步被调用。

依照如上原理,就可以做如下改进
这个是为go-aop包装的新autoload,本质上是在原来的ThinkPHP5的loader上加了一个壳而已。

<?PHP
// file: ./application/tests 

namespace app\tests;

require_once __DIR__ . './../../vendor/composer/ClassLoader.php';

use think\Loader;
use \Composer\Autoload\ClassLoader;
use Go\Instrument\Transformer\FilterInjectorTransformer;
use Go\Instrument\ClassLoading\AopComposerLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;


class ThinkPhpLoaderWrapper extends ClassLoader
{
    static protected $thinkLoader = Loader::class;

    /**
     * Autoload a class by it's name
     */
    public function loadClass($class)
    {
        return Loader::autoload($class);
    }

    /**
     * {@inheritDoc}
     */
    public function findFile($class)
    {
        $allowedNamespace = [
            'app\tests\controller'
        ];
        $isAllowed = false;
        foreach ($allowedNamespace as $ns) {
            if (strpos($class, $ns) === 0) {
                $isAllowed = true;
                break;
            }
        }
        // 不允许被AOP的类,则不进入AopComposer
        if(!$isAllowed)
            return false;

        $obj = new Loader;
        $observer = new \ReflectionClass(Loader::class);

        $method = $observer->getMethod('findFile');
        $method->setAccessible(true);
        $file = $method->invoke($obj, $class);
        return $file;
    }
}
<?PHP
// file: ./application/tests/behavior/Bootstrap.php 在刚刚我们新添加的文件当中
// 这个方法 \app\tests\behavior\Bootstrap::moduleInit 的后面追加如下内容

// 组成AOPComposerAutoLoader
$originalLoader = $thinkLoader = new ThinkPhpLoaderWrapper();
AnnotationRegistry::registerLoader(function ($class) use ($originalLoader) {
    $originalLoader->loadClass($class);

    return class_exists($class, false);
});
$aopLoader = new AopComposerLoader($thinkLoader);
spl_autoload_register([$aopLoader, 'loadClass'], false, true);

return $params;

在这里我们做了一个autload 并直接把它插入到了最前面(如果项目内还有其他autloader,请注意他们的先后顺序)。

最后

现在我们再访问一下 http://127.0.0.1/tests/test1/test1 你就能看到来自 aspect 输出的信息了。
最后我们做个总结:

  1. PHP7目前没有拓展实现的 AOP
  2. ThinkPHP5 有着自己的 Autoloader
  3. Go!AOP 的AOP实现依赖Class Autoloadcallback,通过替换原文件指向Proxy类实现。
  4. ThinkPHP5 整合 Go!AOP 需要调整 autoload

Windows下 VirtualBox Linux 挂载 宿主机磁盘 vbox

Windows 环境下 VirtualBox 如何挂载磁盘到 Linux 呢?

Windows 下 Virutal Box Linux 磁盘挂载

为什么

往往,我们是在Windows 环境下编辑代码,在Linux 环境中挂载代码文件并运行。这样编辑了代码,能立马运行看到结果,而不是通过ftp、scp或者rsync同步代码。

共享文件

Virtual Box 这款优良的开源虚拟机软件,为我们提供了方便的文件挂载功能。不过不是开箱即用,需要而外安装软件——VBoxGuestAdditions

今天就讲以下如何配置文件挂载。

开始安装

  1. 检查下当前Virtual Box(后简称vbox) 软件版本号。好的,我们可以得到版本号——5.1.26

vbox version

  1. 访问 http://download.virtualbox.org/virtualbox/ 找到对应版本的目录,比如我这里就访问的 http://download.virtualbox.org/virtualbox/5.1.26/ ,然后下载对应版本的 VBoxGuestAdditions 软件,也就是:http://download.virtualbox.org/virtualbox/5.1.26/VBoxGuestAdditions_5.1.26.iso

vboxguest

  1. 在vbox将这个 iso 文件挂载到 Linux 虚拟机的光盘上

add iso

  1. 讲下载的ISO文件分配到 Linux 虚拟光驱上
## 通过mount命令将cdrom挂载到/mnt/ISO
$ mkdir /mnt/ISO

$ cd /mnt/ISO

$ ls

ls

注意:可能需要 sudo 权限!加上即可。

  1. 运行 VBoxGuestAdditions 安装脚本
$ sudo sh ./VBoxLinuxAddition.run

如果提示软件依赖,试着安装 gcc gcc-c++ make kernel-devel kernel-headers

run

  1. 配置共享目录

共享文件夹目录就是 Windows 的目录,共享文件夹名称就是一个代号。

share

  1. Linux 虚拟机内挂载
## 创建一个目录用来放挂载 Windows 目录
$ mkdir /mnt/share

## 挂载 共享目录,share 就是 步骤6 中的 共享文件夹名称
$ sudo mount -t vboxsf share /mnt/share

## 以 555 权限挂载
$ sudo mount -t vboxsf -o umask=222 share /c/users-ro

## 以 777 权限挂载
sudo mount -t vboxsf -o umask=000,dmode=777,fmode=777 share /c/users

ls 777 555

注意文件挂载的 访问mod是不可以改变的,也就是在挂载的那一刻就决定了(Windows,Linux的文件权限体系差别挺大的,就别强求了),如果你对某些文件的访问有特殊配置要求,建议单独挂载目录。

  1. 解除挂载
$ sudo umount /c/users
$ sudo umount /c/users-ro