Symfony(PHP) - user000422/0 GitHub Wiki

基本情報

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

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** ⚠️