Commit 255d8599 authored by Jur de Vries's avatar Jur de Vries

Force merge of 8.x-1.x-feature/rework

parent 567731c2
......@@ -73,13 +73,13 @@ before_script:
script:
# go to our Drupal module directory
- mkdir $TRAVIS_BUILD_DIR/../drupal/modules/facet_api
- cp -R $TRAVIS_BUILD_DIR/* $TRAVIS_BUILD_DIR/../drupal/modules/facet_api/
- mkdir $TRAVIS_BUILD_DIR/../drupal/modules/facetapi
- cp -R $TRAVIS_BUILD_DIR/* $TRAVIS_BUILD_DIR/../drupal/modules/facetapi/
# go to our Drupal main directory
- cd $TRAVIS_BUILD_DIR/../drupal
- ls -la $TRAVIS_BUILD_DIR/../drupal/sites/default
# Run the tests
- php core/scripts/run-tests.sh --verbose --color --concurrency 4 --php `which php` --url http://localhost "facet_api" | tee /tmp/test.txt ; export TEST_EXIT=${PIPESTATUS[0]} ; echo $TEST_EXIT
- php core/scripts/run-tests.sh --verbose --color --concurrency 4 --php `which php` --url http://localhost "facetapi" | tee /tmp/test.txt ; export TEST_EXIT=${PIPESTATUS[0]} ; echo $TEST_EXIT
# Check if we had fails in the run-tests.sh script
# Exit with the inverted value, because if there are no fails found, it will exit with 1 and for us that\
# is a good thing so invert it to 0. Travis has some issues with the exclamation mark in front so we have to fiddle a
......@@ -87,7 +87,7 @@ script:
# Also make the grep case insensitive and fail on run-tests.sh regular fails as well on fatal errors.
- TEST_OUTPUT=$(! egrep -i "([0-9]+ fails)|(PHP Fatal error)|([0-9]+ exceptions)" /tmp/test.txt > /dev/null)$?
- cd $TRAVIS_BUILD_DIR/../drupal/core
- ./vendor/bin/phpunit --group facet_api --verbose --debug --coverage-text | tee ; export TEST_PHPUNIT=${PIPESTATUS[0]} ; echo $TEST_PHPUNIT
- ./vendor/bin/phpunit --group facetapi --verbose --debug --coverage-text | tee ; export TEST_PHPUNIT=${PIPESTATUS[0]} ; echo $TEST_PHPUNIT
# if the TEST_EXIT status is 0 AND the TEST_OUTPUT status is also 0 it means we succeeded, in all other cases we
# failed.
# Re-enable when trying to get CodeSniffer doesn't return a 403 anymore.
......
# Developer documentation
Documentation used to describe technical design and thoughts.
## Facetapi api to the external world (hooks and plugins)
For the first version of the drupal 8 module we keep the absolutely necessary hooks
and from the info, only the necessary info items.
Look at the facetapi.api.php file in the drupal 7 module for extra info.
In drupal 8, we can use hooks, or events.
For now we still use hooks to keep the transition simple.
Later on we will replace the hooks by events.
### Searchers (hook_facetapi_searcher_info)
Searchers are synonymous with search pages, or environments. Multiple
searchers can share the same adapter class, but each searcher will spawn a
separate instance of the adapter. Each searcher must be unique, so it is
common practice to prefix the name with the module implementing the hook,
such as "apachesolr@searcher-x", "search_api@searcher-y", etc.
The searchers are used for the following things:
- Creating the admin pages for the facets.
- Storing facets fields per searcher (done by a hook)
The info we need per searcher are:
- label
- Adapter
- Url processor
- path
Because of the nature of drupal 8, the logic thing to do would be to make
a plugin derivative, instead of using a hook.
### Facets (hook_facetapi_facet_info)
Define all facets provided by the module.
Facets correspond with fields in the search index and are usually related to
entity properties and fields. However, it is not a requirement that the
source data be stored in Drupal. For example, if you are indexing external
RSS feeds, facets can be defined that filter by the field in the index that
stores the publication dates.
### Adapters
And adapter was a plugin in the drupal 7 module and will be the same in drupal 8.
### Query types
Query types where plugins in the drupal 7 module and will be the same in drupal 8.
## Internal concepts and plugins.
The following internal concepts and plugins can be recognized:
### Processor (helper class)
From the drupal 7 documentation:
Builds base render array used as a starting point for rendering.
The processor constructs the base render array used by widgets across all
realms. It is responsible for mapping the raw data returned by the index to
human readable values, processing hierarchical data, and building the query
strings for each facet item via the adapter's url processor plugin.
The processors are generated per facets in the processFacets method in the adapter.
Dependencies are injected upon generation (in the constructor).
### FacetapiFacet (helper class)
Wrapper around the facet definition with methods that build render arrays.
Thic class contain methods that assist in render array generation and stores
additional context about how and what generated the render arrays for
consumption by the widget plugins. Objects can also be used as if they are
the facet definitions returned by facetapi_facet_load().
### Url processor (plugin)
Url processor plugins provide a pluggable method of retrieving facet data.
Most commonly facet data is retrieved from a query string variable via $_GET,
however custom plugis can be written to retrieve data from the path as well.
In addition to facet data retrieval, the url processor plugin is also
responsible for building facet links and setting breadcrumb trails.
Each adapter instance is associated with a single url processor plugin. The
plugin is associated with the adapter via hook_facetapi_searcher_info()
implementations.
### Adapter (plugin)
Adapters are responsible for abstracting interactions with the Search backend
that are necessary for faceted search. The adapter is also responsible for
retrieving facet information passed by the user via the url processor plugin
taking the appropriate action, whether it is checking dependencies for all
enabled facets or passing the appropriate query type plugin to the backend
so that it can execute the actual facet query.
### Query types (backend dependent plugin)
Facet API does not perform any facet calculations on it's own. The query type
plugin system provides Facet API with a consistent way to tell backends what
type of query to execute in order to return the appropriate data required
for the faceted navigation display.
Query type plugins are implemented by the backends and perform the
alterations of their internal search engine query mechanisms to execute the
filters and retrieve facet data. For example, modules that integrate with
Apache Solr will set the necessary params for faceting, whereas modules that
extend the core Search module will add SQL joins, filter clauses, and COUNT
queries in order to implement faceting.
Although the actual method of querying the search engine is vastly different
per backend, Facet API operates under the assumption that the types of
queries are the same. For example, a "term" query is assumed to be a straight
filter, whereas a "range" query is assumed to be a search between two values.
Although the common query types such as "term" and "date" should be available
to all backends, it is expected that some backends will have additional query
types based on capability. For example, backends integrating with the Apache
Solr engine might have a "geospatial" query type that modules integrating
with the core Search won't have.
### Widgets (plugin)
Widgets are responsible for altering the render arrays to achieve some user
interface component. For example, the render arrays could produce a list of
clickable links or even clickable charts.
# Flow in requests
The following flow was used in search api in drupal 7.
Go to https://www.drupal.org/node/2348781 for the flow in drupal 7.
# Implementation flow
The best way to start developing is to create a module that should implement
the facetapi.
A search_api_facets module will be made, which requires the facetapi and implements
the hooks as stated in this document. Start with altering the query by the supplied facets.
There will be no ui to enable facets anywhere. We just start with hardcoded facets on
which a ui may be build later.
The first version aims to show a facet block showing counts for links.
\ No newline at end of file
services:
plugin.manager.facet_api.sort:
class: Drupal\facet_api\Sort\SortPluginManager
parent: default_plugin_manager
plugin.manager.facet_api.adapter:
class: Drupal\facet_api\Adapter\AdapterPluginManager
parent: default_plugin_manager
plugin.manager.facet_api.searcher:
class: Drupal\facet_api\Searcher\SearcherPluginManager
parent: default_plugin_manager
\ No newline at end of file
name: Facet API Dummy Backend
type: module
description: Facetapi Dummy Backend used for testing purposes
core: 8.x
package: Other
<?php
/**
* @file
* This module contains a search api dummy backend which implements
* FacetAPI interfaces.
*/
<?php
/**
* Contains Drupal\facet_api_search_api_dummy_backend\Plugin\TestSearcher
*/
namespace Drupal\facet_api_search_api_dummy_backend\Plugin\facet_api\Searcher;
use Drupal\Core\Annotation\Translation;
/**
* @FacetApiSearcher (
* id = "test_searcher",
* name = @Translation("Test Searcher"),
* label = @Translation("Test Searcher"),
* adapter = "TestAdapter",
* urlProcessor = "FacetUrlProcessorStandard",
* types = { "node" },
* path = "",
* supportFacetsMissing = TRUE,
* supportFacetsMincount = TRUE,
* includeDefaultFacets = FALSE
* )
*/
class TestSearcher extends SearcherPluginBase {
// Do nothing at the moment.
}
<?php
/**
* @file
* Contains \Drupal\facet_api_search_api_dummy_backend\Plugin\search_api\backend\DummyBackend
*/
namespace Drupal\facet_api_search_api_dummy_backend\Plugin\search_api\backend;
use Drupal\Core\Form\FormStateInterface;
use Drupal\search_api\Backend\BackendPluginBase;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\Utility;
/**
* Provides a dummy backend for testing purposes.
*
* @SearchApiBackend(
* id = "facet_api_search_api_dummy_backend",
* label = @Translation("Facet Api Test backend"),
* description = @Translation("Dummy backend implementation")
* )
*/
class DummyBackend extends BackendPluginBase {
/**
* {@inheritdoc}
*/
public function viewSettings() {
return array(
array(
'label' => 'Facet Api Dummy Backend',
'info' => 'Dummy Value',
)
);
}
/**
* {@inheritdoc}
*/
public function supportsFeature($feature) {
return $feature == 'search_api_facets';
}
/**
* {@inheritdoc}
*/
public function supportsDataType($type) {
return $type == 'search_api_test_data_type' || $type == 'search_api_altering_test_data_type';
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array('test' => '');
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['test'] = array(
'#type' => 'textfield',
'#title' => $this->t('Test'),
'#default_value' => $this->configuration['test'],
);
return $form;
}
/**
* {@inheritdoc}
*/
public function indexItems(IndexInterface $index, array $items) {
// @todo implement
return array_keys($items);
}
/**
* {@inheritdoc}
*/
public function addIndex(IndexInterface $index) {
// @todo implement
}
/**
* {@inheritdoc}
*/
public function updateIndex(IndexInterface $index) {
// @todo implement
$index->reindex();
}
/**
* {@inheritdoc}
*/
public function removeIndex($index) {
// @todo implement
}
/**
* {@inheritdoc}
*/
public function deleteItems(IndexInterface $index, array $item_ids) {
// @todo implement
}
/**
* {@inheritdoc}
*/
public function deleteAllIndexItems(IndexInterface $index) {
// @todo implement
}
/**
* {@inheritdoc}
*/
public function search(QueryInterface $query) {
// @todo implement
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Contains facet_api.module
* Contains facetapi.module
*/
use Drupal\Core\Routing\RouteMatchInterface;
......@@ -11,11 +11,11 @@ use Drupal\Core\Routing\RouteMatchInterface;
* Implements hook_help().
*/
function facet_api_help($route_name, RouteMatchInterface $route_match)
function facetapi_help($route_name, RouteMatchInterface $route_match)
{
switch ($route_name) {
// Main module help for the facet_api module.
case 'help.page.facet_api':
// Main module help for the facetapi module.
case 'help.page.facetapi':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Facetapi test') . '</p>';
......@@ -28,9 +28,21 @@ function facet_api_help($route_name, RouteMatchInterface $route_match)
* Implements hook_theme().
*/
function facet_api_theme()
function facetapi_theme()
{
$theme = [];
return $theme;
}
/**
* Get all facet definitions.
*
* @return mixed
*/
function facetapi_get_enabled_facets() {
$module_handler = \Drupal::service('module_handler');
$facet_definitions = $module_handler->invokeAll('facetapi_facet_info');
return $facet_definitions;
}
\ No newline at end of file
services:
plugin.manager.facetapi.adapter:
class: Drupal\facetapi\Adapter\AdapterPluginManager
parent: default_plugin_manager
plugin.manager.facetapi.query_type:
class: Drupal\facetapi\QueryType\QueryTypePluginManager
parent: default_plugin_manager
\ No newline at end of file
type: module
name: 'Search API facets'
description: 'Provides facet integration for search api based on facetapi.'
package: Search
core: 8.x
dependencies:
- search_api
- facetapi
\ No newline at end of file
<?php
/**
* Hooks
*/
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Query\QueryInterface;
use Drupal\Component\Plugin\PluginBase;
/**
* Implementation of hook_search_api_query_alter.
*
* @param \Drupal\search_api\Query\QueryInterface $query
*/
function search_api_facets_search_api_query_alter(QueryInterface &$query) {
// Get the adapter derivative.
// Get the plugin manager.
/** @var \Drupal\facetapi\Adapter\AdapterPluginManagerInterface $plugin_manager */
$plugin_manager = \Drupal::service('plugin.manager.facetapi.adapter');
// Get the searcher id.
// This is the id of the view.
$search_id = $query->getOption('search id');
$plugin_id = 'search_api';
$adapter = $plugin_manager->getMyOwnChangeLaterInstance($plugin_id, $search_id);
// Add the active filters.
$adapter->alterQuery($query);
}
function search_api_facets_facetapi_facet_info() {
$facet_info = array();
// Load all views.
/**
* @var EntityManagerInterface
*/
$entity_manager = \Drupal::entityManager();
// @TODO: filter on search api views.
$views = $entity_manager->getStorage('view')->loadMultiple();
// Get the views executable factory.
$views_executable_factory = Drupal::service('views.executable');
foreach ($views as $view_id => $view_entity) {
$view = $views_executable_factory->get($view_entity);
// Determine if the view is an search api view.
if (substr($view->storage->get('base_table'), 0, 17) == 'search_api_index_') {
// Get the view id.
$view_id = $view->id();
$index_id = substr($view->storage->get('base_table'), 17);
$index = Index::load($index_id);
// Get fields.
$fields = $index->getFields();
// Add facets for each view display which is of type page or block.
foreach ($view->storage->get('display') as $display) {
$supported_display_plugins = array(
'page',
'block',
);
if (in_array($display['display_plugin'], $supported_display_plugins)) {
// @TODO: This should be a function in search api. Look it up.
$searcher_name = 'search_api_views:' . $view_id . ':' . $display['id'];
foreach ($fields as $field_name => $field) {
if ( empty($facet_info[$searcher_name])) {
$facet_info[$searcher_name] = array();
}
// Determine the query type.
$term_types = array(
'string',
'integer',
);
if (in_array($field->getType(), $term_types)) {
$facet_info[$searcher_name][$field_name] = array(
'name' => $field_name,
'label' => $field->getLabel(),
'field' => $field->getFieldIdentifier(),
'query type plugin' => 'search_api_term',
'searcher' => $searcher_name,
);
}
}
}
}
}
}
return $facet_info;
}
\ No newline at end of file
<?php
/**
* User: jur
* Date: 17-04-15
* Time: 16:28
*/
namespace Drupal\search_api_facets\Plugin\facetapi\QueryType;
use Drupal\facetapi\Adapter\AdapterInterface;
use \Drupal\facetapi\QueryType\QueryTypePluginBase;
/**
* @FacetApiQueryType(
* id = "search_api_term",
* label = @Translation("Search api term"),
* description = @Translation("Search api term"),
* )
*
*/
class QueryTypeTerm extends QueryTypePluginBase {
/**
* Add facet info to the query using the backend native query object.
*
* @return mixed
*/
public function execute() {
// Alter the query here.
if (! empty($this->query)) {
$options = &$this->query->getOptions();
$field_name = $this->facet['field'];
$options['search_api_facets'][$field_name] = array(
'field' => $field_name,
'limit' => 50,
'operator' => 'and',
'min_count' => 0,
);
}
}
/**
* Build the facet information,
* so it can be rendered.
*
* @return mixed
*/
public function build() {
// TODO: Implement build() method.
$build = array();
if (! empty ($this->results)) {
$items = array();
foreach ($this->results as $result) {
$items[] = $result['filter'] . ' (' . $result['count'] . ')';
}
$build = array(
'#theme' => 'item_list',
'#items' => $items,
);
return $build;
}
return;
}
}
\ No newline at end of file
<?php
/**
* Search api adapter.
*/
namespace Drupal\search_api_facets\Plugin\facetapi\adapter;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\facetapi\Adapter\AdapterPluginBase;
use Drupal\facetapi\QueryType\QueryTypePluginManager;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\Query\ResultsCacheInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @FacetApiAdapter(
* id = "search_api",
* label = @Translation("Search api"),
* description = @Translation("Search api facet api adapter"),
* )
*
*/
class SearchApiViewsAdapter extends AdapterPluginBase {
/*
* @var Drupal\search_api\Query\QueryInterface
*/
protected $searchApiQuery;
protected $searchResultsCache;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
// Insert the module handler.
// @var ModuleHandlerInterface
$module_handler = $container->get('module_handler');
// Insert the plugin manager for query types.
// @var PluginManagerInterface
$query_type_plugin_manager = $container->get('plugin.manager.facetapi.query_type');
// Get the ResultsCache from the container.
// @var ResultsCacheInterface
$results_cache = $container->get('search_api.results_static_cache');
$plugin = new static($configuration, $plugin_id, $plugin_definition, $module_handler, $query_type_plugin_manager, $results_cache);
return $plugin;
}
public function __construct(
array $configuration,
$plugin_id, $plugin_definition,
ModuleHandlerInterface $module_handler,
QueryTypePluginManager $query_type_plugin_manager,
ResultsCacheInterface $results_cache
) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $module_handler, $query_type_plugin_manager);
$this->searchResultsCache = $results_cache;
}
/**
* Add the given facet to the query.
*
* Helper method only for search api.
* Don't move up!!!
*
* @param array $facet
* @param \Drupal\search_api\Query\QueryInterface $query
*/
public function addFacet(array $facet, QueryInterface $query) {
if (isset($this->fields[$facet['name']])) {
$options = &$query->getOptions();
$facet_info = $this->fields[$facet['name']];
if (!empty($facet['query_options'])) {
// Let facet-specific query options override the set options.
$facet_info = $facet['query_options'] + $facet_info;
}
$options['search_api_facets'][$facet['name']] = $facet_info;
}
}
/**
* Process the facets in this adapter in this adapter
* for a test only. This method should disappear later
* when facetapi does it.
*/
public function processFacets() {
// Get the facet values from the query that has been done.
// Store all information in $this->facets.
$results = $this->searchResultsCache->getResults($this->searcher_id);
return $results->getExtraData('search_api_facets');
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
<?php
/**
* Contains Drupal\facet_api\Adapter\AdapterPluginManager
* Contains Drupal\facetapi\AdapterManager
*/
namespace Drupal\facet_api\Adapter;
namespace Drupal\facetapi\Adapter;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
class AdapterPluginManager extends DefaultPluginManager {
class AdapterPluginManager extends DefaultPluginManager implements AdapterPluginManagerInterface {
/**
* @var \Drupal\facetapi\Adapter\AdapterInterface[]
*/
protected $adapters = [];
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/facet_api/adapter', $namespaces, $module_handler, 'Drupal\facet_api\AdapterInterface', 'Drupal\facet_api\Annotation\FacetApiAdapter');
$this->alterInfo('facet_api_adapter_info');
$this->setCacheBackend($cache_backend, 'facet_api_adapter_plugins');
parent::__construct('Plugin/facetapi/Adapter', $namespaces, $module_handler, 'Drupal\facetapi\Adapter\AdapterInterface', 'Drupal\facetapi\Annotation\FacetApiAdapter');
$this->alterInfo('facetapi_adapter_info');
$this->setCacheBackend($cache_backend, 'facetapi_adapter_plugins');
}
public function getMyOwnChangeLaterInstance($plugin_id, $search_id) {
if ( isset($this->adapters[$search_id])) {
return $this->adapters[$search_id];