2017年9月23日土曜日

テストではbootKernel()でリフレッシュしないとpostLoad()が呼ばれない場合がある

イベントリスナーのpostLoadはEntityを取得した後で実行される。しかしユニットテストなどで、あるEntityをEntityManagerによって登録したあと、そのEntityManagerから再び同じEntityを取得すると、イベントが発生しない。その場合は、取得前に一旦、プロセスをリフレッシュすればイベントが発生するようになる。
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class PostLoadTest extends KernelTestCase
{
    /**
     * @var Doctrine\ORM\EntityManager
     */
    private $em;

    protected function setUp()
    {
        self::bootKernel();
        $this->em = static::$kernel->getContainer()
                                   ->get('doctrine')
                                   ->getManager();
    }
    public function testPostLoad()
    {
        $entity = new \Hyoujun\AppBundle\Entity\AnEntity();
        //...
        $this->em->persist($entity);
        $this->em->flush();
        
        self::bootKernel();//ここでプロセスをリフレッシュ。

        $loaded = $this->em->getRepository('HyoujunAppBundle:AnEntity')
                           ->find($entity->getId());
        //...(postLoadが呼ばれたかを検証)
    }
    protected function tearDown()
    {
        parent::tearDown();
        $this->em->close();
        $this->em = null;
    }
}
Symfony\Bundle\FrameworkBundle\Test\KernelTestCase::bootKernel()で、プロセスをリフレッシュすれば、postLoad()が呼ばれる。

2017年9月19日火曜日

postPersist内でpersist()とflush()を呼びたい時の無限再帰を止める方法

イベントリスナーのpostPersist内でEntityManagerがpersist()とflush()を呼ぶと、そのためにイベントリスナーのpostPersistが呼ばるため、無限の再帰となりOutOfMemoryException等が発生する。もし、イベントリスナー内で登録したいEntityクラスがあらゆる場所でそのイベントリスナーを呼ぶ必要がない場合には、無限の再帰を止める方法がある。
use Doctrine\ORM\Event\LifecycleEventArgs;

class EventListener
{
    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        if($entity instanceof Only\In\Listener\Entity) return;
        $entityManager = $args->getEntityManager();
        $entity = new Only\In\Listener\Entity();
        //...
        $entityManager->persist($entity);
        $entityManager->flush();
    }
}
イベントリスナー内で登録されるEntityクラスの登録がイベントの発生源となった場合に、instanceofで判定し、returnすれば良い。

2017年9月9日土曜日

FOSRestBundleの後、JMSSerializerBundleをセットアップしようとした時のエラー解決

Step 1: Setting up the bundleの通りFOSRestBundleをセットアップし、
次に、JMSSerializerBundleをインストールしようとした時に、エラー:
[InvalidArgumentException]
Neither a service called "jms_serializer.serializer" nor "serializer" is av
ailable and no serializer is explicitly configured. You must either enable
the JMSSerializerBundle, enable the FrameworkBundle serializer or configure
 a custom serializer. 
とりあえず、app/config/config.ymlの
#serializer: { enable_annotations: true }
のコメントアウトを外すとインストールできるようになる。

webディレクトリを任意の位置に動かした場合の設定

symfonyでwebディレクトリの名前を変え、かつ階層を下げた場合(web/をfoo/bar/へ)の変更点は以下の通り:
1. composer.jsonの最下方、symfony-web-dir:
"extra": {
    "symfony-app-dir": "app",
    "symfony-bin-dir": "bin",
    "symfony-var-dir": "var",
    "symfony-web-dir": "foo/bar",
    "symfony-tests-dir": "tests",
    "symfony-assets-install": "relative",
    "incenteev-parameters": {
        "file": "app/config/parameters.yml"
    },
    "branch-alias": null
}
2. web/app.php(foo/bar/app.php)の5〜7行目付近でautoload.phpとbootstrap.php.cacheへのパスに「../../」追加:
use Symfony\Component\HttpFoundation\Request;

require __DIR__.'/../../vendor/autoload.php';
if (PHP_VERSION_ID < 70000) {
    include_once __DIR__.'/../../var/bootstrap.php.cache';
}

$kernel = new AppKernel('prod', false);
//...
3. web/app_dev.php(foo/bar/app_dev.php)の21行目付近でautoload.phpへのパスに「../../」追加:
use Symfony\Component\Debug\Debug;
use Symfony\Component\HttpFoundation\Request;
//...
require __DIR__.'/../../vendor/autoload.php';
Debug::enable();

$kernel = new AppKernel('dev', true);
if (PHP_VERSION_ID < 70000) {
    $kernel->loadClassCache();
}
//...

varフォルダのパーミッションを設定する

symfonyは下記リンクの指示通りコマンドを実行しないと、動かない。
 3. Using ACL on a System that Supports setfacl (Linux/BSD)

要は、環境変数HTTPDUSERにサーバーのユーザ名リストを代入し(1行目)
varフォルダに対して、そのリストのユーザと自分(whoami)の権限を777に設定している(3,4行目)、
ということのようだ。

うまく行かない場合のnオプションをつけると、
$ HTTPDUSER=$(ps axo user,comm | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\  -f1)
$ sudo setfacl -ndR -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var
$ sudo setfacl -nR -m u:"$HTTPDUSER":rwX -m u:$(whoami):rwX var
となるようだ。

$ sudo chmod -R 777 var
でもいいかと思いきや、キャッシュを削除するたびにこのコマンドを打たなかればならなかったような記憶がある。

symfonyが動かない時はとりあえずキャッシュクリア

理由もなくコード更新後にsymfonyが動かない場合、キャッシュをクリアすれば大概動く。
ただし、各環境ごとにクリアする必要があるかも知れない。
開発環境:
$ php app/console cache:clear
製品環境:
$ php app/console cache:clear --env=prod

Bundle新規作成後のClassNotFoundExceptionに対してはcomposerを設定

Bundle新規作成後はcomposer.jsonのautoloadの設定を変更し、以下のコマンドを実行:
$ composer dumpautoload
さもなくばClassNotFoundExceptionが出る。

新しいBundle「Hyoujun\AppBundle」を作るとする:
$ php app/console generate:bundle
Are you planning on sharing this bundle across multiple applications? [no]: yes
Bundle namespace: Hyoujun\AppBundle
Bundle name [HyoujunAppBundle]:
Target Directory [src/]:
Configuration format (annotation, yml, xml, php) [xml]: annotation

その場合、composer.jsonの下記3行目を変更:
"autoload": {
    "psr-4": {
        "Hyoujun\\": "src/Hyoujun"
    },
    "classmap": [
        "app/AppKernel.php",
        "app/AppCache.php"
    ]
},
修正を反映させるため以下のコマンドを実行:
$ composer dumpautoload
これでClassNotFoundExceptionは出なくなる。