2019年9月8日 星期日

Symfony4.2 基本應用【第一篇】表單(Form)



你更傾向影片教學?查看教學影片

對網頁開發來說,處理HTML表單是常見且具挑戰性的任務之一,Symfony結合元件Form來幫助你處理。在本文中,你將從簡單開始到複雜表單,逐步學習表單的最重要特性。


安裝

在使用之前請先執行以下指令安裝:
composer require symfony/form
Symfony Form這個元件是獨立的函式庫,因此可以在Symfony的專案以外使用,若想要了解更多資訊,請看Github上的Form component documentation


建立簡單的表單

假設你要建立一個簡單的備忘清單應用來顯示任務"tasks",因為使用者需要編輯和新增任務,所以你需要新增一個表格。但在建立表單之前,先建立一個Task的類(class)來儲存任務資料:
// src/Entity/Task.php
namespace App\Entity;

class Task
{
    protected $task;
    protected $dueDate;

    public function getTask()
    {
        return $this->task;
    }

    public function setTask($task)
    {
        $this->task = $task;
    }

    public function getDueDate()
    {
        return $this->dueDate;
    }

    public function setDueDate(\DateTime $dueDate = null)
    {
        $this->dueDate = $dueDate;
    }
}
這個類是個普通老式的PHP物件,因為到目前為止,它與Symfony或任何資源庫都無關,這個普通的PHP物件能夠直接解決你應用中的問題(i.e. 即這個應用裡代表任務的需求)。在本文結束時,你將能把數據提交到Task實例(透過HTML表單),驗證其數據並將此永久儲存到資料庫中。

創建表單
現在你已經有Task,下一步是新增實際的HTML表單,在Symfony中,實作的方式是產生一個表單的物件並給予模板顯示,現在這一切都可以在控制器內完成:
// src/Controller/TaskController.php
namespace App\Controller;

use App\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;

class TaskController extends AbstractController
{
    public function new(Request $request)
    {
        // creates a task and gives it some dummy data for this example
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));

        $form = $this->createFormBuilder($task)
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, ['label' => 'Create Task'])
            ->getForm();

        return $this->render('task/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}
這個範例事告訴你如何直接在控制器內建立表單,待會在"建立表單類(Creating Form Classes)"的章節裡,你會學到如何在一個獨立的類(a standlone class)裡建立表單,因為可以讓表單重複利用,所以比較推薦的方式。

新增一個表單需要相對短的程式碼,因為Symfony表單物件是由"form builder"所建立,這個"form builder"目的在方便你寫一個簡單的表單"食譜(recipes)",讓它完成實際構建表格的所有繁重工作。

在這個範例裡,你新增兩個欄位到表單 taskdueDate,相應於 Task 類(class)中的兩個屬性 taskdueDate,你也用完整的類名指定了每一個欄位的"類型(type)"(e.g. TextTypeDateType),除此之外,他還代表此欄位該顯示什麼HTML標籤。

最後,你需要增加一個提交(submit)的按鈕,並可以自訂顯示文字(label),來提交表單給伺服器。

Symfony提供許多類型,將稍後討論(請參考Built-in Field Types)

顯示表單(Rendering the Form)
現在表單已經被創建了,下一步就要顯示它,實作的方式是傳一個特殊的表單"視圖(view)"物件到你的模板(請看上面controller範例程式中寫道$form->createView()),接著使用 form helper functions:
{# templates/task/new.html.twig #}
{{ form(form) }}


就是這樣!form這個函式將會顯示所有欄位,夾在 <form> 開始和結束的tag。預設情況下,form method是 POST ,預設情況下目標路徑(target URL)和目前顯示表單的路徑相同,根據需求兩者都可以修改。

像這樣簡短的方式缺乏彈性,通常你需要針對整個或部分的欄位有更多調整的需求,Symfony提供一些方式來達成:
  • 如果你的應用程式有使用CSS框架如Bootstrap或Foundation,可以善用built-in form themes來讓你的表單達成一致的風格。
  • 如果你需要針對少數的欄位進行客製化,或者只是應用中少部分的表單,請閱讀文章How to Customize Form Rendering
  • 如果你希望所有的表單風格都一致,你可建立一個Symfony表單主題(Symfony form theme)(可以以任何內建的主題為基礎或是全部重新建立)。

往下進行下去之前,你會注意到如何顯示task輸入欄位,是因為$task物件有task的值,這是表單的第一個工作:從物件取出資料並轉換成適合在HTML表單中顯示的格式。
表單系統非常聰明,可以藉由Task類中兩個方法getTask()setTask()來取得task被保護屬性(protected property)的值,除非屬性是公開的(public),否則必須有"getter"和"setter"方法使得表單元件可以取得並將資料放入這個屬性,如果是Boolean性質,你可以用"isser"或"hasser"方法(例如:isPublished()hasReminder()來替代getter(例如:getPublished()getReminder()))


處理表單的提交
預設情況下,表格會透過POST的請求提交到與顯示表單同一個控制器。

接下來,第二個步驟是要解析使用者傳送資料的屬性,要達到這個目的,這些提交資料必須寫入Form物件內,請將下列程式碼加入你的控制器:
// ...
use Symfony\Component\HttpFoundation\Request;

public function new(Request $request)
{
    // just setup a fresh $task object (remove the dummy data)
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, ['label' => 'Create Task'])
        ->getForm();

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // $form->getData() holds the submitted values
        // but, the original `$task` variable has also been updated
        $task = $form->getData();

        // ... perform some action, such as saving the task to the database
        // for example, if Task is a Doctrine entity, save it!
        // $entityManager = $this->getDoctrine()->getManager();
        // $entityManager->persist($task);
        // $entityManager->flush();

        return $this->redirectToRoute('task_success');
    }

    return $this->render('task/new.html.twig', [
        'form' => $form->createView(),
    ]);
}
請注意!createView()方法必須在handleRequest()之後才能呼叫,否則*_SUBMIT事件所完成的變化將不會採用到view裡(像是驗證錯誤訊息)。

這個控制器是依照普遍的模式來處理表單,有三種可能的路徑:
  • 當瀏覽器初始載入頁面時,表單會被建立並顯示,handleRequest()知道表單尚未提交並不採取任何行動,若表單沒有被提交,則isSubmitted()回傳false
  • 當用戶提交表單時,handleRequest()會知道並立即把提交的資料寫入$task的屬性taskdueDate,接著對這個物件進行驗證,如果這個物件不合法(驗證將在下個章節說明),isValid()回傳false,並重新顯示表單,並附註驗證錯誤。
  • 當使用者提交合法資料的表單,被提交的資料又再次被寫入表單,但這一次isValid()會回傳true,在你重新導向到其他頁面之前(e.g. 一個"感謝"或"成功"頁面),你有機會用$task物件來執行一些動作(像是永久保存到資料庫)。

    成功地將表單提交後執行重新導向,可以避免使用者重新點擊"重新整理(refresh)"按鈕,導致重複送出資料。

如果你需要操控權,像是何時送出表單或是那些資料被傳輸,你可以使用方法submit(),詳情請看『自行呼叫Form::submit()』(Calling Form::submit() manually)。


表格驗證(Form Validation)

在前面的章節中,你已經知道表單送出的資料可能是合法的(valid)或不合法的(invalid),在Symfony中,驗證可以用在基礎物件(e.g. Task),換句話說,我們所要驗證的並非這個"表單form",而是表單被提交的這些資料所產生$task這個物件是否合法,呼叫$form->isValid()是個最方便的方法來確認$task物件是否為合法的輸入資料。

在使用驗證之前,請先執行下面指令讓你的應用支援這個功能:
composer require symfony/validator
所謂的驗證就是在class裡增加一些規定(稱為限制 constraints),為了展示這項功能,我們添加一些驗證限制使得task這個欄位不能是空的,另外dueDate欄位除了不能是空的,也必須是合法的Datetime物件。
// 省略程式碼
就是這樣!如果你用非合法的資料重新提交表單,你會看到相應的錯誤顯示在表單上。

驗證是Symfony非常強大的功能,可以參考這篇專門的介紹。(連結)

HTML5 Validation
多虧了HTML5,有需多瀏覽器可以在用戶端強制執行某些內建的驗證,最常見的驗證就是替必填的欄位加上required這個屬性,對於支援HTML5的瀏覽器,若使用這嘗試提交帶有空白欄位的表單,就會顯示瀏覽器內建的訊息。

建立帶有可被HTML偵測的屬性之表單將會充分發會這個新的特性,並觸發驗證。然而用戶端的驗證可以在formtag加上novalidate這個屬性,或是提交(submit)tag上加上formnovalidate這個屬性,來取消驗證。當你在測試伺服器端(server-side)的驗證限制時特別實用,但瀏覽器不會允許,例如,提交空白字段。
{# templates/task/new.html.twig #}
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}


內建欄位類型(Built-in Field Types)

Symfony囊括需多常見的表單欄位和資料類型:

Text Fields

Choice Fields

Date and Time Fields

Other Fields

Field Groups

Hidden Fields

Buttons

Base Fields

Field Type Options

每一個欄位類型都有一些選項可以設定,例如dueDat現在顯示3個選擇方框,然而DateType可以設定成顯示單一的文字方塊(使用者需要在方塊內輸入字串):
->add('dueDate', DateType::class, ['widget' => 'single_text'])


每一個欄位類型都有一些不一樣的選項可以以陣列的方式傳入,有許多欄位類型特有的選項,可以參閱各類型的文件了解細節。

必填選項
最常見的選項是required,可以廣泛地應用在任何欄位,預設情況required設定為true,表示HTML5會在用戶端採取這些驗證以防用戶未填寫欄位,如果這並不是你期望的行為,你可以選擇取消HTML5的驗證或將required設定成false
->add('dueDate', DateType::class, [
    'widget' => 'single_text',
    'required' => false
])
請注意required的設定並不會同時在伺服器端採用這些驗證,換句話說,如果用戶提交空值(假設是舊瀏覽器或網路服務),除非你使用SymfonyNotBlankNotNull的驗證條件,否則將被視為合法的值。

也就是說,雖然required很重要,但伺服器端的驗證還是隨時需要。

標籤選項
表單欄位的標籤可以透過label來設定,並可以應用在任何欄位:
->add('dueDate', DateType::class, [
    'widget' => 'single_text',
    'label'  => 'Due Date',
])
欄位的label也可以在模板的表單裡顯示,如果不需要你的輸入和label有關聯,你可以設定成false來取消關聯。
預設下,顯示必填欄位會在label加上required的CSS class,所以你可以應用下述的CSS style在必填欄位顯示一個*:
label.required:before {
    content: "*";
}


Field Type Options GuessingField Type Guessing

現在你為Task新增驗證metadata,Symfony對於你的欄位已經知道些許,如果你允許的話,Symfony可以"猜測guess"你欄位的類型並替你進行設定。例如,Symfony藉由驗證規則會揣測task是一個普通的TextType,而dueDate則是DateType
public function new()
{
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task')
        ->add('dueDate', null, ['widget' => 'single_text'])
        ->add('save', SubmitType::class)
        ->getForm();
}
當你省略add()第二個參數時會啟動"guessing"(或傳入null),如果你傳入帶有選項的陣列作為第三個參數(如上述的dueDate),這些選項會被應用到需要被猜測的欄位。

如果表單使用特定的驗證組,當欄位類型guesser在猜測欄位類型時,會考慮所有的驗證限制(包含不屬於驗證組的一部份但有使用的限制)。


Field Type Options Guessing

為了猜測欄位的"類型",Symfony可以試圖猜出數字欄位的正確的數值。

如此設定後,這些欄位以特殊的HTML屬性提供HTML5在用戶端進行驗證,然而這不會相應地在伺服器端中產生這些限制(e.g. Assert\Length),雖然你必須在伺服器端手動加入驗證,這些欄位類型選項可以由這些資訊被預料的(guessed)。

required
required這個選項可以guessed,基於驗證的規則(i.e. 欄位是不是NotBlank或是NotNull)或是Doctrine metadata(i.e. 欄位是否nullable),這非常實用,用戶端驗證會自動與你的驗證規則相吻合。

maxlength
如果欄位是一種text field,那maxlength選項就可以guessed藉由驗證限制(若使用LengthRange)或藉由Doctrine metadata(藉由欄位長度)。

如果你使用Symfony猜測這些欄位類型,這些欄位選項只能是guessed。(i.e. 省略或傳入null作為add()第二個參數)

如果你想要改變其中一個guessed值,你可以傳入選項的陣列到非必填欄位來覆蓋。
->add('task', null, ['attr' => ['maxlength' => 4]])


建立表單類 Creating Form Classes

就像你看到的,表單可以直接在控制器裡建立並直接使用,然而更好的做法是建立獨立PHP class的表單,並可以在應用中隨處重複使用,建立新的類會包含構建任務表單的邏輯:
// src/Form/TaskType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, ['widget' => 'single_text'])
            ->add('save', SubmitType::class)
        ;
    }
}
這個新的類(class)包含建立Task表單所有需要的動作,控制器需要建立表單物件時可以使用:
// src/Controller/TaskController.php
use App\Form\TaskType;

public function new()
{
    $task = ...;
    $form = $this->createForm(TaskType::class, $task);

    // ...
}
將表單邏輯放在它自有的類,表示表單可以在專案中重複使用,這是最好的建立表單方式,但選擇最後終究取決於你。

Setting the data_class
所有表單都必須知道保存資料的類名稱(e.g. App\Entity\Task)。大多guessed物件作為createForm()(i.e. $task)第二個參數傳入,等一下當你開始嵌入表單時,這樣就不再足夠了。因此,由於它並不是永遠必要的,如同下面範例,通常指定data_class選項為你的表單的type class是一個好的做法。
// src/Form/TaskType.php
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'data_class' => Task::class,
    ]);
}

將表單對應到物件時,所有的欄位都必須被對應到,若表單中有任何欄位不存在於對應的物件,會促發一個例外事件(expection)。

因此如果你需要在表單中加一個額外的欄位(例如:一個核選方框checkbox"請問你同意這些條例嗎"),此欄位並不屬於對應的物件時,你需要將mapped這個選項設定成false
use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('task')
        ->add('dueDate')
        ->add('agreeTerms', CheckboxType::class, ['mapped' => false])
        ->add('save', SubmitType::class)
    ;
}
此外,如果表單中有任何欄位不包含在提交的資料中,這些欄位必須明確地設定成null

欄位資料可以直接在控制器裡這樣存取:
$form->get('agreeTerms')->getData();
另外,非對應於物件的欄位資料可以直接被修改。
$form->get('agreeTerms')->setData(true);

表單名稱是由type class名稱自動產生而來,如果你想要修改,請使用方法createNamed()
// src/Controller/DefaultController.php
use App\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class DefaultController extends AbstractController
{
    public function newAction()
    {
        $task = ...;
        $form = $this->get('form.factory')->createNamed('name', TaskType::class, $task);

        // ...
    }
}
你也可以設定為空字串,直接完全地縮減名稱。


結語

當建立表單時,第一個要謹記於心的是如何將資料從物件(Task)轉換成HTML表格,讓使用者可以修改資料,表單的第二個目標就是讓使用者提交資料,並使這些更動重新儲存於物件。

表單系統還有須多可以學習,也還有很多強大的技巧。


了解更多

  • How to Change the Action and Method of a Form
  • Bootstrap 4 Form Theme
  • How to Choose Validation Groups Based on the Clicked Button
  • How to Create a Custom Form Field Type
  • How to Create a Form Type Extension
  • How to Choose Validation Groups Based on the Submitted Data
  • When and How to Use Data Mappers
  • How to Use Data Transformers
  • How to Use the submit() Function to Handle Form Submissions
  • How to Disable the Validation of Submitted Data
  • How to Dynamically Modify Forms Using Form Events
  • How to Embed Forms
  • Form Events
  • How to Embed a Collection of Forms
  • How to Customize Form Rendering
  • How to Access Services or Config from Inside a Form
  • How to Work with Form Themes
  • How to Reduce Code Duplication with "inherit_data"
  • How to Submit a Form with Multiple Buttons
  • Creating a custom Type Guesser
  • How to Unit Test your Forms
  • How to Configure empty Data for a Form Class
  • How to Dynamically Configure Form Validation Groups
  • How to Define the Validation Groups to Use
  • How to Use a Form without a Data Class
  • How to Upload Files
  • Form Types Reference
  • How to Implement CSRF Protection


資料翻譯自:
https://symfony.com/doc/4.2/forms.html

系列文章:

2019年8月14日 星期三

MySQL/MariaDB遠端連線設定



使用DbVisualizer(版本10.0.21)來連線VMWare開的虛擬機資料庫,卻顯示下面的錯誤:
MySQL Connection Error: (1130) Host 'xxx.xx.x.x' is not allowed to connect to this MySQL server


環境

  • MySQL 5.7.27
  • Ubuntu 18.04


步驟

1. 開放防火牆
預設的埠號(port number)3306。
sudo ufw allow mysql/tcp

sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
3306/tcp                   ALLOW       Anywhere
3306/tcp (v6)              ALLOW       Anywhere (v6)

2. 修改綁定的連線位址
請找到指定檔案
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
把這一行刪掉或註解
bind-address=127.0.0.1

3. 新增遠端連線的使用者
預設的情況下,用戶應該都只能從localhost進行連線,查詢方式:
SELECT User,Host FROM mysql.user;
+--------------+------------------+
| host         | User             |
+--------------+------------------+
| localhost    | debian-sys-maint |
| localhost    | mysql.session    |
| localhost    | mysql.sys        |
| localhost    | root             |
+--------------+------------------+
可以看到有4個使用者,允許從localhost進行連線。

方法一、新增指定IP的連線用戶
CREATE USER 'username'@'ip_address' IDENTIFIED BY 'mypassword';
GRANT ALL PRIVILEGES ON *.* TO 'username'@'ip_address';
方法二、新增任意IP的連線用戶
如果是%就是不指定IP
CREATE USER 'username'@'%' IDENTIFIED BY 'mypassword';
GRANT ALL PRIVILEGES ON *.* TO 'username'@'%';
新增完成之後請執行:
FLUSH PRIVILEGES;

可以確認一下使用者有沒有新增成功,也可以查詢使用者權限:
SHOW GRANTS FOR username@ip_address;

// 查詢現在使用者的權限
SHOW GRANTS;
SHOW GRANTS FOR CURRENT_USER;
SHOW GRANTS FOR CURRENT_USER();

步驟到此就完成了!試著用剛剛的帳密登入mysql囉


DbVisualizer連線步驟

1.設定專案名稱

2. 資料庫類型

3. 填寫資料庫位址、port number、使用者帳密

參考資料:
https://docs.bitnami.com/virtual-machine/apps/reviewboard/administration/connect-remotely/

其他:
DbVisualizer 10.0.21 [安裝]
phpmyadmin的替代方案 https://alternative.me/phpmyadmin

2019年7月28日 星期日

Laravel 5.8入門教學【第一篇】路由(Routing)

基礎路由

最基礎的Laravel路由(Routing)由一個URI和閉包(Closure)組成,是一個簡單且容易理解的定義路由方法:
Route::get('foo', function () {
    return 'Hello World';
});

預設的路由檔案
所有Laravel路由都在路由檔案內定義,並存放在在目錄 routes,框架會自動被載入這些檔案,檔案routes/web.php定義web層的路由,這些路由被歸類到web中介軟體(middleware)提供session state和CSRF protection;而在routes/api.php是stateless,屬於api這個middleware。

在大部分的應用裡,你會從檔案routes/web.php開始定義路由,檔案routes/web.php內的路由將可以透過定義的URL在瀏覽器開啟,例如:你可以在瀏覽器存取下列的路由http://your-app.test/user
Route::get('/user', 'UserController@index');
檔案routes/api.php內的路由在RouteServiceProvider被設定成一個路由群組(Route group),在這個組裡,所有的路由將自動補上前綴(prefix)/api,不需要手動在每個路由設定,可以在RouteServiceProvider修改前綴,或定義其他路由組。
※路徑:./app/Providers/RouteServiceProvider.php

提供的路由方法(Available Router Methods)
提供你註冊路由來回應任何HTTP的動作(verbs):
Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);
有時你可能需要註冊一個可以回應多種HTTP動作的路由,你可以使用match,或你也可以透過any來註冊一個回應所有HTTP動作的路由。
Route::match(['get', 'post'], '/', function () {
    //
});

Route::any('/', function () {
    //
});

CSRF保護機制
任何HTML的表單只要是指向POSTPUTDELETE的路由,都需要一個CSRP的欄位,否則,請求將被回拒,你可以在CSRF documentation了解更多關於CSRF protection。
<form method="POST" action="/profile">
    @csrf
    ...
</form>

重新導向路由(Redirect Routes)
如果要定義一個路由會重新導向到其他URI,你可以使用Route::redirect,這個方法提供一個方便的捷徑,讓你不需要定義完整的路由或控制器(controller)來實現簡單的重新導向:
Route::redirect('/here', '/there');
預設情況,Route::redirect會回傳302的HTTP狀態碼(HTTP status code),你可以用第三個可選參數來自訂狀態碼:
Route::redirect('/here', '/there', 301);
你也可以用Route::permanentRedirect來回傳301的狀態碼:
Route::permanentRedirect('/here', '/there');

視圖路由(View Routes)
如果你的路由僅需回傳一個視圖(view),你可以使用Route::view,就像是redirect,這個方法提供一個簡單的捷徑免於定義完整的路由和控制器,view這個方法可以傳入URI當作第一個參數,用第二個參數為此路由命名。此外,第三個非必選的參數可以傳入一個陣列的資料給視圖使用。
Route::view('/welcome', 'welcome');

Route::view('/welcome', 'welcome', ['name' => 'Taylor']);


路由參數

必填的參數(Required Parameters)
有時你需要擷取URI的一部份,例如:你需要從URL獲得使用者的ID,你可以這樣定義路由參數:
Route::get('user/{id}', function ($id) {
    return 'User '.$id;
});
你可以在路由中視需求定義多個路由參數:
Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});
路由參數必須被{}包起來,名稱為字母組成並不能有-,可以使用底線(_)來取代-,路由參數將會按照順序傳入路由的回調(callbacks)/控制器(controllers),因此取名不重要可以自行定義。

選填的參數(Optional Parameters)
偶爾你需要一個特定的路由參數,但希望這個參數不是一個必填的欄位,你可以放置?的標記在參數名稱後,最後請確定你為路由參數設定了一個相應的預設值。
Route::get('user/{name?}', function ($name = null) {
    return $name;
});

Route::get('user/{name?}', function ($name = 'John') {
    return $name;
});

關係表示式限制
你可以使用where對路由參數的模式賦予限制,方法where傳入參數的名稱和一個定義限制為何的關係表示式(Regular Expression):
Route::get('user/{name}', function ($name) {
    //
})->where('name', '[A-Za-z]+');

Route::get('user/{id}', function ($id) {
    //
})->where('id', '[0-9]+');

Route::get('user/{id}/{name}', function ($id, $name) {
    //
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);

全域限制(Global Constraints)
如果你希望路由參數可以一直遵守給定的限制,你可以使用pattern,你可以在檔案RouteServiceProviderboot方法裡定義這些模式:
/**
 * Define your route model bindings, pattern filters, etc.
 *
 * @return void
 */
public function boot()
{
    Route::pattern('id', '[0-9]+');

    parent::boot();
}
一旦這些模式被定義,將會自動應用到所有使用這個參數名稱的路由:
Route::get('user/{id}', function ($id) {
    // Only executed if {id} is numeric...
});

編碼正斜杠(Encoded Forward Slashes)
Laravel路由允許任何字元,除了/,你必須使用where設定關係表達式(regular expression),明確地開放/納入佔位符(placeholder)的一部份。
Route::get('search/{search}', function ($search) {
    return $search;
})->where('search', '.*');
[!] 編碼正斜杠只對路由的最後一個部份有效


命名路由

命名路由(Named routes)讓產生URLs或重新導向到特定的路由變得方便,你可以透過name連結到路由的定義來指定一個名稱給路由。
Route::get('user/profile', function () {
    //
})->name('profile');
你也可以為控制器動作(controller actions)指定路由名稱:
Route::get('user/profile', 'UserProfileController@show')->name('profile');

產生命名路由的URLs(Generation URLs To Named Routes)
一旦你為路由設定名稱,你可以用這個路由名稱產生URL或使用函式route重新導向:
// Generating URLs...
$url = route('profile');

// Generating Redirects...
return redirect()->route('profile');
若這個命名路由有參數(parameters),你可以在函式route作為第二個傳遞參數,參數將會自動在正確的位置插入URL。
Route::get('user/{id}/profile', function ($id) {
    //
})->name('profile');

$url = route('profile', ['id' => 1]);

檢查目前的路由(Inspecting The Current Route)
如果你需要確認目前的請求要引導的命名路由,你可以在Route的實例中使用named方法,例如:你可以在路由中介軟體確認路由名稱。
/**
 * Handle an incoming request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  \Closure  $next
 * @return mixed
 */
public function handle($request, Closure $next)
{
    if ($request->route()->named('profile')) {
        //
    }

    return $next($request);
}


路由群組(Route Groups)

路由組可以允許大量的路由共用路由的屬性,像是中介軟體〈middleware〉或命名空間〈namespaces〉,而非在路由中個別獨立設定。共用的屬性以陣列格式作為Route :: group方法的第一個參數。

中繼軟體(Middleware)
你可以在路由群組的定義之前,用middleware將群組的所有路由設定中繼軟體,中間軟體將按照它們列出的順序執行:
Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // Uses first & second Middleware
    });

    Route::get('user/profile', function () {
        // Uses first & second Middleware
    });
});


取得現在的路由

你可以使用currentcurrentRouteNamecurrentRouteAction這些Route方法,取得關於路由的資訊來處理進來的請求。
$route = Route::current();

$name = Route::currentRouteName();

$action = Route::currentRouteAction();

文章來源:https://laravel.com/docs/5.8/routing

2019年7月26日 星期五

Ubuntu18+PHP7.2 安裝Laravel 5.8


安裝composer

Composer是PHP套件管理工具,請參考『[Symfony] ubuntu18安裝symfony 4.2教學』第二部分提供兩種安裝方式。[官方安裝說明]


建立專案

提供兩個方式來建立一個專案,命名為 blog
方法一、
composer create-project --prefer-dist laravel/laravel blog
指令執行後會自動 composer install 讀取 composer.json 安裝所需及相依的套件。

方法二、
composer global require "laravel/installer"
2-A. 把 $HOME/.composer/vendor/bin 路徑放置於環境變數 $PATH 裡。
export PATH="~/.config/composer/vendor/bin:$PATH"
laravel new blog
2-B. 如果沒有設定環境變數,可能會有錯誤訊息laravel: command not found,也可以用決定路徑來執行。
# "沒有"設定環境變數
.config/composer/vendor/bin/laravel new blog


兩個方法二擇一,成功後目錄應該會多一個 blog/ 的專案目錄,進入專案並執行:
php artisan key:generate

# Example oupout
Application key set successfully.
如果沒有執行這一段,執行專案可能會跳出錯誤訊息 No application encryption key has been specified.


執行專案

開發時可以執行下面的指令讓專案運作,預設路徑為http://localhost:8000
php artisan serve
也可以指定 HOSTPORT
php artisan serve --port=8080 --host=192.168.88.139


打開瀏覽器看到下面的畫面,就可以進入開發囉!



查詢Laravel安裝版本
php artisan -V
Laravel Framework 5.8.29


參考資料:
https://laravel.com/docs/5.8

2019年6月16日 星期日

[Vue.js] v-for設定key的作用與影響


v-for迭代陣列或物件時需要設定key,是為了避免重複產生DOM元素而浪費資源,因此將key視為一個辨識的依據,所有的key必須保持唯一。因此如果key值不小心重複,console甚至會出現Duplicate keys detected: ... This may cause an update error. 這樣的警示訊息。

為了顯示key的作用和影響,用下面的例子來看,程式內容大意是根據menu的內容產生多個product-box,兩個唯一的差別是前著用indexkey,後者則是用id當作key,此外多加一個button來更動資料的內容。
<template>
  <div>
    <product-box v-for="item,index in menu" :key="index" :value="item.id">
      {{ item.name }}
    </product-box>

    <product-box v-for="item in menu" :key="item.id" :value="item.id">
      {{ item.name }}
    </product-box>

    <button @click="addFirstElement">Add</button>
  </div>
</template>

<script>
import ProductBox from './ProductBox'
export default {
  data () {
    return {
      menu: [
        {id: 'A001', name: 'milk tea'},
        {id: 'A002', name: 'juice'}
      ]
    }
  },
  methods: {
    addFirstElement: function() {
      let first_elm = [{id: 'A000', name: 'coffee'}];
      this.menu = first_elm.concat(this.menu);
    }
  },
  components: {
    ProductBox
  }
}
</script>

ProductBox.vue
<template>
  <div>
    {{ dispay_text }} <slot></slot>
  </div>
</template>

<script>
  
  export default {
    data: function() {
      return {
        dispay_text: ''
      }
    },
    props: {
      'value': String
    },
    created: function() {
      this.dispay_text = this.value;
    }
  }
</script>

初始的情況下:
A001 milk tea
A002 juice

接著,點button在開頭插入一個新的資料,預想的顯示如下,如果用id當作key的話也是產生一樣的結果。
// key=id

A000 coffee
A001 milk tea
A002 juice

這時因為用index當作key,所以原本index 1和2因為沒有偵測到變化並不會重新產生,也就是不會重新經歷created,所以出現所謂的update error,結果會變成:
// key=index

A001 coffee
A002 milk tea
A002 juice

雖然用index當作key很方便,但並非所有的情況都適合使用index當作key。

2019年6月10日 星期一

[Vue.js] 共用Domain Name 部屬多個vue專案


接續『[Vue.js] vue-router設定history mode移除網址的#』的內容,可以在web server裡部屬vue的專案,如果要共用同一個domain name,用不同的子目錄(subdirectory)來區分時,只要根據下面的步驟稍微調整你的專案,既不會影響原本的開發模式,也能在正式區讓多個專案同時運行。

假設兩個專案project-Aproject-B分別為:
1. project-A: /var/www/project_a
http://my.project
2. project-B: /var/www/project_b
http://my.project/group/

所有project-B的URL,都會多一個 /group/ 的子目錄;反之,帶有 /group/ 的網址也會自動與project-B的路由進行比對。
project-A的設定方式不再說明,可以參考『[Vue.js] vue-router設定history mode移除網址的#』,下面講解project-B的設定方式。

Step1. vue-router
設定base
// src/router/index.js
 export default new Router({
   base: '/group/',
   mode: 'history',
   ...
 }

Step2. build參數
設定assetsPublicPath,請注意有兩個不同的key,須設定的是buildassetsPublicPath
// config/index.js
 module.exports = {
   dev: {
       ...
   },
   build: {
     assetsPublicPath: '/group',
     ...
   }
 }

Step3. build
npm run build
取得目錄dist/底下的檔案,搬到目的的資料夾/var/www/project_b

Step4. 建立.htaccess
將檔案放到/var/www/project_b,也就是project-B相同的目錄。
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . group/index.html [L]
</IfModule>

Step5. Apache Config
將檔案放到/var/www/html/project_b,也就是project-B相同的目錄。
<VirtualHost *:443>
  ServerName my.project
  DocumentRoot /var/www/project_a
  DirectoryIndex index.html
  <Directory /var/www//project_a>
    AllowOverride all
  </Directory>
  
  Alias /group /var/www/project_b
  <Directory /var/www/project_b>
    AllowOverride All
  </Directory>
</VirtualHost>

sudo systemctl restart apache2
請記得重啟apache2,設定就完成囉!

2019年5月24日 星期五

[Vue.js] 如何在component自訂v-model


v-model 通常用在HTML的 input,根據輸入的內容會跟著改變這個參數的內容,建立專案時會將一些基礎元件(如:input)定義成一個component,方便在各個頁面引入使用。

其實 v-model 是結合了 propsemit ,可以先參考『[Vue.js] 父子元件的雙向溝通,簡單的props和emit使用範例』,比較能理解下面的內容。

舉例常見的用法來看:
<input v-model="username" />
v-model其實綁定了名為 valuepropsinputemit事件,因此所謂的 v-model 就是一個父子元件的雙向溝通,可以拆寫成:
<input :value="username"
       @input="(value)=>(username=value)" />
跟使用 v-model 達到的效果相同。


定義元件的v-model和使用

1. 定義子元件
input.vue
<template>
    <input ref="input"
        class="my-input"
        type="text"
        :placeholder="placeholder"
        :value="value"
        @input="updateValue" />
</template>

<script>
export default {
  props: {
    value: String,
    placeholder: {
      type: String
     }
  },
  methods: {
    updateValue: function() {
      this.$emit('input', this.$refs.input.value);
    }
  }
}
</script>

<style scoped>
.my-input {
    color: #3b3b3b;
    font-size: 1rem;
    line-height: 1.5;
    border: 1px solid #003377;
    padding: 4px;
}
</style>

2. 在父元件使用
<template>
  <div style="text-align: center">
    <h1>coustom v-model</h1>
    <my-input v-model="username" />
    <p>My name is {{ username }}</p>

    <h1>coustom v-model with placeholder</h1>
    <my-input v-model="email" placeholder="input your email" />
    <p>My email is {{ email }}</p>
  </div>
</template>

<script>
import MyInput from './input.vue'
export default {
  data: function() {
    return {
      username: "",
      eamil: ""
    }
  },
  components: {
    MyInput
  }
}
</script>

頁面預覽:


上述只有把 placehodler 加到 props 裡,其他像是typedisabledreadonly等等常用的attr可以依照這個方式加進你的元件裡。另外除了 inputcheckboxradio這些表格常用的也可以一一寫成元件來使用。


自訂props的預設值和監聽事件

預設情況下,v-model 是綁定的 propsvalueinputemit事件,若要自訂名稱請參考:
export default {
  prop: ['keyword'],
  model: {
    prop: 'keyword',
    event: 'transfer'
  }
  methods: {
    updateValue: function() {
      this.$emit('transfer', this.$refs.input.value)
    }
  }
}
在上面的例子裡,propskeyword,綁定的事件則是trasfer,也就是說這邊的 v-model 應該理解成:
<input :keyword="username"
       @transfer="(value)=>(username=value)" />


參考資料:
https://scotch.io/tutorials/add-v-model-support-to-custom-vuejs-component


2019年5月19日 星期日

[Vue.js] 預覽上傳照片的原始碼分享(single/multiple file)


選擇照片上傳前如何實作預覽的功能,下面拆解成3個部分說明:
1. 上傳檔案
首先需要一個上傳檔案的HTML
<input type="file" accept="image/*" @change="previewImage">
根據上面的程式碼,我們需要一個 previewImage 的function來處理選擇的圖片檔。

2. 實作previewImage
接下來是js的程式部分,我們定義了兩個變數 previewimage,前者是存放預覽圖片,後者則是實際檔案。
new Vue({
  data: function() {
    return {
      preview: null,
      image: null
    };
  },
  methods: {
    previewImage: function(event) {
      var input = event.target;
      if (input.files) {
        var reader = new FileReader();
        reader.onload = (e) => {
          this.preview = e.target.result;
        }
        this.image=input.files[0];
        reader.readAsDataURL(input.files[0]);
      }
    }
  }
});

3. 顯示預覽
previewImage 儲存的結果顯示出來
<template v-if="preview">
  <img :src="preview" />
  <p>file name: {{ image.name }}</p>
  <p>size: {{ image.size/1024 }}KB</p>
</template>

執行上面的程式碼就可以達到預覽的效果,最後只要透過API將 image 這個物件透過API丟到後台,就能完成檔案的上傳。(用formData檔案上傳)


更進階的部分,只要在 input tag中加上multiple 就能一次選擇多個檔案。
<input type="file" accept="image/*" multiple>
除了預覽單一檔案,也能支援選擇多個檔案並預覽,有需要的話完整的原始碼已經放到codepen上,歡迎參考。



[Vue.js] 2.6版本開始使用v-slot取代slot


升級到2.6.0+後,slot的功能有一些更動,以v-slot取代slot,雖然在2.0+仍然可以使用,但確定會在Vue3之後捨棄這個用法,既然如此遲早要學會使用v-slot。

基礎使用

<!-- NavLink.vue -->
<a href="#">
  <slot>Link</slot>
</a>

你可以在模板裡面使用:
<nav-link></nav-link>
會使用 <slot> 內的內容當作預設值顯示。

如果你想要客製內容:
<nav-link>Home</nav-link>
則會取代原本的內容,得到這樣的結果。
<a href="#">
  Home
</a>

未指定 name 這個屬性的情況下,預設的名稱都是 default


即將淘汰的slot

1. 命名方式
<!-- NavLink.vue -->
<a href="#">
  <slot name="main">Link</slot>
</a>

2. 使用方式
可以用在任意的元件上,寫入內容:
<nav-link>
  <template slot="main">Home</template>
</nav-link>

<nav-link>
  <h1 slot="main">Home</h1>
</nav-link>


v-slot特性介紹

1. 命名方式
方式不變

2. 使用方式
僅能在 <template> 元件上,寫入內容:
<nav-link>
  <template v-slot:main>Home</template>
</nav-link>

3. 可以縮寫
就像是 v-on: 縮寫成 @v-bind: 縮寫成 :v-slot: 可以簡寫成 #,舉例來說,v-slot:header 等同於 #header
<nav-link>
  <template #main>Home</template>
</nav-link>

除了不能在任意元件是使用 slot ,基本上更動不大,而且更方便好用。不只是 slot 即將走入歷史, slot-scope 是另一個即將被淘汰的用法,想了解更多資訊可以參考官方文件:
https://vuejs.org/v2/guide/components-slots.html

2019年5月16日 星期四

[Vue.js] 升級vue-cli3建立Vue專案


之前安裝vue-cli2來建立vue專案 --『[Vue.js] 在ubuntu安裝 Vue.js』,有需要可以參考。

開始安裝vue-cli 3
npm install --global @vue/cli

完成安裝後可以用指令檢查版本
vue --version
// or
vue -V

vue cli3多了一個 create 功能來建專案:
vue create [APP_NAME]
你可以使用預設選項或根據需求選擇,結束後會自動幫你執行 npm install

整個專案目錄精簡了不少,少了 config/build/ 兩個目錄,然後 static/ 改為 public/
執行下面指令就可以進入開發囉
npm run serve
vue cli不再使用dev當作開發環境的指令,如果你習慣用dev,可以到 package.json 修改script。



因為移除了 config/ ,因此『[Vue.js] 開發用和正式環境的參數設定方式(.env)』的方式勢必就要調整了。
在專案的根目錄新增兩個檔案 .env.env.production,分別用在開發和正式環境。所有的參數名稱必須加上前綴 VUE_APP_ ,只有名稱 VUE_APP_* 的參數,才能在vue專案裡使用。
1. .env
VUE_APP_NODE_ENV: '"development"'
VUE_APP_ROOT_API: '"http://localhost/api"'
2. .env.production
VUE_APP_NODE_ENV: '"production"'
VUE_APP_ROOT_API: '"https://example.com/api"'

使用方式和之前相同
export default {
  mounted() {
    console.log(process.env.VUE_APP_ROOT_API);
  }
}

關於環境變量,參考下面連結了解更多。
https://cli.vuejs.org/guide/mode-and-env.html#environment-variables