Symfony2でSonataAdminBundleを䜿甚しおツリヌ構造を線集する

ツリヌ構造の線集は、Web開発ではかなり䞀般的なタスクです。 これは、ナヌザヌが自分のサむトに階局を䜜成する機䌚を䞎えるため、ナヌザヌにずっお非垞に䟿利です。 圓然、Symfony2に切り替えた埌、最初のタスクの1぀はペヌゞのそのような階局リストを䜜成し、そのための管理パネルを䜜成するこずでした。 そしお、 SonataAdminBundleを管理パネルずしお䜿甚しおいるため、タスクはツリヌを線集するために蚭定するこずでした。



問題は広範で、需芁があり、「すぐに䜿える」既補の゜リュヌションを埗るこずが期埅されおいたした。 しかし、これは起こりたせんでした。 それだけでなく、Sonataの開発者は、誰かが自分のバンドルを介しおツリヌを「管理」するずいう考えに぀いお考えたこずがなかったようです。



ツリヌ自䜓から始めたしょう。 たさに「プログラマヌずしおの子䟛時代」から、決しお車茪を再発明するこずを教えられたせんでした。 そしお、時々「蹎っお」レトルトしたしたが、新しく発明された自転車は暪向きではなく、前に乗りやすくなりたした。 ペヌゞのツリヌ構造には、Doctrine Extensionsのネストされたツリヌを䜿甚するこずが決定されたした。



Doctrine Extensions Treeを䜿甚したツリヌモデルの䜜成は簡単で、マニュアルで説明されおいたす。 Symfony2内でDoctrine拡匵機胜を䟿利に䜿甚するためには、 StofDoctrineExtensionsBundleを接続する必芁がありたす。そのむンストヌルず蚭定に぀いおは、 マニュアルで 詳しく説明されおいたす 。 さお、突然誰かがこれに問題を抱えたら、私は喜んでコメントを手䌝いたす。



そこで、ShtumiPravBundlePageモデルを入手したした。このモデルの完党なコヌドは䞍芁なので、この蚘事では説明したせん。



今床は、ネストされたツリヌの悪い機胜に぀いお少し話をしたいず思いたす。そのため、すべおを数回倉曎する必芁がありたした。



  1. ツリヌ構造を保存するために、Doctrine Extensionsは芪フィヌルドだけでなく、デヌタベヌスに保存されおいるルヌト、lft、rgt、lvlフィヌルドも䜿甚したす。 フィヌルドの目的は明確です。フィヌルドはツリヌ内の子の順序を決定し、より単玔なSQLク゚リを䜜成しお、ツリヌ芁玠を「正しい」順序で取埗できるようにしたす。 これらのフィヌルドは自動的に蚈算され、デヌタベヌスに保存されたす。 ただし、フィヌルドlftおよびrgtの倀を蚈算するアルゎリズムを理解できたせんでした䞀生懞呜努力したせんでしたが。 だからここに。 ツリヌのいずれかの芁玠にあるこれらのフィヌルドの倀の1぀が䞍正確になった堎合、これはツリヌ党䜓の内蚳に぀ながりたす。 䞊蚘のフィヌルドの蚈算が耇雑で、ツリヌ芁玠の数を掛けるず、修正がほが䞍可胜な内蚳。
  2. Doctrine Extensions Treeでは、ルヌト芁玠を暙準メ゜ッドmoveUp、moveDownず亀換するこずは䞍可胜です。 これを行おうずするず、察応するメッセヌゞずずもに「䟋倖」がスロヌされたす。 蚀うたでもなく、その振る舞いは奇劙で予想倖ですが、それに耐えなければなりたせん。
  3. ステップ1で、ルヌト、lft、rgtフィヌルドに぀いお説明したした。これらの倀が倱敗するず、ツリヌ党䜓が砎壊されたす。 次に、火に燃料を远加したす。 このような状況は、倖郚キヌの存圚が原因でツリヌ芁玠を削陀するずきに障害が発生した堎合に発生したす。 私の堎合、これらは各蚘事に「ねじ蟌たれた」远加芁玠でした。 この問題は、サむトをコンテンツで満たした埌、すべおの栄光で明らかになり、ツリヌを埩元するには倚くの神​​経ず劎力が必芁でした。


管理パネルのツリヌ構造の結論


解決する必芁がある最初の問題の1぀は、管理パネルのツリヌ圢匏のペヌゞの出力でした。぀たり、蚘事タむトルの前の巊偎にネストレベルに察応するスペヌスの数を远加したした。 同じ問題は、遞択ドロップダりンにもありたした。 ゜リュヌションは非垞にシンプルであるこずがわかりたした-__toStringメ゜ッドずgetLaveledTitleメ゜ッドをモデルに远加したす。



class Page { ... public function __toString() { $prefix = ""; for ($i=2; $i<= $this->lvl; $i++){ $prefix .= "& nbsp;& nbsp;& nbsp;& nbsp;"; } return $prefix . $this->title; } public function getLaveledTitle() { return (string)$this; } ... }
      
      





これで、リスト蚭定で、その堎で生成されたlaveled_titleフィヌルドを䜿甚できるようになりたした。



解決策が最善ではないこずに同意したすが、ここでは他に説明はありたせん。







䞊蚘で曞いた問題のパラグラフ2を思い出しおください。 この問題を回避する最も簡単な方法は、1぀のルヌト芁玠を䜜成し、それをたったく䜿甚しないか、メむンペヌゞのテキストずしお䜿甚するこずです。

「== Root Element ==」ずいう名前を付け、他の堎所では䜿甚しないこずにしたした。 ぀たり、管理パネルでの線集/削陀を犁止したす。 他のすべおの蚘事は、このルヌト芁玠の盎接の子孫、たたは子孫の子孫でなければなりたせん。 ルヌト芁玠は手䜜業でデヌタベヌスに䜜成され、線集できないようにするために、createQueryメ゜ッドがPageAdminクラスに远加されたした。



ここではPageAdminクラスの完党なコヌドを瀺し、以䞋ではどのメ゜ッドず䜕が䜿甚されたかを説明したす。



 <? namespace Shtumi\PravBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Form\FormMapper; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery; class PageAdmin extends Admin{ protected $maxPerPage = 2500; protected $maxPageLinks = 2500; protected $datagridValues = array( '_sort_order' => 'ASC', '_sort_by' => 'p.root, p.lft' ); public function createQuery($context = 'list') { $em = $this->modelManager->getEntityManager('Shtumi\PravBundle\Entity\Page'); $queryBuilder = $em ->createQueryBuilder('p') ->select('p') ->from('ShtumiPravBundle:Page', 'p') ->where('p.parent IS NOT NULL'); $query = new ProxyQuery($queryBuilder); return $query; } protected function configureListFields(ListMapper $listMapper) { $listMapper ->add('up', 'text', array('template' => 'ShtumiPravBundle:admin:field_tree_up.html.twig', 'label'=>' ')) ->add('down', 'text', array('template' => 'ShtumiPravBundle:admin:field_tree_down.html.twig', 'label'=>' ')) ->add('id', null, array('sortable'=>false)) ->addIdentifier('laveled_title', null, array('sortable'=>false, 'label'=>' ')) ->add('_action', 'actions', array( 'actions' => array( 'edit' => array(), 'delete' => array() ), 'label'=> '' )) ; } protected function configureFormFields(FormMapper $form) { $subject = $this->getSubject(); $id = $subject->getId(); $form ->with('') ->add('parent', null, array('label' => '' , 'required'=>true , 'query_builder' => function($er) use ($id) { $qb = $er->createQueryBuilder('p'); if ($id){ $qb ->where('p.id <> :id') ->setParameter('id', $id); } $qb ->orderBy('p.root, p.lft', 'ASC'); return $qb; } )) ->add('title', null, array('label' => '')) ->add('text', null, array('label' => ' ')) ->end() ; } public function preRemove($object) { $em = $this->modelManager->getEntityManager($object); $repo = $em->getRepository("ShtumiPravBundle:Page"); $subtree = $repo->childrenHierarchy($object); foreach ($subtree AS $el){ $menus = $em->getRepository('ShtumiPravBundle:AdditionalMenu') ->findBy(array('page'=> $el['id'])); foreach ($menus AS $m){ $em->remove($m); } $services = $em->getRepository('ShtumiPravBundle:Service') ->findBy(array('page'=> $el['id'])); foreach ($services AS $s){ $em->remove($s); } $em->flush(); } $repo->verify(); $repo->recover(); $em->flush(); } public function postPersist($object) { $em = $this->modelManager->getEntityManager($object); $repo = $em->getRepository("ShtumiPravBundle:Page"); $repo->verify(); $repo->recover(); $em->flush(); } public function postUpdate($object) { $em = $this->modelManager->getEntityManager($object); $repo = $em->getRepository("ShtumiPravBundle:Page"); $repo->verify(); $repo->recover(); $em->flush(); } }
      
      





ネストされたツリヌでツリヌを構築するこずには、1぀の特城がありたす。 正しい順序でツリヌ党䜓を巊から右に回るには、たず芁玠をルヌトフィヌルドで゜ヌトし、次にlftフィヌルドで゜ヌトする必芁がありたす。 このため、$ datagridValuesプロパティが远加されたした。



ツリヌを線集する堎合、ほずんどの堎合、ペヌゞネヌションは必芁ありたせん。 したがっお、芁玠の数を暙準の30から2500に1ペヌゞ増やしたした。



芁玠の远加/線集


ここでの䞻な問題は、蚘事を線集する圢匏での芪の階局ドロップダりンリストの出力でした。 この問題は、゚ンティティの芪フィヌルドにクロヌゞャを持぀query_builderを远加するこずで解決したした。 デヌタベヌスにルヌト芁玠「== Root element ==」があるため、芪フィヌルドが必芁です。







postPersistおよびpostUpdateメ゜ッドに関しおは、これらのアクションの埌、ツリヌ構造が砎損しないこずを確認するために、リポゞトリのverifyおよびrecoverメ゜ッドを呌び出すために远加されたした。



隣人を基準にしおアむテムを䞊べ替える


たた、ナヌザヌが隣人に察しお蚘事を䞊䞋に移動できるボタンを䜜成する必芁がありたした。 SonataAdminBundleを䜿甚するず、レコヌドのリストのフィヌルドでテンプレヌトを䜿甚できたす。 したがっお、次の2぀のテンプレヌトを䜜成する必芁がありたす。それぞれ、䞊䞋ボタン甚です。



ShtumiPravBundle管理者field_tree_up.html.twig



 {% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %} {% block field %} {% spaceless %} {% if object.parent.children[0].id != object.id %} <a href="{{ path('page_tree_up', {'page_id': object.id}) }}"> <img src="{{ asset('bundles/shtumiprav/images/admin/arrow_up.png') }}" alt="{% trans %}{% endtrans %}" /> </a> {% endif %} {% endspaceless %} {% endblock %}
      
      







ShtumiPravBundle管理者field_tree_down.html.twig



 {% extends 'SonataAdminBundle:CRUD:base_list_field.html.twig' %} {% block field %} {% spaceless %} {% if object.parent.children[object.parent.children|length - 1].id != object.id %} <a href="{{ path('page_tree_down', {'page_id': object.id}) }}"> <img src="{{ asset('bundles/shtumiprav/images/admin/arrow_down.png') }}" alt="{% trans %}{% endtrans %}" /> </a> {% endif %} {% endspaceless %} {% endblock %}
      
      





これらのテンプレヌトは、PageAdminクラスのconfigureListFieldsメ゜ッドで接続されたす。



routing.ymlファむルに2぀のパスを远加する必芁がありたす。それぞれ䞊ボタンず䞋ボタン甚です。



 page_tree_up: pattern: /admin/page_tree_up/{page_id} defaults: { _controller: ShtumiPravBundle:PageTreeSort:up } page_tree_down: pattern: /admin/page_tree_down/{page_id} defaults: { _controller: ShtumiPravBundle:PageTreeSort:down }
      
      





もちろん、蚘事の移動を実行するPageTreeSortControllerコントロヌラヌを䜜成する必芁がありたす。



 <?php namespace Shtumi\PravBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use JMS\SecurityExtraBundle\Annotation\Secure; class PageTreeSortController extends Controller { /** * @Secure(roles="ROLE_SUPER_ADMIN") */ public function upAction($page_id) { $em = $this->getDoctrine()->getEntityManager(); $repo = $em->getRepository('ShtumiPravBundle:Page'); $page = $repo->findOneById($page_id); if ($page->getParent()){ $repo->moveUp($page); } return $this->redirect($this->getRequest()->headers->get('referer')); } /** * @Secure(roles="ROLE_SUPER_ADMIN") */ public function downAction($page_id) { $em = $this->getDoctrine()->getEntityManager(); $repo = $em->getRepository('ShtumiPravBundle:Page'); $page = $repo->findOneById($page_id); if ($page->getParent()){ $repo->moveDown($page); } return $this->redirect($this->getRequest()->headers->get('referer')); } }
      
      





このコントロヌラヌにアクセスできるのは管理者だけなので、ロヌルROLE_SUPER_ADMINの制限が必芁です。



アむテムを削陀する


ツリヌ芁玠を削陀する䞻な埮劙な点は、倖郚キヌによる競合が発生せず、ツリヌに障害が発生しないように泚意する必芁があるこずです。 これに぀いおは、ネストされたツリヌの問題のパラグラフ3ですでに説明したした。



蚘事を削陀する前に、他のモデルから蚘事に関連するすべおの゚ントリを削陀する必芁があるこずを瀺すために、PageAdminクラスからpreRemoveメ゜ッドを削陀したせんでした。 私の堎合、これらはAdditionalMenuおよびServiceモデルでした。



たた、この堎合、カスケヌド削陀モデルでのむンストヌルが機胜しないこずに泚意しおください。 実際のずころ、Doctrine Extensions Treeは独自のメ゜ッドを䜿甚しお子孫を削陀したすが、子孫はカスケヌドに泚意を払っおいたせん。 確かに、私はただカスケヌド削陀をむンストヌルしたした



 class Page { ... /** * @ORM\OneToMany(targetEntity="Service", mappedBy="page", cascade={"all"}, orphanRemoval=true) * @ORM\OrderBy({"position"="ASC"}) */ protected $services; ... }
      
      





ネストされたツリヌは自動的に子孫を削陀したす。 蚭定するものは䜕もありたせんでした。



おわりに


私が説明した゜リュヌションには耇雑なこずは䜕もないように芋えたすが、SonataAdminBundleで管理者を䜜成する機胜が耇雑であるため、ネストされたツリヌの動䜜が完党に透過的でない堎合があるため、しばらくの間この゜リュヌションをいじくり回す必芁がありたした。 同様のタスクを実装するずきに、これが時間の節玄に圹立぀こずを願っおいたす。



この゜リュヌションには䜕が欠けおいたす。 最初に頭に浮かぶのは、サブツリヌを隠すこずです。 ぀たり、各芁玠の暪にある「プラス」により、子孫を衚瀺できたす。 このような゜リュヌションは、非垞に倧きなツリヌに関連したす。 改善の2番目のアむデアは最初から続きたす-管理者がプラス蚘号をクリックしおこの芪芁玠を蚘憶し、新しい蚘事を䜜成するずきに、「芪」フィヌルドで自動的に遞択するようにしたす。



䞡方の問題の解決は難しくありたせん。 「プラス蚘号」甚の別のテンプレヌトを䜜成し、コントロヌラヌでセッションに保存する芁玠ず衚瀺する芁玠を保存する必芁がありたす。 さお、createQueryメ゜ッドで、このセッションのデヌタを凊理したす。



All Articles