BooleanFilterType
チェックボックス
ChoiceFilterType
CollectionAdapterFilterType
DateFilterType
DateRangeFilterType
DateTimeFilterType
DateTimeRangeFilterType
DocumentFilterType
EmbeddedFilterTypeInterface
EntityFilterType
NumberFilterType
NumberRangeFilterType
SharedableFilterType
TextFilterType
名前から判断すると、独自のフィルターの作成や既存のフィルターの拡張を妨げるものは何もないが、このフィルターまたはそのフィルターが何をするのかはほぼ明確である必要があります。
ここでは、エンティティからテーブルを作成する方法については説明しません。
この記事は、この素晴らしいフレームワークを使用する作業の基本を少なくとも持っている人を対象としています。
TK
フィルター要件のある状況が可能な限り複雑な例を考えます。 さまざまなフィルターを使用してサイト上のイベントの検索フォームを作成し、可能な限り柔軟に行うという課題に直面します。
この場合、フィルタは次の基準に従ってデータベース内のデータを除外する必要があります。
1)このイベントが発生する期間。
2)関連するエンティティであるイベントのステータスとカテゴリ別。
3)別のエンティティとの一定数の関係について-システムに登録されている現在の参加者の最小数と最大数でイベントをフィルタリングできるスライダーを作成します。
4)最低価格と最高価格。
設置
必要な依存関係をダウンロードする
composer require lexik/form-filter-bundle
バンドルをアプリケーションに登録します。
$bundles = array( ... new Lexik\Bundle\FormFilterBundle\LexikFormFilterBundle(),
私たちの場合、MySQLデータベースなので、バンドルの最小構成は次のとおりです。
lexik_form_filter: listeners: doctrine_orm: true doctrine_dbal: false doctrine_mongodb: false
以上で、フィルターのフォームを作成するのにかなりの時間を費やさなければなりません。
実装
最初に、エンティティとデータベースを作成します。
イベントの本質(会議):
コード
namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Meet * * @ORM\Table(name="meet") * @ORM\Entity(repositoryClass="AppBundle\Repository\MeetRepository") */ class Meet { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string") */ private $title; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Category") */ private $category; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Status") */ private $status; /** * @ORM\Column(type="decimal") */ private $price = 0; /** * @ORM\Column(type="datetime") */ private $startDate; /** * @ORM\Column(type="datetime") */ private $endDate; /** * @ORM\OneToMany(targetEntity="AppBundle\Entity\Participant", mappedBy="meet") */ private $participants; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Constructor */ public function __construct() { $this->participants = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Set title * * @param string $title * * @return Meet */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } /** * Set price * * @param string $price * * @return Meet */ public function setPrice($price) { $this->price = $price; return $this; } /** * Get price * * @return string */ public function getPrice() { return $this->price; } /** * Set startDate * * @param \DateTime $startDate * * @return Meet */ public function setStartDate($startDate) { $this->startDate = $startDate; return $this; } /** * Get startDate * * @return \DateTime */ public function getStartDate() { return $this->startDate; } /** * Set endDate * * @param \DateTime $endDate * * @return Meet */ public function setEndDate($endDate) { $this->endDate = $endDate; return $this; } /** * Get endDate * * @return \DateTime */ public function getEndDate() { return $this->endDate; } /** * Set category * * @param \AppBundle\Entity\Category $category * * @return Meet */ public function setCategory(\AppBundle\Entity\Category $category = null) { $this->category = $category; return $this; } /** * Get category * * @return \AppBundle\Entity\Category */ public function getCategory() { return $this->category; } /** * Set status * * @param \AppBundle\Entity\Status $status * * @return Meet */ public function setStatus(\AppBundle\Entity\Status $status = null) { $this->status = $status; return $this; } /** * Get status * * @return \AppBundle\Entity\Status */ public function getStatus() { return $this->status; } /** * Add participant * * @param \AppBundle\Entity\Participant $participant * * @return Meet */ public function addParticipant(\AppBundle\Entity\Participant $participant) { $this->participants[] = $participant; return $this; } /** * Remove participant * * @param \AppBundle\Entity\Participant $participant */ public function removeParticipant(\AppBundle\Entity\Participant $participant) { $this->participants->removeElement($participant); } /** * Get participants * * @return \Doctrine\Common\Collections\Collection */ public function getParticipants() { return $this->participants; } }
イベントのステータスの本質(ステータス):
コード
namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Status * * @ORM\Table(name="status") * @ORM\Entity(repositoryClass="AppBundle\Repository\StatusRepository") */ class Status { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string") */ private $title; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set title * * @param string $title * * @return Status */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } }
イベントのカテゴリーの本質(カテゴリー):
コード
namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Category * * @ORM\Table(name="category") * @ORM\Entity(repositoryClass="AppBundle\Repository\CategoryRepository") */ class Category { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string") */ private $title; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set title * * @param string $title * * @return Category */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title * * @return string */ public function getTitle() { return $this->title; } }
ユーザーと多対多イベントを接続する中間エンティティ(ユーザーを参加者に変更)(参加者):
コード
ユーザー(ユーザー)の本質:
namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * Participant * * @ORM\Table(name="participant") * @ORM\Entity(repositoryClass="AppBundle\Repository\ParticipantRepository") */ class Participant { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="participants") */ private $user; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Meet", inversedBy="participants") */ private $meet; /** * Get id * * @return int */ public function getId() { return $this->id; } /** * Set user * * @param \AppBundle\Entity\User $user * * @return Participant */ public function setUser(\AppBundle\Entity\User $user = null) { $this->user = $user; return $this; } /** * Get user * * @return \AppBundle\Entity\User */ public function getUser() { return $this->user; } /** * Set meet * * @param \AppBundle\Entity\Meet $meet * * @return Participant */ public function setMeet(\AppBundle\Entity\Meet $meet = null) { $this->meet = $meet; return $this; } /** * Get meet * * @return \AppBundle\Entity\Meet */ public function getMeet() { return $this->meet; } }
ユーザー(ユーザー)の本質:
namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * User * * @ORM\Table(name="user") * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository") */ class User { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; }
さて、すでに動作可能な構造を作成しました。 これで、フォームのクラスの作成を開始できます。
namespace AppBundle\Filter; use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface; use AppBundle\Entity\Category; use AppBundle\Entity\Status; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Lexik\Bundle\FormFilterBundle\Filter\Form\Type as Filters; class MeetFilter extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->setMethod('GET'); $builder ->add('category', Filters\EntityFilterType::class, [ 'data_class' => Category::class, 'class' => Category::class ]) ->add('status', Filters\EntityFilterType::class, [ 'data_class' => Status::class, 'class' => Status::class ]) ->add('startDate', Filters\DateTimeRangeFilterType::class) ->add('participant_count', Filters\NumberRangeFilterType::class, [ 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { if (empty($values['value']['left_number'][0]) && empty($values['value']['right_number'][0])) { return null; } $start = !empty($values['value']['left_number'][0]) ? $values['value']['left_number'][0] : null; $end = !empty($values['value']['right_number'][0]) ? $values['value']['right_number'][0] : null; $paramName = sprintf('p_%s', str_replace('.', '_', $field)); $filterQuery->getQueryBuilder() ->leftJoin('meet.participants', 'pp') ->addSelect(sprintf('COUNT(pp) AS %s', $paramName)) ->addGroupBy('meet.id') ; if($start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d AND %s < %d', $paramName, $start, $paramName, $end)); } elseif($start && !$end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d', $paramName, $start)); } elseif(!$start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s < %d', $paramName, $end)); } } ]) ->add('price', Filters\NumberRangeFilterType::class); } public function getBlockPrefix() { return 'item_filter'; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'csrf_protection' => false, 'validation_groups' => array('filtering') // avoid NotBlank() constraint-related message )); } }
ここで最も興味深いのは、participantCountフィルターのオプションのapply_filterコールバックです。
このフィルターは、選択対象の参加者の最小数と最大数を設定し、そのデータはフォームから取得されます。 次に、リクエストに変更を加え、Doctrineにリクエストに参加者とのミーティング接続の数に関するデータを持たせることを伝えます:
$paramName = sprintf('p_%s', str_replace('.', '_', $field)); $filterQuery->getQueryBuilder() ->leftJoin('meet.participants', 'pp') ->addSelect(sprintf('COUNT(pp) AS %s', $paramName)) ->addGroupBy('meet.id') ;
また、次の条件のみを設定できます。
if($start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d AND %s < %d', $paramName, $start, $paramName, $end)); } elseif($start && !$end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s > %d', $paramName, $start)); } elseif(!$start && $end) { $filterQuery->getQueryBuilder() ->having(sprintf('%s < %d', $paramName, $end)); }
これは、状況に関連して、以下を満たさなければなりません。
1)両方のパラメーターが指定されている場合:条件の最大数と最小数-両方を使用します。
2)最小数の参加者のみが指定されている場合、指定された多数のイベントを探しています。
3)最大数の参加者のみが指定されている場合、指定された参加者よりも少ない数のイベントを探しています。
フォームは、Symfonyの他のフォームと同じ簡単な方法で作成されます。 新しいものから、ここではほとんどすべてのデータフィルタリングタスクを解決するのに役立つ新しいFieldTypeクラスのセットのみが表示されます。これらのタイプは記事の冒頭で説明しました。
次に、コントローラーを作成します。
public function indexAction(Request $request) { $repository = $this->getDoctrine() ->getRepository('AppBundle:Meet'); $form = $this->get('form.factory')->create(MeetFilter::class); if ($request->query->has($form->getName())) { $form->submit($request->query->get($form->getName())); $filterBuilder = $repository->createQueryBuilder('meet'); $this->get('lexik_form_filter.query_builder_updater')->addFilterConditions($form, $filterBuilder); $filterBuilder->join('meet.status', 's'); $query = $filterBuilder->getQuery(); $form = $this->get('form.factory')->create(MeetFilter::class); } else { $query = $repository->createQueryBuilder('meet') ->join('meet.status', 's') ->getQuery(); } $meets = $query->getResult(); }
ここではすべてが簡単です。フォームからデータが送信されている場合、addFilterConditionsを実行して、MeetリポジトリからフォームデータとQueryBuilderを通知します。
次に、バンドルサービスは、必要な条件をQueryBuilderに提供します。
その結果、フォームの標準表現は次のようになります。

すべてが美しいときは本当に大好きなので、ここに置いておきます:

このバンドルはQueryBuilderオブジェクトを補完するだけで、後でそれをどのように処理するかを決定できます。ページネーションに渡すか、独自のカスタム項目を追加します。
その結果、DQLは次のようになります。
SELECT meet, COUNT(pp) AS p_meet_participant_count FROM AppBundle\Entity\Meet meet LEFT JOIN meet.participants pp INNER JOIN meet.status s WHERE meet.category = :p_meet_category AND meet.status = :p_meet_status AND (meet.startDate <= '2017-01-31 00:00:00' AND meet.startDate >= '2017-01-01 00:00:00') AND (meet.price >= :p_meet_price_left AND meet.price <= :p_meet_price_right) GROUP BY meet.id HAVING p_meet_participant_count > 90 AND p_meet_participant_count < 256
→ Github