mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 09:02:12 +00:00
Adding sylius search feature
This commit is contained in:
parent
03a2a52125
commit
31b7d18033
53 changed files with 4135 additions and 7 deletions
|
|
@ -80,3 +80,46 @@ swiftmailer:
|
|||
username: %sylius.mailer.user%
|
||||
password: %sylius.mailer.password%
|
||||
spool: { type: memory }
|
||||
|
||||
sylius_search:
|
||||
form: 'SyliusSearchBundle::form.html.twig'
|
||||
items_per_page: 9
|
||||
driver: orm
|
||||
orm_indexes:
|
||||
product:
|
||||
class: Sylius\Component\Core\Model\Product
|
||||
mappings:
|
||||
id: ~
|
||||
name: ~
|
||||
description: ~
|
||||
filters:
|
||||
search_filter:
|
||||
enabled: true
|
||||
taxonomy: category
|
||||
facet_groups:
|
||||
search_set:
|
||||
values: [taxons, price, made_of, color]
|
||||
categories_set:
|
||||
values: [price, made_of, color]
|
||||
all_set:
|
||||
values: [taxons, price, made_of]
|
||||
facets:
|
||||
taxons:
|
||||
display_name: 'Basic categories'
|
||||
type: terms
|
||||
value: ~
|
||||
price:
|
||||
display_name: 'Available prices'
|
||||
type: range
|
||||
values:
|
||||
- { from: 0, to: 2000}
|
||||
- { from: 2001, to: 5000}
|
||||
- { from: 5001, to: 10000}
|
||||
made_of:
|
||||
display_name: 'Material'
|
||||
type: terms
|
||||
value: ~
|
||||
color:
|
||||
display_name: 'Available colors'
|
||||
type: terms
|
||||
value: ~
|
||||
|
|
|
|||
|
|
@ -271,6 +271,20 @@ default:
|
|||
filters:
|
||||
tags: "@javascript"
|
||||
|
||||
search:
|
||||
contexts:
|
||||
- Sylius\Bundle\SearchBundle\Behat\SearchContext
|
||||
- Behat\MinkExtension\Context\MinkContext
|
||||
- Sylius\Bundle\AddressingBundle\Behat\AddressingContext
|
||||
- Sylius\Bundle\CoreBundle\Behat\CoreContext
|
||||
- Sylius\Bundle\ProductBundle\Behat\ProductContext
|
||||
- Sylius\Bundle\ResourceBundle\Behat\BaseContext
|
||||
- Sylius\Bundle\ShippingBundle\Behat\ShippingContext
|
||||
- Sylius\Bundle\WebBundle\Behat\WebContext
|
||||
- Sylius\Bundle\MoneyBundle\Behat\MoneyContext
|
||||
filters:
|
||||
tags: "@search"
|
||||
|
||||
extensions:
|
||||
Behat\MinkExtension:
|
||||
sessions:
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@
|
|||
"white-october/pagerfanta-bundle": "~1.0",
|
||||
"winzou/state-machine-bundle": "~0.2",
|
||||
"willdurand/hateoas-bundle": "1.0.*@dev",
|
||||
"friendsofsymfony/oauth-server-bundle": "dev-master"
|
||||
"friendsofsymfony/oauth-server-bundle": "dev-master",
|
||||
"friendsofsymfony/elastica-bundle": "~3.0.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "~2.2",
|
||||
|
|
@ -111,6 +112,8 @@
|
|||
"sylius/registry": "self.version",
|
||||
"sylius/resource": "self.version",
|
||||
"sylius/resource-bundle": "self.version",
|
||||
"sylius/search": "self.version",
|
||||
"sylius/search-bundle": "self.version",
|
||||
"sylius/sequence": "self.version",
|
||||
"sylius/sequence-bundle": "self.version",
|
||||
"sylius/settings-bundle": "self.version",
|
||||
|
|
|
|||
53
features/backend/search.feature
Normal file
53
features/backend/search.feature
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
@search_orm_only
|
||||
Feature: Orm indexer event listener
|
||||
In order to have a consistent indexing when orm is enabled
|
||||
As a administrator
|
||||
I want to update the index when a product change occurs
|
||||
|
||||
Background:
|
||||
Given there is default currency configured
|
||||
And I am logged in as administrator
|
||||
And there are following taxonomies defined:
|
||||
| name |
|
||||
| Category |
|
||||
And taxonomy "Category" has following taxons:
|
||||
| Clothing > T-Shirts |
|
||||
| Clothing > PHP T-Shirts |
|
||||
| Clothing > Gloves |
|
||||
And the following products exist:
|
||||
| name | price | taxons | description |
|
||||
| Super T-Shirt | 19.99 | T-Shirts | super black t-shirt |
|
||||
| Black T-Shirt | 18.99 | T-Shirts | black t-shirt |
|
||||
| Sylius Tee | 12.99 | PHP T-Shirts | a very nice php t-shirt |
|
||||
| Symfony T-Shirt | 15.00 | PHP T-Shirts | symfony t-shirt |
|
||||
| Doctrine T-Shirt | 15.00 | PHP T-Shirts | doctrine t-shirt |
|
||||
|
||||
Scenario: Viewing the dashboard at website root
|
||||
Given I am on the dashboard page
|
||||
Then I should see "Administration dashboard"
|
||||
|
||||
Scenario: Creating simple product and indexing it
|
||||
Given I am on the product creation page
|
||||
When I fill in the following:
|
||||
| Name | Index test product |
|
||||
| Description | Interesting description |
|
||||
| Price | 29.99 |
|
||||
And I press "Create"
|
||||
Then I should be on the page of product "Index test product"
|
||||
And I should see "Product has been successfully created."
|
||||
And I should find an indexed entry for "Interesting description"
|
||||
|
||||
Scenario: Updating the product description and update the index accordingly
|
||||
Given I am editing product "Sylius Tee"
|
||||
When I fill in "Description" with "Another description"
|
||||
And I press "Save changes"
|
||||
Then I should be on the page of product "Sylius Tee"
|
||||
And I should see "Another description"
|
||||
And I should find an indexed entry for "Another description"
|
||||
|
||||
Scenario: Deleting product should delete the index as well
|
||||
Given I am on the page of product "Sylius Tee"
|
||||
When I press "delete"
|
||||
Then I should be on the product index page
|
||||
And I should see "Product has been successfully deleted."
|
||||
And I should not find an indexed entry for "a very nice php t-shirt"
|
||||
70
features/frontend/search.feature
Normal file
70
features/frontend/search.feature
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
@search
|
||||
Feature: Search products
|
||||
In order to be able to find products
|
||||
As a visitor
|
||||
I want to be able to search the products
|
||||
|
||||
Background:
|
||||
Given there is default currency configured
|
||||
And there are following taxonomies defined:
|
||||
| name |
|
||||
| Category |
|
||||
And taxonomy "Category" has following taxons:
|
||||
| Clothing > T-Shirts |
|
||||
| Clothing > PHP T-Shirts |
|
||||
| Clothing > Gloves |
|
||||
And the following products exist:
|
||||
| name | price | taxons | description |
|
||||
| Super T-Shirt | 19.99 | T-Shirts | super black t-shirt |
|
||||
| Black T-Shirt | 18.99 | T-Shirts | black t-shirt |
|
||||
| Sylius Tee | 12.99 | PHP T-Shirts | a very nice php t-shirt |
|
||||
| Symfony T-Shirt | 15.00 | PHP T-Shirts | symfony t-shirt |
|
||||
| Doctrine T-Shirt | 15.00 | PHP T-Shirts | doctrine t-shirt |
|
||||
And I populate the index
|
||||
|
||||
Scenario: Search homepage is accessible
|
||||
Given I am on homepage
|
||||
Then I should see "Login"
|
||||
And the response status code should be 200
|
||||
|
||||
Scenario: Search for a product
|
||||
Given I am on homepage
|
||||
When I fill in "search" with "black"
|
||||
And I press "search-button"
|
||||
Then I should be on "/search/"
|
||||
And I should see "black"
|
||||
And I should see "T-Shirts (2)"
|
||||
And I should see "€0.00 to €20.00 (2)"
|
||||
|
||||
Scenario: Apply filters to a search result
|
||||
Given I am on homepage
|
||||
When I fill in "search" with "black"
|
||||
And I press "search-button"
|
||||
Then I should be on "/search/"
|
||||
And I should see "black"
|
||||
When I select the "price-0"
|
||||
And I press "Filter"
|
||||
Then I should see "€0.00 to €20.00 (2)"
|
||||
|
||||
Scenario: Get facets for a taxon
|
||||
Given I am on homepage
|
||||
And I follow "T-Shirts"
|
||||
Then I should see there 2 products
|
||||
And I should see "€0.00 to €20.00 (2)"
|
||||
|
||||
Scenario: Apply filters on a taxon
|
||||
Given I am on homepage
|
||||
And I follow "T-Shirts"
|
||||
Then I should see there 2 products
|
||||
And I should see "€0.00 to €20.00 (2)"
|
||||
When I select the "price-0"
|
||||
And I press "Filter"
|
||||
Then I should see "€0.00 to €20.00 (2)"
|
||||
|
||||
Scenario: If I search for something that does not exists I should see a no products message
|
||||
Given I am on homepage
|
||||
When I fill in "search" with "crazy frog"
|
||||
And I press "search-button"
|
||||
Then I should be on "/search/"
|
||||
And I should see "There are no products to display"
|
||||
|
||||
|
|
@ -42,6 +42,7 @@ suites:
|
|||
ProductBundle : { namespace: Sylius, spec_path: src/Sylius/Bundle/ProductBundle }
|
||||
PromotionBundle : { namespace: Sylius, spec_path: src/Sylius/Bundle/PromotionBundle }
|
||||
ResourceBundle : { namespace: Sylius, spec_path: src/Sylius/Bundle/ResourceBundle }
|
||||
SearchBundle : { namespace: Sylius, spec_path: src/Sylius/Bundle/SearchBundle }
|
||||
SequenceBundle : { namespace: Sylius, spec_path: src/Sylius/Bundle/SequenceBundle }
|
||||
SettingsBundle : { namespace: Sylius, spec_path: src/Sylius/Bundle/SettingsBundle }
|
||||
ShippingBundle : { namespace: Sylius, spec_path: src/Sylius/Bundle/ShippingBundle }
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
|||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Sylius\Bundle\SearchBundle\Query\TaxonQuery;
|
||||
|
||||
/**
|
||||
* Product controller.
|
||||
|
|
@ -39,6 +40,9 @@ class ProductController extends ResourceController
|
|||
*/
|
||||
public function indexByTaxonAction(Request $request, $permalink)
|
||||
{
|
||||
$criteria = $request->get('sylius_filter_form');
|
||||
unset($criteria['_token'], $criteria['filter']);
|
||||
|
||||
if ($request->attributes->has('_sylius_entity')) {
|
||||
$taxon = $request->attributes->get('_sylius_entity');
|
||||
} else {
|
||||
|
|
@ -50,12 +54,15 @@ class ProductController extends ResourceController
|
|||
}
|
||||
}
|
||||
|
||||
$paginator = $this
|
||||
->getRepository()
|
||||
->createByTaxonPaginator($taxon)
|
||||
;
|
||||
$finder = $this->get('sylius_search.finder')
|
||||
->setFacetGroup('categories_set')
|
||||
->find(new TaxonQuery($taxon, $request->query->get('filters')));
|
||||
|
||||
return $this->renderResults($taxon, $paginator, 'indexByTaxon.html', $request->get('page', 1));
|
||||
$config = $this->container->getParameter("sylius_search.config");
|
||||
|
||||
$paginator = $finder->getPaginator();
|
||||
|
||||
return $this->renderResults($taxon, $paginator, 'indexByTaxon.html', $request->get('page', 1), $finder->getFacets(), $config['filters']['facets']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -182,7 +189,7 @@ class ProductController extends ResourceController
|
|||
return parent::findOr404($request, $criteria);
|
||||
}
|
||||
|
||||
private function renderResults(TaxonInterface $taxon, Pagerfanta $results, $template, $page)
|
||||
private function renderResults(TaxonInterface $taxon, Pagerfanta $results, $template, $page, $facets = null, $facetTags = null)
|
||||
{
|
||||
$results->setCurrentPage($page, true, true);
|
||||
$results->setMaxPerPage($this->config->getPaginationMaxPerPage());
|
||||
|
|
@ -193,6 +200,8 @@ class ProductController extends ResourceController
|
|||
->setData(array(
|
||||
'taxon' => $taxon,
|
||||
'products' => $results,
|
||||
'facets' => $facets,
|
||||
'facetTags' => $facetTags,
|
||||
))
|
||||
;
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ abstract class Kernel extends BaseKernel
|
|||
new \Sylius\Bundle\PricingBundle\SyliusPricingBundle(),
|
||||
new \Sylius\Bundle\SequenceBundle\SyliusSequenceBundle(),
|
||||
new \Sylius\Bundle\ContentBundle\SyliusContentBundle(),
|
||||
new \Sylius\Bundle\SearchBundle\SyliusSearchBundle(),
|
||||
|
||||
new \Sylius\Bundle\CoreBundle\SyliusCoreBundle(),
|
||||
new \Sylius\Bundle\WebBundle\SyliusWebBundle(),
|
||||
|
|
@ -88,6 +89,7 @@ abstract class Kernel extends BaseKernel
|
|||
new \FOS\RestBundle\FOSRestBundle(),
|
||||
|
||||
new \FOS\UserBundle\FOSUserBundle(),
|
||||
new \FOS\ElasticaBundle\FOSElasticaBundle(),
|
||||
new \Knp\Bundle\GaufretteBundle\KnpGaufretteBundle(),
|
||||
new \Knp\Bundle\MenuBundle\KnpMenuBundle(),
|
||||
new \Knp\Bundle\SnappyBundle\KnpSnappyBundle(),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ class ProductContext extends DefaultContext
|
|||
if (isset($data['sku'])) {
|
||||
$product->setSku($data['sku']);
|
||||
}
|
||||
|
||||
if (isset($data['description'])) {
|
||||
$product->setDescription($data['description']);
|
||||
}
|
||||
|
||||
if (isset($data['quantity'])) {
|
||||
$product->getMasterVariant()->setOnHand($data['quantity']);
|
||||
}
|
||||
|
|
|
|||
5
src/Sylius/Bundle/SearchBundle/.gitignore
vendored
Normal file
5
src/Sylius/Bundle/SearchBundle/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
vendor/
|
||||
bin/
|
||||
|
||||
composer.phar
|
||||
composer.lock
|
||||
14
src/Sylius/Bundle/SearchBundle/.travis.yml
Normal file
14
src/Sylius/Bundle/SearchBundle/.travis.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
language: php
|
||||
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
|
||||
before_script: composer install --prefer-source --no-interaction
|
||||
|
||||
script: bin/phpspec run -fpretty --verbose
|
||||
|
||||
notifications:
|
||||
email: "travis-ci@sylius.org"
|
||||
irc: "irc.freenode.org#sylius-dev"
|
||||
63
src/Sylius/Bundle/SearchBundle/Accessor/ProductAccessor.php
Normal file
63
src/Sylius/Bundle/SearchBundle/Accessor/ProductAccessor.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Accessor;
|
||||
|
||||
use Symfony\Component\PropertyAccess\Exception;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessor;
|
||||
use Symfony\Component\PropertyAccess\PropertyPathInterface;
|
||||
|
||||
/**
|
||||
* Product Accessor
|
||||
*
|
||||
* This class uses the property accessor to dynamically translate getters to values.
|
||||
* In case of sylius it extends the default functionality to include options and attributes.
|
||||
*
|
||||
* As a result someone can use any attribute or option name and get the value of it, something like a
|
||||
* dynamic getter. This class is quite useful and maybe it belongs to the product bundle itself rather than here.
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class ProductAccessor extends PropertyAccessor
|
||||
{
|
||||
/**
|
||||
* @param array|object $objectOrArray
|
||||
* @param string|PropertyPathInterface $propertyPath
|
||||
*
|
||||
* @return array|mixed
|
||||
*/
|
||||
public function getValue($objectOrArray, $propertyPath)
|
||||
{
|
||||
try {
|
||||
return parent::getValue($objectOrArray, $propertyPath);
|
||||
}catch (Exception\NoSuchPropertyException $e){
|
||||
$tags = array();
|
||||
|
||||
foreach ($objectOrArray->getAvailableVariants() as $variant) {
|
||||
foreach ($variant->getOptions() as $option) {
|
||||
|
||||
if (strtolower($option->getPresentation()) == strtolower($propertyPath)) {
|
||||
$tags[] = $option->getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($objectOrArray->getAttributes() as $attribute) {
|
||||
|
||||
if (strtolower(str_replace(' ', '_', $attribute->getPresentation())) == strtolower($propertyPath)) {
|
||||
$tags[] = $attribute->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/Sylius/Bundle/SearchBundle/Behat/SearchContext.php
Normal file
104
src/Sylius/Bundle/SearchBundle/Behat/SearchContext.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Behat;
|
||||
|
||||
use PhpSpec\Exception\Exception;
|
||||
use Sylius\Bundle\ResourceBundle\Behat\DefaultContext;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SearchContext extends DefaultContext
|
||||
{
|
||||
|
||||
/**
|
||||
* @Given /^I populate the index$/
|
||||
*/
|
||||
public function iPopulateTheIndex()
|
||||
{
|
||||
$command = $this->kernel->getRootDir() . "/console sylius:search:index --env=test";
|
||||
$process = new Process($command);
|
||||
$process->run();
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
throw new \RuntimeException($process->getErrorOutput());
|
||||
}
|
||||
|
||||
print $process->getOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I select the "([^""]*)"$/
|
||||
*/
|
||||
public function iSelectThe($id)
|
||||
{
|
||||
$html = $this->getSession()->getPage()->getHtml();
|
||||
|
||||
$crawler = new Crawler($html);
|
||||
|
||||
$element = $crawler->filter('#'.$id)->first();
|
||||
|
||||
$element->addHtmlContent('checked');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I should find an indexed entry for "([^""]*)"$/
|
||||
*/
|
||||
public function iCreateAndIndex($id)
|
||||
{
|
||||
$em = $this->getContainer()->get('doctrine')->getManager();
|
||||
$queryBuilder = $em->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->select('u')
|
||||
->from('Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex', 'u')
|
||||
->where('u.value LIKE :id')
|
||||
->setParameter('id', '%'.$id.'%')
|
||||
;
|
||||
|
||||
$result = $queryBuilder->getQuery()->getResult();
|
||||
|
||||
if (!$result) {
|
||||
throw new Exception(
|
||||
"The entry does not exist in the index"
|
||||
);
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^I should not find an indexed entry for "([^""]*)"$/
|
||||
*/
|
||||
public function iDeleteAnIndex($id)
|
||||
{
|
||||
$em = $this->getContainer()->get('doctrine')->getManager();
|
||||
$queryBuilder = $em->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->select('u')
|
||||
->from('Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex', 'u')
|
||||
->where('u.value LIKE :id')
|
||||
->setParameter('id', '%'.$id.'%')
|
||||
;
|
||||
|
||||
$result = $queryBuilder->getQuery()->getResult();
|
||||
|
||||
if (!empty($result)) {
|
||||
throw new Exception(
|
||||
"The entry does exist in the index"
|
||||
);
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/Sylius/Bundle/SearchBundle/CHANGELOG.md
Normal file
4
src/Sylius/Bundle/SearchBundle/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
* Initial dev release.
|
||||
42
src/Sylius/Bundle/SearchBundle/Command/IndexCommand.php
Normal file
42
src/Sylius/Bundle/SearchBundle/Command/IndexCommand.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Command;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Command to populate a search index
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class IndexCommand extends ContainerAwareCommand
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('sylius:search:index')
|
||||
->setDescription('Populate the index')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$output->writeln('Index populate command');
|
||||
|
||||
$this->getContainer()->get('sylius_search.command')->populate($this->getContainer()->get('doctrine.orm.entity_manager'));
|
||||
|
||||
$output->writeln('Done');
|
||||
}
|
||||
|
||||
}
|
||||
133
src/Sylius/Bundle/SearchBundle/Controller/SearchController.php
Normal file
133
src/Sylius/Bundle/SearchBundle/Controller/SearchController.php
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Controller;
|
||||
|
||||
use Sylius\Bundle\SearchBundle\Entity\SearchLog;
|
||||
use Sylius\Bundle\SearchBundle\Query\SearchStringQuery;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Search landing page controller.
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SearchController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Search landing page.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction(Request $request)
|
||||
{
|
||||
$finder = $this->get('sylius_search.finder')
|
||||
->setTargetIndex('product')
|
||||
->setFacetGroup('search_set')
|
||||
->find(new SearchStringQuery(
|
||||
$request->query->get('q'),
|
||||
$request->query->get('search_param'),
|
||||
$request->query->get('filters'),
|
||||
$this->container->getParameter('sylius_search.filter.enabled')
|
||||
)
|
||||
);
|
||||
|
||||
$selectedFilters = array();
|
||||
if (is_array($request->query->get('filters'))) {
|
||||
$selectedFilters = $request->query->get('filters');
|
||||
}
|
||||
|
||||
$paginator = $finder->getPaginator();
|
||||
$facets = $finder->getFacets();
|
||||
|
||||
$config = $this->container->getParameter("sylius_search.config");
|
||||
|
||||
if (isset($paginator)) {
|
||||
$paginator->setMaxPerPage($config['items_per_page']);
|
||||
$paginator->setCurrentPage($request->query->get('page', 1));
|
||||
}
|
||||
|
||||
$this->logSearchString(
|
||||
$request->query->get('q'),
|
||||
$request->headers->get('User-Agent'),
|
||||
$request->getClientIp()
|
||||
);
|
||||
|
||||
return $this->render('SyliusSearchBundle::search.html.twig', array(
|
||||
'results' => $paginator,
|
||||
'facets' => $facets,
|
||||
'selectedFilters' => $selectedFilters,
|
||||
'facetTags' => $config['filters']['facets'],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function formAction(Request $request)
|
||||
{
|
||||
$filters = array();
|
||||
|
||||
if ($this->container->getParameter('sylius_search.filter.enabled')) {
|
||||
$taxonomy = $this->get('sylius.repository.taxonomy')
|
||||
->findOneBy(
|
||||
array(
|
||||
'name' => strtoupper($this->container->getParameter('sylius_search.filter.taxonomy'))
|
||||
)
|
||||
);
|
||||
|
||||
$filters = array();
|
||||
if (!empty($taxonomy)) {
|
||||
foreach ($taxonomy->getTaxons() as $taxon) {
|
||||
$filters[] = $taxon->getName();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this->render($this->container->getParameter('sylius_search.form'), array(
|
||||
'filters' => $filters,
|
||||
'searchTerm' => $request->query->get('q'),
|
||||
'searchParam' => $request->query->get('search_param'),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a search
|
||||
*
|
||||
* TODO: I could move this to a listener hooked in a find event
|
||||
*
|
||||
* @param $searchString
|
||||
* @param $userAgent
|
||||
* @param $remoteAddress
|
||||
*
|
||||
* @internal param $request
|
||||
*/
|
||||
private function logSearchString($searchString, $userAgent, $remoteAddress)
|
||||
{
|
||||
$searchLog = new SearchLog();
|
||||
|
||||
$searchLog->setSearchString($searchString);
|
||||
$searchLog->setClient($userAgent);
|
||||
$searchLog->setRemoteAddress($remoteAddress);
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->persist($searchLog);
|
||||
$em->flush();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
/**
|
||||
* This is the class that validates and merges configuration from your app/config files
|
||||
*
|
||||
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class Configuration implements ConfigurationInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder();
|
||||
$rootNode = $treeBuilder->root('sylius_search');
|
||||
|
||||
$this->addFormSection($rootNode);
|
||||
|
||||
$this->addFilterSection($rootNode);
|
||||
|
||||
$this->addDriverSection($rootNode);
|
||||
|
||||
$this->addIndexesSection($rootNode);
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds form section
|
||||
*
|
||||
* @param ArrayNodeDefinition $node
|
||||
*/
|
||||
private function addFormSection(ArrayNodeDefinition $node)
|
||||
{
|
||||
$node
|
||||
->children()
|
||||
->scalarNode('form')
|
||||
->info('Define the search form')
|
||||
->end()
|
||||
->scalarNode('items_per_page')
|
||||
->info('Define paginated items')
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds filter section
|
||||
*
|
||||
* @param ArrayNodeDefinition $node
|
||||
*/
|
||||
private function addFilterSection(ArrayNodeDefinition $node)
|
||||
{
|
||||
$node
|
||||
->children()
|
||||
->arrayNode('filters')
|
||||
->children()
|
||||
->arrayNode('search_filter')
|
||||
->children()
|
||||
->scalarNode('enabled')->end()
|
||||
->scalarNode('taxonomy')->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('finders')
|
||||
->useAttributeAsKey('name')
|
||||
->prototype('array')
|
||||
->children()
|
||||
->scalarNode('facet_group')->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('facet_groups')
|
||||
->useAttributeAsKey('name')
|
||||
->prototype('array')
|
||||
->children()
|
||||
->arrayNode('values')
|
||||
->useAttributeAsKey('name')
|
||||
->prototype('scalar')
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('facets')
|
||||
->useAttributeAsKey('name')
|
||||
->prototype('array')
|
||||
->children()
|
||||
->scalarNode('display_name')->end()
|
||||
->scalarNode('type')->end()
|
||||
->scalarNode('value')->end()
|
||||
->arrayNode('values')
|
||||
->prototype('array')
|
||||
->children()
|
||||
->scalarNode('from')->end()
|
||||
->scalarNode('to')->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds driver section
|
||||
*
|
||||
* @param ArrayNodeDefinition $node
|
||||
*/
|
||||
private function addDriverSection(ArrayNodeDefinition $node)
|
||||
{
|
||||
$node
|
||||
->children()
|
||||
->scalarNode('driver')
|
||||
->info('Defaults to the first client defined')
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
}
|
||||
|
||||
private function addIndexesSection(ArrayNodeDefinition $node)
|
||||
{
|
||||
$node
|
||||
->children()
|
||||
->arrayNode('orm_indexes')
|
||||
->useAttributeAsKey('name')
|
||||
->prototype('array')
|
||||
->children()
|
||||
->scalarNode('class')
|
||||
->end()
|
||||
->arrayNode('mappings')
|
||||
->prototype('array')
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader;
|
||||
use Symfony\Component\DependencyInjection\DefinitionDecorator;
|
||||
|
||||
/**
|
||||
* Class SyliusSearchExtension
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SyliusSearchExtension extends Extension
|
||||
{
|
||||
|
||||
public function load(array $configs, ContainerBuilder $container)
|
||||
{
|
||||
$configuration = new Configuration();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
|
||||
$loader->load('services.xml');
|
||||
|
||||
$container->setParameter('search', $config['driver']);
|
||||
$container->setParameter('sylius_search.config', $config);
|
||||
|
||||
$searchClass = 'Sylius\Bundle\SearchBundle\Searcher\\'.$config['driver'].'Searcher';
|
||||
$finderClass = 'Sylius\Bundle\SearchBundle\Finder\\'.$config['driver'].'Finder';
|
||||
|
||||
$commandClass = sprintf('sylius.search.%s.indexer', $config['driver']);
|
||||
$container->setAlias('sylius_search.command', $commandClass);
|
||||
|
||||
$container->setParameter('sylius_search.engine', $searchClass);
|
||||
$container->setParameter('sylius_search.finder', $finderClass);
|
||||
$container->setParameter('sylius_search.command', $commandClass);
|
||||
$container->setParameter('sylius_search.form', $config['form']);
|
||||
$container->setParameter('sylius_search.filter.enabled', $config['filters']['search_filter']['enabled']);
|
||||
$container->setParameter('sylius_search.filter.taxonomy', $config['filters']['search_filter']['taxonomy']);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Doctrine\ORM;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductRepository as BaseProductRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SyliusSearchIndexRepository
|
||||
{
|
||||
/**
|
||||
* @var \Doctrine\ORM\EntityManager
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
* @var \Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductRepository
|
||||
*/
|
||||
private $productRepository;
|
||||
|
||||
/**
|
||||
* @param EntityManager $em
|
||||
* @param BaseProductRepository $productRepository
|
||||
*/
|
||||
public function __construct(EntityManager $em, BaseProductRepository $productRepository)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->productRepository = $productRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product ids for a given taxon
|
||||
*
|
||||
* @param $taxonName
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProductIdsFromTaxonName($taxonName)
|
||||
{
|
||||
|
||||
// Gets the taxon ids
|
||||
$query = $this->em->createQuery('SELECT u.id FROM Sylius\Component\Core\Model\Taxon u WHERE u.name = ?1');
|
||||
$query->setParameter(1, $taxonName);
|
||||
$taxonId = $query->getResult(Query::HYDRATE_SINGLE_SCALAR);
|
||||
|
||||
$filteredProducts = $this->productRepository->getProductsByTaxons(array($taxonId));
|
||||
|
||||
$filteredIds = array();
|
||||
foreach ($filteredProducts as $filteredProduct) {
|
||||
$filteredIds[] = $filteredProduct->getId();
|
||||
}
|
||||
|
||||
return $filteredIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $criteria
|
||||
* @param array $orderBy
|
||||
*
|
||||
* @return mixed|\Pagerfanta\Pagerfanta
|
||||
*/
|
||||
public function createPaginator(array $criteria = null, array $orderBy = null)
|
||||
{
|
||||
return $this->productRepository->createPaginator($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $ids
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProductsByIds(array $ids)
|
||||
{
|
||||
return $this->productRepository->findBy(array('id'=>$ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
*/
|
||||
public function getProductsQueryBuilder()
|
||||
{
|
||||
return $this->productRepository->getCollectionQueryBuilder();
|
||||
}
|
||||
|
||||
}
|
||||
149
src/Sylius/Bundle/SearchBundle/Entity/SearchLog.php
Normal file
149
src/Sylius/Bundle/SearchBundle/Entity/SearchLog.php
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* SearchLog entity
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SearchLog
|
||||
{
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $searchString;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $remoteAddress;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set searchString
|
||||
*
|
||||
* @param string $searchString
|
||||
* @return SearchLog
|
||||
*/
|
||||
public function setSearchString($searchString)
|
||||
{
|
||||
$this->searchString = $searchString;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get searchString.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSearchString()
|
||||
{
|
||||
return $this->searchString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set client.
|
||||
*
|
||||
* @param string $client
|
||||
* @return SearchLog
|
||||
*/
|
||||
public function setClient($client)
|
||||
{
|
||||
$this->client = $client;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getClient()
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $remoteAddress
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRemoteAddress($remoteAddress)
|
||||
{
|
||||
$this->remoteAddress = $remoteAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remoteAddress
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteAddress()
|
||||
{
|
||||
return $this->remoteAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set createdAt
|
||||
*
|
||||
* @param \DateTime $createdAt
|
||||
* @return SearchLog
|
||||
*/
|
||||
public function setCreatedAt($createdAt)
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get createdAt
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getCreatedAt()
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
}
|
||||
177
src/Sylius/Bundle/SearchBundle/Entity/SyliusSearchIndex.php
Normal file
177
src/Sylius/Bundle/SearchBundle/Entity/SyliusSearchIndex.php
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* SearchLog entity
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SyliusSearchIndex
|
||||
{
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var integer
|
||||
*/
|
||||
private $itemId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $entity;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $tags;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
|
||||
/**
|
||||
* Get id
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set itemId
|
||||
*
|
||||
* @param integer $itemId
|
||||
* @return SyliusSearchIndex
|
||||
*/
|
||||
public function setItemId($itemId)
|
||||
{
|
||||
$this->itemId = $itemId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get itemId
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getItemId()
|
||||
{
|
||||
return $this->itemId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set entity
|
||||
*
|
||||
* @param string $entity
|
||||
* @return SyliusSearchIndex
|
||||
*/
|
||||
public function setEntity($entity)
|
||||
{
|
||||
$this->entity = $entity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEntity()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value
|
||||
*
|
||||
* @param string $value
|
||||
* @return SyliusSearchIndex
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $tags
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTags($tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set createdAt
|
||||
*
|
||||
* @param \DateTime $createdAt
|
||||
* @return SyliusSearchIndex
|
||||
*/
|
||||
public function setCreatedAt($createdAt)
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get createdAt
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getCreatedAt()
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
}
|
||||
417
src/Sylius/Bundle/SearchBundle/Finder/ElasticsearchFinder.php
Normal file
417
src/Sylius/Bundle/SearchBundle/Finder/ElasticsearchFinder.php
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Finder;
|
||||
|
||||
use Sylius\Bundle\SearchBundle\Query\Query;
|
||||
use Sylius\Bundle\SearchBundle\Query\SearchStringQuery;
|
||||
use Sylius\Bundle\SearchBundle\Query\TaxonQuery;
|
||||
|
||||
/**
|
||||
* Elasticsearch Finder
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class ElasticsearchFinder implements FinderInterface
|
||||
{
|
||||
|
||||
/* @var */
|
||||
private $searchRepository;
|
||||
|
||||
/* @var */
|
||||
private $config;
|
||||
|
||||
/* @var */
|
||||
private $productRepository;
|
||||
|
||||
/* @var */
|
||||
private $container;
|
||||
|
||||
/* @var */
|
||||
private $facets;
|
||||
|
||||
/* @var */
|
||||
private $paginator;
|
||||
|
||||
/* @var */
|
||||
private $facetGroup;
|
||||
|
||||
/* @var */
|
||||
private $targetIndex;
|
||||
|
||||
/**
|
||||
* @param $searchRepository
|
||||
* @param $config
|
||||
* @param $productRepository
|
||||
* @param $container
|
||||
*/
|
||||
public function __construct($searchRepository, $config, $productRepository, $container)
|
||||
{
|
||||
$this->searchRepository = $searchRepository;
|
||||
$this->config = $config;
|
||||
$this->productRepository = $productRepository;
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPaginator()
|
||||
{
|
||||
return $this->paginator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFacets()
|
||||
{
|
||||
return $this->facets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $targetIndex
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTargetIndex($targetIndex)
|
||||
{
|
||||
$this->targetIndex = $targetIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $facetGroup
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFacetGroup($facetGroup)
|
||||
{
|
||||
$this->facetGroup = $facetGroup;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* TODO: a simple if does the job for now, in future this should move to a
|
||||
* chain of responsibility pattern or something similar.
|
||||
*/
|
||||
public function find(Query $queryObject)
|
||||
{
|
||||
if ($queryObject instanceof SearchStringQuery) {
|
||||
return $this->getResults($queryObject);
|
||||
|
||||
} elseif ($queryObject instanceof TaxonQuery) {
|
||||
return $this->getResultsForTaxon($queryObject);
|
||||
|
||||
} else {
|
||||
throw new Exception("finder can't handle this currently, feel free to implement it!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResultsForTaxon(TaxonQuery $query)
|
||||
{
|
||||
if (isset($this->facetGroup)) {
|
||||
$this->getConfiguredFilterSetsForFinders($this->facetGroup);
|
||||
}
|
||||
|
||||
$finder = $this->container->get('fos_elastica.index.sylius.product');
|
||||
|
||||
$elasticaQuery = $this->compileElasticsearchQuery(null, $query->getAppliedFilters(), $this->config, $query->getTaxon()->getName());
|
||||
|
||||
$products = $finder->search($elasticaQuery);
|
||||
|
||||
$facets = null;
|
||||
if (isset($this->facetGroup)) {
|
||||
$facets = $this->transformFacetsForPresentation($products, $query->getAppliedFilters());
|
||||
}
|
||||
|
||||
$paginator = $this->productRepository->createByTaxonPaginator($query->getTaxon(), array('id' => $this->getProductIdsFromFulltextSearch($products)));
|
||||
|
||||
$this->facets = $facets;
|
||||
$this->paginator = $paginator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResults(SearchStringQuery $query)
|
||||
{
|
||||
if (isset($this->facetGroup)) {
|
||||
$this->getConfiguredFilterSetsForFinders($this->facetGroup);
|
||||
}
|
||||
|
||||
if (isset($this->targetIndex)) {
|
||||
$finder = $this->container->get('fos_elastica.index.sylius.'.$this->targetIndex);
|
||||
}else{
|
||||
$finder = $this->container->get('fos_elastica.index.sylius');
|
||||
}
|
||||
|
||||
$elasticaQuery = $this->compileElasticsearchQuery(
|
||||
$query->getSearchTerm(),
|
||||
$query->getAppliedFilters(),
|
||||
$this->config,
|
||||
$query->getSearchParam()
|
||||
);
|
||||
|
||||
$products = $finder->search($elasticaQuery);
|
||||
|
||||
$facets = null;
|
||||
if (isset($this->facetGroup)) {
|
||||
$facets = $this->transformFacetsForPresentation($products, $query->getAppliedFilters());
|
||||
}
|
||||
|
||||
$paginator = $this->searchRepository->createPaginator(array('id' => $this->getProductIdsFromFulltextSearch($products)));
|
||||
|
||||
$this->facets = $facets;
|
||||
$this->paginator = $paginator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $searchTerm
|
||||
* @param null $facets
|
||||
* @param $configuration
|
||||
* @param null $taxon
|
||||
*
|
||||
* @return \Elastica\Query
|
||||
*/
|
||||
public function compileElasticsearchQuery($searchTerm, $facets = null, $configuration, $taxon = null)
|
||||
{
|
||||
$elasticaQuery = new \Elastica\Query();
|
||||
$boolFilter = new \Elastica\Filter\Bool();
|
||||
|
||||
if (!$searchTerm) {
|
||||
$query = new \Elastica\Query\Filtered();
|
||||
|
||||
$taxonFromRequestFilter = new \Elastica\Filter\Terms();
|
||||
$taxonFromRequestFilter->setTerms('taxons', array($taxon));
|
||||
$boolFilter->addMust($taxonFromRequestFilter);
|
||||
|
||||
$query->setFilter($boolFilter);
|
||||
|
||||
$elasticaQuery->setQuery($query);
|
||||
|
||||
} else {
|
||||
|
||||
if ($taxon != 'all') {
|
||||
$query = new \Elastica\Query\Filtered();
|
||||
$query->setQuery(new \Elastica\Query\QueryString($searchTerm));
|
||||
|
||||
$taxonFromRequestFilter = new \Elastica\Filter\Terms();
|
||||
$taxonFromRequestFilter->setTerms('taxons', array($taxon));
|
||||
$boolFilter->addMust($taxonFromRequestFilter);
|
||||
|
||||
$query->setFilter($boolFilter);
|
||||
} else {
|
||||
$query = new \Elastica\Query\QueryString($searchTerm);
|
||||
}
|
||||
|
||||
$elasticaQuery->setQuery($query);
|
||||
}
|
||||
|
||||
foreach ($configuration['filters']['facets'] as $name => $facet) {
|
||||
|
||||
// terms facet creation
|
||||
if ($facet['type'] === 'terms') {
|
||||
${$name . 'AggregationFilter'} = new \Elastica\Aggregation\Filter($name);
|
||||
|
||||
${$name . 'Aggregation'} = new \Elastica\Aggregation\Terms($name);
|
||||
${$name . 'Aggregation'}->setField($name);
|
||||
${$name . 'Aggregation'}->setSize(550);
|
||||
|
||||
${$name . 'AggregationFilter'}->addAggregation(${$name . 'Aggregation'});
|
||||
}
|
||||
|
||||
// range facet creation
|
||||
if ($facet['type'] === 'range') {
|
||||
|
||||
${$name . 'AggregationFilter'} = new \Elastica\Aggregation\Filter($name);
|
||||
|
||||
${$name . 'Aggregation'} = new \Elastica\Aggregation\Range($name);
|
||||
foreach($facet['values'] as $value) {
|
||||
${$name . 'Aggregation'}
|
||||
->setField($name)
|
||||
->addRange($value['from'], $value['to'])
|
||||
;
|
||||
}
|
||||
|
||||
${$name . 'AggregationFilter'}->addAggregation(${$name . 'Aggregation'});
|
||||
}
|
||||
|
||||
if ($facet['type'] === 'attribute') {
|
||||
${$name . 'AggregationFilter'} = new \Elastica\Aggregation\Filter($name);
|
||||
|
||||
${$name . 'Aggregation'} = new \Elastica\Aggregation\Terms($name);
|
||||
${$name . 'Aggregation'}->setField($name);
|
||||
|
||||
${$name . 'AggregationFilter'}->addAggregation(${$name . 'Aggregation'});
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($facets)) {
|
||||
|
||||
$termFilters = new \Elastica\Filter\Term();
|
||||
$rangeFilters = new \Elastica\Filter\Range();
|
||||
$boolFilter = new \Elastica\Filter\Bool();
|
||||
|
||||
$filters = array();
|
||||
foreach ($facets as $facet) {
|
||||
|
||||
if (strpos($facet[key($facet)], "|") !== false) {
|
||||
$filters[key($facet)] = array('ranges' => explode('|', $facet[key($facet)]));
|
||||
} else {
|
||||
$filters[key($facet)] = array('term' => $facet[key($facet)]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($filters as $name => $value) {
|
||||
|
||||
if (is_array($value[key($value)])) {
|
||||
foreach ($value as $range) {
|
||||
$rangeFilters->addField($name, array('gte' => $range[0], 'lte' => $range[1]));
|
||||
$boolFilter->addMust($rangeFilters);
|
||||
}
|
||||
} else {
|
||||
$termFilters->setTerm($name, $value[key($value)]);
|
||||
$boolFilter->addMust($termFilters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$elasticaQuery->setFilter($boolFilter);
|
||||
|
||||
foreach ($facets as $name => $facet) {
|
||||
|
||||
$normName = key($facet);
|
||||
|
||||
${$normName . 'TermFilter'} = new \Elastica\Filter\Term();
|
||||
${$normName . 'RangeFilter'} = new \Elastica\Filter\Range();
|
||||
${$normName . 'BoolFilter'} = new \Elastica\Filter\Bool();
|
||||
|
||||
foreach ($filters as $value) {
|
||||
|
||||
if (is_array($value[key($value)])) {
|
||||
foreach ($value as $range) {
|
||||
${$normName . 'RangeFilter'}->addField($name, array('gte' => $range[0], 'lte' => $range[1]));
|
||||
${$normName . 'BoolFilter'}->addMust($rangeFilters);
|
||||
}
|
||||
} else {
|
||||
${$normName . 'TermFilter'}->setTerm($name, $value[key($value)]);
|
||||
${$normName . 'BoolFilter'}->addMust($termFilters);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach ($this->config['filters']['facets'] as $name => $facet) {
|
||||
|
||||
foreach ($facets as $value) {
|
||||
|
||||
if (count($facets)>=count($this->config['filters']['facets'])) {
|
||||
${$name . 'AggregationFilter'}->setFilter($boolFilter);
|
||||
|
||||
} elseif ($name!=key($value)) {
|
||||
if (isset(${key($value) . 'BoolFilter'})) {
|
||||
${$name . 'AggregationFilter'}->setFilter(${key($value) . 'BoolFilter'});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach ($configuration['filters']['facets'] as $name => $facet) {
|
||||
|
||||
if (!empty($facet)) {
|
||||
$param = ${$name. 'AggregationFilter'}->hasParam('filter');
|
||||
|
||||
if ($param) {
|
||||
$elasticaQuery->addAggregation(${$name. 'AggregationFilter'});
|
||||
} else {
|
||||
$elasticaQuery->addAggregation(${$name. 'Aggregation'});
|
||||
}
|
||||
} else {
|
||||
|
||||
$elasticaQuery->addAggregation(${$name. 'Aggregation'});
|
||||
}
|
||||
}
|
||||
|
||||
return $elasticaQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $elements
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function transformFacetsForPresentation($elements)
|
||||
{
|
||||
$tempFacets = $elements->getAggregations();
|
||||
|
||||
$elasticaFacets = array();
|
||||
|
||||
foreach($tempFacets as $name=>$facetData) {
|
||||
unset($facetData['doc_count']);
|
||||
|
||||
if (isset($facetData[key($facetData)]['buckets'])) {
|
||||
$elasticaFacets[key($facetData)] = $facetData[key($facetData)]['buckets'];
|
||||
} else {
|
||||
$elasticaFacets[$name] = $facetData['buckets'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_reverse($elasticaFacets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $products
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function getProductIdsFromFulltextSearch($products)
|
||||
{
|
||||
$ids = array();
|
||||
foreach ($products as $product) {
|
||||
$ids[] = $product->getId();
|
||||
}
|
||||
|
||||
if (empty($ids)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $filterSetName
|
||||
*/
|
||||
private function getConfiguredFilterSetsForFinders($filterSetName)
|
||||
{
|
||||
foreach ($this->config['filters']['facets'] as $name => $value) {
|
||||
if (!in_array($name, $this->config['filters']['facet_groups'][$filterSetName]['values'])) {
|
||||
unset($this->config['filters']['facets'][$name]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
47
src/Sylius/Bundle/SearchBundle/Finder/FinderInterface.php
Normal file
47
src/Sylius/Bundle/SearchBundle/Finder/FinderInterface.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Finder;
|
||||
|
||||
use Sylius\Bundle\SearchBundle\Query\Query;
|
||||
use Sylius\Bundle\SearchBundle\Query\SearchStringQuery;
|
||||
use Sylius\Bundle\SearchBundle\Query\TaxonQuery;
|
||||
|
||||
/**
|
||||
* Finder
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
interface FinderInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @param TaxonQuery $query
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getResultsForTaxon(TaxonQuery $query);
|
||||
|
||||
/**
|
||||
* @param SearchStringQuery $query
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getResults(SearchStringQuery $query);
|
||||
|
||||
/**
|
||||
* @param Query $queryObject
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function find(Query $queryObject);
|
||||
|
||||
}
|
||||
536
src/Sylius/Bundle/SearchBundle/Finder/OrmFinder.php
Normal file
536
src/Sylius/Bundle/SearchBundle/Finder/OrmFinder.php
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Finder;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Sylius\Bundle\SearchBundle\Query\Query;
|
||||
use Sylius\Bundle\SearchBundle\Query\SearchStringQuery;
|
||||
use Sylius\Bundle\SearchBundle\Query\TaxonQuery;
|
||||
use Symfony\Component\Config\Definition\Exception\Exception;
|
||||
|
||||
/**
|
||||
* OrmFinder
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class OrmFinder implements FinderInterface
|
||||
{
|
||||
/* @var */
|
||||
private $searchRepository;
|
||||
|
||||
/* @var */
|
||||
private $config;
|
||||
|
||||
/* @var */
|
||||
private $productRepository;
|
||||
|
||||
/* @var */
|
||||
private $container;
|
||||
|
||||
/* @var \Doctrine\ORM\EntityManager */
|
||||
private $em;
|
||||
|
||||
/* @var */
|
||||
private $facets;
|
||||
|
||||
/* @var */
|
||||
private $paginator;
|
||||
|
||||
/* @var */
|
||||
private $facetGroup;
|
||||
|
||||
/* @var */
|
||||
private $targetIndex;
|
||||
|
||||
/**
|
||||
* @param $searchRepository
|
||||
* @param $config
|
||||
* @param $productRepository
|
||||
* @param $container
|
||||
* @param EntityManager $em
|
||||
*/
|
||||
public function __construct($searchRepository, $config, $productRepository, $container, EntityManager $em)
|
||||
{
|
||||
$this->searchRepository = $searchRepository;
|
||||
$this->config = $config;
|
||||
$this->productRepository = $productRepository;
|
||||
$this->container = $container;
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPaginator()
|
||||
{
|
||||
return $this->paginator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFacets()
|
||||
{
|
||||
return $this->facets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $targetIndex
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTargetIndex($targetIndex)
|
||||
{
|
||||
$this->targetIndex = $targetIndex;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $facetGroup
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFacetGroup($facetGroup)
|
||||
{
|
||||
$this->facetGroup = $facetGroup;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* TODO: a simple if does the job for now, in future this should move to a
|
||||
* chain of responsibility pattern or something similar.
|
||||
*/
|
||||
public function find(Query $queryObject)
|
||||
{
|
||||
if ($queryObject instanceof SearchStringQuery) {
|
||||
return $this->getResults($queryObject);
|
||||
|
||||
} elseif ($queryObject instanceof TaxonQuery) {
|
||||
return $this->getResultsForTaxon($queryObject);
|
||||
|
||||
} else {
|
||||
throw new Exception("finder can't handle this currently, feel free to implement it!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResultsForTaxon(TaxonQuery $query)
|
||||
{
|
||||
if (isset($this->facetGroup)) {
|
||||
$this->getConfiguredFilterSetsForFinders($this->facetGroup);
|
||||
}
|
||||
|
||||
$paginator = $this->productRepository->createByTaxonPaginator($query->getTaxon(), array());
|
||||
|
||||
// calculates the facets of the result set
|
||||
$ids = array();
|
||||
foreach ($paginator as $product) {
|
||||
$ids[] = $product->getId();
|
||||
}
|
||||
|
||||
$result = array();
|
||||
$queryBuilder = $this->em->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->select('u.itemId, u.tags')
|
||||
->from('Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex', 'u')
|
||||
->where('u.itemId IN (:ids)')
|
||||
->setParameter('ids', $ids);
|
||||
|
||||
$res = $queryBuilder->getQuery()->getResult();
|
||||
foreach ($res as $facet) {
|
||||
$result[$facet['itemId']] = $facet['tags'];
|
||||
}
|
||||
|
||||
$appliedFilters = $query->getAppliedFilters();
|
||||
if (!$appliedFilters) {
|
||||
$appliedFilters = array();
|
||||
}
|
||||
|
||||
list($facetFilteredIds, $idsFromAllFacets) = $this->getFilteredIds($appliedFilters, $ids);
|
||||
|
||||
$facets = null;
|
||||
if (isset($this->facetGroup)) {
|
||||
$facets = $this->calculateNewFacets($result, $facetFilteredIds);
|
||||
}
|
||||
|
||||
$paginator = $this->productRepository->createByTaxonPaginator($query->getTaxon(), array('id' => $idsFromAllFacets));
|
||||
|
||||
$this->facets = $facets;
|
||||
$this->paginator = $paginator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResults(SearchStringQuery $query)
|
||||
{
|
||||
if (isset($this->facetGroup)) {
|
||||
$this->getConfiguredFilterSetsForFinders($this->facetGroup);
|
||||
}
|
||||
// get ids and tags from full text search
|
||||
$result = $this->query($query->getSearchTerm(), $this->em);
|
||||
|
||||
$ids = array_keys($result);
|
||||
|
||||
//filter the ids if searchParam is not all
|
||||
if ($query->getSearchParam() != 'all' && $query->isDropdownFilterEnabled()) {
|
||||
$ids = array_intersect(array_keys($result), $this->searchRepository->getProductIdsFromTaxonName($query->getSearchParam()));
|
||||
}
|
||||
|
||||
$appliedFilters = $query->getAppliedFilters();
|
||||
if (!$appliedFilters) {
|
||||
$appliedFilters = array();
|
||||
}
|
||||
|
||||
$idsFromAllFacets = null;
|
||||
$facets = null;
|
||||
if (!empty($ids)) {
|
||||
list($facetFilteredIds, $idsFromAllFacets) = $this->getFilteredIds($appliedFilters, $ids);
|
||||
|
||||
if (isset($this->facetGroup)) {
|
||||
$facets = $this->calculateNewFacets($result, $facetFilteredIds);
|
||||
}
|
||||
}
|
||||
|
||||
$paginator = $this->searchRepository->createPaginator(array('id' => $idsFromAllFacets));
|
||||
|
||||
$this->facets = $facets;
|
||||
$this->paginator = $paginator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $filters
|
||||
* @param $ids
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFilteredIds($filters, $ids)
|
||||
{
|
||||
// Build up lists of product ids for each facet and the total intersect of all for full filtered set
|
||||
$facetFilteredIds = array();
|
||||
$idsFromAllFacets = $ids;
|
||||
|
||||
foreach ($this->config['filters']['facets'] as $facetName => $facetConfig) {
|
||||
if ($thisFacetFilters = $this->getFiltersAppliedForFacet($facetName, $filters)) {
|
||||
$facetFilteredIds[$facetName] = $this->getFilteredResults($ids, $thisFacetFilters);
|
||||
} else {
|
||||
$facetFilteredIds[$facetName] = $ids;
|
||||
}
|
||||
|
||||
// Intersect this (possibly filtered) set of products for the facet with the main list
|
||||
$idsFromAllFacets = array_intersect($idsFromAllFacets, $facetFilteredIds[$facetName]);
|
||||
}
|
||||
|
||||
return array($facetFilteredIds, $idsFromAllFacets);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with the new calculated facets
|
||||
*
|
||||
* @param $result
|
||||
* @param $facetFilteredIds
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function calculateNewFacets($result, $facetFilteredIds)
|
||||
{
|
||||
$ids = array_keys($result);
|
||||
// Fetch the 'options' for each facet based on the limited set of products from the other facets
|
||||
$facets = array();
|
||||
|
||||
foreach ($this->config['filters']['facets'] as $facetName => $ormFacet) {
|
||||
|
||||
$idsFromOtherFacets = $ids;
|
||||
// Loop around other facets to get the intersect of all of their possibly filtered sets
|
||||
foreach ($facetFilteredIds as $otherFacetName => $otherFacetIds) {
|
||||
if ($otherFacetName != $facetName) {
|
||||
$idsFromOtherFacets = array_intersect($idsFromOtherFacets, $otherFacetIds);
|
||||
}
|
||||
}
|
||||
|
||||
$facets[$facetName] = $this->getFacet($idsFromOtherFacets, $this->config['filters']['facets'], $facetName, $result);
|
||||
}
|
||||
|
||||
return $facets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an individual facet category
|
||||
*
|
||||
* @param $idsFromOtherFacets
|
||||
* @param $facets
|
||||
* @param $givenFacetName
|
||||
* @param $result
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFacet($idsFromOtherFacets, $facets, $givenFacetName, $result)
|
||||
{
|
||||
// gathers the appearance of the elements
|
||||
$rawFacets = $this->calculateRawFacets($idsFromOtherFacets, $result);
|
||||
|
||||
// formats the data for sending out the presentation array
|
||||
$facetConfig = $facets[$givenFacetName];
|
||||
$finalFacets[$givenFacetName] = array();
|
||||
|
||||
if (isset($rawFacets[$givenFacetName])) {
|
||||
|
||||
foreach ($rawFacets[$givenFacetName] as $facet => $count) {
|
||||
|
||||
if (is_numeric($facet) && $facetConfig['type'] == 'range') {
|
||||
|
||||
foreach ($facetConfig['values'] as $key => $range) {
|
||||
if ($facet >= $range['from'] && $facet <= $range['to']) {
|
||||
if (empty ($finalFacets[$givenFacetName][$key])) {
|
||||
$finalFacets[$givenFacetName][$key] = array('from' => $range['from'], 'to' => $range['to'], 'doc_count' => 1);
|
||||
} else {
|
||||
$finalFacets[$givenFacetName][$key]['doc_count'] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
asort($finalFacets[$givenFacetName]);
|
||||
} else {
|
||||
$finalFacets[$givenFacetName][] = array('key' => $facet, 'doc_count' => $count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $finalFacets[$givenFacetName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $facetName
|
||||
* @param $filters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFiltersAppliedForFacet($facetName, $filters)
|
||||
{
|
||||
$filtersForFacet = array();
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
|
||||
$filterName = key($filter);
|
||||
|
||||
if ($facetName == preg_replace('/\d/', '', $filterName)) {
|
||||
$filtersForFacet[] = array($filterName => $filter[key($filter)]);
|
||||
}
|
||||
}
|
||||
|
||||
return $filtersForFacet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the result set based on facets
|
||||
*
|
||||
* @param array $ids
|
||||
* @param array $filters
|
||||
*
|
||||
* @internal param array $facets
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getFilteredResults(array $ids, array $filters)
|
||||
{
|
||||
$filteredIds = array_intersect(
|
||||
$this->getFilteredResultsForRange($ids, $filters),
|
||||
$this->getFilteredResultsForTerms($ids, $filters)
|
||||
);
|
||||
|
||||
return $filteredIds;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $ids
|
||||
* @param array $filters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFilteredResultsForRange(array $ids, array $filters)
|
||||
{
|
||||
foreach ($filters as $key => $filter) {
|
||||
if (strpos($filter[key($filter)], "|") === false) {
|
||||
unset($filters[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($filters)) {
|
||||
return $ids;
|
||||
}
|
||||
|
||||
$queryBuilder = $this->searchRepository->getProductsQueryBuilder();
|
||||
$queryBuilder
|
||||
->leftJoin('product.taxons', 'taxon')
|
||||
->leftJoin('product.attributes', 'attribute')
|
||||
->leftJoin('product.variants', 'variant')
|
||||
->where('product.id IN (:ids)')
|
||||
->setParameter('ids', $ids);
|
||||
|
||||
$orx = $queryBuilder->expr()->orX();
|
||||
|
||||
|
||||
foreach ($filters as $separateFilter) {
|
||||
|
||||
$filter = $separateFilter[key($separateFilter)];
|
||||
|
||||
if (strpos($filter, "|")) {
|
||||
$range = explode("|", $filter);
|
||||
|
||||
$orx->add('variant.price>=' . $range[0] . ' AND variant.price<=' . $range[1] . ' AND variant.master=1');
|
||||
} elseif (strpos($filter, "taxon") !== false) {
|
||||
$orx->add('taxon.name = \'' . $filter . '\'');
|
||||
} elseif (strpos($filter, "_") !== false) {
|
||||
$orx->add('attribute.value = \'' . $filter . '\'');
|
||||
}
|
||||
}
|
||||
|
||||
$queryBuilder->andWhere($orx);
|
||||
|
||||
$results = $queryBuilder->getQuery()->getResult();
|
||||
|
||||
$ids = array();
|
||||
foreach ($results as $result) {
|
||||
$ids[] = $result->getId();
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
function getFilteredResultsForTerms(array $ids, array $filters)
|
||||
{
|
||||
foreach ($filters as $key => $filter) {
|
||||
if (strpos($filter[key($filter)], "|")) {
|
||||
unset($filters[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($filters)) {
|
||||
return $ids;
|
||||
}
|
||||
|
||||
$result = array();
|
||||
$queryBuilder = $this->em->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->select('u.itemId, u.tags')
|
||||
->from('Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex', 'u')
|
||||
->where('u.id IN (:ids)')
|
||||
->setParameter('ids', $ids);
|
||||
|
||||
$res = $queryBuilder->getQuery()->getResult();
|
||||
foreach ($res as $facet) {
|
||||
|
||||
foreach ($filters as $separateFilter) {
|
||||
$tags = unserialize($facet['tags']);
|
||||
|
||||
if (in_array(ucfirst($separateFilter[key($separateFilter)]), $tags[strtolower(key($separateFilter))])) {
|
||||
$result[$facet['itemId']] = $facet['tags'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $searchTerm
|
||||
* @param EntityManager $em
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function query($searchTerm, EntityManager $em)
|
||||
{
|
||||
$sql = 'select item_id, tags, entity from sylius_search_index WHERE MATCH(value) AGAINST (:searchterm)';
|
||||
|
||||
$stmt = $em->getConnection()->prepare($sql);
|
||||
$stmt->execute(array('searchterm' => strip_tags($searchTerm)));
|
||||
$results = $stmt->fetchAll();
|
||||
|
||||
$facets = array();
|
||||
foreach ($results as $result) {
|
||||
|
||||
if (isset($this->targetIndex) && $result['entity'] != $this->config['orm_indexes'][$this->targetIndex]['class']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$facets[$result['item_id']] = $result['tags'];
|
||||
}
|
||||
|
||||
return $facets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $filterSetName
|
||||
*/
|
||||
private function getConfiguredFilterSetsForFinders($filterSetName)
|
||||
{
|
||||
foreach ($this->config['filters']['facets'] as $name => $value) {
|
||||
if (!in_array($name, $this->config['filters']['facet_groups'][$filterSetName]['values'])) {
|
||||
unset($this->config['filters']['facets'][$name]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $idsFromOtherFacets
|
||||
* @param $result
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function calculateRawFacets($idsFromOtherFacets, $result)
|
||||
{
|
||||
$rawFacets = array();
|
||||
|
||||
foreach ($result as $id => $tags) {
|
||||
if (!in_array($id, $idsFromOtherFacets)) {
|
||||
unset($result[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($result as $serializedTags) {
|
||||
$tags = unserialize($serializedTags);
|
||||
foreach ($tags as $name => $value) {
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $v) {
|
||||
$rawFacets[$name][] = $v;
|
||||
}
|
||||
} elseif (is_numeric($value)) {
|
||||
$rawFacets[$name][] = intval($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rawFacets as $name => $facet) {
|
||||
$rawFacets[$name] = array_count_values($facet);
|
||||
}
|
||||
|
||||
return $rawFacets;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Indexer;
|
||||
|
||||
use Sylius\Bundle\CoreBundle\Kernel\Kernel;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* Elasticsearch indexer
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class ElasticSearchIndexer implements IndexerInterface
|
||||
{
|
||||
/* @var \Sylius\Bundle\CoreBundle\Kernel\Kernel */
|
||||
private $kernel;
|
||||
|
||||
/**
|
||||
* @param Kernel $kernel
|
||||
*/
|
||||
public function __construct(Kernel $kernel)
|
||||
{
|
||||
$this->kernel = $kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function populate(EntityManager $em = null)
|
||||
{
|
||||
$environment = $this->kernel->getEnvironment();
|
||||
|
||||
$populateCommand = sprintf("/console fos:elastica:populate --env=%s", $environment);
|
||||
|
||||
$command = $this->kernel->getRootDir() . $populateCommand;
|
||||
$process = new Process($command);
|
||||
$process->run();
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
throw new \RuntimeException($process->getErrorOutput());
|
||||
}
|
||||
|
||||
print $process->getOutput();
|
||||
}
|
||||
|
||||
}
|
||||
37
src/Sylius/Bundle/SearchBundle/Indexer/IndexerInterface.php
Normal file
37
src/Sylius/Bundle/SearchBundle/Indexer/IndexerInterface.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Indexer;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* Interface IndexerInterface
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
interface IndexerInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Populates the index table.
|
||||
*
|
||||
* Entity manager is needed by orm indexer but not for es indexer. It exists
|
||||
* here to overcome a circular reference error on the orm side. There was a solution
|
||||
* by using events but ends up creating more complicated code than it suppose to be.
|
||||
*
|
||||
* @param EntityManager $em
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function populate(EntityManager $em);
|
||||
|
||||
}
|
||||
266
src/Sylius/Bundle/SearchBundle/Indexer/OrmIndexer.php
Normal file
266
src/Sylius/Bundle/SearchBundle/Indexer/OrmIndexer.php
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Indexer;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer;
|
||||
use Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex;
|
||||
|
||||
/**
|
||||
* Orm Indexer
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class OrmIndexer implements IndexerInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $transformer;
|
||||
|
||||
const SPACER = ' ';
|
||||
|
||||
public function __construct(Array $config, ModelToElasticaAutoTransformer $transformer)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->transformer = $transformer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EntityManager $em
|
||||
*/
|
||||
public function setEntityManager(EntityManager $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function populate(EntityManager $em = null)
|
||||
{
|
||||
$this->setEntityManager($em);
|
||||
|
||||
echo 'Reseting index' . PHP_EOL;
|
||||
|
||||
$sql = 'show index from sylius_search_index';
|
||||
$stmt = $this->em->getConnection()
|
||||
->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
$res = $stmt->fetchAll(\PDO::FETCH_COLUMN, 2);
|
||||
|
||||
if (in_array('fulltext_search_idx', array_values($res))) {
|
||||
$sql = 'alter table sylius_search_index drop key fulltext_search_idx';
|
||||
$stmt = $this->em->getConnection()
|
||||
->prepare($sql);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
$sql = 'truncate sylius_search_index';
|
||||
$stmt = $this->em->getConnection()
|
||||
->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
foreach ($this->config['orm_indexes'] as $index) {
|
||||
$this->createIndex($index['class'], $index['mappings']);
|
||||
}
|
||||
|
||||
$sql = 'create fulltext index fulltext_search_idx on sylius_search_index (value)';
|
||||
$stmt = $this->em->getConnection()
|
||||
->prepare($sql);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $entity
|
||||
* @param $fields
|
||||
*
|
||||
* @internal param $table
|
||||
*/
|
||||
private function createIndex($entity, $fields)
|
||||
{
|
||||
|
||||
$a = array_keys($fields);
|
||||
foreach ($a as &$value) {
|
||||
$value = 'u.' . $value;
|
||||
}
|
||||
|
||||
echo 'Populating index table with ' . $entity . ' data' . PHP_EOL;
|
||||
|
||||
$queryBuilder = $this->em->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->select('u')
|
||||
->from($entity, 'u')
|
||||
->where('u.deletedAt IS NULL');
|
||||
$results = $queryBuilder->getQuery()->getResult();
|
||||
|
||||
foreach ($results as $element) {
|
||||
$this->createIndexForEntity($entity, $fields, $element);
|
||||
}
|
||||
|
||||
echo 'Index created successfully' . PHP_EOL;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entities
|
||||
*/
|
||||
public function insertMany(array $entities)
|
||||
{
|
||||
foreach($entities as $entity) {
|
||||
$class = get_class($entity);
|
||||
|
||||
$indexName = $this->nestedValues($this->config['orm_indexes'], $class);
|
||||
|
||||
$this->createIndexForEntity($class, $this->config['orm_indexes'][$indexName]['mappings'], $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entities
|
||||
*/
|
||||
public function removeMany(array $entities)
|
||||
{
|
||||
foreach($entities as $entity) {
|
||||
$this->removeIndexForEntity($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $entityName
|
||||
* @param $fields
|
||||
* @param $entity
|
||||
*/
|
||||
public function createIndexForEntity($entityName, $fields, $entity)
|
||||
{
|
||||
$content = $this->compileSearchableContent($fields, $entity);
|
||||
|
||||
$queryBuilder = $this->em->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->select('u')
|
||||
->from('Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex', 'u')
|
||||
->where('u.itemId = :item_id')
|
||||
->andWhere('u.entity = :entity_namespace')
|
||||
->setParameter(':item_id', $entity->getId())
|
||||
->setParameter(':entity_namespace', get_class($entity));
|
||||
|
||||
try {
|
||||
|
||||
$syliusSearchIndex = $queryBuilder->getQuery()->getSingleResult();
|
||||
$syliusSearchIndex->setValue($content);
|
||||
|
||||
}catch(\Doctrine\ORM\NoResultException $e) {
|
||||
|
||||
$syliusSearchIndex = new SyliusSearchIndex();
|
||||
|
||||
$syliusSearchIndex->setItemId($entity->getId());
|
||||
$syliusSearchIndex->setEntity($entityName);
|
||||
$syliusSearchIndex->setValue($content);
|
||||
}
|
||||
|
||||
$this->getTagsForElementAndSave($entity, $syliusSearchIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $entity
|
||||
*/
|
||||
public function removeIndexForEntity($entity)
|
||||
{
|
||||
$queryBuilder = $this->em->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->delete('Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex', 'u')
|
||||
->where('u.itemId = :item_id')
|
||||
->andWhere('u.entity = :entity_namespace')
|
||||
->setParameter(':item_id', $entity->getId())
|
||||
->setParameter(':entity_namespace', get_class($entity));
|
||||
|
||||
$result = $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $element
|
||||
* @param $syliusSearchIndex
|
||||
*/
|
||||
public function getTagsForElementAndSave($element, $syliusSearchIndex)
|
||||
{
|
||||
/*
|
||||
* We bound orm with elasticsearch at this point. I could separate the logic but this
|
||||
* means that we will have logic duplication. Maybe this could be refactored in the future.
|
||||
*/
|
||||
$elasticaDocument = $this->transformer->transform($element, array_flip(array_keys($this->config['filters']['facets'])));
|
||||
$syliusSearchIndex->setTags(serialize($elasticaDocument->getData()));
|
||||
|
||||
$this->em->persist($syliusSearchIndex);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $fields
|
||||
* @param $element
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function compileSearchableContent($fields, $element)
|
||||
{
|
||||
$content = '';
|
||||
foreach (array_keys(array_slice($fields, 1)) as $field) {
|
||||
$func = 'get' . ucfirst($field);
|
||||
$content .= $element->$func() . self::SPACER;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object is indexable or not.
|
||||
*
|
||||
* @param object $object
|
||||
* @return bool
|
||||
*/
|
||||
public function isObjectIndexable($object)
|
||||
{
|
||||
foreach ($this->config['orm_indexes'] as $index) {
|
||||
if ($index['class'] == get_class($object) && $this->config['driver'] == 'orm') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $node
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function nestedValues($node) {
|
||||
if (is_array($node)) {
|
||||
$ret = '';
|
||||
foreach($node as $key => $val)
|
||||
$ret = $this->nestedValues($key, $val);
|
||||
return $ret;
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
}
|
||||
130
src/Sylius/Bundle/SearchBundle/Listener/OrmListener.php
Normal file
130
src/Sylius/Bundle/SearchBundle/Listener/OrmListener.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Listener;
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\Event\PostFlushEventArgs;
|
||||
use Sylius\Bundle\SearchBundle\Indexer\OrmIndexer;
|
||||
|
||||
/**
|
||||
* Orm Listener
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class OrmListener implements EventSubscriber
|
||||
{
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
private $ormIndexer;
|
||||
|
||||
/**
|
||||
* Objects scheduled for insertion and replacement
|
||||
*/
|
||||
public $scheduledForInsertion = array();
|
||||
public $scheduledForDeletion = array();
|
||||
|
||||
/**
|
||||
* @param OrmIndexer $ormIndexer
|
||||
*/
|
||||
public function __construct(OrmIndexer $ormIndexer)
|
||||
{
|
||||
$this->ormIndexer = $ormIndexer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
'postFlush',
|
||||
'postPersist',
|
||||
'postUpdate',
|
||||
'preRemove',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Happens after updating a product
|
||||
*
|
||||
* @param LifecycleEventArgs $args
|
||||
*/
|
||||
public function postUpdate(LifecycleEventArgs $args)
|
||||
{
|
||||
$entity = $args->getEntity();
|
||||
|
||||
if ($this->ormIndexer->isObjectIndexable($entity)) {
|
||||
$this->scheduledForInsertion[] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
public function postPersist(LifecycleEventArgs $args)
|
||||
{
|
||||
$entity = $args->getEntity();
|
||||
|
||||
if ($this->ormIndexer->isObjectIndexable($entity)) {
|
||||
$this->scheduledForInsertion[] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PostFlushEventArgs $args
|
||||
*/
|
||||
public function postFlush(PostFlushEventArgs $args)
|
||||
{
|
||||
$this->index($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Happens before the deletion of a product
|
||||
*
|
||||
* @param LifecycleEventArgs $args
|
||||
*/
|
||||
public function preRemove(LifecycleEventArgs $args)
|
||||
{
|
||||
$entity = $args->getEntity();
|
||||
|
||||
if ($this->ormIndexer->isObjectIndexable($entity)) {
|
||||
$this->scheduledForDeletion[] = $entity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $args
|
||||
*/
|
||||
public function index($args)
|
||||
{
|
||||
// workaround to avoid circular reference of entity manager on indexer service definition
|
||||
$this->ormIndexer->setEntityManager($args->getEntityManager());
|
||||
|
||||
if (count($this->scheduledForInsertion)) {
|
||||
|
||||
// trick to clear the array and avoid looping
|
||||
$scheduledForInsertion = $this->scheduledForInsertion;
|
||||
$this->scheduledForInsertion = array();
|
||||
|
||||
$this->ormIndexer->insertMany($scheduledForInsertion);
|
||||
}
|
||||
|
||||
if (count($this->scheduledForDeletion)) {
|
||||
|
||||
//this uses dql
|
||||
$this->ormIndexer->removeMany($this->scheduledForDeletion);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
39
src/Sylius/Bundle/SearchBundle/Query/Query.php
Normal file
39
src/Sylius/Bundle/SearchBundle/Query/Query.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Query;
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class Query
|
||||
{
|
||||
|
||||
/* @var */
|
||||
private $appliedFilters;
|
||||
|
||||
/**
|
||||
* @param $appliedFilters
|
||||
*/
|
||||
public function setAppliedFilters($appliedFilters)
|
||||
{
|
||||
$this->appliedFilters = $appliedFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAppliedFilters()
|
||||
{
|
||||
return $this->appliedFilters;
|
||||
}
|
||||
|
||||
}
|
||||
84
src/Sylius/Bundle/SearchBundle/Query/SearchStringQuery.php
Normal file
84
src/Sylius/Bundle/SearchBundle/Query/SearchStringQuery.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Query;
|
||||
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SearchStringQuery extends Query
|
||||
{
|
||||
|
||||
/* @var */
|
||||
private $searchTerm;
|
||||
|
||||
/* @var */
|
||||
private $searchParam;
|
||||
|
||||
/* @var */
|
||||
private $dropdownFilterEnabled;
|
||||
|
||||
/**
|
||||
* @param $searchTerm
|
||||
* @param $searchParam
|
||||
* @param $appliedFilters
|
||||
* @param $dropdownFilterEnabled
|
||||
*/
|
||||
public function __construct($searchTerm, $searchParam, $appliedFilters, $dropdownFilterEnabled)
|
||||
{
|
||||
parent::setAppliedFilters($appliedFilters);
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->searchParam = $searchParam;
|
||||
$this->dropdownFilterEnabled = $dropdownFilterEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $searchTerm
|
||||
*/
|
||||
public function setSearchTerm($searchTerm)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSearchTerm()
|
||||
{
|
||||
return $this->searchTerm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $searchParam
|
||||
*/
|
||||
public function setSearchParam($searchParam)
|
||||
{
|
||||
$this->searchParam = $searchParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSearchParam()
|
||||
{
|
||||
return $this->searchParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function isDropdownFilterEnabled()
|
||||
{
|
||||
return $this->dropdownFilterEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
50
src/Sylius/Bundle/SearchBundle/Query/TaxonQuery.php
Normal file
50
src/Sylius/Bundle/SearchBundle/Query/TaxonQuery.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle\Query;
|
||||
use Sylius\Component\Core\Model\Taxon;
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class TaxonQuery extends Query
|
||||
{
|
||||
|
||||
/* @var */
|
||||
private $taxon;
|
||||
|
||||
/**
|
||||
* @param Taxon $taxon
|
||||
* @param $appliedFilters
|
||||
*/
|
||||
public function __construct(Taxon $taxon, $appliedFilters)
|
||||
{
|
||||
parent::setAppliedFilters($appliedFilters);
|
||||
$this->taxon = $taxon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Taxon $taxon
|
||||
*/
|
||||
public function setTaxon(Taxon $taxon)
|
||||
{
|
||||
$this->taxon = $taxon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Taxon
|
||||
*/
|
||||
public function getTaxon()
|
||||
{
|
||||
return $this->taxon;
|
||||
}
|
||||
|
||||
}
|
||||
247
src/Sylius/Bundle/SearchBundle/README.md
Normal file
247
src/Sylius/Bundle/SearchBundle/README.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
SyliusSearchBundle [](http://travis-ci.org/Sylius/SyliusSearchBundle)
|
||||
===================
|
||||
|
||||
Search system for [**Symfony2**](http://symfony.com) applications.
|
||||
|
||||
It supports search functionality for mysql and elastic search.
|
||||
|
||||
Usage:
|
||||
|
||||
Place the following snippets on a twig view and you are ready to go.
|
||||
|
||||
```php
|
||||
{% render controller('SyliusSearchBundle:Search:form', {'request':app.request}) %}
|
||||
|
||||
{% include 'SyliusSearchBundle::filter_form.html.twig' %}
|
||||
```
|
||||
|
||||
If you want to use the search pragmatically there are currently 2 query types, string query and taxon query.
|
||||
|
||||
```php
|
||||
$finder = $this->get('sylius_search.finder')
|
||||
->setTargetIndex('product') // target index searches in a specific type, if it's not set it searches in all types
|
||||
->setFacetGroup('search_set') // configuration based, uses the relevant facet set, if not set it does not show facets
|
||||
->find(new SearchStringQuery(...));
|
||||
```
|
||||
|
||||
Taxon
|
||||
```php
|
||||
$finder = $this->get('sylius_search.finder')
|
||||
->setFacetGroup('categories_set')
|
||||
->find(new TaxonQuery(...));
|
||||
```
|
||||
|
||||
Basic configuration guidelines:
|
||||
|
||||
### Form
|
||||
```yaml
|
||||
form: 'SyliusSearchBundle::form.html.twig'
|
||||
```
|
||||
|
||||
The actual form you want to use for performing a search. As long as the naming of the elements is the same you can define a new twig file with your own design.
|
||||
|
||||
### Items per page
|
||||
|
||||
```yaml
|
||||
items_per_page: 9
|
||||
```
|
||||
|
||||
Pagination items for search results
|
||||
|
||||
### Driver
|
||||
|
||||
```yaml
|
||||
driver: orm
|
||||
```
|
||||
|
||||
Possible values: orm, elasticsearch
|
||||
|
||||
If orm is selected the search uses mysql as engine, if elasticsearch is selected if uses the elasticsearch search engine, which must be configured through the fos_elastica bundle. For documentation on fos_elastica please check the [fos_elastica github page](https://github.com/FriendsOfSymfony/FOSElasticaBundle)
|
||||
|
||||
### Indexes
|
||||
```yaml
|
||||
orm_indexes: # it is being used only when orm is selected as a driver
|
||||
product: # indentifier of an index
|
||||
class: Sylius\Component\Core\Model\Product # the corresponding model
|
||||
mappings: # appart from the id, the rest of the fields will be used to compile the searchable content
|
||||
id: ~
|
||||
name: ~
|
||||
description: ~
|
||||
```
|
||||
|
||||
if elasticsearch is selected here is a sample configuration
|
||||
|
||||
```yaml
|
||||
fos_elastica:
|
||||
clients:
|
||||
elasticsearch:
|
||||
servers:
|
||||
- { host: 10.0.0.100, port: 9200, logger: true }
|
||||
- { host: 10.0.0.101, port: 9200, logger: true }
|
||||
#for clustering you need to define the logger because of the https://github.com/FriendsOfSymfony/FOSElasticaBundle/issues/543
|
||||
indexes:
|
||||
sylius:
|
||||
client: elasticsearch
|
||||
finder: ~
|
||||
settings:
|
||||
analysis:
|
||||
filter:
|
||||
synonym:
|
||||
type: synonym
|
||||
synonyms: synonym.txt
|
||||
analyzer:
|
||||
my_analyzer:
|
||||
filter: synonym
|
||||
type: standard
|
||||
tokenizer: standard
|
||||
types:
|
||||
product:
|
||||
mappings:
|
||||
name:
|
||||
type: string
|
||||
analyzer: my_analyzer
|
||||
description: ~
|
||||
slug: ~
|
||||
color: ~
|
||||
price:
|
||||
type: integer
|
||||
taxons:
|
||||
type: string
|
||||
index: not_analyzed
|
||||
size: ~
|
||||
author: ~
|
||||
made_of:
|
||||
type: string
|
||||
index: not_analyzed
|
||||
|
||||
persistence:
|
||||
driver: orm
|
||||
model: Sylius\Component\Core\Model\Product
|
||||
model_to_elastica_transformer:
|
||||
service: sylius.search.transformers.model.product
|
||||
provider: ~
|
||||
listener: ~
|
||||
finder: ~
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
search_filter: # the small drop down menu on the side of the search field
|
||||
enabled: true
|
||||
taxonomy: category # possible values are the high level taxons (category, brand for sylius)
|
||||
facet_groups: # possible facet groups, you assign them in a finder object
|
||||
search_set:
|
||||
values: [taxons, price, made_of, color]
|
||||
categories_set:
|
||||
values: [price, made_of, color]
|
||||
all_set:
|
||||
values: [taxons, price, made_of]
|
||||
facets: # possible facets, as long as a model exposes attributes, options or getters with the given name, it will be used as a facet
|
||||
taxons:
|
||||
display_name: 'Basic categories'
|
||||
type: terms
|
||||
value: ~
|
||||
price:
|
||||
display_name: 'Available prices'
|
||||
type: range
|
||||
values:
|
||||
- { from: 0, to: 2000}
|
||||
- { from: 2001, to: 5000}
|
||||
- { from: 5001, to: 10000}
|
||||
made_of:
|
||||
display_name: 'Material'
|
||||
type: terms
|
||||
value: ~
|
||||
color:
|
||||
display_name: 'Available colors'
|
||||
type: terms
|
||||
value: ~
|
||||
```
|
||||
|
||||
### Indexing data
|
||||
|
||||
After configuring the indexer properly you execute the following command to populate the appropriate engine
|
||||
|
||||
```bash
|
||||
app/console sylius:search:index
|
||||
```
|
||||
|
||||
Sylius
|
||||
------
|
||||
|
||||
**Sylius** - Modern ecommerce for Symfony2. Visit [Sylius.org](http://sylius.org).
|
||||
|
||||
[phpspec](http://phpspec.net) examples
|
||||
--------------------------------------
|
||||
|
||||
```bash
|
||||
$ composer install
|
||||
$ bin/phpspec run -fpretty
|
||||
```
|
||||
|
||||
[behat](http://behat.org) examples for search bundle
|
||||
--------------------------------------
|
||||
|
||||
```bash
|
||||
$ composer install
|
||||
$ /bin/behat --suite=search
|
||||
```
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation is available on [**docs.sylius.org**](http://docs.sylius.org/en/latest/bundles/SyliusProductBundle/index.html).
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
All informations about contributing to Sylius can be found on [this page](http://docs.sylius.org/en/latest/contributing/index.html).
|
||||
|
||||
Mailing lists
|
||||
-------------
|
||||
|
||||
### Users
|
||||
|
||||
Questions? Feel free to ask on [users mailing list](http://groups.google.com/group/sylius).
|
||||
|
||||
### Developers
|
||||
|
||||
To contribute and develop this bundle, use the [developers mailing list](http://groups.google.com/group/sylius-dev).
|
||||
|
||||
Sylius twitter account
|
||||
----------------------
|
||||
|
||||
If you want to keep up with updates, [follow the official Sylius account on twitter](http://twitter.com/Sylius).
|
||||
|
||||
Bug tracking
|
||||
------------
|
||||
|
||||
This bundle uses [GitHub issues](https://github.com/Sylius/Sylius/issues).
|
||||
If you have found bug, please create an issue.
|
||||
|
||||
Versioning
|
||||
----------
|
||||
|
||||
Releases will be numbered with the format `major.minor.patch`.
|
||||
|
||||
And constructed with the following guidelines.
|
||||
|
||||
* Breaking backwards compatibility bumps the major.
|
||||
* New additions without breaking backwards compatibility bumps the minor.
|
||||
* Bug fixes and misc changes bump the patch.
|
||||
|
||||
For more information on SemVer, please visit [semver.org website](http://semver.org/).
|
||||
This versioning method is same for all **Sylius** bundles and applications.
|
||||
|
||||
MIT License
|
||||
-----------
|
||||
|
||||
License can be found [here](https://github.com/Sylius/SyliusProductBundle/blob/master/Resources/meta/LICENSE).
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
The bundle was originally created by [Argyrios Gounaris](http://agounaris.github.io).
|
||||
See the list of [contributors](https://github.com/Sylius/SyliusProductBundle/contributors).
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
|
||||
This file is part of the Sylius package.
|
||||
|
||||
(c) Paweł Jędrzejewski
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
|
||||
-->
|
||||
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Sylius\Bundle\SearchBundle\Entity\SearchLog" table="sylius_search_log">
|
||||
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="AUTO"/>
|
||||
</id>
|
||||
|
||||
<field name="searchString" type="string" column="search_string" length="255"/>
|
||||
|
||||
<field name="client" type="string" column="client" length="255"/>
|
||||
|
||||
<field name="remoteAddress" type="string" column="remote_address" length="20"/>
|
||||
|
||||
<field name="createdAt" type="datetime" column="created_at">
|
||||
<gedmo:timestampable on="create"/>
|
||||
</field>
|
||||
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
|
||||
This file is part of the Sylius package.
|
||||
|
||||
(c) Paweł Jędrzejewski
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
|
||||
-->
|
||||
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
|
||||
<entity repository-class="Sylius\Bundle\SearchBundle\Doctrine\ORM\SyliusSearchIndexRepository"
|
||||
name="Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex"
|
||||
table="sylius_search_index">
|
||||
|
||||
<options>
|
||||
<option name="engine">MyISAM</option>
|
||||
</options>
|
||||
|
||||
<!--
|
||||
Need full text index but it's not available until doctrine 2.5
|
||||
Dummy index, it's is updating with the index:populate command
|
||||
http://www.doctrine-project.org/jira/browse/DDC-3014
|
||||
-->
|
||||
<indexes>
|
||||
<index name="fulltext_search_idx" columns="item_id"/>
|
||||
<index name="item_id_idx" columns="item_id"/>
|
||||
<index name="item_id_entity_idx" columns="item_id,entity"/>
|
||||
</indexes>
|
||||
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="AUTO"/>
|
||||
</id>
|
||||
|
||||
<field name="itemId" type="integer" column="item_id"/>
|
||||
<field name="entity" type="string" column="entity" length="255"/>
|
||||
<field name="value" type="text" column="value"/>
|
||||
<field name="tags" type="text" column="tags" nullable="true"/>
|
||||
<field name="createdAt" type="datetime" column="created_at">
|
||||
<gedmo:timestampable on="create"/>
|
||||
</field>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
86
src/Sylius/Bundle/SearchBundle/Resources/config/services.xml
Normal file
86
src/Sylius/Bundle/SearchBundle/Resources/config/services.xml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
|
||||
This file is part of the Sylius package.
|
||||
|
||||
(c) Paweł Jędrzejewski
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
|
||||
-->
|
||||
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services
|
||||
http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<parameters>
|
||||
<parameter key="sylius_search.searchindex.repository.class">Sylius\Bundle\SearchBundle\Doctrine\ORM\SyliusSearchIndexRepository</parameter>
|
||||
<parameter key="fos_elastica.index.populate.class">FOS\ElasticaBundle\Command\PopulateCommand</parameter>
|
||||
<parameter key="sylius_search.orm.finder.class">Sylius\Bundle\SearchBundle\Finder\OrmFinder</parameter>
|
||||
<parameter key="sylius_search.elasticsearch.finder.class">Sylius\Bundle\SearchBundle\Finder\ElasticsearchFinder</parameter>
|
||||
<parameter key="sylius_search.finder.class">%sylius_search.finder%</parameter>
|
||||
<parameter key="sylius_search.finder.product_by_taxon.class">Sylius\Bundle\SearchBundle\Finder\ProductTaxonFinder</parameter>
|
||||
<parameter key="sylius_search.finder.product_by_text.class">Sylius\Bundle\SearchBundle\Finder\ProductTextFinder</parameter>
|
||||
<parameter key="sylius_search.finder.everything_by_text.class">Sylius\Bundle\SearchBundle\Finder\EverythingTextFinder</parameter>
|
||||
<parameter key="fos_elastica.model_to_elastica_transformer.class">FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer</parameter>
|
||||
<parameter key="sylius.search.orm.indexer.class">Sylius\Bundle\SearchBundle\Indexer\OrmIndexer</parameter>
|
||||
<parameter key="sylius.search.elasticsearch.indexer.class">Sylius\Bundle\SearchBundle\Indexer\ElasticsearchIndexer</parameter>
|
||||
</parameters>
|
||||
|
||||
<services>
|
||||
<service id="sylius_search.repository" class="%sylius_search.searchindex.repository.class%">
|
||||
<argument type="service" id="doctrine.orm.entity_manager" />
|
||||
<argument type="service" id="sylius.repository.product" />
|
||||
</service>
|
||||
|
||||
<service id="sylius_search" class="%sylius_search.engine%">
|
||||
<argument type="service" id="doctrine.orm.entity_manager" />
|
||||
<argument>%sylius_search.config%</argument>
|
||||
<argument type="service" id="sylius_search.repository" />
|
||||
<argument type="service" id="service_container" />
|
||||
</service>
|
||||
|
||||
<!-- dummy service name, is being aliaced in dependency injection -->
|
||||
<service id="sylius_search.command" />
|
||||
|
||||
<service id="fos_elastica.index.populate" class="%fos_elastica.index.populate.class%">
|
||||
</service>
|
||||
|
||||
<service id="sylius_search.finder" class="%sylius_search.finder.class%">
|
||||
<argument type="service" id="sylius_search.repository" />
|
||||
<argument>%sylius_search.config%</argument>
|
||||
<argument type="service" id="sylius.repository.product" />
|
||||
<argument type="service" id="service_container" />
|
||||
<argument type="service" id="doctrine.orm.entity_manager" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.search.transformers.model.product" class="%fos_elastica.model_to_elastica_transformer.class%">
|
||||
<call method="setPropertyAccessor">
|
||||
<argument type="service" id="sylius.search.product.property_accessor" />
|
||||
</call>
|
||||
</service>
|
||||
|
||||
<service id="sylius.search.product.property_accessor" class="Sylius\Bundle\SearchBundle\Accessor\ProductAccessor" />
|
||||
|
||||
<service id="sylius.search.orm.listener" class="Sylius\Bundle\SearchBundle\Listener\OrmListener">
|
||||
<argument type="service" id="sylius.search.orm.indexer" />
|
||||
<tag name="doctrine.event_listener" event="postFlush" />
|
||||
<tag name="doctrine.event_listener" event="postPersist" />
|
||||
<tag name="doctrine.event_listener" event="postUpdate" />
|
||||
<tag name="doctrine.event_listener" event="preRemove" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.search.orm.indexer" class="%sylius.search.orm.indexer.class%">
|
||||
<argument>%sylius_search.config%</argument>
|
||||
<argument type="service" id="sylius.search.transformers.model.product" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.search.elasticsearch.indexer" class="%sylius.search.elasticsearch.indexer.class%">
|
||||
<argument type="service" id="kernel" />
|
||||
</service>
|
||||
</services>
|
||||
|
||||
</container>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{#This form has no form type. I could use a form type but there is no data binding with a model#}
|
||||
{#and also I don't currently need any validation.#}
|
||||
|
||||
{#For a future release I could move it to a form type if there is a good reason for it.#}
|
||||
|
||||
{% if facets is defined and facets is not null %}
|
||||
|
||||
<form id="filter-form" action="{{ app.request.requestUri }}" method="get">
|
||||
{% for name, facet in facets %}
|
||||
{% if facet is not empty %}
|
||||
{{ facetTags[name]['display_name'] }}
|
||||
<ul class="list-unstyled">
|
||||
{% for element in facet %}
|
||||
{% if 'from' in element|keys %}
|
||||
<li>
|
||||
<label class="checkbox-inline">
|
||||
<input
|
||||
{% if app.request.query.get("filters") is iterable %}
|
||||
{% for elem in app.request.query.get("filters") %}
|
||||
{% if [element['from'],element['to']]|join('|') in elem %}
|
||||
{{ 'checked' }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
id="{{ name ~ '-' ~ loop.index0 }}"
|
||||
type="checkbox"
|
||||
name="filters[][{{ name }}]"
|
||||
value="{{ element['min'] is defined? element['min']:element['from'] }}|{{ element['max'] is defined? element['max']:element['to'] }}" />
|
||||
{{ element['min'] is defined? element['min']|sylius_price:element['from']|sylius_price }}
|
||||
to
|
||||
{{ element['max'] is defined? element['max']|sylius_price:element['to']|sylius_price }} ({{ element['doc_count'] }})
|
||||
</label>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<label class="checkbox-inline">
|
||||
<input
|
||||
{% if app.request.query.get("filters") is iterable %}
|
||||
{% for elem in app.request.query.get("filters") %}
|
||||
{% if element['key'] in elem %}
|
||||
{{ 'checked' }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
type="checkbox" name="filters[][{{ name }}]" value="{{ element['key'] }}"> {{ (':' in element['key'])? element['key']|split(':')[1]:element['key'] }} ({{ element['doc_count'] }})
|
||||
</label>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<input type="hidden" name="q" value="{{ app.request.query.get('q') }}"/>
|
||||
<input type="hidden" name="search_param" value="{{ app.request.query.get('search_param') }}"/>
|
||||
<input type="submit" value="Filter" class="btn btn-info"/>
|
||||
</form>
|
||||
<br>
|
||||
{% endif %}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{#This form has no form type. I could use a form type but there is no data binding with a model#}
|
||||
{#and also I don't currently need any validation.#}
|
||||
|
||||
{#For a future release I could move it to a form type if there is a good reason for it.#}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<form action="{{ path('sylius_search_homepage') }}" class="input-group">
|
||||
<div class="input-group-btn search-panel">
|
||||
{% if filters %}
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<span id="search_concept">{{ searchParam ?: 'All' }}</span> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="#all">All</a></li>
|
||||
{% for filter in filters %}
|
||||
<li><a href="#{{ filter }}">{{ filter }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="hidden" name="search_param" value="{{ searchParam ?: 'all' }}" id="search_param">
|
||||
<input type="text" class="form-control" id="search" placeholder="Search term..." name="q" value="{{ searchTerm ?: '' }}"
|
||||
style="height: 34px">
|
||||
<span class="input-group-btn">
|
||||
<button id="search-button" type="submit" class="btn btn-default"><span
|
||||
class="glyphicon glyphicon-search"></span></button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#sloppy part TODO: should this be in a file? #}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
$('.search-panel .dropdown-menu').find('a').click(function (e) {
|
||||
e.preventDefault();
|
||||
var param = $(this).attr("href").replace("#", "");
|
||||
var concept = $(this).text();
|
||||
$('.search-panel span#search_concept').text(concept);
|
||||
$('.input-group #search_param').val(param);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{% extends 'SyliusWebBundle:Frontend:layout.html.twig' %}
|
||||
{% from 'SyliusWebBundle:Frontend/Product:macros.html.twig' import grid %}
|
||||
{% from 'SyliusWebBundle:Frontend/Macros:misc.html.twig' import pagination %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if results %}
|
||||
{{ pagination(results) }}
|
||||
{{ grid(results) }}
|
||||
{{ pagination(results) }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
23
src/Sylius/Bundle/SearchBundle/SyliusSearchBundle.php
Normal file
23
src/Sylius/Bundle/SearchBundle/SyliusSearchBundle.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Sylius\Bundle\SearchBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
/**
|
||||
* Search bundle.
|
||||
*
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SyliusSearchBundle extends Bundle
|
||||
{
|
||||
}
|
||||
49
src/Sylius/Bundle/SearchBundle/composer.json
Normal file
49
src/Sylius/Bundle/SearchBundle/composer.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "sylius/search-bundle",
|
||||
"type": "symfony-bundle",
|
||||
"description": "Product catalog for your Symfony2 applications.",
|
||||
"keywords": ["shop", "ecommerce", "products", "product", "search"],
|
||||
"homepage": "http://sylius.org",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Argyrios Gounaris",
|
||||
"homepage": "http://agounaris.github.io"
|
||||
},
|
||||
{
|
||||
"name": "Sylius project",
|
||||
"homepage": "http://sylius.org"
|
||||
},
|
||||
{
|
||||
"name": "Community contributions",
|
||||
"homepage": "http://github.com/Sylius/Sylius/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.3",
|
||||
|
||||
"friendsofsymfony/elastica-bundle": "~3.0.2",
|
||||
"twig/twig": "~1.11",
|
||||
"doctrine/orm": "~2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/phpspec": "~2.0",
|
||||
"twig/twig": "~1.11",
|
||||
"doctrine/orm": "~2.3",
|
||||
},
|
||||
"config": {
|
||||
"bin-dir": "bin"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Sylius\\Bundle\\SearchBundle\\": "" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Sylius\\Bundle\\SearchBundle\\spec\\": "spec/" }
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.11-dev"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Doctrine\ORM;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Query;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Prophecy\Argument;
|
||||
use Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductRepository as BaseProductRepository;
|
||||
|
||||
class SyliusSearchIndexRepositorySpec extends ObjectBehavior
|
||||
{
|
||||
|
||||
function let(
|
||||
EntityManager $em,
|
||||
BaseProductRepository $productRepository
|
||||
)
|
||||
{
|
||||
$this->beConstructedWith($em, $productRepository);
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Doctrine\ORM\SyliusSearchIndexRepository');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Entity;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SearchLogSpec extends ObjectBehavior
|
||||
{
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Entity\SearchLog');
|
||||
}
|
||||
|
||||
function its_search_string_is_mutable()
|
||||
{
|
||||
$this->setSearchString('black');
|
||||
$this->getSearchString()->shouldReturn('black');
|
||||
}
|
||||
|
||||
function its_client_is_mutable()
|
||||
{
|
||||
$this->setClient('Mozilla/5.0');
|
||||
$this->getClient()->shouldReturn('Mozilla/5.0');
|
||||
}
|
||||
|
||||
function its_remote_address_is_mutable()
|
||||
{
|
||||
$this->setRemoteAddress('100.100.100.100');
|
||||
$this->getRemoteAddress()->shouldReturn('100.100.100.100');
|
||||
}
|
||||
|
||||
function its_created_at_is_mutable()
|
||||
{
|
||||
$this->setCreatedAt('2014-08-08 16:18:00');
|
||||
$this->getCreatedAt()->shouldReturn('2014-08-08 16:18:00');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Entity;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SyliusSearchIndexSpec extends ObjectBehavior
|
||||
{
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Entity\SyliusSearchIndex');
|
||||
}
|
||||
|
||||
function its_item_id_is_mutable()
|
||||
{
|
||||
$this->setItemId(20);
|
||||
$this->getItemId()->shouldReturn(20);
|
||||
}
|
||||
|
||||
function its_entity_is_mutable()
|
||||
{
|
||||
$this->setEntity('Sylius\Component\Core\Model\Product');
|
||||
$this->getEntity()->shouldReturn('Sylius\Component\Core\Model\Product');
|
||||
}
|
||||
|
||||
function its_value_is_mutable()
|
||||
{
|
||||
$this->setValue('black t-shirt');
|
||||
$this->getValue()->shouldReturn('black t-shirt');
|
||||
}
|
||||
|
||||
function its_tags_is_mutable()
|
||||
{
|
||||
$this->setTags('a:4:{s:6:"taxons";a:0:{}s:5:"price";i:5400;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}');
|
||||
$this->getTags()->shouldReturn('a:4:{s:6:"taxons";a:0:{}s:5:"price";i:5400;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}');
|
||||
}
|
||||
|
||||
function its_created_at_is_mutable()
|
||||
{
|
||||
$this->setCreatedAt('2014-08-08 15:53:07');
|
||||
$this->getCreatedAt()->shouldReturn('2014-08-08 15:53:07');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Finder;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Bundle\SearchBundle\Doctrine\ORM\SyliusSearchIndexRepository;
|
||||
|
||||
/**
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class ElasticsearchFinderSpec extends ObjectBehavior
|
||||
{
|
||||
function let(
|
||||
SyliusSearchIndexRepository $searchRepository,
|
||||
$config,
|
||||
$productRepository,
|
||||
$container
|
||||
)
|
||||
{
|
||||
$this->beConstructedWith(
|
||||
$searchRepository,
|
||||
$config,
|
||||
$productRepository,
|
||||
$container
|
||||
);
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Finder\ElasticsearchFinder');
|
||||
}
|
||||
|
||||
function it_compiles_an_elasticsearch_query()
|
||||
{
|
||||
$config = array(
|
||||
'filters' => array(
|
||||
'facets' => array(
|
||||
'taxons' => array(
|
||||
'display_name' => 'Basic categories',
|
||||
'type' => 'terms',
|
||||
'value' => null,
|
||||
'values' => array(),
|
||||
),
|
||||
'price' => array(
|
||||
'display_name' => 'Available prices',
|
||||
'type' => 'range',
|
||||
'value' => null,
|
||||
'values' => array(
|
||||
array('from' => 0, 'to' => 2000),
|
||||
array('from' => 2001, 'to' => 5000),
|
||||
array('from' => 5001, 'to' => 10000),
|
||||
),
|
||||
),
|
||||
'made_of' => array(
|
||||
'display_name' => 'Material',
|
||||
'type' => 'terms',
|
||||
'value' => null,
|
||||
'values' => array(),
|
||||
),
|
||||
'color' => array(
|
||||
'display_name' => 'Available colors',
|
||||
'type' => 'terms',
|
||||
'value' => null,
|
||||
'values' => array(),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
);
|
||||
|
||||
$this->compileElasticsearchQuery('modi', null, $config, null)->shouldHaveType('\Elastica\Query');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Finder;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\PDOStatement;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Prophecy\Argument;
|
||||
use Sylius\Bundle\SearchBundle\Doctrine\ORM\SyliusSearchIndexRepository;
|
||||
|
||||
|
||||
/**
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class OrmFinderSpec extends ObjectBehavior
|
||||
{
|
||||
function let(
|
||||
SyliusSearchIndexRepository $searchRepository,
|
||||
$config,
|
||||
$productRepository,
|
||||
$container,
|
||||
EntityManager $entityManager
|
||||
)
|
||||
{
|
||||
$this->beConstructedWith(
|
||||
$searchRepository,
|
||||
(array)$config,
|
||||
$productRepository,
|
||||
$container,
|
||||
$entityManager
|
||||
);
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Finder\OrmFinder');
|
||||
}
|
||||
|
||||
function it_calculates_the_new_facets()
|
||||
{
|
||||
$idsFromOtherFacets = array(
|
||||
0 => 89,
|
||||
1 => 67,
|
||||
2 => 30,
|
||||
3 => 103,
|
||||
4 => 40,
|
||||
5 => 62,
|
||||
6 => 1,
|
||||
7 => 42,
|
||||
8 => 117,
|
||||
);
|
||||
|
||||
$ormFacets = array(
|
||||
'taxons' => array(
|
||||
'display_name' => 'Basic categories',
|
||||
'type' => 'terms',
|
||||
'value' => null,
|
||||
'values' => array(),
|
||||
),
|
||||
'price' => array(
|
||||
'display_name' => 'Available prices',
|
||||
'type' => 'range',
|
||||
'value' => null,
|
||||
'values' => array(
|
||||
array('from' => 0, 'to' => 2000),
|
||||
array('from' => 2001, 'to' => 5000),
|
||||
array('from' => 5001, 'to' => 10000),
|
||||
),
|
||||
),
|
||||
'made_of' => array(
|
||||
'display_name' => 'Material',
|
||||
'type' => 'terms',
|
||||
'value' => null,
|
||||
'values' => array(),
|
||||
),
|
||||
'color' => array(
|
||||
'display_name' => 'Available colors',
|
||||
'type' => 'terms',
|
||||
'value' => null,
|
||||
'values' => array(),
|
||||
),
|
||||
);
|
||||
|
||||
$givenFacetName = 'taxons';
|
||||
|
||||
$result = array(
|
||||
89 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:705;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
67 => 'a:4:{s:6:"taxons";a:2:{i:0;s:8:"T-Shirts";i:1;s:9:"SuperTees";}s:5:"price";i:2840;s:7:"made_of";a:1:{i:0;s:9:"Polyester";}s:5:"color";a:3:{i:0;s:3:"Red";i:1;s:4:"Blue";i:2;s:5:"Green";}}',
|
||||
30 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:3905;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
103 => 'a:4:{s:6:"taxons";a:2:{i:0;s:8:"T-Shirts";i:1;s:9:"SuperTees";}s:5:"price";i:6222;s:7:"made_of";a:1:{i:0;s:24:"Polyester 10% / Wool 90%";}s:5:"color";a:3:{i:0;s:3:"Red";i:1;s:4:"Blue";i:2;s:5:"Green";}}',
|
||||
40 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:4089;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
62 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:5979;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
1 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:449;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
42 => 'a:4:{s:6:"taxons";a:2:{i:0;s:8:"Stickers";i:1;s:11:"Stickypicky";}s:5:"price";i:8330;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
117 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:4188;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
);
|
||||
|
||||
$this->getFacet($idsFromOtherFacets, $ormFacets, $givenFacetName, $result)->shouldHaveCount(6);
|
||||
|
||||
}
|
||||
|
||||
public function it_calculates_the_raw_facets()
|
||||
{
|
||||
$idsFromOtherFacets = array(
|
||||
0 => 89,
|
||||
1 => 67,
|
||||
2 => 30,
|
||||
3 => 103,
|
||||
4 => 40,
|
||||
5 => 62,
|
||||
6 => 1,
|
||||
7 => 42,
|
||||
8 => 117,
|
||||
);
|
||||
|
||||
$result = array(
|
||||
89 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:705;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
67 => 'a:4:{s:6:"taxons";a:2:{i:0;s:8:"T-Shirts";i:1;s:9:"SuperTees";}s:5:"price";i:2840;s:7:"made_of";a:1:{i:0;s:9:"Polyester";}s:5:"color";a:3:{i:0;s:3:"Red";i:1;s:4:"Blue";i:2;s:5:"Green";}}',
|
||||
30 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:3905;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
103 => 'a:4:{s:6:"taxons";a:2:{i:0;s:8:"T-Shirts";i:1;s:9:"SuperTees";}s:5:"price";i:6222;s:7:"made_of";a:1:{i:0;s:24:"Polyester 10% / Wool 90%";}s:5:"color";a:3:{i:0;s:3:"Red";i:1;s:4:"Blue";i:2;s:5:"Green";}}',
|
||||
40 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:4089;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
62 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:5979;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
1 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:449;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
42 => 'a:4:{s:6:"taxons";a:2:{i:0;s:8:"Stickers";i:1;s:11:"Stickypicky";}s:5:"price";i:8330;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
117 => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:4188;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
);
|
||||
|
||||
$this->calculateRawFacets($idsFromOtherFacets, $result)->shouldHaveCount(4);
|
||||
|
||||
}
|
||||
|
||||
public function it_performs_a_fulltext_query(
|
||||
EntityManagerInterface $entityManager,
|
||||
Connection $connection,
|
||||
PDOStatement $pdoStmt)
|
||||
{
|
||||
$result = array(
|
||||
0 =>
|
||||
array(
|
||||
'item_id' => '89',
|
||||
'tags' => 'a:4:{s:6:"taxons";a:2:{i:0;s:5:"Books";i:1;s:9:"Bookmania";}s:5:"price";i:705;s:7:"made_of";a:0:{}s:5:"color";a:0:{}}',
|
||||
'entity' => 'Sylius\Component\Core\Model\Product'),
|
||||
1 =>
|
||||
array(
|
||||
'item_id' => '67',
|
||||
'tags' => 'a:4:{s:6:"taxons";a:2:{i:0;s:8:"T-Shirts";i:1;s:9:"SuperTees";}s:5:"price";i:2840;s:7:"made_of";a:1:{i:0;s:9:"Polyester";}s:5:"color";a:3:{i:0;s:3:"Red";i:1;s:4:"Blue";i:2;s:5:"Green";}}',
|
||||
'entity' => 'Sylius\Component\Core\Model\Product')
|
||||
);
|
||||
|
||||
$entityManager->getConnection()->shouldBeCalled()->willReturn($connection);
|
||||
|
||||
$connection->prepare(Argument::any())->shouldBeCalled()->willReturn($pdoStmt);
|
||||
|
||||
$pdoStmt->execute(Argument::any())->shouldBeCalled();
|
||||
|
||||
$pdoStmt->fetchAll()->willReturn($result);
|
||||
|
||||
$this->query('black', $entityManager, false)->shouldHaveCount(2);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Indexer;
|
||||
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Prophecy\Argument;
|
||||
use Sylius\Bundle\CoreBundle\Kernel\Kernel;
|
||||
|
||||
/**
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class ElasticsearchIndexerSpec extends ObjectBehavior
|
||||
{
|
||||
function let(
|
||||
Kernel $kernel
|
||||
)
|
||||
{
|
||||
$this->beConstructedWith(
|
||||
$kernel
|
||||
);
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Indexer\ElasticsearchIndexer');
|
||||
}
|
||||
|
||||
function it_implements_the_indexer_interface_interface()
|
||||
{
|
||||
$this->shouldImplement('Sylius\Bundle\SearchBundle\Indexer\IndexerInterface');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Indexer;
|
||||
|
||||
use FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class OrmIndexerSpec extends ObjectBehavior
|
||||
{
|
||||
function let(
|
||||
$config,
|
||||
ModelToElasticaAutoTransformer $transformer
|
||||
)
|
||||
{
|
||||
$this->beConstructedWith(
|
||||
(Array)$config,
|
||||
$transformer
|
||||
);
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Indexer\OrmIndexer');
|
||||
}
|
||||
|
||||
function it_implements_the_indexer_interface_interface()
|
||||
{
|
||||
$this->shouldImplement('Sylius\Bundle\SearchBundle\Indexer\IndexerInterface');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Listener;
|
||||
|
||||
use Doctrine\Common\EventArgs;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\Event\PostFlushEventArgs;
|
||||
use Sylius\Bundle\SearchBundle\Indexer\OrmIndexer;
|
||||
|
||||
/**
|
||||
* @author Argyrios Gounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class OrmListenerSpec extends ObjectBehavior
|
||||
{
|
||||
|
||||
function let(OrmIndexer $ormIndexer)
|
||||
{
|
||||
$this->beConstructedWith($ormIndexer);
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Listener\OrmListener');
|
||||
}
|
||||
|
||||
function it_populates_the_insertion_array(LifecycleEventArgs $args)
|
||||
{
|
||||
$this->postPersist($args);
|
||||
|
||||
$this->scheduledForInsertion->shouldBeArray();
|
||||
}
|
||||
|
||||
function it_populates_the_update_array(LifecycleEventArgs $args)
|
||||
{
|
||||
$this->postUpdate($args);
|
||||
|
||||
$this->scheduledForInsertion->shouldBeArray();
|
||||
}
|
||||
|
||||
function it_populates_the_delete_array(LifecycleEventArgs $args)
|
||||
{
|
||||
$this->preRemove($args);
|
||||
|
||||
$this->scheduledForDeletion->shouldBeArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Query;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class SearchStringQuerySpec extends ObjectBehavior
|
||||
{
|
||||
|
||||
function let()
|
||||
{
|
||||
$this->beConstructedWith('search term', 'all', array('test'), true);
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Query\SearchStringQuery');
|
||||
}
|
||||
|
||||
public function it_has_a_search_term()
|
||||
{
|
||||
$this->getSearchTerm()->shouldReturn('search term');
|
||||
}
|
||||
|
||||
public function it_has_a_search_param()
|
||||
{
|
||||
$this->getSearchParam()->shouldReturn('all');
|
||||
}
|
||||
|
||||
public function it_has_some_applied_filters()
|
||||
{
|
||||
$this->getAppliedFilters()->shouldReturn(array('test'));
|
||||
}
|
||||
|
||||
public function it_should_take_in_mind_the_dropdown_filter()
|
||||
{
|
||||
$this->isDropdownFilterEnabled()->shouldReturn(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace spec\Sylius\Bundle\SearchBundle\Query;
|
||||
use PhpSpec\ObjectBehavior;
|
||||
use Sylius\Component\Core\Model\Taxon;
|
||||
|
||||
|
||||
/**
|
||||
* @author agounaris <agounaris@gmail.com>
|
||||
*/
|
||||
class TaxonQuerySpec extends ObjectBehavior
|
||||
{
|
||||
|
||||
function let(Taxon $taxon)
|
||||
{
|
||||
$this->beConstructedWith($taxon, array('test'));
|
||||
}
|
||||
|
||||
function it_is_initializable()
|
||||
{
|
||||
$this->shouldHaveType('Sylius\Bundle\SearchBundle\Query\TaxonQuery');
|
||||
}
|
||||
|
||||
public function it_has_a_taxon()
|
||||
{
|
||||
$this->getTaxon()->shouldBeAnInstanceOf('Sylius\Component\Core\Model\Taxon');
|
||||
}
|
||||
|
||||
public function it_has_some_applied_filters()
|
||||
{
|
||||
$this->getAppliedFilters()->shouldReturn(array('test'));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -46,3 +46,7 @@ sylius_cart_save:
|
|||
sylius_contact:
|
||||
resource: @SyliusWebBundle/Resources/config/routing/frontend/contact.yml
|
||||
prefix: /contact
|
||||
|
||||
sylius_search:
|
||||
resource: @SyliusWebBundle/Resources/config/routing/frontend/search.yml
|
||||
prefix: /search
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# This file is part of the Sylius package.
|
||||
# (c) Paweł Jędrzejewski
|
||||
|
||||
sylius_search_homepage:
|
||||
pattern: /
|
||||
defaults:
|
||||
_controller: SyliusSearchBundle:Search:index
|
||||
|
|
@ -76,7 +76,12 @@
|
|||
<div class="row">
|
||||
<div class="col-md-3" id="sidebar">
|
||||
{% block sidebar %}
|
||||
<div class="pull-left" style="margin-bottom: 10px;">
|
||||
{% render controller('SyliusSearchBundle:Search:form', {'request':app.request}) %}
|
||||
</div>
|
||||
<h4>{{ 'sylius.shop_by'|trans }}</h4><br>
|
||||
{% include 'SyliusSearchBundle::filter_form.html.twig' %}<br>
|
||||
|
||||
{{ knp_menu_render('sylius.frontend.taxonomies', {'template': 'SyliusWebBundle:Frontend:menu.html.twig'}) }}
|
||||
{{ render(url('sylius_partial_product_latest', {'limit': 5, 'template': 'SyliusWebBundle:Frontend/Product:latestSidebar.html.twig'})) }}
|
||||
{{ sonata_block_render({'name': '/cms/blocks/contact'}) }}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue