Symfony(PHP) - user000422/0 GitHub Wiki

基本情報

MVCアーキテクチャ。
非常に堅牢であり大規模開発の実績も多く、Webアプリケーション開発に必要な機能は全て備えている。
CakePHP LaravelもSymfonyを基に作られている。

Symfonyバージョンについて(公式) https://symfony.com/releases

バージョン サポート終了 PHP条件 長期サポート版
4.4 2023/11 7.1 以上 ○
5.4 2025/11 7.2 以上 ○
6.4 2027/11 8.1 以上 ○

公式 SymfonyBook 日本語有(超参考になる)https://symfony.com/book
公式 ドキュメント(特に参考になる)https://symfony.com/doc/current/index.html

2015年Symfonyエンジニア「Symfony公式ドキュメントの中で、The Symfony Bookと並んで重要なのがSymfony Best Practicesです。」
2015年Symfonyエンジニア「Symfony Best Practicesは、Symfony Wayを強制するものではありませんが、Symfonyらしい設計・実装方法のヒントになります。」

構成公開 https://github.com/ttskch/symfony-example-app/tree/tagged

Formでやり取りするデータはデータ管理クラス(DTO)に設定すべきで、データ管理クラスはバリデーションチェックの設定が可能

■DI(DependencyInjection)依存性の注入
引数でインスタンス(new XXX のアレ)を受け取るイメージ、メソッド内部でnewしなくてよくなる。
Symfonyには、Service ContainerというDIコンテナが初めから導入されて、とても簡単にDIをすることができます。
読んでおくこと https://qiita.com/ippey_s/items/2e279486e1114272a570
読んでおくこと https://engineering.otobank.co.jp/entry/2017/06/13/191624


導入

# 一般的
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
chmod +x /usr/local/bin/composer
cd /var/www/html

# Symfonyプロジェクトを作成
composer create-project symfony/webapp-pack sample-symfony-app # 検証は行っていない
# 下記のような「website-skeleton」と「skeleton」は旧コマンド 
# composer create-project symfony/website-skeleton sample-project

ディレクトリ(ファイル)解説

Symfony 6.1

パス 重要 説明
bin
config 設定
migrations ※昔はなかった?
public ○ ルート
src ○ プログラム ソースコード
templates ○ テンプレート twig
tests テスト関連
translations 国際化対応関連
var ログ キャッシュ
vendor
パス 説明
.env 環境設定(DB)
src/Controller Controller
src/Entity データ管理クラス(DTOのような)
assets css ※composer require symfony/webpack-encore-bundle で導入

コマンド

# アプリケーション起動 プロジェクトのディレクトリで行うこと
php -S localhost:8080 -t public # 2023/11/20 RHEL9 Symfony6
symfony server:start # 2023/11/20 RHEL9 Symfony6 なぜか使えなかった

# バージョン確認 プロジェクトのディレクトリで行うこと
bin/console -V

# キャッシュを削除
php bin/console cache:clear

# Controller作成(Symfony4構文)
# テンプレートも作成される
php bin/console make:controller SampleController

Controller

■基本型
アノテーションによるルート … @Route([URLでアクセスするパス部分], name=[割り当てる名称])
AbstractControllerの継承 … 継承するのが一般的、便利な機能を備えている。
ビジネスロジックは記述しないこと。

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;

class SampleController extends AbstractController
{
    // ルーティング Symfony6構文
    #[Route('/sample', name: 'app_sample')]
    public function index(Request $request)
    {
        // request->get POST値を取得
        $sample_name = $request->request->get('sample_name');
        $sample_name = $request->request->get('sample_name', 'default_value'); // 空の場合のデフォルト値を設定

        // request 全て取得
        $sample_requests = $request->query->all();

        // request->get->has POST値の存在チェック
        if($request->request->has('sample_name')){
            // POST値が存在する
        }

        // render テンプレートを読み込む ※よく使う
        // 第一引数: テンプレートパス(テンプレートパスは「templates/」をカレント)
        // 第二引数: テンプレートへ値を渡す
        return $this->render('sample/index.html.twig', [
            'controller_name' => 'SampleController',
        ]);
    }
}

ルーティング

ルート … アドレスと処理との関連付け
アノテーションによるルーティングと、ルート設定ファイルによるルーティングの2種類ある。
アノテーションによるルーティングがベストプラクティスとされている(Symfony2より)
ルート設定ファイル config/routes.yaml

# path : パスの指定(URLのドメインより後ろのパス)
# controller: コントローラーとアクションの指定
sample:
  path: /sample
  controller: App/Controller/SampleController::index

テンプレートエンジン(Twig)

構成 : ベーステンプレート + 部品テンプレート + メインテンプレート(画面ごとの)
Symfony標準のテンプレートエンジン。非常に分かりやすいシンプルな構文。
<form> は直接記述しないこと(「Symfony Forms」機能の利用を推奨)
タグ … {% %}

<!-- ベーステンプレート base.html.twig -->

<!-- block ベーステンプレートを継承するテンプレートが定義する区画 -->
{% block sample %}
{% endblock %}
<!-- extends 別のテンプレートを継承する -->
{% extends 'base.html.twig' %}

<!-- include 部品の継承(ベースほど大きくないテンプレートの継承) -->
{% include '/sample/sample.html.twig' %}

<!-- block ベーステンプレートの定義に対応しHTMLを定義 -->
{% block sample %}
<!-- 値を埋め込む -->
<p> {{sample_value}} </p>
{% endblock %} <!-- block 終了 -->

<!-- if -->
{% if flg %} <!-- 例では変数「flg」の真偽を検査 -->
<span> Hello </span>
{% endif %} <!-- if 終了 -->

<!-- for -->
{% for item in data %}
<p> {{item.name}} </p>
{% endfor %}

<!-- 変数 -->
{% set result = 'hello' %} <!-- 文字列を設定する場合 -->
{% set result = sample_value %} <!-- 変数を設定する場合 -->

<!-- 画像関連 -->
<!-- asset publicをカレントとする -->
<img src="{{ asset('images/sample.jpg') }}" alt="">
<img src="{{ asset('images/' ~ sample_val ~ '.jpg') }}" alt=""> <!-- 変数を組み込む場合 「~」が文字結合 -->

<!-- HTMLタグとして扱う -->
{{ sample_tab|raw }} <!-- 変数には何かしらのHTMLタグ文字列が代入されている -->

Model データの管理(基本的にデータベースアクセスのみ)

順序 エンティティ作成 → マイグレーション作成 → マイグレーションの適用
エンティティの変更を行ったら → マイグレーション作成 → マイグレーションの適用
※エンティティを作成しマイグレーションを行うと既存のDBは更新されます(確実に意図しない構成になります)
非推奨コマンド doctrine:mapping:import ※2019年より

■データベースの設定
.env … データベースの設定ファイル。

DATABASE_URL="postgresql://127.0.0.1:5432/db?serverVersion=14&charset=utf8"

■エンティティファイルの作成
php-mysqlnd.x86_64のインストールが必須
エンティティクラス名はテーブル名をもとにした名前にすること

下記コマンドはSymfony4~6対応コマンド

項目 コマンド
エンティティファイルの作成 php bin/console make:entity
マイグレーションファイルの作成 php bin/console make:migration
マイグレーションの適用 php bin/console doctrine:migrations:migrate

エンティティファイル作成時のウィザード回答は下記公式を参考
https://symfony.com/doc/current/doctrine.html#creating-an-entity-class

■エンティティの削除方法
エンティティクラスを削除 rm src/Entity/SampleEntity.php
マイグレーションファイルの作成 … php bin/console make:migration
マイグレーションの適用 … php bin/console doctrine:migrations:migrate

■エンティティクラスの構成例(色々なテクニックを紹介)

#[ORM\Entity(repositoryClass: SampleTableRepository::class)]
#[ORM\Table(name: "sample_table")] // テーブル名の設定
#[ORM\Index(name: "sample_idx", fields: ["sample_id", "sample_date"])] // インデックスの設定(複数カラム指定はカンマ区切り)
class SampleTable
{
    // デフォルト値「0」を設定する例
    #[ORM\Column(type: Types::SMALLINT, options:['default' => 0])]
    private ?int $sample_flg = 0;

    // デフォルト値「現在時刻」を設定する例 : CURRENT_TIMESTAMP
    #[ORM\Column(type: Types::DATETIME_MUTABLE, options: ["default" => "CURRENT_TIMESTAMP"])]
    private ?\DateTimeInterface $update_date;

    // 主キーにする場合 [ORM\Id]
    // 複数カラムに対して設定可
    #[ORM\Id]
    private ?int $sample_col = null;

    // 外部キー
    // JoinColumn name:このテーブルの外部キーを設定するカラム
    // JoinColumn referencedColumnName:参照先のカラム
    #[ORM\ManyToOne(targetEntity: TestTable::class)]
    #[ORM\JoinColumn(name: "sample_col", referencedColumnName: "test_col")]
    private $testCol;
}

■既存テーブルからエンティティを作成(要確認 要注意)
事前に composer require doctrine/annotations
移動 … cd [プロジェクトディレクトリ]
エンティティファイルの作成 … php bin/console doctrine:mapping:convert --from-database annotation ./src/Entity
作成したエンティティクラスを手動で編集 「namespace App\Entity;」を上部に記述する
getter/setter等を追加 … php bin/console make:entity --regenerate App


Service Container

パス : src/Service
構成 : Service + Controller + services.yaml
Service … ビジネスロジックをまとめたクラス
Service Container … サービスをインスタンス化するときに、必要とする定数や他のサービスのオブジェクトを注入したりと、依存関係を解決する役割を担ってくれます。
https://symfony.com/doc/current/service_container.html

// Service Class : src/Service/SampleService.php
namespace App\Service;

class SampleService
{
    public function getSample(): string
    {
        return 'hello';
    }
}
// Controller
use App\Service\SampleService;

public function new(SampleService $sampleService): Response
{
    // Serviceを呼び出す
    $result = $sampleService->getSample();
}
# config/services.yaml
# デフォルトでサービス自動登録が有効になっている。

Symfony Forms

Form支援機能
https://symfony.com/doc/current/forms.html
必須セット : コントローラー(Form制御) + テンプレート(Form表示) + データ管理クラス(DTO)

// Controller 関数外コード省略
public function index()
{
    // データ管理クラス インスタンス化
    $sample = new Sample();

    // createFormBuilder FormBuilderを作成 ※このようなメソッドチェーン記述が一般的
    // add項目はデータ管理クラスのパラメータと同一にすること
    $form = $this->createFormBuilder($sample)
        // add Form要素を定義
        ->add('name', TextType::class) // textbox定義
        ->add('save', SubmitType::class, ['label' => 'Click']) // submit定義
        ->getForm();

    return $this->render('sample/index.html.twig', [
        // createView ビューの生成
        'form' => $form->createView(),
    ]);
}

// Form受け取り側
public function sample()
{
    // handleRequest Form情報をハンドリング
    $form->handleRequest($request);
    $obj = $form->getData(); // Formハンドルから情報を取得(オブジェクト化)
}
<!-- テンプレート formを埋め込む(受け取って表示) -->
<body>
  {{form_start(form)}} <!-- Form 開始 <form> -->
  {{form_widget(form)}} <!-- Form コントロールの生成 -->
  {{form_end(form)}} <!-- Form 終了 </form> -->
</body>
// データ管理クラス
class Sample
{
    protected $name;

    public function getName()
    {
        return $this->name;
    }
    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }
}

DB関連

エンティティ … DBと連携するため、テーブルの内容をPHPのクラスで定義したもの(表したもの)
リポジトリ … DB操作ユーティリティ
命名(公式)関数名:SQL … show:SELECT、create:INSERT
簡単な操作はControllerで行う。複雑な操作はRepositoryクラスで行う。

■実践用(実際に動かしたコード Symfony6)

// src/Controller/SampleController.php
use App\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;

// 割愛

class ProductController extends AbstractController
{
    public function show(EntityManagerInterface $entityManager): Response
    {
        // getRepository リポジトリを取得 第一引数:エンティティクラス
        $repository = $entityManager->getRepository(Person::class);

        // find SELECT文 第一引数:カラム「id」の条件
        $person = $repository->find(10); // カラム「id」が「10」のレコードを取得

        // findOneBy SELECT文 第一引数:指定カラムの条件(1つの場合)
        $person = $repository->findOneBy(['name' => 'yamada']); // カラム「name」が「yamada」のレコードを取得

        // findOneBy SELECT文 第一引数:指定カラムの条件(複数の場合)
        $person = $repository->findOneBy([
            'name' => 'yamada',
            'color' => 'red',
        ]);

        // findall SELECT文 全てのレコードを取得
        $person = $repository->findAll();

        /*
         * 取得したデータの加工
         * Entityオブジェクトのためまず分割しなければならない
         */
        foreach($person as $row){
            // get 取得結果から指定カラムの値を取得
            $data[$i] = $person->getName();
            $i++;
        }
    }
}
// 実践 QueryBuilder Repository
// SQL操作を行いたいテーブルのエンティティクラスに対応したRepositoryクラスを使用
// 参考 : https://symfony.com/doc/current/doctrine.html#doctrine-queries
// src/Repository/SampleRepository.php
class SampleRepository extends ServiceEntityRepository
{
    // 自由にメソッドを定義する
    // (公式例)findAllGreaterThanPrice
    public function findSample()
    {
        // createQueryBuilder
        $qb = $this->createQueryBuilder('s') // エイリアスはテーブル名から
            ->select('s.id') // SELECTはSQLべた書きのイメージ
            ->where('s.color = :color')
            ->setParameter('color', 'red')
            ->groupBy('s.id')
            ->orderBy('s.id', 'ASC')
            ->addOrderBy('s.color', 'ASC');

        $query = $qb->getQuery();

        // return
        return $query->execute();
        // $query->getResult() を使用する人もいる
        // return $query->getResult()
    }
}

public function index(Request $request)
{
    // Doctrine オブジェクトを取得(共通 基本型)
    // getRepository リポジトリを取得 第一引数 : エンティティ
    $repository = $this->getDoctrine()->getRepository(Sample::class);

    // findall 全レコードのエンティティを取得
    $result = $repository->findall();
}

// find 指定IDのエンティティを取得
// 「指定IDのエンティティを取得」は最もよく使われる
// 下記はエンティティの自動フェッチ(DBに関する記述を必要としない アノテーションで管理)
// Routeの{id}がエンティティのIDと連携する
/**
 * @Route("/find/{id}", name="find")
 * /
public function find(Request $request, Person $person)
{
    return xxx;
}

// find by(WHERE sample = 'value')
/**
 * @Route("/find", name="name")
 * /
public function find(Request $request, Person $person)
{
    $form->handleRequest($request);
    $findstr = $form->getData()->getfind(); // Form入力値の取得
    $repository = $this->getDoctrine()->getRepository(Sample::class); // repositoryの取得

    // findBy 一致するレコードを検索 ['カラム' => '条件値']
    $result = $repository->findBy(['name' => $findstr]);
}

// Create(Insert)
/**
 * @Route("/create", name="create")
 * /
public function create(Request $request)
{
    $form->handleRequest($request);
    $person = $form->getData();

    // getManager Managerを取得
    $manager = $this->getDoctrine()->getManager();

    // persist インスタンスの保存(Insertデータの保存)
    $manager->persist($person);

    // flush 反映(Insert実行)
    $manager->flush();
}

■Native SQL
テーブルに外部キーを設定できない等のプロジェクト制約でQuerybuillderが使用できない場合に。
参考 : https://symfony.com/doc/current/doctrine.html#querying-with-sql

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

    $sql = 'SELECT * FROM sample_table WHERE price > :price';

    $resultSet = $conn->executeQuery($sql, ['price' => $price]);

    return $resultSet->fetchAllAssociative();
}

バリデーションチェック

Entity + Controller
Formで定義する方法もあるが、時間がないため割愛。
エンティティ(またはデータ管理クラス)にて、アノテーション(@Assert)で定義。

// エンティティクラス
/**
 * @ORM xxxxx
 * @Assert\NotBlank() // Null、空のテキストを禁止。
 * @Assert\Type(type="string") // データ型指定。
 * @Assert\Length(min = 1, max = 10) // 文字数
 */
private sample;

コントローラーにて、バリデーションチェック

// Controller
public function sample(Request $request, ValidatorInterface $validator)
{
    $sample = $form->getData();

    // validate バリデーションチェック 引数: エンティティ
    // 戻り値: エラーデータの配列
    $errors = $validator->validate($sample);

    if (count($errors) == 0){ /* エラーなしの場合 */ }
}

応用

標準でSymfonyに用意されているクラス。

クラス名 説明
JsonResponse JSONとデータ連携
use Psr/Log/LoggerInterface ログ操作

■twigでcssを扱いたい(Webpack Encore)

# RHEL9

composer require symfony/webpack-encore-bundle

# yarnの導入
curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo
yum install yarn
yarn install
yarn build

・CSSの追加 assets/app.js


■Session

// 引数の「SessionInterface」が必須
public function index(SessionInterface $session)
{
    // remove 指定sessionを削除
    $session->remove('sample');
}

DB関連 テーブル連携(SELECT * FROM a_table, b_table)

参考 : https://symfony.com/doc/current/doctrine/associations.html
フィールドとゲッターセッターを定義する

// SampleTableとSubTableのエンティティリレーション
use App\Entity\SubTable;

// OneToOneの例
#[ORM\OneToOne(targetEntity: SubTable::class)]
private $subTable;

// get set 忘れずに
// querybuilderの例
$qb = $this->createQueryBuilder('a')
    ->select('a.id, b.name')
    // テーブルaで定義した他テーブルを呼び出す, エイリアス, WITH(ON句), ON句の内容
    ->join('a.subTable', 'b', 'WITH', 'a.id = b.id');

エラー対応

■ResourceNotFoundException(No Root)
Controllerの中に定義の怪しいものがある。

■Compile Error: Cannot declare class Sample, because the name is already in use
namespaceが不正。

⚠️ **GitHub.com Fallback** ⚠️