Laravel 環境設定

在 2021 年中旬,我曾經寫過一篇 Laravel 環境設定,不過因為工具上有些許變化,所以在 2022 年末將其重新整理一遍。

事先聲明,本文中所寫的環境設計題專門為了我自己的工作流而打造。如果不適合你,那你是對的,請儘管改成適合你的工作流。

概念

  • macOS 與 Linux 系統通用
    • 2022 年都快過完了,還有人用 Windows 開發不用 WSL/WSL2 的?
  • 輕量化、啟動速度快
  • 規範化,適合團隊開發

Docker 整合

在當前的 Laravel 專案中建立 docker-compose.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 到本地環境中:

1
2
3
4
5
6
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/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 "[email protected]"

建立 docker/compose/develop.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 設定即可。

1
2
3
4
5
6
7
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 為基礎並且提供一個開箱即用的開發環境。

註:相較於 HomesteadValet,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 為基礎的測試框架,因為富有表現力的語法而相當受歡迎。

1
2
$ composer require pestphp/pest pestphp/pest-plugin-laravel --dev --with-all-dependencies
$ php artisan pest:install

./vendor/bin/pestphp artisan test 即可執行測試

用 Pint 修飾 Coding Style

Pint 是一款由 Laravel 官方推出的 Coding Style 檢測/修復工具,它以 PHP-CS-Fixer 為基礎

1
$ composer require laravel/pint --dev

./vendor/bin/pint 即可自動修復 Coding Style,或是加上 --test 參數使其只檢測、不修復

順帶一提,以下是我常用的 Pint 設定檔,將其複製後在專案根目錄建立一個 pint.json 即可套用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "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/pestcomposer lint 就執行 ./vendor/bin/pint

此時我們可以設定 composer.json,在 scripts 中加入 testlint

1
2
3
4
5
6
{
  "scripts": {
    "test": "pest",
    "lint": "pint",
  }
}

設定 Git Hooks

在團隊協作時,通常會希望整個團隊能遵守一個共同的規範:

  • 每一個 commit 之前都會先經過 coding style 的驗證
  • 每一個 push 之前都會先確定套件是最新的,而且通過測試

建立 scripts/git/pre-commit.hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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 即可建立好整個開發所需的環境