等待所有 ajax 請求完成

用 jQuery 實現 promise,待所有 ajax 執行完後才執行指定操作

notice: 所有 ajax call 為非同步執行

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
function demonstrate() {
var urls = ["url_1", "url_2", "url_3"];

callAjaxRequsts(urls)
.done(function() {
console.log("all done");
})
.fail(function() {
console.log("error");
});
}

function callAjaxRequsts(urls) {
var requests = $.map(urls, function(value, index) {
return $
.ajax({
url: value,
type: "post"
})
.done(function(response) {
console.log(response);
});
});

return $.when.apply($, requests);
}

Reference

https://stackoverflow.com/questions/23300168/deferred-promise-ajax-loop-then-never-happens
https://stackoverflow.com/questions/3709597/wait-until-all-jquery-ajax-requests-are-done

在 apache 設定 virtual host

用途

在同一台 server 上運行多個服務,例如 HTTP、FTP、EMAIL 等。

設定檔位置

傳統設定位置為/etc/httpd/conf/httpd.conf

apache2 的設定位置為 /etc/apache2/sites-available/000-default.conf
或是在 /etc/apache2/sites-available 下建立一個專用的設定檔,並 link 到 /etc/apache2/sites-enabled
預設情況是 /etc/apache2/sites-enabled/*.conf 都會被使用,詳細內容可以參見 /etc/apach2/apach2.conf

修改完設定後重起服務,才會生效。

模式

網址名稱對應(Name-based)

在同一台機器以及 ip 下,架設不同的網站。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<VirtualHost *:80>
ServerName ilovecats.com
DocumentRoot "/www/cat"
</VirtualHost>

<VirtualHost *:80>
ServerName ilovedogs.com
DocumentRoot "/www/dogs"
</VirtualHost>

<VirtualHost *:80>
ServerName api.ilovedogs.com
DocumentRoot "/www/api/dogs"
</VirtualHost>

IP位址對應(IP-based)

在同一台機器但不同 ip 下,架設不同的網站。

1
2
3
4
5
6
7
8
9
<VirtualHost 192.168.1.123:80>
ServerName ilovecats.com
DocumentRoot "/www/cat"
</VirtualHost>

<VirtualHost 123.123.123.123:8888>
ServerName ilovedog.com
DocumentRoot "/www/dog"
</VirtualHost>

Reference

https://zh.wikipedia.org/zh-tw/%E8%99%9A%E6%8B%9F%E4%B8%BB%E6%9C%BA

https://notfalse.net/53/http-virtual-host

常用 git 指令

建立一個新的 repository

  1. Create a new project at your git server(remote)

  2. Initialize your new project at local

  3. Set the remote url

ssh: git@git.server.com:namespace/project.git
https: https://git.server.com/namespace/project.git

1
git remote add origin <remote_url>
  1. Push your project
  2. Done

config

檢查設定

1
git config --list

設定

1
2
$ git config --global user.name "Yuchi Tung"
$ git config --global user.email yuchi@example.com

設定檔路徑

1
~/.gitconfig




初始化

建立一個新的 repository

1
git init

取得 remote repository

1
2
3
git clone <url>

git clone git@gitlab.com:project/myProject.git




基本指令

查看狀態

1
git status

將修改的檔案加入 stage

1
git add  <file_path/file_name>

所有修改的檔案加入 stage

1
git add .

檢視所有 commit

1
git log

檢視特定檔案的所有 commit

1
git log --follow <file_name>

放棄所有沒有 commit 的修改

1
git reset --hard




commit 相關

建立 commit

1
2
git commit
git commit -m "commit message"

修改最後一個 commit 的 message

1
git commit --amend

查看 commit 的 comment

1
git show <commit_id>




branch 相關

查看 branch

1
git branch <branch_name>

列出所有 branch

1
git branch

切換 branch

1
git checkout <branch_name>

建立新 branch 並 checkout

1
git checkout -b <branch_name>

重新命名 branch name

1
git branch -m <new_branch_name>

刪除 branch

1
git branch -d <branch_name>

建立一個與 remote branch 同名的 branch 並 track 它

1
git checkout --track origin/branch_name

設定 branch 的 upstream

1
git branch -u <remote name> <branch name>




push & pull

pull

1
git pull

push

1
git push <remote_name> <branch_name>

強制 push

1
git push <remote name> <branch name> --force

Push the current branch and set the remote as upstream

1
git push -u <remote name> <branch_name>




merge 相關

開發分支上有多 commit,rebase 整理主線進分支,合併線圖,線圖看起來比較乾淨

1
git rebase master

把別的 branch merge 到目前 branch

1
git merge <target_branch_to_merge>

預覽 merge,不是 true merge,若是使用後發先有衝突可以使用 git merge –abort 還原。–no-ff 的意思是不要使用快轉模式合併

1
git merge --no-commit --no-ff <target_branch>




取消

取消這次的修改

1
git checkout -- <my_file>

將檔案移除 stage 狀態

1
2
git reset Head <my_file>
git reset -- <my_file>

取消剛剛的 merge/rebase/reset,回到上一個 commit

1
git reset --hard ORIG_HEAD

回到上一個 commit

1
git reset --hard HEAD^

回到上兩個 commit

1
git reset --hard HEAD~2

回到指定 commit

1
git reset --hard <commit_hsa1>




查詢

顯示 remote branch

1
2
3
git remote show origin
git branch -r
git branch -a

查詢 branch 追蹤的遠端分支

1
git branch -vv

以關鍵字查詢 branch name

1
git branch -a | grep <keyword>




diff

查看 staged files 的更動

1
git diff --staged

查看還沒有加入 stage 的異動

1
git diff <my_file_name>

diff 兩個 branch

1
git diff <branch_1> <branch_2>

diff 單一檔案

1
git diff <branch>:<dir>/<filename> <branch>:<div>/<filename>




stash 暫存

暫存

1
git stash

查看所有 stash

1
git stash list

套用最後一個 stash

1
git stash apply

套用指定的 stash

1
git stash apply <stash@{index}>

使用 cherry pick 挑出指定 commit

1
git cherry-pick <sha1> <sha1>

使用 cherry pick 挑出指定多個指定 commit

1
git cherry-pick <sha1> <sha1>




其他

從 git 中移除已經被 commited 的檔案

除了以下動作別忘了也要將移除的的檔案加入 .gitignore

只移除追蹤,保留檔案

1
git rm --cached <path/file>

移除追蹤,刪除檔案

1
git rm <path/file>

移除追蹤,移除 folder 下所有 file, -r 表 recursive

1
git rm -r --cached <path>

Reference

git cherry-pick 使用指南

How to undo ‘git reset’?

透過rebase -i, reset, revert還原某個commit的方法

Git 版本控制:利用 git reset 恢復檔案、暫存狀態、commit 訊息

How can I see which Git branches are tracking which remote / upstream branch?

Ignoring a directory from a Git repo after it’s been added

在 Merge 之前想試試看有沒有衝突?

手把手建立 Laravel api

本範例分成幾個部份來實作 Laravel API:

  • 安裝 Dingo api
  • 安裝 tymon/jwt-auth
  • 安裝 laravel 5 repository
  • 安裝 barryvdh/laravel-cors
  • 建立 RESTful api 範例
  • 坑點

環境

  • 安裝 laravel
    本範例使用版本為 5.7.28



安裝 Dingo api

安裝

在 composer.json 裡加入

1
2
3
4
5
"require": {
"dingo/api": "2.0.0-alpha1"
},
"minimum-stability": "dev",
"prefer-stable": true

然後執行

1
composer update

產生 api configuration file

1
php artisan vendor:publish --provider="Dingo\Api\Provider\LaravelServiceProvider"

以上指令面會產生一個config/api.php,之後可以在這個檔案設定關於 api 的參數

設定.env

參數內容可視需求調整

1
2
3
4
5
API_STANDARDS_TREE=vnd
API_VERSION=v1
API_DEBUG=true
API_PREFIX=api
API_DOMAIN=api.myapp.com

config/app.php 中注册 Service Provider

Laravel 5.4 以下加入

1
2
3
4
5
6
'providers' => [
/**
* Dingo api
*/
Dingo\Api\Provider\LaravelServiceProvider::class
]




安裝 tymon/jwt-auth

安裝

1
composer require tymon/jwt-auth 1.0.*

產生 jwt-auth configuration file

1
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

config/app.php 中注册 Service Provider

Laravel 5.4 以下加入

1
2
3
4
5
6
'providers' => [
/**
* Tymon/jwt-auth
*/
Tymon\JWTAuth\Providers\LaravelServiceProvider::class
]

產生 jwt secret key

1
php artisan jwt:secret

設定 api 的 auth 方式為 jwt

在 config/api.php 加上

1
2
3
'auth' => [
'jwt' => Dingo\Api\Auth\Provider\JWT::class
]

在 config/app.php 加上

1
2
3
'aliases' => [
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class
]




安裝 laravel 5 repository

安裝

1
composer require prettus/l5-repository

產生 repository configuration file

1
php artisan vendor:publish --provider "Prettus\Repository\Providers\RepositoryServiceProvider"

config/repository.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'generator' => [
'basePath' => app()->path(),
'rootNamespace' => 'App\\',
'stubsOverridePath' => app()->path(),
'paths' => [
'models' => 'Entities',
'repositories' => 'Repositories',
'interfaces' => 'Repositories',
'transformers' => 'Transformers',
'presenters' => 'Presenters',
'validators' => 'Validators',
'controllers' => 'Http/Controllers/Api',//api controller 專用路徑
'provider' => 'RepositoryServiceProvider',
'criteria' => 'Criteria'
]
]

config/app.php

1
2
3
4
'providers' => [
...
App\Providers\RepositoryServiceProvider::class
]




安裝 barryvdh/laravel-cors

安裝

1
composer require barryvdh/laravel-cors

產生 CORS configuration file

1
php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider"

config/api.php 加入 CORS middleware

1
2
3
'middleware' => [
Barryvdh\Cors\HandleCors::class
]




建立 RESTful api

基本準備

1
php artisan make:entity user

app/User.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
namespace App\Entities;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
use Notifiable;

protected $hidden = [
'password', 'remember_token',
];

protected $fillable = ['name','email','password'];

/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}

/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}

}

app/Http/Controllers/Api/UsersController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Http\Controllers\Api;

use Dingo\Api\Routing\Helpers;
use Illuminate\Routing\Controller;

class UsersController extends Controller
{
use Helpers;

public function __construct()
{
$this->middleware('api.auth');
}

public function index(){

$user = $this->auth->user();

return $user;
}
}

app/laravel-api/app/Http/Requests/UserCreateRequest.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
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserCreateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email|unique:users',
'password' => 'required'
];
}
}

app/Validators/UserValidator.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
namespace App\Validators;

use \Prettus\Validator\Contracts\ValidatorInterface;
use \Prettus\Validator\LaravelValidator;

/**
* Class UserValidator.
*
* @package namespace App\Validators;
*/
class UserValidator extends LaravelValidator
{
/**
* Validation Rules
*
* @var array
*/
protected $rules = [
ValidatorInterface::RULE_CREATE => [
'email' => 'required|email',
'password' => 'required'
],
ValidatorInterface::RULE_UPDATE => [],
];
}

app/Http/Controllers/Api/Auth/RegisterController.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
namespace App\Http\Controllers\Api\Auth;

use App\Http\Requests\UserCreateRequest;
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use Prettus\Validator\Exceptions\ValidatorException;
use \Throwable;

class RegisterController extends Controller
{

/**
* Register new user
*
* @param UserCreateRequest
* @param UserRepository
* @return \Symfony\Component\HttpFoundation\Response
*/
public function register(UserCreateRequest $request,UserRepository $repository)
{
try{

$user = $repository->create([
'name' => $request->input('name'),
'email' => $request->input('email'),
'password' =>app('hash')->make($request->input('password'))
]);

return response()->json([
'message' => 'Success',
'data' => $user
]);

} catch (ValidatorException $e) {

return response()->json(['error' => 'validation exception'], 500);

} catch (Throwable $e) {

return response()->json(['errors' => 'Error'], 500);
}

}
}

app/Http/Controllers/Api/Auth/LoginController.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
namespace App\Http\Controllers\Api\Auth;

use App\Http\Requests\UserLoginRequest;
use Illuminate\Http\Response;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use App\Http\Controllers\Controller;

class LoginController extends Controller
{
/**
* Handle a login request to the application.
*
* @param App\Http\Requests\UserLoginRequest
* @return \Symfony\Component\HttpFoundation\Response
*/
public function login(UserLoginRequest $request)
{
/**
* Verify the credentials and create a token for the user
*/
try {
if (!$token = JWTAuth::attempt(
$this->getCredentials($request)
)) {
return $this->onUnauthorized();
}
} catch (JWTException $e) {

return $this->onJwtGenerationError();
}

return $this->onAuthorized($token);
}

/**
* Unauthorized
*
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function onUnauthorized()
{
return response()->json([
'message' => 'invalid_credentials'
], Response::HTTP_UNAUTHORIZED);
}

/**
* Can not generate a token
*
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function onJwtGenerationError()
{
return response()->json([
'message' => 'could_not_create_token'
], Response::HTTP_INTERNAL_SERVER_ERROR);
}

/**
* Authorized
*
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function onAuthorized($token)
{
return response()->json([
'message' => 'token_generated',
'data' => [
'token' => $token,
]
]);
}

/**
* Get the needed authorization credentials from the request
*
* @param App\Http\Requests\UserLoginRequest
* @return array
*/
protected function getCredentials(UserLoginRequest $request)
{
return $request->only('email', 'password');
}

/**
* Invalidate a token
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function deleteInvalidate()
{
$token = JWTAuth::parseToken();

$token->invalidate();

return response()->json(['message' => 'token_invalidated']);
}

/**
* Refresh a token
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function patchRefresh()
{
$token = JWTAuth::parseToken();

$newToken = $token->refresh();

return response()->json([
'message' => 'token_refreshed',
'data' => [
'token' => $newToken
]
]);
}

/**
* Get authenticated user
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function getUser()
{
return response()->json([
'message' => 'authenticated_user',
'data' => JWTAuth::parseToken()->authenticate()
]);
}
}

修改 config/repository.php

修改serializer,使用自己建立的,也可不改,使用預設的,但是回傳值會用 data 這個 key 值包起來

1
2
3
4
5
6
7
8
9
 'fractal' => [
'params' => [
'include' => 'include'
],
/**
* use custom array serializer
*/
'serializer' => App\Tools\ArraySerializer::class
],

加上客制化 class

如果在上一步驟有將 serializer 改成使用客制化的 serializer,則這一步驟為建立所需的 serializer

app/Tools/ArraySerializer.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace App\Tools;

use League\Fractal\Serializer\ArraySerializer as FractalArraySerializer;

class ArraySerializer extends FractalArraySerializer
{
public function collection($resourceKey, array $data)
{
if ($resourceKey) {
return [$resourceKey => $data];
}

return $data;
}

public function item($resourceKey, array $data)
{
if ($resourceKey) {
return [$resourceKey => $data];
}
return $data;
}
}

來寫一支 API 吧

情境:寫一個 book 資源 的 CRUD

撰寫 API 路徑
routes/api.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
$api = app('Dingo\Api\Routing\Router');

$api->version('v1', function ($api) {
$api->group(['namespace' => 'App\Http\Controllers\Api'], function () use ($api) {

$api->group(['namespace' => 'Auth'], function () use ($api) {
$api->post('auth/register', 'RegisterController@register');
$api->post('auth/login', 'LoginController@login');
});

$api->group(['middleware' => 'api.auth'], function ($api) {
/**
* Get a book
*/
$api->get('/books/{id}', 'BooksController@show');

/**
* Create a book
*/
$api->post('/books', 'BooksController@store');

/**
* Update a book
*/
$api->patch('/books/{id}', 'BooksController@update');

/**
* Delete a book
*/
$api->delete('/books/{id}', 'BooksController@destroy');
});
});
});

使用 artisan 指令輕鬆產出 entity, controller, presenter, validator, transformer…

1
php artisan make:entity Book

編寫 table 欄位

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
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

/**
* Class CreateBooksTable.
*/
class CreateBooksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('books', function(Blueprint $table) {
$table->increments('id');
$table->string('name',64);
$table->string('author',64);
$table->decimal('price',10,2);
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('books');
}
}

產生 table

1
php artisan migrate
依照需求修改 controller, repository, validator, transformer

app/Http/Controllers/Api/BooksController.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Prettus\Validator\Exceptions\ValidatorException;
use App\Http\Requests\BookCreateRequest;
use App\Http\Requests\BookUpdateRequest;
use App\Repositories\BookRepository;
use \Throwable;

/**
* Class BooksController.
*
* @package namespace App\Http\Controllers\Api;
*/
class BooksController extends Controller
{
/**
* @var BookRepository
*/
protected $repository;

/**
* BooksController constructor.
*
* @param BookRepository $repository
* @param BookValidator $validator
*/
public function __construct(BookRepository $repository)
{
$this->repository = $repository;
}

/**
* Store a newly created resource in storage.
*
* @param BookCreateRequest $request
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Prettus\Validator\Exceptions\ValidatorException
* @throws \Throwable
*/
public function store(BookCreateRequest $request)
{
try {
$book = $this->repository->create(
[
'name' => $request->input('name'),
'author' => $request->input('author'),
'price' => $request->input('price'),
]
);

return response()->json([
'message' => 'Book created.',
'data' => $book
]);
} catch (ValidatorException $e) {
return response()->json([
'error' => true,
'message' => $e->getMessageBag()
], 500);
} catch (Throwable $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage()
], 500);
}
}

/**
* Display the specified resource.
*
* @param int $id
* @return \Symfony\Component\HttpFoundation\Response
*/
public function show($id)
{
$book = $this->repository->find($id);

return response()->json([
'message' => 'Get a book.',
'data' => $book,
]);
}

/**
* Update the specified resource in storage.
*
* @param BookUpdateRequest $request
* @param integer $id
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Prettus\Validator\Exceptions\ValidatorException
* @throws \Throwable
*/
public function update(BookUpdateRequest $request, $id)
{
try {
$book = $this->repository->update(
[
'name' => $request->input('name'),
'author' => $request->input('author'),
'price' => $request->input('price'),
],
$id
);

return response()->json([
'message' => 'Book updated.',
'data' => $book
]);
} catch (ValidatorException $e) {
return response()->json([
'error' => true,
'message' => $e->getMessageBag()
], 500);
} catch (Throwable $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage()
], 500);
}
}

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Throwable
*/
public function destroy($id)
{
try {
$deleted = $this->repository->delete($id);

return response()->json([
'message' => 'Book deleted.',
'data' => $deleted
]);
} catch (Throwable $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage()
], 500);
}
}
}

設定 validator 的內容
app/Validators/BookValidator.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Validation Rules
*
* @var array
*/
protected $rules = [
ValidatorInterface::RULE_CREATE => [
'name' => "required|string",
'author' => "required|string",
'price' => 'required|numeric|between:0.01,1000000000.99'
],
ValidatorInterface::RULE_UPDATE => [
'name' => "required|string",
'author' => "required|string",
'price' => 'required|numeric|between:0.01,1000000000.99'
],
];
用 Postman 來測試看看!

先註冊
register

再登入
login

建立一個 book 資源
create a book

更新 book
update a book

取得 book
get a book

刪除 book
delete a book

Postman 小技巧

在拿到 token 後,跟 api 發出一個請求都要夾帶 token,這裡可以手動貼上,也可以在 test 的部份貼上一段程式碼,將 token 存成變數,之後在 header 裡 Authorization 直接對應 Bearer {{AUTH_TOKEN}} ,Postman 就會帶入剛剛拿到的 token 了。

1
2
3
4
5
6
7
8
9
tests["Body matches string"] = responseBody.has("token");
tests["Status code is 200"] = responseCode.code === 200;

var jsonData = JSON.parse(responseBody);
var token = jsonData.data.token;

tests["Has token inside it"] = typeof token === "string";

postman.setGlobalVariable("AUTH_TOKEN", token);

坑點

在測試 cors 情境的時候,遇到 server 回傳的 response header 一直沒有夾帶 Access-Control-Allow 系列的參數,以為是設定有錯,追了原始碼才確認不是 PHP 段的問題。最終發現,問題出在 server 安裝的 pagespeed module,所以在/etc/apache2/sites-available/000-default.conf/etc/apache2/sites-available/000-default-le-ssl.conf 將 api server 的 pagespeed 關閉,cors 就能正常使用了。這個 issue 可以參考
Missing CORS headers in response from PHP/Apache2

Reference

Dingo Api
https://github.com/dingo/api

tymon/jwt-auth
https://github.com/tymondesigns/jwt-auth

Laravel 5 repository
https://github.com/andersao/l5-repository

Laravel CORS
https://github.com/barryvdh/laravel-cors

Missing CORS headers in response from PHP/Apache2
https://stackoverflow.com/questions/51934338/missing-cors-headers-in-response-from-php-apache2

How to update environment variables based on a response in Postman
https://ppolyzos.com/2016/11/20/how-to-update-environment-variables-based-on-a-response-in-postman/

設定 Ubuntu & php & mysql timezone

ubuntu

查詢可用 timezone 列表

1
sudo timedatectl list-timezones

設定 timezone

1
sudo timedatectl set-timezone America/New_York

接著重啟一些有影響的服務,例如 cron、apache、mysql 等

1
sudo service cron restart

php

方法一

修改 php.ini

1
2
[Date]
date.timezone = "America/New_York"

結束後重啟 apache

方法二

php 裡設定

1
date_default_timezone_set("America/New_York")

方法三

php 裡設定

1
ini_set('date.timezone','America/New_York');

附上 php timezone list

mysql

說明

mysql timezone 有兩種格式

  • 與 UTC 的時差例如 +00:00
  • 時區名稱例如 Europe/Helsinki

方法一:需重新啟動 mysql

修改設定檔/etc/mysql/my.cnf

1
2
3
default-time-zone = '+8:00'
或是
timezone='UTC'

重啟 mysql

1
sudo systemctl restart mysqld

方法二:不用重新啟動 mysql

設定 global timezone:
有三種方式

1
2
3
SET GLOBAL time_zone = '+8:00';
SET GLOBAL time_zone = 'Europe/Helsinki';
SET @@global.time_zone='+00:00';

設定 session timezone:
有三種方式

1
2
3
SET time_zone = 'Europe/Helsinki';
SET time_zone = "+00:00";
SET @@session.time_zone = "+00:00";

integer , timestamp , datetime 比較

integer

  • 長度是4個位元組
  • 儲存空間上比 datatime 少,integer 索引儲存空間也相對較小,排序和查詢效率相對較高一點點
  • 可讀性差

timestamp

  • 長度是4個位元組
  • 值以 UTC 格式儲存
  • 時區轉化,儲存時對當前的時區進行轉換,檢索時再轉換回當前的時區
  • timestamp 值不能早於 1970 或晚於 2037

datetime

  • 長度是8個位元組
  • 與時區無關
  • 以 YYYY-MM-DD HH:MM:SS 格式檢索和顯示 datetime 值。支援的範圍為 1000-01-01 00:00:00 到 9999-12-31 23:59:59
Data Type Storage Required Before MySQL 5.6.4 Storage Required as of MySQL 5.6.4
DATETIME 8 bytes 5 bytes + fractional seconds storage
TIMESTAMP 4 bytes 4 bytes + fractional seconds storage

Mysql Date and Time Type Storage Requirements

查詢 timezone

1
SHOW VARIABLES LIKE '%time_zone%';

補充

mysql 還有一個 system_time_zone,是在 mysql server 開啟時就預設為與機器時間相同。


結論

DB 設定上考量到日光節約的原因,直接把 timezone 設為 UTC 似乎是比較好的方式,如果把 timezone 設為 UTC offset 的話,遇到日光節約時間又要調一次時區,不是很實際。若是把時區設為 UTC 在取出時再依 web server 時間(PHP timezone)轉換時間,比較好處理。

不過,timestamp 有 2038 年問題,在問題發生前就要再轉別的格式,例如整數型態或是 datetime,不過在那之前官方的解決方案應該就出來了。

另 DB table 裡有關時間欄位的型態設定,自動產生的時間應設為 timestamp,例如資料的 date_add、date_upd等。因為如果 DB 換了 timezone 時間才會跟著換,如果是 datetime,就是固定不變,可以視為一段字串來看。但是如果對時間的需求是設定的概念,例如設定商品的特價時間區間、或是文章的發佈時間,存成 datetime 是可以的,使用的時候也是取出後根據 web server timezone 去轉換,或是轉成使用者 local timezone ,視需求而定。

更多閱讀

只因「閏秒」這 1 秒的解決方案,AWS 工程師可能花上數百小時
到底是 GMT+8 還是 UTC+8 ?
Rails 時區好亂啊,通通轉成 UTC

Reference

https://stackoverflow.com/questions/19023978/should-mysql-have-its-timezone-set-to-utc

https://stackoverflow.com/questions/2532729/daylight-saving-time-and-time-zone-best-practices?rq=1

https://stackoverflow.com/tags/timezone/info

https://javorszky.co.uk/2016/06/06/today-i-learned-about-mysql-and-timezones/

https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html

Singleton 設計模式

說明

同個請求的生命週期裡,對於使用 singleton 的類別永遠只會產生一個實體 (instance)


做法

把建構子設為 private,建立一個 static 的 getInstance function ,在裡面控制只能 new 一次實體。


常用情境

  • 登入
  • 快取
  • 資料庫連線
  • 與驅動程式的溝通

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
class Singleton
{
/** Singleton instance */
private static $instance = null;

/** Constructor not to be used */
private function __construct()
{

}

/**
* Gets the instance of Singleton.
*
* @return self
*/
public static function getInstance()
{
if (!isset(self::$instance)) {
self::$instance = new self();
}

return self::$instance;
}
}

多執行緒語言需注意的情況

要注意多執行緒環境中,可能因時間差造成產生兩的個 instance
解法(以 Java 為例):

  • 使用同步化 (synchronized),缺點為其實只有第一次初始化的時候才需要使用 synchronzied ,因爲 instance 已經產生,後面都是多餘的,這會導致效能的浪費。

  • 預先初始化(eager initialization), 一開始就初始化,不管後面會不會用到,缺點是若沒有用到就是浪費記憶體。

  • 使用雙重檢查上鎖,先檢查是否已經建立了 instance,如果沒有才進行同步化,也就是只有第一次呼叫才會使用同步化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null){
synchronized(Singleton.class){
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

Reference

深入淺出設計模式 (Head First Design Patterns)