🎻

[Symfony] 既存のアプリを機能ごとにバンドルに分割したときにやったことまとめ

2020/08/06に公開

既存の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

extendsinclude などで他のテンプレートファイルを参照している箇所がある場合は、以下のようにそのテンプレートファイルがどのバンドルに属しているかを明記する必要があるので、該当箇所すべてを修正します。

- {% 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.yamlUser エンティティや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']

まとめ

だいたいこんな流れでバンドル分割できると思います👍

ベースができていれば、機能の塊が増えるときにバンドルを追加するのは容易ですね。

というわけで、参考になれば幸いです。

GitHubで編集を提案

Discussion