Symfonyに触ったメモ

## どうしてSymfonyを？
既存システムを解析する案件が来る予定だったのでちょこちょこ調べた。
ざっくばらんなメモなので、通して読むよりトピック的に拾う方が正しい。

## Symfonyに触れる環境を作る

[SymfonyCastsの「Charming Development in Symfony 5」](https://symfonycasts.com/screencast/symfony/setup)をなぞるのが手取り早い。
「03. Route, Controllers&Responses!」までやると、とりあえず動くようになる。

## Symfony解析時の基本的な流れ

1. `routing.yml`でルーティング情報を確認する
2. `Controller`に格納されている`XxxController.php`でルーティングに対応する関数を確認する
3. 関数内の処理を解析する

## フォルダパス

```
Sample\SampleBundle
├─Controller           // コントローラ。ルーティングに対応するのはここを参照。
├─DataFixtures         // 初期データ等を用意するデータフィクスチャ
│  └─ORM               // ORMで実行するデータフィクスチャ
├─DependencyInjection
├─Entity               // データを運搬するためのEntity。ORMとのマッピングもここ
├─Form                 // Entityをごにょごにょする(要調査)
├─Repository           // DBとのやりとりする処理を格納
├─Resources
│  ├─config
│  │  ├─config.yml     // 設定値を保存するyamlファイル
│  │  ├─routing.yml    // [重要]ルーティング情報を保存するyamlファイル
│  │  └─services.yml
│  ├─public
│  │  └─css            // CSS
│  └─views             // コントローラ名に対応するフォルダ。フォルダ内はtwigファイル。
│      └─AAA           // AAAコントローラに対応するtwigファイル
├─Tests                // 自動テストに使用するファイ
└─Twig
    └─Extensions
```

## 登録されているバンドルを確認する

以下のファイルにインストールされているバンドルの情報が記載される。

`%アプリケーションのルートディレクトリ%/app/AppKernel.php`

- `$bundle`に対して`new`されているものは状態を問わずに使われる
- `dev`と付いているものは開発環境で使われる
- `test`と付いているものはテスト環境で使われる

## バンドルのルーティングを確認する

以下のファイルに使用するバンドルのルーティングを記載したyamlの格納パスが記載される。

`%アプリケーションのルートディレクトリ%/app/config/routing.yaml`

上記ファイルパスを開いた例が以下である。

```yaml:routing.yaml
SampleBundle:
    resource: "@SampleBundle/Resources/config/routing.yml"
    prefix:   /
```

`resource`に設定されたファイルはファイルパスに直すと以下になる。

`%アプリケーションのルートディレクリ%/src/%ネームスペース名%/%バンドル名%/Resources/config/routing.yaml`

上記ファイルパスを開いた例が以下である。

```yaml:routing.yaml
SampleBundle_homepage:
    pattern:  /
    defaults: { _controller: SampleBundle:Page:index }
    requirements:
        _method:  GET
        
SampleBundle_about:
    pattern:  /about
    defaults: { _controller: SampleBundle:Page:about }
    requirements:
        _method:  GET
```

こちらの例であれば、以下のルーティングが設定されている。

- `/`に`GET`でアクセスした際、`PageController`の`indexAction`を呼び出す
- `/about`に`GET`でアクセスした際、`PageController`の`aboutAction`を呼び出す


## ルーティング

上記のルーティングの設定にコメントを付与すると以下になる。

```yaml:routing.yaml(抜粋)
SampleBundle_homepage:
    pattern:  /                                        # URLパターン
    defaults: { _controller: SampleBundle:Page:index } # 呼び出すコントローラと関数
    requirements:
        _method:  GET                                  # リクエスト
```

URLパターンの一部をブラケット(`{}`)で括るとその部分はプレースホルダーとしてどのような値でもマッチする。
設定例は以下である。

```yaml:パターンサンプル
SampleBundle_anyvalue:
    pattern:  /anyvalue/{value}
    defaults: { _controller: SampleBundle:Page:anyvalue }
```

上記設定では以下がマッチする。

- `%バンドルに設定したパス%/anyvalue/`
- `%バンドルに設定したパス%/anyvalue/hoge`
- `%バンドルに設定したパス%/anyvalue/foobar`

また、上記例では`requirements`の`_method`が省略されているが、その場合、`GET`、`POST`、`PUT`、`DELETE`いずれにもマッチする。

## URLに含まれる不要なスラッグ

以下のようなルーティングとURLを考える。

```yaml:slugサンプル
SampleBundle_slugsample:
    pattern:  /{id}/{value}
    defaults: { _controller: SampleBundle:Page:slugsample }
    requirements:
        _method:  GET
        id: \d+
```

```URL:URL
http://somedomain/appname/1/this_is_a_slug
```

上記例では`requirements`に`id(=1)`は含まれているが、`slug`は含まれていない。
このような場合、URLから内容がわかるため、SEOを期待している。
設定する場合は、登録時にタイトルの非ASCII文字を`-`に置き換えてDBに格納すると良い。


## テンプレート

基本的に`PHP`か`twig`が使われる。
テンプレートの継承とデザインは「[3レベル継承(three-level template inheritance)](https://symfony.com/doc/current/templates.html#template-inheritance-and-layouts)」アプローチが推奨されている。

`twig`であればテンプレートの格納先は以下のどちらかになる。

- `%アプリケーションのルートディレクリ%/app/Resources/views/xxxx.html.twig`
- `%アプリケーションのルートディレクリ%/src/%ネームスペース名%/%バンドル名%/Resources/views/xxxx.html.twig`

## リンク

`twig`を使用している場合のリンクは以下のようにルーティングで設定した名前となる。

```twig:sample.html.twig
{% block navigation %}
<nav>
    <a href="{{ path('SampleBundle_homepage') }}">Home</a>
</nav>
{% endblock %}
```

間違っても以下のように**URLで設定してはいけない**。
これはSymfonyのルーティングを無視することになるため、正常に動作しない。

```twig:sample.html.twig
{% block navigation %}
<nav>
    <!-- DO NOT USE -->
    <a href="/">Home</a>
</nav>
{% endblock %}
```

## EntityとForm

値を格納する最小単位がEntityである。
Entityでは以下のような要素を格納する。

- 変数
- 変数へのアクセサ(変数hogeへのgetHoge/setHoge)
- バリデーション(loadValidatorMetadata)

FormBuilderInterfaceを使用したり、Controllerで設定する際に使用されるのがFormである。
AbstractTypeをextendsしていることがある。

## ルーティング設定例

ルーティングを設定するには以下の方法がある。
どちらを軸に設定されているか確認する。

1. routes.ymlで設定する
2. アノテーションで設定する

### routes.yamlで設定する

routes.ymlの配置先は以下の通り。

`%プロジェクトのフォルダ%/config/routes.yml`

開くとデフォルトで以下のようになっている。

``` yaml:routes.yml
#index:
#    path: /
#    controller: App\Controller\DefaultController::index
```

行頭の「#」はコメントアウトを示す。
pathはURLのパスを、controllerは対応するコントローラを示す。
上記のコメントアウトを外した場合、「/」で「%プロジェクトのフォルダ%/src/Controller/DefaultController.php」の「index」が呼ばれることになる。

### アノテーションで設定する

パスをアノテーションで関数単位で設定する。
以下の例では「/」で画面に「sample response」と表示される。

```php:SampleController.php
<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SampleController
{
    /**
     * @Route("/")
     */
    public function homepage()
    {
        return new Response('sample response');
    }
}
?>
```

## 設定されているパスを一覧で表示する

terminalでプロジェクトのフォルダを開き、以下のコマンドを叩く。

`$ php bin/console debug:router`

以下のように表示される。

```terminal:terminal
 ----------------------- -------- -------- ------ -------------------------- 
  Name                    Method   Scheme   Host   Path                      
 ----------------------- -------- -------- ------ -------------------------- 
  _preview_error          ANY      ANY      ANY    /_error/{code}.{_format}  
  app_sample_homepage     ANY      ANY      ANY    /                                
 ----------------------- -------- -------- ------ -------------------------- 
```

## バリデーション

入力値をDB登録前に検証する。
Entity内に設定することで検証が行われる。

- [Validation Constraints Reference
](https://symfony.com/doc/current/reference/constraints.html)
- [How to Create a custom Validation Constraint](https://symfony.com/doc/current/validation/custom_constraint.html)

バリデータを使う際は以下のように使う制約をuse命令文で追加する

`use Symfony\Component\Validator\Mapping\ClassMetadata;`

# Doctrine2

SQLではなく`Doctrine Query Language(DQL)`を使用する。
ORMとして動作し、各DBとのやりとりを行い、データの永続化に寄与する。
`Entity`に`porotected`な変数を宣言し、アノテーションでマッピングを行う。

```php:Sample.php(変数宣言)
namespace Sample\SampleBundle\Entity;

class Sample
{
    protected $id;
    protected $value1;

    // 後述のgetter/setterは省略
}
```

```php:Sample.php(ORM)
namespace Sample\SampleBundle\Entity;

use Doctrine\ORM\Mapping as ORM; // Doctrineを読み込み

//
/**
 * @ORM\Entity
 * @ORM\Table(name="sample")
 */
class Sample
{
    // オートインクリメントするIDの設定
    /**
    * @ORM\Id
    * @ORM\Column(type="integer")
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    protected $id;

    // 文字列型のvbalue1の設定
    /**
    * @ORM\Column(type="string")
    */
    protected $value1;
}
```

実際に設定する場合は、インスタンスを生成して値を入れる。

```
$sample = new Sample();
$sample->setValue1("val1");
// idは自動歳晩なのでset不要
```

Doctrine2を使用してSQLを流すこともできるが、データベース抽象を使用しないため**推奨されない**。
SQLを実行したい場合は`QueryBuilder`を使用するとよい。

```php:QueryBuilderSample.php
// QueryBuilder以下を表現
//  SELECT 'hoge', 'created' FROM SampleBundle ORDER BY 'created' DESC;
$em = $this->getDoctrine()->getManager();

$records = $em->createQueryBuilder()
              ->select('hoge')
              ->from('SampleBundle:Sample',  'hoge')
              ->addOrderBy('hoge.created', 'DESC')
              ->getQuery()
              ->getResult();
```

クエリー実行は以下の理由からコントローラ内で***行わないこと***。

- アプリケーション内で同一のクエリーを実行する場合、再利用ができない
- コードが重複しする(DRY原則に反する)
- 分離することにより、クエリー単体のテストが実行可能となる

⇒分離する際はDoctrine2 Repositoriesを使用する