【Laravel官方文件導讀】超完整HTTP測試攻略

【Laravel官方文件導讀】超完整HTTP測試攻略

簡介

Laravel 提供了一個非常流暢的 API 用來向應用發出 HTTP 請求並檢查其回應。例如下面的這段功能測試:

//tests\Feature\ExampleTest.php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{

    public function test_a_basic_request()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

例子中 get() 向應用發出 GET 請求,而 assertStatus() 則確認返回的回應具有指定的 HTTP 狀態碼。除了這個簡單的確認外,Laravel 還包含用於檢查 Header,內容,JSON 結構等的各種確認工具

建立請求

要為你的應用去建立請求,需要在測試用例內去呼叫 get() . post() . put() . patch() 或 delete() 等方法。實際上這些方法並不是真的去發出 HTTP 請求到應用,僅僅是做內部模擬而已

也就是說,回應的並非 Illuminate\Http\Response 實例,而是 Illuminate\Testing\TestResponse 的實例。它提供了一系列有用的確認工具來允許你去檢查應用的回應

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_a_basic_request()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

而為了方便, CSRF 防護中介層會在執行測試時被自動關閉

自定義請求頭(Request Header)

你可使用 withHeaders() 自定義請求的標頭,然後再將其發送到應用內。這使你可自由的將任何想要的自定義標頭添加到請求中,請看下面這個例子:

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_interacting_with_headers()
    {
        $response = $this->withHeaders([
            'X-Header' => 'Value',
        ])->post('/user', ['name' => 'Sally']);

        //201狀態碼表示成功新增資料
        $response->assertStatus(201);
    }
}

Cookies

在發送請求前你可以使用 withCookie() 或 withCookies() 設置 cookie。withCookie() 接受 cookie 的名稱和值這兩個參數,而 withCookies() 接受一個名稱 / 值對陣列:

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_interacting_with_cookies()
    {
        $response = $this->withCookie('color', 'blue')->get('/');

        $response = $this->withCookies([
            'color' => 'blue',
            'name' => 'Taylor',
        ])->get('/');
    }
}

會話與驗證(Session / Authentication)

Laravel 提供了一些幫助函式用於在 HTTP 測試過程中去與 Session 互動。首先,你能夠利用 withSession() 去設定 Session 到指定陣列。這有助於如果需要在請求中去取得 Session 時很有用

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_interacting_with_the_session()
    {
        $response = $this->withSession(['banned' => false])->get('/');
    }
}

Laravel 的對話 (Session) 常用於保存當前登入者的狀態。這時候 actingAs() 提供了一種便利方式讓你以某個用戶作為測試請求登入者。例如下面這個例子,我們使用模型工廠來產生一個新用戶並用其進行登入:

\\tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use App\Models\User;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_an_action_that_requires_authentication()
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)
                         ->withSession(['banned' => false])
                         ->get('/');
    }
}

你也可以通過傳入看守器名稱作為 actingAs() 的第二參數以指定用戶通過哪種看守器來認證:

$this->actingAs($user, 'api')

對回應進行除錯

在對應用進行測試請求後,dump() . dumpHeaders() 和 dumpSession() 可以用來分析並針對回應來進行除錯:

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_basic_test()
    {
        $response = $this->get('/');

        $response->dumpHeaders();

        $response->dumpSession();

        $response->dump();
    }
}

測試 JSON 的 API

Laravel 也提供了幾個幫助函式來測試 JSON API 和其回應。例如,json() . getJson() . postJson() . putJson() . patchJson() . deleteJson() 以及 optionsJson() 可以被用於發送各種 HTTP 動詞。你也可以輕鬆地將資料和請求頭傳遞到這些方法中。下面的這個測試用例, 發送 POST 請求到 /user ,並確認返回的期望資料:

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_making_an_api_request()
    {
        $response = $this->postJson('/api/user', ['name' => 'Sally']);

        $response
            ->assertStatus(201)
            ->assertJson([
                'created' => true,
            ]);
    }
}

JSON 回應資料可當作陣列變數來進行取用,方便你去檢查所傳回的某個值

技巧:

assertJson 將回應轉換為一個陣列並利用 PHPUnit::assertArraySubset() 來驗證指定的陣列存在於應用返回的 JSON 回應中。因此如果 JSON 回應中有其他屬性,測試仍舊會在指定陣列存在的情況下通過

$this->assertTrue($response['created']);

確認 JSON 完全匹配

如果你想驗證指定的陣列完全而非部份匹配應用返回的 JSON 結果,可使用 assertExactJson():

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_asserting_an_exact_json_match()
    {
        $response = $this->json('POST', '/user', ['name' => 'Sally']);

        $response
            ->assertStatus(201)
            ->assertExactJson([
                'created' => true,
            ]);
    }
}

Asserting On JSON Paths If you would like to verify that the JSON response contains the given data at a specified path, you should use the assertJsonPath method:

確認 JSON 路徑

如果你想確認 JSON 回應是否包含指定路徑上的某些指定資料,可以使用 assertJsonPath() :

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_asserting_a_json_paths_value()
    {
        $response = $this->json('POST', '/user', ['name' => 'Sally']);

        $response->assertStatus(201)
                 ->assertJsonPath('team.owner.name', 'Darian');
    }
}

更流暢的進行 JSON 測試

Laravel 也提供一個更漂亮的方式來讓你測試應用的回應。首先,傳入一個 Closure 到 assertJson()。這個 Closure 將得到 Illuminate\Testing\Fluent\AssertableJson 的實例用來確認應用所傳回的 JSON

where() 用來確認 JSON 內的指定值是否存在且正確,反之,missing() 用來確認 JSON 不存在該指定值

use Illuminate\Testing\Fluent\AssertableJson;

public function test_fluent_json()
{
    $response = $this->json('GET', '/users/1');

    $response
        ->assertJson(fn (AssertableJson $json) =>
            $json->where('id', 1)
                 ->where('name', 'Victoria Faith')
                 ->missing('password')
                 ->etc()
        );
}
關於 etc()

在上面的例子中,你可能注意到有個 etc() 被串在確認鏈的最後頭。這個方法是用來向 Laravel 報告說 JSON 物件裡頭還有其他沒有確認的屬性值。假如你沒有使用 etc(),這個測試會失敗因為你沒有驗證完 JSON 上的所有屬性

Laravel 這樣設計的背後原因是為了保護你,避免在無意間將敏感資料暴露在 JSON 回應裡頭。為此,你的處理策略有兩個:對該屬性進行驗證又或者是乾脆呼叫 etc() 來讓 Laravel 不要管

確認 JSON 集合

在你回傳的 JSON 回應包含多筆資料是很常見的,比如多個用戶:

Route::get('/users', function () {
    return User::all();
});

在這種情況下,可使用 JSON 物件的 has() 來確認指定用戶是否存在於回應的集合內。例如下面這個例子,首先確認 JSON 回應包含3筆資料。接下來我們用 first() 來對第一個用戶進行一些確認, first() 接受一個 Closure 並傳入測試用 JSON 字串 ,讓我們能夠使用剛才的技巧來檢查 JSON 集合內的第一筆內容

$response->assertJson(fn (AssertableJson $json) =>
    $json->has(3)
         ->first(fn ($json) =>
            $json->where('id', 1)
                 ->where('name', 'Victoria Faith')
                 ->missing('password')
                 ->etc()
         )
);
Scoping JSON Collection Assertions

有時候,你所回傳的 JSON 集合為鍵值對的結構:

Route::get('/users', function () {
    return [
        'meta' => [...],
        'users' => User::all(),
    ];
})

當驗證這樣的回應,可使用 has() 來檢查集合內某個鍵值所對應的陣列裡頭的元素數量。此外,你也能夠使用 has() 來針對某個區域進行一連串的驗證,請看下面這個例子

$response->assertJson(fn (AssertableJson $json) =>
    $json->has('meta')
         ->has('users', 3)
         ->has('users.0', fn ($json) =>
            $json->where('id', 1)
                 ->where('name', 'Victoria Faith')
                 ->missing('password')
                 ->etc()
         )
);

上面的這個寫法還可以再進行簡化,你可以把後面兩個關於 users 的驗證整合成一個,做法是在 has() 多傳入一個第二參數,內容為要驗證的數量。在這情況下,你不再需要去指定索引值為0的元素,因為它預設就會針對集合內的第一筆資料來進行驗證。下面這個寫法結果和上面完全相同:

$response
->assertJson(fn (AssertableJson $json) =>
    $json->has('meta')
         ->has('users', 3, fn ($json) =>
            $json->where('id', 1)
                 ->where('name', 'Victoria Faith')
                 ->missing('password')
                 ->etc()
         )
);

確認 JSON 類型

你可能只想要確認 JSON 回應的某個屬性是否為指定類型。Illuminate\Testing\Fluent\AssertableJson 類別提供了 whereType() 和 whereAllType() 就是為此而生的

$response->assertJson(fn (AssertableJson $json) =>
    $json->whereType('id', 'integer')
         ->whereAllType([
            'users.0.name' => 'string',
            'meta' => 'array'
        ])
);

如果要驗證多個類型可使用 | 字元,或者是將第二參數改為傳入一個陣列,裡頭放入要驗證的所有類型。在這情況下,只要該回應值的類型屬於陣列內的任何一種就算成功

$response->assertJson(fn (AssertableJson $json) =>
    $json->whereType('name', 'string|null')
         ->whereType('id', ['string', 'integer'])
);

whereType() 和 whereTypeAll() 支援的類型:

類型 說明
string 字串
integer 整數
double 浮點數
boolean 布林值
array 陣列
null 空值

測試檔案上傳

Illuminate\Http\UploadedFile 提供了一個 fake() 用於生成虛擬的文件或者圖像以供測試之用。它可以和 Storage facade 的 fake() 相結合,大幅簡化檔案上傳測試。舉個例子,下面結合這兩個功能來進行頭像上傳表單的測試:

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_avatars_can_be_uploaded()
    {
        //模擬生成 avatars 資料夾
        Storage::fake('avatars');

        //模擬出一個 avatar.jpg 圖片
        $file = UploadedFile::fake()->image('avatar.jpg');

        $response = $this->post('/avatar', [
            'avatar' => $file,
        ]);

        // 確認文件有被儲存
        Storage::disk('avatars')->assertExists($file->hashName());

        // 確認 missing.jpg 文件不存在
        Storage::disk('avatars')->assertMissing('missing.jpg');
    }
}

虛擬文件自定義

在使用 UploadedFile 類別所提供的 fake() 創建文件時,你可以指定圖片的寬高以及大小,從而更好的確認測試規則:

UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100);

除了創建圖片外,你也可以用 create() 來創建其他類型的文件

UploadedFile::fake()->create('document.pdf', $sizeInKilobytes);

如果需要,你還可以傳入一個 $mimeType 參數來明確定義文件應返回的 MIME 類型

UploadedFile::fake()->create(
    'document.pdf', $sizeInKilobytes, 'application/pdf'
);

測試視圖

Laravel 允許你在不向應用程序發出模擬 HTTP 請求的情況下獨立呈現視圖。為此,你可以在測試中使用 view() 。view() 接受視圖名稱和一個可選的資料陣列。這個方法返回一個 Illuminate\Testing\TestView 的實例,它提供了幾個方法來方便地確認視圖的內容

//tests\TestCase\ExampleTest.php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_a_welcome_view_can_be_rendered()
    {
        $view = $this->view('welcome', ['name' => 'Taylor']);

        $view->assertSee('Taylor');
    }
}

TestView 物件提供了以下確認方法:assertSee() . assertSeeInOrder() . assertSeeText() . assertSeeTextInOrder() . assertDontSee() 和 assertDontSeeText()

如果需要,你可以通過將 TestView 實例轉型為一個字串來獲得原始的視圖內容

$contents = (string) $this->view('welcome');

共享錯誤

一些視圖可能依賴於 Laravel 提供的全域錯誤包中共享的錯誤。要在錯誤包中生成錯誤訊息,可以使用 withViewErrors():

$view = $this->withViewErrors([
    'name' => ['請提供有效的名字']
])->view('form');

$view->assertSee('請提供有效的名字');

渲染原始的 Blade

如有必要,你可以使用 blade() 來計算和呈現原始的 Blade 字串。與 view() 一樣,blade() 返回的是 Illuminate\Testing\TestView 實例

$view = $this->blade(
    '<x-component :name="$name" />',
    ['name' => '哥布林工程師']
);

$view->assertSee('哥布林工程師');

你能夠使用 component() 來驗證與渲染一個 Blade 組件。和 view() 一樣,component() 會回傳 Illuminate\Testing\TestView 實例

$view = $this->component(Profile::class, ['name' => '哥布林工程師']);

$view->assertSee('哥布林工程師');

所支持的確認方法

回應確認

Laravel 的 Illuminate\Testing\TestResponse 為你的功能測試提供了各種自定義確認方法。這些確認可以串在從 json() .get() .post() . put() 和 delete() 等測試方法返回的回應之後:

assertCookie()

確認回應中包含指定的 cookie

$response->assertCookie($cookieName, $value = null);
assertCookieExpired()

確認回應包含指定的已過期 cookie

$response->assertCookieExpired($cookieName);
assertCookieNotExpired()

確認回應包含指定的尚未過期 cookie

$response->assertCookieNotExpired($cookieName);
assertCookieMissing()

確認回應不包含指定的 cookie:

$response->assertCookieMissing($cookieName);
assertCreated()

確認回應的狀態碼為 201:

$response->assertCreated();
assertDontSee()

確認指定的字串不包含在回應中。除非傳遞第二個參數 false,否則此確認將指定字串進行跳脫後再匹配:

$response->assertDontSee($value, $escaped = true);
assertDontSeeText()

確認指定字串不包含在回應文字內容中。除非傳遞第二個參數 false,否則此確認將指定字串進行跳脫後才匹配。另外此方法將會在測試前跳過回應中包含 strip_tags()的內容:

$response->assertDontSeeText($value, $escaped = true);
assertExactJson()

確認回應包含與指定 JSON 資料完全匹配

$response->assertExactJson(array $data);
assertForbidden()

確認回應中有禁止訪問 (403) 狀態碼

$response->assertForbidden();
assertHeader()

確認指定的 header 在回應中存在

$response->assertHeader($headerName, $value = null);
assertHeaderMissing()

確認指定的 header 在回應中不存在

$response->assertHeaderMissing($headerName);
assertJson()

確認回應包含指定的 JSON 資料

$response->assertJson(array $data, $strict = false);

這方法會將回應轉成陣列並執行 PHPUnit::assertArraySubset() 來確認指定陣列存在於 JSON 回應內。因此如果JSON回應中包含其他屬性的話,只要指定陣列有存在就沒關係

assertJsonCount()

確認回應 JSON 中有一個陣列,其中包含指定鍵的預期元素數量

$response->assertJsonCount($count, $key = null);
assertJsonFragment()

確認回應包含指定 JSON 片段

Route::get('/users', function () {
    return [
        'users' => [
            [
                'name' => 'Taylor Otwell',
            ],
        ],
    ];
});

$response->assertJsonFragment(['name' => 'Taylor Otwell']);
assertJsonMissing()

確認回應未包含指定的 JSON 片段

$response->assertJsonMissing(array $data);
assertJsonMissingExact()

確認回應不包含確切的 JSON 片段

$response->assertJsonMissingExact(array $data);
assertJsonMissingValidationErrors()

在 Laravel 驗證返回的 JSON 格式的錯誤中缺少指定的鍵

$response->assertJsonMissingValidationErrors($keys);
assertJsonPath()

確認 JSON 回應包含指定節點上的指定資料

$response->assertJsonPath($path, $expectedValue);

例如 JSON 回應包含以下資料:

{
    "user": {
        "name": "Steve Schoger"
    }
}

你可能想要確認 user 物件的 name 屬性符合某個指定值:

$response->assertJsonPath('user.name', '哥布林');
assertJsonStructure()

確認回應具有指定的 JSON 結構

$response->assertJsonStructure(array $structure);

例如,回應包含以下資料:

{
    "user": {
        "name": "Steve Schoger"
    }
}

你能夠像這樣去確認 JSON 結構是否符合你的預期:

$response->assertJsonStructure([
    'user' => [
        'name',
    ]
]);

有時, JSON 回應會包含物件陣列:

{
    "user": [
        {
            "name": "Steve Schoger",
            "age": 55,
            "location": "Earth"
        },  
        {
            "name": "Mary Schoger",
            "age": 60,
            "location": "Earth"
        }
    ]
}

在這情況下,你能使用萬用字元(*) 來驗證在該陣列裡頭的所有物件之結構:

$response->assertJsonStructure([
    'user' => [
        '*' => [
             'name',
             'age',
             'location'
        ]
    ]
]);
assertJsonValidationErrors()

在 Laravel 驗證返回的 JSON 格式的錯誤中包含指定的鍵。此方法只能驗證錯誤包是以 JSON 結構而非存放於 session 的設計

$response->assertJsonValidationErrors(array $data);
assertLocation()

確認回應在 Location Header 中具有指定的 URI 值

$response->assertLocation($uri);
assertNoContent()

確認回應具有指定的狀態碼且沒有內容

$response->assertNoContent($status = 204);
assertNotFound()

確認回應具有未找到狀態碼(404)

$response->assertNotFound();
assertOk()

確認回應有正常狀態碼(200)

$response->assertOk();
assertPlainCookie()

確認回應包含指定的 cookie (未加密)

$response->assertPlainCookie($cookieName, $value = null);
assertRedirect()

確認回應會轉址到指定的 URI

$response->assertRedirect($uri);
assertSee()

確認指定的字串包含在回應中。除非傳遞第二個參數 false ,否則此確認將指定字串進行跳脫後再比對

$response->assertSee($value, $escaped = true);
assertSeeInOrder()

確認指定的字串按順序包含在回應中。除非傳遞第二個參數 false ,否則此確認將指定字串進行跳脫後再比對

$response->assertSeeInOrder(array $values, $escaped = true);
assertSeeText()

確認指定字串包含在回應文字內容中。除非傳遞第二個參數 false,否則此確認將指定字串進行跳脫後再比對,在比對前會跳過 strip_tags()

$response->assertSeeText($value, $escaped = true);
assertSeeTextInOrder()

確認指定的字串按順序包含在回應的文字內容中。除非傳遞第二個參數 false ,否則此確認將指定字串進行跳脫後再比對,在比對前會跳過 strip_tags()

$response->assertSeeTextInOrder(array $values, $escaped = true);
assertSessionHas()

確認 session 包含指定的資料

$response->assertSessionHas($key, $value = null);
assertSessionHasInput()

session 在閃存輸入陣列中確認具有指定值

$response->assertSessionHasInput($key, $value = null);
assertSessionHasAll()

確認 Session 中具有指定的鍵值對列表

$response->assertSessionHasAll(array $data);

例如,應用的 session 包含名字和狀態這兩個鍵,你能夠驗證它們是否存在並是否具備指定值,如下寫法:

$response->assertSessionHasAll([
    'name' => 'Taylor Otwell',
    'status' => 'active',
]);
assertSessionHasErrors()

確認 session 包含指定 $keys 的 Laravel 驗證錯誤。如果 $keys 是關聯陣列,則確認 session 包含每個欄位(key)的特定錯誤訊息(value)。此方法適用於驗證錯誤是以 session 進行儲存而非以 JSON 結構來回傳的設計

$response->assertSessionHasErrors(
    array $keys, $format = null, $errorBag = 'default'
);

例如確認 name 和 email 欄位有驗證錯誤且被快閃存放於 session ,你就能夠呼叫 the assertSessionHasErrors(),像這樣:

$response->assertSessionHasErrors(['name', 'email']);

或者,你也能夠確認指定欄位是否具備特定的驗證錯誤訊息

$response->assertSessionHasErrors([
    'name' => 'The given name was invalid.'
]);
assertSessionHasErrorsIn()

功能與前者 assertSessionHasErrors() 大致相同,但針對特定的錯誤包

$response->assertSessionHasErrorsIn($errorBag, $keys = [], $format = null);
assertSessionHasNoErrors()

確認 session 中沒有 laravel 驗證錯誤:

$response->assertSessionHasNoErrors();
assertSessionDoesntHaveErrors()

確認 session 缺少指定的錯誤,如果 $key 為空,則確認 session 沒有任何錯誤:

$response->assertSessionDoesntHaveErrors($keys = [], $format = null, $errorBag = 'default');
assertSessionMissing()

確認 session 中缺少指定的 $key

$response->assertSessionMissing($key);
assertStatus()

確認回應指定的 http 狀態碼

Assert that the response has a given HTTP status code:

$response->assertStatus($code);
assertSuccessful()

確認回應為成功的狀態碼 (>= 200 且 < 300)

$response->assertSuccessful();
assertUnauthorized()

確認回應為未認證的狀態碼 (401)

$response->assertUnauthorized();
assertViewHas()

確認回應視圖包含了指定鍵值對資料

$response->assertViewHas($key, $value = null);

此外,視圖資料可作為回應上的陣列變數,方便你進行檢查:

$this->assertEquals('哥布林工程師', $response['name']);
assertViewHasAll()

確認回應視圖具有指定的資料列表

$response->assertViewHasAll(array $data);

此方法用於確認該視圖只包含指定鍵的資料

$response->assertViewHasAll([
    'name',
    'email',
]);

如果你不只想確認指定鍵存在,而要確認值是否正確,可改成這樣:

$response->assertViewHasAll([
    'name' => 'Taylor Otwell',
    'email' => 'taylor@example.com,',
]);
assertViewIs()

確認當前路由返回的的視圖是指定的視圖

$response->assertViewIs($value);
assertViewMissing()

確認回應的視圖缺少某個鍵的資料

$response->assertViewMissing($key);

驗證確認

Laravel 還為 PHPUnit 提供了各種與身份驗證相關的確認方便你用於功能測試

要注意的是這些方法是透過測試用例本身來進行呼叫而非 get() 和 post() 所回傳的 Illuminate\Testing\TestResponse 實例

assertAuthenticated()

確認當前用戶已通過身份驗證

$this->assertAuthenticated($guard = null);
assertGuest()

確認當前用戶沒有通過身份驗證

$this->assertGuest($guard = null);
assertAuthenticatedAs()

確認指定的用戶已通過身份驗證

$this->assertAuthenticatedAs($user, $guard = null);

分享這篇文章:

關聯文章:

訂閱電子報,索取 Laravel 學習手冊

價值超過 3000 元,包含常用 Laravel 語法與指令!

Laravel 百萬年薪特訓營

從最基礎的 PHP 語法開始,包含所有你該知道的網頁基礎知識,連同 Laravel 從零開始一直到實戰,最後還將告訴你如何找好工作,讓你及早擁有百萬年薪