SonataAdminBundleバンドルを使用して管理パネルにタグシステムを実装する

Symfony2で開発する場合、多くの人がSonataAdminBundle バンドルを使用します。 このバンドルを使用すると、DoctrineおよびMongoエンティティのCRUD管理パネルをすばやく作成できます。 特に、1対多関係や多対多関係など、エンティティを追加するためのページをすばやく簡単に作成できます。 ここで最後の段落で問題がありました。 この記事では、 FPNTagBundleバンドルを使用して1つの中間テーブルのみを使用して複数のエンティティのタグのインストールを整理する方法と、 SonataAdminでこのバンドルを機能させるために必要なことを示します。 最初に、単純なSonataAdminでエンティティの編集(タグを含む)を実装する方法を見てみましょう。



シンプルなタグの実装



現在のプロジェクトにはいくつかのエンティティがあり(このプロジェクトには7つありますが、記事とニュースと条件付きで呼んでいます)、タグを配置する機会を与える必要があり、さらに、1つのエンティティが複数のタグを設定できます。つまり、多対多の関係が実装されます。

まず、FPNTagBundleバンドルなしで管理パネルでタグを編集する方法を見てみましょう。 他の全員が継承する親エンティティを作成しました:

エンティティベースエンティティ
namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; //   ORM\Entity -             class Entity { /** * @var integer * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @var boolean * @ORM\Column(type="boolean", options={"default":false}) */ protected $published = false; /** * @var string * @ORM\Column(type="string", length=255) */ protected $title; /** * @var string * @ORM\Column(type="text") */ protected $content; //   /** * Get id * @return integer */ public function getId() { return $this->id; } /** * Set published * @param boolean $published * @return Entity */ public function setPublished($published) { $this->published = $published; return $this; } /** * Toggle published * @return Entity */ public function togglePublished() { $this->published = !$this->published; return $this; } /** * Get published * @return boolean */ public function getPublished() { return $this->published; } /** * Set title * @param string $title * @return Entity */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * @return string */ public function getTitle() { return $this->title; } /** * Set content * @param string $content * @return Entity */ public function setContent($content) { $this->content = $content; return $this; } /** * Get content * @return string */ public function getContent() { return $this->content; } }
      
      







2つの編集可能なエンティティ:

エンティティの記事
 namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; /** * @ORM\Table() * @ORM\Entity() */ class Article extends Entity { /** * @var ArrayCollection * @ORM\ManyToMany(targetEntity="Tag", inversedBy="articles") * @ORM\JoinTable(name="article_tags") */ protected $tags; /** * @return ArrayCollection */ public function getTags() { return $this->tags ?: $this->tags = new ArrayCollection(); } public function addTag(Tag $tag) { $tag->addArticle($this); $this->tags[] = $tag; } public function removeTag(Tag $tag) { return $this->tags->removeElement($tag); } }
      
      







エッセンスニュース
 namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; /** * @ORM\Table() * @ORM\Entity() */ class News extends Entity { /** * @var \DateTime * @ORM\Column(type="datetime", nullable=true) */ protected $publishedAt; /** * @var ArrayCollection * @ORM\ManyToMany(targetEntity="Tag", inversedBy="news") * @ORM\JoinTable(name="news_tags") */ protected $tags; /** * Set publishedAt * @param \DateTime $publishedAt * @return News */ public function setPublishedAt($publishedAt) { $this->publishedAt = $publishedAt; return $this; } /** * Get publishedAt * @return \DateTime */ public function getPublishedAt() { return $this->publishedAt; } /** * @return ArrayCollection */ public function getTags() { return $this->tags ?: $this->tags = new ArrayCollection(); } public function addTag(Tag $tag) { $tag->addArticle($this); $this->tags[] = $tag; } public function removeTag(Tag $tag) { return $this->tags->removeElement($tag); } }
      
      







そして、タグの本質:

タグエンティティ
 namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; /** * @ORM\Table() * @ORM\Entity() */ class Tag { public function __construct() { $this->articles = new ArrayCollection(); $this->news = new ArrayCollection(); } /** * @var integer $id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") * @ORM\Id */ protected $id; /** * @var string * @ORM\Column(type="string", length=100) */ protected $name; /** * @ORM\ManyToMany(targetEntity="Article", mappedBy="tags") */ private $articles; /** * @ORM\ManyToMany(targetEntity="News", mappedBy="tags") */ private $news; public function addArticle(Article $article) { $this->articles[] = $article; } public function addNews(News $news) { $this->news[] = $news; } public function getArticles() { $this->articles; } public function getNews() { $this->news; } /** * @return integer */ public function getId() { return $this->id; } /** * @param string $name * @return Tag */ public function setName($name) { $this->name = $name; return $this; } /** * @return string */ public function getName() { return $this->name; } }
      
      







ArticleとNewsの2つのエンティティは、多対多の関係のテーブルの名前のみが異なることがわかります。 また、現時点では重要ではないニュースに追加のフィールドが存在します。



Doctrineでは、注釈の数行のレベルで多対多の関係が非常に簡単に確立されます。 Doctrineで作業した人は、すでにこの単純さを見てきました。 この場合、中間テーブルが自動的に作成されます。 各エンティティに対してこのような関係を確立すると、Sonata管理パネルでエンティティのタグ付けを簡単に構成できます。

エンティティの基本管理者
 namespace App\AppBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Form\FormMapper; //      Admin,  Sonata       class EntityAdminBase extends Admin { protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('title', 'text') ->add('content', 'ckeditor') ->add('tags', 'entity', array( 'class'=>'AppBundle:Tag', 'multiple' => true, 'attr'=>array('style'=>'width: 100%;')) ) //  width: 100%      Select2-, //    ,      ; } protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('title') ->add('tags', null, array(), null, array('multiple' => true)) ; } protected function configureListFields(ListMapper $listMapper) { $listMapper ->addIdentifier('title') ->add('published') ; } }
      
      







管理者向け記事
 namespace App\AppBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Form\FormMapper; class ArticleAdmin extends EntityAdminBase { }
      
      







管理者ニュース
 namespace App\AppBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Form\FormMapper; class NewsAdmin extends EntityAdminBase { protected function configureFormFields(FormMapper $formMapper) { parent::configureFormFields($formMapper); $formMapper ->add('publishedAt', 'datetime') } protected function configureListFields(ListMapper $listMapper) { parent::configureListFields($listMapper); $listMapper ->add('publishedAt') ; } }
      
      







ご覧のとおり、エンティティに共通のフィールドは親クラスに配置され、特定のエンティティに固有のフィールドは各管理パネルに追加されます。 管理領域のサービスを登録するだけです。

管理サービスの設定
 # /src/App/AppBundle/Resources/config/admin.yml services: sonata.admin.article: class: App\AppBundle\Admin\ArticleAdmin tags: - { name: sonata.admin, manager_type: orm, group: "Content", label: "Articles" } arguments: - ~ - App\AppBundle\Entity\Article - ~ calls: - [ setTranslationDomain, [admin]] sonata.admin.news: class: App\AppBundle\Admin\NewsAdmin tags: - { name: sonata.admin, manager_type: orm, group: "Content", label: "News" } arguments: - ~ - App\AppBundle\Entity\News - ~ calls: - [ setTranslationDomain, [admin]] #         # /app/config/config.yml imports: - { resource: parameters.yml } - { resource: security.yml } - { resource: @AppBundle/Resources/config/admin.yml }
      
      







それだけです。ソナタは、記事やニュースのリストを編集するために必要なものをすべて自動的に作成します。



タグとエンティティの関係を単一のテーブルに保存する



そして、各エンティティについて、タグとの多対多の関係を整理するための個別のテーブルが作成されることに気付くまで、すべてが完全に機能しました。 (このようなエンティティが2つしかない場合は、おそらくこれでスチームバスを使用することはなかったでしょうが、この場合は7つの異なるテーブルを作成し、これらのテーブルの検索を整理したくありませんでした。)ソリューションについては、 FPNTagBundleバンドルを見つけました中間のタグ付けエンティティを導入することで、多対多の関係を2つの多対1および1対多の関係に分割します。 一般的に、このような分離はDoctrineExtentionsで実装され、バンドルはSymfonyへの統合を追加し、TagManagerクラスを実装します。 かなり明白なことを行う素晴らしいバンドルは、追加のResourceTypeフィールドを持つ1つのテーブルを作成することです-タグがバインドされるレコードのタイプ。 問題は、Sonataがそのような接続をサポートしていないことと、管理パネルも実装できないことです。



しかし、エンティティに加えられた変更を見てみましょう。

エンティティベースエンティティ
 namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; class Entity { //   //     //   -   ! protected $tags; public function getTags() { return $this->tags ?: $this->tags = new ArrayCollection(); } public function getTaggableType() { //        ( ) return substr(strrchr(get_class($this), "\\"), 1); } public function getTaggableId() { return $this->getId(); } }
      
      







エンティティの記事
 namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table() * @ORM\Entity() */ class Article extends Entity { }
      
      







エッセンスニュース
 namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table() * @ORM\Entity() */ class News extends Entity { /** * @var \DateTime * @ORM\Column(type="datetime", nullable=true) */ protected $publishedAt; /** * Set publishedAt * @param \DateTime $publishedAt * @return News */ public function setPublishedAt($publishedAt) { $this->publishedAt = $publishedAt; return $this; } /** * Get publishedAt * @return \DateTime */ public function getPublishedAt() { return $this->publishedAt; } }
      
      







変更されたタグエンティティ
 namespace App\AppBundle\Entity; use \Doctrine\ORM\Mapping as ORM; use \FPN\TagBundle\Entity\Tag as BaseTag; /** * @ORM\Table() * @ORM\Entity() */ class Tag extends BaseTag { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\OneToMany(targetEntity="Tagging", mappedBy="tag", fetch="EAGER") **/ protected $tagging; /** * @return integer */ public function getId() { return $this->id; } }
      
      







エンティティのタグ付け
 namespace App\AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\UniqueConstraint; use \FPN\TagBundle\Entity\Tagging as BaseTagging; /** * @ORM\Table(uniqueConstraints={@UniqueConstraint(name="tagging_idx", columns={"tag_id", "resource_type", "resource_id"})}) * @ORM\Entity */ class Tagging extends BaseTagging { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Tag", inversedBy="tagging") * @ORM\JoinColumn(name="tag_id", referencedColumnName="id") **/ protected $tag; }
      
      







タグは基本エンティティに配置され、エンティティ自体のクラスには余分なものは含まれません。



ソリューションを探してSonataAdminBundleコードを掘り始め、そのようなタグをどのように使用するかを彼女に教える方法を知り、まず保存フックをダイヤルし 、それらを破棄して、TagManagerを実装できる独自のタイプのフィールドを実装する方法を探し始めました。 しかし、習熟していないため、ややこしいコードがあります。 そして、編集ページの管理者の古いタグ設定では、タグのリストが引き続き表示され、保存すると、タグがエンティティの$ tagsプロパティに分類されることに気付きました。 確かに、ソナタはそれらをデータベースに保存しません(このプロパティにはドクトリンアノテーションがなく、存在する場合でもできません)が、エンティティタグのコレクションでタグを見つけることが、TagManagerの動作に必要なものです。 エンティティを変更するときにタグマネージャを実行することはそのままで、便利なのはフックの保存でした。



adminクラスでは、タグフィールドの説明を変更しませんでした。ソナタは保存時にタグをコレクションプロパティに配置します。 postPersistおよびpostUpdateフックの助けを借りて、データベースへのタグ接続の永続性が呼び出されます。

  /** * @return FPN\TagBundle\Entity\TagManager */ protected function getTagManager() { return $this->getConfigurationPool()->getContainer() ->get('fpn_tag.tag_manager'); } public function postPersist($object) { $this->getTagManager()->saveTagging($object); } public function postUpdate($object) { $this->getTagManager()->saveTagging($object); } public function preRemove($object) { $this->getTagManager()->deleteTagging($object); $this->getDoctrine()->getManager()->flush(); }
      
      





別の待ち伏せがあります-Sonataのバグです。これは、(リスト内の)バッチ削除でpreRemoveフックとpostRemoveフックが呼び出されないという事実につながります。 解決策は、標準のSonata CRUDコントローラーを拡張することです。

カスタムCRUDコントローラー
 namespace App\AppBundle\Controller; use Sonata\AdminBundle\Controller\CRUDController as Controller; use Symfony\Component\HttpFoundation\RedirectResponse; use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; class CRUDController extends Controller { public function publishAction() { $id = $this->get('request')->get($this->admin->getIdParameter()); $object = $this->admin->getObject($id); if (!$object) { throw new NotFoundHttpException(sprintf('unable to find the object with id : %s', $id)); } $object->togglePublished(); $this->admin->getModelManager()->update($object); $message = $object->getPublished() ? 'Publish successfully' : 'Unpublish successfully'; $trans = $this->get('translator.default'); $this->addFlash('sonata_flash_success', $trans->trans($message, array(), 'admin')); return new RedirectResponse($this->admin->generateUrl('list')); } public function batchActionDelete(ProxyQueryInterface $query) { if (method_exists($this->admin, 'preRemove')) { foreach ($query->getQuery()->iterate() as $object) { $this->admin->preRemove($object[0]); } } $response = parent::batchActionDelete($query); if (method_exists($this->admin, 'postRemove')) { foreach ($query->getQuery()->iterate() as $object) { $this->admin->postRemove($object[0]); } } return $response; } }
      
      







エンティティリストの公開ボタンのメソッドが同じコントローラーに追加されました。 このボタンには、Twigテンプレートと、adminクラスにconfigureListFields設定を追加する必要もあります。

リスト内のカスタムアクションテンプレート
 {# src/App/AppBundle/Resources/views/CRUD/list__action_publish.html.twig #} {% if object.published %} <a class="btn btn-sm btn-danger" href="{{ admin.generateObjectUrl('publish', object) }}"> {% trans from 'admin' %}Unpublish{% endtrans %} </a> {% else %} <a class="btn btn-sm btn-success" href="{{ admin.generateObjectUrl('publish', object) }}"> {% trans from 'admin' %}Publish{% endtrans %} </a> {% endif %}
      
      







リスト内のカスタムアクション設定
 protected function configureListFields(ListMapper $listMapper) { $listMapper //   ->add('_action', 'actions', array( 'actions' => array( 'Publish' => array( 'template' => 'AppBundle:CRUD:list__action_publish.html.twig' ) ) )) ; }
      
      







高度なコントローラーを有効にするには、その名前(AppBundle:CRUD)をサービス構成の3番目の引数として渡します。



次のタスクは、エンティティの編集時に既に割り当てられているタグを表示することです。 それは非常に簡単に解決されます-タイプエンティティのタグフィールドにタグのリストを渡す必要があります。 これは、まさにAdminExtension拡張機能で取り出すことができなかった部分です。

割り当てられたタグを表示
 protected function configureFormFields(FormMapper $formMapper) { $tags = $this->hasSubject() ? $this->getTagManager()->loadTagging($this->getSubject()) : array(); $formMapper //   ->add('tags', 'entity', array( 'class'=>'AppBundle:Tag', 'choices' => $tags, 'multiple' => true, 'attr'=>array('style'=>'width: 100%;')) ) ; }
      
      









おわりに



したがって、SonataAdminBundle管理パネルに便利なFPNTagBundleバンドルを実装し、1つの共通テーブル内のすべてのリンクの保存を実現し、Sonataの内部をよりよく調べることができました。



ボーナス-タグリクエスト



しばらく前、 コメントで、タグを操作するための一連のSQLクエリを含む記事投稿することを約束しました。 別の記事は始めませんでしたが、ここで紹介します。



与えられた:



目的:指定したタグを含むすべての記事とニュースを検索し、最初に指定した3つのタグすべてを含むエントリを表示し、次に入力したタグのうち少なくとも2つを含むエントリを表示し、最後に少なくとも1つのタグを含むエントリを表示します。



最初のクエリは、見つかったレコードのID(およびレコードのタイプ)を表示します

 SELECT resource_id, resource_type, count(*) as weight FROM Tagging WHERE tag_id IN (1,2,3) GROUP BY resource_id ORDER BY weight DESC
      
      





2番目のクエリは、見つかった記事をリストします。

 SELECT Article.id, Article.title FROM Tagging, Article WHERE Tagging.resource_id=Article.id AND Tagging.tag_id IN (1,2,3) GROUP BY Tagging.resource_id ORDER BY count(*) DESC
      
      





Habrauser Nashevは、タグを除くクエリオプションを提案しました。つまり、タグ(1、2、3)を含み、( 4、5、6 )を含まないすべてのレコードを表示します。

 SELECT resource_id, resource_type FROM Tagging WHERE tag_id IN (1,2,3) AND resource_id NOT IN (SELECT resource_id FROM Tagging WHERE tag_id IN (4,5,6)) GROUP BY resource_id ORDER BY count(*) DESC
      
      






All Articles