在 2021 年中旬,我曾經寫過一篇 Laravel 環境設定,不過因為工具上有些許變化,所以在 2022 年末將其重新整理一遍。
事先聲明,本文中所寫的環境設計題專門為了我自己的工作流而打造。如果不適合你,那你是對的,請儘管改成適合你的工作流。
概念
- macOS 與 Linux 系統通用
- 2022 年都快過完了,還有人用 Windows 開發不用 WSL/WSL2 的?
- 輕量化、啟動速度快
- 規範化,適合團隊開發
Docker 整合
在當前的 Laravel 專案中建立 docker-compose.yaml
networks:
intranet:
driver: bridge
services:
database:
image: 'postgres:alpine'
environment:
- POSTGRES_USER=forge
- POSTGRES_PASSWORD=password
- PGPASSWORD=password
- POSTGRES_DB=forge
networks: ['intranet']
redis:
image: 'redis:alpine'
networks: ['intranet']
當然,也可以在這個檔額中加入一些將會用到的服務,例如加入 Minio 或以 MariaDB 取代 PostgreSQL。
這個檔案的目的在於「整個專案相依的第三方服務」,任何人都可以藉由點開這個檔案立即瞭解系統用了哪些第三方服務。
註:如果
environment
過於複雜,可以另外抽成一個environment.yaml
去設定
Loacl 環境建立
所謂 Local 環境是指該專案的開發者(通常是後端工程師)會使用的環境,在設計這個環境時有幾個目標:
- 開發者應該自行維護 PHP 運行環境
- 開發者應該要能夠存取這些相依服務(例如可以連進 DB)
建立 docker/compose/bind-local.yaml
將相依的服務 expose 到本地環境中:
services:
database: # PostgreSQL
ports: ['5432:5432']
redis:
ports: ['6379:6379']
此時,開發者可以利用 docker compose -f docker-compose.yaml -f docker/compose/bind-local.yaml up
啟動環境;並且利用 php artisan serve
啟動 PHP Built-in Web server 開發。
Develop 環境建立
所謂 Develop 環境是指使用此專案的開發者(例如前端工程師或 App 開發者),在設計這個環境時有幾個目標:
- 開發者不需建立 PHP 運行環境
- 開發者有一個開箱即用的 API 服務
- 開發者並不關心實際上相依服務是否正常
建立 docker/file/dev.dockerfile
建立 Image
FROM alpine
ENV PHP_PACKAGES php81 \
php81-bcmath php81-ctype php81-dom php81-exif php81-fileinfo php81-gd php81-intl php81-mbstring php81-opcache \
php81-openssl php81-pcntl php81-pdo php81-pdo_sqlite php81-pdo_pgsql php81-posix \
php81-session php81-tokenizer php81-xml php81-xmlwriter \
php81-pecl-redis php81-pecl-xdebug php81-pecl-swoole
ENV DEV_SOFTWARE composer postgresql
WORKDIR /app
RUN apk update && apk upgrade && \
apk add ${PHP_PACKAGES} ${DEV_SOFTWARE} && \
cp $(which php81) /usr/bin/php
建立 docker/entrypoint/dev-init.sh
#!/bin/sh
[ ! -d "/app/vendor" ] && composer install
until pg_isready -h database ; do
>&2 echo "Postgres is unavailable - sleeping"
sleep 1
done
php artisan key:generate --force
php artisan migrate
exec "$@"
建立 docker/compose/develop.yaml
services:
api:
image: 'laravel-api' # 名字可自行決定,通常會用專案名稱
build:
context: .
dockerfile: ./docker/file/dev.dockerfile
environment:
- DB_HOST=database
- REDIS_HOST=redis
ports: ['8000:8000']
volumes:
- .:/app
networks: ['intranet']
entrypoint: ['/app/docker/entrypoint/dev-init.sh']
command: ['php', 'artisan', 'serve', '--host=0.0.0.0']
開發者可以利用 docker compose -f docker-compose.yaml -f docker/compose/develop.yaml
啟動一個位於 8000 Port 的 API 服務,如果有需要,也可以加上 -f bind-local.yaml
將相依服務 expose 出來。
其它設定檔
其它的設定檔(例如 php.ini
)也可以利用 volume 的方式被綁進容器中,只需要修改 docker/compose/develop.yaml
的 volume 設定即可。
services:
api:
# ...
volumes:
- .:/app
- ./docker/conf/php.ini:/etc/php81/php.ini
# ...
補充說明
Docker vs. Podman/Containerd
自從 Docker Inc. 在 2022 年初調整授權條款將 Docker Engine 在某些條件下會要求訂閱 Pro 以上版本,我就一直在尋覓能夠替代的方案。不幸地,即便存在如 lima-vm/lima 這般優秀的 Docker 替代方案,卻因為生態系等因素而難以接入。
直到目前為止,在 macOS 上的容器解決方案仍以依賴 Docker Engine 為主;至於 Linux 的用戶若熟稔容器機制可自由挑選如 Podman, Containerd 等解決方案取代 Docker Engine。
為什麼不用 Laravel Sail?
Laravel Sail 是由 Laravel 官方所設計的「輕量級」開發環境,它以 Docker 為基礎並且提供一個開箱即用的開發環境。
註:相較於 Homestead 或 Valet,Sail 的設計相對合理:相較於 Valet 有良好的隔離、Homestead 有更快的啟動效率。
然而,Laravel Sail 的設計相對於它宣稱的不是這麼地「輕」,開發者出於各種奇怪的理由加入各種詭異的設計,再加上定位不明,導致 Laravel Sail 根本就是大雜燴:
- Base Image 採用 Ubuntu
- 在 GCP 中 Best practices for building containers 一文中提到應該盡可能使用小的 Base Image
- Laravel Sail 的這部份設計事實上把 Docker 當 VM 在用,這已經悖離容器的核心思想
- Laravel Sail 由 Composer 管理相依性
- 開發者(例如前端或 APP 開發者)的裝置中並不一定存在 PHP 與 Composer,安裝「PHP 開發環境」(PHP + Composer)去啟動「另一個 PHP 開發環境」(Laravel Sail)這完全是本末倒置
用 Pest 取代原本的 PHPUnit
Pest 是一款以 PHPUnit 為基礎的測試框架,因為富有表現力 的語法而相當受歡迎。
$ composer require pestphp/pest pestphp/pest-plugin-laravel --dev --with-all-dependencies
$ php artisan pest:install
以 ./vendor/bin/pest
或 php artisan test
即可執行測試
用 Pint 修飾 Coding Style
Pint 是一款由 Laravel 官方推出的 Coding Style 檢測/修復工具,它以 PHP-CS-Fixer 為基礎
$ composer require laravel/pint --dev
以 ./vendor/bin/pint
即可自動修復 Coding Style,或是加上 --test
參數使其只檢測、不修復
順帶一提,以下是我常用的 Pint 設定檔,將其複製後在專案根目錄建立一個 pint.json
即可套用
{
"preset": "laravel",
"rules": {
"ordered_imports": { "sort_algorithm": "length" },
"array_syntax": { "syntax": "short" },
"ordered_class_elements": {
"order": [
"use_trait",
"constant_public",
"constant_protected",
"constant_private",
"property_public",
"property_protected",
"property_private",
"construct",
"destruct",
"magic",
"phpunit"
]
}
}
}
Composer Script 設定
Composer Script 是一個 Composer 的功能,讓用戶可以自定義指令並執行:例如,composer test
就執行 ./vendor/bin/pest
;composer lint
就執行 ./vendor/bin/pint
。
此時我們可以設定 composer.json
,在 scripts
中加入 test
與 lint
:
{
"scripts": {
"test": "pest",
"lint": "pint",
}
}
設定 Git Hooks
在團隊協作時,通常會希望整個團隊能遵守一個共同的規範:
- 每一個 commit 之前都會先經過 coding style 的驗證
- 每一個 push 之前都會先確定套件是最新的,而且通過測試
建立 scripts/git/pre-commit.hook
#!/usr/bin/env bash
ERROR='\033[0;31m' # Red
SUCCESS='\033[1;32m' # Green
WARNING='\033[1;33m' # Yellow
NC='\033[0m' # No Color
COMPOSER=$(which composer)
if [ $? -ne 0 ]; then
echo -e "${ERROR}Composer not found.${NC} Please install composer from: https://getcomposer.org/" >&2
exit 1
fi
if [ ! -f "./vendor/bin/pint" ]; then
echo -e "${ERROR}Pint not found.${NC} Please install packages via 'composer install'" >&2
exit 2
fi
$COMPOSER lint -- --test
if [ $? -ne 0 ]; then
echo -e "${ERROR}CS check failed.${NC} Please run 'composer lint' for fixing coding style." >&2
exit 3
fi
建立 scripts/git/pre-push.hook
#!/usr/bin/env bash
ERROR='\033[0;31m' # Red
SUCCESS='\033[1;32m' # Green
WARNING='\033[1;33m' # Yellow
NC='\033[0m' # No Color
COMPOSER_UPDATE_OPTS="--no-interaction"
COMPOSER=$(which composer)
if [ $? -ne 0 ]; then
echo -e "${ERROR}Composer not found.${NC} Please install composer from: https://getcomposer.org/" >&2
exit 1
fi
echo -e "${WARNING}Updating composer dependencies...${NC}"
$COMPOSER update $COMPOSER_UPDATE_OPTS
git diff-index --quiet HEAD
if [ $? -ne 0 ]; then
git commit -am "package: composer update"
fi
$COMPOSER test
if [ $? -ne 0 ]; then
echo -e "${ERROR}Tests failed. Aborting push.${NC}"
exit 1
fi
echo -e "${SUCCESS}Pre-push check passed!${NC}"
echo
建立 scripts/git/install-hooks.sh
#!/usr/bin/env bash
ERROR='\033[0;31m' # Red
SUCCESS='\033[1;32m' # Green
WARNING='\033[1;33m' # Yellow
NC='\033[0m' # No Color
if [ ! -d .git ]; then
echo "${ERROR}.git not found.${NC} Execute scripts/install-git-hooks in the top-level directory."
exit 1
fi
mkdir -p .git/hooks
ln -sf ../../scripts/git/pre-commit.hook .git/hooks/pre-commit || exit 1
chmod +x .git/hooks/pre-commit
ln -sf ../../scripts/git/pre-push.hook .git/hooks/pre-push || exit 1
chmod +x .git/hooks/pre-push
touch .git/hooks/applied || exit 1
echo
echo "Git hooks are installed successfully."
建立 Makefile
PHP = php
COMPOSER = composer
COMPOSER_PACKAGES := vendor
GIT_HOOKS := .git/hooks/applied
ENV := .env
install: $(GIT_HOOKS) $(COMPOSER_PACKAGES) $(ENV)
$(GIT_HOOKS):
@scripts/git/install-git-hooks.sh
@echo
$(COMPOSER_PACKAGES):
$(COMPOSER) install
@echo
$(ENV):
cp .env.example .env
$(PHP) artisan key:generate
@echo
如此一來,開發者只需要在 Clone 專案後執行 make install
即可建立好整個開發所需的環境