Sonataインポートバンドル

これまで、Symfonyの最高の管理パネルの1つはSonataAdminBundleです。これには正当な理由があります。 簡単なインストール、設定、すぐに使える多くの機能、大規模なコミュニティ。

それから欠けている唯一のものは、ファイルのインポートです。 同意、重要な機能。

ネットワークにはSonataの多くのインポート実装が含まれていますが、どこにも小さな欠陥があります-エンティティではなくテキストフィールドのみをインポートする機能はコレクションでは機能せず、1時間以上処理できる巨大なデータベースをロードするのに問題があります...

今日はかなり長い間首尾よく使用してきた実装を紹介したいと思いますが、今ではすべてを組み合わせて別のバンドルに配置することができました。

画像

ここでは、インストールおよび構成プロセス全体については説明しません。 さらに、多くの実装とは異なり、非常に簡単です。 これはすべてREADME.mdおよびgithub wikiで読むことができます。


この記事では、作成中に出会った興味深い点のみを説明します。

大量のデータ


このバンドルを実装するというアイデアは、かなり大きな地域、都市、町、広場、およびほぼすべてのテーブルを顧客のデータベース(約300万行)に転送する必要が生じたときに初めて思い付きました。 サーバーにアクセスできなかったことがすべて複雑になりました。
いくつかの既成のソリューションを試しましたが、それらはサーバーからの応答を待っている間にダウンロードできる小さなボリューム用に設計されていることに気付きました。

解決策


これは、Webサーバーではなく、php-cliを介して実装する必要がありました。 さいわい、Symfonyにはコンソールコマンドを操作するための非常に優れたツールがあります。
呼び出すには優れたApplicationクラスがあります。

$application = new Application(); // ... register commands $application->run(); 

ただし、この方法はWebサーバーを介して機能するため、機能しません。 コンソールで直接動作するため、 Symfony \ Component \ Process \ Processの 1つだけが残っています。 単純なコマンドを作成します(より美しく正確なソリューションを提供してくれたoxidmodに感謝します):

 $command = sprintf( '/usr/bin/php %s/console promoatlas:sonata:import %d "%s" "%s" > /dev/null 2>&1 &', $this->get('kernel')->getRootDir(), $fileEntity->getId(), $this->admin->getCode(), $fileEntity->getEncode() ? $fileEntity->getEncode() : 'utf8' ); 

非同期操作の最終行。 そして、すべてをバックグラウンドで実行します。

報告


何が起こっているのかを正確に理解せずに、1分以上待つのは難しいことに同意します。 そして、このプロセスが1時間続くとしたら? 二人?

そのため、何らかのコンソールコマンドログが必要です。 通常、ログにはテキストファイルを使用しますが、今回は情報量が多いため、データベースを使用することにしました。
エンティティは各行を担当します: Doctrs \ SonataImportBundle \ Entity \ ImportLog
各エントリはファイルの行に対応し、必要なものがすべて含まれています。


これらのデータから、ダウンロードプロセスを引き続き監視し、最終的な詳細レポートを表示します。

イテレータはファイルの解析に使用されるため、完了の割合を導出することはできません。 処理されたレコードの総数を表示するだけです。

間違い


残念ながら、FatalErrorをキャッチする方法を学んだことはありません。 したがって、この場合、たとえば、

 function setOwner(Owner $owner); $owner = $em->findOwner(); //  ,  null $entity->setOwner($owner); 

チームはFatalErrorに陥ります。

私が遭遇した別の例外はORMExceptionです。
彼の何がそんなに面白いの? 無効なデータを含むリクエストを処理しようとしたときの一般的な例外。

実際、これはまさにその目的です。ただし、そのような例外をスローした後、EntityManagerは接続を閉じ、データベースへのクエリ試行に応答します。
EntityManagerは閉じられています

私のバンドルでは、このような例外が2つのケースでスローされます。 1つ目は、エンティティ検証が正しく構成されていない場合です(エンティティはデータベースに追加される前に検証されます)

 $validator = $this->getContainer()->get('validator'); $errors = $validator->validate($entity); 

そして2番目はタイプ選択実体の分野でバンドルの仕事に関連しています 。 基本的に子エンティティがある場合(たとえば、本に著者がいます。著者はデータベースから選択されます)、本をインポートするときに、IDまたは名前を使用して著者を指定できます。 フィールドが数値でない場合、システムは名前フィールドでエンティティを見つけようとします。 エンティティにそのようなフィールドがない場合(たとえば、作成者の名前が名前ではなく、ログインまたはユーザー名に保存されている場合)、ORMExceptionが発生します。

原則として、非常に頻繁に発生するため、EntityManagerを再起動するために小さなハックを作成する必要がありました。これにより、例外をスローした後、システムがSTATUS_ERRORファイルを設定し、インターフェースにすべてを正常に表示できます。

 if (!$this->em->isOpen()) { $this->em = $this->em->create( $this->em->getConnection(), $this->em->getConfiguration() ); } 

インポート/エクスポートの構成


デフォルトでは、Sonataは単純なフィールド(テキスト、日付、数字)のみをエクスポートします。 ネストされたエンティティをエクスポートするには、getExportFieldsメソッドで明示的に設定する必要があります。 さらに、ネストされたエンティティは__toString()メソッドを構成する必要があります。 文字列としてのエンティティの表現がエクスポートされます。

また、ImportBundleはこのメソッドを使用して、新しくインポートされたファイルを変更せずにデータベースにロードできるようにします。 ファイルを再作成すると、列とフィールドの対応を含むテーブルがインポートページに表示されます。

拡張性


バンドル内のいくつかの行を変更するために、 easy-extendsを使用してアドインを実行する必要がある(それほど複雑ではないが、あまり便利ではない)という事実は、私は決して好きではありませんでした。
したがって、できることはすべて、configに入れます。 ファイルが解析されるクラスですら。 そのため、この場合、XMLとJSONおよびXLSの両方のロードをいつでも実装できます。

 doctrs_sonata_import: mappings: - { name: center_point, class: promaotlas.form_format.point} - { name: city_autocomplete, class: promoatlas.form_format.city_pa} upload_dir: %kernel.root_dir%/../web/uploads class_loader: Doctrs\SonataImportBundle\Loaders\CsvFileLoader encode: default: utf8 list: - cp1251 - utf8 - koir8 

wikiですべての構成オプションについて詳しく読んでください

カスタムフィールドタイプ


データベースに非標準フィールドがある場合(たとえば、私の場合、 center_pointはデータベースの座標です)、ファイルからのデータを処理し、それらが存在するフォームにそれらをもたらすクラスを宣言する必要がありますmysqlにあふれます。

たとえば、タイプcenter_pointは座標です(MySqlタイプはpoint )。 データベースに追加され、データベースから取得されると、 Pointクラスのオブジェクトになります。 Pointオブジェクトには__toStringメソッドがあります。

 public function __toString(){ retrun $this->x . ', ' . $this->y; } 

それを使用して、インポートが行われ、インポートファイルで美しい座標を取得します。 データベースに同じx、yを入力しようとすると、ORMExceptionが待機します。 これがまさにマッピング配列の目的です。 この場合、 Doctrs \ SonataImportBundle \ Service \ ImportAbstractインターフェースを実装するid doctrs.form_format.pointのサービスを取得し 、受け取った値に基づいて、データベースに入力できる目的のタイプを返します。

これがサービス自体のコードです
 class Point implements ImportAbstract { public function getFormatValue($value){ $value = explode(',', $value); $point = new \PHPOpenGIS\MainBundle\Geometry\Point($value[0], $value[1] ?? 0); return $point; } } 

サービスコードdoctrs.form_format.city_pa

 class CityPa implements ImportAbstract, ContainerAwareInterface { private $container; public function setContainer(ContainerInterface $container = null) { $this->container = $container; } public function getFormatValue($value){ /** @var ContainerInterface $container */ $container = $this->container; $city = $container->get('promoatlas.city_autocomplete')->byName($value); return $city; } } 

ご覧のように、mappingsパラメーターではクラス名ではなくidサービスを指定しているため、アクションの自由が与えられます。 たとえば、city_autocomplete型を変換するには、コンテナーが必要でした。

おわりに


このバンドルを6か月間使用しました(その時点ではまだ発行されていなかったため、ビットバケツでプルしました)。 もちろん、重大でないエラーがいくつかありましたが、 packagist.orgに登録した後、質問や不明瞭なエラーメッセージがないようにすべてを修正しようとします。

このバンドルを改善するための小さな計画がありますが、彼らの手が彼らに届くかどうか見てみましょう。

私はどんなコメントや発言にも喜んでいるでしょう。

Source: https://habr.com/ru/post/J338986/


All Articles