Adding sylius search feature

This commit is contained in:
Argyrios Gounaris 2014-08-12 10:13:51 +01:00
parent 03a2a52125
commit 31b7d18033
53 changed files with 4135 additions and 7 deletions

View file

@ -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: ~

View file

@ -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:

View file

@ -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",

View 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"

View 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"

View file

@ -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 }

View file

@ -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,
))
;

View file

@ -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(),

View file

@ -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']);
}

View file

@ -0,0 +1,5 @@
vendor/
bin/
composer.phar
composer.lock

View 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"

View 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));
}
}
}

View 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;
}
}
}

View file

@ -0,0 +1,4 @@
CHANGELOG
=========
* Initial dev release.

View 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');
}
}

View 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();
}
}

View file

@ -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()
;
}
}

View file

@ -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']);
}
}

View file

@ -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();
}
}

View 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;
}
}

View 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;
}
}

View 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]);
}
}
}
}

View 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);
}

View 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;
}
}

View file

@ -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();
}
}

View 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);
}

View 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;
}
}

View 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);
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View file

@ -0,0 +1,247 @@
SyliusSearchBundle [![Build status...](https://secure.travis-ci.org/Sylius/SyliusSearchBundle.png?branch=master)](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).

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}

View 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
{
}

View 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"
}

View file

@ -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');
}
}

View 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 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');
}
}

View file

@ -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');
}
}

View 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 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');
}
}

View file

@ -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);
}
}

View file

@ -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');
}
}

View file

@ -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');
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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'));
}
}

View file

@ -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

View file

@ -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

View file

@ -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'}) }}