16

Doctrine 2 ORMを利用するSymfony 2.3プロジェクトに取り組んでいます。当然のことながら、機能は分割され、ほとんど独立したバンドルにグループ化され、他のプロジェクトでコードを再利用できるようになっています。

UserBundle と ContactInfoBundle があります。他のエンティティが連絡先情報を関連付けている可能性があるため、連絡先情報は分割されていますが、ユーザーが連絡先情報を必要としないシステムが構築される可能性があることは想像に難くありません。そのため、これら 2 つがハード リンクを共有しないことを強く望みます。

ただし、 User エンティティから ContactInfo エンティティへの関連付けマッピングを作成すると、バンドルが無効になるとすぐに、Doctrine は ContactInfo が登録された名前空間のいずれにも含まれていないというエラーをスローします。

私の調査により、これに対抗するはずのいくつかの戦略が明らかになりましたが、完全に機能しているようには見えません。

  1. Doctrine 2 の ResolveTargetEntityListener

    これは、インターフェイスが実行時に実際に置き換えられる限り機能します。バンドルの依存関係はオプションであると想定されているため、利用可能な具体的な実装がない (つまり、contactInfoBundle がロードされていない) 可能性が非常に高くなります。

    ターゲット エンティティがない場合、プレースホルダー オブジェクトはエンティティではない (そして /Entity 名前空間内にない) ため、構成全体がそれ自体に折りたたまれます。理論的には、実際には何もしないモック エンティティにそれらをリンクできます。しかし、このエンティティはその後、独自のテーブルを取得し (そして照会され)、まったく新しいワームの缶を開きます。

  2. 関係を逆にする

    ContactInfo の場合、User が所有側になるのが最も理にかなっています。2 つのバンドルのみが含まれている限り、ContactInfo を所有側にすることで、依存関係のオプション部分をうまく回避できます。ただし、3 番目の (オプションの) バンドルが ContactInfo との (オプションの) リンクを要求するとすぐに、ContactInfo を所有側にすると、3 番目のバンドルの ContactInfo からの強い依存関係が作成されます。

    ユーザーを所有する側を論理的にすることは、特定の状況です。ただし、エンティティ A に B が含まれ、C に B が含まれる場合、この問題は普遍的です。

  3. 単一テーブルの継承を使用する

    オプションのバンドルが新しく追加された関連付けと相互作用する唯一のものである限り、UserBundle\Entities\User を拡張する独自の User エンティティを各バンドルに与えることができます。ただし、単一のエンティティを急速に拡張する複数のバンドルがあると、これが少し混乱します。どの機能がどこで利用できるかを完全に確認することはできず、(Symfony 2 の DependencyInjection メカニズムでサポートされているように) バンドルのオン/オフに何らかの形でコントローラーを応答させることはほとんど不可能になります。

この問題を回避する方法についてのアイデアや洞察は大歓迎です。レンガの壁に数日ぶつかった後、私はアイデアがなくなりました。Symfony にはこれを行う何らかの方法があると思われますが、ドキュメントには ResolveTargetEntityListener しか記載されていませんが、これは最適ではありません。

4

2 に答える 2

8

私は最終的に、私のプロジェクトに適したこの問題の解決策を準備することができました. はじめに、私のアーキテクチャのバンドルは「星のように」配置されていると言わざるを得ません。つまり、基本依存モジュールとして機能し、すべてのプロジェクトに存在するコアまたは基本バンドルが 1 つあるということです。他のすべてのバンドルは、それのみに依存できます。他のバンドル間に直接的な依存関係はありません。アーキテクチャが単純であるため、この提案されたソリューションがこの場合に機能することは間違いありません。また、この方法にはデバッグの問題が含まれる可能性があるのではないかと心配していますが、たとえば、構成設定に応じて、簡単にオンまたはオフに切り替えることができるようにすることができます。

基本的な考え方は、関連するエンティティが見つからない場合にエンティティの関連付けをスキップする、独自の ResolveTargetEntityListener を作成することです。これにより、インターフェイスにバインドされたクラスが欠落している場合でも、実行プロセスを続行できます。おそらく、構成のタイプミスの影響を強調する必要はありません。クラスが見つからず、デバッグが困難なエラーが発生する可能性があります。そのため、開発段階でオフにしてから、本番環境でオンに戻すことをお勧めします。このようにして、可能性のあるすべてのエラーが Doctrine によって指摘されます。

実装

実装は、ResolveTargetEntityListener のコードを再利用し、remapAssociationメソッド内にいくつかの追加コードを配置することで構成されます。これが私の最終的な実装です:

<?php
namespace Name\MyBundle\Core;

use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;

class ResolveTargetEntityListener
{
    /**
     * @var array
     */
    private $resolveTargetEntities = array();

    /**
     * Add a target-entity class name to resolve to a new class name.
     *
     * @param string $originalEntity
     * @param string $newEntity
     * @param array $mapping
     * @return void
     */
    public function addResolveTargetEntity($originalEntity, $newEntity, array $mapping)
    {
        $mapping['targetEntity'] = ltrim($newEntity, "\\");
        $this->resolveTargetEntities[ltrim($originalEntity, "\\")] = $mapping;
    }

    /**
     * Process event and resolve new target entity names.
     *
     * @param LoadClassMetadataEventArgs $args
     * @return void
     */
    public function loadClassMetadata(LoadClassMetadataEventArgs $args)
    {
        $cm = $args->getClassMetadata();
        foreach ($cm->associationMappings as $mapping) {
            if (isset($this->resolveTargetEntities[$mapping['targetEntity']])) {
                $this->remapAssociation($cm, $mapping);
            }
        }
    }

    private function remapAssociation($classMetadata, $mapping)
    {
        $newMapping = $this->resolveTargetEntities[$mapping['targetEntity']];
        $newMapping = array_replace_recursive($mapping, $newMapping);
        $newMapping['fieldName'] = $mapping['fieldName'];

        unset($classMetadata->associationMappings[$mapping['fieldName']]);

        // Silently skip mapping the association if the related entity is missing
        if (class_exists($newMapping['targetEntity']) === false)
        {
            return;
        }

        switch ($mapping['type'])
        {
            case ClassMetadata::MANY_TO_MANY:
                $classMetadata->mapManyToMany($newMapping);
                break;
            case ClassMetadata::MANY_TO_ONE:
                $classMetadata->mapManyToOne($newMapping);
                break;
            case ClassMetadata::ONE_TO_MANY:
                $classMetadata->mapOneToMany($newMapping);
                break;
            case ClassMetadata::ONE_TO_ONE:
                $classMetadata->mapOneToOne($newMapping);
                break;
        }
    }
}

switchエンティティ関係をマップするために使用されるステートメントの前のサイレント リターンに注意してください。関連するエンティティのクラスが存在しない場合、メソッドはエラーのあるマッピングを実行してエラーを生成するのではなく、単に戻ります。これには、フィールドが欠落しているという意味もあります (多対多の関係でない場合)。その場合、外部キーはデータベース内で欠落するだけですが、エンティティ クラスに存在するため、すべてのコードは引き続き有効です (誤って外部キーのゲッターまたはセッターを呼び出しても、メソッドが見つからないというエラーは発生しません)。

活用する

このコードを使用できるようにするには、パラメーターを 1 つ変更するだけです。この更新されたパラメーターを、常にロードされるサービス ファイルまたは他の同様の場所に配置する必要があります。目標は、使用するバンドルに関係なく、常に使用される場所に置くことです。ベースバンドルサービスファイルに入れました:

doctrine.orm.listeners.resolve_target_entity.class: Name\MyBundle\Core\ResolveTargetEntityListener

これにより、元の ResolveTargetEntityListener があなたのバージョンにリダイレクトされます。念のため、キャッシュを配置した後は、キャッシュをクリアしてウォームアップする必要もあります。

テスト

このアプローチが期待どおりに機能することを証明したいくつかの簡単なテストのみを行いました。今後数週間、この方法を頻繁に使用するつもりであり、必要に応じてフォローアップします. また、試してみようと決心した他の人から有益なフィードバックを得たいと思っています。

于 2013-10-20T16:33:57.413 に答える