この記事では、SymfonyとAngularを使用してシングルページアプリケーション(SPA)のフォーム機能をゼロから構築する方法を紹介します。
サーバーサイドでAPIを構築しフロントエンドでフォームを作成しAPI連携までを行います。
ただフォームを作るだけでは面白くないので、PokeAPIを使用してフォームに入力されたポケモンの名前を送信するとそのポケモンの画像を表示するアプリを作成します。
目次
最初にざっと構築する流れを紹介して、その後全体的な処理のフローを紹介する流れで進めていきます。
1. 作成するもの
ポケモンの名前をフォームに入力し送信すると、ポケモンの名前と画像が表示されます。
↓本当は正式のキャラ画像が表示されますが、著作権の関係でオリジナルのイラストにしています。
完成しているコードはこちらに公開しています。
- Symfony側:https://github.com/mako5656/symfony-angular-spa-form-app
- Angular側:https://github.com/mako5656/symfony-angular-spa-form-app-frontend
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
エンドポイントを作成して以下を実装します。
- 機能概要
- フロントエンドから送信されたポケモン名を受け取る。
- PokeAPIを使用してポケモン情報を取得する。
- ポケモン名と画像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.json
のserve
セクションに次を追加します。
"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を作成してみてください!