Symfony

エンタープライズ向けの高性能PHPフレームワーク。モジュラー設計で柔軟性が高く、大規模アプリケーションに最適。

PHPフレームワークBackendMVC依存性注入バンドルエンタープライズ

GitHub概要

symfony/symfony

The Symfony PHP framework

スター30,487
ウォッチ1,062
フォーク9,670
作成日:2010年1月4日
言語:PHP
ライセンス:MIT License

トピックス

bundleframeworkhacktoberfestphpphp-frameworksymfonysymfony-bundle

スター履歴

symfony/symfony Star History
データ取得日時: 2025/8/13 01:43

Webフレームワーク

Symfony

概要

Symfonyは、PHPで書かれた高性能で堅牢なWebアプリケーションフレームワークです。再利用可能なコンポーネントの豊富なセットと、企業グレードの機能を提供する現代的なアーキテクチャが特徴で、大規模なWebアプリケーション開発に最適化されています。

詳細

Symfonyは2005年にFabien Potencierによって開発され、現在も活発に維持されているオープンソースフレームワークです。最新バージョンは7.3で、HTTP Early Hints、AssetMapperコンポーネント、Schedulerコンポーネント、Clockコンポーネントなどの革新的な機能が追加されています。

主要なアーキテクチャの特徴:

  • コンポーネントベース設計: 独立した再利用可能なPHPコンポーネント群
  • Kernelシステム: HTTPリクエストライフサイクルの中央管理
  • Bundleシステム: 機能のモジュール化と自己完結型パッケージ
  • 依存性注入コンテナ: 自動的なサービス管理とオートワイヤリング
  • イベント駆動アーキテクチャ: kernel.request、kernel.controllerなどのイベントシステム
  • MicroKernelTrait: 軽量アプリケーション開発のサポート

SymfonyはLTS(Long Term Support)バージョンを提供し、セマンティックバージョニングに厳密に準拠することで企業での安定した運用を保証しています。DoctrineORM、Twig、Swiftmailerなどの強力なコンポーネントとの統合により、フルスタック開発を効率的に行えます。

メリット・デメリット

メリット

  • 企業グレードの堅牢性: 大規模アプリケーションでの実証された安定性と性能
  • 豊富なコンポーネント: 50以上の独立したコンポーネントで機能拡張が容易
  • 強力な依存性注入: オートワイヤリングによる自動的なサービス解決
  • 優れたデバッグ機能: WebProfilerやDevツールバーによる開発効率向上
  • 広範なエコシステム: 豊富なバンドル、ライブラリ、サードパーティツール
  • 長期サポート: LTSバージョンによる3年間のセキュリティサポート
  • コミュニティ支援: 活発なコミュニティと豊富な学習リソース
  • テスト充実: PHPUnit統合による包括的なテスト環境

デメリット

  • 学習曲線の急さ: 概念やコンポーネントが多く初心者には困難
  • パフォーマンス: 機能豊富な分、軽量フレームワークより重い
  • 設定の複雑さ: YAML/XMLによる設定ファイルが複雑になる場合がある
  • メモリ使用量: 大規模なコンポーネントセットによる高いメモリ消費
  • 開発初期の遅さ: 小規模プロジェクトでは過剰な機能性
  • バージョン互換性: メジャーバージョンアップ時の破壊的変更

参考ページ

書き方の例

Hello World

<?php
// public/index.php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

require_once '../vendor/autoload.php';

$request = Request::createFromGlobals();

if ('/' === $request->getPathInfo()) {
    $response = new Response('Hello World!');
} elseif ('/hello' === $request->getPathInfo()) {
    $response = new Response('Hello Symfony!');
} else {
    $response = new Response('Not Found', 404);
}

$response->send();

コントローラーとルーティング

<?php
// src/Controller/HelloController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class HelloController extends AbstractController
{
    #[Route('/', name: 'app_hello')]
    public function index(): Response
    {
        return $this->render('hello/index.html.twig', [
            'message' => 'Hello Symfony!'
        ]);
    }

    #[Route('/hello/{name}', name: 'app_hello_name', methods: ['GET'])]
    public function hello(string $name = 'World'): Response
    {
        return new Response(
            sprintf('Hello %s!', $name)
        );
    }

    #[Route('/api/users', name: 'api_users', methods: ['GET'])]
    public function getUsers(): Response
    {
        $users = [
            ['id' => 1, 'name' => '太郎'],
            ['id' => 2, 'name' => '花子']
        ];

        return $this->json($users);
    }
}

Twigテンプレート

{# templates/hello/index.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Hello Symfony{% endblock %}

{% block body %}
<div class="container">
    <h1>{{ message }}</h1>
    <p>現在時刻: {{ "now"|date('Y-m-d H:i:s') }}</p>
    
    <ul>
    {% for i in 1..5 %}
        <li>Item {{ i }}</li>
    {% endfor %}
    </ul>
    
    <a href="{{ path('app_hello_name', {'name': 'Symfony'}) }}">
        Hello Symfonyページへ
    </a>
</div>
{% endblock %}

{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Welcome!{% endblock %}</title>
    <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
</head>
<body>
    {% block body %}{% endblock %}
</body>
</html>

サービスと依存性注入

<?php
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    public function getHappyMessage(): string
    {
        $messages = [
            'Symfony is awesome!',
            'PHP rocks with Symfony!',
            'Building great apps with Symfony!'
        ];

        $message = $messages[array_rand($messages)];
        $this->logger->info('Happy message generated', ['message' => $message]);

        return $message;
    }
}

// src/Controller/MessageController.php
namespace App\Controller;

use App\Service\MessageGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class MessageController extends AbstractController
{
    #[Route('/message', name: 'app_message')]
    public function message(MessageGenerator $messageGenerator): Response
    {
        $message = $messageGenerator->getHappyMessage();
        
        return $this->render('message/show.html.twig', [
            'message' => $message
        ]);
    }
}

Doctrineエンティティとリポジトリ

<?php
// src/Entity/Product.php
namespace App\Entity;

use App\Repository\ProductRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ProductRepository::class)]
#[ORM\Table(name: 'products')]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column]
    private ?float $price = null;

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $description = null;

    // ゲッター・セッター
    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): static
    {
        $this->name = $name;
        return $this;
    }

    public function getPrice(): ?float
    {
        return $this->price;
    }

    public function setPrice(float $price): static
    {
        $this->price = $price;
        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): static
    {
        $this->description = $description;
        return $this;
    }
}

// src/Repository/ProductRepository.php
namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    public function findExpensiveProducts(float $minPrice): array
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.price >= :minPrice')
            ->setParameter('minPrice', $minPrice)
            ->orderBy('p.price', 'ASC')
            ->getQuery()
            ->getResult();
    }

    public function findByNamePattern(string $pattern): array
    {
        return $this->createQueryBuilder('p')
            ->andWhere('p.name LIKE :pattern')
            ->setParameter('pattern', '%' . $pattern . '%')
            ->getQuery()
            ->getResult();
    }
}

フォーム処理とバリデーション

<?php
// src/Form/ProductType.php
namespace App\Form;

use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => '商品名',
                'constraints' => [
                    new Assert\NotBlank(message: '商品名は必須です'),
                    new Assert\Length(
                        min: 2,
                        max: 255,
                        minMessage: '商品名は2文字以上で入力してください',
                        maxMessage: '商品名は255文字以下で入力してください'
                    )
                ]
            ])
            ->add('price', MoneyType::class, [
                'label' => '価格',
                'currency' => 'JPY',
                'constraints' => [
                    new Assert\NotBlank(message: '価格は必須です'),
                    new Assert\PositiveOrZero(message: '価格は0以上で入力してください')
                ]
            ])
            ->add('description', TextareaType::class, [
                'label' => '説明',
                'required' => false,
                'attr' => ['rows' => 5]
            ])
            ->add('save', SubmitType::class, [
                'label' => '保存'
            ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Product::class,
        ]);
    }
}

// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
use App\Form\ProductType;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/products')]
class ProductController extends AbstractController
{
    #[Route('/', name: 'product_index', methods: ['GET'])]
    public function index(ProductRepository $productRepository): Response
    {
        return $this->render('product/index.html.twig', [
            'products' => $productRepository->findAll(),
        ]);
    }

    #[Route('/new', name: 'product_new', methods: ['GET', 'POST'])]
    public function new(Request $request, EntityManagerInterface $entityManager): Response
    {
        $product = new Product();
        $form = $this->createForm(ProductType::class, $product);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager->persist($product);
            $entityManager->flush();

            $this->addFlash('success', '商品が正常に作成されました');
            return $this->redirectToRoute('product_index');
        }

        return $this->render('product/new.html.twig', [
            'product' => $product,
            'form' => $form,
        ]);
    }

    #[Route('/{id}', name: 'product_show', methods: ['GET'])]
    public function show(Product $product): Response
    {
        return $this->render('product/show.html.twig', [
            'product' => $product,
        ]);
    }
}

API開発とシリアライゼーション

<?php
// src/Controller/ApiController.php
namespace App\Controller;

use App\Entity\Product;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

#[Route('/api/products')]
class ApiController extends AbstractController
{
    public function __construct(
        private ProductRepository $productRepository,
        private EntityManagerInterface $entityManager,
        private SerializerInterface $serializer,
        private ValidatorInterface $validator
    ) {
    }

    #[Route('', name: 'api_products_list', methods: ['GET'])]
    public function list(): JsonResponse
    {
        $products = $this->productRepository->findAll();
        
        return $this->json($products, Response::HTTP_OK, [], [
            'groups' => ['product:read']
        ]);
    }

    #[Route('/{id}', name: 'api_product_show', methods: ['GET'])]
    public function show(Product $product): JsonResponse
    {
        return $this->json($product, Response::HTTP_OK, [], [
            'groups' => ['product:read', 'product:detail']
        ]);
    }

    #[Route('', name: 'api_product_create', methods: ['POST'])]
    public function create(Request $request): JsonResponse
    {
        $product = $this->serializer->deserialize(
            $request->getContent(),
            Product::class,
            'json'
        );

        $errors = $this->validator->validate($product);
        if (count($errors) > 0) {
            return $this->json(['errors' => (string) $errors], Response::HTTP_BAD_REQUEST);
        }

        $this->entityManager->persist($product);
        $this->entityManager->flush();

        return $this->json($product, Response::HTTP_CREATED, [], [
            'groups' => ['product:read']
        ]);
    }

    #[Route('/{id}', name: 'api_product_update', methods: ['PUT'])]
    public function update(Product $product, Request $request): JsonResponse
    {
        $this->serializer->deserialize(
            $request->getContent(),
            Product::class,
            'json',
            ['object_to_populate' => $product]
        );

        $errors = $this->validator->validate($product);
        if (count($errors) > 0) {
            return $this->json(['errors' => (string) $errors], Response::HTTP_BAD_REQUEST);
        }

        $this->entityManager->flush();

        return $this->json($product, Response::HTTP_OK, [], [
            'groups' => ['product:read']
        ]);
    }

    #[Route('/{id}', name: 'api_product_delete', methods: ['DELETE'])]
    public function delete(Product $product): JsonResponse
    {
        $this->entityManager->remove($product);
        $this->entityManager->flush();

        return $this->json(null, Response::HTTP_NO_CONTENT);
    }
}