├── .gitignore ├── deployments ├── app-caculate-fpm-separate.yaml ├── app-caculate-nginx-ingress.yaml ├── app-caculate-nginx-sidecar.yaml └── ns.yaml ├── dockerfile ├── lumen.Dockerfile └── runtime.Dockerfile ├── readme-img ├── mesh.jpg ├── nginx-ingress.jpg ├── separate.jpg └── sidecar.jpg ├── readme.md └── src └── lumen-app ├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .styleci.yml ├── Dockerfile ├── README.md ├── app ├── Console │ ├── Commands │ │ └── .gitkeep │ └── Kernel.php ├── Events │ ├── Event.php │ └── ExampleEvent.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ └── ExampleController.php │ └── Middleware │ │ ├── Authenticate.php │ │ └── ExampleMiddleware.php ├── Jobs │ ├── ExampleJob.php │ └── Job.php ├── Listeners │ └── ExampleListener.php ├── Models │ └── User.php └── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ └── EventServiceProvider.php ├── artisan ├── bootstrap └── app.php ├── composer.json ├── composer.lock ├── database ├── factories │ └── UserFactory.php ├── migrations │ └── .gitkeep └── seeders │ └── DatabaseSeeder.php ├── phpunit.xml ├── public ├── .htaccess └── index.php ├── purecode.Dockerfile ├── resources └── views │ └── .gitkeep ├── routes └── web.php ├── storage ├── app │ └── .gitignore ├── framework │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore └── tests ├── ExampleTest.php └── TestCase.php /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | -------------------------------------------------------------------------------- /deployments/app-caculate-fpm-separate.yaml: -------------------------------------------------------------------------------- 1 | 2 | # 为 php-fpm 部署 service 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: caculate-standalone 7 | namespace: php-test 8 | labels: 9 | app: caculate-standalone 10 | spec: 11 | ports: 12 | - port: 9000 13 | name: tcp 14 | selector: 15 | app: caculate-standalone 16 | 17 | --- 18 | # 部署具体应用 19 | apiVersion: apps/v1 20 | kind: Deployment 21 | metadata: 22 | name: caculate-standalone 23 | namespace: php-test 24 | labels: 25 | app: caculate-standalone 26 | spec: 27 | replicas: 1 28 | selector: 29 | matchLabels: 30 | app: caculate-standalone 31 | template: 32 | metadata: 33 | labels: 34 | app: caculate-standalone 35 | spec: 36 | containers: 37 | - image: cloudbeer/php-caculate:1.0 38 | name: caculate 39 | resources: 40 | requests: 41 | memory: "1Gi" 42 | cpu: "500m" 43 | limits: 44 | memory: "1Gi" 45 | cpu: "500m" 46 | ports: 47 | - containerPort: 9000 48 | protocol: TCP 49 | --- 50 | # nginx config 51 | apiVersion: v1 52 | kind: ConfigMap 53 | metadata: 54 | name: caculate-standalone-config 55 | namespace: php-test 56 | data: 57 | config: |- 58 | server { 59 | listen 8081; 60 | root /app/public; 61 | index index.php 62 | charset utf-8; 63 | # sendfile off; 64 | 65 | location / { 66 | try_files $uri $uri/ /index.php?$args; 67 | } 68 | 69 | location ~ \.php$ { 70 | # fastcgi_split_path_info ^(.+?\.php)(/.*)$; 71 | fastcgi_pass caculate-standalone:9000; 72 | fastcgi_index index.php; 73 | fastcgi_param SCRIPT_FILENAME /app/public/index.php; 74 | include fastcgi_params; 75 | } 76 | 77 | } 78 | --- 79 | # nginx service deploy 80 | apiVersion: v1 81 | kind: Service 82 | metadata: 83 | name: caculate-standalone-service 84 | namespace: php-test 85 | spec: 86 | type: LoadBalancer 87 | selector: 88 | app: nginx 89 | ports: 90 | - protocol: TCP 91 | port: 8081 92 | targetPort: 8081 93 | 94 | --- 95 | # nginx app deploy 96 | apiVersion: apps/v1 97 | kind: Deployment 98 | metadata: 99 | name: caculate-standalone-nginx-deployment 100 | namespace: php-test 101 | spec: 102 | selector: 103 | matchLabels: 104 | app: nginx 105 | template: 106 | metadata: 107 | labels: 108 | app: nginx 109 | spec: 110 | containers: 111 | - name: nginx 112 | image: nginx:alpine 113 | volumeMounts: 114 | - name: nginx-php-config 115 | mountPath: /etc/nginx/conf.d/caculate-standalone.conf 116 | subPath: caculate-standalone.conf 117 | volumes: 118 | - name: nginx-php-config 119 | configMap: 120 | name: caculate-standalone-config 121 | items: 122 | - key: config 123 | path: caculate-standalone.conf -------------------------------------------------------------------------------- /deployments/app-caculate-nginx-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: caculate-nginx-ingress-config 5 | namespace: php-test 6 | data: 7 | SCRIPT_FILENAME: "/app/public/index.php" 8 | 9 | --- 10 | apiVersion: v1 11 | kind: Service 12 | metadata: 13 | name: caculate-standalone-fcgi 14 | namespace: php-test 15 | labels: 16 | app: caculate-standalone-fcgi 17 | spec: 18 | ports: 19 | - port: 9000 20 | targetPort: 9000 21 | name: fastcgi 22 | selector: 23 | app: caculate-standalone 24 | 25 | --- 26 | apiVersion: networking.k8s.io/v1beta1 27 | kind: Ingress 28 | metadata: 29 | annotations: 30 | kubernetes.io/ingress.class: "ng-ingress-cal" 31 | nginx.ingress.kubernetes.io/backend-protocol: "FCGI" 32 | nginx.ingress.kubernetes.io/fastcgi-index: "index.php" 33 | # nginx.ingress.kubernetes.io/configuration-snippet: | 34 | # location / { 35 | # try_files $uri $uri/ /index.php?$args; 36 | # } 37 | nginx.ingress.kubernetes.io/fastcgi-params-configmap: "caculate-nginx-ingress-config" 38 | name: caculate-nginx-ingress 39 | namespace: php-test 40 | spec: 41 | rules: 42 | - http: 43 | paths: 44 | - backend: 45 | serviceName: caculate-standalone 46 | servicePort: 9000 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /deployments/app-caculate-nginx-sidecar.yaml: -------------------------------------------------------------------------------- 1 | 2 | # nginx config 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | name: caculate-sidecar-config 7 | namespace: php-test 8 | data: 9 | config: |- 10 | server { 11 | listen 8081; 12 | root /app/public; 13 | index index.php 14 | charset utf-8; 15 | # sendfile off; 16 | 17 | location / { 18 | try_files $uri $uri/ /index.php?$args; 19 | } 20 | 21 | location ~ \.php$ { 22 | # fastcgi_split_path_info ^(.+?\.php)(/.*)$; 23 | fastcgi_pass 127.0.0.1:9000; 24 | fastcgi_index index.php; 25 | fastcgi_param SCRIPT_FILENAME /app/public/index.php; 26 | include fastcgi_params; 27 | } 28 | 29 | } 30 | --- 31 | # 部署具体应用 32 | apiVersion: apps/v1 33 | kind: Deployment 34 | metadata: 35 | name: caculate-nginx-sidecar 36 | namespace: php-test 37 | labels: 38 | app: caculate-nginx-sidecar 39 | spec: 40 | replicas: 1 41 | selector: 42 | matchLabels: 43 | app: caculate-nginx-sidecar 44 | template: 45 | metadata: 46 | labels: 47 | app: caculate-nginx-sidecar 48 | spec: 49 | volumes: 50 | # web app 的工作目录,绑定到内存中 51 | - name: webapp-data 52 | emptyDir: {} 53 | # nginx 的配置文件,存在 cm 中 54 | - name: nginx-php-config 55 | configMap: 56 | name: caculate-sidecar-config 57 | items: 58 | - key: config 59 | path: caculate-sidecar.conf 60 | # 初始化,将 镜像中的源文件拷贝到容器内存。 61 | initContainers: 62 | - name: copy-code 63 | image: cloudbeer/php-caculate-purecode:1.0 64 | volumeMounts: 65 | - name: webapp-data 66 | mountPath: /webapp 67 | command: [cp, -R, /app/, /webapp] 68 | - name: copy-lumen 69 | image: cloudbeer/my-lumen:1.0 70 | volumeMounts: 71 | - name: webapp-data 72 | mountPath: /webapp 73 | command: [cp, -R, /app/, /webapp] 74 | containers: 75 | # fpm app 76 | - name: caculate 77 | image: cloudbeer/php-runtime:1.0 78 | resources: 79 | requests: 80 | memory: "1Gi" 81 | cpu: "500m" 82 | limits: 83 | memory: "1Gi" 84 | cpu: "500m" 85 | ports: 86 | - containerPort: 9000 87 | protocol: TCP 88 | volumeMounts: 89 | - name: webapp-data 90 | mountPath: /app 91 | subPath: app 92 | # nginx sidecar 93 | - name: nginx-sidecar 94 | image: nginx:alpine 95 | volumeMounts: 96 | - name: nginx-php-config 97 | mountPath: /etc/nginx/conf.d/caculate-sidecar.conf 98 | subPath: caculate-sidecar.conf 99 | - name: webapp-data 100 | mountPath: /app 101 | subPath: app 102 | # imagePullSecrets: 103 | # - name: wellxiecred 104 | --- 105 | # nginx service deploy 106 | apiVersion: v1 107 | kind: Service 108 | metadata: 109 | name: caculate-sidecar-service 110 | namespace: php-test 111 | spec: 112 | type: LoadBalancer 113 | selector: 114 | app: caculate-nginx-sidecar 115 | ports: 116 | - protocol: TCP 117 | port: 8081 118 | targetPort: 8081 119 | -------------------------------------------------------------------------------- /deployments/ns.yaml: -------------------------------------------------------------------------------- 1 | # 创建命名空间 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: php-test 6 | spec: 7 | finalizers: 8 | - kubernetes 9 | -------------------------------------------------------------------------------- /dockerfile/lumen.Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM ccr.ccs.tencentyun.com/wellxie/php-runtime:1.0 2 | FROM cloudbeer/php-runtime:1.0 3 | 4 | RUN mkdir /app 5 | WORKDIR /app 6 | 7 | RUN echo '{\ 8 | "require": {\ 9 | "php": "^7.3|^8.0",\ 10 | "laravel/lumen-framework": "^8.0"\ 11 | }\ 12 | }' > composer.json 13 | 14 | RUN composer i 15 | 16 | -------------------------------------------------------------------------------- /dockerfile/runtime.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0.3-fpm 2 | 3 | 4 | # 安装php模块,8.0.3 已内置了如下: 5 | # Core 6 | # ctype 7 | # curl 8 | # date 9 | # dom 10 | # fileinfo 11 | # filter 12 | # ftp 13 | # hash 14 | # iconv 15 | # json 16 | # libxml 17 | # mbstring 18 | # mysqlnd 19 | # openssl 20 | # pcre 21 | # PDO 22 | # pdo_sqlite 23 | # Phar 24 | # posix 25 | # readline 26 | # Reflection 27 | # session 28 | # SimpleXML 29 | # sodium 30 | # SPL 31 | # sqlite3 32 | # standard 33 | # tokenizer 34 | # xml 35 | # xmlreader 36 | # xmlwriter 37 | # zlib 38 | 39 | # 自定义类库示例 (composer 包安装依赖 git) 40 | RUN apt-get update && \ 41 | apt-get install -y git zlib1g-dev libpng-dev libicu-dev 42 | 43 | RUN docker-php-ext-install \ 44 | gd \ 45 | intl 46 | 47 | 48 | # configure intl 49 | RUN docker-php-ext-configure intl 50 | 51 | 52 | 53 | # 安装 composer 54 | RUN curl -sS https://getcomposer.org/installer | php -- \ 55 | --install-dir=/usr/local/bin --filename=composer \ 56 | && chmod +x /usr/local/bin/composer 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /readme-img/mesh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudbeer/php-best-practice/bfb089288abf4f9d469eba1f80798c2ccd95f43d/readme-img/mesh.jpg -------------------------------------------------------------------------------- /readme-img/nginx-ingress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudbeer/php-best-practice/bfb089288abf4f9d469eba1f80798c2ccd95f43d/readme-img/nginx-ingress.jpg -------------------------------------------------------------------------------- /readme-img/separate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudbeer/php-best-practice/bfb089288abf4f9d469eba1f80798c2ccd95f43d/readme-img/separate.jpg -------------------------------------------------------------------------------- /readme-img/sidecar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudbeer/php-best-practice/bfb089288abf4f9d469eba1f80798c2ccd95f43d/readme-img/sidecar.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # php应用容器化部署实践​ 2 | 目前市场上 php 仍有一席之地。本文章将探讨如何将 php 应用容器化并迁移部署到 TKE。 3 | 4 | ## php 应用镜像准备 5 | 镜像的层次:基础依赖镜像->运行框架->应用/代码镜像 6 | 7 | 基于容器的单进程运行理念,下面的部署过程并未使用单体的 nginx+php-fpm 一体的容器运行方式,而是将 php-fpm 和 nginx 拆散。 8 | 9 | ### 基础镜像 10 | 安装基础系统依赖包和公司 php 应用中各个开发小组都会用到的扩展包。 11 | 下面的示例基于官方 fpm,安装了通用系统级的依赖和 php 包管理器。 12 | 如果可以,建议使用更基础的镜像从 php 源码进行编译。 13 | 14 | ```Dockerfile 15 | # runtime.Dockerfile 16 | FROM php:8.0.3-fpm 17 | 18 | # 自定义类库示例 19 | RUN apt-get update && \ 20 | apt-get install -y git zlib1g-dev libpng-dev libicu-dev 21 | RUN docker-php-ext-install \ 22 | gd \ 23 | intl 24 | RUN docker-php-ext-configure intl 25 | 26 | 27 | # 安装 composer 28 | RUN curl -sS https://getcomposer.org/installer | php -- \ 29 | --install-dir=/usr/local/bin --filename=composer \ 30 | && chmod +x /usr/local/bin/composer 31 | ``` 32 | 33 | 将上述文件编译成镜像,并 push 到仓库: 34 | 35 | ```shell 36 | docker build -t cloudbeer/php-runtime:1.0 -f runtime.Dockerfile . 37 | docker push cloudbeer/php-runtime:1.0 38 | ``` 39 | 40 | ### 应用层框架镜像 41 | 如果开发框架比较稳定,建议直接把框架打包成基础镜像以避免后续部署过程中频繁安装依赖包,加速发布打包发布过程,如业务开发A组使用了 lumen 框架,我们可以专门为 lumen 打一个镜像。 42 | 43 | 如下镜像,安装了 lumen web 框架。 44 | 45 | ```Dockerfile 46 | # lumen.Dockerfile 47 | FROM cloudbeer/php-runtime:1.0 48 | 49 | RUN mkdir /app 50 | WORKDIR /app 51 | 52 | RUN echo '{\ 53 | "require": {\ 54 | "php": "^7.3|^8.0",\ 55 | "laravel/lumen-framework": "^8.0"\ 56 | }\ 57 | }' > composer.json 58 | 59 | RUN composer i 60 | ``` 61 | 62 | 上述镜像打包为:cloudbeer/my-lumen:1.0 63 | 64 | ### 应用层镜像 65 | 由于我们在应用层框架里已经把 lumen 运行时都安装好了,所以这个镜像里,只需拷贝纯源码即可。记得创建 .gitignore 或者 .dockerignore 文件,排除 vender,test 等目录。 66 | ``` 67 | # .dockerignore 68 | /vendor 69 | /tests 70 | /.idea 71 | /.git 72 | .gitignore 73 | Dockerfile 74 | Homestead.json 75 | Homestead.yaml 76 | .env 77 | .ent.example 78 | .phpunit.result.cache 79 | ``` 80 | 81 | 应用镜像里只需要拷贝脚本文件即可。这个镜像包含了完整的 php 运行时和业务代码,启动后可以直接接收 fastcgi 调用。 82 | 83 | ```Dockerfile 84 | FROM cloudbeer/my-lumen:1.0 85 | COPY ./ /app/ 86 | ``` 87 | 88 | 上面的镜像打包为: cloudbeer/php-caculate:1.0 89 | 90 | 另一种打包代码方式,我们使用纯的容器将源代码打包,后面会在 K8S 中部署时将文件拷贝到框架运行时容器中运行。 91 | 92 | ```Dockerfile 93 | FROM debian:buster-slim 94 | RUN mkdir /app 95 | COPY ./ /app/ 96 | ``` 97 | 98 | 上面的镜像打包为: cloudbeer/php-caculate-purecode:1.0 99 | 100 | 代码层在还可以有更多的打包方式,如上传到对象存储里,或者使用 NFS 存储,后期绑定到容器运行时运行。 101 | 102 | 103 | ### 本地测试 104 | 105 | 启动镜像 cloudbeer/php-caculate:1.0 106 | 107 | ``` 108 | docker run --rm -it cloudbeer/php-caculate:1.0 sh 109 | 110 | # 容器内运行 111 | # 启动 php 测试 112 | php -S localhost:8000 -t public & 113 | # 查看结果 114 | curl http://localhost:8000/cpu 115 | ``` 116 | 完美运行。 117 | 118 | 目前已经完成了应用镜像打包。其中前两层镜像可以复用,真正的业务应用只需拷贝代码。 119 | 120 | 上述代码中的镜像,我均已打包上传到 docker hub 官网,可以忽略 build 和 push 过程,直接进行测试。 121 | 122 | 123 | ## 部署到 K8S/TKE 124 | 125 | php 应用部署到容器环境,最自然的一种方式是:直接将 php 的运行环境和web server 以及业务源代码打包放在一个容器中运行。这个方式是最简单的方式,php 官方也提供了 php:nginx 这种镜像底包。 126 | 127 | 但 php 运行时和 web server 是在两个进程中运行,这个不符合容器的最佳实践。一般建议将这两个进程分别运行在不同的容器中。 128 | 129 | ### nginx 作为 sidecar 运行 130 | 131 | K8S 在同一个 pod 中,可以运行多个容器。我们将 php-fpm 的业务代码部署在一个容器中,与之相伴生的有一个 nginx 容器,nginx 作为fastcgi的调用方,并可以代理一些静态资源,这个模式类似 mesh 的sidecar 模式。架构图如下: 132 | 133 | ![nginx 作为 sidecar 部署](./readme-img/sidecar.jpg) 134 | 135 | #### nginx 配置 136 | 由于 nginx 和 php-fpm 在一个 pod 中,所以只需发起 localhost 调用即可。 nginx 的配置如下,我们将这个配置写到 cm 中,后面通过 volume 绑定到容器中。这个配置有几点需要注意的: 137 | - 应用使用了 lumen 的 route 体系,所以需要将路由通过 try_files 全部指向 ./public/index.php 文件。 138 | - fastcgi_param SCRIPT_FILENAME /app/public/index.php 这里也将所有脚本指向这个文件。 139 | 140 | ```yaml 141 | # nginx config 142 | apiVersion: v1 143 | kind: ConfigMap 144 | metadata: 145 | name: caculate-sidecar-config 146 | namespace: php-test 147 | data: 148 | config: |- 149 | server { 150 | listen 8081; 151 | root /app/public; 152 | index index.php 153 | charset utf-8; 154 | 155 | location / { 156 | try_files $uri $uri/ /index.php?$args; 157 | } 158 | 159 | location ~ \.php$ { 160 | fastcgi_pass 127.0.0.1:9000; 161 | fastcgi_index index.php; 162 | fastcgi_param SCRIPT_FILENAME /app/public/index.php; 163 | include fastcgi_params; 164 | } 165 | } 166 | ``` 167 | 168 | #### 应用部署脚本 169 | 170 | 在下面的部署脚本中,有几点值得关注一下: 171 | - 使用了 emptyDir:{} 作为容器的源代码存储介质,这样可以将应用读取到临时目录中,加速运行时 php 源码的加载。如果脚本文件不大,可以指定 emptyDir 使用内存运行,这个可以更加加速脚本加载。 172 | - pod 启动的时候使用了 2 个 初始化容器,使用的镜像分别是:极简源代码的镜像(php-caculate-purecode)和框架运行时镜像(my-lumen),在启动的时候分别将 /app 的代码拷贝到了 emptyDir 卷中,分别拷贝了lumen的 vendor 依赖和业务源代码。这里拷贝业务源代码过程亦可以使用 cfs 或者 cos 来实现。由于使用了宿主机存储源代码,当源代码过于庞大的时候,请注意超出宿主机存储容量风险。 173 | - 由于源码和依赖包都已经在 initContainers 组织好了,所以,只需要使用 php 基础容器来启动应用即可。 174 | 175 | 176 | ```yaml 177 | apiVersion: apps/v1 178 | kind: Deployment 179 | metadata: 180 | name: caculate-nginx-sidecar 181 | namespace: php-test 182 | labels: 183 | app: caculate-nginx-sidecar 184 | spec: 185 | replicas: 1 186 | selector: 187 | matchLabels: 188 | app: caculate-nginx-sidecar 189 | template: 190 | metadata: 191 | labels: 192 | app: caculate-nginx-sidecar 193 | spec: 194 | volumes: 195 | # web app 的工作目录 196 | - name: webapp-data 197 | emptyDir: {} 198 | # nginx 的配置文件,从 cm 中绑定过来 199 | - name: nginx-php-config 200 | configMap: 201 | name: caculate-sidecar-config 202 | items: 203 | - key: config 204 | path: caculate-sidecar.conf 205 | # 初始化,将镜像中的源文件拷贝到容器内存。 206 | initContainers: 207 | - name: copy-code 208 | image: cloudbeer/php-caculate-purecode:1.0 209 | volumeMounts: 210 | - name: webapp-data 211 | mountPath: /webapp 212 | command: [cp, -R, /app/, /webapp] 213 | - name: copy-lumen 214 | image: cloudbeer/my-lumen:1.0 215 | volumeMounts: 216 | - name: webapp-data 217 | mountPath: /webapp 218 | command: [cp, -R, /app/, /webapp] 219 | containers: 220 | # fpm 应用运行环境 221 | - name: caculate 222 | image: cloudbeer/php-runtime:1.0 223 | resources: 224 | requests: 225 | memory: "1Gi" 226 | cpu: "500m" 227 | limits: 228 | memory: "1Gi" 229 | cpu: "500m" 230 | ports: 231 | - containerPort: 9000 232 | protocol: TCP 233 | volumeMounts: 234 | - name: webapp-data 235 | mountPath: /app 236 | subPath: app 237 | # nginx sidecar 238 | - name: nginx-sidecar 239 | image: nginx:alpine 240 | volumeMounts: 241 | - name: nginx-php-config 242 | mountPath: /etc/nginx/conf.d/caculate-sidecar.conf 243 | subPath: caculate-sidecar.conf 244 | - name: webapp-data 245 | mountPath: /app 246 | subPath: app 247 | --- 248 | ``` 249 | 250 | 挂一个 LoadBalancer 看看。 251 | 252 | ```yaml 253 | apiVersion: v1 254 | kind: Service 255 | metadata: 256 | name: caculate-sidecar-service 257 | namespace: php-test 258 | spec: 259 | type: LoadBalancer 260 | selector: 261 | app: caculate-nginx-sidecar 262 | ports: 263 | - protocol: TCP 264 | port: 8081 265 | targetPort: 8081 266 | ``` 267 | 访问对应的 外部 ip:8081,完美运行。 268 | 269 | ### nginx 独立部署 270 | 271 | 通常情况下,运维部门希望将 web server 收敛并统一管理,开发也不太关心 nginx 的具体配置,将两者进行拆分众望所归,并且在微服务的横向扩展中,这种方式也更加“优雅”。 272 | 273 | 部署架构图如下: 274 | 275 | ![nginx 独立部署架构](./readme-img/separate.jpg) 276 | 277 | 278 | 279 | #### 部署 fpm 业务应用 280 | - 此处部署了 php-caculate 镜像,此镜像里包含了源代码,Web框架以及 php 运行时,是一个完整的 php-fpm 业务应用。 281 | - 通过 service 发布应用,以便 nginx 能发现 fpm 服务,并解偶了 webserver 和 业务服务。后期可以做纯php 业务的横向扩展和 nginx 的集中管理。 282 | 283 | ```yaml 284 | # 为 php-fpm 部署 service 285 | apiVersion: v1 286 | kind: Service 287 | metadata: 288 | name: caculate-standalone 289 | namespace: php-test 290 | labels: 291 | app: caculate-standalone 292 | spec: 293 | ports: 294 | - port: 9000 295 | name: tcp 296 | selector: 297 | app: caculate-standalone 298 | 299 | --- 300 | # 部署具体应用 301 | apiVersion: apps/v1 302 | kind: Deployment 303 | metadata: 304 | name: caculate-standalone 305 | namespace: php-test 306 | labels: 307 | app: caculate-standalone 308 | spec: 309 | replicas: 1 310 | selector: 311 | matchLabels: 312 | app: caculate-standalone 313 | template: 314 | metadata: 315 | labels: 316 | app: caculate-standalone 317 | spec: 318 | containers: 319 | - image: cloudbeer/php-caculate:1.0 320 | name: caculate 321 | resources: 322 | requests: 323 | memory: "1Gi" 324 | cpu: "500m" 325 | limits: 326 | memory: "1Gi" 327 | cpu: "500m" 328 | ports: 329 | - containerPort: 9000 330 | protocol: TCP 331 | ``` 332 | 333 | #### nginx 部署 334 | 335 | 此部分的 nginx 配置基本和上面一样,唯一的区别就是 fastcgi_pass 的调用目标变成了 php 业务的 service:caculate-standalone 了。 336 | 337 | ```yaml 338 | apiVersion: v1 339 | kind: ConfigMap 340 | metadata: 341 | name: caculate-standalone-config 342 | namespace: php-test 343 | data: 344 | config: |- 345 | server { 346 | listen 8081; 347 | root /app/public; 348 | index index.php 349 | charset utf-8; 350 | 351 | location / { 352 | try_files $uri $uri/ /index.php?$args; 353 | } 354 | 355 | location ~ \.php$ { 356 | fastcgi_pass caculate-standalone:9000; 357 | fastcgi_index index.php; 358 | fastcgi_param SCRIPT_FILENAME /app/public/index.php; 359 | include fastcgi_params; 360 | } 361 | } 362 | ``` 363 | 364 | 下面为 nginx 配置单独 pod 启动。 365 | 366 | ```yaml 367 | apiVersion: apps/v1 368 | kind: Deployment 369 | metadata: 370 | name: caculate-standalone-nginx-deployment 371 | namespace: php-test 372 | spec: 373 | selector: 374 | matchLabels: 375 | app: nginx 376 | template: 377 | metadata: 378 | labels: 379 | app: nginx 380 | spec: 381 | containers: 382 | - name: nginx 383 | image: nginx:alpine 384 | volumeMounts: 385 | - name: nginx-php-config 386 | mountPath: /etc/nginx/conf.d/caculate-standalone.conf 387 | subPath: caculate-standalone.conf 388 | volumes: 389 | - name: nginx-php-config 390 | configMap: 391 | name: caculate-standalone-config 392 | items: 393 | - key: config 394 | path: caculate-standalone.conf 395 | ``` 396 | 397 | 现在,给 nginx 应用挂一个 LoadBalancer 测试一下,亦完美运行。 398 | 399 | 400 | ### 使用 nginx-ingress 部署 401 | 402 | 上面的部署架构图中,ingress 和 nginx 分别进行了部署,但 nginx-ingress 其实已经合并了这两个部分,并且 TKE 提供了现成的 nginx-ingress。现在,我们试试使用 nginx-ingress 部署。 403 | 404 | ![使用 nginx-ingress 部署](./readme-img/nginx-ingress.jpg) 405 | 406 | 407 | 在 TKE 集群内安装 nginx-ingress,参考这篇: 408 | https://cloud.tencent.com/document/product/457/50503 409 | 410 | 使用上面的已经部署好的 fpm 业务的service: caculate-standalone 。 411 | 412 | 部署脚本如下: 413 | - ingress.class 的值是创建 nginx-ingress 时候在控制台定义的。 414 | - 与纯 nginx 配置不同,nginx-ingress 无需配置 try_files 节点,下面的配置其实已经将全部请求转发到了 public/index.php。 415 | 416 | ```yaml 417 | apiVersion: v1 418 | kind: ConfigMap 419 | metadata: 420 | name: caculate-nginx-ingress-config 421 | namespace: php-test 422 | data: 423 | SCRIPT_FILENAME: "/app/public/index.php" 424 | --- 425 | apiVersion: networking.k8s.io/v1beta1 426 | kind: Ingress 427 | metadata: 428 | annotations: 429 | kubernetes.io/ingress.class: "your-ingress-class" 430 | nginx.ingress.kubernetes.io/backend-protocol: "FCGI" 431 | nginx.ingress.kubernetes.io/fastcgi-index: "index.php" 432 | nginx.ingress.kubernetes.io/fastcgi-params-configmap: "caculate-nginx-ingress-config" 433 | name: caculate-nginx-ingress 434 | namespace: php-test 435 | spec: 436 | rules: 437 | - http: 438 | paths: 439 | - backend: 440 | serviceName: caculate-standalone 441 | servicePort: 9000 442 | ``` 443 | TKE 的 nginx-ingress 直接提供了一个外网地址,访问一下试试,完美运行。 444 | 445 | 446 | 447 | ## MORE:mesh 化 448 | 449 | 在 php mesh 化中,需要考虑的问题如下: 450 | - fastcgi 使用 TCP 协议,并且有自己的序列化方法,此特性并未在 istio 和 envoy 中支持,无法进行精细的流量控制。 451 | - fpm + envoy sidecar 可以实现服务级别的流量控制。 fpm + nginx-sidecar + envoy sidecar 将 fastcgi 调用转化为 http 调用,可以实现基于 http 协议的精细流量控制。 452 | - 实现调用链监控,需要使用 http 进行远程调用,有可能需要改造代码,在header 中封装 opentracing 。 453 | 454 | 当下,php mesh 的部署模式建议采用 fpm 业务层,nginx-sidecar, envoy-sidecar 三个容器为一个pod 的部署模式。 455 | 456 | 架构图如下: 457 | 458 | ![php 应用的 mesh 部署架构](./readme-img/mesh.jpg) 459 | 460 | 461 | 此处的部署与第一部分的内容 - nginx 作为 sidecar 运行类似,在腾讯云中需要开通 TCM,并注入 envoy 的 sidecar。 462 | 463 | 464 | --- 465 | 466 | 本文相关的源代码说明: 467 | 位置:https://github.com/cloudbeer/php-best-practice 468 | 469 | ## src/lumen-app/ 470 | php 业务应用,映射了2个路径 471 | - /cpu: 两种方式计算了 pi 472 | - /mem: 在数组里塞了字符串为了撑爆内存 473 | 474 | ## deployments/ 475 | 476 | 部署代码 477 | 478 | - app-caculate-fpm-separate.yaml: nginx 和 fpm 分在不同的 pod 中部署 479 | - app-caculate-nginx-ingress.yaml: nginx-ingress 直接代理 fpm 服务 480 | - app-caculate-nginx-sidecar.yaml: nginx 和 fpm 部署在同一个pod中的两个容器 481 | 482 | ## dockerfile/ 483 | 484 | - lumen.Dockerfile:包含 lumen 框架的运行环境 485 | - runtime.Dockerfile:php 基础运行环境 486 | 487 | 业务代码的 Dockerfile 位于 src/lumen-app/Dockerfile 和 src/lumen-app/purecode.Dockerfile 488 | 489 | 490 | 491 | 492 | -------------------------------------------------------------------------------- /src/lumen-app/.dockerignore: -------------------------------------------------------------------------------- 1 | # .dockerignore 2 | /vendor 3 | /.idea 4 | /.git 5 | .gitignore 6 | /tests 7 | Dockerfile 8 | Homestead.json 9 | Homestead.yaml 10 | .env 11 | .ent.example 12 | .phpunit.result.cache -------------------------------------------------------------------------------- /src/lumen-app/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /src/lumen-app/.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Lumen 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | APP_TIMEZONE=UTC 7 | 8 | LOG_CHANNEL=stack 9 | LOG_SLACK_WEBHOOK_URL= 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE=homestead 15 | DB_USERNAME=homestead 16 | DB_PASSWORD=secret 17 | 18 | CACHE_DRIVER=file 19 | QUEUE_CONNECTION=sync 20 | -------------------------------------------------------------------------------- /src/lumen-app/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | Homestead.json 4 | Homestead.yaml 5 | .env 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /src/lumen-app/.styleci.yml: -------------------------------------------------------------------------------- 1 | php: 2 | preset: laravel 3 | disabled: 4 | - unused_use 5 | js: true 6 | css: true 7 | -------------------------------------------------------------------------------- /src/lumen-app/Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM ccr.ccs.tencentyun.com/wellxie/my-lumen:1.0 2 | FROM cloudbeer/my-lumen:1.0 3 | 4 | COPY ./ /app/ 5 | -------------------------------------------------------------------------------- /src/lumen-app/README.md: -------------------------------------------------------------------------------- 1 | # Lumen PHP Framework 2 | 3 | [![Build Status](https://travis-ci.org/laravel/lumen-framework.svg)](https://travis-ci.org/laravel/lumen-framework) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/laravel/framework)](https://packagist.org/packages/laravel/lumen-framework) 5 | [![Latest Stable Version](https://img.shields.io/packagist/v/laravel/framework)](https://packagist.org/packages/laravel/lumen-framework) 6 | [![License](https://img.shields.io/packagist/l/laravel/framework)](https://packagist.org/packages/laravel/lumen-framework) 7 | 8 | Laravel Lumen is a stunningly fast PHP micro-framework for building web applications with expressive, elegant syntax. We believe development must be an enjoyable, creative experience to be truly fulfilling. Lumen attempts to take the pain out of development by easing common tasks used in the majority of web projects, such as routing, database abstraction, queueing, and caching. 9 | 10 | ## Official Documentation 11 | 12 | Documentation for the framework can be found on the [Lumen website](https://lumen.laravel.com/docs). 13 | 14 | ## Contributing 15 | 16 | Thank you for considering contributing to Lumen! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). 17 | 18 | ## Security Vulnerabilities 19 | 20 | If you discover a security vulnerability within Lumen, please send an e-mail to Taylor Otwell at taylor@laravel.com. All security vulnerabilities will be promptly addressed. 21 | 22 | ## License 23 | 24 | The Lumen framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). 25 | -------------------------------------------------------------------------------- /src/lumen-app/app/Console/Commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudbeer/php-best-practice/bfb089288abf4f9d469eba1f80798c2ccd95f43d/src/lumen-app/app/Console/Commands/.gitkeep -------------------------------------------------------------------------------- /src/lumen-app/app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 26 | } 27 | 28 | /** 29 | * Handle an incoming request. 30 | * 31 | * @param \Illuminate\Http\Request $request 32 | * @param \Closure $next 33 | * @param string|null $guard 34 | * @return mixed 35 | */ 36 | public function handle($request, Closure $next, $guard = null) 37 | { 38 | if ($this->auth->guard($guard)->guest()) { 39 | return response('Unauthorized.', 401); 40 | } 41 | 42 | return $next($request); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lumen-app/app/Http/Middleware/ExampleMiddleware.php: -------------------------------------------------------------------------------- 1 | app['auth']->viaRequest('api', function ($request) { 34 | if ($request->input('api_token')) { 35 | return User::where('api_token', $request->input('api_token'))->first(); 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lumen-app/app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 16 | \App\Listeners\ExampleListener::class, 17 | ], 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /src/lumen-app/artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make( 32 | 'Illuminate\Contracts\Console\Kernel' 33 | ); 34 | 35 | exit($kernel->handle(new ArgvInput, new ConsoleOutput)); 36 | -------------------------------------------------------------------------------- /src/lumen-app/bootstrap/app.php: -------------------------------------------------------------------------------- 1 | bootstrap(); 8 | 9 | date_default_timezone_set(env('APP_TIMEZONE', 'UTC')); 10 | 11 | /* 12 | |-------------------------------------------------------------------------- 13 | | Create The Application 14 | |-------------------------------------------------------------------------- 15 | | 16 | | Here we will load the environment and create the application instance 17 | | that serves as the central piece of this framework. We'll use this 18 | | application as an "IoC" container and router for this framework. 19 | | 20 | */ 21 | 22 | $app = new Laravel\Lumen\Application( 23 | dirname(__DIR__) 24 | ); 25 | 26 | // $app->withFacades(); 27 | 28 | // $app->withEloquent(); 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Register Container Bindings 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Now we will register a few bindings in the service container. We will 36 | | register the exception handler and the console kernel. You may add 37 | | your own bindings here if you like or you can make another file. 38 | | 39 | */ 40 | 41 | $app->singleton( 42 | Illuminate\Contracts\Debug\ExceptionHandler::class, 43 | App\Exceptions\Handler::class 44 | ); 45 | 46 | $app->singleton( 47 | Illuminate\Contracts\Console\Kernel::class, 48 | App\Console\Kernel::class 49 | ); 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Register Config Files 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Now we will register the "app" configuration file. If the file exists in 57 | | your configuration directory it will be loaded; otherwise, we'll load 58 | | the default version. You may register other files below as needed. 59 | | 60 | */ 61 | 62 | $app->configure('app'); 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Register Middleware 67 | |-------------------------------------------------------------------------- 68 | | 69 | | Next, we will register the middleware with the application. These can 70 | | be global middleware that run before and after each request into a 71 | | route or middleware that'll be assigned to some specific routes. 72 | | 73 | */ 74 | 75 | // $app->middleware([ 76 | // App\Http\Middleware\ExampleMiddleware::class 77 | // ]); 78 | 79 | // $app->routeMiddleware([ 80 | // 'auth' => App\Http\Middleware\Authenticate::class, 81 | // ]); 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Register Service Providers 86 | |-------------------------------------------------------------------------- 87 | | 88 | | Here we will register all of the application's service providers which 89 | | are used to bind services into the container. Service providers are 90 | | totally optional, so you are not required to uncomment this line. 91 | | 92 | */ 93 | 94 | // $app->register(App\Providers\AppServiceProvider::class); 95 | // $app->register(App\Providers\AuthServiceProvider::class); 96 | // $app->register(App\Providers\EventServiceProvider::class); 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Load The Application Routes 101 | |-------------------------------------------------------------------------- 102 | | 103 | | Next we will include the routes file so that they can all be added to 104 | | the application. This will provide all of the URLs the application 105 | | can respond to, as well as the controllers that may handle them. 106 | | 107 | */ 108 | 109 | $app->router->group([ 110 | 'namespace' => 'App\Http\Controllers', 111 | ], function ($router) { 112 | require __DIR__.'/../routes/web.php'; 113 | }); 114 | 115 | return $app; 116 | -------------------------------------------------------------------------------- /src/lumen-app/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/lumen", 3 | "description": "The Laravel Lumen Framework.", 4 | "keywords": ["framework", "laravel", "lumen"], 5 | "license": "MIT", 6 | "type": "project", 7 | "require": { 8 | "php": "^7.3|^8.0", 9 | "laravel/lumen-framework": "^8.0" 10 | }, 11 | "require-dev": { 12 | "fakerphp/faker": "^1.9.1", 13 | "mockery/mockery": "^1.3.1", 14 | "phpunit/phpunit": "^9.3" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "App\\": "app/", 19 | "Database\\Factories\\": "database/factories/", 20 | "Database\\Seeders\\": "database/seeders/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "classmap": [ 25 | "tests/" 26 | ] 27 | }, 28 | "config": { 29 | "preferred-install": "dist", 30 | "sort-packages": true, 31 | "optimize-autoloader": true 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true, 35 | "scripts": { 36 | "post-root-package-install": [ 37 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lumen-app/database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 26 | 'email' => $this->faker->unique()->safeEmail, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lumen-app/database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudbeer/php-best-practice/bfb089288abf4f9d469eba1f80798c2ccd95f43d/src/lumen-app/database/migrations/.gitkeep -------------------------------------------------------------------------------- /src/lumen-app/database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call('UsersTableSeeder'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lumen-app/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lumen-app/public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Handle Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /src/lumen-app/public/index.php: -------------------------------------------------------------------------------- 1 | run(); 29 | -------------------------------------------------------------------------------- /src/lumen-app/purecode.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | RUN mkdir /app 3 | COPY ./ /app/ 4 | -------------------------------------------------------------------------------- /src/lumen-app/resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudbeer/php-best-practice/bfb089288abf4f9d469eba1f80798c2ccd95f43d/src/lumen-app/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/lumen-app/routes/web.php: -------------------------------------------------------------------------------- 1 | get('/', function () use ($router) { 17 | return $router->app->version(); 18 | }); 19 | 20 | 21 | $router->get('/cpu', function () use ($router) { 22 | $pi = 4; $top = 4; $bot = 3; $minus = TRUE; 23 | $accuracy = 20000000; 24 | 25 | $start = microtime(true); 26 | for($i = 0; $i < $accuracy; $i++) 27 | { 28 | $pi += ( $minus ? -($top/$bot) : ($top/$bot) ); 29 | $minus = ( $minus ? FALSE : TRUE); 30 | $bot += 2; 31 | } 32 | $end = microtime(true); 33 | $time1 = (($end-$start)*1000) . 'ms'; 34 | 35 | 36 | $start = microtime(true); 37 | $bottom = 1; 38 | $pi2 = 0; 39 | for ($i = 1; $i < $accuracy; $i++) { 40 | if ($i % 2 == 1) { 41 | $pi2 += 4 / $bottom; 42 | } else { 43 | $pi2 -= 4 / $bottom; 44 | } 45 | $bottom += 2; 46 | } 47 | $end = microtime(true); 48 | $time2 = (($end-$start)*1000) . 'ms'; 49 | return array( 50 | 'pi-01'=> $pi, 51 | 'pi-02'=> $pi2, 52 | 'loop'=>$accuracy, 53 | 'pi-01-duration'=>$time1, 54 | 'pi-02-duration'=> $time2 55 | ); 56 | }); 57 | 58 | 59 | $router->get('/mem', function () use ($router) { 60 | $accuracy = 100000; 61 | $container = array(); 62 | $start = microtime(true); 63 | for ($i = 0; $i < $accuracy; $i++) { 64 | array_push($container, 'abcd' . microtime() ."-" . rand(), 'xyz' . microtime() ."-" . rand()); 65 | } 66 | $end = microtime(true); 67 | return array( 68 | 'loop'=>$accuracy, 69 | 'len'=> count($container), 70 | 'duration' => (($end-$start)*1000) . 'ms' 71 | ); 72 | 73 | }); -------------------------------------------------------------------------------- /src/lumen-app/storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/lumen-app/storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /src/lumen-app/storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/lumen-app/storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/lumen-app/storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/lumen-app/tests/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 16 | 17 | $this->assertEquals( 18 | $this->app->version(), $this->response->getContent() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lumen-app/tests/TestCase.php: -------------------------------------------------------------------------------- 1 |