2019年12月26日 星期四

[Vue.js] 跨元件的資料傳遞 Eventbus 簡易使用範例


Eventbus可以達成跨元件的資料傳遞,這個範例只是作為Eventbus效果的示範,依據實際的應用時,不一定使用eventbus是最合適的,可以考量使用props/emit父子元件的傳遞方式,或是vuex來需要共用的資源。

先參考一下整體的檔案結構:
├── App.vue
├── components
│   ├── Button.vue
│   └── DisplayBoard.vue
├── libs
│   └── eventbus.js
└── main.js

eventbus.js
import Vue from 'vue';
export const EventBus = new Vue();

MyButton.js
傳遞eventbus的元件,當按下按鈕時,會將目前的值傳出去,以$emit發出一個 "add-count" 的事件。
<template>
  <button @click="onClickCountAdd">Click</button>
</template>

<script>
// Import the EventBus we just created.
import { EventBus } from '../libs/eventbus.js';

export default {
  data() {
    return {
      clickCount: 0,
    }
  },
  methods: {
    onClickCountAdd() {
      this.clickCount++;
      // Send the event on a channel (add-count) with a payload (the click count.)
      EventBus.$emit('add-count', this.clickCount);
    }
  }
}
</script>

DisplayBoard.js
註冊eventbus的元件,$on是註冊;$off是取消,兩者必須成雙出現,可以避免產生重複綁定。display-board註冊 "add-count" 的事件,後面的參數是時間接收到事件時,負責處理的method。
<template>
  <div v-text="displayCount" />
</template>

<script>
// Import the EventBus we just created.
import { EventBus } from '../libs/eventbus.js';

export default {
  data() {
    return {
      displayCount: 0,
    }
  },
  created() {
    this.handleClick = this.handleClick.bind(this);
    // Listen to the event.
    EventBus.$on('add-count', this.handleClick);
  },
  destroyed() {
    // Stop listening.
    EventBus.$off('add-count', this.handleClick);
  },
  methods: {
    handleClick(clickCount) {
      this.displayCount = clickCount;
    }
  }
}
</script>

App.js
<template>
  <div>
    <my-button />
    <display-board />
  </div>
</template>

<script>
import MyButton from './components/MyButton'
import DisplayBoard from './components/DisplayBoard'

export default {
  components: {
    MyButton,
    DisplayBoard,
  }
}
</script>

最終成果,隨著my-button內按鍵點擊,display-board需呈現目前的點擊數。



2019年12月25日 星期三

Symfony4.3 基本應用【第四篇】權限與安全 Authentication & Security



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

Symfony的安全系統非常強大,但是設定方法常常讓人困擾,別擔心!在這篇文章,將一步一步的教你如何設定應用程式的安全系統:
  1. 安裝套件
  2. 建立User Class
  3. 認證與防火牆
  4. 拒絕存取(權限)
  5. 獲取目前的User物件

還有幾個重要的主題也會在後面討論。


1) 安裝

在應用程式裡使用安全系統的功能前,先透過
Symfony Flex執行下面的指令安裝:
composer require symfony/security-bundle


2a) 建立你的User Class

無論你將如何認證(authenticate)(e.g. 登入表單或是API tokens)或者你的使用者資料存放在哪裡(資料庫、單一登入入口),你都必須先建立一個"User" class,最簡單的方式是使用MakerBundle

我們先假設你會把使用者資料用Doctrine存在資料庫裡:
php bin/console make:user

The name of the security user class (e.g. User) [User]:
> User

Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes

Enter a property name that will be the unique "display" name for the user (e.g.
email, username, uuid [email]
> email

Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes

created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml
就是這樣!指令會詢問幾個問題來了解你的需求並產生User class,最重要的是檔案 User.phpUser class唯一的規定就是必須實作(implemnt) UserInterface,如果有需要你也可以任意加上任何其他欄位或是程式邏輯,若你的 User class是一個實例(entity)(像是上述的例子),你可以使用make:entity command來加上更多欄位,並且要記得對新的實例執行移植(migration)。
php bin/console make:migration
php bin/console doctrine:migrations:migrate


2b) The "User Provider"

除了 User class之外,你還需要一個"User provider":一個class,能夠幫助你從session重新載入使用者資訊,以及一些選擇性的功能像是remember meimpersonation

幸運地是,指令 make:user 已經幫你在 security.yaml 以關鍵字(key) providers 設定好了。

如果你的 User class是一個實體,你不需要額外做任何事,但如果你的class不是實體,指令 make:user 會需要產生一個需要由你完成的class UserProvider ,了解更多關於user providers的資訊:User Providers


2c) 密碼編碼(Encoding Passwords)

並不是所有應用程式的"user"都需要密碼,如果用戶需要設定密碼,你可以在 security.yaml 決定如何將密碼進行編碼,make:user 指令會重新幫你設定:
# config/packages/security.yaml
security:
    # ...

    encoders:
        # use your user class name here
        App\Entity\User:
            # Use native password encoder
            # This value auto-selects the best possible hashing algorithm.
            algorithm: auto
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <encoder class="App\Entity\User"
            algorithm="bcrypt"
            cost="12"/>

        <!-- ... -->
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    // ...

    'encoders' => [
        'App\Entity\User' => [
            'algorithm' => 'bcrypt',
            'cost' => 12,
        ]
    ],

    // ...
]);
現在Symfony知道你想要如何對密碼編碼,你可以在儲存使用者到資料庫之前,使用 UserPasswordEncoderInterface 這個服務。

例如,透過DoctrineFixturesBundle,你可以在資料庫建立一些用戶虛擬資料:
php bin/console make:fixtures

The class name of the fixtures to create (e.g. AppFixtures):
> UserFixtures
使用這個服務對密碼編碼:
// src/DataFixtures/UserFixtures.php

+ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
// ...

class UserFixtures extends Fixture
{
    private $passwordEncoder;

    public function __construct(UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->passwordEncoder = $passwordEncoder;
    }

    public function load(ObjectManager $manager)
    {
        $user = new User();
        // ...

        $user->setPassword($this->passwordEncoder->encodePassword(
            $user,
            'the_new_password'
        ));

        // ...
    }
}
你也可以執行下面指令手動編碼密碼:
php bin/console security:encode-password


3a) 認證與防火牆(Authentication & Firewalls)

安全系統的設定都在 config/packages/security.yaml,最重要的部分是 firewalls
# config/packages/security.yaml
security:
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <firewall name="dev"
            pattern="^/(_(profiler|wdt)|css|images|js)/"
            security="false"/>

        <firewall name="main">
            <anonymous/>
        </firewall>
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    'firewalls' => [
        'dev' => [
            'pattern'   => '^/(_(profiler|wdt)|css|images|js)/',
            'security'  => false,
        ),
        'main' => [
            'anonymous' => null,
        ],
    ],
]);
認證系統的"firewall":底下的設定定義用戶如何進行認證(e.g. 登入表單、API token等等)。

有一個防火牆對每一個請求都啟用效果:Symfony使用關鍵字(key)
pattern 尋找第一個符合(你也可以match by host or other things)。dev 防火牆是虛設的:它只會確保你不會不小心阻擋Symfony開發工具 - 前綴為 /_profiler/_wdt的URLs 。

所有真正的URLs會由main防火牆處理(如果沒有 pattern key代表適用所有的URLs),但是這並不表示所有的URL都需要認證。多虧了 anonymous key,這個防火牆可以被匿名訪問。

事實上,如果你馬上回到首頁,你"將會"發現你被"認證(authenticated)"為 anon. ,別被下面Authenticated的"Yes"騙了,防火牆驗證不出你的身分,所以你是匿名訪客。



你將會學到如何拒絕某些URLs或控制器的存取。

如果你看不到底下工具列,請安裝profiler
composer require --dev symfony/profiler-pack

現在我們已經了解防火牆,下一步是建立一個用戶認證的方法!


3b) 對用戶認證

Symfony認證有一點神奇,因為你不需要建立路由或是控制器來處理登入,你只要啟動認證提供者(authentication provider):某些程式在控制器之前會自動被呼叫。

Symfony有幾個內建的認證提供者(built-in authentication providers),如果使用情境與其中一個正好相符,那就太棒了!但是在大部分的情況,包含一個登入表單,我們建議建立一個Guard Authenticator: 一個可以讓你控制每一個部分認證步驟的class(請看下個章節)。

如果應用程式透過第三方服務像是Google、Facebook或是Twitter(社群登入)讓使用者登入,請查看HWIOAuthBundle


Guard Authenticators
Guard authenticator是提供認證步驟完整的控制權的class,有很多不同方式建立認證,所以這邊有一些常見的情境:
  • 如何建立登入表單
  • 客製化認證系統(API Token)

如果要瞭解更詳細的認證和其運作方式,請看Custom Authentication System with Guard (API Token Example)


4) 拒絕存取、角色以及其他認證

使用者現在可以透過登入表單登入你的應用程式,太棒了!現在你需要學習如何拒絕訪問,以及操作User物件,這被稱為authorization,它的工作是決定用戶是否可以使用某些資源(URL、model、呼叫method等等...)。

認證分為兩個部分:
  1. 使用者在登入後獲得特定的角色列表(e.g. ROLE_ADMIN )。
  2. 你加入特定的代碼,使得資源(e.g. URL、controller)需要特定的"屬性(attribute)"(大多像是一個 ROLE_ADMIN 的角色)才能夠存取。

角色(Roles)
當用戶登入,Symfony呼叫 User 內的方法 getRoles() 來決定此用戶的角色,在先前產生的 User ,角色為一個存放在資料庫的陣列,且所有的使用者至少會有一個角色:ROLE_USER
// src/Entity/User.php
// ...

/**
 * @ORM\Column(type="json")
 */
private $roles = [];

public function getRoles(): array
{
    $roles = $this->roles;
    // guarantee every user at least has ROLE_USER
    $roles[] = 'ROLE_USER';

    return array_unique($roles);
}
這是一個很常見的狀況,但你可以根據使用者需要什麼角色來決定怎麼做,這邊有一些準則可以參考:
  • 每一個角色必須ROLE_ 作為開頭(否則,無法如預期運作)
  • 除了上述規則,角色只是一段字串,你可以發明所需的內容(e.g. ROLE_PRODUCT_ADMIN)

你將使用這些角色來授予特定區域的存取權限,你還可以使用角色層次性結構(role hierarchy),使得擁有某些角色會自動獲得其他角色。

拒絕訪問的代碼設定(Add Code to Deny Access)
有兩種方法來拒絕訪問:
  1. access_control in security.yaml讓你可以確保URL格式(e.g. /admin/*),簡單但是彈性較少。
  2. in your controller (or other code)

安全的URL格式 (access_control)
保護應用程式最簡單的方法之一就是在 security.yaml 設定完整的URL格式,例如,所有 ROLE_ADMIN 的URL路徑都以 /admin 開頭:
# config/packages/security.yaml
security:
    # ...

    firewalls:
        # ...
        main:
            # ...

    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: '^/admin', roles: ROLE_ADMIN }

        # the 'path' value can be any valid regular expression
        # (this one will match URLs like /api/post/7298 and /api/comment/528491)
        - { path: ^/api/(post|comment)/\d+$, roles: ROLE_USER }
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <firewall name="main">
            <!-- ... -->
        </firewall>

        <!-- require ROLE_ADMIN for /admin* -->
        <rule path="^/admin" role="ROLE_ADMIN"/>

        <!-- the 'path' value can be any valid regular expression
             (this one will match URLs like /api/post/7298 and /api/comment/528491) -->
        <rule path="^/api/(post|comment)/\d+$" role="ROLE_USER"/>
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    // ...

    'firewalls' => [
        // ...
        'main' => [
            // ...
        ],
    ],
    'access_control' => [
        // require ROLE_ADMIN for /admin*
        ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],

        // the 'path' value can be any valid regular expression
        // (this one will match URLs like /api/post/7298 and /api/comment/528491)
        ['path' => '^/api/(post|comment)/\d+$', 'roles' => 'ROLE_USER'],
    ],
]);
只要你需要,可以設定多個URL格式,每一個URL格式都是正規表達式(regular expression),但是每一個請求只會和其中一個配對:Symfony會從設定表的開頭依序比對,直到找到第一個符合的就停止:
# config/packages/security.yaml
security:
    # ...

    access_control:
        # matches /admin/users/*
        - { path: '^/admin/users', roles: ROLE_SUPER_ADMIN }

        # matches /admin/* except for anything matching the above rule
        - { path: '^/admin', roles: ROLE_ADMIN }
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd">

    <config>
        <!-- ... -->

        <rule path="^/admin/users" role="ROLE_SUPER_ADMIN"/>
        <rule path="^/admin" role="ROLE_ADMIN"/>
    </config>
</srv:container>
// config/packages/security.php
$container->loadFromExtension('security', [
    // ...

    'access_control' => [
        ['path' => '^/admin/users', 'roles' => 'ROLE_SUPER_ADMIN'],
        ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
    ],
]);
在路徑之前加上
^ 表示只有URLs以此模式開頭才算是符合,例如:有個路徑
/admin (沒有加上 ^ )將會與 /admin/foo,甚至是 /foo/admin

每一個 access_control 也可以配對IP位址、主機名稱和HTTP methods,也可以用來將用戶重新導向到 https 版本的URL,更多請看How Does the Security access_control Work?

安全的控制器和其他程式碼 (Securing Controllers and other Code)
你可以在控制器裡拒絕存取:
// src/Controller/AdminController.php
// ...

public function adminDashboard()
{
    $this->denyAccessUnlessGranted('ROLE_ADMIN');

    // or add an optional message - seen by developers
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN');
}
就是這樣,如果不允許被存取,會拋出一個特殊的例外 AccessDeniedException ,且不會執行控制器內其他程式,接著有兩件事會發生:
  1. 如果使用者尚未登入,他們將會被要求登入。(例如導回登入頁面)
  2. 如果使用者已經登入,但是沒有 ROLE_ADMIN ,他們將會看到顯示403拒絕存取的頁面(此頁面也可以客製)
幸虧SensioFrameworkExtraBundle,你也可以使用annotations加強控制器的安全:
// src/Controller/AdminController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

/**
 * Require ROLE_ADMIN for *every* controller method in this class.
 *
 * @IsGranted("ROLE_ADMIN")
 */
class AdminController extends AbstractController
{
    /**
     * Require ROLE_ADMIN for only this controller method.
     *
     * @IsGranted("ROLE_ADMIN")
     */
    public function adminDashboard()
    {
        // ...
    }
}
若要知道更多資訊,請看FrameworkExtraBundle documentation

模板的存取控制
如果你想要確認現在的用戶是否有某個角色權限,你可以用一個既有的函式 is_granted(),可用於任何的Twig模板:
{% if is_granted('ROLE_ADMIN') %}
    <a href="...">Delete</a>
{% endif %}

Securing other Services
詳見How to Secure any Service or Method in your Application



資料翻譯自:
https://symfony.com/doc/4.3/security.html

系列文章:

2019年9月23日 星期一

[Vue.js] storybook安裝方式與環境建置

storybook是UI的開發環境,可以讓開發人員獨立建立元件,並在隔離的開發環境中互動式地展示元件。

storybook在主程式以外運作,因此不需要擔心應用程式特定的依賴關係或需求,用戶可以獨立開發UI元件。

環境
  • Ubuntu
  • node.js v10.15.1

套件版本:
  • Vue 2.6.10
  • Storybook 5.2.1

第一步、安裝storybook及相關套件

安裝 @storybook/vue 套件
npm install @storybook/vue --save-dev
安裝其他相依套件,包含:vuevue-loadervue-template-compiler@babel/corebabel-loaderbabel-preset-vue
npm install vue --save
npm install vue-loader vue-template-compiler @babel/core babel-loader babel-preset-vue --save-dev


第二步、新增npm script

在檔案package.json新增script:
{
  "scripts": {
    "storybook": "start-storybook"
  }
  ...
}


第三步、新增config

在檔案.storybook/config.js新增config:
import { configure } from '@storybook/vue';

configure(require.context('../src/components/stories', true, /\.stories\.js$/), module);
代表會載入所有在資料夾 ../src/components/stories 內的符合 *.stories.js 的檔案。

第四步、新增stories

新增一個stories Button.stories.js
// src/components/stories/Button.stories.js
import Vue from 'vue';
import MyButton from '../Button.vue';

export default { title: 'Button' };

export const withText = () => '<my-button>with text</my-button>';

export const withEmoji = () => '<my-button>😀 😎 👍 💯</my-button>';

export const asAComponent = () => ({
  components: { MyButton },
  template: '<my-button :rounded="true">rounded</my-button>'
});


第五步、執行

npm run storybook
打開瀏覽器確認




參考專案架構:
./
├── .storybook
│   └── config.js
├── babel.config.js
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   └── index.html
├── README.md
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   ├── Button.vue
    │   └── stories
    │       └── Button.stories.js
    └── main.js

資料來源:https://storybook.js.org/docs/guides/guide-vue/

2019年9月22日 星期日

Symfony4.3 基本應用【第三篇】主控台指令 Console Commands


Symfony透過bin/console提供許多指令(e.g. 常見的指令bin/console cache:clear),這些指令是由Console component建立,你可以使用它來建立新指令。


The Console: APP_ENV & APP_DEBUG

檔案.env裡的變數APP_ENV定義了主控台指令所運行的環境(environment),預設為 dev ,另外也會讀取 APP_DEBUG 的值來決定是否開啟或關閉"除錯(debug)"模式(預設 1 是開啟)。

若要在其他環境跑指令或是除錯模式,可以編輯 APP_ENVAPP_DEBUG 的值。


建立指令

指令定義在一個延展(extned)Command的class。例如,你可能想要有個指令可以建立使用者:
// src/Command/CreateUserCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CreateUserCommand extends Command
{
    // the name of the command (the part after "bin/console")
    protected static $defaultName = 'app:create-user';

    protected function configure()
    {
        // ...
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // ...
    }
}


設定指令

你可以選擇要不要定義敘述(description)、提示語和輸入選項及變數(input options and arguments):
// ...
protected function configure()
{
    $this
        // the short description shown while running "php bin/console list"
        ->setDescription('Creates a new user.')

        // the full command description shown when running the command with
        // the "--help" option
        ->setHelp('This command allows you to create a user...')
    ;
}
在指令的建構裡,最後會自動呼叫configure(),如果你的指令定義在自己的建構函數裡,先設定屬性並呼叫上層的建構函數(constructor),讓它的屬性可以在configure()裡被使用。
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;

class CreateUserCommand extends Command
{
    // ...

    public function __construct(bool $requirePassword = false)
    {
        // best practices recommend to call the parent constructor first and
        // then set your own properties. That wouldn't work in this case
        // because configure() needs the properties set in this constructor
        $this->requirePassword = $requirePassword;

        parent::__construct();
    }

    protected function configure()
    {
        $this
            // ...
            ->addArgument('password', $this->requirePassword ? InputArgument::REQUIRED : InputArgument::OPTIONAL, 'User password')
        ;
    }
}


註冊指令

Symfony指令必須註冊成為一個服務(serviece)並標記(tagged)上標籤console.command,如果你使用預設的service.yaml設定檔(default service.yaml configuration),它已經幫你把這些事處理好了,真是多虧了autoconfiguration


執行指令

設定並註冊指令之後,你可以在終端機裡執行:
php bin/console app:create-user
就像你預期的一樣,這個指令並不會執行任何事情,因為你根本還沒寫任何程式邏輯在內,請把這些寫到 execute() 裡面。


主控台輸出(Console Output)

execute()方法可以存取輸出端,將訊息呈現在主控台上。
// ...
protected function execute(InputInterface $input, OutputInterface $output)
{
    // outputs multiple lines to the console (adding "\n" at the end of each line)
    $output->writeln([
        'User Creator',
        '============',
        '',
    ]);

    // the value returned by someMethod() can be an iterator (https://secure.php.net/iterator)
    // that generates and returns the messages with the 'yield' PHP keyword
    $output->writeln($this->someMethod());

    // outputs a message followed by a "\n"
    $output->writeln('Whoa!');

    // outputs a message without adding a "\n" at the end of the line
    $output->write('You are about to ');
    $output->write('create a user.');
}
現在請試著執行指令:
php bin/console app:create-user
User Creator
============

Whoa!
You are about to create a user.


輸出區塊(Output Sections)

一般主控台的輸入可以分為多個獨立的區塊(section),稱為"output sections",當你需要清除或是覆蓋掉輸出的資訊,可以建立一個或是多個區塊。

section()會回傳一個ConsoleSectionOutput實例(instance),用來建立區塊:
class MyCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $section1 = $output->section();
        $section2 = $output->section();
        $section1->writeln('Hello');
        $section2->writeln('World!');
        // Output displays "Hello\nWorld!\n"

        // overwrite() replaces all the existing section contents with the given content
        $section1->overwrite('Goodbye');
        // Output now displays "Goodbye\nWorld!\n"

        // clear() deletes all the section contents...
        $section2->clear();
        // Output now displays "Goodbye\n"

        // ...but you can also delete a given number of lines
        // (this example deletes the last two lines of the section)
        $section1->clear(2);
        // Output is now completely empty!
    }
}
每個區塊的資訊會自動換行。

輸出區塊可以讓你以更進階的方式操控主控台輸出,像是呈現多個進度條(displaying multiple progress bars),能夠獨立地更新;以及加入多列(rows)到已經呈現的表格中。


主控台輸入(Console Input)

使用輸入選項或變數來傳遞訊息給指令:
use Symfony\Component\Console\Input\InputArgument;

// ...
protected function configure()
{
    $this
        // configure an argument
        ->addArgument('username', InputArgument::REQUIRED, 'The username of the user.')
        // ...
    ;
}

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    $output->writeln([
        'User Creator',
        '============',
        '',
    ]);

    // retrieve the argument value using getArgument()
    $output->writeln('Username: '.$input->getArgument('username'));
}
現在,你可以在指令裡傳入使用者名稱:
php bin/console app:create-user Wouter
User Creator
============

Username: Wouter
請看Console Input (Arguments & Options)了解更多關於輸入選項或變數的訊息。


從服務容器中取得服務(Getting Services from the Service Container)

為了要真正建立新的使用者,指令必須存取一些服務(services),因為指令已經註冊成服務,所以你可以使用一般的依賴注入(dependency injection),想像一下你打算存取一個服務 App\Service\UserManager
// ...
use App\Service\UserManager;
use Symfony\Component\Console\Command\Command;

class CreateUserCommand extends Command
{
    private $userManager;

    public function __construct(UserManager $userManager)
    {
        $this->userManager = $userManager;

        parent::__construct();
    }

    // ...

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // ...

        $this->userManager->create($input->getArgument('username'));

        $output->writeln('User successfully generated!');
    }
}


指令的生命週期

運行指令時有三個生命週期方法可以調用(invoked):
initialize()(optional)
這個方法會在interact()execute()之前執行,主要的目的是初始化在其他指令方法中使用的變數。

interact()(optional)
這個方法在initialize()之後、execute()之前執行,目的是確認某些選項或變數是否遺漏,以及互動式地詢問使用者那些值,這是最後一次確認遺漏的選項或變數,若經過這個步驟仍有缺少的選項或變數,會導致錯誤發生。

execute()(required)
這個方法在interact()initialize()之後執行,它包含所有指令程式邏輯。


測試指令

Symfony提供一些工具幫助你測試指令,最有用的是一個叫CommandTester的class,它使用特殊的輸入和輸出classes,無需實際控制台即可輕鬆進行測試:
// tests/Command/CreateUserCommandTest.php
namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class CreateUserCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = static::createKernel();
        $application = new Application($kernel);

        $command = $application->find('app:create-user');
        $commandTester = new CommandTester($command);
        $commandTester->execute([
            'command'  => $command->getName(),

            // pass arguments to the helper
            'username' => 'Wouter',

            // prefix the key with two dashes when passing options,
            // e.g: '--some-option' => 'option_value',
        ]);

        // the output of the command in the console
        $output = $commandTester->getDisplay();
        $this->assertContains('Username: Wouter', $output);

        // ...
    }
}
你可以透過ApplicationTester測試整個主控台應用程式。

當你在獨立的專案使用Console元件,請用Symfony\Component\Console\Application並且延展\PHPUnit\Framework\TestCase


紀錄指令錯誤(Logging Command Errors)

運行指令時若拋出任何例外(exception),Symfony會包含整個失敗的指令新增一筆紀錄。此外,Symfony註冊事件訂閱者(event subscriber)來監聽ConsoleEvents::TERMINATE event,以及每當指令並沒有以狀態0成功離開時,記錄這些訊息。


資料翻譯自:
https://symfony.com/doc/4.3/console.html

系列文章:


2019年9月21日 星期六

Symfony4.3 基本應用【第二篇】Doctrine ORM



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

Symfony並未提供元件來與資料庫互動,但它確實與第三方套件Doctrine有緊密的結合。

這篇文章主要關於使用Doctrine ORM,如果你比較喜歡使用資料庫的query指令,建議看"如何使用Doctrine DBAL"。

若你堅持使用Doctrine ODM套件將資料放在MongoDB,請參考文件"DoctrineMongoDBBundle"。


安裝 Doctrine

首先,透過ORMpack以及MakeBundle安裝Doctrine,以利於產生一些代碼:
composer require symfony/orm-pack
composer require --dev symfony/maker-bundle


設定資料庫

資料庫的連線資料會當作環境變數儲存,稱為 DATABASE_URL ,開發時,你可以找到檔案 .env 並自訂。
# .env (or override DATABASE_URL in .env.local to avoid committing your changes)

# customize this line!
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name"

# to use sqlite:
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
如果username、password、host或是database name含有被URI視為特殊的字元(像是+, @, $, #, /, :, *, !),你必須對它進行編碼。請參考RFC 3986所列出的保留字元或urlencode函式來對它們編碼,在這種情況下,你需要移除檔案config/packages/doctrine.yaml裡的前綴resolve:,以避免錯誤url: '%env(resolve:DATABASE_URL)%'

現在你的連線參數已經設定好,Doctrine可以為你建立 db_name 資料庫。
php bin/console doctrine:database:create
config/packages/doctrine.yaml有更多選項可以設定,包括你的 server_version (e.g. 假設你使用MySQL5.7),會影響到Dontrine如何操作。

Doctrine有很多指令,執行php bin/console list doctrine可以知道所有指令。


建立一個實體類 Creating an Entity Class

假設你一個需要顯示商品的應用程式,就算不考慮Doctrine或資料庫,你也知道需要一個 Product 物件來代表那些產品。

你可以使用指令 make:entity 建立這個class和需要的欄位,這個指令會詢問你一些問題 - 請這樣回答它:
php bin/console make:entity

Class name of the entity to create or update:
> Product

 to stop adding fields):
> name

Field type (enter ? to see all types) [string]:
> string

Field length [255]:
> 255

Can this field be null in the database (nullable) (yes/no) [no]:
> no

 to stop adding fields):
> price

Field type (enter ? to see all types) [string]:
> integer

Can this field be null in the database (nullable) (yes/no) [no]:
> no

 to stop adding fields):
>
(press enter again to finish)
New in version 1.3: 指令 make:entity 互動式的行為在MakerBundle 1.3開始導入。

Woh!現在你有一個新檔案src/Entity/Product.php
// src/Entity/Product.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\Column(type="integer")
     */
    private $price;

    public function getId()
    {
        return $this->id;
    }

    // ... getter and setter methods
}
你對價格是整數感到困惑嗎?別擔心,這只是一個範例,但是將價格當作整數(e.g. 100 = $1USD)可以避免四捨五入的問題。

如果使用SQLite資料庫,你會看到底下的錯訊息:PDOException: SQLSTATE[HY000]: General error: 1 Cannot add a NOT NULL column with default value NULL,請新增設定屬性nullable=true來修正這個問題。

MySQL5.6使用InnDB table有limit of 767 bytes for the index key prefix,255字元的字串欄位和utf8mb4編碼會壓縮這個限制,這代表任何類型stringunique=true必須將最大值設為190,否則你將看到錯誤訊息:"[PDOException] SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes"

這個類稱為"實體entity",待會兒你就可以儲存、從資料表 product 取得Product物件,每一個實體 Product 的屬性可以對應到一個資料表欄位,通常由註釋(annotations)完成:請看下面每一個屬性的註解@ORM\...


指令make:entity雖然可以讓事情變得簡單的工具,但新增移除欄位、新增移除方法或是更新設定都要自己寫。

Doctrine支援多樣的欄位種類,每種都有自己的選項,若要知道完整的清單,請看文件Doctrine's Mapping Types,如果你想要使用XML而非annotations,新增 type: xmldir:

請小心不要在資料表或是欄位名稱使用SQL的保留字(e.g. GROUP 或 USER),請看Doctrine Reserved SQL keywords documentation了解詳細如何跳脫這些,或者將資料表名稱改成@ORM\Table(name="groups"),或將欄位名稱設定name="group_name


Migrations: Creating the Database Tables/Schema

Product已經完全設定好,並且準備存到資料表product,如果你剛定義好這個class,資料庫實際上並沒有product這個資料表,如果要新增,你需要藉由剛剛已經安裝的DoctrineMigrationsBundle來完成:
php bin/console make:migration
如果成功,你將會看到下列的訊息像是:

SUCCESS!

Next: Review the new migration "src/Migrations/Version20180207231217.php" Then: Run the migration with php bin/console doctrine:migrations:migrate


如果你打開這個檔案,裡面包含更新資料庫的SQL,如果實行migrations執行這些SQL,指令:
php bin/console doctrine:migrations:migrate
與你的資料庫現況不衝突為原則,這個指令將執行所有migration檔案,在正式環境需要下指令來部屬,以確保正式環境的資料庫已經更新到最新版。


Migrations 和增加更多欄位

但如果你要在Product新增新的欄位,像是description,你可以編輯這個class、加入新的屬性。但是你也可以再次使用make:entity
php bin/console make:entity

Class name of the entity to create or update
> Product

 to stop adding fields):
> description

Field type (enter ? to see all types) [string]:
> text

Can this field be null in the database (nullable) (yes/no) [no]:
> no

 to stop adding fields):
>
(press enter again to finish)
多了新的屬性description,以及getDescription()setDescription()
// src/Entity/Product.php
// ...

class Product
{
    // ...

+     /**
+      * @ORM\Column(type="text")
+      */
+     private $description;

    // getDescription() & setDescription() were also added
}
此時新屬性仍不存在於資料表product,沒錯!產生新的migration吧
php bin/console make:migration
這次,產生的SQL看起來會像是這樣 :
ALTER TABLE product ADD description LONGTEXT NOT NULL
migration系統很聰明,它會自動比對所有實體在資料庫的現在的狀態,並產生需要的SQL來同步,如果先前的步驟,執行指令來觸發migrations:
php bin/console doctrine:migrations:migrate
這只會執行一個新的migration檔案,因為DoctrineMigrationBundle知道第一個migration已經在先前執行過,背後使用資料表 migration_versions 來追蹤這些紀錄。

每次修改你的schema,執行兩個指令來產生migration和實行migration,請確保commit這些migration檔案並且在部屬時執行。

如果你比較喜歡手動增加屬性,指令 make:entit 可以幫你產生getter和setter方法:
php bin/console make:entity --regenerate
如果你做了一些修正,並且想要重新產生所有的getter/setter方法,要加上--overwrite


將物件保存到資料庫 Persisting Objects to the Database

是時候把Product物件存到資料庫了!先建立一個新的控制器來試驗:
php bin/console make:controller ProductController
在控制器中,你可以建立新的Product物件,設定資料並保存。
// src/Controller/ProductController.php
namespace App\Controller;

// ...
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="create_product")
     */
    public function createProduct(): Response
    {
        // you can fetch the EntityManager via $this->getDoctrine()
        // or you can add an argument to the action: createProduct(EntityManagerInterface $entityManager)
        $entityManager = $this->getDoctrine()->getManager();

        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(1999);
        $product->setDescription('Ergonomic and stylish!');

        // tell Doctrine you want to (eventually) save the Product (no queries yet)
        $entityManager->persist($product);

        // actually executes the queries (i.e. the INSERT query)
        $entityManager->flush();

        return new Response('Saved new product with id '.$product->getId());
    }
}
試試看!


恭喜你!你剛剛已經建立了第一行資料到product,為了證明成功建立,你可以直接從資料庫取出:
php bin/console doctrine:query:sql 'SELECT * FROM product'

# on Windows systems not using Powershell, run this command instead:
# php bin/console doctrine:query:sql "SELECT * FROM product"
請仔細看一下剛剛的範例:
  • line 18 方法$this->getDoctrine()->getManager()取得Doctrine實體管理員(entity manager)物件,也是Dontrine中最重要的物件,負責儲存物件到資料庫及從資料庫取得物件。
  • line 20-23 這個部分,$product物件如同其他一般的PHP物件使用。
  • line 26 persist($product) 呼叫Doctrine"管理"" $product 物件,這並不會對資料庫實際執行任何指令。
  • line 29 當方法 flush() 被呼叫時,Doctrine查看所有管理的物件,並判斷是否需要保存到資料庫。在這個範例中,$product物件在這個資料庫中不存在,所以entity manager會執行 INSERT ,新增一筆紀錄到資料表 product

如果呼叫flush()失敗,會拋出錯誤訊息Doctrine\ORM\ORMException,請參閱Transactions and Concurrency

不論是新增或更新物件,流程都是相同的:Doctrine能聰明地判斷要執行INSERT或是UPDATE你的實體。


驗證物件

The Symfony validator重複利用Doctrine metadata實現一些基礎的驗證任務:
namespace App\Controller;

use App\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
// ...

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="create_product")
     */
    public function createProduct(ValidatorInterface $validator): Response
    {
        $product = new Product();
        // This will trigger an error: the column isn't nullable in the database
        $product->setName(null);
        // This will trigger a type mismatch error: an integer is expected
        $product->setPrice('1999');

        // ...

        $errors = $validator->validate($product);
        if (count($errors) > 0) {
            return new Response((string) $errors, 400);
        }

        // ...
    }
}
雖然 Product 並未定義任何明確的驗證設定(validation configuration),Symfony針對Dontrice對應設置進行審視,進而推斷一些驗證規則。例如,屬性name在資料庫不能為null,因為這個屬性已經自動加入NotNull constraint這個限制(若此屬性尚未有此限制)。

下面的表格統整了Symfony會自動加入的Doctrine metadata和驗證限制的應對關係:

Dontrine屬性(attribute) 驗證限制 備註
nullable=false NotNull 需要安裝PropertyInfo component
type Type 需要安裝PropertyInfo component
unique=true UniqueEntity
length Length

因為表單元件和API平台內部使用驗證元件,因此所有的表單和Web API都會因為自動化的驗證限制受惠。

自動驗證能夠讓寫程式更加有效率,是一個很棒的特色,但這無法完全取代驗證設定,你依然需要加入驗證限制(validation constraints)來保證提供給使用者的資料都是正確的。

New in version 4.3: Symfony 4.3開始使用自動化驗證。


從資料庫取得物件 Fetching Objects from the Database

從資料庫取得物件很簡單,假設你可以到 /product/1 看你的新商品:
// src/Controller/ProductController.php
// ...

/**
 * @Route("/product/{id}", name="product_show")
 */
public function show($id)
{
    $product = $this->getDoctrine()
        ->getRepository(Product::class)
        ->find($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    return new Response('Check out this great product: '.$product->getName());

    // or render a template
    // in the template, print things with {{ product.name }}
    // return $this->render('product/show.html.twig', ['product' => $product]);
}
打開瀏覽器看看!


當你要取得特定物件,你就會用到"知識庫(repository)",你可以想成repository就像是一個PHP class,它唯一個工作就是幫你找出某一類(class)的實體(entity)。

一旦有repository物件,你有需要好用的方法:
$repository = $this->getDoctrine()->getRepository(Product::class);

// look for a single Product by its primary key (usually "id")
$product = $repository->find($id);

// look for a single Product by name
$product = $repository->findOneBy(['name' => 'Keyboard']);
// or find by name and price
$product = $repository->findOneBy([
    'name' => 'Keyboard',
    'price' => 1999,
]);

// look for multiple Product objects matching the name, ordered by price
$products = $repository->findBy(
    ['name' => 'Keyboard'],
    ['price' => 'ASC']
);

// look for *all* Product objects
$products = $repository->findAll();
你可以客製方法來執行更複雜的queries,更多請看章節Querying for Objects: The Repository

在HTML頁面的正下方,除錯工具列(debug toolbar)會顯示query執行的數量和時間。


若query數量過多,符號顯示黃色來表示可能有些錯誤發生,點選符號打開Symfony Profiler去看實際執行的queries,如果你沒看到除錯工具列,請執行指令composer require --dev symfony/profiler-pack進行安裝。


自動取得物件 Automatially Fetching Objects (ParamConverter)

在很多狀況時,你可以使用SensioFrameworkExtraBundle來幫你自動執行query!首先,安裝bundle以防你沒有:
composer require sensio/framework-extra-bundle
現在,簡化你的控制器:
// src/Controller/ProductController.php
use App\Entity\Product;

/**
 * @Route("/product/{id}", name="product_show")
 */
public function show(Product $product)
{
    // use the Product!
    // ...
}
就是這樣!這個套件(bundle)使用路由(route)中的 {id} 來query Productid 欄位,如果查無資訊,則顯示404頁面。

還有很多選擇可以使用,請參閱ParamConverter了解更多。


更新物件

一旦你從Doctrine獲得一個物件,你可以如同PHP model一樣與其互動:
/**
 * @Route("/product/edit/{id}")
 */
public function update($id)
{
    $entityManager = $this->getDoctrine()->getManager();
    $product = $entityManager->getRepository(Product::class)->find($id);

    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }

    $product->setName('New product name!');
    $entityManager->flush();

    return $this->redirectToRoute('product_show', [
        'id' => $product->getId()
    ]);
}
使用Doctrine來編輯既有的product共有三個步驟:
  1. 從Donctrine獲取物件
  2. 修改物件
  3. 呼叫實體管理者(entity manager)的flush()
你可以呼叫 $entityManager->persist($product) ,但這不重要,因為Doctrine已經知道你物件的改變。


刪除物件

刪除物件的方法也非常相似,但是需要呼叫方法 remove() 通知Doctrine你想要從資料庫刪除這個給定的物件,DELETE指令不會真正被執行,直到呼叫方法 flush()


Querying for Objects: The Repository

你已經見識過repository物件如何不費工夫地讓你執行一些基礎queries:
// from inside a controller
$repository = $this->getDoctrine()->getRepository(Product::class);

$product = $repository->find($id);
但是如果你需要更複雜的query呢?當你執行 make:entity 來產生你的實體(entity),同時會產生類(class) ProductRepository
// src/Repository/ProductRepository.php
namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }
}
當你取得repository(i.e. ->getRepository(Product::class)),它是真正的物件實體!這是因為 repositoryClass 設定已經在 entity class的最上面產生。

假設你要query所有大於某個價格的商品物件,在你的repository新增新的方法:
// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    /**
     * @param $price
     * @return Product[]
     */
    public function findAllGreaterThanPrice($price): array
    {
        // automatically knows to select Products
        // the "p" is an alias you'll use in the rest of the query
        $qb = $this->createQueryBuilder('p')
            ->andWhere('p.price > :price')
            ->setParameter('price', $price)
            ->orderBy('p.price', 'ASC')
            ->getQuery();

        return $qb->execute();

        // to get just one result:
        // $product = $qb->setMaxResults(1)->getOneOrNullResult();
    }
}
這使用到Doctrine的Query Builder,它是非常強大且有用戶友善的方法客製queries,現在你可以呼叫repository的這個方法:
// from inside a controller
$minPrice = 1000;

$products = $this->getDoctrine()
    ->getRepository(Product::class)
    ->findAllGreaterThanPrice($minPrice);

// ...
如果你需要將Service/Config加入(inject)服務中(Injecting Services/Config into a Service),則可以輸入提示class ProductRepository,並像平常一樣將其注入(inject)。

若要瞭解更多細節,請看Doctrine文件Query Builder


Querying with DQL or SQL

除了query產生器之外,你還可以使用Doctrine Query Language進行query:
// src/Repository/ProductRepository.php
// ...

public function findAllGreaterThanPrice($price): array
{
    $entityManager = $this->getEntityManager();

    $query = $entityManager->createQuery(
        'SELECT p
        FROM App\Entity\Product p
        WHERE p.price > :price
        ORDER BY p.price ASC'
    )->setParameter('price', $price);

    // returns an array of Product objects
    return $query->execute();
}
如果有需要也可以直接使用SQL:
// src/Repository/ProductRepository.php
// ...

public function findAllGreaterThanPrice($price): array
{
    $conn = $this->getEntityManager()->getConnection();

    $sql = '
        SELECT * FROM product p
        WHERE p.price > :price
        ORDER BY p.price ASC
        ';
    $stmt = $conn->prepare($sql);
    $stmt->execute(['price' => $price]);

    // returns an array of arrays (i.e. a raw data set)
    return $stmt->fetchAll();
}
若使用SQL,你將取得資料列(raw data);而非物件(除非你使用NativeQuery)。


設定

請看Doctrine config reference


關聯性(Relationships and Associations)

Doctrine提供管理資料庫關聯性的所有功能(被稱謂associations),包含ManyToOne、OneToMany、OneToOne和ManyToMany關係。


虛擬資料裝置(Dummy Data Fixtures)

Doctrine提供函式庫(library)讓你可以載入測試資料道專案裡(i.e. "fixture data"),安裝方式:
composer require --dev doctrine/doctrine-fixtures-bundle
接者使用指令make:fixtures來產生空的fixture class:
php bin/console make:fixtures

The class name of the fixtures to create (e.g. AppFixtures):
ProductFixture
客製化新的class來載入物件 Product 到Doctrine:
// src/DataFixtures/ProductFixture.php
namespace App\DataFixtures;

use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;

class ProductFixture extends Fixture
{
    public function load(ObjectManager $manager)
    {
        $product = new Product();
        $product->setName('Priceless widget!');
        $product->setPrice(14.50);
        $product->setDescription('Ok, I guess it *does* have a price');
        $manager->persist($product);

        // add more products

        $manager->flush();
    }
}
清空資料庫並重新仔入所有的fixure classes:
php bin/console doctrine:fixtures:load
若要取得更多資料,請看文件DoctrineFixturesBundle



資料翻譯自:
https://symfony.com/doc/4.3/doctrine.html

系列文章:


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