みなさんこんにちは、山本です。

Symfonyの3系もリリースされた中、今更ながら弊社でもSymfonyの2系を使い始めました。
1系は慣れているのですが、2系は初めてなので不慣れな分、当然戸惑うこともありましたが、概ね好意的な印象を持ちました。

が、それでも幾つか強い不満を覚えるものがありました。

例えば、1系の sfContext::getInstance()です。

2系ですと、Containerなのですが、これがどこでも呼べるものではなくなっていたりします。
これがなければ、configのパラメータですら満足に呼び出せず、非常に難儀してしまいます。

他にもModelにはBaseクラスがあったはずなのですが、2系ではBaseクラスが存在しなくなっていました。
その為、Entityクラスに自動生成のメソッドと追加したものとが混在し、非常に可読性を低くしているように思えました。

何より、一度生成後にスキーマのデフォルト値を変更しても反映されなかったり色々と不便でした。

その不満を、ちょこちょこ調整してきたのでその辺を書き連ねていこうかと思います。

さて、今回はその中でもBaseモデルを作るように拡張をしようと思います。

コマンド

通常Entitiyの生成はコマンドから作成します。

ですので、まずは簡単にコマンドを作るところから始めましょう。

バンドル直下に、Commandディレクトリを生成し、その中に***Command.phpのファイルを作成します。

vi src/CommonBundle/Command/GenerateEntitiesDoctrineCommand.php

<?php

namespace Application\CommonBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;

class GenerateEntitiesDoctrineCommand extends ContainerAwareCommand
{
  protected function configure()
  {
    $this
      ->setName('Foobar')
    ;
  }
}
$ app/console

Available commands:
  cc                                      Clears the cache
  Foobar                                  
  help                                    Displays help for a command
  list                                    Lists commands

指定したnameのコマンドが一覧に表示されました。

このルールに従って作成すると簡単にコマンドを実装することが出来ます。
とても簡単ですね。

次に、作ったコマンドを拡張してBaseEntityを生成出来るようにしてみます。

vi src/CommonBundle/Command/GenerateEntitiesDoctrineCommand.php

namespace CommonBundle\Command;

use Doctrine\Bundle\DoctrineBundle\Command\GenerateEntitiesDoctrineCommand as BaseGenerateEntitiesDoctrineCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\ORM\Tools\EntityRepositoryGenerator;
use Doctrine\Bundle\DoctrineBundle\Mapping\DisconnectedMetadataFactory;

class GenerateEntitiesDoctrineCommand extends BaseGenerateEntitiesDoctrineCommand
{
  protected function execute(InputInterface $input, OutputInterface $output)
  {
    $manager = new DisconnectedMetadataFactory($this->getContainer()->get('doctrine'));

    try {
      $bundle = $this->getApplication()->getKernel()->getBundle($input->getArgument('name'));

      $output->writeln(sprintf('Generating entities for bundle "<info>%s</info>"', $bundle->getName()));
      $metadata = $manager->getBundleMetadata($bundle);
    } catch (\InvalidArgumentException $e) {
      $name = strtr($input->getArgument('name'), '/', '\\');

      if (false !== $pos = strpos($name, ':')) {
        $name = $this->getContainer()->get('doctrine')->getAliasNamespace(substr($name, 0, $pos)).'\\'.substr($name, $pos + 1);
      }

      if (class_exists($name)) {
        $output->writeln(sprintf('Generating entity "<info>%s</info>"', $name));
        $metadata = $manager->getClassMetadata($name, $input->getOption('path'));
      } else {
        $output->writeln(sprintf('Generating entities for namespace "<info>%s</info>"', $name));
        $metadata = $manager->getNamespaceMetadata($name, $input->getOption('path'));
      }
    }

    $generator = $this->getEntityGenerator();

    $backupExisting = !$input->getOption('no-backup');
    $generator->setBackupExisting($backupExisting);

    $repoGenerator = new EntityRepositoryGenerator();
    foreach ($metadata->getMetadata() as $m) {
      if ($backupExisting) {
        $basename = substr($m->name, strrpos($m->name, '\\') + 1);
        $output->writeln(sprintf(' > backing up <comment>%s.php</comment> to <comment>%s.php~</comment>', $basename, $basename));
      }
      // Getting the metadata for the entity class once more to get the correct path if the namespace has multiple occurrences
      try {
        $entityMetadata = $manager->getClassMetadata($m->getName(), $input->getOption('path'));
      } catch (\RuntimeException $e) {
        // fall back to the bundle metadata when no entity class could be found
        $entityMetadata = $metadata;
      }

      $baseMeta = clone $m;

      $className = str_replace($baseMeta->namespace.'\\', '', $baseMeta->name);

      $baseMeta->name = sprintf('%s\\Base\\Base%s', $baseMeta->namespace, $className);
      $generator->setRegenerateEntityIfExists(true);
      $generator->setFieldVisibility($generator::FIELD_VISIBLE_PROTECTED);

      $output->writeln(sprintf('  > generating <comment>%s</comment>', $baseMeta->name));
      $generator->generate(array($baseMeta), $entityMetadata->getPath());

      require_once $entityMetadata->getPath() . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $baseMeta->name) . '.php';

      $m->fieldMappings       = [];
      $m->reflFields          = [];
      $m->associationMappings = [];

      $generator->setRegenerateEntityIfExists(false);
      $generator->setClassToExtend($baseMeta->name);
      $output->writeln(sprintf('  > generating <comment>%s</comment>', $m->name));
      $generator->generate(array($m), $entityMetadata->getPath());

      if ($m->customRepositoryClassName && false !== strpos($m->customRepositoryClassName, $metadata->getNamespace())) {
        $repoGenerator->writeEntityRepositoryClass($m->customRepositoryClassName, $metadata->getPath());
      }
    }
  }
}

CoreのGenerateEntitiesDoctrineCommandを継承して、executeをoverrideします。

BaseEntity用にEntityのメタを複製して、BaseEntityのname(namespace)を変更します。
nameを元にEntityファイルを生成するので、上記のソースでは以下の構成でファイルが生成されます。

Entity/Test.php
Entity/Base/BaseTest.php

そして中身が、Baseクラスを継承するようになります。

cat Entity/Test.php

namespace CommonBundle\Entity\Base;

/**
 * BaseTest
 */
class BaseTest
{
  /*****/
}

cat Entity/Base/BaseTest.php

namespace CommonBundle\Entity\Base;

/**
 * Test
 */
class Test extends \CommonBundle\Entity\Base\BaseTest
{
}

又、setRegenerateEntityIfExistsをtrueにしているので、Baseクラスは毎回新規に作り直しをするようになりました。

これで、スキーマを変更しても再生成しても反映されなかったり、自動生成のものと、自作のメソッドが混在することもなくなりました。

次回

・どこでも必要となるContainerの注入
・リスナー、フィルターの作り方
・Fixtureとダミーデータの作り方
・SessionをDBにする

等などと色々と候補があるのでその辺で記事を書こうかと思います。