promptui 的使用

1
$ go get -t github.com/manifoldco/promptui

Import

1
import "github.com/manifoldco/promptui"

Input

1
2
prompt := promptui.Prompt{Label: "Project Name"}
prjName, err := prompt.Run()

執行會得到:

Select

1
2
3
4
5
prompt := promptui.Select{
Label: "Project Type",
Items: []string{"go", "quasar"},
}
_, prjType, err := prompt.Run()

執行會得到:

Facades

Laravel 提供的 Facade 是 Facade pattern。實作上利用 static function 把 Laravel 的功能包起來,可以在使用的時候更簡潔。Laravel 在 Facade 做些 function 讓測試時可以假造 Facade class 的結果,因此雖然是 static function 卻仍擁有可測試性。

使用 Facade 要注意以下兩點:

  • class 的大小。
    • Facade 依然是 class 的 dependency,使用 Facade 不會讓 dependency 顯現在 constructor,就不容易看得出 class 長得太大。
    • 這跟設計有關,只要設計 ok,當然可以用 Facade
  • 做與 Laravel 互動的 third party package 時,不適合用 Facade,應該要走一般 DI。
    • 因為 third party package 本身沒有 Laravel framework,無法是用 Laravel Facade 提供的 test 能力。

Contracts

Laravel 的 Contracts 是一堆定義 framework 提供的 core service 的 interface。所有 Laravel 的 contract 有他們自己的 Github repo,寫 package 時可以只使用其中幾個 contract。

Contracts vs Facades

每個 facade 都有對應的 contract。contract 跟 facade 是可以達到同樣事情的不同寫法或做法。

如果喜歡在 constructor explicit 的寫出一個 class 的 dependency,就用 contract。

一般來說,偏好使用 contract 還是 facade 對 application 而言沒有太大差別。但是 package 用 contract 會比較好,因為對 package 來說 contract 比較好測。

何時使用 Contracts

用 facade 還是用 contract 基本上是偏好問題。

有設計好 class 的責任,用 facade 跟 contract 沒太大差別。

用 contracts 的原因:loose coupling 跟 simplicity

Loose Coupling

contracts package 沒有任何 implementation 也沒有 dependency,所以 depend on 它不會被任何 library 或 framework 綁住,反而可以利用定義好的 interface,底下實作可以任意抽換。

Simplicity

定義良好的 interface 可以很容易看出 laravel 提供的功能。

比起依賴於複雜又龐大的 class,依賴於定義良好的 interface,code 比較好懂。

如何使用 Contracts

在 type hint 寫 contract 的名字,service container 會進行 resolve 並且 inject 適當的 instance。

Reference

定義:一個隔離框架是一套可用來幫助寫程式的 API,使用這套 API 來建立假物件比手刻假物件要容易得多、快得多、簡潔許多。它是一個可以在 runtime 建立和設定假物件的 library,這些假物件稱為 dynamic stub 或 dynamic mock。

之所以稱為隔離框架,是因為這些框架幫你隔離工作 unit 跟它的 dependency object。

使用隔離框架的好處是讓隔離框架幫我們生 stub 跟 mock object,省去手刻的麻煩。

動態產生假物件

動態假物件是在 runtime 才建立的 stub 或 mock object,它的建立不需要手刻假物件的 code(寫死在假 class 中)。

Codeception

可以用 Codeception Stub github 來製造假的 stub 跟 mock object。寫起來大概會長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
public function testFoo()
{
$foo = new Foo;
$this->assertEquals(1, $foo->bar());
$this->assertEquals(2, $foo->hasArg(1));
$this->assertEquals(5566, $foo->noFake());

$fooStub = Stub::make('Foo', ['bar' => 2, 'hasArg'> 3]);
$this->assertEquals(2, $fooStub->bar());
$this->assertEquals(3, $fooStub->hasArg(1));
$this->assertEquals(5566, $fooStub->noFake());
}
Foo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Foo {
public function bar() {
return 1;
}

public function hasArg($ha) {
return 2;
}

public function noFake()
{
return 5566;
}
}

除了 Stub::make() 之外還有 Stub::makeEmpty()。class name 可以是 interface。要小心不要把 Stub 當作 extract and override 在寫……

可以配合 Codeception\Stub\Expected 做 mock object。如果是 mock object,Stub::make() 系列要記得傳最後一個參數 testCase,不然像 Expected::once() 會沒 verify 到。

這可以省去寫一堆 ‘’class FakeXXX’’,但是針對 API json 或 xml response 還是只能手動把這些 json 跟 xml 存下來去做假物件。

SQLite 的 ORM package

https://pub.dev/packages/floor

看起來是走 Entity pattern 那一系的。覺得可以直接用 class 去定義 schema 好像不錯

Entity class

代表一筆資料的 class,也可以說是 table 的欄位。

1
2
3
4
5
6
7
8
9
10
11
12
13
// person.dart

import 'package:floor/floor.dart';

@entity
class Person {
@primaryKey
final int id;

String name;

Person(this.id, this.name);
}

DAO (Data Access Object)

負責 access 底層 SQLite database。得是 abstract class,method 必須 return FutureStream。並且用 annotation 標示 CRUD 的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// person_dao.dart

import 'package:floor/floor.dart';

import 'person.dart';

@dao
abstract class PersonDao {
@Query('SELECT * FROM Person')
Future<List<Person>> findAllPersons();

@Query('SELECT * FROM Person WHERE id = :id')
Future<Person> findPersonById(int id);

@insert
Future<void> insertPerson(Person person);

@update
Future<void> updatePerson(Person person);

@delete
Future<void> deletePerson(Person person);
}

Database

要是一個 extend FloorDatabase 的 abstract class。

在 class signature 要加 @Database(),並且要把 entity 加進其中 entities attribute 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// database.dart

import 'dart:async';
import 'package:floor/floor.dart';
import 'package:sqflite/sqflite.dart' as sqflite;

import 'person_dao.dart';
import 'person.dart';

part 'database.g.dart'; // the generated code will be there

@Database(version: 1, entities: [Person])
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}

import 後要記得加 part 'database.g.dart'

接著執行 flutter packages pub run build_runner buil 就會產生 database.g.dart。如果要在 database 改變的時候自動 build 出 database.g.dart,可以下 flutter packages pub run build_runner watch

最後是使用 database:

1
2
3
4
5
6
7
8
9
10
// database.db 是 db 的 file name
final database = await $FloorAppDatabase.databaseBuilder('database.db').build();
final personDao = database.personDao;

final person = Person(1, 'John');
await personDao.insertPerson(person);

final result = await personDao.findPersonById(1);

print(result.name);

Laravel 提供兩種主要的 authorizing action:gate 跟 policy

可以把 gates 跟 policies 想像成 routes 跟 controllers。

Gates are most applicable to actions which are not related to any model or resource, such as viewing an administrator dashboard. In contrast, policies should be used when you wish to authorize an action for a particular model or resource.

通常 gates 跟 policies 會混合使用。

Gates

Gates are simply closures that determine if a user is authorized to perform a given action.

gates 通常定義在 App\Providers\AuthServiceProvider::boot(),用 Gate facade 來定義:

1
2
3
4
5
6
7
8
public function boot()
{
$this->registerPolicies();

Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}

也可以用 class callback 形式:

1
Gate::define('update-post', [PostPolicy::class, 'update']);

定義 gate 後,要在需要擋權限的地方(例如 controller action)使用 Gate::allows()Gate::denies()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post)
{
if (! Gate::allows('update-post', $post)) {
abort(403);
}

// Update the post...
}
}

不用自己丟目前登入的 user instance 進去,laravel 會 handle 這件事。

如果不是要看現在登入的 user,而是要 authorize 另外的 user,可以使用 forUser()

1
2
3
4
5
6
7
if (Gate::forUser($user)->allows('update-post', $post)) {
// The user can update the post...
}

if (Gate::forUser($user)->denies('update-post', $post)) {
// The user can't update the post...
}

可以用 any()none() 來同時 authorize 多個 action:

1
2
3
4
5
6
7
if (Gate::any(['update-post', 'delete-post'], $post)) {
// The user can update or delete the post...
}

if (Gate::none(['update-post', 'delete-post'], $post)) {
// The user can't update or delete the post...
}

如果想要在 authorize 失敗的時候自動丟 Illuminate\Auth\Access\AuthorizationException

1
Gate::authorize('update-post', $post);

Illuminate\Auth\Access\AuthorizationException 會自動被轉成 403 response by Laravel’s exception handler。

可以 support 複雜的 context:https://laravel.com/docs/8.x/authorization#gates-supplying-additional-context

Gate define 的時候也可以 return Illuminate\Auth\Access\Response 來帶更多資訊。Ref

如果想要讓某個 user 可以 grant all ability,可以用 before() Ref

1
2
3
4
5
Gate::before(function ($user, $ability) {
if ($user->isAdministrator()) {
return true;
}
});

If the before closure returns a non-null result that result will be considered the result of the authorization check.

也可以用 after 來定義在所有 authorization check 執行後執行的 closure。

Policies

Policies are classes that organize authorization logic around a particular model or resource.

可以用 make:policy 來產生 policy class,產生的 policy 會放在 app/Policies/

如果想要有跟某個 model 關聯的 example policy:

1
$ php artisan make:policy PostPolicy --model=Post

Register policy

有 policy class 後,要 register 它。

Registering policies is how we can inform Laravel which policy to use when authorizing actions against a given model type.

App\Providers\AuthServiceProvider 裡有個 policies property,它的 key 是 model class,value 是對應的 policy class name。

Registering a policy will instruct Laravel which policy to utilize when authorizing actions against a given Eloquent model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];

/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();

//
}
}

Write Policy

大致長這樣,可以寫任意 function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*
* @param \App\Models\User $user
* @param \App\Models\Post $post
* @return bool
*/
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}

如果用 --model 生,會生出 CRUD 有關的 function。

使用 Policy

透過 User Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PostController extends Controller
{
/**
* Update the given post.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Post $post
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Post $post)
{
if ($request->user()->cannot('update', $post)) {
abort(403);
}

// Update the post...
}
}

沒有 model instance 的 policy:

1
2
3
4
5
6
7
8
public function store(Request $request)
{
if ($request->user()->cannot('create', Post::class)) {
abort(403);
}

// Create the post...
}

透過 Controller Helpers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PostController extends Controller
{
/**
* Update the given blog post.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Post $post
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);

// The current user can update the blog post...
}
}

如果是 resource controller,可以這樣:

1
2
3
4
5
6
7
8
9
10
11
12
class PostController extends Controller
{
/**
* Create the controller instance.
*
* @return void
*/
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
}

要注意 CRUD 的 function 的 signature 要符合 laravel 需要的 type hint 才能用

透過 middleware

1
2
3
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');

MySQL UUID Smackdown: UUID vs. INT for Primary Key

可以用 BINARY 存 UUID,然後用以下 function 做 human-readable format 的轉換:

  • UUID_TO_BIN
  • BIN_TO_UUID
  • IS_UUID

這些 function 要 MySQL 8.0 後才有。

Example

把 UUID 當 PK 的 example

create table

1
2
3
4
CREATE TABLE customers (
id BINARY(16) PRIMARY KEY,
name VARCHAR(255)
);

insertion

1
2
3
4
INSERT INTO customers(id, name)
VALUES(UUID_TO_BIN(UUID()),'John Doe'),
(UUID_TO_BIN(UUID()),'Will Smith'),
(UUID_TO_BIN(UUID()),'Mary Jane');

query

1
2
3
4
5
SELECT 
BIN_TO_UUID(id) id,
name
FROM
customers;

以 build & deploy blog 練習使用 CodePipeline 相關 service。

  1. 在 CodeCommit 開 repository,把 blog source 放上去。CodeCommit 是類似 Github 的 AWS service。
  2. CodePipeline 的 Source stage 設定 source 為 CodeCommit。
  3. Build stage 使用 CodeBuild,image 用 AWS 的 Ubuntu image。
  4. 在 source code 裡加入 buildspec.yml,讓 CodeBuild 知道要跑什麼,內容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: 0.2

phases:
install:
on-failure: ABORT
commands:
- ./blog init
build:
on-failure: ABORT
commands:
- ./blog g

artifacts:
files:
- '**/*'
base-directory: public
name: blog-$(date +%Y-%m-%d)
  1. deploy stage 設定 deploy 到 S3 的 static web hosting bucket。

pipeline 會在 push code 後自動開始跑,一路 build 出 static html、deploy 到 S3。source stage 跟 build stage 的 artifact 會存到 S3。

Ref

這章在講設計 API 的 best practice。

Designing for Real-Life Use Cases

設計 API 時要以特定的、實際的 use case 進行決策,不要空想。

千萬不要公開內部基礎架構,把焦點放在與外部開發者(或 API 使用者)與 API 的互動體驗上。

藉由選擇特定的工作流程或 use case,你可以把重點放在一項設計上,並測試它是否可以幫助你的使用者。

在 brainstorming 階段去想「如果怎樣的話…」是有幫助的,可是到了設計階段,太多的「如果怎樣的話」的想像,反而會讓設計失焦,所以針對特定的 use case 是比較好的。

Designing for a Great Developer Experience

提供開發者好的開發體驗。

讓 API 能更快、更容易上手

文件可以幫開發者上手,有 tutorial 跟 getting started guide 也很好。

也可以提供線上互動式文件、線上的 sandbox 來讓 developer 可以實際測試。

提供 SDK 也可以幫助 developer 使用 API。

最後是應該要讓 developer 不需要登入或註冊就能使用 API。如果一定得要註冊,要盡可能減少註冊需要的資料。如果 API 使用 OAuth 保護,那麼應該要能讓 developer 在 UI 中產生 token 以使用 API。

維持一致性

像是 entry nmae、request 參數、response 等等,應該要維持一致性,讓 developer 即使不看文件也能猜到部份的 API。

在漸進修改的過程中,盡量與既有的設計模式保持一致,對使用者來說是最好的做法。

不要讓同樣的東西使用不同的名稱。

一致性很重要的原因是它可以減少試著了解你的 API 的 developer 的認知負擔(cognitive load)

Make Troubleshooting Easy

藉由回傳有意義的 error 跟 building tool 做到這點。

設計 API 時應該有系統的組織跟分類錯誤以及他們的回傳方式來方便 developer 排除問題。

有意義的 error 容易了解、明確而且可以讓人採取行動。它們可以協助 developer 了解問題並處理它。提供這些 error 的 detail 可以帶來較佳的使用者體驗。

machine-readable 的 error code 字串可以讓 developer 以程式處理錯誤。

除了 machine-readable 的 error code 之外,也可以加入 human-readable 的敘述,來讓 developer 更了解發生了什麼問題。

error 要講出具體錯誤的原因,像是「token 因為被撤銷而造成驗證失敗」,用 token_revoked 比 invalid_auth 好。

幫 error 分類

將 API request 過程(從 request 開始,到 architecture 的各種 service 邊界)的各種 high-level error 分門別類,例如:

按照程式碼路徑將 error 分類後,要考慮對這些 error 而言,採取哪個 level 的 communication 是有意義的。一種方式是在 response payload 中放入 HTTP status code 跟 header,以及 machine-readable 的 code 或更詳細的 human-readable 的錯誤訊息。

大部分情況下,要盡量具體的說明來讓 developer 採取正確的後續動作。但有些時候,尤其跟安全有關的時候,可能要回傳比較籠統的資訊,不然原始訊息可能會透漏如資料庫 connection 資訊等會引發安全問題的資訊。

在程式結構上,可以用一套相同的 library 檢查 request 並將 error format 成相同的格式來 response。

將 error 文件化,像是寫在 API 文件中。

與 HTTP API 錯誤與問題相關可以參考 RFC 7807

Build tooling

log HTTP status、error、error 的頻率以及其他 request metadata,好方便在內部或外部進行 debug 或處理問題。

建立 dashboard 來協助 developer 分析 API request 的 metadata,例如可以統計最常用的 API entry、找出沒被用過的 API 參數、分類常見錯誤等等。

log 跟 dashboard 都有很多現成的工具。

Make Your API Extensible

要擬定 API 的發展策略,讓 API 是可擴展的(extensible)。

API 應該提供可開啟新工作流程的基本元素,而非只是對映你的 app 的工作流程。API 的建立方式決定了 API 的使用者可用他來做什麼事情。如果你提供太低階的操作,可能會讓整合者負擔太多工作。如果你提供太高階的操作,可能會讓大多數的整合只是對應你自己的 app 所作的工作而已。為了實踐創新,你必須找到適當的平衡點,讓使用者能夠啟動不屬於你的 app 或 API 本身的工作流程。

在前後端分離的架構下,要區分內部使用跟對外公開的 API。兩者對於要提供什麼樣的 API 會有不同的考量。

extensible 的其中一個部份是確保 top partner 有提供 feedback 的機會。要設法 release 某些功能給 top partner 用用看,讓他們給予 feedback。

在想要用版本管理 API 時,如果早期就加入版本管理系統會比較容易,越晚加入越難實作。版本管理系統的好處在於它可以讓你用新版本進行 breaking changes,同時又能維持就版本的回溯相容性(backward compatibility)。breaking changes 就是會讓之前使用你的 API 可以正常運作的 app 無法繼續運作的 changes。

不過,維護版本是需要成本的。如果很多年沒有能力支援舊版本,或者認為 API 不太需要變動,那麼可以不用版本,改用 additive(附加式)變動策略,在單一、穩定的版本中維持 backward compatibility。

如果預計在「未來任何時刻」都有可能出現 breaking changes 與更新,最好建立版本管理系統。在一開始就建立版本管理系統需要付出的成本比之後迫切需要它的時候才加入低得多。

Auth

把 authentication 資訊寫在 ~/.pgpass,格式如下:

1
host:port:database:user:password

在用 pg_dumppsql 時就可以不用輸入密碼。

當然這個檔案的權限要是 600

Backup

1
2
3
4
5
6
7
8
$ pg_dump \
--dbname="[DBNAME]" \
--file="[FILEPATH]" \
--inserts --create \
-h [HOST] \
-p [PORT] \
-U [USER] \
-w

Restore

1
psql -d [DBNAME] -f [SQL_FILE_PATH] -h [HOST] -p [PORT] -U [USER]