POABOB

小小工程師的筆記分享

0%

使用 PHP Slim 4 建立一個 RESTful 框架 (基本配置篇)

前言

上學期學校的實作課之中,介紹了 Nodejs 的事件驅動、異步I/O特點,同時也練習使用 Express 和 React 充當前後端的框架。

於是乎我在想,PHP 當紅的框架是 Laravel,當初使用 Laravel 5.6 的時候,只知道它是一個完善的 PHP MVC 框架,可是如果單純使用 RESTful Api 的形式去使用,又顯得過於肥大,再加上 Laravel 學習曲線偏高,導致我對於這個框架避而不談。

就在我查找其他替代框架時(Symphony、CakePHP、CodeIgniter 4、Slim)發現了 Slim,Slim 這個框架非常特別,它不像主流框架一開始就跟你說它有什麼什麼功能,而是你需要什麼功能,可以按需擴充成自己想要的狀態。

更何況它符合PSR-7(Request、Response)PSR-11(Denpendency Injection)PSR-15(Middleware)的規範,要找適用的函式庫都有一定的形式規範,不用擔心遇到寫法雜亂的 library。

  • 以下我構想這個框架要符合什麼條件:
    1. 基於 ADR 模式
    2. 要有一個基礎的 ORM(可以不用 Migration)
    3. 要有 Jwt 功能
    4. 能夠輕鬆寫單元測試、E2E 測試
    5. 要使用 Docker 一鍵啟動
    6. 使用 Github Actions CI / CD
  • 本篇主要是參考 FoglessSlim 4 搭建 RESTful API 以及 Daniel OpitzSlim 4 - Tutorial,並且結合自身所學,再根據個人習慣建構。基礎篇的建構方向是能夠有基本的環境、 Slim 雛形、基本的返回JSON資訊、以及 Middleware 和 php-di 的相關配置。

安裝

本機最低需求

  • PHP 8.0 (目前沒有使用 php-7.4 實測過,所以希望至少可以 8.0 以上)
  • Composer
  • Docker、Docker-Compose
    • Nginx
    • PHP 8.1
    • MariaDB

Docker

  • 使用 Docker 的原因很簡單,因為可以將自己想要的服務配置寫在一個yml檔,並用一個指令達成開啟 / 關閉的功能,再者,我的系統是 Windows,對於開發者而言其實安裝環境不是特別友善。

  • 我們可以在專案目錄下建立一個 docker-compose.yml,並建立一個 docker 資料夾創建鏡像所需配置以利於打包。

docker-compose.yml

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
version: "3.8"

services:
# PHP 的容器配置,將會由 ./docker/php/Dockerfile 來進行打包生成
# port 則是容器與機器端口的映射,可以自行調整
# volumes 用於掛載本機與容器之間的文件,因為容器一旦關閉,所產生的文件就會被 Reset,所以掛載我們的專案才可以持續使用
# depends_on 是表示容器之間的依賴關係,可以用來決定啟動順序。原本容器間都是隔離不相通的,為了讓他們可以互相溝通所以使用
# image 是已經被打包好發布在 Docker Hub 的鏡像
# restart 可以設定容器掛了是否要不要重啟
# environment 是基本的配置,像是Mysql的部分直接配置好User、DB,不用手動慢慢建立
# command 是開啟容器後,可以自動讓他下指令,我這邊的指令是用於把我的DB schema自動導入
php:
container_name: slim_php
build:
context: ./docker/php
ports:
- '9000:9000'
volumes:
- .:/var/www/slim_app
depends_on:
- mysql
nginx:
container_name: slim_nginx
image: nginx:stable-alpine
ports:
- '8080:80'
volumes:
- .:/var/www/slim_app
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
# AS YOU NEED
mysql:
container_name: slim_mysql
image: mariadb:10.4
restart: always
environment:
- MYSQL_DATABASE=Example
- MYSQL_ROOT_PASSWORD=root
- MYSQL_USER=root
- MYSQL_PASSWORD=root
command: --init-file /data/application/init.sql
ports:
- "3306:3306"
volumes:
- "./docker/data/db/mysql:/var/lib/mysql"
- ./init.sql:/data/application/init.sql
  • 配置需要的 PHP 環境

docker/php/Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM php:8.1-fpm

RUN apt update \
&& apt install -y zlib1g-dev g++ git libicu-dev zip libzip-dev zip libpq-dev \
&& docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \
&& docker-php-ext-install intl opcache pdo pdo_mysql pdo_pgsql \
&& pecl install apcu \
&& docker-php-ext-enable apcu \
&& docker-php-ext-configure zip \
&& docker-php-ext-install zip

WORKDIR /var/www/slim_app

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

RUN git config --global user.email "{your-email}" \
&& git config --global user.name "{your-name}"
  • 配置 Nginx,主要是藉由 Http Server 向 php-fpm 傳遞服務請求,並將入口文件定義在 index.php,以及 log 存放位置(不過這邊我沒有掛載log,需要可以去yml配置掛載路徑), fastcgi 的設定。

docker/nginx/default.conf

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
server {
# 監聽端口
listen 80;
# 當訪問"/"路徑,自動查找index.php
index index.php;
# 域名
server_name localhost;
# 入口文件
root /var/www/slim_app/public;
# log 位置
error_log /var/log/nginx/project_error.log;
access_log /var/log/nginx/project_access.log;

# 訪問 uri("/") 時的相關操作,
# try_files 按順序檢查文件是否存在,這邊使用index.php作為入口文件
# $uri 就是不帶任何參數的路徑
# $is_args是否帶有參數,如果有就會顯示"?",沒有則為空
# $args就是參數
# Example:http://localhost/abc?user_id=1
# $uri = "/abc"
# $is_args = "?""
# $args = "user_id=1"
location / {
try_files $uri /index.php$is_args$args;
}

# 當上一段訪問到index.php之後,則會跳來這裡
# 這邊主要是使用 cgi 的形式去訪問php的服務
# 因為我們使用php-fpm(預設服務端口9000)
# 再來,php是我們容器的名稱,容器會自動轉換成內網,我記得以 172.0.0.X 的形式顯示
# 剩下的就是官方基本配置了
location ~ ^/index\\.php(/|$) {
fastcgi_pass php:9000;
fastcgi_split_path_info ^(.+\\.php)(/.*)$;
include fastcgi_params;

fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;

fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;

internal;
}

# 如果很可惜配對不到php檔案,那麼就返回404
location ~ \\.php$ {
return 404;
}
}
  • 配置DB Schema,目前主要建立一個 Example 作為本次實作資料庫,並且有一個 Users 表處理基本需求

init.sql

1
2
3
CREATE DATABASE IF NOT EXISTS Example;
USE Example;
CREATE TABLE IF NOT EXISTS `Example`.`Users` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT , `name` VARCHAR(64) NOT NULL, `password` VARCHAR(64) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;
  • 以上這些步驟處理完,使用 Docker 建立服務的步驟就算完成了。

安裝相關依賴

  • Compose 是 php 的套件管理器,它的功能就跟 Nodejs 的 Npm 是一樣的。請先在本機安裝 PHP、Compose 之後在 terminal 輸入以下安裝指令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Medoo 是一個輕量化的 PHP ORM 函式庫,並且支援多種不同類型的 DB(sqlite、MSSQL、Oracle、PostgreSQL、Sybase),也是我最常使用的 ORM
composer require catfan/medoo: ^2.1
# php-di 幫助我們在 Slim 4 使用依賴注入和自動配裝功能
composer require php-di/php-di: ^6.4
# 方便定位 Basepase 的好工具
composer require selective/basepath: ^2.1
# 基於 slim/psr7 的裝飾器,主要是原生返回資訊的方法不夠簡潔,所以才採用
composer require slim/http: ^1.0
# PSR-7 函式庫
composer require slim/psr7: ^1.0
# Slim 4 的核心組件
composer require slim/slim: ^4.10
# 引入 .env 檔案作為系統的環境變數
composer require symfony/dotenv: ^6.0

新增.gitignore,避免我們的 vendor、資料庫資料、敏感資訊上傳到 Github 上

.gitignore

1
2
3
4
5
docker/data/
.env
vendor/
.idea/
*.db

建構 Slim 4

環境配置(Settings)

  • 本專案將所有配置文件都放到 config 資料夾內。

  • 首先可以建立 config/settings.php 當作我們的系統設定。這邊可以定義系統的基本設定(時區、除錯相關的配置),在 return array 之中可以設定你想要給系統的一些變數。

config/settings

1
2
3
4
5
6
7
8
9
10
11
12
<?php

declare(strict_types=1);

error_reporting(1);
ini_set("display_errors", "1");

// Timezone
date_default_timezone_set("Asia/Taipei");

// 基本設定
return [];
  • Fogless 這篇文章有配置 DB 資訊ERROR 顯示相關敏感訊息,但我認為如果提交到 Github 上可能會有安全性的風險,於是我才使用 symfony/dotenv 來讀取我們 .env 的資訊,如果認為麻煩也可以直接略過。

.env

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
# dev/prod/stage/test
MODE=dev
# MYSQL CONFIG
DB_DRIVER=mysql
DB_NAME=Example
DB_HOST=mysql
DB_USER=root
DB_PASS=root
DB_CHARSET=utf8mb4

# SQLITE CONFIG
# DB_DRIVER=sqlite
# DB_NAME=./path/Example.db

# SETTINGS
DISPLAY_ERROR_DETAILS=1
LOG_ERROR_DETAILS=1
LOG_ERRORS=1


# JWT SETTINGS
JWT_ISSUER=SLIM_4
JWT_LIFETIME=86400
JWT_PRIVATE_KEY=
JWT_PUBLIC_KEY=

依賴注入容器(Container)

  • 其實這個專有名詞有涉及三種相關的概念,分別是:
    1. 依賴注入(Denpendency Injection)
    2. 依賴注入容器(DIC,Dependency Injection Container)
    3. 自動配裝(Autowiring)

依賴注入

  • 簡單來說,當我們在 OOP 設計的時候,常常需要使用到其他類的相關功能,但是在類中直接實例化會導致兩個類之間存在著強耦合(Strong Coupling)的關係,然而這樣維護程式碼的時候一個修改可能導致其他依賴類受到嚴重的影響,造成程式碼不易維護的情況。

  • 這邊我們以手槍類子彈類作為我們的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Bullet_2_34
{
public function load() {
echo "填彈";
}
}

class Gun
{
public function shoot() {
$bullet = new Bullet_2_34();
$bullet->load();
echo "射擊";
}
}

$gun = new Gun();
$gun->shoot();
// 填彈
// 射擊
  • 沒有依賴注入流程:
    1. Class Gun 實例化
    2. Class Gun 調用 shoot 方法
    3. 發現需要必須要有 Class Bullet_2_34
    4. Class Bullet_2_34 實例化
    5. Class Bullet_2_34 調用 load 方法
    6. Class Bullet_2_34 echo “填彈”
    7. Class Gun echo “射擊”
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
<?php
class Bullet_2_34
{
public function load() {
echo "填彈";
}
}

class Gun
{
private Bullet_2_34 $bullet;

public function __construct(Bullet_2_34 $bullet) {
$this->bullet = $bullet;
}
public function shoot() {
$this->bullet->load();
echo "射擊";
}
}

$bullet = new Bullet_2_34();
$gun = new Gun($bullet);
$gun->shoot();
// 填彈
// 射擊
  • 使用依賴注入流程:
    1. Class Gun 發現需要必須要有 Class Bullet_2_34
    2. Class Bullet_2_34 實例化
    3. Class Gun 實例化並且將 Class Bullet_2_34 注入
    4. Class Gun 調用 shoot 方法
    5. Class Bullet_2_34 調用 load 方法
    6. Class Bullet_2_34 echo “填彈”
    7. Class Gun echo “射擊”
  • 上述流程可以發現,我們在實例化類的順序明顯遭到調換,從原本 Gun -> Bullet_2_34 相反變成 Bullet_2_34 -> Gun,這種模式我們又稱為控制反轉(Inversion of Control),這樣們如果我們的 Gun 想要換不同彈徑的 Bullet,可以直接新增一個新的 Bullet_5_56(5.56mm) 類注入 Gun 類之中,不用去修改原本 Bullet_2_34 類的程式碼(可能有其他不同的 Gun 類需要)。

依賴注入容器

  • 從依賴注入的例子我們可以發現,每次對它類有依賴的時候我們需要提前實例化該類並注入,那麼是不是就需要有一個容器來存放當作一個管理工具呢?依賴注入容器就為此而生了。
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
class Bullet_2_34
{
public function load() {
echo "填彈";
}
}

class Gun
{
private Bullet_2_34 $bullet;

public function __construct(Bullet_2_34 $bullet) {
$this->bullet = $bullet;
}
public function shoot() {
$this->bullet->load();
echo "射擊";
}
}

class Container
{
// 存放所綁定的類
private static array $register = [];

// 綁定函數
public static function bind($name, Closure $closure) {
self::$register[$name] = $closure;
}

// 創建實例化
public static function make($name) {
$closure = self::$register[$name];
return $closure();
}
}

Container::bind(Bullet_2_34::class, function () {
return new Bullet_2_34();
})

Container::bind(Gun::class, function () {
return new Gun(Container::make(Bullet_2_34::class));
})


$gun = Container::make(Gun::class);
$gun->shoot();
// 填彈
// 射擊
  • 我們只需要在容器內註冊所有類之後,就可以更好的為我們實現依賴注入了。

自動配裝

  • 自動配裝這個機制的產生是因為,我們在使用依賴注入的時候直接將容器給注入到我們的類中,以下是 Slim 3 的範例程式碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class UsersController
{
public function __construct(ContainerInterface $container)
{
$this->repository = $container["userRepository"];
}

public function getAllUsers($request, $response)
{
return $response->withJson($this->repository->getAllUsers());
}
}
  • 一般我們稱這種情況叫做反面模式(Anti-pattern),其中違反了 SOLID 原則中的依賴反轉
  • 慶幸的是,Slim 4 解決的這個尷尬的局面,引用符合 PSR-11 標準的 PHP-DI,它會讓容器可以自動創建和注入依賴類的的能力。所以我們只要在建構子(__construct)中顯示聲明依賴類,那麼依賴注入容器就會自動幫你創建和注入好了。
1
2
3
4
5
6
7
8
9
10
11
<?php
class UsersController
{
private UserRepository $repository;

public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
// ...
}

容器配置

  • 講了那麼多容器肯定看到頭都暈了,接下來我們建立 config/container.php 把我們需要的依賴都寫進去
    • settings: 一開始的設定,有其他需求的人可以再自行定義
    • Dotenv::class: 這是我們讀取 .env 的一個函式庫
    • App::class: 將我們 SLIM 應用注入容器(參考 Dependency Injection in Slim 4)
    • BasePathMiddleware::class: Basepath 這個中間件幫我們自動註冊好路徑,不用再額外設定(不使用的話就要自行依據資料夾路徑來定義)
    • Medoo::class: 這是一個輕量的 PHP ORM 函式庫,$_ENV是我們 .env 裡面定義的變數

config/container.php

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php

declare(strict_types=1);

use Psr\Container\ContainerInterface;
use Selective\BasePath\BasePathMiddleware;
use Slim\App;
use Slim\Factory\AppFactory;
use Medoo\Medoo;

return [

"settings" => function() {
return require __DIR__ . "/settings.php";
},

Dotenv::class => function () {
return new Dotenv();
},

// 註冊SLIM APP
App::class => function (ContainerInterface $container) {
AppFactory::setContainer($container);
return AppFactory::create();
},

// 找BasePath
BasePathMiddleware::class => function (ContainerInterface $container) {
return new BasePathMiddleware($container->get(App::class));
},

Medoo::class => function () {
if($_ENV['DB_DRIVER'] == "sqlite") {
$database = [
'type' => $_ENV['DB_DRIVER'],
'database' => __DIR__ . "/../" . $_ENV['DB_NAME']
];
} else {
$database = [
'type' => $_ENV['DB_DRIVER'],
'database' => $_ENV['DB_NAME'],
'host' => $_ENV['DB_HOST'],
'username' => $_ENV['DB_USER'],
'password' => $_ENV['DB_PASS'],
'charset' => $_ENV['DB_CHARSET'],
'option' => [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
//千萬不能開啟,會造成ACID失敗
// PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false
]
];
}
return new Medoo($database);
},

];

中間件(Middleware)

  • 我們可以把向 SERVER 的 HTTP 請求想像成一個正要出國的人SERVER 就是我們要入境的國家,那麼每當我們出入海關的時候是不是就要示出我們的護照(COOKIE、TOKEN…),來證明自己的來自哪個國家,可以避免任何危險人物入境。

  • 中間件也是扮演著類似的角色,無論我們要向 SERVER 做什麼請求,在處理請求之前可以同步預先額外處理驗證,以便我們路由後續的業務能夠更加順利。像是有的中間件就是用於權限的限制,我們則必須攜帶 SERVER 分發的 COOKIE 或 TOKEN 證明自己的身分。

  • 以下是 SLIM 4 官方的範例程式碼,可以看到使用 $app->add() 的方式去替返回的 Body 增加前綴和後綴。

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
31
32
33
34
35
36
<?php
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
use Slim\Psr7\Response;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$app->add(function (Request $request, RequestHandler $handler) {
$response = $handler->handle($request);
$existingContent = (string) $response->getBody();

$response = new Response();
$response->getBody()->write('BEFORE ' . $existingContent);

return $response;
});

$app->add(function (Request $request, RequestHandler $handler) {
$response = $handler->handle($request);
$response->getBody()->write(' AFTER');
return $response;
});

$app->get('/', function (Request $request, Response $response, $args) {
$response->getBody()->write('Hello World');
return $response;
});

$app->run();


// GET /
// BEFORE Hello World AFTER
  • 這是把我們把路由前綴做 Group 劃分,並針對該 Group 去做額外處理的範例。
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
31
32
33
34
35
36
37
38
39
40
41
42
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$app->get('/', function (Request $request, Response $response) {
$response->getBody()->write('Hello World');
return $response;
});

$app->group('/utils', function (RouteCollectorProxy $group) {
$group->get('/date', function (Request $request, Response $response) {
$response->getBody()->write(date('Y-m-d H:i:s'));
return $response;
});

$group->get('/time', function (Request $request, Response $response) {
$response->getBody()->write((string)time());
return $response;
});
})->add(function (Request $request, RequestHandler $handler) use ($app) {
$response = $handler->handle($request);
$dateOrTime = (string) $response->getBody();

$response = $app->getResponseFactory()->createResponse();
$response->getBody()->write('It is now ' . $dateOrTime . '. Enjoy!');

return $response;
});

$app->run();

// GET /utils/date
// It is now 2015-07-06 03:11:01. Enjoy!
// GET /utils/time
// It is now 1436148762. Enjoy!
  • 雖然目前只是初步構建我們的專案,不太需要複雜的處理,所以我們就在 config/middleware.php 新增基本的設定就好,主要是配置好我們解析 Body 的功能、將中間件插入我們的路由、配置 BASEPATH、錯誤處理。

config/middleware.php

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
<?php

declare(strict_types=1);

use Slim\App;
use Selective\BasePath\BasePathMiddleware;

return function(App $app)
{
// 解析JSON
$app->addBodyParsingMiddleware();

// SLIM內建路由中間件
$app->addRoutingMiddleware();

// BASEPATH
$app->add(BasePathMiddleware::class);

// DEBUG
$app->addErrorMiddleware(
(bool)$_ENV["DISPLAY_ERROR_DETAILS"],
(bool)$_ENV["LOG_ERROR_DETAILS"],
(bool)$_ENV["LOG_ERRORS"]
);
};

路由(Routes)

  • 我們可以把 HTTP 請求方法分成五種,分別是 GET(獲取資源)POST(新增資源)PATCH(更新,修改資源的部分內容)PUT(更新,通常做替換一個資源功能)DELETE(刪除資源),我們通常會根據我們需要的操作去提交對應的方法,而如果要讓 SERVER 知道我們要操作什麼資源,就必須提供URI(Uniform Resource Identifier,範例:”/api/user”)才可以讓 SERVER 知道我們想要什麼服務。

  • 舉個例子,假設我們今天要去郵局存錢,可是郵局就有儲匯、郵務、保險這幾種服務(URI),那麼我就應該選擇儲匯業務(“/api/money”)並跟櫃台服務人員說我要存(POST)多少({“money”: 2000}),但是進入郵局一開始要先跟櫃台領取號碼牌(中間件,rate limiter)跟著其他人排隊,最後櫃台服務人員就會根據你的資料(TOKEN)來判斷你的存錢服務是否成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Http\Response;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();

$app->post('/api/money', function (Request $request, Response $response, $args) {
$data = (array)$req->getParsedBody();
$return = saveMoney($data);
return $response->withJson($return, 200, JSON_UNESCAPED_UNICODE);
})->add(function (Request $request, RequestHandler $handler) {
AUTH();
RATE_LIMITER();
return $response;
});

$app->run();
  • 看完解釋我們可以清楚地了解到路由就是 RESTful API 提供服務的最基本要素,所以我們必須建立 config/routes.php,來新增最基礎的路由。

config/routes.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

declare(strict_types=1);

use Slim\Http\Response;
// 發現使用Slim\Http\Request常常會報錯,所以使用官方的Request當作請求
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteCollectorProxy;
use Slim\App;

return function(App $app) {
$app->group("/api", function (RouteCollectorProxy $app) {
$app->get("/home[/]", function(Request $req, Response $res, array $args) :Response {
return $res->withJson("GET HOME", 200, JSON_UNESCAPED_UNICODE);
});
});
};

啟動(Bootstrap)

  • 剛剛我們把 SLIM 4 最基本的要素都建立好了,那我們來將那些整合到我們的啟動 config/bootstrap 之中。

這個啟動可以直接在測試中直接引入,進行整合或單元測試。

config/bootstrap.php

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
<?php

declare(strict_types=1);

use DI\ContainerBuilder;
use Symfony\Component\Dotenv\Dotenv;
use Slim\App;

require_once __DIR__ . '/../vendor/autoload.php';

// 註冊我們設定好的CLASS
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions(__DIR__ . '/container.php');
$container = $containerBuilder->build();

// LOAD .ENV
$dotenv = $container->get(Dotenv::class);
$dotenv->load(__DIR__.'/../.env');

// 建立App INSTANCE
$app = $container->get(App::class);

// 中間件
(require __DIR__ . '/middleware.php')($app);

// 路由
(require __DIR__ . '/routes.php')($app);

return $app;

入口文件(Index)

  • 寫好啟動後,就可以在我們的入口文件 public/index.php 直接引入並且執行。

public/index.php

1
2
3
4
5
<?php

declare(strict_types=1);

(require __DIR__ . "/../config/bootstrap.php")->run();

開啟服務

  • 當我們的 SLIM 應用都配置好的時候,可以直接在專案路徑下的命令行執行 docker,然後可以使用 postman 查看是否有沒有返回請求。
1
docker-compose up -d

HOME

基本應用

Hello World

  • 除了固定地返回方式之外,URI 中可以配置參數($args),來動態獲取我們需要的資源。

其他路由更多的用法可以參考官方文檔

config/routes.php

1
2
3
4
5
6
7
8
9
10
11
<?php
// ...

return function(App $app) {
$app->group("/api", function (RouteCollectorProxy $app) {
// ...
$app->get("/home/{name}[/]", function(Request $req, Response $res, array $args) :Response {
return $res->withJson("GET Hello {$args['name']}!", 200, JSON_UNESCAPED_UNICODE);
});
});
};

成果展示

HOME NAME

SQL CRUD

  • 首先我們先在 config/bootstrap.php 獲取我們設定好的 Medoo,將它傳遞到 routes 的函數中,以利我們使用資料庫操作。

config/bootstrap.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

declare(strict_types=1);

// ...
use Medoo\Medoo;
// ...

// ...
$db = $container->get(Medoo::class);
// ...

// 路由
(require __DIR__ . '/routes.php')($app, $db);
// ...
  • 簡單撰寫一下資料庫請求的 API。

  • 請注意當我們要將 $db 傳遞進去我們的閉包函數時,我們必須使用到 use ($db) 才能使用。

Medoo 使用方法相對 PDO 簡便,如果有更進階的需求可以參考官方文檔

config/routes.php

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

// ...
use Medoo/Medoo;
// ...

return function(App $app, Medoo $db) {

$app->group("/api" . $prefix, function (RouteCollectorProxy $app) use ($db) {
$app->get("/user[/]", function(Request $req, Response $res, array $args) use($db) :Response {
// 獲取全部User
$data = $db->select("Users", ["id", "name"], 1);
return $res->withJson($data, 200, JSON_UNESCAPED_UNICODE);
});

$app->post("/user[/]", function(Request $req, Response $res, array $args) use($db) :Response {
try {
$data = (array)$req->getParsedBody();
$db->insert("Users", $data);
$id = $db->id();
return $response->withJson($id, 200, JSON_UNESCAPED_UNICODE);
} catch (\PDOException $e) {
return $response->withJson($e->getMessage(), 500, JSON_UNESCAPED_UNICODE);
}
});

$app->patch("/user[/]", function(Request $req, Response $res, array $args) use($db) :Response {
try {
$data = (array)$req->getParsedBody();
$db->update("Users", ["name" => $data["name"]], ["id" => $data["id"]]);
return $response->withJson("Success", 200, JSON_UNESCAPED_UNICODE);
} catch (\PDOException $e) {
return $response->withJson($e->getMessage(), 500, JSON_UNESCAPED_UNICODE);
}
});

$app->delete("/user[/]", function(Request $req, Response $res, array $args) use($db) :Response {
try {
$data = (array)$req->getParsedBody();
$db->delete("Users", ["id" => $data["id"]]);
return $response->withJson("Success", 200, JSON_UNESCAPED_UNICODE);
} catch (\PDOException $e) {
return $response->withJson($e->getMessage(), 500, JSON_UNESCAPED_UNICODE);
}
});
});
};

成果展示

  • 順序:新增 -> 更新 -> 獲取 -> 刪除 -> 獲取。

HOME NAME
HOME NAME
HOME NAME
HOME NAME
HOME NAME

結語

當前目錄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.
├── config/ 應用配置目錄
| ├── bootstrap.php 啟動文件
| ├── container.php 依賴注入容器文件
| ├── middleware.php 中間件文件
| ├── routes.php 路由文件
| └── settings.php 配置設定文件
├── docker/ Docker 相關目錄
| ├── nginx/ Nginx 目錄
│ | └── default.conf Nginx 配置文件
| └── php/ PHP 目錄
│ └── Dockerfile PHP 容器配置文件
├── public/ 網站根目錄
│ └── index.php 入口文件
├── src/ PHP 原始碼目錄(App Namespace)
├── vendor/ Composer 目錄
├── .env 系統變數
├── .gitignore Git 忽略文件
├── composer.json Composer 配置文件
├── composer.lock Composer 鎖定文件
├── docker-compose.yml Docker 容器配置文件
└── init.sql DB Schema

未來工作

  • 由於目前我們學會配置基本的設定操作,如果是一般精簡的微服務,可以照接下來的模式進行開發,但是我希望能夠藉由物件導向的概念,將它設計成一個 ADR模式的框架。

  • 下一篇(進階篇),我將會新增表單驗證 Class使用者登入 ApiJwt Class 封裝/加入中間件Response 統一格式 Class 等相關功能。

非常感謝 FoglessSlim 4 搭建 RESTful APIDaniel OpitzSlim 4 - Tutorial,讓我能夠站在巨人的肩膀上開發 SLIM 4 的應用,本文範例程式碼在 GitHub 上的 POABOB/Slim-Simple

------ 本文結束 ------