この記事では、SymfonyとAngularを使用してシングルページアプリケーション(SPA)のフォーム機能をゼロから構築する方法を紹介します。

サーバーサイドでAPIを構築しフロントエンドでフォームを作成しAPI連携までを行います。

ただフォームを作るだけでは面白くないので、PokeAPIを使用してフォームに入力されたポケモンの名前を送信するとそのポケモンの画像を表示するアプリを作成します。

目次

最初にざっと構築する流れを紹介して、その後全体的な処理のフローを紹介する流れで進めていきます。

1. 作成するもの

ポケモンの名前をフォームに入力し送信すると、ポケモンの名前と画像が表示されます。

↓本当は正式のキャラ画像が表示されますが、著作権の関係でオリジナルのイラストにしています。

完成しているコードはこちらに公開しています。

2. サーバーサイド(Symfony)の構築

  • PHP:v8.3.14
  • Symfony:v7.2.0

2.1. Symfonyのインストールとパッケージ設定

以下のコマンドでSymfonyプロジェクトを新規作成します。

$ symfony new symfony-angular-spa-form-app

必要なパッケージをインストールします。

$ composer require symfony/validator symfony/serializer-pack guzzlehttp/guzzle

これらは、フォームバリデーションやJSONレスポンス、HTTPリクエスト処理のために必要です。

2.2. APIエンドポイントの作成

/api/search-pokemon エンドポイントを作成して以下を実装します。

  • 機能概要
    1. フロントエンドから送信されたポケモン名を受け取る。
    2. PokeAPIを使用してポケモン情報を取得する。
    3. ポケモン名と画像URLをJSONで返す。
// FormController.php
class FormController extends AbstractController
{
    #[Route('/api/search-pokemon', name: 'search-pokemon', methods: ['GET'])]
    public function searchPokemon(Request $request): JsonResponse {
        $pokemonName = $request->query->get('pokemonName');
        $pokemonInfo = (new PokeAPI())->fetchPokemon($pokemonName);

        return new JsonResponse([
            'name' => $pokemonInfo['name'],
            'sprites_front_default_url' => $pokemonInfo['sprites']['front_default'],
        ]);
    }
}

2.3 PokeAPIクラスの作成

PokeAPIを使用してポケモンの情報を取得します。

PokeAPIのエンドポイントはこちらになり、https://pokeapi.co/api/v2/pokemon/{ポケモンの名前(英語)} でポケモンの情報を取得できます。

// PokeAPI.php
class PokeAPI
{
    private Client $client;

    public function __construct()
    {
        $this->client = new Client([
            'base_uri' => 'https://pokeapi.co/api/v2/',
        ]);
    }

    /**
     * @throws GuzzleException
     * @throws JsonException
     */
    public function fetchPokemon(string $name): array
    {
        $response = $this->client->request('GET', 'pokemon/' . $name);
        $responseBody = $response->getBody()->getContents();

        return json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
    }
}

3. フロントエンド(Angular)の構築

  • Angular:v19.0.1
  • Node.js:v20.9.0

3.1. Angularのインストール

以下のコマンドを実行してプロジェクトを作成します。

$ ng new symfony-angular-spa-form-app-frontend

※スタイルシート形式はSass(SCSS)を選択します。

3.2. コンポーネントとサービスの作成

フォームコンポーネント

以下コマンドを実行してhomeコンポーネントを生成します。

$ ng g c home

サービスの作成

以下コマンドを実行してAPI呼び出し用のapiサービスを生成します。

$ mkdir src/app/api
$ ng g s api/api

3.3. フォームの実装(homeコンポーネント)

homeコンポーネントでは、ユーザーがポケモン名を入力し送信ボタンを押すとAPIからデータを取得して画面に表示します。

フォームコンポーネント(home.component.ts)

export class HomeComponent {
   homeForm: FormGroup<HomeForm>;
   searchPokemonSubscription: Subscription | null = null;
   pokemonData: { name: string; url: string } | null = null;
   errorMessage: string | null = null;

   constructor(private api: ApiService) {
      this.homeForm = new FormGroup<HomeForm>({
         pokemonName: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.maxLength(30)] }),
      });
   }

   onSubmit(): void {
      if (this.homeForm.invalid) return;

      const formValue = this.homeForm.value;
      const param = {
         pokemonName: formValue.pokemonName as string,
      };

      this.searchPokemonSubscription = this.api.searchPokemon(param).subscribe({
         next: (response) => {
            this.pokemonData = {
               name: response.name,
               url: response.sprites_front_default_url,
            };
         },
         error: (error) => {
            console.error('エラー:', error);
         },
      });
   }
}

フォームテンプレート(home.component.html)

<form [formGroup]="homeForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="pokemonName">ポケモンの名前:</label>
    <input
      id="pokemonName"
      type="text"
      formControlName="pokemonName"
      required
    />
  </div>
  
  <button type="submit">送信</button>
</form>

<div *ngIf="pokemonData; else noData">
  <h2>{{ pokemonData!.name }}</h2>
  <img [src]="pokemonData!.url" alt="{{ pokemonData!.name }}" />
</div>

<ng-template #noData>
  <p>ポケモンデータがありません。</p>
</ng-template>

3.4. API呼び出し(apiサービス)

このAPI呼び出しのところは、バックエンド側と通信する上で特に重要なところになります。

ここでAngularアプリからバックエンド(Symfony API)にデータを送信して、レスポンスを受け取る仕組みを作ります。

API呼び出し用のApiServiceを以下のように実装します。

// api.service.ts
interface FormDTO {
   pokemonName: string;
}

interface FormResponse {
   name: string;
   sprites_front_default_url: string;
}

@Injectable({
   providedIn: 'root'
})
export class ApiService {
   private readonly FORM_API = '/api/search-pokemon';

   constructor(private http: HttpClient) {}

   searchPokemon(dto: FormDTO): Observable<FormResponse> {
       const params = new HttpParams().set('pokemonName', dto.pokemonName);
       return this.http.get<FormResponse>(this.FORM_API, { params }).pipe(
              catchError((error) => {
                 console.error('APIエラー:', error);
                 return throwError(() => new Error('API呼び出しに失敗しました。'));
              })
      );
   }
}

3.5. 最小限の設定

開発環境でAPIを正しくルーティングするための設定を追加します。

プロキシ設定

Angular(localhost:4200)とSymfony(localhost:8000)はポート番号が違うので、直接やりとりをしようとするとブラウザがリクエストをブロックしてしまいます。 この問題を解決するため、Angularではproxy.conf.jsonを設定してリクエストをSymfonyサーバーにスムーズに転送できるようにします。

プロキシ設定(proxy.conf.json) を作成します。

{
  "/api": {
    "target": "https://localhost:8000",
    "secure": false
  }
}

angular.jsonserveセクションに次を追加します。

"serve": {
   "options": {
      "proxyConfig": "proxy.conf.json"
    },
    ...

HTTP通信とパス設定の調整

main.js, app.routes.ts, app.config.ts, app.componets.tsなどをhomeコンポーネントに合わせて調整します。

4. 全体の処理フロー

ざっと構築する流れを紹介しましたが、全体の処理フローが掴みづらいと思ったので図にして整理したり重要なポイントを解説します。

4.1 フロントエンドからバックエンドにデータの送信

① フォームからデータを取得:フォームで入力されたポケモン名を取得します。

const param = { pokemonName: this.homeForm.value.pokemonName };

② HTTP GETリクエストを送信:ApiServiceを利用して以下のようにクエリパラメータを使用してリクエストを送信します。

this.apiService.searchPokemon(param).subscribe({
  next: (response) => console.log('成功:', response),
  error: (err) => console.error('失敗:', err),
});

③ 送信先のAPIエンドポイント

データはGET /api/search-pokemon?pokemonName={{ポケモン名}}形式でバックエンドに送信されます。 送信はAngularのプロキシ設定で http://localhost:4200/api はバックエンドの http://localhost:8000/api に転送されます。

4.2 バックエンドでの受信と処理

① Symfony APIでリクエストを受け取る

フロントエンドから送信されたGETリクエストは、バックエンドの/api/search-pokemonエンドポイントで受け取ります。 以下のようなコントローラーで、リクエストパラメータpokemonNameを取得し、PokeAPIを呼び出します。

#[Route('/api/search-pokemon', name: 'search-pokemon', methods: ['GET'])]
public function searchPokemon(Request $request): JsonResponse {
    // PokeAPIを使ってデータを取得し、JSONで返す
}

② レスポンスデータを返却する

取得したポケモン情報を整形し、以下のJSON形式でフロントエンドに返します。

{
  "name": "ポケモン名",
  "sprites_front_default_url": "画像URL"
}

4.3 フロントエンドでのデータの受信と表示

① レスポンスデータを処理

home.component.tsでAPIから返されたレスポンスをnextコールバックで処理し、ポケモン名と画像URLをpokemonDataに格納します。

this.apiService.searchPokemon(param).subscribe({
  next: (response) => {
    this.pokemonData = {
      name: response.name,
      url: response.sprites_front_default_url,
    };
  },
  error: () => {
    this.errorMessage = 'ポケモンデータを取得できませんでした。';
  },
});

② 画面に反映

受け取ったデータ(ポケモン名と画像URL)をテンプレートでバインディングして表示します。

<h2>{{ pokemonData.name }}</h2>
<img [src]="pokemonData.url" alt="{{ pokemonData.name }}" />

5. API連携して動作確認

最後に、API連携してみて実際動作するか確認してみます。

① バックエンド (Symfony) のサーバーを起動

$ symfony server:start

② フロントエンド (Angular) のサーバーを起動

$ ng serve

③ ブラウザで確認

http://localhost:4200 を開いてフォームにポケモンの名前を入力して送信すると、画像と名前が表示されます🎉🎉🎉

6. まとめ

この記事では、SymfonyとAngularを使用してシンプルなSPAのフォーム機能をゼロから構築する方法を紹介しました。

ポケモンの名前を入力して画像を表示するアプリを例にして以下のポイントに焦点を当てて紹介しました。

  • Symfonyを使ったAPIエンドポイントの実装
  • Angularを使ったフォーム作成とデータの受け渡し方法

弊社でもWeb広告APIからデータを取得・加工してフロントエンドに提供するSPAアプリケーションを構築しています。 この記事の内容は同様の仕組みをシンプルに体験できるようにまとめました。

今回は触れなかったですがSPAがなぜ多くの企業で採用されているのかそのメリットを調べてみるとさらに理解が深まると思います。

ぜひこの記事を参考に、自分だけのSPAを作成してみてください!