[Symfony] 既存のアプリを機能ごとにバンドルに分割したときにやったことまとめ
既存のSymfonyアプリに大きめの機能追加を行うことになり、さすがに(広義の)モノリスでは管理しきれないボリュームになってきたので、既存機能をバンドルに分割しました。
そのときにやったことを備忘録としてまとめておきます。
アプリをバンドルに分割するのはNG?
ちなみに Symfonyのドキュメント には
In Symfony versions prior to 4.0, it was recommended to organize your own application code using bundles. This is no longer recommended and bundles should only be used to share code and features between multiple applications.
という記載があり、アプリを機能ごとにバンドルに分割するのは推奨されない方法のようです。
正しくやるなら、Gitリポジトリごと分けて、各機能を「再利用可能なバンドル」として実装し、メインのアプリに各バンドルをインストールして全体を組み立てることになるのでしょうか🤔(詳しい方いたら教えてください🙏)
そうは言っても現実的には以前のようにアプリ内にバンドルを複数作りたいケースは多いと思うので、今回は非推奨であることには目をつぶっていただいて🙈、教養として読んでもらえたらいいかなと思います😅
1. src 配下のファイルをバンドルのディレクトリに移動
まず、 src 配下に Bundle/バンドル名 というパスでディレクトリを切り、各種ファイルをここに移動します。
Before
.
└── src
├── Command
├── Controller
├── Entity
├── EntityListener
├── Event
├── EventSubscriber
├── Form
├── Repository
├── Security
├── Twig
├── Validator
└── Kernel.php
After
.
└── src
├── Bundle
│ ├── AppBundle
│ │ ├── Command
│ │ ├── Controller
│ │ ├── Entity
│ │ ├── EntityListener
│ │ ├── Form
│ │ ├── Repository
│ │ ├── Security
│ │ ├── Twig
│ │ └── Validator
│ └── FooBundle
│ ├── Controller
│ ├── Entity
│ ├── EntityListener
│ ├── Event
│ ├── EventSubscriber
│ ├── Form
│ ├── Repository
│ ├── Twig
│ └── Validator
└── Kernel.php
ユーザー認証周りや機能に依らない共通部品っぽいものを AppBundle に、 Foo 機能に関連するものを FooBundle に振り分けるとよいでしょう。
移動したファイルの namespace はディレクトリ位置に合わせて適切に変更し、そのクラスを参照している他のファイルの use 文もあわせて変更します。
これが一番大変な作業だと思います。IDEの力を借りつつ頑張りましょう💪
2. バンドルをフレームワークに登録
src/Bundle/AppBundle/AppBundle.php および src/Bundle/FooBundle/FooBundle.php を作成します。
<?php
namespace App\Bundle\AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
}
<?php
namespace App\Bundle\FooBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class FooBundle extends Bundle
{
}
そしてこれらを config/bundles.php に登録します。
<?php
return [
// ...
App\Bundle\AppBundle\AppBundle::class => ['all' => true],
App\Bundle\FooBundle\FooBundle::class => ['all' => true],
];
3. templates 配下のファイルを移動
各バンドルに Resources/views ディレクトリを作成し、 templates 配下のビューのファイルをそこに移動します。
Before
.
└── templates
├── home
│ ├── index.html.twig
│ ├── show.html.twig
│ ├── new.html.twig
│ └── edit.html.twig
├── user
│ ├── index.html.twig
│ ├── show.html.twig
│ ├── new.html.twig
│ └── edit.html.twig
├── bar
│ ├── index.html.twig
│ ├── show.html.twig
│ ├── new.html.twig
│ └── edit.html.twig
├── baz
│ ├── index.html.twig
│ ├── show.html.twig
│ ├── new.html.twig
│ └── edit.html.twig
└── base.html.twig
After
.
└── src
├── Bundle
│ ├── AppBundle
│ │ :
│ │ :
│ │ ├── Resources
│ │ │ └── views
│ │ │ ├── home
│ │ │ │ ├── index.html.twig
│ │ │ │ ├── show.html.twig
│ │ │ │ ├── new.html.twig
│ │ │ │ └── edit.html.twig
│ │ │ ├── user
│ │ │ │ ├── index.html.twig
│ │ │ │ ├── show.html.twig
│ │ │ │ ├── new.html.twig
│ │ │ │ └── edit.html.twig
│ │ │ └── base.html.twig
│ │ └── AppBundle.php
│ └── FooBundle
│ :
│ :
│ ├── Resources
│ │ └── views
│ │ ├── bar
│ │ │ ├── index.html.twig
│ │ │ ├── show.html.twig
│ │ │ ├── new.html.twig
│ │ │ └── edit.html.twig
│ │ └── baz
│ │ ├── index.html.twig
│ │ ├── show.html.twig
│ │ ├── new.html.twig
│ │ └── edit.html.twig
│ └── FooBundle.php
└── Kernel.php
extends や include などで他のテンプレートファイルを参照している箇所がある場合は、以下のようにそのテンプレートファイルがどのバンドルに属しているかを明記する必要があるので、該当箇所すべてを修正します。
- {% extends 'base.html.twig' %}
+ {% extends '@App/base.html.twig' %}
4. テストファイルを移動
各バンドルに Tests ディレクトリを作成し、 tests 配下のファイルをそこに移動します。
Before
.
└── tests
├── Controller
│ ├── HomeControllerTest.php
│ ├── UserControllerTest.php
│ ├── BarControllerTest.php
│ └── BazControllerTest.php
├── Repository
│ ├── UserRepositoryTest.php
│ ├── BarRepositoryTest.php
│ └── BazRepositoryTest.php
├── fixture
│ ├── Controller
│ │ ├── HomeControllerTest.yaml
│ │ ├── UserControllerTest.yaml
│ │ ├── BarControllerTest.yaml
│ │ └── BazControllerTest.yaml
│ └── Repository
│ ├── UserRepositoryTest.yaml
│ ├── BarControllerTest.yaml
│ └── BazRepositoryTest.yaml
└── bootstrap.php
After
.
├── src
│ ├── Bundle
│ │ ├── AppBundle
│ │ │ :
│ │ │ :
│ │ │ ├── Tests
│ │ │ │ ├── Controller
│ │ │ │ │ ├── HomeControllerTest.php
│ │ │ │ │ └── UserControllerTest.php
│ │ │ │ ├── Repository
│ │ │ │ │ └── UserRepositoryTest.php
│ │ │ │ └── fixture
│ │ │ │ ├── Controller
│ │ │ │ │ ├── HomeControllerTest.yaml
│ │ │ │ │ └── UserControllerTest.yaml
│ │ │ │ └── Repository
│ │ │ │ └── UserRepositoryTest.yaml
│ │ │ └── AppBundle.php
│ │ └── FooBundle
│ │ :
│ │ :
│ │ ├── Tests
│ │ │ ├── Controller
│ │ │ │ ├── BarControllerTest.php
│ │ │ │ └── BazControllerTest.php
│ │ │ ├── Repository
│ │ │ │ ├── BarRepositoryTest.php
│ │ │ │ └── BazRepositoryTest.php
│ │ │ └── fixture
│ │ │ ├── Controller
│ │ │ │ ├── BarControllerTest.yaml
│ │ │ │ └── BazControllerTest.yaml
│ │ │ └── Repository
│ │ │ ├── BarRepositoryTest.yaml
│ │ │ └── BazRepositoryTest.yaml
│ │ └── FooBundle.php
│ └── Kernel.php
└── tests
└── bootstrap.php
プロダクトコード同様、namespaceの変更が必要です。
続けて、 phpunit.xml.dist を修正します。
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="bin/.phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="7.5" />
</php>
<testsuites>
- <testsuite name="Project Test Suite">
- <directory>tests</directory>
- </testsuite>
+ <testsuite name="AppBundle">
+ <directory>src/Bundle/AppBundle/Tests</directory>
+ </testsuite>
+ <testsuite name="FooBundle">
+ <directory>src/Bundle/FooBundle/Tests</directory>
+ </testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
</phpunit>
liip/test-fixtures-bundle などを使っている場合は、フィクスチャ定義のYAMLファイル内でもエンティティのFQCNを修正する必要があるので注意しましょう。
- App\Entity\User:
+ App\Bundle\AppBundle\Entity\User:
user1:
email: user1@test.com
plainPassword: password
5. ルーティングを設定
各バンドルに Resources/config/routing.yaml を追加し、両方のファイルに以下の内容を記述します。
controllers:
resource: ../../Controller/
type: annotation
そして、 config/routes.yaml に以下を追記します。
app_bundle:
resource: '@AppBundle/Resources/config/routing.yaml'
foo_bundle:
resource: '@FooBundle/Resources/config/routing.yaml'
config/routes/annotations.yaml に以下のような内容が記述されている場合は、不要なので削除します。
- controllers:
- resource: ../../src/Controller/
- type: annotation
6. doctrine.yaml を修正
config/packages/doctrine.yaml でエンティティのマッピング設定を修正します。
doctrine:
orm:
mappings:
App:
is_bundle: false
type: annotation
- dir: '%kernel.project_dir%/src/Entity'
- prefix: 'App\Entity'
+ dir: '%kernel.project_dir%/src/Bundle/AppBundle/Entity'
+ prefix: 'App\Bundle\AppBundle\Entity'
alias: App
+ Foo:
+ is_bundle: false
+ type: annotation
+ dir: '%kernel.project_dir%/src/Bundle/FooBundle/Entity'
+ prefix: 'App\Bundle\FooBundle\Entity'
+ alias: Foo
7. security.yaml を修正
config/packages/security.yaml に User エンティティやGuardクラスのFQCNが書いてあるので、これも修正します。
security:
encoders:
- App\Entity\User:
+ App\Bundle\AppBundle\Entity\User:
algorithm: bcrypt
providers:
app_user_provider:
entity:
- class: App\Entity\User
+ class: App\Bundle\AppBundle\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
provider: app_user_provider
guard:
authenticators:
- - App\Security\LoginFormAuthenticator
+ - App\Bundle\AppBundle\Security\LoginFormAuthenticator
logout:
path: user_logout
target: user_login
switch_user: true
remember_me:
secret: '%kernel.secret%'
8. services.yaml を修正
config/services.yaml にもディレクトリ構造に依存した設定がいろいろあるはずなので、以下のような要領で修正します。
parameters:
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
#
# Controllers
#
- App\Controller\:
- resource: '../src/Controller'
- tags: ['controller.service_arguments']
+ App\Bundle\AppBundle\Controller\:
+ resource: '../src/Bundle/AppBundle/Controller'
+ tags: ['controller.service_arguments']
+
+ App\Bundle\FooBundle\Controller\:
+ resource: '../src/Bundle/FooBundle/Controller'
+ tags: ['controller.service_arguments']
#
# Entity listeners
#
- App\EntityListener\:
- resource: '../src/EntityListener'
- tags: ['doctrine.orm.entity_listener']
+ App\Bundle\AppBundle\EntityListener\:
+ resource: '../src/Bundle/AppBundle/EntityListener'
+ tags: ['doctrine.orm.entity_listener']
+
+ App\Bundle\FooBundle\EntityListener\:
+ resource: '../src/Bundle/FooBundle/EntityListener'
+ tags: ['doctrine.orm.entity_listener']
#
# Twig extensions
#
- App\Twig\AppExtension:
- tags: ['twig.extension']
+ App\Bundle\AppBundle\Twig\AppExtension:
+ tags: ['twig.extension']
まとめ
だいたいこんな流れでバンドル分割できると思います👍
ベースができていれば、機能の塊が増えるときにバンドルを追加するのは容易ですね。
というわけで、参考になれば幸いです。

Discussion