EC-Cube4で商品検索でタグも検索条件に追加したいときどうすれば出来るのかいろいろ試してみました。
そもそも商品検索のクエリにdtb_product_tagが含まれていないので、WhereCustomizerではなくQueryCustomizerで下記のように作りました。
namespace Customize\Repository;
use Doctrine\ORM\QueryBuilder;
use Eccube\Doctrine\Query\QueryCustomizer;
use Eccube\Repository\QueryKey;
class TagSearchQueryCustomizer implements QueryCustomizer {
protected $tag;
public function __construct(TagRepository $tagRepository){
$this->tag = $tagRepository;
}
public function getQueryKey() {
return QueryKey::PRODUCT_SEARCH;
}
public function customize(QueryBuilder $builder, $params, $queryKey){
if ($params['name']) {
$builder->innerJoin("p.ProductTag", "pt");
$tags = $this->tag->createQueryBuilder('t')
->where("NORMALIZE( t.name ) LIKE NORMALIZE( :name )")
->setParameter("name", '%' . $params['name'] . '%')
->getQuery()->getResult();
foreach ($tags as $tag) {
$builder->orWhere("pt.Tag = :TagId");
$builder->setParameter("TagId", $tag->getId());
}
}
}
}
タグレポジトリからLIKE検索した結果のタグIDをキーに検索してくれるのですが、これだと下記のようなクエリになってしまい、表示属性や廃止属性を無視してしまいます。
FROM
dtb_product d0_
INNER JOIN
dtb_product_class d1_
ON d0_.id = d1_.product_id
AND d1_.discriminator_type IN('productclass')
INNER JOIN
dtb_product_tag d2_
ON d0_.id = d2_.product_id
AND d2_.discriminator_type IN('producttag')
WHERE
(
(
d0_.product_status_id = 1
AND (
CONVERT(d0_.name USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
OR CONVERT(d0_.search_word USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
OR EXISTS(
SELECT
d3_.id
FROM
dtb_product_class d3_
WHERE
(
d0_.id = d3_.product_id
AND CONVERT(d3_.product_code USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
)
AND d3_.discriminator_type IN('productclass')
)
)
AND d1_.visible = 1
)
OR d2_.tag_id = ?
)
AND d0_.discriminator_type IN('product')
search_wordやnameをみているグルーピングの中にORでタグ条件を入れたいのですが、QueryCustomizerだと入れ込めませんでした。。
次にWhereCustomizerならsearch_wordやnameの部分に入れ込めるか試してみました。
<?php
namespace Customize\Repository;
use Eccube\Doctrine\Query\WhereClause;
use Eccube\Doctrine\Query\WhereCustomizer;
use Eccube\Repository\QueryKey;
use Eccube\Repository\TagRepository;
class TagSearchWhereCustomizer extends WhereCustomizer {
protected $tag;
public function __construct(TagRepository $tagRepository) {
$this->tag = $tagRepository;
}
public function getQueryKey() {
return QueryKey::PRODUCT_SEARCH;
}
protected function createStatements($params, $queryKey) {
$whereCluse = [];
if ($params['name']) {
$tags = $this->tag->createQueryBuilder('t')
->where("NORMALIZE( t.name ) LIKE NORMALIZE( :name )")
->setParameter("name", '%' . $params['name'] . '%')
->getQuery()->getResult();
// foreach ($tags as $tag) {
// // $whereCluse[] = WhereClause::eq("pt.Tag", ':Tag', $tag->getId());
// }
}
return [
WhereClause::isNull('p.note'),
];
// return $whereCluse;
}
}
一旦テストとして、dtb_productのノート属性を検索条件にしてcreateStatementsの戻り値がクエリのどこに追加されるのか確認してみました。
INNER JOIN
dtb_product_tag d2_
ON d0_.id = d2_.product_id
AND d2_.discriminator_type IN('producttag')
WHERE
(
d0_.product_status_id = 1
AND (
CONVERT(d0_.name USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
OR CONVERT(d0_.search_word USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
OR EXISTS(
SELECT
d3_.id
FROM
dtb_product_class d3_
WHERE
(
d0_.id = d3_.product_id
AND CONVERT(d3_.product_code USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
)
AND d3_.discriminator_type IN('productclass')
)
)
AND d1_.visible = 1
AND d0_.note IS NULL
WhereCustomizerでも思った部分にはクエリは追加されませんでした。QueryCustomizerでinnerJoinして、WhereCustomizerで条件追加という作戦は失敗のようです。
ここに来てやっとProductRepositoryのソースをみてみると、getQueryBuilderBySearchDataの中でandWhereで追加したいグループが追加された後、customizeが呼ばれているので、自分が入れ込みたい場所にいれるにはProductRepositoryをいじる必要があるのかもしれない。。
public function getQueryBuilderBySearchData($searchData)
{
$qb = $this->createQueryBuilder('p')
->andWhere('p.Status = 1');
// category
$categoryJoin = false;
if (!empty($searchData['category_id']) && $searchData['category_id']) {
$Categories = $searchData['category_id']->getSelfAndDescendants();
if ($Categories) {
$qb
->innerJoin('p.ProductCategories', 'pct')
->innerJoin('pct.Category', 'c')
->andWhere($qb->expr()->in('pct.Category', ':Categories'))
->setParameter('Categories', $Categories);
$categoryJoin = true;
}
}
// name
if (isset($searchData['name']) && StringUtil::isNotBlank($searchData['name'])) {
$keywords = preg_split('/[\s ]+/u', str_replace(['%', '_'], ['\\%', '\\_'], $searchData['name']), -1, PREG_SPLIT_NO_EMPTY);
foreach ($keywords as $index => $keyword) {
$key = sprintf('keyword%s', $index);
$qb
->andWhere(sprintf('NORMALIZE(p.name) LIKE NORMALIZE(:%s) OR
NORMALIZE(p.search_word) LIKE NORMALIZE(:%s) OR
EXISTS (SELECT wpc%d FROM \Eccube\Entity\ProductClass wpc%d WHERE p = wpc%d.Product AND NORMALIZE(wpc%d.code) LIKE NORMALIZE(:%s))',
$key, $key, $index, $index, $index, $index, $key))
->setParameter($key, '%'.$keyword.'%');
}
}
並び替えなど。。。
return $this->queries->customize(QueryKey::PRODUCT_SEARCH, $qb, $searchData);
}
ECCUBEの作法というかいろいろわかってないけど、ProductRepository直接編集ってNGな気がするけど、どうするのが正しいんだろうか。。
もしProductRepositoryを修正していいならこんな感じでできたけど、proxyとかでやるのかな?
foreach ($keywords as $index => $keyword) {
$key = sprintf('keyword%s', $index);
$qb
->andWhere(sprintf('NORMALIZE(p.name) LIKE NORMALIZE(:%s) OR
NORMALIZE(p.search_word) LIKE NORMALIZE(:%s) OR
EXISTS (SELECT wpc%d FROM \Eccube\Entity\ProductClass wpc%d WHERE p = wpc%d.Product AND NORMALIZE(wpc%d.code) LIKE NORMALIZE(:%s)) OR
EXISTS (
SELECT eept%d FROM \Eccube\Entity\ProductTag eept%d
WHERE p = eept%d.Product
AND eept%d.Tag IN (
SELECT eet%d FROM \Eccube\Entity\Tag eet%d
WHERE NORMALIZE( eet%d.name ) LIKE NORMALIZE( :%s )
)
)',
$key, $key, $index, $index, $index, $index, $key,
$index, $index, $index, $index, $index, $index, $index, $key))
->setParameter($key, '%'.$keyword.'%');
traitの例はカラム追加とかが多くて、メソッド書き換の方法を調べてみます。