Converted to PSR-2

This commit is contained in:
helpfulrobot 2015-11-21 19:19:20 +13:00
parent c96d4bf749
commit 314feddd48
44 changed files with 6459 additions and 5854 deletions

View File

@ -3,11 +3,11 @@
/** /**
* Base class to manage active search indexes. * Base class to manage active search indexes.
*/ */
class FullTextSearch { class FullTextSearch
{
protected static $all_indexes = null;
static protected $all_indexes = null; protected static $indexes_by_subclass = array();
static protected $indexes_by_subclass = array();
/** /**
* Optional list of index names to limit to. If left empty, all subclasses of SearchIndex * Optional list of index names to limit to. If left empty, all subclasses of SearchIndex
@ -26,7 +26,8 @@ class FullTextSearch {
* @param String $class - Class name to filter indexes by, so that all returned indexes are subclasses of provided class * @param String $class - Class name to filter indexes by, so that all returned indexes are subclasses of provided class
* @param bool $rebuild - If true, don't use cached values * @param bool $rebuild - If true, don't use cached values
*/ */
static function get_indexes($class = null, $rebuild = false) { public static function get_indexes($class = null, $rebuild = false)
{
if ($rebuild) { if ($rebuild) {
self::$all_indexes = null; self::$all_indexes = null;
self::$indexes_by_subclass = array(); self::$indexes_by_subclass = array();
@ -43,7 +44,7 @@ class FullTextSearch {
foreach ($classes as $class) { foreach ($classes as $class) {
// Check if this index is disabled // Check if this index is disabled
$hides = $class::config()->hide_ancestor; $hides = $class::config()->hide_ancestor;
if($hides) { if ($hides) {
$hidden[] = $hides; $hidden[] = $hides;
} }
@ -56,13 +57,13 @@ class FullTextSearch {
$candidates[] = $class; $candidates[] = $class;
} }
if($hidden) { if ($hidden) {
$candidates = array_diff($candidates, $hidden); $candidates = array_diff($candidates, $hidden);
} }
// Create all indexes // Create all indexes
$concrete = array(); $concrete = array();
foreach($candidates as $class) { foreach ($candidates as $class) {
$concrete[$class] = singleton($class); $concrete[$class] = singleton($class);
} }
@ -70,14 +71,15 @@ class FullTextSearch {
} }
return self::$all_indexes; return self::$all_indexes;
} } else {
else {
if (!isset(self::$indexes_by_subclass[$class])) { if (!isset(self::$indexes_by_subclass[$class])) {
$all = self::get_indexes(); $all = self::get_indexes();
$valid = array(); $valid = array();
foreach ($all as $indexclass => $instance) { foreach ($all as $indexclass => $instance) {
if (is_subclass_of($indexclass, $class)) $valid[$indexclass] = $instance; if (is_subclass_of($indexclass, $class)) {
$valid[$indexclass] = $instance;
}
} }
self::$indexes_by_subclass[$class] = $valid; self::$indexes_by_subclass[$class] = $valid;
@ -99,7 +101,8 @@ class FullTextSearch {
* *
* Alternatively you can use `FullTextSearch.indexes` to configure a list of indexes via config. * Alternatively you can use `FullTextSearch.indexes` to configure a list of indexes via config.
*/ */
static function force_index_list() { public static function force_index_list()
{
$indexes = func_get_args(); $indexes = func_get_args();
// No arguments = back to automatic // No arguments = back to automatic
@ -109,18 +112,25 @@ class FullTextSearch {
} }
// Arguments can be a single array // Arguments can be a single array
if (is_array($indexes[0])) $indexes = $indexes[0]; if (is_array($indexes[0])) {
$indexes = $indexes[0];
}
// Reset to empty first // Reset to empty first
self::$all_indexes = array(); self::$indexes_by_subclass = array(); self::$all_indexes = array();
self::$indexes_by_subclass = array();
// And parse out alternative type combos for arguments and add to allIndexes // And parse out alternative type combos for arguments and add to allIndexes
foreach ($indexes as $class => $index) { foreach ($indexes as $class => $index) {
if (is_string($index)) { $class = $index; $index = singleton($class); } if (is_string($index)) {
if (is_numeric($class)) $class = get_class($index); $class = $index;
$index = singleton($class);
}
if (is_numeric($class)) {
$class = get_class($index);
}
self::$all_indexes[$class] = $index; self::$all_indexes[$class] = $index;
} }
} }
} }

View File

@ -27,8 +27,8 @@
* - Specifying update rules that are not extractable from metadata (because the values come from functions for instance) * - Specifying update rules that are not extractable from metadata (because the values come from functions for instance)
* *
*/ */
abstract class SearchIndex extends ViewableData { abstract class SearchIndex extends ViewableData
{
/** /**
* Allows this index to hide a parent index. Specifies the name of a parent index to disable * Allows this index to hide a parent index. Specifies the name of a parent index to disable
* *
@ -37,7 +37,8 @@ abstract class SearchIndex extends ViewableData {
*/ */
private static $hide_ancestor; private static $hide_ancestor;
public function __construct() { public function __construct()
{
parent::__construct(); parent::__construct();
$this->init(); $this->init();
@ -48,7 +49,8 @@ abstract class SearchIndex extends ViewableData {
$this->buildDependancyList(); $this->buildDependancyList();
} }
public function __toString() { public function __toString()
{
return 'Search Index ' . get_class($this); return 'Search Index ' . get_class($this);
} }
@ -56,7 +58,8 @@ abstract class SearchIndex extends ViewableData {
* Examines the classes this index is built on to try and find defined fields in the class hierarchy for those classes. * Examines the classes this index is built on to try and find defined fields in the class hierarchy for those classes.
* Looks for db and viewable-data fields, although can't nessecarily find type for viewable-data fields. * Looks for db and viewable-data fields, although can't nessecarily find type for viewable-data fields.
*/ */
public function fieldData($field, $forceType = null, $extraOptions = array()) { public function fieldData($field, $forceType = null, $extraOptions = array())
{
$fullfield = str_replace(".", "_", $field); $fullfield = str_replace(".", "_", $field);
$sources = $this->getClasses(); $sources = $this->getClasses();
@ -86,16 +89,14 @@ abstract class SearchIndex extends ViewableData {
'call' => 'method', 'method' => $lookup, 'call' => 'method', 'method' => $lookup,
'through' => 'has_one', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => "{$lookup}ID" 'through' => 'has_one', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => "{$lookup}ID"
); );
} } elseif ($hasMany = $singleton->has_many($lookup)) {
else if ($hasMany = $singleton->has_many($lookup)) {
$class = $hasMany; $class = $hasMany;
$options['multi_valued'] = true; $options['multi_valued'] = true;
$options['lookup_chain'][] = array( $options['lookup_chain'][] = array(
'call' => 'method', 'method' => $lookup, 'call' => 'method', 'method' => $lookup,
'through' => 'has_many', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => $singleton->getRemoteJoinField($lookup, 'has_many') 'through' => 'has_many', 'class' => $dataclass, 'otherclass' => $class, 'foreignkey' => $singleton->getRemoteJoinField($lookup, 'has_many')
); );
} } elseif ($manyMany = $singleton->many_many($lookup)) {
else if ($manyMany = $singleton->many_many($lookup)) {
$class = $manyMany[1]; $class = $manyMany[1];
$options['multi_valued'] = true; $options['multi_valued'] = true;
$options['lookup_chain'][] = array( $options['lookup_chain'][] = array(
@ -105,14 +106,18 @@ abstract class SearchIndex extends ViewableData {
} }
if ($class) { if ($class) {
if (!isset($options['origin'])) $options['origin'] = $dataclass; if (!isset($options['origin'])) {
$options['origin'] = $dataclass;
}
$next[$class] = $options; $next[$class] = $options;
continue 2; continue 2;
} }
} }
} }
if (!$next) return $next; // Early out to avoid excessive empty looping if (!$next) {
return $next;
} // Early out to avoid excessive empty looping
$sources = $next; $sources = $next;
} }
} }
@ -122,23 +127,28 @@ abstract class SearchIndex extends ViewableData {
while (count($dataclasses)) { while (count($dataclasses)) {
$dataclass = array_shift($dataclasses); $dataclass = array_shift($dataclasses);
$type = null; $fieldoptions = $options; $type = null;
$fieldoptions = $options;
$fields = DataObject::database_fields($dataclass); $fields = DataObject::database_fields($dataclass);
if (isset($fields[$field])) { if (isset($fields[$field])) {
$type = $fields[$field]; $type = $fields[$field];
$fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field); $fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field);
} } else {
else {
$singleton = singleton($dataclass); $singleton = singleton($dataclass);
if ($singleton->hasMethod("get$field") || $singleton->hasField($field)) { if ($singleton->hasMethod("get$field") || $singleton->hasField($field)) {
$type = $singleton->castingClass($field); $type = $singleton->castingClass($field);
if (!$type) $type = 'String'; if (!$type) {
$type = 'String';
}
if ($singleton->hasMethod("get$field")) $fieldoptions['lookup_chain'][] = array('call' => 'method', 'method' => "get$field"); if ($singleton->hasMethod("get$field")) {
else $fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field); $fieldoptions['lookup_chain'][] = array('call' => 'method', 'method' => "get$field");
} else {
$fieldoptions['lookup_chain'][] = array('call' => 'property', 'property' => $field);
}
} }
} }
@ -146,7 +156,9 @@ abstract class SearchIndex extends ViewableData {
// Don't search through child classes of a class we matched on. TODO: Should we? // Don't search through child classes of a class we matched on. TODO: Should we?
$dataclasses = array_diff($dataclasses, array_values(ClassInfo::subclassesFor($dataclass))); $dataclasses = array_diff($dataclasses, array_values(ClassInfo::subclassesFor($dataclass)));
// Trim arguments off the type string // Trim arguments off the type string
if (preg_match('/^(\w+)\(/', $type, $match)) $type = $match[1]; if (preg_match('/^(\w+)\(/', $type, $match)) {
$type = $match[1];
}
// Get the origin // Get the origin
$origin = isset($fieldoptions['origin']) ? $fieldoptions['origin'] : $dataclass; $origin = isset($fieldoptions['origin']) ? $fieldoptions['origin'] : $dataclass;
@ -191,7 +203,8 @@ abstract class SearchIndex extends ViewableData {
* @param String $class - The class to include * @param String $class - The class to include
* @param array $options - TODO: Remove * @param array $options - TODO: Remove
*/ */
public function addClass($class, $options = array()) { public function addClass($class, $options = array())
{
if ($this->fulltextFields || $this->filterFields || $this->sortFields) { if ($this->fulltextFields || $this->filterFields || $this->sortFields) {
throw new Exception('Can\'t add class to Index after fields have already been added'); throw new Exception('Can\'t add class to Index after fields have already been added');
} }
@ -210,7 +223,10 @@ abstract class SearchIndex extends ViewableData {
/** /**
* Get the classes added by addClass * Get the classes added by addClass
*/ */
public function getClasses() { return $this->classes; } public function getClasses()
{
return $this->classes;
}
/** /**
* Add a field that should be fulltext searchable * Add a field that should be fulltext searchable
@ -218,11 +234,15 @@ abstract class SearchIndex extends ViewableData {
* @param String $forceType - The type to force this field as (required in some cases, when not detectable from metadata) * @param String $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
* @param String $extraOptions - Dependent on search implementation * @param String $extraOptions - Dependent on search implementation
*/ */
public function addFulltextField($field, $forceType = null, $extraOptions = array()) { public function addFulltextField($field, $forceType = null, $extraOptions = array())
{
$this->fulltextFields = array_merge($this->fulltextFields, $this->fieldData($field, $forceType, $extraOptions)); $this->fulltextFields = array_merge($this->fulltextFields, $this->fieldData($field, $forceType, $extraOptions));
} }
public function getFulltextFields() { return $this->fulltextFields; } public function getFulltextFields()
{
return $this->fulltextFields;
}
/** /**
* Add a field that should be filterable * Add a field that should be filterable
@ -230,11 +250,15 @@ abstract class SearchIndex extends ViewableData {
* @param String $forceType - The type to force this field as (required in some cases, when not detectable from metadata) * @param String $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
* @param String $extraOptions - Dependent on search implementation * @param String $extraOptions - Dependent on search implementation
*/ */
public function addFilterField($field, $forceType = null, $extraOptions = array()) { public function addFilterField($field, $forceType = null, $extraOptions = array())
{
$this->filterFields = array_merge($this->filterFields, $this->fieldData($field, $forceType, $extraOptions)); $this->filterFields = array_merge($this->filterFields, $this->fieldData($field, $forceType, $extraOptions));
} }
public function getFilterFields() { return $this->filterFields; } public function getFilterFields()
{
return $this->filterFields;
}
/** /**
* Add a field that should be sortable * Add a field that should be sortable
@ -242,11 +266,15 @@ abstract class SearchIndex extends ViewableData {
* @param String $forceType - The type to force this field as (required in some cases, when not detectable from metadata) * @param String $forceType - The type to force this field as (required in some cases, when not detectable from metadata)
* @param String $extraOptions - Dependent on search implementation * @param String $extraOptions - Dependent on search implementation
*/ */
public function addSortField($field, $forceType = null, $extraOptions = array()) { public function addSortField($field, $forceType = null, $extraOptions = array())
{
$this->sortFields = array_merge($this->sortFields, $this->fieldData($field, $forceType, $extraOptions)); $this->sortFields = array_merge($this->sortFields, $this->fieldData($field, $forceType, $extraOptions));
} }
public function getSortFields() { return $this->sortFields; } public function getSortFields()
{
return $this->sortFields;
}
/** /**
* Add all database-backed text fields as fulltext searchable fields. * Add all database-backed text fields as fulltext searchable fields.
@ -254,14 +282,19 @@ abstract class SearchIndex extends ViewableData {
* For every class included in the index, examines those classes and all subclasses looking for "Text" database * For every class included in the index, examines those classes and all subclasses looking for "Text" database
* fields (Varchar, Text, HTMLText, etc) and adds them all as fulltext searchable fields. * fields (Varchar, Text, HTMLText, etc) and adds them all as fulltext searchable fields.
*/ */
public function addAllFulltextFields($includeSubclasses = true) { public function addAllFulltextFields($includeSubclasses = true)
{
foreach ($this->getClasses() as $class => $options) { foreach ($this->getClasses() as $class => $options) {
foreach (SearchIntrospection::hierarchy($class, $includeSubclasses, true) as $dataclass) { foreach (SearchIntrospection::hierarchy($class, $includeSubclasses, true) as $dataclass) {
$fields = DataObject::database_fields($dataclass); $fields = DataObject::database_fields($dataclass);
foreach ($fields as $field => $type) { foreach ($fields as $field => $type) {
if (preg_match('/^(\w+)\(/', $type, $match)) $type = $match[1]; if (preg_match('/^(\w+)\(/', $type, $match)) {
if (is_subclass_of($type, 'StringField')) $this->addFulltextField($field); $type = $match[1];
}
if (is_subclass_of($type, 'StringField')) {
$this->addFulltextField($field);
}
} }
} }
} }
@ -273,34 +306,45 @@ abstract class SearchIndex extends ViewableData {
* *
* @return MultipleArrayIterator * @return MultipleArrayIterator
*/ */
public function getFieldsIterator() { public function getFieldsIterator()
{
return new MultipleArrayIterator($this->fulltextFields, $this->filterFields, $this->sortFields); return new MultipleArrayIterator($this->fulltextFields, $this->filterFields, $this->sortFields);
} }
public function excludeVariantState($state) { public function excludeVariantState($state)
{
$this->excludedVariantStates[] = $state; $this->excludedVariantStates[] = $state;
} }
/** Returns true if some variant state should be ignored */ /** Returns true if some variant state should be ignored */
public function variantStateExcluded($state) { public function variantStateExcluded($state)
{
foreach ($this->excludedVariantStates as $excludedstate) { foreach ($this->excludedVariantStates as $excludedstate) {
$matches = true; $matches = true;
foreach ($excludedstate as $variant => $variantstate) { foreach ($excludedstate as $variant => $variantstate) {
if (!isset($state[$variant]) || $state[$variant] != $variantstate) { $matches = false; break; } if (!isset($state[$variant]) || $state[$variant] != $variantstate) {
$matches = false;
break;
}
} }
if ($matches) return true; if ($matches) {
return true;
}
} }
} }
public $dependancyList = array(); public $dependancyList = array();
public function buildDependancyList() { public function buildDependancyList()
{
$this->dependancyList = array_keys($this->getClasses()); $this->dependancyList = array_keys($this->getClasses());
foreach ($this->getFieldsIterator() as $name => $field) { foreach ($this->getFieldsIterator() as $name => $field) {
if (!isset($field['class'])) continue; if (!isset($field['class'])) {
continue;
}
SearchIntrospection::add_unique_by_ancestor($this->dependancyList, $field['class']); SearchIntrospection::add_unique_by_ancestor($this->dependancyList, $field['class']);
} }
} }
@ -311,12 +355,15 @@ abstract class SearchIndex extends ViewableData {
* Returns an array where each member is all the fields and the classes that are at the end of some * Returns an array where each member is all the fields and the classes that are at the end of some
* specific lookup chain from one of the base classes * specific lookup chain from one of the base classes
*/ */
public function getDerivedFields() { public function getDerivedFields()
{
if ($this->derivedFields === null) { if ($this->derivedFields === null) {
$this->derivedFields = array(); $this->derivedFields = array();
foreach ($this->getFieldsIterator() as $name => $field) { foreach ($this->getFieldsIterator() as $name => $field) {
if (count($field['lookup_chain']) < 2) continue; if (count($field['lookup_chain']) < 2) {
continue;
}
$key = sha1($field['base'].serialize($field['lookup_chain'])); $key = sha1($field['base'].serialize($field['lookup_chain']));
$fieldname = "{$field['class']}:{$field['field']}"; $fieldname = "{$field['class']}:{$field['field']}";
@ -324,8 +371,7 @@ abstract class SearchIndex extends ViewableData {
if (isset($this->derivedFields[$key])) { if (isset($this->derivedFields[$key])) {
$this->derivedFields[$key]['fields'][$fieldname] = $fieldname; $this->derivedFields[$key]['fields'][$fieldname] = $fieldname;
SearchIntrospection::add_unique_by_ancestor($this->derivedFields['classes'], $field['class']); SearchIntrospection::add_unique_by_ancestor($this->derivedFields['classes'], $field['class']);
} } else {
else {
$chain = array_reverse($field['lookup_chain']); $chain = array_reverse($field['lookup_chain']);
array_shift($chain); array_shift($chain);
@ -350,7 +396,8 @@ abstract class SearchIndex extends ViewableData {
* @param Array $state - The variant state of the object * @param Array $state - The variant state of the object
* @return string - The document ID as a string * @return string - The document ID as a string
*/ */
public function getDocumentIDForState($base, $id, $state) { public function getDocumentIDForState($base, $id, $state)
{
ksort($state); ksort($state);
$parts = array('id' => $id, 'base' => $base, 'state' => json_encode($state)); $parts = array('id' => $id, 'base' => $base, 'state' => json_encode($state));
return implode('-', array_values($parts)); return implode('-', array_values($parts));
@ -364,7 +411,8 @@ abstract class SearchIndex extends ViewableData {
* @param Boolean $includesubs - TODO: Probably going away * @param Boolean $includesubs - TODO: Probably going away
* @return string - The document ID as a string * @return string - The document ID as a string
*/ */
public function getDocumentID($object, $base, $includesubs) { public function getDocumentID($object, $base, $includesubs)
{
return $this->getDocumentIDForState($base, $object->ID, SearchVariant::current_state($base, $includesubs)); return $this->getDocumentIDForState($base, $object->ID, SearchVariant::current_state($base, $includesubs));
} }
@ -375,13 +423,16 @@ abstract class SearchIndex extends ViewableData {
* @param Array $field - The field definition to use * @param Array $field - The field definition to use
* @return Mixed - The value of the field, or null if we couldn't look it up for some reason * @return Mixed - The value of the field, or null if we couldn't look it up for some reason
*/ */
protected function _getFieldValue($object, $field) { protected function _getFieldValue($object, $field)
{
set_error_handler(create_function('$no, $str', 'throw new Exception("HTML Parse Error: ".$str);'), E_ALL); set_error_handler(create_function('$no, $str', 'throw new Exception("HTML Parse Error: ".$str);'), E_ALL);
try { try {
foreach ($field['lookup_chain'] as $step) { foreach ($field['lookup_chain'] as $step) {
// Just fail if we've fallen off the end of the chain // Just fail if we've fallen off the end of the chain
if (!$object) return null; if (!$object) {
return null;
}
// If we're looking up this step on an array or SS_List, do the step on every item, merge result // If we're looking up this step on an array or SS_List, do the step on every item, merge result
if (is_array($object) || $object instanceof SS_List) { if (is_array($object) || $object instanceof SS_List) {
@ -391,15 +442,18 @@ abstract class SearchIndex extends ViewableData {
if ($step['call'] == 'method') { if ($step['call'] == 'method') {
$method = $step['method']; $method = $step['method'];
$item = $item->$method(); $item = $item->$method();
} } else {
else {
$property = $step['property']; $property = $step['property'];
$item = $item->$property; $item = $item->$property;
} }
if ($item instanceof SS_List) $next = array_merge($next, $item->toArray()); if ($item instanceof SS_List) {
elseif (is_array($item)) $next = array_merge($next, $item); $next = array_merge($next, $item->toArray());
else $next[] = $item; } elseif (is_array($item)) {
$next = array_merge($next, $item);
} else {
$next[] = $item;
}
} }
$object = $next; $object = $next;
@ -409,20 +463,18 @@ abstract class SearchIndex extends ViewableData {
if ($step['call'] == 'method') { if ($step['call'] == 'method') {
$method = $step['method']; $method = $step['method'];
$object = $object->$method(); $object = $object->$method();
} } elseif ($step['call'] == 'variant') {
elseif ($step['call'] == 'variant') {
$variants = SearchVariant::variants($field['base'], true); $variants = SearchVariant::variants($field['base'], true);
$variant = $variants[$step['variant']]; $method = $step['method']; $variant = $variants[$step['variant']];
$method = $step['method'];
$object = $variant->$method($object); $object = $variant->$method($object);
} } else {
else {
$property = $step['property']; $property = $step['property'];
$object = $object->$property; $object = $object->$property;
} }
} }
} }
} } catch (Exception $e) {
catch (Exception $e) {
$object = null; $object = null;
} }
@ -442,13 +494,13 @@ abstract class SearchIndex extends ViewableData {
* @param $fields * @param $fields
* @return array * @return array
*/ */
public function getDirtyIDs($class, $id, $statefulids, $fields) { public function getDirtyIDs($class, $id, $statefulids, $fields)
{
$dirty = array(); $dirty = array();
// First, if this object is directly contained in the index, add it // First, if this object is directly contained in the index, add it
foreach ($this->classes as $searchclass => $options) { foreach ($this->classes as $searchclass => $options) {
if ($searchclass == $class || ($options['include_children'] && is_subclass_of($class, $searchclass))) { if ($searchclass == $class || ($options['include_children'] && is_subclass_of($class, $searchclass))) {
$base = ClassInfo::baseDataClass($searchclass); $base = ClassInfo::baseDataClass($searchclass);
$dirty[$base] = array(); $dirty[$base] = array();
foreach ($statefulids as $statefulid) { foreach ($statefulids as $statefulid) {
@ -464,8 +516,12 @@ abstract class SearchIndex extends ViewableData {
// Then, for every derived field // Then, for every derived field
foreach ($this->getDerivedFields() as $derivation) { foreach ($this->getDerivedFields() as $derivation) {
// If the this object is a subclass of any of the classes we want a field from // If the this object is a subclass of any of the classes we want a field from
if (!SearchIntrospection::is_subclass_of($class, $derivation['classes'])) continue; if (!SearchIntrospection::is_subclass_of($class, $derivation['classes'])) {
if (!array_intersect_key($fields, $derivation['fields'])) continue; continue;
}
if (!array_intersect_key($fields, $derivation['fields'])) {
continue;
}
foreach (SearchVariant::reindex_states($class, false) as $state) { foreach (SearchVariant::reindex_states($class, false) as $state) {
SearchVariant::activate_state($state); SearchVariant::activate_state($state);
@ -478,8 +534,7 @@ abstract class SearchIndex extends ViewableData {
singleton($step['class'])->extend('augmentSQL', $sql); singleton($step['class'])->extend('augmentSQL', $sql);
$ids = $sql->execute()->column(); $ids = $sql->execute()->column();
} } elseif ($step['through'] == 'has_many') {
else if ($step['through'] == 'has_many') {
$sql = new SQLQuery('"'.$step['class'].'"."ID"', '"'.$step['class'].'"', '"'.$step['otherclass'].'"."ID" IN ('.implode(',', $ids).')'); $sql = new SQLQuery('"'.$step['class'].'"."ID"', '"'.$step['class'].'"', '"'.$step['otherclass'].'"."ID" IN ('.implode(',', $ids).')');
$sql->addInnerJoin($step['otherclass'], '"'.$step['class'].'"."ID" = "'.$step['otherclass'].'"."'.$step['foreignkey'].'"'); $sql->addInnerJoin($step['otherclass'], '"'.$step['class'].'"."ID" = "'.$step['otherclass'].'"."'.$step['foreignkey'].'"');
singleton($step['class'])->extend('augmentSQL', $sql); singleton($step['class'])->extend('augmentSQL', $sql);
@ -492,7 +547,9 @@ abstract class SearchIndex extends ViewableData {
if ($ids) { if ($ids) {
$base = $derivation['base']; $base = $derivation['base'];
if (!isset($dirty[$base])) $dirty[$base] = array(); if (!isset($dirty[$base])) {
$dirty[$base] = array();
}
foreach ($ids as $id) { foreach ($ids as $id) {
$statefulid = array('id' => $id, 'state' => $state); $statefulid = array('id' => $id, 'state' => $state);
@ -526,32 +583,39 @@ abstract class SearchIndex extends ViewableData {
/** /**
* A search index that does nothing. Useful for testing * A search index that does nothing. Useful for testing
*/ */
abstract class SearchIndex_Null extends SearchIndex { abstract class SearchIndex_Null extends SearchIndex
{
public function add($object)
{
}
public function add($object) { } public function delete($base, $id, $state)
{
public function delete($base, $id, $state) { } }
public function commit() { }
public function commit()
{
}
} }
/** /**
* A search index that just records actions. Useful for testing * A search index that just records actions. Useful for testing
*/ */
abstract class SearchIndex_Recording extends SearchIndex { abstract class SearchIndex_Recording extends SearchIndex
{
public $added = array(); public $added = array();
public $deleted = array(); public $deleted = array();
public $committed = false; public $committed = false;
public function reset() { public function reset()
{
$this->added = array(); $this->added = array();
$this->deleted = array(); $this->deleted = array();
$this->committed = false; $this->committed = false;
} }
public function add($object) { public function add($object)
{
$res = array(); $res = array();
$res['ID'] = $object->ID; $res['ID'] = $object->ID;
@ -564,13 +628,16 @@ abstract class SearchIndex_Recording extends SearchIndex {
$this->added[] = $res; $this->added[] = $res;
} }
public function getAdded($fields = array()) { public function getAdded($fields = array())
{
$res = array(); $res = array();
foreach ($this->added as $added) { foreach ($this->added as $added) {
$filtered = array(); $filtered = array();
foreach ($fields as $field) { foreach ($fields as $field) {
if (isset($added[$field])) $filtered[$field] = $added[$field]; if (isset($added[$field])) {
$filtered[$field] = $added[$field];
}
} }
$res[] = $filtered; $res[] = $filtered;
} }
@ -578,25 +645,29 @@ abstract class SearchIndex_Recording extends SearchIndex {
return $res; return $res;
} }
public function delete($base, $id, $state) { public function delete($base, $id, $state)
{
$this->deleted[] = array('base' => $base, 'id' => $id, 'state' => $state); $this->deleted[] = array('base' => $base, 'id' => $id, 'state' => $state);
} }
public function commit() { public function commit()
{
$this->committed = true; $this->committed = true;
} }
public function getIndexName() { public function getIndexName()
{
return get_class($this); return get_class($this);
} }
public function getIsCommitted() { public function getIsCommitted()
{
return $this->committed; return $this->committed;
} }
public function getService() { public function getService()
{
// Causes commits to the service to be redirected back to the same object // Causes commits to the service to be redirected back to the same object
return $this; return $this;
} }
} }

View File

@ -3,8 +3,8 @@
/** /**
* Some additional introspection tools that are used often by the fulltext search code * Some additional introspection tools that are used often by the fulltext search code
*/ */
class SearchIntrospection { class SearchIntrospection
{
protected static $ancestry = array(); protected static $ancestry = array();
/** /**
@ -14,7 +14,8 @@ class SearchIntrospection {
* @param $of * @param $of
* @return bool * @return bool
*/ */
static function is_subclass_of ($class, $of) { public static function is_subclass_of($class, $of)
{
$ancestry = isset(self::$ancestry[$class]) ? self::$ancestry[$class] : (self::$ancestry[$class] = ClassInfo::ancestry($class)); $ancestry = isset(self::$ancestry[$class]) ? self::$ancestry[$class] : (self::$ancestry[$class] = ClassInfo::ancestry($class));
return is_array($of) ? (bool)array_intersect($of, $ancestry) : array_key_exists($of, $ancestry); return is_array($of) ? (bool)array_intersect($of, $ancestry) : array_key_exists($of, $ancestry);
} }
@ -30,18 +31,27 @@ class SearchIntrospection {
* @param bool $dataOnly - True to only return classes that have tables * @param bool $dataOnly - True to only return classes that have tables
* @return Array - Integer keys, String values as classes sorted by depth (most super first) * @return Array - Integer keys, String values as classes sorted by depth (most super first)
*/ */
static function hierarchy ($class, $includeSubclasses = true, $dataOnly = false) { public static function hierarchy($class, $includeSubclasses = true, $dataOnly = false)
{
$key = "$class!" . ($includeSubclasses ? 'sc' : 'an') . '!' . ($dataOnly ? 'do' : 'al'); $key = "$class!" . ($includeSubclasses ? 'sc' : 'an') . '!' . ($dataOnly ? 'do' : 'al');
if (!isset(self::$hierarchy[$key])) { if (!isset(self::$hierarchy[$key])) {
$classes = array_values(ClassInfo::ancestry($class)); $classes = array_values(ClassInfo::ancestry($class));
if ($includeSubclasses) $classes = array_unique(array_merge($classes, array_values(ClassInfo::subclassesFor($class)))); if ($includeSubclasses) {
$classes = array_unique(array_merge($classes, array_values(ClassInfo::subclassesFor($class))));
}
$idx = array_search('DataObject', $classes); $idx = array_search('DataObject', $classes);
if ($idx !== false) array_splice($classes, 0, $idx+1); if ($idx !== false) {
array_splice($classes, 0, $idx+1);
}
if ($dataOnly) foreach($classes as $i => $class) { if ($dataOnly) {
if (!DataObject::has_own_table($class)) unset($classes[$i]); foreach ($classes as $i => $class) {
if (!DataObject::has_own_table($class)) {
unset($classes[$i]);
}
}
} }
self::$hierarchy[$key] = $classes; self::$hierarchy[$key] = $classes;
@ -53,9 +63,12 @@ class SearchIntrospection {
/** /**
* Add classes to list, keeping only the parent when parent & child are both in list after add * Add classes to list, keeping only the parent when parent & child are both in list after add
*/ */
static function add_unique_by_ancestor(&$list, $class) { public static function add_unique_by_ancestor(&$list, $class)
{
// If class already has parent in list, just ignore // If class already has parent in list, just ignore
if (self::is_subclass_of($class, $list)) return; if (self::is_subclass_of($class, $list)) {
return;
}
// Strip out any subclasses of $class already in the list // Strip out any subclasses of $class already in the list
$children = ClassInfo::subclassesFor($class); $children = ClassInfo::subclassesFor($class);
@ -68,11 +81,13 @@ class SearchIntrospection {
/** /**
* Does this class, it's parent (or optionally one of it's children) have the passed extension attached? * Does this class, it's parent (or optionally one of it's children) have the passed extension attached?
*/ */
static function has_extension($class, $extension, $includeSubclasses = true) { public static function has_extension($class, $extension, $includeSubclasses = true)
{
foreach (self::hierarchy($class, $includeSubclasses) as $relatedclass) { foreach (self::hierarchy($class, $includeSubclasses) as $relatedclass) {
if ($relatedclass::has_extension($extension)) return true; if ($relatedclass::has_extension($extension)) {
return true;
}
} }
return false; return false;
} }
} }

View File

@ -5,12 +5,12 @@
* *
* API very much still in flux. * API very much still in flux.
*/ */
class SearchQuery extends ViewableData { class SearchQuery extends ViewableData
{
public static $missing = null;
public static $present = null;
static $missing = null; public static $default_page_size = 10;
static $present = null;
static $default_page_size = 10;
/** These are public, but only for index & variant access - API users should not manually access these */ /** These are public, but only for index & variant access - API users should not manually access these */
@ -26,9 +26,14 @@ class SearchQuery extends ViewableData {
/** These are the API functions */ /** These are the API functions */
function __construct() { public function __construct()
if (self::$missing === null) self::$missing = new stdClass(); {
if (self::$present === null) self::$present = new stdClass(); if (self::$missing === null) {
self::$missing = new stdClass();
}
if (self::$present === null) {
self::$present = new stdClass();
}
} }
/** /**
@ -37,7 +42,8 @@ class SearchQuery extends ViewableData {
* @param array $boost Map of composite field names to float values. The higher the value, * @param array $boost Map of composite field names to float values. The higher the value,
* the more important the field gets for relevancy. * the more important the field gets for relevancy.
*/ */
function search($text, $fields = null, $boost = array()) { public function search($text, $fields = null, $boost = array())
{
$this->search[] = array('text' => $text, 'fields' => $fields ? (array)$fields : null, 'boost' => $boost, 'fuzzy' => false); $this->search[] = array('text' => $text, 'fields' => $fields ? (array)$fields : null, 'boost' => $boost, 'fuzzy' => false);
} }
@ -50,11 +56,13 @@ class SearchQuery extends ViewableData {
* @param array $fields See {@link search()} * @param array $fields See {@link search()}
* @param array $boost See {@link search()} * @param array $boost See {@link search()}
*/ */
function fuzzysearch($text, $fields = null, $boost = array()) { public function fuzzysearch($text, $fields = null, $boost = array())
{
$this->search[] = array('text' => $text, 'fields' => $fields ? (array)$fields : null, 'boost' => $boost, 'fuzzy' => true); $this->search[] = array('text' => $text, 'fields' => $fields ? (array)$fields : null, 'boost' => $boost, 'fuzzy' => true);
} }
function inClass($class, $includeSubclasses = true) { public function inClass($class, $includeSubclasses = true)
{
$this->classes[] = array('class' => $class, 'includeSubclasses' => $includeSubclasses); $this->classes[] = array('class' => $class, 'includeSubclasses' => $includeSubclasses);
} }
@ -65,7 +73,8 @@ class SearchQuery extends ViewableData {
* @param String $field Composite name of the field * @param String $field Composite name of the field
* @param Mixed $values Scalar value, array of values, or an instance of SearchQuery_Range * @param Mixed $values Scalar value, array of values, or an instance of SearchQuery_Range
*/ */
function filter($field, $values) { public function filter($field, $values)
{
$requires = isset($this->require[$field]) ? $this->require[$field] : array(); $requires = isset($this->require[$field]) ? $this->require[$field] : array();
$values = is_array($values) ? $values : array($values); $values = is_array($values) ? $values : array($values);
$this->require[$field] = array_merge($requires, $values); $this->require[$field] = array_merge($requires, $values);
@ -77,30 +86,36 @@ class SearchQuery extends ViewableData {
* @param String $field * @param String $field
* @param mixed $values * @param mixed $values
*/ */
function exclude($field, $values) { public function exclude($field, $values)
{
$excludes = isset($this->exclude[$field]) ? $this->exclude[$field] : array(); $excludes = isset($this->exclude[$field]) ? $this->exclude[$field] : array();
$values = is_array($values) ? $values : array($values); $values = is_array($values) ? $values : array($values);
$this->exclude[$field] = array_merge($excludes, $values); $this->exclude[$field] = array_merge($excludes, $values);
} }
function start($start) { public function start($start)
{
$this->start = $start; $this->start = $start;
} }
function limit($limit) { public function limit($limit)
{
$this->limit = $limit; $this->limit = $limit;
} }
function page($page) { public function page($page)
{
$this->start = $page * self::$default_page_size; $this->start = $page * self::$default_page_size;
$this->limit = self::$default_page_size; $this->limit = self::$default_page_size;
} }
function isfiltered() { public function isfiltered()
{
return $this->search || $this->classes || $this->require || $this->exclude; return $this->search || $this->classes || $this->require || $this->exclude;
} }
function __toString() { public function __toString()
{
return "Search Query\n"; return "Search Query\n";
} }
} }
@ -109,25 +124,29 @@ class SearchQuery extends ViewableData {
* Create one of these and pass as one of the values in filter or exclude to filter or exclude by a (possibly * Create one of these and pass as one of the values in filter or exclude to filter or exclude by a (possibly
* open ended) range * open ended) range
*/ */
class SearchQuery_Range { class SearchQuery_Range
{
public $start = null; public $start = null;
public $end = null; public $end = null;
function __construct($start = null, $end = null) { public function __construct($start = null, $end = null)
{
$this->start = $start; $this->start = $start;
$this->end = $end; $this->end = $end;
} }
function start($start) { public function start($start)
{
$this->start = $start; $this->start = $start;
} }
function end($end) { public function end($end)
{
$this->end = $end; $this->end = $end;
} }
function isfiltered() { public function isfiltered()
{
return $this->start !== null || $this->end !== null; return $this->start !== null || $this->end !== null;
} }
} }

View File

@ -12,16 +12,19 @@
* *
* TODO: The way we bind in is awful hacky. * TODO: The way we bind in is awful hacky.
*/ */
class SearchUpdater extends Object { class SearchUpdater extends Object
{
/** /**
* Replace the database object with a subclass that captures all manipulations and passes them to us * Replace the database object with a subclass that captures all manipulations and passes them to us
*/ */
static function bind_manipulation_capture() { public static function bind_manipulation_capture()
{
global $databaseConfig; global $databaseConfig;
$current = DB::getConn(); $current = DB::getConn();
if (!$current || @$current->isManipulationCapture) return; // If not yet set, or its already captured, just return if (!$current || @$current->isManipulationCapture) {
return;
} // If not yet set, or its already captured, just return
$type = get_class($current); $type = get_class($current);
$file = TEMP_FOLDER."/.cache.SMC.$type"; $file = TEMP_FOLDER."/.cache.SMC.$type";
@ -58,9 +61,9 @@ class SearchUpdater extends Object {
DB::setConn($captured); DB::setConn($captured);
} }
static $registered = false; public static $registered = false;
/** @var SearchUpdateProcessor */ /** @var SearchUpdateProcessor */
static $processor = null; public static $processor = null;
/** /**
* Called by the SearchManiplateCapture database adapter with every manipulation made against the database. * Called by the SearchManiplateCapture database adapter with every manipulation made against the database.
@ -72,7 +75,8 @@ class SearchUpdater extends Object {
* it expects the actual class of the object to be present as a table, regardless of if any fields changed in that table * it expects the actual class of the object to be present as a table, regardless of if any fields changed in that table
* (so a class => array( 'fields' => array() ) item), in order to find the actual class for a set of table manipulations * (so a class => array( 'fields' => array() ) item), in order to find the actual class for a set of table manipulations
*/ */
static function handle_manipulation($manipulation) { public static function handle_manipulation($manipulation)
{
// First, extract any state that is in the manipulation itself // First, extract any state that is in the manipulation itself
foreach ($manipulation as $table => $details) { foreach ($manipulation as $table => $details) {
$manipulation[$table]['class'] = $table; $manipulation[$table]['class'] = $table;
@ -86,7 +90,9 @@ class SearchUpdater extends Object {
$writes = array(); $writes = array();
foreach ($manipulation as $table => $details) { foreach ($manipulation as $table => $details) {
if (!isset($details['id']) || !isset($details['fields'])) continue; if (!isset($details['id']) || !isset($details['fields'])) {
continue;
}
$id = $details['id']; $id = $details['id'];
$state = $details['state']; $state = $details['state'];
@ -109,7 +115,7 @@ class SearchUpdater extends Object {
); );
} }
// Otherwise update the class label if it's more specific than the currently recorded one // Otherwise update the class label if it's more specific than the currently recorded one
else if (is_subclass_of($class, $writes[$key]['class'])) { elseif (is_subclass_of($class, $writes[$key]['class'])) {
$writes[$key]['class'] = $class; $writes[$key]['class'] = $class;
} }
@ -133,7 +139,8 @@ class SearchUpdater extends Object {
* *
* @param array $writes * @param array $writes
*/ */
public static function process_writes($writes) { public static function process_writes($writes)
{
foreach ($writes as $write) { foreach ($writes as $write) {
// For every index // For every index
foreach (FullTextSearch::get_indexes() as $index => $instance) { foreach (FullTextSearch::get_indexes() as $index => $instance) {
@ -170,7 +177,8 @@ class SearchUpdater extends Object {
/** /**
* Throw away the recorded dirty IDs without doing anything with them. * Throw away the recorded dirty IDs without doing anything with them.
*/ */
static function clear_dirty_indexes() { public static function clear_dirty_indexes()
{
self::$processor = null; self::$processor = null;
} }
@ -179,19 +187,25 @@ class SearchUpdater extends Object {
* either immediately update the indexes, queue a messsage to update the indexes at some point in the future, or * either immediately update the indexes, queue a messsage to update the indexes at some point in the future, or
* just throw the dirty IDs away. * just throw the dirty IDs away.
*/ */
static function flush_dirty_indexes() { public static function flush_dirty_indexes()
if (!self::$processor) return; {
if (!self::$processor) {
return;
}
self::$processor->triggerProcessing(); self::$processor->triggerProcessing();
self::$processor = null; self::$processor = null;
} }
} }
class SearchUpdater_BindManipulationCaptureFilter implements RequestFilter { class SearchUpdater_BindManipulationCaptureFilter implements RequestFilter
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model) { {
public function preRequest(SS_HTTPRequest $request, Session $session, DataModel $model)
{
SearchUpdater::bind_manipulation_capture(); SearchUpdater::bind_manipulation_capture();
} }
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) { public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model)
{
/* NOP */ /* NOP */
} }
} }
@ -203,11 +217,14 @@ class SearchUpdater_BindManipulationCaptureFilter implements RequestFilter {
* indexed. This causes the object to be marked for deletion from the index. * indexed. This causes the object to be marked for deletion from the index.
*/ */
class SearchUpdater_ObjectHandler extends DataExtension { class SearchUpdater_ObjectHandler extends DataExtension
{
public function onAfterDelete() { public function onAfterDelete()
{
// Calling delete() on empty objects does nothing // Calling delete() on empty objects does nothing
if (!$this->owner->ID) return; if (!$this->owner->ID) {
return;
}
// Force SearchUpdater to mark this record as dirty // Force SearchUpdater to mark this record as dirty
$manipulation = array( $manipulation = array(
@ -224,7 +241,8 @@ class SearchUpdater_ObjectHandler extends DataExtension {
/** /**
* Forces this object to trigger a re-index in the current state * Forces this object to trigger a re-index in the current state
*/ */
public function triggerReindex() { public function triggerReindex()
{
if (!$this->owner->ID) { if (!$this->owner->ID) {
return; return;
} }
@ -252,5 +270,4 @@ class SearchUpdater_ObjectHandler extends DataExtension {
SearchUpdater::process_writes($writes); SearchUpdater::process_writes($writes);
} }
} }

View File

@ -4,9 +4,11 @@
* A Search Variant handles decorators and other situations where the items to reindex or search through are modified * A Search Variant handles decorators and other situations where the items to reindex or search through are modified
* from the default state - for instance, dealing with Versioned or Subsite * from the default state - for instance, dealing with Versioned or Subsite
*/ */
abstract class SearchVariant { abstract class SearchVariant
{
function __construct() {} public function __construct()
{
}
/*** OVERRIDES start here */ /*** OVERRIDES start here */
@ -19,25 +21,25 @@ abstract class SearchVariant {
* Return false if there is something missing from the environment (probably a * Return false if there is something missing from the environment (probably a
* not installed module) that means this variant can't apply to any class * not installed module) that means this variant can't apply to any class
*/ */
abstract function appliesToEnvironment(); abstract public function appliesToEnvironment();
/** /**
* Return true if this variant applies to the passed class & subclass * Return true if this variant applies to the passed class & subclass
*/ */
abstract function appliesTo($class, $includeSubclasses); abstract public function appliesTo($class, $includeSubclasses);
/** /**
* Return the current state * Return the current state
*/ */
abstract function currentState(); abstract public function currentState();
/** /**
* Return all states to step through to reindex all items * Return all states to step through to reindex all items
*/ */
abstract function reindexStates(); abstract public function reindexStates();
/** /**
* Activate the passed state * Activate the passed state
*/ */
abstract function activateState($state); abstract public function activateState($state);
/** /**
* Apply this variant to a search query * Apply this variant to a search query
@ -67,7 +69,8 @@ abstract class SearchVariant {
* @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
* @return array - An array of (string)$variantClassName => (Object)$variantInstance pairs * @return array - An array of (string)$variantClassName => (Object)$variantInstance pairs
*/ */
public static function variants($class = null, $includeSubclasses = true) { public static function variants($class = null, $includeSubclasses = true)
{
if (!$class) { if (!$class) {
if (self::$variants === null) { if (self::$variants === null) {
$classes = ClassInfo::subclassesFor('SearchVariant'); $classes = ClassInfo::subclassesFor('SearchVariant');
@ -77,7 +80,9 @@ abstract class SearchVariant {
$ref = new ReflectionClass($variantclass); $ref = new ReflectionClass($variantclass);
if ($ref->isInstantiable()) { if ($ref->isInstantiable()) {
$variant = singleton($variantclass); $variant = singleton($variantclass);
if ($variant->appliesToEnvironment()) $concrete[$variantclass] = $variant; if ($variant->appliesToEnvironment()) {
$concrete[$variantclass] = $variant;
}
} }
} }
@ -85,15 +90,16 @@ abstract class SearchVariant {
} }
return self::$variants; return self::$variants;
} } else {
else {
$key = $class . '!' . $includeSubclasses; $key = $class . '!' . $includeSubclasses;
if (!isset(self::$class_variants[$key])) { if (!isset(self::$class_variants[$key])) {
self::$class_variants[$key] = array(); self::$class_variants[$key] = array();
foreach (self::variants() as $variantclass => $instance) { foreach (self::variants() as $variantclass => $instance) {
if ($instance->appliesTo($class, $includeSubclasses)) self::$class_variants[$key][$variantclass] = $instance; if ($instance->appliesTo($class, $includeSubclasses)) {
self::$class_variants[$key][$variantclass] = $instance;
}
} }
} }
@ -119,11 +125,14 @@ abstract class SearchVariant {
* *
* @return An object with one method, call() * @return An object with one method, call()
*/ */
static function with($class = null, $includeSubclasses = true) { public static function with($class = null, $includeSubclasses = true)
{
// Make the cache key // Make the cache key
$key = $class ? $class . '!' . $includeSubclasses : '!'; $key = $class ? $class . '!' . $includeSubclasses : '!';
// If no SearchVariant_Caller instance yet, create it // If no SearchVariant_Caller instance yet, create it
if (!isset(self::$call_instances[$key])) self::$call_instances[$key] = new SearchVariant_Caller(self::variants($class, $includeSubclasses)); if (!isset(self::$call_instances[$key])) {
self::$call_instances[$key] = new SearchVariant_Caller(self::variants($class, $includeSubclasses));
}
// Then return it // Then return it
return self::$call_instances[$key]; return self::$call_instances[$key];
} }
@ -133,7 +142,8 @@ abstract class SearchVariant {
* *
* SearchVariant::call(...) ==== SearchVariant::with()->call(...); * SearchVariant::call(...) ==== SearchVariant::with()->call(...);
*/ */
static function call($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) { public static function call($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null)
{
return self::with()->call($method, $a1, $a2, $a3, $a4, $a5, $a6, $a7); return self::with()->call($method, $a1, $a2, $a3, $a4, $a5, $a6, $a7);
} }
@ -142,7 +152,8 @@ abstract class SearchVariant {
* @static * @static
* @return array * @return array
*/ */
static function current_state($class = null, $includeSubclasses = true) { public static function current_state($class = null, $includeSubclasses = true)
{
$state = array(); $state = array();
foreach (self::variants($class, $includeSubclasses) as $variant => $instance) { foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
$state[$variant] = $instance->currentState(); $state[$variant] = $instance->currentState();
@ -157,9 +168,12 @@ abstract class SearchVariant {
* SearchVariant::current_state() * SearchVariant::current_state()
* @return void * @return void
*/ */
static function activate_state($state) { public static function activate_state($state)
{
foreach (self::variants() as $variant => $instance) { foreach (self::variants() as $variant => $instance) {
if (isset($state[$variant])) $instance->activateState($state[$variant]); if (isset($state[$variant])) {
$instance->activateState($state[$variant]);
}
} }
} }
@ -171,11 +185,14 @@ abstract class SearchVariant {
* @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class * @param bool $includeSubclasses - True if variants should be included if they apply to at least one subclass of $class
* @return SearchVariant_ReindexStateIteratorRet - The iterator to foreach loop over * @return SearchVariant_ReindexStateIteratorRet - The iterator to foreach loop over
*/ */
static function reindex_states($class = null, $includeSubclasses = true) { public static function reindex_states($class = null, $includeSubclasses = true)
{
$allstates = array(); $allstates = array();
foreach (self::variants($class, $includeSubclasses) as $variant => $instance) { foreach (self::variants($class, $includeSubclasses) as $variant => $instance) {
if ($states = $instance->reindexStates()) $allstates[$variant] = $states; if ($states = $instance->reindexStates()) {
$allstates[$variant] = $states;
}
} }
return $allstates ? new CombinationsArrayIterator($allstates) : array(array()); return $allstates ? new CombinationsArrayIterator($allstates) : array(array());
@ -185,24 +202,28 @@ abstract class SearchVariant {
/** /**
* Internal utility class used to hold the state of the SearchVariant::with call * Internal utility class used to hold the state of the SearchVariant::with call
*/ */
class SearchVariant_Caller { class SearchVariant_Caller
{
protected $variants = null; protected $variants = null;
function __construct($variants) { public function __construct($variants)
{
$this->variants = $variants; $this->variants = $variants;
} }
function call($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) { public function call($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null)
{
$values = array(); $values = array();
foreach ($this->variants as $variant) { foreach ($this->variants as $variant) {
if (method_exists($variant, $method)) { if (method_exists($variant, $method)) {
$value = $variant->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7); $value = $variant->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
if ($value !== null) $values[] = $value; if ($value !== null) {
$values[] = $value;
}
} }
} }
return $values; return $values;
} }
} }

View File

@ -1,40 +1,47 @@
<?php <?php
class SearchVariantSiteTreeSubsitesPolyhome extends SearchVariant { class SearchVariantSiteTreeSubsitesPolyhome extends SearchVariant
{
function appliesToEnvironment() { public function appliesToEnvironment()
{
return class_exists('Subsite') && class_exists('SubsitePolyhome'); return class_exists('Subsite') && class_exists('SubsitePolyhome');
} }
function appliesTo($class, $includeSubclasses) { public function appliesTo($class, $includeSubclasses)
{
return SearchIntrospection::has_extension($class, 'SiteTreeSubsitesPolyhome', $includeSubclasses); return SearchIntrospection::has_extension($class, 'SiteTreeSubsitesPolyhome', $includeSubclasses);
} }
function currentState() { public function currentState()
{
return Subsite::currentSubsiteID(); return Subsite::currentSubsiteID();
} }
function reindexStates() { public function reindexStates()
{
static $ids = null; static $ids = null;
if ($ids === null) { if ($ids === null) {
$ids = array(0); $ids = array(0);
foreach (DataObject::get('Subsite') as $subsite) $ids[] = $subsite->ID; foreach (DataObject::get('Subsite') as $subsite) {
$ids[] = $subsite->ID;
}
} }
return $ids; return $ids;
} }
function activateState($state) { public function activateState($state)
{
if (Controller::has_curr()) { if (Controller::has_curr()) {
Subsite::changeSubsite($state); Subsite::changeSubsite($state);
} } else {
else {
// TODO: This is a nasty hack - calling Subsite::changeSubsite after request ends // TODO: This is a nasty hack - calling Subsite::changeSubsite after request ends
// throws error because no current controller to access session on // throws error because no current controller to access session on
$_REQUEST['SubsiteID'] = $state; $_REQUEST['SubsiteID'] = $state;
} }
} }
function alterDefinition($base, $index) { public function alterDefinition($base, $index)
{
$self = get_class($this); $self = get_class($this);
$index->filterFields['_subsite'] = array( $index->filterFields['_subsite'] = array(
@ -48,22 +55,26 @@ class SearchVariantSiteTreeSubsitesPolyhome extends SearchVariant {
); );
} }
public function alterQuery($query, $index) { public function alterQuery($query, $index)
{
$subsite = Subsite::currentSubsiteID(); $subsite = Subsite::currentSubsiteID();
$query->filter('_subsite', array($subsite, SearchQuery::$missing)); $query->filter('_subsite', array($subsite, SearchQuery::$missing));
} }
static $subsites = null; public static $subsites = null;
/** /**
* We need _really_ complicated logic to find just the changed subsites (because we use versions there's no explicit * We need _really_ complicated logic to find just the changed subsites (because we use versions there's no explicit
* deletes, just new versions with different members) so just always use all of them * deletes, just new versions with different members) so just always use all of them
*/ */
function extractManipulationWriteState(&$writes) { public function extractManipulationWriteState(&$writes)
{
$self = get_class($this); $self = get_class($this);
foreach ($writes as $key => $write) { foreach ($writes as $key => $write) {
if (!$this->appliesTo($write['class'], true)) continue; if (!$this->appliesTo($write['class'], true)) {
continue;
}
if (self::$subsites === null) { if (self::$subsites === null) {
$query = new SQLQuery('ID', 'Subsite'); $query = new SQLQuery('ID', 'Subsite');
@ -81,5 +92,4 @@ class SearchVariantSiteTreeSubsitesPolyhome extends SearchVariant {
$writes[$key]['statefulids'] = $next; $writes[$key]['statefulids'] = $next;
} }
} }
} }

View File

@ -1,12 +1,14 @@
<?php <?php
class SearchVariantSubsites extends SearchVariant { class SearchVariantSubsites extends SearchVariant
{
function appliesToEnvironment() { public function appliesToEnvironment()
{
return class_exists('Subsite'); return class_exists('Subsite');
} }
function appliesTo($class, $includeSubclasses) { public function appliesTo($class, $includeSubclasses)
{
// Include all DataExtensions that contain a SubsiteID. // Include all DataExtensions that contain a SubsiteID.
// TODO: refactor subsites to inherit a common interface, so we can run introspection once only. // TODO: refactor subsites to inherit a common interface, so we can run introspection once only.
return SearchIntrospection::has_extension($class, 'SiteTreeSubsites', $includeSubclasses) || return SearchIntrospection::has_extension($class, 'SiteTreeSubsites', $includeSubclasses) ||
@ -15,29 +17,35 @@ class SearchVariantSubsites extends SearchVariant {
SearchIntrospection::has_extension($class, 'SiteConfigSubsites', $includeSubclasses); SearchIntrospection::has_extension($class, 'SiteConfigSubsites', $includeSubclasses);
} }
function currentState() { public function currentState()
{
return (string)Subsite::currentSubsiteID(); return (string)Subsite::currentSubsiteID();
} }
function reindexStates() { public function reindexStates()
{
static $ids = null; static $ids = null;
if ($ids === null) { if ($ids === null) {
$ids = array('0'); $ids = array('0');
foreach (DataObject::get('Subsite') as $subsite) $ids[] = (string)$subsite->ID; foreach (DataObject::get('Subsite') as $subsite) {
$ids[] = (string)$subsite->ID;
}
} }
return $ids; return $ids;
} }
function activateState($state) { public function activateState($state)
{
// We always just set the $_GET variable rather than store in Session - this always works, has highest priority // We always just set the $_GET variable rather than store in Session - this always works, has highest priority
// in Subsite::currentSubsiteID() and doesn't persist unlike Subsite::changeSubsite // in Subsite::currentSubsiteID() and doesn't persist unlike Subsite::changeSubsite
$_GET['SubsiteID'] = $state; $_GET['SubsiteID'] = $state;
Permission::flush_permission_cache(); Permission::flush_permission_cache();
} }
function alterDefinition($base, $index) { public function alterDefinition($base, $index)
{
$self = get_class($this); $self = get_class($this);
$index->filterFields['_subsite'] = array( $index->filterFields['_subsite'] = array(
@ -51,22 +59,26 @@ class SearchVariantSubsites extends SearchVariant {
); );
} }
function alterQuery($query, $index) { public function alterQuery($query, $index)
{
$subsite = Subsite::currentSubsiteID(); $subsite = Subsite::currentSubsiteID();
$query->filter('_subsite', array($subsite, SearchQuery::$missing)); $query->filter('_subsite', array($subsite, SearchQuery::$missing));
} }
static $subsites = null; public static $subsites = null;
/** /**
* We need _really_ complicated logic to find just the changed subsites (because we use versions there's no explicit * We need _really_ complicated logic to find just the changed subsites (because we use versions there's no explicit
* deletes, just new versions with different members) so just always use all of them * deletes, just new versions with different members) so just always use all of them
*/ */
function extractManipulationWriteState(&$writes) { public function extractManipulationWriteState(&$writes)
{
$self = get_class($this); $self = get_class($this);
foreach ($writes as $key => $write) { foreach ($writes as $key => $write) {
if (!$this->appliesTo($write['class'], true)) continue; if (!$this->appliesTo($write['class'], true)) {
continue;
}
if (self::$subsites === null) { if (self::$subsites === null) {
$query = new SQLQuery('"ID"', '"Subsite"'); $query = new SQLQuery('"ID"', '"Subsite"');
@ -84,5 +96,4 @@ class SearchVariantSubsites extends SearchVariant {
$writes[$key]['statefulids'] = $next; $writes[$key]['statefulids'] = $next;
} }
} }
} }

View File

@ -1,20 +1,32 @@
<?php <?php
class SearchVariantVersioned extends SearchVariant { class SearchVariantVersioned extends SearchVariant
{
function appliesToEnvironment() { public function appliesToEnvironment()
{
return class_exists('Versioned'); return class_exists('Versioned');
} }
function appliesTo($class, $includeSubclasses) { public function appliesTo($class, $includeSubclasses)
{
return SearchIntrospection::has_extension($class, 'Versioned', $includeSubclasses); return SearchIntrospection::has_extension($class, 'Versioned', $includeSubclasses);
} }
function currentState() { return Versioned::current_stage(); } public function currentState()
function reindexStates() { return array('Stage', 'Live'); } {
function activateState($state) { Versioned::reading_stage($state); } return Versioned::current_stage();
}
public function reindexStates()
{
return array('Stage', 'Live');
}
public function activateState($state)
{
Versioned::reading_stage($state);
}
function alterDefinition($base, $index) { public function alterDefinition($base, $index)
{
$self = get_class($this); $self = get_class($this);
$index->filterFields['_versionedstage'] = array( $index->filterFields['_versionedstage'] = array(
@ -28,12 +40,14 @@ class SearchVariantVersioned extends SearchVariant {
); );
} }
public function alterQuery($query, $index) { public function alterQuery($query, $index)
{
$stage = Versioned::current_stage(); $stage = Versioned::current_stage();
$query->filter('_versionedstage', array($stage, SearchQuery::$missing)); $query->filter('_versionedstage', array($stage, SearchQuery::$missing));
} }
function extractManipulationState(&$manipulation) { public function extractManipulationState(&$manipulation)
{
$self = get_class($this); $self = get_class($this);
foreach ($manipulation as $table => $details) { foreach ($manipulation as $table => $details) {
@ -52,7 +66,8 @@ class SearchVariantVersioned extends SearchVariant {
} }
} }
function extractStates(&$table, &$ids, &$fields) { public function extractStates(&$table, &$ids, &$fields)
{
$class = $table; $class = $table;
$suffix = null; $suffix = null;
@ -66,5 +81,4 @@ class SearchVariantVersioned extends SearchVariant {
} }
} }
} }
} }

View File

@ -3,8 +3,8 @@
/** /**
* Provides batching of search updates * Provides batching of search updates
*/ */
abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor { abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor
{
/** /**
* List of batches to be processed * List of batches to be processed
* *
@ -45,7 +45,8 @@ abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor {
*/ */
private static $batch_soft_cap = 10; private static $batch_soft_cap = 10;
public function __construct() { public function __construct()
{
parent::__construct(); parent::__construct();
$this->batches = array(); $this->batches = array();
@ -57,12 +58,14 @@ abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor {
* *
* @param int $batch Index of the batch * @param int $batch Index of the batch
*/ */
protected function setBatch($batch) { protected function setBatch($batch)
{
$this->currentBatch = $batch; $this->currentBatch = $batch;
} }
protected function getSource() { protected function getSource()
if(isset($this->batches[$this->currentBatch])) { {
if (isset($this->batches[$this->currentBatch])) {
return $this->batches[$this->currentBatch]; return $this->batches[$this->currentBatch];
} }
} }
@ -72,12 +75,17 @@ abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor {
* *
* @return boolean * @return boolean
*/ */
public function process() { public function process()
{
// Skip blank queues // Skip blank queues
if(empty($this->batches)) return true; if (empty($this->batches)) {
return true;
}
// Don't re-process completed queue // Don't re-process completed queue
if($this->currentBatch >= count($this->batches)) return true; if ($this->currentBatch >= count($this->batches)) {
return true;
}
// Send current patch to indexes // Send current patch to indexes
$this->prepareIndexes(); $this->prepareIndexes();
@ -93,10 +101,13 @@ abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor {
* @param array $source Source input * @param array $source Source input
* @return array Batches * @return array Batches
*/ */
protected function segmentBatches($source) { protected function segmentBatches($source)
{
// Measure batch_size // Measure batch_size
$batchSize = Config::inst()->get(get_class(), 'batch_size'); $batchSize = Config::inst()->get(get_class(), 'batch_size');
if($batchSize === 0) return array($source); if ($batchSize === 0) {
return array($source);
}
$softCap = Config::inst()->get(get_class(), 'batch_soft_cap'); $softCap = Config::inst()->get(get_class(), 'batch_soft_cap');
// Clear batches // Clear batches
@ -106,18 +117,24 @@ abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor {
// Build batches from data // Build batches from data
foreach ($source as $base => $statefulids) { foreach ($source as $base => $statefulids) {
if (!$statefulids) continue; if (!$statefulids) {
continue;
}
foreach ($statefulids as $stateKey => $statefulid) { foreach ($statefulids as $stateKey => $statefulid) {
$state = $statefulid['state']; $state = $statefulid['state'];
$ids = $statefulid['ids']; $ids = $statefulid['ids'];
if(!$ids) continue; if (!$ids) {
continue;
}
// Extract items from $ids until empty // Extract items from $ids until empty
while($ids) { while ($ids) {
// Estimate maximum number of items to take for this iteration, allowing for the soft cap // Estimate maximum number of items to take for this iteration, allowing for the soft cap
$take = $batchSize - $currentSize; $take = $batchSize - $currentSize;
if(count($ids) <= $take + $softCap) $take += $softCap; if (count($ids) <= $take + $softCap) {
$take += $softCap;
}
$items = array_slice($ids, 0, $take, true); $items = array_slice($ids, 0, $take, true);
$ids = array_slice($ids, count($items), null, true); $ids = array_slice($ids, count($items), null, true);
@ -132,7 +149,7 @@ abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor {
) )
); );
$current = $current ? array_merge_recursive($current, $merge) : $merge; $current = $current ? array_merge_recursive($current, $merge) : $merge;
if($currentSize >= $batchSize) { if ($currentSize >= $batchSize) {
$batches[] = $current; $batches[] = $current;
$current = array(); $current = array();
$currentSize = 0; $currentSize = 0;
@ -141,17 +158,21 @@ abstract class SearchUpdateBatchedProcessor extends SearchUpdateProcessor {
} }
} }
// Add incomplete batch // Add incomplete batch
if($currentSize) $batches[] = $current; if ($currentSize) {
$batches[] = $current;
}
return $batches; return $batches;
} }
public function batchData() { public function batchData()
{
$this->batches = $this->segmentBatches($this->dirty); $this->batches = $this->segmentBatches($this->dirty);
$this->setBatch(0); $this->setBatch(0);
} }
public function triggerProcessing() { public function triggerProcessing()
{
$this->batchData(); $this->batchData();
} }
} }

View File

@ -1,9 +1,11 @@
<?php <?php
if(!interface_exists('QueuedJob')) return; if (!interface_exists('QueuedJob')) {
return;
class SearchUpdateCommitJobProcessor implements QueuedJob { }
class SearchUpdateCommitJobProcessor implements QueuedJob
{
/** /**
* The QueuedJob queue to use when processing commits * The QueuedJob queue to use when processing commits
* *
@ -76,28 +78,32 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
* @param string $startAfter Start date * @param string $startAfter Start date
* @return int The ID of the next queuedjob to run. This could be a new one or an existing one. * @return int The ID of the next queuedjob to run. This could be a new one or an existing one.
*/ */
public static function queue($dirty = true, $startAfter = null) { public static function queue($dirty = true, $startAfter = null)
{
$commit = Injector::inst()->create(__CLASS__); $commit = Injector::inst()->create(__CLASS__);
$id = singleton('QueuedJobService')->queueJob($commit, $startAfter); $id = singleton('QueuedJobService')->queueJob($commit, $startAfter);
if($dirty) { if ($dirty) {
$indexes = FullTextSearch::get_indexes(); $indexes = FullTextSearch::get_indexes();
static::$dirty_indexes = array_keys($indexes); static::$dirty_indexes = array_keys($indexes);
} }
return $id; return $id;
} }
public function getJobType() { public function getJobType()
{
return Config::inst()->get(__CLASS__, 'commit_queue'); return Config::inst()->get(__CLASS__, 'commit_queue');
} }
public function getSignature() { public function getSignature()
{
// There is only ever one commit job on the queue so the signature is consistent // There is only ever one commit job on the queue so the signature is consistent
// See QueuedJobService::queueJob() for the code that prevents duplication // See QueuedJobService::queueJob() for the code that prevents duplication
return __CLASS__; return __CLASS__;
} }
public function getTitle() { public function getTitle()
{
return "FullTextSearch Commit Job"; return "FullTextSearch Commit Job";
} }
@ -106,39 +112,44 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
* *
* @return array * @return array
*/ */
public function getAllIndexes() { public function getAllIndexes()
if(empty($this->indexes)) { {
if (empty($this->indexes)) {
$indexes = FullTextSearch::get_indexes(); $indexes = FullTextSearch::get_indexes();
$this->indexes = array_keys($indexes); $this->indexes = array_keys($indexes);
} }
return $this->indexes; return $this->indexes;
} }
public function jobFinished() { public function jobFinished()
{
// If we've indexed exactly as many as we would like, we are done // If we've indexed exactly as many as we would like, we are done
return $this->skipped return $this->skipped
|| (count($this->getAllIndexes()) <= count($this->completed)); || (count($this->getAllIndexes()) <= count($this->completed));
} }
public function prepareForRestart() { public function prepareForRestart()
{
// NOOP // NOOP
} }
public function afterComplete() { public function afterComplete()
{
// NOOP // NOOP
} }
/** /**
* Abort this job, potentially rescheduling a replacement if there is still work to do * Abort this job, potentially rescheduling a replacement if there is still work to do
*/ */
protected function discardJob() { protected function discardJob()
{
$this->skipped = true; $this->skipped = true;
// If we do not have dirty records, then assume that these dirty records were committed // If we do not have dirty records, then assume that these dirty records were committed
// already this request (but probably another job), so we don't need to commit anything else. // already this request (but probably another job), so we don't need to commit anything else.
// This could occur if we completed multiple searchupdate jobs in a prior request, and // This could occur if we completed multiple searchupdate jobs in a prior request, and
// we only need one commit job to commit all of them in the current request. // we only need one commit job to commit all of them in the current request.
if(empty(static::$dirty_indexes)) { if (empty(static::$dirty_indexes)) {
$this->addMessage("Indexing already completed this request: Discarding this job"); $this->addMessage("Indexing already completed this request: Discarding this job");
return; return;
} }
@ -158,10 +169,11 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
static::queue(false, $runat); static::queue(false, $runat);
} }
public function process() { public function process()
{
// If we have already run an instance of SearchUpdateCommitJobProcessor this request, immediately // If we have already run an instance of SearchUpdateCommitJobProcessor this request, immediately
// quit this job to prevent hitting warming search limits in Solr // quit this job to prevent hitting warming search limits in Solr
if(static::$has_run) { if (static::$has_run) {
$this->discardJob(); $this->discardJob();
return true; return true;
} }
@ -187,10 +199,11 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
* @param SolrIndex $index * @param SolrIndex $index
* @throws Exception * @throws Exception
*/ */
protected function commitIndex($index) { protected function commitIndex($index)
{
// Skip index if this is already complete // Skip index if this is already complete
$name = get_class($index); $name = get_class($index);
if(in_array($name, $this->completed)) { if (in_array($name, $this->completed)) {
$this->addMessage("Skipping already comitted index {$name}"); $this->addMessage("Skipping already comitted index {$name}");
return; return;
} }
@ -201,7 +214,7 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
$this->addMessage("Committing index {$name} was successful"); $this->addMessage("Committing index {$name} was successful");
// If this index is currently marked as dirty, it's now clean // If this index is currently marked as dirty, it's now clean
if(in_array($name, static::$dirty_indexes)) { if (in_array($name, static::$dirty_indexes)) {
static::$dirty_indexes = array_diff(static::$dirty_indexes, array($name)); static::$dirty_indexes = array_diff(static::$dirty_indexes, array($name));
} }
@ -209,11 +222,13 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
$this->completed[] = $name; $this->completed[] = $name;
} }
public function setup() { public function setup()
{
// NOOP // NOOP
} }
public function getJobData() { public function getJobData()
{
$data = new stdClass(); $data = new stdClass();
$data->totalSteps = count($this->getAllIndexes()); $data->totalSteps = count($this->getAllIndexes());
$data->currentStep = count($this->completed); $data->currentStep = count($this->completed);
@ -228,7 +243,8 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
return $data; return $data;
} }
public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages) { public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages)
{
$this->isComplete = $isComplete; $this->isComplete = $isComplete;
$this->messages = $messages; $this->messages = $messages;
@ -237,13 +253,14 @@ class SearchUpdateCommitJobProcessor implements QueuedJob {
$this->indexes = $jobData->indexes; $this->indexes = $jobData->indexes;
} }
public function addMessage($message, $severity='INFO') { public function addMessage($message, $severity='INFO')
{
$severity = strtoupper($severity); $severity = strtoupper($severity);
$this->messages[] = '[' . date('Y-m-d H:i:s') . "][$severity] $message"; $this->messages[] = '[' . date('Y-m-d H:i:s') . "][$severity] $message";
} }
public function getMessages() { public function getMessages()
{
return $this->messages; return $this->messages;
} }
} }

View File

@ -1,7 +1,9 @@
<?php <?php
class SearchUpdateImmediateProcessor extends SearchUpdateProcessor { class SearchUpdateImmediateProcessor extends SearchUpdateProcessor
public function triggerProcessing() { {
public function triggerProcessing()
{
$this->process(); $this->process();
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
class SearchUpdateMessageQueueProcessor extends SearchUpdateProcessor { class SearchUpdateMessageQueueProcessor extends SearchUpdateProcessor
{
/** /**
* The MessageQueue to use when processing updates * The MessageQueue to use when processing updates
* @config * @config
@ -8,7 +9,8 @@ class SearchUpdateMessageQueueProcessor extends SearchUpdateProcessor {
*/ */
private static $reindex_queue = "search_indexing"; private static $reindex_queue = "search_indexing";
public function triggerProcessing() { public function triggerProcessing()
{
MessageQueue::send( MessageQueue::send(
Config::inst()->get('SearchMessageQueueUpdater', 'reindex_queue'), Config::inst()->get('SearchMessageQueueUpdater', 'reindex_queue'),
new MethodInvocationMessage($this, "process") new MethodInvocationMessage($this, "process")

View File

@ -1,7 +1,7 @@
<?php <?php
abstract class SearchUpdateProcessor { abstract class SearchUpdateProcessor
{
/** /**
* List of dirty records to process in format * List of dirty records to process in format
* *
@ -26,25 +26,26 @@ abstract class SearchUpdateProcessor {
*/ */
protected $dirty; protected $dirty;
public function __construct() { public function __construct()
{
$this->dirty = array(); $this->dirty = array();
} }
public function addDirtyIDs($class, $statefulids, $index) { public function addDirtyIDs($class, $statefulids, $index)
{
$base = ClassInfo::baseDataClass($class); $base = ClassInfo::baseDataClass($class);
$forclass = isset($this->dirty[$base]) ? $this->dirty[$base] : array(); $forclass = isset($this->dirty[$base]) ? $this->dirty[$base] : array();
foreach ($statefulids as $statefulid) { foreach ($statefulids as $statefulid) {
$id = $statefulid['id']; $id = $statefulid['id'];
$state = $statefulid['state']; $statekey = serialize($state); $state = $statefulid['state'];
$statekey = serialize($state);
if (!isset($forclass[$statekey])) { if (!isset($forclass[$statekey])) {
$forclass[$statekey] = array('state' => $state, 'ids' => array($id => array($index))); $forclass[$statekey] = array('state' => $state, 'ids' => array($id => array($index)));
} } elseif (!isset($forclass[$statekey]['ids'][$id])) {
else if (!isset($forclass[$statekey]['ids'][$id])) {
$forclass[$statekey]['ids'][$id] = array($index); $forclass[$statekey]['ids'][$id] = array($index);
} } elseif (array_search($index, $forclass[$statekey]['ids'][$id]) === false) {
else if (array_search($index, $forclass[$statekey]['ids'][$id]) === false) {
$forclass[$statekey]['ids'][$id][] = $index; $forclass[$statekey]['ids'][$id][] = $index;
// dirty count stays the same // dirty count stays the same
} }
@ -58,13 +59,16 @@ abstract class SearchUpdateProcessor {
* *
* @return array * @return array
*/ */
protected function prepareIndexes() { protected function prepareIndexes()
{
$originalState = SearchVariant::current_state(); $originalState = SearchVariant::current_state();
$dirtyIndexes = array(); $dirtyIndexes = array();
$dirty = $this->getSource(); $dirty = $this->getSource();
$indexes = FullTextSearch::get_indexes(); $indexes = FullTextSearch::get_indexes();
foreach ($dirty as $base => $statefulids) { foreach ($dirty as $base => $statefulids) {
if (!$statefulids) continue; if (!$statefulids) {
continue;
}
foreach ($statefulids as $statefulid) { foreach ($statefulids as $statefulid) {
$state = $statefulid['state']; $state = $statefulid['state'];
@ -106,7 +110,8 @@ abstract class SearchUpdateProcessor {
* @param SolrIndex $index Index object * @param SolrIndex $index Index object
* @return bool Flag indicating success * @return bool Flag indicating success
*/ */
protected function commitIndex($index) { protected function commitIndex($index)
{
return $index->commit() !== false; return $index->commit() !== false;
} }
@ -115,7 +120,8 @@ abstract class SearchUpdateProcessor {
* *
* @return array * @return array
*/ */
protected function getSource() { protected function getSource()
{
return $this->dirty; return $this->dirty;
} }
@ -124,11 +130,14 @@ abstract class SearchUpdateProcessor {
* *
* @return bool Flag indicating success * @return bool Flag indicating success
*/ */
public function process() { public function process()
{
// Generate and commit all instances // Generate and commit all instances
$indexes = $this->prepareIndexes(); $indexes = $this->prepareIndexes();
foreach ($indexes as $index) { foreach ($indexes as $index) {
if(!$this->commitIndex($index)) return false; if (!$this->commitIndex($index)) {
return false;
}
} }
return true; return true;
} }

View File

@ -1,9 +1,11 @@
<?php <?php
if(!interface_exists('QueuedJob')) return; if (!interface_exists('QueuedJob')) {
return;
class SearchUpdateQueuedJobProcessor extends SearchUpdateBatchedProcessor implements QueuedJob { }
class SearchUpdateQueuedJobProcessor extends SearchUpdateBatchedProcessor implements QueuedJob
{
/** /**
* The QueuedJob queue to use when processing updates * The QueuedJob queue to use when processing updates
* @config * @config
@ -13,42 +15,51 @@ class SearchUpdateQueuedJobProcessor extends SearchUpdateBatchedProcessor implem
protected $messages = array(); protected $messages = array();
public function triggerProcessing() { public function triggerProcessing()
{
parent::triggerProcessing(); parent::triggerProcessing();
singleton('QueuedJobService')->queueJob($this); singleton('QueuedJobService')->queueJob($this);
} }
public function getTitle() { public function getTitle()
{
return "FullTextSearch Update Job"; return "FullTextSearch Update Job";
} }
public function getSignature() { public function getSignature()
{
return md5(get_class($this) . time() . mt_rand(0, 100000)); return md5(get_class($this) . time() . mt_rand(0, 100000));
} }
public function getJobType() { public function getJobType()
{
return Config::inst()->get('SearchUpdateQueuedJobProcessor', 'reindex_queue'); return Config::inst()->get('SearchUpdateQueuedJobProcessor', 'reindex_queue');
} }
public function jobFinished() { public function jobFinished()
{
return $this->currentBatch >= count($this->batches); return $this->currentBatch >= count($this->batches);
} }
public function setup() { public function setup()
{
// NOP // NOP
} }
public function prepareForRestart() { public function prepareForRestart()
{
// NOP // NOP
} }
public function afterComplete() { public function afterComplete()
{
// Once indexing is complete, commit later in order to avoid solr limits // Once indexing is complete, commit later in order to avoid solr limits
// see http://stackoverflow.com/questions/7512945/how-to-fix-exceeded-limit-of-maxwarmingsearchers // see http://stackoverflow.com/questions/7512945/how-to-fix-exceeded-limit-of-maxwarmingsearchers
SearchUpdateCommitJobProcessor::queue(); SearchUpdateCommitJobProcessor::queue();
} }
public function getJobData() { public function getJobData()
{
$data = new stdClass(); $data = new stdClass();
$data->totalSteps = count($this->batches); $data->totalSteps = count($this->batches);
$data->currentStep = $this->currentBatch; $data->currentStep = $this->currentBatch;
@ -62,7 +73,8 @@ class SearchUpdateQueuedJobProcessor extends SearchUpdateBatchedProcessor implem
return $data; return $data;
} }
public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages) { public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages)
{
$this->isComplete = $isComplete; $this->isComplete = $isComplete;
$this->messages = $messages; $this->messages = $messages;
@ -70,15 +82,17 @@ class SearchUpdateQueuedJobProcessor extends SearchUpdateBatchedProcessor implem
$this->currentBatch = $jobData->currentBatch; $this->currentBatch = $jobData->currentBatch;
} }
public function addMessage($message, $severity='INFO') { public function addMessage($message, $severity='INFO')
{
$severity = strtoupper($severity); $severity = strtoupper($severity);
$this->messages[] = '[' . date('Y-m-d H:i:s') . "][$severity] $message"; $this->messages[] = '[' . date('Y-m-d H:i:s') . "][$severity] $message";
} }
public function process() { public function process()
{
$result = parent::process(); $result = parent::process();
if($this->jobFinished()) { if ($this->jobFinished()) {
$this->addMessage("All batched updates complete. Queuing commit job"); $this->addMessage("All batched updates complete. Queuing commit job");
} }

View File

@ -5,8 +5,8 @@ use Monolog\Handler\StreamHandler;
use Monolog\Logger; use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class Solr { class Solr
{
/** /**
* Configuration on where to find the solr server and how to get new index configurations into it. * Configuration on where to find the solr server and how to get new index configurations into it.
* *
@ -47,7 +47,8 @@ class Solr {
* Update the configuration for Solr. See $solr_options for a discussion of the accepted array keys * Update the configuration for Solr. See $solr_options for a discussion of the accepted array keys
* @param array $options - The options to update * @param array $options - The options to update
*/ */
static function configure_server($options = array()) { public static function configure_server($options = array())
{
self::$solr_options = array_merge(self::$solr_options, $options); self::$solr_options = array_merge(self::$solr_options, $options);
self::$merged_solr_options = null; self::$merged_solr_options = null;
@ -59,8 +60,11 @@ class Solr {
* Get the configured Solr options with the defaults all merged in * Get the configured Solr options with the defaults all merged in
* @return array - The merged options * @return array - The merged options
*/ */
static function solr_options() { public static function solr_options()
if (self::$merged_solr_options) return self::$merged_solr_options; {
if (self::$merged_solr_options) {
return self::$merged_solr_options;
}
$defaults = array( $defaults = array(
'host' => 'localhost', 'host' => 'localhost',
@ -72,14 +76,13 @@ class Solr {
// Build some by-version defaults // Build some by-version defaults
$version = isset(self::$solr_options['version']) ? self::$solr_options['version'] : $defaults['version']; $version = isset(self::$solr_options['version']) ? self::$solr_options['version'] : $defaults['version'];
if (version_compare($version, '4', '>=')){ if (version_compare($version, '4', '>=')) {
$versionDefaults = array( $versionDefaults = array(
'service' => 'Solr4Service', 'service' => 'Solr4Service',
'extraspath' => Director::baseFolder().'/fulltextsearch/conf/solr/4/extras/', 'extraspath' => Director::baseFolder().'/fulltextsearch/conf/solr/4/extras/',
'templatespath' => Director::baseFolder().'/fulltextsearch/conf/solr/4/templates/', 'templatespath' => Director::baseFolder().'/fulltextsearch/conf/solr/4/templates/',
); );
} } else {
else {
$versionDefaults = array( $versionDefaults = array(
'service' => 'Solr3Service', 'service' => 'Solr3Service',
'extraspath' => Director::baseFolder().'/fulltextsearch/conf/solr/3/extras/', 'extraspath' => Director::baseFolder().'/fulltextsearch/conf/solr/3/extras/',
@ -91,15 +94,16 @@ class Solr {
} }
static function set_service_class($class) { public static function set_service_class($class)
{
user_error('set_service_class is deprecated - pass as part of $options to configure_server', E_USER_WARNING); user_error('set_service_class is deprecated - pass as part of $options to configure_server', E_USER_WARNING);
self::configure_server(array('service' => $class)); self::configure_server(array('service' => $class));
} }
/** @var SolrService | null - The instance of SolrService for core management */ /** @var SolrService | null - The instance of SolrService for core management */
static protected $service_singleton = null; protected static $service_singleton = null;
/** @var [SolrService_Core] - The instances of SolrService_Core for each core */ /** @var [SolrService_Core] - The instances of SolrService_Core for each core */
static protected $service_core_singletons = array(); protected static $service_core_singletons = array();
/** /**
* Get a SolrService * Get a SolrService
@ -107,7 +111,8 @@ class Solr {
* @param string $core Optional core name * @param string $core Optional core name
* @return SolrService_Core * @return SolrService_Core
*/ */
static function service($core = null) { public static function service($core = null)
{
$options = self::solr_options(); $options = self::solr_options();
if (!self::$service_singleton) { if (!self::$service_singleton) {
@ -129,7 +134,8 @@ class Solr {
} }
} }
static function get_indexes() { public static function get_indexes()
{
return FullTextSearch::get_indexes('SolrIndex'); return FullTextSearch::get_indexes('SolrIndex');
} }
@ -137,7 +143,8 @@ class Solr {
* Include the thirdparty Solr client api library. Done this way to avoid issues where code is called in * Include the thirdparty Solr client api library. Done this way to avoid issues where code is called in
* mysite/_config before fulltextsearch/_config has a change to update the include path. * mysite/_config before fulltextsearch/_config has a change to update the include path.
*/ */
static function include_client_api() { public static function include_client_api()
{
static $included = false; static $included = false;
if (!$included) { if (!$included) {
@ -153,8 +160,8 @@ class Solr {
/** /**
* Abstract class for build tasks * Abstract class for build tasks
*/ */
class Solr_BuildTask extends BuildTask { class Solr_BuildTask extends BuildTask
{
protected $enabled = false; protected $enabled = false;
/** /**
@ -169,7 +176,8 @@ class Solr_BuildTask extends BuildTask {
* *
* @return LoggerInterface * @return LoggerInterface
*/ */
public function getLogger() { public function getLogger()
{
return $this->logger; return $this->logger;
} }
@ -178,14 +186,16 @@ class Solr_BuildTask extends BuildTask {
* *
* @param LoggerInterface $logger * @param LoggerInterface $logger
*/ */
public function setLogger(LoggerInterface $logger) { public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger; $this->logger = $logger;
} }
/** /**
* @return SearchLogFactory * @return SearchLogFactory
*/ */
protected function getLoggerFactory() { protected function getLoggerFactory()
{
return Injector::inst()->get('SearchLogFactory'); return Injector::inst()->get('SearchLogFactory');
} }
@ -194,7 +204,8 @@ class Solr_BuildTask extends BuildTask {
* *
* @param SS_HTTPReqest $request * @param SS_HTTPReqest $request
*/ */
public function run($request) { public function run($request)
{
$name = get_class($this); $name = get_class($this);
$verbose = $request->getVar('verbose'); $verbose = $request->getVar('verbose');
@ -207,21 +218,21 @@ class Solr_BuildTask extends BuildTask {
} }
class Solr_Configure extends Solr_BuildTask { class Solr_Configure extends Solr_BuildTask
{
protected $enabled = true; protected $enabled = true;
public function run($request) { public function run($request)
{
parent::run($request); parent::run($request);
// Find the IndexStore handler, which will handle uploading config files to Solr // Find the IndexStore handler, which will handle uploading config files to Solr
$store = $this->getSolrConfigStore(); $store = $this->getSolrConfigStore();
$indexes = Solr::get_indexes(); $indexes = Solr::get_indexes();
foreach ($indexes as $instance) { foreach ($indexes as $instance) {
try { try {
$this->updateIndex($instance, $store); $this->updateIndex($instance, $store);
} catch(Exception $e) { } catch (Exception $e) {
// We got an exception. Warn, but continue to next index. // We got an exception. Warn, but continue to next index.
$this $this
->getLogger() ->getLogger()
@ -236,7 +247,8 @@ class Solr_Configure extends Solr_BuildTask {
* @param SolrIndex $instance Instance * @param SolrIndex $instance Instance
* @param SolrConfigStore $store * @param SolrConfigStore $store
*/ */
protected function updateIndex($instance, $store) { protected function updateIndex($instance, $store)
{
$index = $instance->getIndexName(); $index = $instance->getIndexName();
$this->getLogger()->info("Configuring $index."); $this->getLogger()->info("Configuring $index.");
@ -262,7 +274,8 @@ class Solr_Configure extends Solr_BuildTask {
* *
* @return SolrConfigStore * @return SolrConfigStore
*/ */
protected function getSolrConfigStore() { protected function getSolrConfigStore()
{
$options = Solr::solr_options(); $options = Solr::solr_options();
if (!isset($options['indexstore']) || !($indexstore = $options['indexstore'])) { if (!isset($options['indexstore']) || !($indexstore = $options['indexstore'])) {
@ -281,7 +294,6 @@ class Solr_Configure extends Solr_BuildTask {
} else { } else {
user_error('Unknown Solr index mode '.$indexstore['mode'], E_USER_ERROR); user_error('Unknown Solr index mode '.$indexstore['mode'], E_USER_ERROR);
} }
} }
} }
@ -300,8 +312,8 @@ class Solr_Configure extends Solr_BuildTask {
* - variantstate * - variantstate
* - verbose (optional) * - verbose (optional)
*/ */
class Solr_Reindex extends Solr_BuildTask { class Solr_Reindex extends Solr_BuildTask
{
protected $enabled = true; protected $enabled = true;
/** /**
@ -317,14 +329,16 @@ class Solr_Reindex extends Solr_BuildTask {
* *
* @return SolrReindexHandler * @return SolrReindexHandler
*/ */
protected function getHandler() { protected function getHandler()
{
return Injector::inst()->get('SolrReindexHandler'); return Injector::inst()->get('SolrReindexHandler');
} }
/** /**
* @param SS_HTTPRequest $request * @param SS_HTTPRequest $request
*/ */
public function run($request) { public function run($request)
{
parent::run($request); parent::run($request);
// Reset state // Reset state
@ -336,7 +350,8 @@ class Solr_Reindex extends Solr_BuildTask {
/** /**
* @param SS_HTTPRequest $request * @param SS_HTTPRequest $request
*/ */
protected function doReindex($request) { protected function doReindex($request)
{
$class = $request->getVar('class'); $class = $request->getVar('class');
// Deprecated reindex mechanism // Deprecated reindex mechanism
@ -354,7 +369,7 @@ class Solr_Reindex extends Solr_BuildTask {
// Otherwise each group is processed via a SolrReindexGroupJob // Otherwise each group is processed via a SolrReindexGroupJob
$groups = $request->getVar('groups'); $groups = $request->getVar('groups');
$handler = $this->getHandler(); $handler = $this->getHandler();
if($groups) { if ($groups) {
// Run grouped batches (id % groups = group) // Run grouped batches (id % groups = group)
$group = $request->getVar('group'); $group = $request->getVar('group');
$indexInstance = singleton($request->getVar('index')); $indexInstance = singleton($request->getVar('index'));
@ -372,7 +387,8 @@ class Solr_Reindex extends Solr_BuildTask {
/** /**
* @deprecated since version 2.0.0 * @deprecated since version 2.0.0
*/ */
protected function runFrom($index, $class, $start, $variantstate) { protected function runFrom($index, $class, $start, $variantstate)
{
DeprecationTest_Deprecation::notice('2.0.0', 'Solr_Reindex now uses a new grouping mechanism'); DeprecationTest_Deprecation::notice('2.0.0', 'Solr_Reindex now uses a new grouping mechanism');
// Set time limit and state // Set time limit and state
@ -386,7 +402,7 @@ class Solr_Reindex extends Solr_BuildTask {
// Add child filter // Add child filter
$classes = $index->getClasses(); $classes = $index->getClasses();
$options = $classes[$class]; $options = $classes[$class];
if(!$options['include_children']) { if (!$options['include_children']) {
$items = $items->filter('ClassName', $class); $items = $items->filter('ClassName', $class);
} }

View File

@ -1,8 +1,10 @@
<?php <?php
class Solr3Service_Core extends SolrService_Core { class Solr3Service_Core extends SolrService_Core
{
} }
class Solr3Service extends SolrService { class Solr3Service extends SolrService
{
private static $core_class = 'Solr3Service_Core'; private static $core_class = 'Solr3Service_Core';
} }

View File

@ -1,12 +1,13 @@
<?php <?php
class Solr4Service_Core extends SolrService_Core { class Solr4Service_Core extends SolrService_Core
{
/** /**
* Replace underlying commit function to remove waitFlush in 4.0+, since it's been deprecated and 4.4 throws errors * Replace underlying commit function to remove waitFlush in 4.0+, since it's been deprecated and 4.4 throws errors
* if you pass it * if you pass it
*/ */
public function commit($expungeDeletes = false, $waitFlush = null, $waitSearcher = true, $timeout = 3600) { public function commit($expungeDeletes = false, $waitFlush = null, $waitSearcher = true, $timeout = 3600)
{
if ($waitFlush) { if ($waitFlush) {
user_error('waitFlush must be false when using Solr 4.0+' . E_USER_ERROR); user_error('waitFlush must be false when using Solr 4.0+' . E_USER_ERROR);
} }
@ -51,7 +52,7 @@ class Solr4Service_Core extends SolrService_Core {
} }
} }
class Solr4Service extends SolrService { class Solr4Service extends SolrService
{
private static $core_class = 'Solr4Service_Core'; private static $core_class = 'Solr4Service_Core';
} }

View File

@ -5,14 +5,15 @@
* *
* The interface Solr_Configure uses to upload configuration files to Solr * The interface Solr_Configure uses to upload configuration files to Solr
*/ */
interface SolrConfigStore { interface SolrConfigStore
{
/** /**
* Upload a file to Solr for index $index * Upload a file to Solr for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index) * @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
* @param $file string - A path to a file to upload. The base name of the file will be used on the remote side * @param $file string - A path to a file to upload. The base name of the file will be used on the remote side
* @return null * @return null
*/ */
function uploadFile($index, $file); public function uploadFile($index, $file);
/** /**
* Upload a file to Solr from a string for index $index * Upload a file to Solr from a string for index $index
@ -21,13 +22,13 @@ interface SolrConfigStore {
* @param $strong string - The contents of the file * @param $strong string - The contents of the file
* @return null * @return null
*/ */
function uploadString($index, $filename, $string); public function uploadString($index, $filename, $string);
/** /**
* Get the instanceDir to tell Solr to use for index $index * Get the instanceDir to tell Solr to use for index $index
* @param $index string - The name of an index (which is also used as the name of the Solr core for the index) * @param $index string - The name of an index (which is also used as the name of the Solr core for the index)
*/ */
function instanceDir($index); public function instanceDir($index);
} }
/** /**
@ -36,19 +37,22 @@ interface SolrConfigStore {
* A ConfigStore that uploads files to a Solr instance on a locally accessible filesystem * A ConfigStore that uploads files to a Solr instance on a locally accessible filesystem
* by just using file copies * by just using file copies
*/ */
class SolrConfigStore_File implements SolrConfigStore { class SolrConfigStore_File implements SolrConfigStore
function __construct($config) { {
public function __construct($config)
{
$this->local = $config['path']; $this->local = $config['path'];
$this->remote = isset($config['remotepath']) ? $config['remotepath'] : $config['path']; $this->remote = isset($config['remotepath']) ? $config['remotepath'] : $config['path'];
} }
function getTargetDir($index) { public function getTargetDir($index)
{
$targetDir = "{$this->local}/{$index}/conf"; $targetDir = "{$this->local}/{$index}/conf";
if (!is_dir($targetDir)) { if (!is_dir($targetDir)) {
$worked = @mkdir($targetDir, 0770, true); $worked = @mkdir($targetDir, 0770, true);
if(!$worked) { if (!$worked) {
throw new RuntimeException( throw new RuntimeException(
sprintf('Failed creating target directory %s, please check permissions', $targetDir) sprintf('Failed creating target directory %s, please check permissions', $targetDir)
); );
@ -58,17 +62,20 @@ class SolrConfigStore_File implements SolrConfigStore {
return $targetDir; return $targetDir;
} }
function uploadFile($index, $file) { public function uploadFile($index, $file)
{
$targetDir = $this->getTargetDir($index); $targetDir = $this->getTargetDir($index);
copy($file, $targetDir.'/'.basename($file)); copy($file, $targetDir.'/'.basename($file));
} }
function uploadString($index, $filename, $string) { public function uploadString($index, $filename, $string)
{
$targetDir = $this->getTargetDir($index); $targetDir = $this->getTargetDir($index);
file_put_contents("$targetDir/$filename", $string); file_put_contents("$targetDir/$filename", $string);
} }
function instanceDir($index) { public function instanceDir($index)
{
return $this->remote.'/'.$index; return $this->remote.'/'.$index;
} }
} }
@ -78,8 +85,10 @@ class SolrConfigStore_File implements SolrConfigStore {
* *
* A ConfigStore that uploads files to a Solr instance via a WebDAV server * A ConfigStore that uploads files to a Solr instance via a WebDAV server
*/ */
class SolrConfigStore_WebDAV implements SolrConfigStore { class SolrConfigStore_WebDAV implements SolrConfigStore
function __construct($config) { {
public function __construct($config)
{
$options = Solr::solr_options(); $options = Solr::solr_options();
$this->url = implode('', array( $this->url = implode('', array(
@ -91,27 +100,35 @@ class SolrConfigStore_WebDAV implements SolrConfigStore {
$this->remote = $config['remotepath']; $this->remote = $config['remotepath'];
} }
function getTargetDir($index) { public function getTargetDir($index)
{
$indexdir = "{$this->url}/$index"; $indexdir = "{$this->url}/$index";
if (!WebDAV::exists($indexdir)) WebDAV::mkdir($indexdir); if (!WebDAV::exists($indexdir)) {
WebDAV::mkdir($indexdir);
}
$targetDir = "{$this->url}/$index/conf"; $targetDir = "{$this->url}/$index/conf";
if (!WebDAV::exists($targetDir)) WebDAV::mkdir($targetDir); if (!WebDAV::exists($targetDir)) {
WebDAV::mkdir($targetDir);
}
return $targetDir; return $targetDir;
} }
function uploadFile($index, $file) { public function uploadFile($index, $file)
{
$targetDir = $this->getTargetDir($index); $targetDir = $this->getTargetDir($index);
WebDAV::upload_from_file($file, $targetDir.'/'.basename($file)); WebDAV::upload_from_file($file, $targetDir.'/'.basename($file));
} }
function uploadString($index, $filename, $string) { public function uploadString($index, $filename, $string)
{
$targetDir = $this->getTargetDir($index); $targetDir = $this->getTargetDir($index);
WebDAV::upload_from_string($string, "$targetDir/$filename"); WebDAV::upload_from_string($string, "$targetDir/$filename");
} }
function instanceDir($index) { public function instanceDir($index)
{
return $this->remote ? "{$this->remote}/$index" : $index; return $this->remote ? "{$this->remote}/$index" : $index;
} }
} }

View File

@ -2,15 +2,15 @@
Solr::include_client_api(); Solr::include_client_api();
abstract class SolrIndex extends SearchIndex { abstract class SolrIndex extends SearchIndex
{
static $fulltextTypeMap = array( public static $fulltextTypeMap = array(
'*' => 'text', '*' => 'text',
'HTMLVarchar' => 'htmltext', 'HTMLVarchar' => 'htmltext',
'HTMLText' => 'htmltext' 'HTMLText' => 'htmltext'
); );
static $filterTypeMap = array( public static $filterTypeMap = array(
'*' => 'string', '*' => 'string',
'Boolean' => 'boolean', 'Boolean' => 'boolean',
'Date' => 'tdate', 'Date' => 'tdate',
@ -22,7 +22,7 @@ abstract class SolrIndex extends SearchIndex {
'Double' => 'tdouble' 'Double' => 'tdouble'
); );
static $sortTypeMap = array(); public static $sortTypeMap = array();
protected $analyzerFields = array(); protected $analyzerFields = array();
@ -59,7 +59,8 @@ abstract class SolrIndex extends SearchIndex {
* @return String Absolute path to the folder containing * @return String Absolute path to the folder containing
* templates which are used for generating the schema and field definitions. * templates which are used for generating the schema and field definitions.
*/ */
function getTemplatesPath() { public function getTemplatesPath()
{
$globalOptions = Solr::solr_options(); $globalOptions = Solr::solr_options();
return $this->templatesPath ? $this->templatesPath : $globalOptions['templatespath']; return $this->templatesPath ? $this->templatesPath : $globalOptions['templatespath'];
} }
@ -68,20 +69,24 @@ abstract class SolrIndex extends SearchIndex {
* @return String Absolute path to the configuration default files, * @return String Absolute path to the configuration default files,
* e.g. solrconfig.xml. * e.g. solrconfig.xml.
*/ */
function getExtrasPath() { public function getExtrasPath()
{
$globalOptions = Solr::solr_options(); $globalOptions = Solr::solr_options();
return $this->extrasPath ? $this->extrasPath : $globalOptions['extraspath']; return $this->extrasPath ? $this->extrasPath : $globalOptions['extraspath'];
} }
function generateSchema() { public function generateSchema()
{
return $this->renderWith($this->getTemplatesPath() . '/schema.ss'); return $this->renderWith($this->getTemplatesPath() . '/schema.ss');
} }
function getIndexName() { public function getIndexName()
{
return get_class($this); return get_class($this);
} }
function getTypes() { public function getTypes()
{
return $this->renderWith($this->getTemplatesPath() . '/types.ss'); return $this->renderWith($this->getTemplatesPath() . '/types.ss');
} }
@ -95,20 +100,26 @@ abstract class SolrIndex extends SearchIndex {
* @param String $type * @param String $type
* @param Array $params Parameters for the analyzer, usually at least a "class" * @param Array $params Parameters for the analyzer, usually at least a "class"
*/ */
function addAnalyzer($field, $type, $params) { public function addAnalyzer($field, $type, $params)
{
$fullFields = $this->fieldData($field); $fullFields = $this->fieldData($field);
if($fullFields) foreach($fullFields as $fullField => $spec) { if ($fullFields) {
if(!isset($this->analyzerFields[$fullField])) $this->analyzerFields[$fullField] = array(); foreach ($fullFields as $fullField => $spec) {
if (!isset($this->analyzerFields[$fullField])) {
$this->analyzerFields[$fullField] = array();
}
$this->analyzerFields[$fullField][$type] = $params; $this->analyzerFields[$fullField][$type] = $params;
} }
} }
}
/** /**
* Get the default text field, normally '_text' * Get the default text field, normally '_text'
* *
* @return string * @return string
*/ */
public function getDefaultField() { public function getDefaultField()
{
return $this->config()->default_field; return $this->config()->default_field;
} }
@ -118,9 +129,10 @@ abstract class SolrIndex extends SearchIndex {
* *
* @return array * @return array
*/ */
protected function getCopyDestinations() { protected function getCopyDestinations()
{
$copyFields = $this->config()->copy_fields; $copyFields = $this->config()->copy_fields;
if($copyFields) { if ($copyFields) {
return $copyFields; return $copyFields;
} }
// Fallback to default field // Fallback to default field
@ -128,7 +140,8 @@ abstract class SolrIndex extends SearchIndex {
return array($df); return array($df);
} }
public function getFieldDefinitions() { public function getFieldDefinitions()
{
$xml = array(); $xml = array();
$stored = $this->getStoredDefault(); $stored = $this->getStoredDefault();
@ -154,12 +167,16 @@ abstract class SolrIndex extends SearchIndex {
} }
foreach ($this->filterFields as $name => $field) { foreach ($this->filterFields as $name => $field) {
if ($field['fullfield'] == 'ID' || $field['fullfield'] == 'ClassName') continue; if ($field['fullfield'] == 'ID' || $field['fullfield'] == 'ClassName') {
continue;
}
$xml[] = $this->getFieldDefinition($name, $field); $xml[] = $this->getFieldDefinition($name, $field);
} }
foreach ($this->sortFields as $name => $field) { foreach ($this->sortFields as $name => $field) {
if ($field['fullfield'] == 'ID' || $field['fullfield'] == 'ClassName') continue; if ($field['fullfield'] == 'ID' || $field['fullfield'] == 'ClassName') {
continue;
}
$xml[] = $this->getFieldDefinition($name, $field); $xml[] = $this->getFieldDefinition($name, $field);
} }
@ -172,13 +189,14 @@ abstract class SolrIndex extends SearchIndex {
* @param mixed $collation * @param mixed $collation
* @return string * @return string
*/ */
protected function getCollatedSuggestion($collation = '') { protected function getCollatedSuggestion($collation = '')
if(is_string($collation)) { {
if (is_string($collation)) {
return $collation; return $collation;
} }
if(is_object($collation)) { if (is_object($collation)) {
if(isset($collation->misspellingsAndCorrections)) { if (isset($collation->misspellingsAndCorrections)) {
foreach($collation->misspellingsAndCorrections as $key => $value) { foreach ($collation->misspellingsAndCorrections as $key => $value) {
return $value; return $value;
} }
} }
@ -191,7 +209,8 @@ abstract class SolrIndex extends SearchIndex {
* @param String $collation * @param String $collation
* @return String * @return String
*/ */
protected function getNiceSuggestion($collation = '') { protected function getNiceSuggestion($collation = '')
{
$collationParts = explode(' ', $collation); $collationParts = explode(' ', $collation);
// Remove advanced query params from the beginning of each collation part. // Remove advanced query params from the beginning of each collation part.
@ -209,7 +228,8 @@ abstract class SolrIndex extends SearchIndex {
* @param String $collation * @param String $collation
* @return String * @return String
*/ */
protected function getSuggestionQueryString($collation = '') { protected function getSuggestionQueryString($collation = '')
{
return str_replace(' ', '+', $this->getNiceSuggestion($collation)); return str_replace(' ', '+', $this->getNiceSuggestion($collation));
} }
@ -221,7 +241,8 @@ abstract class SolrIndex extends SearchIndex {
* detectable from metadata) * detectable from metadata)
* @param array $extraOptions Dependent on search implementation * @param array $extraOptions Dependent on search implementation
*/ */
public function addStoredField($field, $forceType = null, $extraOptions = array()) { public function addStoredField($field, $forceType = null, $extraOptions = array())
{
$options = array_merge($extraOptions, array('stored' => 'true')); $options = array_merge($extraOptions, array('stored' => 'true'));
$this->addFulltextField($field, $forceType, $options); $this->addFulltextField($field, $forceType, $options);
} }
@ -235,24 +256,26 @@ abstract class SolrIndex extends SearchIndex {
* @param array $extraOptions Dependent on search implementation * @param array $extraOptions Dependent on search implementation
* @param float $boost Numeric boosting value (defaults to 2) * @param float $boost Numeric boosting value (defaults to 2)
*/ */
public function addBoostedField($field, $forceType = null, $extraOptions = array(), $boost = 2) { public function addBoostedField($field, $forceType = null, $extraOptions = array(), $boost = 2)
{
$options = array_merge($extraOptions, array('boost' => $boost)); $options = array_merge($extraOptions, array('boost' => $boost));
$this->addFulltextField($field, $forceType, $options); $this->addFulltextField($field, $forceType, $options);
} }
public function fieldData($field, $forceType = null, $extraOptions = array()) { public function fieldData($field, $forceType = null, $extraOptions = array())
{
// Ensure that 'boost' is recorded here without being captured by solr // Ensure that 'boost' is recorded here without being captured by solr
$boost = null; $boost = null;
if(array_key_exists('boost', $extraOptions)) { if (array_key_exists('boost', $extraOptions)) {
$boost = $extraOptions['boost']; $boost = $extraOptions['boost'];
unset($extraOptions['boost']); unset($extraOptions['boost']);
} }
$data = parent::fieldData($field, $forceType, $extraOptions); $data = parent::fieldData($field, $forceType, $extraOptions);
// Boost all fields with this name // Boost all fields with this name
if(isset($boost)) { if (isset($boost)) {
foreach($data as $fieldName => $fieldInfo) { foreach ($data as $fieldName => $fieldInfo) {
$this->boostedFields[$fieldName] = $boost; $this->boostedFields[$fieldName] = $boost;
} }
} }
@ -269,11 +292,12 @@ abstract class SolrIndex extends SearchIndex {
* @param string $field Full field key (Model_Field) * @param string $field Full field key (Model_Field)
* @param float|null $level Numeric boosting value. Set to null to clear boost * @param float|null $level Numeric boosting value. Set to null to clear boost
*/ */
public function setFieldBoosting($field, $level) { public function setFieldBoosting($field, $level)
if(!isset($this->fulltextFields[$field])) { {
if (!isset($this->fulltextFields[$field])) {
throw new InvalidArgumentException("No fulltext field $field exists on ".$this->getIndexName()); throw new InvalidArgumentException("No fulltext field $field exists on ".$this->getIndexName());
} }
if($level === null) { if ($level === null) {
unset($this->boostedFields[$field]); unset($this->boostedFields[$field]);
} else { } else {
$this->boostedFields[$field] = $level; $this->boostedFields[$field] = $level;
@ -285,7 +309,8 @@ abstract class SolrIndex extends SearchIndex {
* *
* @return array * @return array
*/ */
public function getBoostedFields() { public function getBoostedFields()
{
return $this->boostedFields; return $this->boostedFields;
} }
@ -294,9 +319,10 @@ abstract class SolrIndex extends SearchIndex {
* *
* @return array|null List of query fields, or null if not specified * @return array|null List of query fields, or null if not specified
*/ */
public function getQueryFields() { public function getQueryFields()
{
// Not necessary to specify this unless boosting // Not necessary to specify this unless boosting
if(empty($this->boostedFields)) { if (empty($this->boostedFields)) {
return null; return null;
} }
$queryFields = array(); $queryFields = array();
@ -306,7 +332,7 @@ abstract class SolrIndex extends SearchIndex {
// If any fields are queried, we must always include the default field, otherwise it will be excluded // If any fields are queried, we must always include the default field, otherwise it will be excluded
$df = $this->getDefaultField(); $df = $this->getDefaultField();
if($queryFields && !isset($this->boostedFields[$df])) { if ($queryFields && !isset($this->boostedFields[$df])) {
$queryFields[] = $df; $queryFields[] = $df;
} }
@ -318,7 +344,8 @@ abstract class SolrIndex extends SearchIndex {
* *
* @return string A default value for the 'stored' field option, either 'true' or 'false' * @return string A default value for the 'stored' field option, either 'true' or 'false'
*/ */
protected function getStoredDefault() { protected function getStoredDefault()
{
return Director::isDev() ? 'true' : 'false'; return Director::isDev() ? 'true' : 'false';
} }
@ -328,14 +355,17 @@ abstract class SolrIndex extends SearchIndex {
* @param Array $typeMap * @param Array $typeMap
* @return String XML * @return String XML
*/ */
protected function getFieldDefinition($name, $spec, $typeMap = null) { protected function getFieldDefinition($name, $spec, $typeMap = null)
if(!$typeMap) $typeMap = self::$filterTypeMap; {
if (!$typeMap) {
$typeMap = self::$filterTypeMap;
}
$multiValued = (isset($spec['multi_valued']) && $spec['multi_valued']) ? "true" : ''; $multiValued = (isset($spec['multi_valued']) && $spec['multi_valued']) ? "true" : '';
$type = isset($typeMap[$spec['type']]) ? $typeMap[$spec['type']] : $typeMap['*']; $type = isset($typeMap[$spec['type']]) ? $typeMap[$spec['type']] : $typeMap['*'];
$analyzerXml = ''; $analyzerXml = '';
if(isset($this->analyzerFields[$name])) { if (isset($this->analyzerFields[$name])) {
foreach($this->analyzerFields[$name] as $analyzerType => $analyzerParams) { foreach ($this->analyzerFields[$name] as $analyzerType => $analyzerParams) {
$analyzerXml .= $this->toXmlTag($analyzerType, $analyzerParams); $analyzerXml .= $this->toXmlTag($analyzerType, $analyzerParams);
} }
} }
@ -366,11 +396,14 @@ abstract class SolrIndex extends SearchIndex {
* @param String $content Inner content * @param String $content Inner content
* @return String XML tag * @return String XML tag
*/ */
protected function toXmlTag($tag, $attrs, $content = null) { protected function toXmlTag($tag, $attrs, $content = null)
{
$xml = "<$tag "; $xml = "<$tag ";
if($attrs) { if ($attrs) {
$attrStrs = array(); $attrStrs = array();
foreach($attrs as $attrName => $attrVal) $attrStrs[] = "$attrName='$attrVal'"; foreach ($attrs as $attrName => $attrVal) {
$attrStrs[] = "$attrName='$attrVal'";
}
$xml .= $attrStrs ? implode(' ', $attrStrs) : ''; $xml .= $attrStrs ? implode(' ', $attrStrs) : '';
} }
$xml .= $content ? ">$content</$tag>" : '/>'; $xml .= $content ? ">$content</$tag>" : '/>';
@ -381,8 +414,11 @@ abstract class SolrIndex extends SearchIndex {
* @param String $source Composite field name (<class>_<fieldname>) * @param String $source Composite field name (<class>_<fieldname>)
* @param String $dest * @param String $dest
*/ */
function addCopyField($source, $dest, $extraOptions = array()) { public function addCopyField($source, $dest, $extraOptions = array())
if(!isset($this->copyFields[$source])) $this->copyFields[$source] = array(); {
if (!isset($this->copyFields[$source])) {
$this->copyFields[$source] = array();
}
$this->copyFields[$source][] = array_merge( $this->copyFields[$source][] = array_merge(
array('source' => $source, 'dest' => $dest), array('source' => $source, 'dest' => $dest),
$extraOptions $extraOptions
@ -394,11 +430,12 @@ abstract class SolrIndex extends SearchIndex {
* *
* @return string * @return string
*/ */
public function getCopyFieldDefinitions() { public function getCopyFieldDefinitions()
{
$xml = array(); $xml = array();
// Default copy fields // Default copy fields
foreach($this->getCopyDestinations() as $copyTo) { foreach ($this->getCopyDestinations() as $copyTo) {
foreach ($this->fulltextFields as $name => $field) { foreach ($this->fulltextFields as $name => $field) {
$xml[] = "<copyField source='{$name}' dest='{$copyTo}' />"; $xml[] = "<copyField source='{$name}' dest='{$copyTo}' />";
} }
@ -406,7 +443,7 @@ abstract class SolrIndex extends SearchIndex {
// Explicit copy fields // Explicit copy fields
foreach ($this->copyFields as $source => $fields) { foreach ($this->copyFields as $source => $fields) {
foreach($fields as $fieldAttrs) { foreach ($fields as $fieldAttrs) {
$xml[] = $this->toXmlTag('copyField', $fieldAttrs); $xml[] = $this->toXmlTag('copyField', $fieldAttrs);
} }
} }
@ -414,42 +451,54 @@ abstract class SolrIndex extends SearchIndex {
return implode("\n\t", $xml); return implode("\n\t", $xml);
} }
protected function _addField($doc, $object, $field) { protected function _addField($doc, $object, $field)
{
$class = get_class($object); $class = get_class($object);
if ($class != $field['origin'] && !is_subclass_of($class, $field['origin'])) return; if ($class != $field['origin'] && !is_subclass_of($class, $field['origin'])) {
return;
}
$value = $this->_getFieldValue($object, $field); $value = $this->_getFieldValue($object, $field);
$type = isset(self::$filterTypeMap[$field['type']]) ? self::$filterTypeMap[$field['type']] : self::$filterTypeMap['*']; $type = isset(self::$filterTypeMap[$field['type']]) ? self::$filterTypeMap[$field['type']] : self::$filterTypeMap['*'];
if (is_array($value)) foreach($value as $sub) { if (is_array($value)) {
foreach ($value as $sub) {
/* Solr requires dates in the form 1995-12-31T23:59:59Z */ /* Solr requires dates in the form 1995-12-31T23:59:59Z */
if ($type == 'tdate') { if ($type == 'tdate') {
if(!$sub) continue; if (!$sub) {
continue;
}
$sub = gmdate('Y-m-d\TH:i:s\Z', strtotime($sub)); $sub = gmdate('Y-m-d\TH:i:s\Z', strtotime($sub));
} }
/* Solr requires numbers to be valid if presented, not just empty */ /* Solr requires numbers to be valid if presented, not just empty */
if (($type == 'tint' || $type == 'tfloat' || $type == 'tdouble') && !is_numeric($sub)) continue; if (($type == 'tint' || $type == 'tfloat' || $type == 'tdouble') && !is_numeric($sub)) {
continue;
}
$doc->addField($field['name'], $sub); $doc->addField($field['name'], $sub);
} }
} else {
else {
/* Solr requires dates in the form 1995-12-31T23:59:59Z */ /* Solr requires dates in the form 1995-12-31T23:59:59Z */
if ($type == 'tdate') { if ($type == 'tdate') {
if(!$value) return; if (!$value) {
return;
}
$value = gmdate('Y-m-d\TH:i:s\Z', strtotime($value)); $value = gmdate('Y-m-d\TH:i:s\Z', strtotime($value));
} }
/* Solr requires numbers to be valid if presented, not just empty */ /* Solr requires numbers to be valid if presented, not just empty */
if (($type == 'tint' || $type == 'tfloat' || $type == 'tdouble') && !is_numeric($value)) return; if (($type == 'tint' || $type == 'tfloat' || $type == 'tdouble') && !is_numeric($value)) {
return;
}
$doc->setField($field['name'], $value); $doc->setField($field['name'], $value);
} }
} }
protected function _addAs($object, $base, $options) { protected function _addAs($object, $base, $options)
{
$includeSubs = $options['include_children']; $includeSubs = $options['include_children'];
$doc = new Apache_Solr_Document(); $doc = new Apache_Solr_Document();
@ -460,12 +509,16 @@ abstract class SolrIndex extends SearchIndex {
$doc->setField('ID', $object->ID); $doc->setField('ID', $object->ID);
$doc->setField('ClassName', $object->ClassName); $doc->setField('ClassName', $object->ClassName);
foreach (SearchIntrospection::hierarchy(get_class($object), false) as $class) $doc->addField('ClassHierarchy', $class); foreach (SearchIntrospection::hierarchy(get_class($object), false) as $class) {
$doc->addField('ClassHierarchy', $class);
}
// Add the user-specified fields // Add the user-specified fields
foreach ($this->getFieldsIterator() as $name => $field) { foreach ($this->getFieldsIterator() as $name => $field) {
if ($field['base'] == $base) $this->_addField($doc, $object, $field); if ($field['base'] == $base) {
$this->_addField($doc, $object, $field);
}
} }
try { try {
@ -478,7 +531,8 @@ abstract class SolrIndex extends SearchIndex {
return $doc; return $doc;
} }
function add($object) { public function add($object)
{
$class = get_class($object); $class = get_class($object);
$docs = array(); $docs = array();
@ -492,15 +546,19 @@ abstract class SolrIndex extends SearchIndex {
return $docs; return $docs;
} }
function canAdd($class) { public function canAdd($class)
{
foreach ($this->classes as $searchclass => $options) { foreach ($this->classes as $searchclass => $options) {
if ($searchclass == $class || ($options['include_children'] && is_subclass_of($class, $searchclass))) return true; if ($searchclass == $class || ($options['include_children'] && is_subclass_of($class, $searchclass))) {
return true;
}
} }
return false; return false;
} }
function delete($base, $id, $state) { public function delete($base, $id, $state)
{
$documentID = $this->getDocumentIDForState($base, $id, $state); $documentID = $this->getDocumentIDForState($base, $id, $state);
try { try {
@ -521,8 +579,9 @@ abstract class SolrIndex extends SearchIndex {
* @param array $classes List of non-obsolete classes in the same format as SolrIndex::getClasses() * @param array $classes List of non-obsolete classes in the same format as SolrIndex::getClasses()
* @return bool Flag if successful * @return bool Flag if successful
*/ */
public function clearObsoleteClasses($classes) { public function clearObsoleteClasses($classes)
if(empty($classes)) { {
if (empty($classes)) {
return false; return false;
} }
@ -544,7 +603,8 @@ abstract class SolrIndex extends SearchIndex {
return true; return true;
} }
function commit() { public function commit()
{
try { try {
$this->getService()->commit(false, false, false); $this->getService()->commit(false, false, false);
} catch (Exception $e) { } catch (Exception $e) {
@ -561,7 +621,8 @@ abstract class SolrIndex extends SearchIndex {
* @return ArrayData Map with the following keys: * @return ArrayData Map with the following keys:
* - 'Matches': ArrayList of the matched object instances * - 'Matches': ArrayList of the matched object instances
*/ */
public function search(SearchQuery $query, $offset = -1, $limit = -1, $params = array()) { public function search(SearchQuery $query, $offset = -1, $limit = -1, $params = array())
{
$service = $this->getService(); $service = $this->getService();
$searchClass = count($query->classes) == 1 $searchClass = count($query->classes) == 1
@ -580,7 +641,7 @@ abstract class SolrIndex extends SearchIndex {
// If using boosting, set the clean term separately for highlighting. // If using boosting, set the clean term separately for highlighting.
// See https://issues.apache.org/jira/browse/SOLR-2632 // See https://issues.apache.org/jira/browse/SOLR-2632
if(array_key_exists('hl', $params) && !array_key_exists('hl.q', $params)) { if (array_key_exists('hl', $params) && !array_key_exists('hl.q', $params)) {
$params['hl.q'] = implode(' ', $hlq); $params['hl.q'] = implode(' ', $hlq);
} }
@ -589,36 +650,51 @@ abstract class SolrIndex extends SearchIndex {
foreach ($query->classes as $class) { foreach ($query->classes as $class) {
if (!empty($class['includeSubclasses'])) { if (!empty($class['includeSubclasses'])) {
$classq[] = 'ClassHierarchy:'.$class['class']; $classq[] = 'ClassHierarchy:'.$class['class'];
} else {
$classq[] = 'ClassName:'.$class['class'];
} }
else $classq[] = 'ClassName:'.$class['class'];
} }
if ($classq) $fq[] = '+('.implode(' ', $classq).')'; if ($classq) {
$fq[] = '+('.implode(' ', $classq).')';
}
// Filter by filters // Filter by filters
$fq = array_merge($fq, $this->getFiltersComponent($query)); $fq = array_merge($fq, $this->getFiltersComponent($query));
// Prepare query fields unless specified explicitly // Prepare query fields unless specified explicitly
if(isset($params['qf'])) { if (isset($params['qf'])) {
$qf = $params['qf']; $qf = $params['qf'];
} else { } else {
$qf = $this->getQueryFields(); $qf = $this->getQueryFields();
} }
if(is_array($qf)) { if (is_array($qf)) {
$qf = implode(' ', $qf); $qf = implode(' ', $qf);
} }
if($qf) { if ($qf) {
$params['qf'] = $qf; $params['qf'] = $qf;
} }
if(!headers_sent() && !Director::isLive()) { if (!headers_sent() && !Director::isLive()) {
if ($q) header('X-Query: '.implode(' ', $q)); if ($q) {
if ($fq) header('X-Filters: "'.implode('", "', $fq).'"'); header('X-Query: '.implode(' ', $q));
if ($qf) header('X-QueryFields: '.$qf); }
if ($fq) {
header('X-Filters: "'.implode('", "', $fq).'"');
}
if ($qf) {
header('X-QueryFields: '.$qf);
}
} }
if ($offset == -1) $offset = $query->start; if ($offset == -1) {
if ($limit == -1) $limit = $query->limit; $offset = $query->start;
if ($limit == -1) $limit = SearchQuery::$default_page_size; }
if ($limit == -1) {
$limit = $query->limit;
}
if ($limit == -1) {
$limit = SearchQuery::$default_page_size;
}
$params = array_merge($params, array('fq' => implode(' ', $fq))); $params = array_merge($params, array('fq' => implode(' ', $fq)));
@ -631,20 +707,20 @@ abstract class SolrIndex extends SearchIndex {
); );
$results = new ArrayList(); $results = new ArrayList();
if($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) { if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) {
foreach ($res->response->docs as $doc) { foreach ($res->response->docs as $doc) {
$result = DataObject::get_by_id($doc->ClassName, $doc->ID); $result = DataObject::get_by_id($doc->ClassName, $doc->ID);
if($result) { if ($result) {
$results->push($result); $results->push($result);
// Add highlighting (optional) // Add highlighting (optional)
$docId = $doc->_documentid; $docId = $doc->_documentid;
if($res->highlighting && $res->highlighting->$docId) { if ($res->highlighting && $res->highlighting->$docId) {
// TODO Create decorator class for search results rather than adding arbitrary object properties // TODO Create decorator class for search results rather than adding arbitrary object properties
// TODO Allow specifying highlighted field, and lazy loading // TODO Allow specifying highlighted field, and lazy loading
// in case the search API needs another query (similar to SphinxSearchable->buildExcerpt()). // in case the search API needs another query (similar to SphinxSearchable->buildExcerpt()).
$combinedHighlights = array(); $combinedHighlights = array();
foreach($res->highlighting->$docId as $field => $highlights) { foreach ($res->highlighting->$docId as $field => $highlights) {
$combinedHighlights = array_merge($combinedHighlights, $highlights); $combinedHighlights = array_merge($combinedHighlights, $highlights);
} }
@ -677,12 +753,12 @@ abstract class SolrIndex extends SearchIndex {
$ret['Matches']->setPageLength($limit); $ret['Matches']->setPageLength($limit);
// Include spellcheck and suggestion data. Requires spellcheck=true in $params // Include spellcheck and suggestion data. Requires spellcheck=true in $params
if(isset($res->spellcheck)) { if (isset($res->spellcheck)) {
// Expose all spellcheck data, for custom handling. // Expose all spellcheck data, for custom handling.
$ret['Spellcheck'] = $res->spellcheck; $ret['Spellcheck'] = $res->spellcheck;
// Suggestions. Requires spellcheck.collate=true in $params // Suggestions. Requires spellcheck.collate=true in $params
if(isset($res->spellcheck->suggestions->collation)) { if (isset($res->spellcheck->suggestions->collation)) {
// Extract string suggestion // Extract string suggestion
$suggestion = $this->getCollatedSuggestion($res->spellcheck->suggestions->collation); $suggestion = $this->getCollatedSuggestion($res->spellcheck->suggestions->collation);
@ -709,7 +785,8 @@ abstract class SolrIndex extends SearchIndex {
* @param array &$hlq Highlight query returned by reference * @param array &$hlq Highlight query returned by reference
* @return array * @return array
*/ */
protected function getQueryComponent(SearchQuery $searchQuery, &$hlq = array()) { protected function getQueryComponent(SearchQuery $searchQuery, &$hlq = array())
{
$q = array(); $q = array();
foreach ($searchQuery->search as $search) { foreach ($searchQuery->search as $search) {
$text = $search['text']; $text = $search['text'];
@ -719,7 +796,7 @@ abstract class SolrIndex extends SearchIndex {
foreach ($parts[0] as $part) { foreach ($parts[0] as $part) {
$fields = (isset($search['fields'])) ? $search['fields'] : array(); $fields = (isset($search['fields'])) ? $search['fields'] : array();
if(isset($search['boost'])) { if (isset($search['boost'])) {
$fields = array_merge($fields, array_keys($search['boost'])); $fields = array_merge($fields, array_keys($search['boost']));
} }
if ($fields) { if ($fields) {
@ -729,8 +806,7 @@ abstract class SolrIndex extends SearchIndex {
$searchq[] = "{$field}:".$part.$fuzzy.$boost; $searchq[] = "{$field}:".$part.$fuzzy.$boost;
} }
$q[] = '+('.implode(' OR ', $searchq).')'; $q[] = '+('.implode(' OR ', $searchq).')';
} } else {
else {
$q[] = '+'.$part.$fuzzy; $q[] = '+'.$part.$fuzzy;
} }
$hlq[] = $part; $hlq[] = $part;
@ -745,7 +821,8 @@ abstract class SolrIndex extends SearchIndex {
* @param SearchQuery $searchQuery * @param SearchQuery $searchQuery
* @return array List of parsed string values for each require * @return array List of parsed string values for each require
*/ */
protected function getRequireFiltersComponent(SearchQuery $searchQuery) { protected function getRequireFiltersComponent(SearchQuery $searchQuery)
{
$fq = array(); $fq = array();
foreach ($searchQuery->require as $field => $values) { foreach ($searchQuery->require as $field => $values) {
$requireq = array(); $requireq = array();
@ -753,11 +830,9 @@ abstract class SolrIndex extends SearchIndex {
foreach ($values as $value) { foreach ($values as $value) {
if ($value === SearchQuery::$missing) { if ($value === SearchQuery::$missing) {
$requireq[] = "(*:* -{$field}:[* TO *])"; $requireq[] = "(*:* -{$field}:[* TO *])";
} } elseif ($value === SearchQuery::$present) {
else if ($value === SearchQuery::$present) {
$requireq[] = "{$field}:[* TO *]"; $requireq[] = "{$field}:[* TO *]";
} } elseif ($value instanceof SearchQuery_Range) {
else if ($value instanceof SearchQuery_Range) {
$start = $value->start; $start = $value->start;
if ($start === null) { if ($start === null) {
$start = '*'; $start = '*';
@ -767,8 +842,7 @@ abstract class SolrIndex extends SearchIndex {
$end = '*'; $end = '*';
} }
$requireq[] = "$field:[$start TO $end]"; $requireq[] = "$field:[$start TO $end]";
} } else {
else {
$requireq[] = $field.':"'.$value.'"'; $requireq[] = $field.':"'.$value.'"';
} }
} }
@ -784,7 +858,8 @@ abstract class SolrIndex extends SearchIndex {
* @param SearchQuery $searchQuery * @param SearchQuery $searchQuery
* @return array List of parsed string values for each exclusion * @return array List of parsed string values for each exclusion
*/ */
protected function getExcludeFiltersComponent(SearchQuery $searchQuery) { protected function getExcludeFiltersComponent(SearchQuery $searchQuery)
{
$fq = array(); $fq = array();
foreach ($searchQuery->exclude as $field => $values) { foreach ($searchQuery->exclude as $field => $values) {
$excludeq = array(); $excludeq = array();
@ -793,11 +868,9 @@ abstract class SolrIndex extends SearchIndex {
foreach ($values as $value) { foreach ($values as $value) {
if ($value === SearchQuery::$missing) { if ($value === SearchQuery::$missing) {
$missing = true; $missing = true;
} } elseif ($value === SearchQuery::$present) {
else if ($value === SearchQuery::$present) {
$excludeq[] = "{$field}:[* TO *]"; $excludeq[] = "{$field}:[* TO *]";
} } elseif ($value instanceof SearchQuery_Range) {
else if ($value instanceof SearchQuery_Range) {
$start = $value->start; $start = $value->start;
if ($start === null) { if ($start === null) {
$start = '*'; $start = '*';
@ -807,8 +880,7 @@ abstract class SolrIndex extends SearchIndex {
$end = '*'; $end = '*';
} }
$excludeq[] = "$field:[$start TO $end]"; $excludeq[] = "$field:[$start TO $end]";
} } else {
else {
$excludeq[] = $field.':"'.$value.'"'; $excludeq[] = $field.':"'.$value.'"';
} }
} }
@ -824,7 +896,8 @@ abstract class SolrIndex extends SearchIndex {
* @param SearchQuery $searchQuery * @param SearchQuery $searchQuery
* @return array * @return array
*/ */
public function getFiltersComponent(SearchQuery $searchQuery) { public function getFiltersComponent(SearchQuery $searchQuery)
{
return array_merge( return array_merge(
$this->getRequireFiltersComponent($searchQuery), $this->getRequireFiltersComponent($searchQuery),
$this->getExcludeFiltersComponent($searchQuery) $this->getExcludeFiltersComponent($searchQuery)
@ -836,12 +909,16 @@ abstract class SolrIndex extends SearchIndex {
/** /**
* @return SolrService * @return SolrService
*/ */
public function getService() { public function getService()
if(!$this->service) $this->service = Solr::service(get_class($this)); {
if (!$this->service) {
$this->service = Solr::service(get_class($this));
}
return $this->service; return $this->service;
} }
public function setService(SolrService $service) { public function setService(SolrService $service)
{
$this->service = $service; $this->service = $service;
return $this; return $this;
} }
@ -851,7 +928,8 @@ abstract class SolrIndex extends SearchIndex {
* *
* @param SolrConfigStore $store * @param SolrConfigStore $store
*/ */
public function uploadConfig($store) { public function uploadConfig($store)
{
// Upload the config files for this index // Upload the config files for this index
$store->uploadString( $store->uploadString(
$this->getIndexName(), $this->getIndexName(),

View File

@ -5,7 +5,8 @@ Solr::include_client_api();
/** /**
* The API for accessing a specific core of a Solr server. Exactly the same as Apache_Solr_Service for now. * The API for accessing a specific core of a Solr server. Exactly the same as Apache_Solr_Service for now.
*/ */
class SolrService_Core extends Apache_Solr_Service { class SolrService_Core extends Apache_Solr_Service
{
} }
/** /**
@ -13,13 +14,15 @@ class SolrService_Core extends Apache_Solr_Service {
* plus extra methods for interrogating, creating, reloading and getting SolrService_Core instances * plus extra methods for interrogating, creating, reloading and getting SolrService_Core instances
* for Solr cores. * for Solr cores.
*/ */
class SolrService extends SolrService_Core { class SolrService extends SolrService_Core
{
private static $core_class = 'SolrService_Core'; private static $core_class = 'SolrService_Core';
/** /**
* Handle encoding the GET parameters and making the HTTP call to execute a core command * Handle encoding the GET parameters and making the HTTP call to execute a core command
*/ */
protected function coreCommand($command, $core, $params=array()) { protected function coreCommand($command, $core, $params=array())
{
$command = strtoupper($command); $command = strtoupper($command);
$params = array_merge($params, array('action' => $command, 'wt' => 'json')); $params = array_merge($params, array('action' => $command, 'wt' => 'json'));
@ -33,7 +36,8 @@ class SolrService extends SolrService_Core {
* @param $core string - The name of the core * @param $core string - The name of the core
* @return boolean - True if that core exists & is active * @return boolean - True if that core exists & is active
*/ */
public function coreIsActive($core) { public function coreIsActive($core)
{
$result = $this->coreCommand('STATUS', $core); $result = $this->coreCommand('STATUS', $core);
return isset($result->status->$core->uptime); return isset($result->status->$core->uptime);
} }
@ -47,11 +51,18 @@ class SolrService extends SolrService_Core {
* @param $datadir string - The path to store data for this core on the server. Default depends on solrconfig.xml * @param $datadir string - The path to store data for this core on the server. Default depends on solrconfig.xml
* @return Apache_Solr_Response * @return Apache_Solr_Response
*/ */
public function coreCreate($core, $instancedir, $config=null, $schema=null, $datadir=null) { public function coreCreate($core, $instancedir, $config=null, $schema=null, $datadir=null)
{
$args = array('instanceDir' => $instancedir); $args = array('instanceDir' => $instancedir);
if ($config) $args['config'] = $config; if ($config) {
if ($schema) $args['schema'] = $schema; $args['config'] = $config;
if ($datadir) $args['dataDir'] = $datadir; }
if ($schema) {
$args['schema'] = $schema;
}
if ($datadir) {
$args['dataDir'] = $datadir;
}
return $this->coreCommand('CREATE', $core, $args); return $this->coreCommand('CREATE', $core, $args);
} }
@ -61,7 +72,8 @@ class SolrService extends SolrService_Core {
* @param $core string - The name of the core * @param $core string - The name of the core
* @return Apache_Solr_Response * @return Apache_Solr_Response
*/ */
public function coreReload($core) { public function coreReload($core)
{
return $this->coreCommand('RELOAD', $core); return $this->coreCommand('RELOAD', $core);
} }
@ -70,7 +82,8 @@ class SolrService extends SolrService_Core {
* @param $core string - The name of the core * @param $core string - The name of the core
* @return Solr3Service_Core * @return Solr3Service_Core
*/ */
public function serviceForCore($core) { public function serviceForCore($core)
{
$klass = Config::inst()->get(get_called_class(), 'core_class'); $klass = Config::inst()->get(get_called_class(), 'core_class');
return new $klass($this->_host, $this->_port, $this->_path.$core, $this->_httpTransport); return new $klass($this->_host, $this->_port, $this->_path.$core, $this->_httpTransport);
} }

View File

@ -5,9 +5,10 @@ use Psr\Log\LoggerInterface;
/** /**
* Base class for re-indexing of solr content * Base class for re-indexing of solr content
*/ */
abstract class SolrReindexBase implements SolrReindexHandler { abstract class SolrReindexBase implements SolrReindexHandler
{
public function runReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null) { public function runReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
{
foreach (Solr::get_indexes() as $indexInstance) { foreach (Solr::get_indexes() as $indexInstance) {
$this->processIndex($logger, $indexInstance, $batchSize, $taskName, $classes); $this->processIndex($logger, $indexInstance, $batchSize, $taskName, $classes);
} }
@ -49,15 +50,16 @@ abstract class SolrReindexBase implements SolrReindexHandler {
* @param string|array $filterClasses Optional class or classes to limit to * @param string|array $filterClasses Optional class or classes to limit to
* @return array List of classes, where the key is the classname and value is list of options * @return array List of classes, where the key is the classname and value is list of options
*/ */
protected function getClassesForIndex(SolrIndex $index, $filterClasses = null) { protected function getClassesForIndex(SolrIndex $index, $filterClasses = null)
{
// Get base classes // Get base classes
$classes = $index->getClasses(); $classes = $index->getClasses();
if(!$filterClasses) { if (!$filterClasses) {
return $classes; return $classes;
} }
// Apply filter // Apply filter
if(!is_array($filterClasses)) { if (!is_array($filterClasses)) {
$filterClasses = explode(',', $filterClasses); $filterClasses = explode(',', $filterClasses);
} }
return array_intersect_key($classes, array_combine($filterClasses, $filterClasses)); return array_intersect_key($classes, array_combine($filterClasses, $filterClasses));
@ -83,7 +85,7 @@ abstract class SolrReindexBase implements SolrReindexHandler {
// Count records // Count records
$query = $class::get(); $query = $class::get();
if(!$includeSubclasses) { if (!$includeSubclasses) {
$query = $query->filter('ClassName', $class); $query = $query->filter('ClassName', $class);
} }
$total = $query->count(); $total = $query->count();
@ -172,7 +174,8 @@ abstract class SolrReindexBase implements SolrReindexHandler {
* @param int $group * @param int $group
* @return DataList * @return DataList
*/ */
protected function getRecordsInGroup(SolrIndex $indexInstance, $class, $groups, $group) { protected function getRecordsInGroup(SolrIndex $indexInstance, $class, $groups, $group)
{
// Generate filtered list of local records // Generate filtered list of local records
$baseClass = ClassInfo::baseDataClass($class); $baseClass = ClassInfo::baseDataClass($class);
$items = DataList::create($class) $items = DataList::create($class)
@ -187,7 +190,7 @@ abstract class SolrReindexBase implements SolrReindexHandler {
// Add child filter // Add child filter
$classes = $indexInstance->getClasses(); $classes = $indexInstance->getClasses();
$options = $classes[$class]; $options = $classes[$class];
if(!$options['include_children']) { if (!$options['include_children']) {
$items = $items->filter('ClassName', $class); $items = $items->filter('ClassName', $class);
} }
@ -204,12 +207,13 @@ abstract class SolrReindexBase implements SolrReindexHandler {
* @param int $groups Number of groups, if clearing from a striped group * @param int $groups Number of groups, if clearing from a striped group
* @param int $group Group number, if clearing from a striped group * @param int $group Group number, if clearing from a striped group
*/ */
protected function clearRecords(SolrIndex $indexInstance, $class, $groups = null, $group = null) { protected function clearRecords(SolrIndex $indexInstance, $class, $groups = null, $group = null)
{
// Clear by classname // Clear by classname
$conditions = array("+(ClassHierarchy:{$class})"); $conditions = array("+(ClassHierarchy:{$class})");
// If grouping, delete from this group only // If grouping, delete from this group only
if($groups) { if ($groups) {
$conditions[] = "+_query_:\"{!frange l={$group} u={$group}}mod(ID, {$groups})\""; $conditions[] = "+_query_:\"{!frange l={$group} u={$group}}mod(ID, {$groups})\"";
} }
@ -217,7 +221,7 @@ abstract class SolrReindexBase implements SolrReindexHandler {
$query = new SearchQuery(); $query = new SearchQuery();
SearchVariant::with($class) SearchVariant::with($class)
->call('alterQuery', $query, $indexInstance); ->call('alterQuery', $query, $indexInstance);
if($query->isfiltered()) { if ($query->isfiltered()) {
$conditions = array_merge($conditions, $indexInstance->getFiltersComponent($query)); $conditions = array_merge($conditions, $indexInstance->getFiltersComponent($query));
} }

View File

@ -5,8 +5,8 @@ use Psr\Log\LoggerInterface;
/** /**
* Provides interface for queueing a solr reindex * Provides interface for queueing a solr reindex
*/ */
interface SolrReindexHandler { interface SolrReindexHandler
{
/** /**
* Trigger a solr-reindex * Trigger a solr-reindex
* *

View File

@ -7,9 +7,10 @@ use Psr\Log\LoggerInterface;
* *
* Internally batches of records will be invoked via shell tasks in the background * Internally batches of records will be invoked via shell tasks in the background
*/ */
class SolrReindexImmediateHandler extends SolrReindexBase { class SolrReindexImmediateHandler extends SolrReindexBase
{
public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null) { public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
{
$this->runReindex($logger, $batchSize, $taskName, $classes); $this->runReindex($logger, $batchSize, $taskName, $classes);
} }
@ -59,7 +60,7 @@ class SolrReindexImmediateHandler extends SolrReindexBase {
// Execute script via shell // Execute script via shell
$res = $logger ? passthru($cmd) : `$cmd`; $res = $logger ? passthru($cmd) : `$cmd`;
if($logger) { if ($logger) {
$logger->info(preg_replace('/\r\n|\n/', '$0 ', $res)); $logger->info(preg_replace('/\r\n|\n/', '$0 ', $res));
} }

View File

@ -2,10 +2,12 @@
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
if(!class_exists('MessageQueue')) return; if (!class_exists('MessageQueue')) {
return;
class SolrReindexMessageHandler extends SolrReindexImmediateHandler { }
class SolrReindexMessageHandler extends SolrReindexImmediateHandler
{
/** /**
* The MessageQueue to use when processing updates * The MessageQueue to use when processing updates
* @config * @config
@ -13,7 +15,8 @@ class SolrReindexMessageHandler extends SolrReindexImmediateHandler {
*/ */
private static $reindex_queue = "search_indexing"; private static $reindex_queue = "search_indexing";
public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null) { public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
{
$queue = Config::inst()->get(__CLASS__, 'reindex_queue'); $queue = Config::inst()->get(__CLASS__, 'reindex_queue');
$logger->info('Queuing message'); $logger->info('Queuing message');
@ -30,7 +33,8 @@ class SolrReindexMessageHandler extends SolrReindexImmediateHandler {
* @param string $taskName * @param string $taskName
* @param array|string|null $classes * @param array|string|null $classes
*/ */
public static function run_reindex($batchSize, $taskName, $classes = null) { public static function run_reindex($batchSize, $taskName, $classes = null)
{
// @todo Logger for message queue? // @todo Logger for message queue?
$logger = Injector::inst()->createWithArgs('Monolog\Logger', array(strtolower(get_class()))); $logger = Injector::inst()->createWithArgs('Monolog\Logger', array(strtolower(get_class())));

View File

@ -2,17 +2,20 @@
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
if(!interface_exists('QueuedJob')) return; if (!interface_exists('QueuedJob')) {
return;
}
/** /**
* Represents a queued task to start the reindex job * Represents a queued task to start the reindex job
*/ */
class SolrReindexQueuedHandler extends SolrReindexBase { class SolrReindexQueuedHandler extends SolrReindexBase
{
/** /**
* @return QueuedJobService * @return QueuedJobService
*/ */
protected function getQueuedJobService() { protected function getQueuedJobService()
{
return singleton('QueuedJobService'); return singleton('QueuedJobService');
} }
@ -22,7 +25,8 @@ class SolrReindexQueuedHandler extends SolrReindexBase {
* @param string $type Type of job to cancel * @param string $type Type of job to cancel
* @return int Number of jobs cleared * @return int Number of jobs cleared
*/ */
protected function cancelExistingJobs($type) { protected function cancelExistingJobs($type)
{
$clearable = array( $clearable = array(
// Paused jobs need to be discarded // Paused jobs need to be discarded
QueuedJob::STATUS_PAUSED, QueuedJob::STATUS_PAUSED,
@ -47,7 +51,8 @@ class SolrReindexQueuedHandler extends SolrReindexBase {
return DB::affectedRows(); return DB::affectedRows();
} }
public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null) { public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
{
// Cancel existing jobs // Cancel existing jobs
$queues = $this->cancelExistingJobs('SolrReindexQueuedJob'); $queues = $this->cancelExistingJobs('SolrReindexQueuedJob');
$groups = $this->cancelExistingJobs('SolrReindexGroupQueuedJob'); $groups = $this->cancelExistingJobs('SolrReindexGroupQueuedJob');
@ -90,5 +95,4 @@ class SolrReindexQueuedHandler extends SolrReindexBase {
$logger->info("Queuing commit on all changes"); $logger->info("Queuing commit on all changes");
SearchUpdateCommitJobProcessor::queue(); SearchUpdateCommitJobProcessor::queue();
} }
} }

View File

@ -1,6 +1,8 @@
<?php <?php
if(!interface_exists('QueuedJob')) return; if (!interface_exists('QueuedJob')) {
return;
}
/** /**
* Queuedjob to re-index a small group within an index. * Queuedjob to re-index a small group within an index.
@ -11,8 +13,8 @@ if(!interface_exists('QueuedJob')) return;
* list of IDs. Instead groups are segmented by ID. Additionally, this task does incremental * list of IDs. Instead groups are segmented by ID. Additionally, this task does incremental
* deletions of records. * deletions of records.
*/ */
class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase { class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase
{
/** /**
* Name of index to reindex * Name of index to reindex
* *
@ -48,7 +50,8 @@ class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase {
*/ */
protected $group; protected $group;
public function __construct($indexName = null, $state = null, $class = null, $groups = null, $group = null) { public function __construct($indexName = null, $state = null, $class = null, $groups = null, $group = null)
{
parent::__construct(); parent::__construct();
$this->indexName = $indexName; $this->indexName = $indexName;
$this->state = $state; $this->state = $state;
@ -57,7 +60,8 @@ class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase {
$this->group = $group; $this->group = $group;
} }
public function getJobData() { public function getJobData()
{
$data = parent::getJobData(); $data = parent::getJobData();
// Custom data // Custom data
@ -70,7 +74,8 @@ class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase {
return $data; return $data;
} }
public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages) { public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages)
{
parent::setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages); parent::setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages);
// Custom data // Custom data
@ -81,11 +86,13 @@ class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase {
$this->group = $jobData->group; $this->group = $jobData->group;
} }
public function getSignature() { public function getSignature()
{
return md5(get_class($this) . time() . mt_rand(0, 100000)); return md5(get_class($this) . time() . mt_rand(0, 100000));
} }
public function getTitle() { public function getTitle()
{
return sprintf( return sprintf(
'Solr Reindex Group (%d/%d) of %s in %s', 'Solr Reindex Group (%d/%d) of %s in %s',
($this->group+1), ($this->group+1),
@ -95,9 +102,10 @@ class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase {
); );
} }
public function process() { public function process()
{
$logger = $this->getLogger(); $logger = $this->getLogger();
if($this->jobFinished()) { if ($this->jobFinished()) {
$logger->notice("reindex group already complete"); $logger->notice("reindex group already complete");
return; return;
} }
@ -113,5 +121,4 @@ class SolrReindexGroupQueuedJob extends SolrReindexQueuedJobBase {
$logger->info("Completed reindex group"); $logger->info("Completed reindex group");
$this->isComplete = true; $this->isComplete = true;
} }
} }

View File

@ -1,12 +1,14 @@
<?php <?php
if(!interface_exists('QueuedJob')) return; if (!interface_exists('QueuedJob')) {
return;
}
/** /**
* Represents a queuedjob which invokes a reindex * Represents a queuedjob which invokes a reindex
*/ */
class SolrReindexQueuedJob extends SolrReindexQueuedJobBase { class SolrReindexQueuedJob extends SolrReindexQueuedJobBase
{
/** /**
* Size of each batch to run * Size of each batch to run
* *
@ -29,14 +31,16 @@ class SolrReindexQueuedJob extends SolrReindexQueuedJobBase {
*/ */
protected $classes; protected $classes;
public function __construct($batchSize = null, $taskName = null, $classes = null) { public function __construct($batchSize = null, $taskName = null, $classes = null)
{
$this->batchSize = $batchSize; $this->batchSize = $batchSize;
$this->taskName = $taskName; $this->taskName = $taskName;
$this->classes = $classes; $this->classes = $classes;
parent::__construct(); parent::__construct();
} }
public function getJobData() { public function getJobData()
{
$data = parent::getJobData(); $data = parent::getJobData();
// Custom data // Custom data
@ -47,7 +51,8 @@ class SolrReindexQueuedJob extends SolrReindexQueuedJobBase {
return $data; return $data;
} }
public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages) { public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages)
{
parent::setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages); parent::setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages);
// Custom data // Custom data
@ -56,17 +61,20 @@ class SolrReindexQueuedJob extends SolrReindexQueuedJobBase {
$this->classes = $jobData->classes; $this->classes = $jobData->classes;
} }
public function getSignature() { public function getSignature()
{
return __CLASS__; return __CLASS__;
} }
public function getTitle() { public function getTitle()
{
return 'Solr Reindex Job'; return 'Solr Reindex Job';
} }
public function process() { public function process()
{
$logger = $this->getLogger(); $logger = $this->getLogger();
if($this->jobFinished()) { if ($this->jobFinished()) {
$logger->notice("reindex already complete"); $logger->notice("reindex already complete");
return; return;
} }
@ -85,7 +93,8 @@ class SolrReindexQueuedJob extends SolrReindexQueuedJobBase {
* *
* @return int * @return int
*/ */
public function getBatchSize() { public function getBatchSize()
{
return $this->batchSize; return $this->batchSize;
} }
} }

View File

@ -3,13 +3,15 @@
use Monolog\Logger; use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
if(!interface_exists('QueuedJob')) return; if (!interface_exists('QueuedJob')) {
return;
}
/** /**
* Base class for jobs which perform re-index * Base class for jobs which perform re-index
*/ */
abstract class SolrReindexQueuedJobBase implements QueuedJob { abstract class SolrReindexQueuedJobBase implements QueuedJob
{
/** /**
* Flag whether this job is done * Flag whether this job is done
* *
@ -31,7 +33,8 @@ abstract class SolrReindexQueuedJobBase implements QueuedJob {
*/ */
protected $logger; protected $logger;
public function __construct() { public function __construct()
{
$this->isComplete = false; $this->isComplete = false;
$this->messages = array(); $this->messages = array();
} }
@ -39,7 +42,8 @@ abstract class SolrReindexQueuedJobBase implements QueuedJob {
/** /**
* @return SearchLogFactory * @return SearchLogFactory
*/ */
protected function getLoggerFactory() { protected function getLoggerFactory()
{
return Injector::inst()->get('SearchLogFactory'); return Injector::inst()->get('SearchLogFactory');
} }
@ -48,8 +52,9 @@ abstract class SolrReindexQueuedJobBase implements QueuedJob {
* *
* @return LoggerInterface * @return LoggerInterface
*/ */
protected function getLogger() { protected function getLogger()
if($this->logger) { {
if ($this->logger) {
return $this->logger; return $this->logger;
} }
@ -65,11 +70,13 @@ abstract class SolrReindexQueuedJobBase implements QueuedJob {
* *
* @param LoggerInterface $logger * @param LoggerInterface $logger
*/ */
public function setLogger($logger) { public function setLogger($logger)
{
$this->logger = $logger; $this->logger = $logger;
} }
public function getJobData() { public function getJobData()
{
$data = new stdClass(); $data = new stdClass();
// Standard fields // Standard fields
@ -83,7 +90,8 @@ abstract class SolrReindexQueuedJobBase implements QueuedJob {
return $data; return $data;
} }
public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages) { public function setJobData($totalSteps, $currentStep, $isComplete, $jobData, $messages)
{
$this->isComplete = $isComplete; $this->isComplete = $isComplete;
$this->messages = $messages; $this->messages = $messages;
} }
@ -93,31 +101,38 @@ abstract class SolrReindexQueuedJobBase implements QueuedJob {
* *
* @return SolrReindexHandler * @return SolrReindexHandler
*/ */
protected function getHandler() { protected function getHandler()
{
return Injector::inst()->get('SolrReindexHandler'); return Injector::inst()->get('SolrReindexHandler');
} }
public function jobFinished() { public function jobFinished()
{
return $this->isComplete; return $this->isComplete;
} }
public function prepareForRestart() { public function prepareForRestart()
{
// NOOP // NOOP
} }
public function setup() { public function setup()
{
// NOOP // NOOP
} }
public function afterComplete() { public function afterComplete()
{
// NOOP // NOOP
} }
public function getJobType() { public function getJobType()
{
return QueuedJob::QUEUED; return QueuedJob::QUEUED;
} }
public function addMessage($message) { public function addMessage($message)
{
$this->messages[] = $message; $this->messages[] = $message;
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
class CombinationsArrayIterator implements Iterator { class CombinationsArrayIterator implements Iterator
{
protected $arrays; protected $arrays;
protected $keys; protected $keys;
protected $numArrays; protected $numArrays;
@ -8,7 +9,8 @@ class CombinationsArrayIterator implements Iterator {
protected $isValid = false; protected $isValid = false;
protected $k = 0; protected $k = 0;
function __construct($args) { public function __construct($args)
{
$this->arrays = array(); $this->arrays = array();
$this->keys = array(); $this->keys = array();
@ -26,41 +28,53 @@ class CombinationsArrayIterator implements Iterator {
$this->rewind(); $this->rewind();
} }
function rewind() { public function rewind()
{
if (!$this->numArrays) { if (!$this->numArrays) {
$this->isValid = false; $this->isValid = false;
} } else {
else {
$this->isValid = true; $this->isValid = true;
$this->k = 0; $this->k = 0;
for ($i = 0; $i < $this->numArrays; $i++) reset($this->arrays[$i]); for ($i = 0; $i < $this->numArrays; $i++) {
reset($this->arrays[$i]);
}
} }
} }
function valid() { public function valid()
{
return $this->isValid; return $this->isValid;
} }
function next() { public function next()
{
$this->k++; $this->k++;
for ($i = 0; $i < $this->numArrays; $i++) { for ($i = 0; $i < $this->numArrays; $i++) {
if (next($this->arrays[$i]) === false) { if (next($this->arrays[$i]) === false) {
if ($i == $this->numArrays-1) $this->isValid = false; if ($i == $this->numArrays-1) {
else reset($this->arrays[$i]); $this->isValid = false;
} else {
reset($this->arrays[$i]);
}
} else {
break;
} }
else break;
} }
} }
function current() { public function current()
{
$res = array(); $res = array();
for ($i = 0; $i < $this->numArrays; $i++) $res[$this->keys[$i]] = current($this->arrays[$i]); for ($i = 0; $i < $this->numArrays; $i++) {
$res[$this->keys[$i]] = current($this->arrays[$i]);
}
return $res; return $res;
} }
function key() { public function key()
{
return $this->k; return $this->k;
} }
} }

View File

@ -1,44 +1,58 @@
<?php <?php
class MultipleArrayIterator implements Iterator { class MultipleArrayIterator implements Iterator
{
protected $arrays; protected $arrays;
protected $active; protected $active;
function __construct() { public function __construct()
{
$args = func_get_args(); $args = func_get_args();
$this->arrays = array(); $this->arrays = array();
foreach ($args as $arg) { foreach ($args as $arg) {
if (is_array($arg) && count($arg)) $this->arrays[] = $arg; if (is_array($arg) && count($arg)) {
$this->arrays[] = $arg;
}
} }
$this->rewind(); $this->rewind();
} }
function rewind() { public function rewind()
{
$this->active = $this->arrays; $this->active = $this->arrays;
if ($this->active) reset($this->active[0]); if ($this->active) {
reset($this->active[0]);
}
} }
function current() { public function current()
{
return $this->active ? current($this->active[0]) : false; return $this->active ? current($this->active[0]) : false;
} }
function key() { public function key()
{
return $this->active ? key($this->active[0]) : false; return $this->active ? key($this->active[0]) : false;
} }
function next() { public function next()
if (!$this->active) return; {
if (!$this->active) {
return;
}
if (next($this->active[0]) === false) { if (next($this->active[0]) === false) {
array_shift($this->active); array_shift($this->active);
if ($this->active) reset($this->active[0]); if ($this->active) {
reset($this->active[0]);
}
} }
} }
function valid() { public function valid()
{
return $this->active && (current($this->active[0]) !== false); return $this->active && (current($this->active[0]) !== false);
} }
} }

View File

@ -1,8 +1,9 @@
<?php <?php
class WebDAV { class WebDAV
{
static function curl_init($url, $method) { public static function curl_init($url, $method)
{
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@ -12,7 +13,8 @@ class WebDAV {
return $ch; return $ch;
} }
static function exists($url) { public static function exists($url)
{
// WebDAV expects that checking a directory exists has a trailing slash // WebDAV expects that checking a directory exists has a trailing slash
if (substr($url, -1) != '/') { if (substr($url, -1) != '/') {
$url .= '/'; $url .= '/';
@ -23,13 +25,18 @@ class WebDAV {
$res = curl_exec($ch); $res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($code == 404) return false; if ($code == 404) {
if ($code == 200 || $code == 207) return true; return false;
}
if ($code == 200 || $code == 207) {
return true;
}
user_error("Got error from webdav server - ".$code, E_USER_ERROR); user_error("Got error from webdav server - ".$code, E_USER_ERROR);
} }
static function mkdir($url) { public static function mkdir($url)
{
$ch = self::curl_init(rtrim($url, '/').'/', 'MKCOL'); $ch = self::curl_init(rtrim($url, '/').'/', 'MKCOL');
$res = curl_exec($ch); $res = curl_exec($ch);
@ -38,7 +45,8 @@ class WebDAV {
return $code == 201; return $code == 201;
} }
static function put($handle, $url) { public static function put($handle, $url)
{
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
@ -53,16 +61,16 @@ class WebDAV {
return curl_getinfo($ch, CURLINFO_HTTP_CODE); return curl_getinfo($ch, CURLINFO_HTTP_CODE);
} }
static function upload_from_string($string, $url) { public static function upload_from_string($string, $url)
{
$fh = tmpfile(); $fh = tmpfile();
fwrite($fh, $string); fwrite($fh, $string);
fseek($fh, 0); fseek($fh, 0);
return self::put($fh, $url); return self::put($fh, $url);
} }
static function upload_from_file($string, $url) { public static function upload_from_file($string, $url)
{
return self::put(fopen($string, 'rb'), $url); return self::put(fopen($string, 'rb'), $url);
} }
} }

View File

@ -8,14 +8,15 @@ use Monolog\Logger;
/** /**
* Provides logging based on monolog * Provides logging based on monolog
*/ */
class MonologFactory implements SearchLogFactory { class MonologFactory implements SearchLogFactory
{
public function getOutputLogger($name, $verbose) { public function getOutputLogger($name, $verbose)
{
$logger = $this->getLoggerFor($name); $logger = $this->getLoggerFor($name);
$formatter = $this->getFormatter(); $formatter = $this->getFormatter();
// Notice handling // Notice handling
if($verbose) { if ($verbose) {
$messageHandler = $this->getStreamHandler($formatter, 'php://stdout', Logger::INFO); $messageHandler = $this->getStreamHandler($formatter, 'php://stdout', Logger::INFO);
$logger->pushHandler($messageHandler); $logger->pushHandler($messageHandler);
} }
@ -26,7 +27,8 @@ class MonologFactory implements SearchLogFactory {
return $logger; return $logger;
} }
public function getQueuedJobLogger($job) { public function getQueuedJobLogger($job)
{
$logger = $this->getLoggerFor(get_class($job)); $logger = $this->getLoggerFor(get_class($job));
$handler = $this->getJobHandler($job); $handler = $this->getJobHandler($job);
$logger->pushHandler($handler); $logger->pushHandler($handler);
@ -42,7 +44,8 @@ class MonologFactory implements SearchLogFactory {
* @param bool $bubble * @param bool $bubble
* @return HandlerInterface * @return HandlerInterface
*/ */
protected function getStreamHandler(FormatterInterface $formatter, $stream, $level = Logger::DEBUG, $bubble = true) { protected function getStreamHandler(FormatterInterface $formatter, $stream, $level = Logger::DEBUG, $bubble = true)
{
// Unless cli, force output to php://output // Unless cli, force output to php://output
$stream = Director::is_cli() ? $stream : 'php://output'; $stream = Director::is_cli() ? $stream : 'php://output';
$handler = Injector::inst()->createWithArgs( $handler = Injector::inst()->createWithArgs(
@ -58,10 +61,11 @@ class MonologFactory implements SearchLogFactory {
* *
* @return FormatterInterface * @return FormatterInterface
*/ */
protected function getFormatter() { protected function getFormatter()
{
// Get formatter // Get formatter
$format = LineFormatter::SIMPLE_FORMAT; $format = LineFormatter::SIMPLE_FORMAT;
if(!Director::is_cli()) { if (!Director::is_cli()) {
$format = "<p>$format</p>"; $format = "<p>$format</p>";
} }
return Injector::inst()->createWithArgs( return Injector::inst()->createWithArgs(
@ -76,7 +80,8 @@ class MonologFactory implements SearchLogFactory {
* @param string $name * @param string $name
* @return Logger * @return Logger
*/ */
protected function getLoggerFor($name) { protected function getLoggerFor($name)
{
return Injector::inst()->createWithArgs( return Injector::inst()->createWithArgs(
'Monolog\Logger', 'Monolog\Logger',
array(strtolower($name)) array(strtolower($name))
@ -89,7 +94,8 @@ class MonologFactory implements SearchLogFactory {
* @param QueuedJob $job * @param QueuedJob $job
* @return HandlerInterface * @return HandlerInterface
*/ */
protected function getJobHandler($job) { protected function getJobHandler($job)
{
return Injector::inst()->createWithArgs( return Injector::inst()->createWithArgs(
'QueuedJobLogHandler', 'QueuedJobLogHandler',
array($job, Logger::INFO) array($job, Logger::INFO)

View File

@ -3,13 +3,15 @@
use Monolog\Handler\AbstractProcessingHandler; use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger; use Monolog\Logger;
if(!interface_exists('QueuedJob')) return; if (!interface_exists('QueuedJob')) {
return;
}
/** /**
* Handler for logging events into QueuedJob message data * Handler for logging events into QueuedJob message data
*/ */
class QueuedJobLogHandler extends AbstractProcessingHandler { class QueuedJobLogHandler extends AbstractProcessingHandler
{
/** /**
* Job to log to * Job to log to
* *
@ -22,7 +24,8 @@ class QueuedJobLogHandler extends AbstractProcessingHandler {
* @param integer $level The minimum logging level at which this handler will be triggered * @param integer $level The minimum logging level at which this handler will be triggered
* @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not
*/ */
public function __construct(QueuedJob $queuedJob, $level = Logger::DEBUG, $bubble = true) { public function __construct(QueuedJob $queuedJob, $level = Logger::DEBUG, $bubble = true)
{
parent::__construct($level, $bubble); parent::__construct($level, $bubble);
$this->setQueuedJob($queuedJob); $this->setQueuedJob($queuedJob);
} }
@ -32,7 +35,8 @@ class QueuedJobLogHandler extends AbstractProcessingHandler {
* *
* @param QueuedJob $queuedJob * @param QueuedJob $queuedJob
*/ */
public function setQueuedJob(QueuedJob $queuedJob) { public function setQueuedJob(QueuedJob $queuedJob)
{
$this->queuedJob = $queuedJob; $this->queuedJob = $queuedJob;
} }
@ -41,13 +45,14 @@ class QueuedJobLogHandler extends AbstractProcessingHandler {
* *
* @return QueuedJob * @return QueuedJob
*/ */
public function getQueuedJob() { public function getQueuedJob()
{
return $this->queuedJob; return $this->queuedJob;
} }
protected function write(array $record) { protected function write(array $record)
{
// Write formatted message // Write formatted message
$this->getQueuedJob()->addMessage($record['formatted']); $this->getQueuedJob()->addMessage($record['formatted']);
} }
} }

View File

@ -2,8 +2,8 @@
use Psr\Log; use Psr\Log;
interface SearchLogFactory { interface SearchLogFactory
{
/** /**
* Make a logger for a queuedjob * Make a logger for a queuedjob
* *

View File

@ -1,23 +1,28 @@
<?php <?php
class BatchedProcessorTest_Object extends SiteTree implements TestOnly { class BatchedProcessorTest_Object extends SiteTree implements TestOnly
{
private static $db = array( private static $db = array(
'TestText' => 'Varchar' 'TestText' => 'Varchar'
); );
} }
class BatchedProcessorTest_Index extends SearchIndex_Recording implements TestOnly { class BatchedProcessorTest_Index extends SearchIndex_Recording implements TestOnly
function init() { {
public function init()
{
$this->addClass('BatchedProcessorTest_Object'); $this->addClass('BatchedProcessorTest_Object');
$this->addFilterField('TestText'); $this->addFilterField('TestText');
} }
} }
class BatchedProcessor_QueuedJobService { class BatchedProcessor_QueuedJobService
{
protected $jobs = array(); protected $jobs = array();
public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null) { public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
{
$this->jobs[] = array( $this->jobs[] = array(
'job' => $job, 'job' => $job,
'startAfter' => $startAfter 'startAfter' => $startAfter
@ -25,7 +30,8 @@ class BatchedProcessor_QueuedJobService {
return $job; return $job;
} }
public function getJobs() { public function getJobs()
{
return $this->jobs; return $this->jobs;
} }
} }
@ -33,8 +39,8 @@ class BatchedProcessor_QueuedJobService {
/** /**
* Tests {@see SearchUpdateQueuedJobProcessor} * Tests {@see SearchUpdateQueuedJobProcessor}
*/ */
class BatchedProcessorTest extends SapphireTest { class BatchedProcessorTest extends SapphireTest
{
protected $oldProcessor; protected $oldProcessor;
protected $extraDataObjects = array( protected $extraDataObjects = array(
@ -48,15 +54,17 @@ class BatchedProcessorTest extends SapphireTest {
) )
); );
public function setUpOnce() { public function setUpOnce()
{
// Disable illegal extensions if skipping this test // Disable illegal extensions if skipping this test
if(class_exists('Subsite') || !interface_exists('QueuedJob')) { if (class_exists('Subsite') || !interface_exists('QueuedJob')) {
$this->illegalExtensions = array(); $this->illegalExtensions = array();
} }
parent::setUpOnce(); parent::setUpOnce();
} }
public function setUp() { public function setUp()
{
parent::setUp(); parent::setUp();
Config::nest(); Config::nest();
@ -65,7 +73,7 @@ class BatchedProcessorTest extends SapphireTest {
$this->markTestSkipped("These tests need the QueuedJobs module installed to run"); $this->markTestSkipped("These tests need the QueuedJobs module installed to run");
} }
if(class_exists('Subsite')) { if (class_exists('Subsite')) {
$this->skipTest = true; $this->skipTest = true;
$this->markTestSkipped(get_class() . ' skipped when running with subsites'); $this->markTestSkipped(get_class() . ' skipped when running with subsites');
} }
@ -89,8 +97,9 @@ class BatchedProcessorTest extends SapphireTest {
SearchUpdater::$processor = new SearchUpdateQueuedJobProcessor(); SearchUpdater::$processor = new SearchUpdateQueuedJobProcessor();
} }
public function tearDown() { public function tearDown()
if($this->oldProcessor) { {
if ($this->oldProcessor) {
SearchUpdater::$processor = $this->oldProcessor; SearchUpdater::$processor = $this->oldProcessor;
} }
Config::unnest(); Config::unnest();
@ -102,9 +111,10 @@ class BatchedProcessorTest extends SapphireTest {
/** /**
* @return SearchUpdateQueuedJobProcessor * @return SearchUpdateQueuedJobProcessor
*/ */
protected function generateDirtyIds() { protected function generateDirtyIds()
{
$processor = SearchUpdater::$processor; $processor = SearchUpdater::$processor;
for($id = 1; $id <= 42; $id++) { for ($id = 1; $id <= 42; $id++) {
// Save to db // Save to db
$object = new BatchedProcessorTest_Object(); $object = new BatchedProcessorTest_Object();
$object->TestText = 'Object ' . $id; $object->TestText = 'Object ' . $id;
@ -126,7 +136,8 @@ class BatchedProcessorTest extends SapphireTest {
/** /**
* Tests that large jobs are broken up into a suitable number of batches * Tests that large jobs are broken up into a suitable number of batches
*/ */
public function testBatching() { public function testBatching()
{
$index = singleton('BatchedProcessorTest_Index'); $index = singleton('BatchedProcessorTest_Index');
$index->reset(); $index->reset();
$processor = $this->generateDirtyIds(); $processor = $this->generateDirtyIds();
@ -139,7 +150,7 @@ class BatchedProcessorTest extends SapphireTest {
$this->assertEquals(0, count($index->getAdded())); $this->assertEquals(0, count($index->getAdded()));
// Advance state // Advance state
for($pass = 1; $pass <= 8; $pass++) { for ($pass = 1; $pass <= 8; $pass++) {
$processor->process(); $processor->process();
$data = $processor->getJobData(); $data = $processor->getJobData();
$this->assertEquals($pass, $data->currentStep); $this->assertEquals($pass, $data->currentStep);
@ -164,7 +175,8 @@ class BatchedProcessorTest extends SapphireTest {
/** /**
* Test creation of multiple commit jobs * Test creation of multiple commit jobs
*/ */
public function testMultipleCommits() { public function testMultipleCommits()
{
$index = singleton('BatchedProcessorTest_Index'); $index = singleton('BatchedProcessorTest_Index');
$index->reset(); $index->reset();
@ -210,7 +222,8 @@ class BatchedProcessorTest extends SapphireTest {
/** /**
* Tests that the batch_soft_cap setting is properly respected * Tests that the batch_soft_cap setting is properly respected
*/ */
public function testSoftCap() { public function testSoftCap()
{
$index = singleton('BatchedProcessorTest_Index'); $index = singleton('BatchedProcessorTest_Index');
$index->reset(); $index->reset();
$processor = $this->generateDirtyIds(); $processor = $this->generateDirtyIds();
@ -235,7 +248,7 @@ class BatchedProcessorTest extends SapphireTest {
$this->assertEquals(8, $data->totalSteps); $this->assertEquals(8, $data->totalSteps);
// Process all data and ensure that all are processed adequately // Process all data and ensure that all are processed adequately
for($pass = 1; $pass <= 8; $pass++) { for ($pass = 1; $pass <= 8; $pass++) {
$processor->process(); $processor->process();
} }
$data = $processor->getJobData(); $data = $processor->getJobData();

View File

@ -1,6 +1,7 @@
<?php <?php
class SearchUpdaterTest_Container extends DataObject { class SearchUpdaterTest_Container extends DataObject
{
private static $db = array( private static $db = array(
'Field1' => 'Varchar', 'Field1' => 'Varchar',
'Field2' => 'Varchar', 'Field2' => 'Varchar',
@ -20,7 +21,8 @@ class SearchUpdaterTest_Container extends DataObject {
); );
} }
class SearchUpdaterTest_HasOne extends DataObject { class SearchUpdaterTest_HasOne extends DataObject
{
private static $db = array( private static $db = array(
'Field1' => 'Varchar', 'Field1' => 'Varchar',
'Field2' => 'Varchar' 'Field2' => 'Varchar'
@ -31,7 +33,8 @@ class SearchUpdaterTest_HasOne extends DataObject {
); );
} }
class SearchUpdaterTest_HasMany extends DataObject { class SearchUpdaterTest_HasMany extends DataObject
{
private static $db = array( private static $db = array(
'Field1' => 'Varchar', 'Field1' => 'Varchar',
'Field2' => 'Varchar' 'Field2' => 'Varchar'
@ -42,7 +45,8 @@ class SearchUpdaterTest_HasMany extends DataObject {
); );
} }
class SearchUpdaterTest_ManyMany extends DataObject { class SearchUpdaterTest_ManyMany extends DataObject
{
private static $db = array( private static $db = array(
'Field1' => 'Varchar', 'Field1' => 'Varchar',
'Field2' => 'Varchar' 'Field2' => 'Varchar'
@ -53,8 +57,10 @@ class SearchUpdaterTest_ManyMany extends DataObject {
); );
} }
class SearchUpdaterTest_Index extends SearchIndex_Recording { class SearchUpdaterTest_Index extends SearchIndex_Recording
function init() { {
public function init()
{
$this->addClass('SearchUpdaterTest_Container'); $this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('Field1'); $this->addFilterField('Field1');
@ -63,17 +69,21 @@ class SearchUpdaterTest_Index extends SearchIndex_Recording {
} }
} }
class SearchUpdaterTest extends SapphireTest { class SearchUpdaterTest extends SapphireTest
{
protected $usesDatabase = true; protected $usesDatabase = true;
private static $index = null; private static $index = null;
function setUp() { public function setUp()
{
parent::setUp(); parent::setUp();
if (self::$index === null) self::$index = singleton(get_class($this).'_Index'); if (self::$index === null) {
else self::$index->reset(); self::$index = singleton(get_class($this).'_Index');
} else {
self::$index->reset();
}
SearchUpdater::bind_manipulation_capture(); SearchUpdater::bind_manipulation_capture();
@ -87,13 +97,15 @@ class SearchUpdaterTest extends SapphireTest {
SearchUpdater::clear_dirty_indexes(); SearchUpdater::clear_dirty_indexes();
} }
function tearDown() { public function tearDown()
{
Config::unnest(); Config::unnest();
parent::tearDown(); parent::tearDown();
} }
function testBasic() { public function testBasic()
{
$item = new SearchUpdaterTest_Container(); $item = new SearchUpdaterTest_Container();
$item->write(); $item->write();
@ -101,7 +113,8 @@ class SearchUpdaterTest extends SapphireTest {
// TODO: Get updating just field2 to not update item (maybe not possible - variants complicate) // TODO: Get updating just field2 to not update item (maybe not possible - variants complicate)
} }
function testHasOneHook() { public function testHasOneHook()
{
$hasOne = new SearchUpdaterTest_HasOne(); $hasOne = new SearchUpdaterTest_HasOne();
$hasOne->write(); $hasOne->write();
@ -126,7 +139,7 @@ class SearchUpdaterTest extends SapphireTest {
$added = self::$index->getAdded(array('ID')); $added = self::$index->getAdded(array('ID'));
// Some databases don't output $added in a consistent order; that's okay // Some databases don't output $added in a consistent order; that's okay
usort($added, function($a,$b) {return $a['ID']-$b['ID']; }); usort($added, function ($a, $b) {return $a['ID']-$b['ID']; });
$this->assertEquals($added, array( $this->assertEquals($added, array(
array('ID' => $container1->ID), array('ID' => $container1->ID),
@ -144,7 +157,7 @@ class SearchUpdaterTest extends SapphireTest {
SearchUpdater::flush_dirty_indexes(); SearchUpdater::flush_dirty_indexes();
$added = self::$index->getAdded(array('ID')); $added = self::$index->getAdded(array('ID'));
// Some databases don't output $added in a consistent order; that's okay // Some databases don't output $added in a consistent order; that's okay
usort($added, function($a,$b) {return $a['ID']-$b['ID']; }); usort($added, function ($a, $b) {return $a['ID']-$b['ID']; });
$this->assertEquals($added, array( $this->assertEquals($added, array(
array('ID' => $container1->ID), array('ID' => $container1->ID),
@ -174,7 +187,8 @@ class SearchUpdaterTest extends SapphireTest {
)); ));
} }
function testHasManyHook() { public function testHasManyHook()
{
$container1 = new SearchUpdaterTest_Container(); $container1 = new SearchUpdaterTest_Container();
$container1->write(); $container1->write();

View File

@ -1,46 +1,55 @@
<?php <?php
class SearchVariantSiteTreeSubsitesPolyhomeTest_Item extends SiteTree { class SearchVariantSiteTreeSubsitesPolyhomeTest_Item extends SiteTree
{
// TODO: Currently theres a failure if you addClass a non-table class // TODO: Currently theres a failure if you addClass a non-table class
private static $db = array( private static $db = array(
'TestText' => 'Varchar' 'TestText' => 'Varchar'
); );
} }
class SearchVariantSiteTreeSubsitesPolyhomeTest_Index extends SearchIndex_Recording { class SearchVariantSiteTreeSubsitesPolyhomeTest_Index extends SearchIndex_Recording
function init() { {
public function init()
{
$this->addClass('SearchVariantSiteTreeSubsitesPolyhomeTest_Item'); $this->addClass('SearchVariantSiteTreeSubsitesPolyhomeTest_Item');
$this->addFilterField('TestText'); $this->addFilterField('TestText');
} }
} }
class SearchVariantSiteTreeSubsitesPolyhomeTest extends SapphireTest { class SearchVariantSiteTreeSubsitesPolyhomeTest extends SapphireTest
{
private static $index = null; private static $index = null;
private static $subsite_a = null; private static $subsite_a = null;
private static $subsite_b = null; private static $subsite_b = null;
function setUp() { public function setUp()
{
parent::setUp(); parent::setUp();
// Check subsites installed // Check subsites installed
if(!class_exists('Subsite') || !class_exists('SubsitePolyhome')) { if (!class_exists('Subsite') || !class_exists('SubsitePolyhome')) {
return $this->markTestSkipped('The subsites polyhome module is not installed'); return $this->markTestSkipped('The subsites polyhome module is not installed');
} }
if (self::$index === null) self::$index = singleton('SearchVariantSiteTreeSubsitesPolyhomeTest_Index'); if (self::$index === null) {
self::$index = singleton('SearchVariantSiteTreeSubsitesPolyhomeTest_Index');
}
if (self::$subsite_a === null) { if (self::$subsite_a === null) {
self::$subsite_a = new Subsite(); self::$subsite_a->write(); self::$subsite_a = new Subsite();
self::$subsite_b = new Subsite(); self::$subsite_b->write(); self::$subsite_a->write();
self::$subsite_b = new Subsite();
self::$subsite_b->write();
} }
FullTextSearch::force_index_list(self::$index); FullTextSearch::force_index_list(self::$index);
SearchUpdater::clear_dirty_indexes(); SearchUpdater::clear_dirty_indexes();
} }
function testSavingDirect() { public function testSavingDirect()
{
// Initial add // Initial add
$item = new SearchVariantSiteTreeSubsitesPolyhomeTest_Item(); $item = new SearchVariantSiteTreeSubsitesPolyhomeTest_Item();
@ -70,7 +79,5 @@ class SearchVariantSiteTreeSubsitesPolyhomeTest extends SapphireTest {
'SearchVariantVersioned' => 'Stage', 'SearchVariantSiteTreeSubsitesPolyhome' => self::$subsite_b->ID 'SearchVariantVersioned' => 'Stage', 'SearchVariantSiteTreeSubsitesPolyhome' => self::$subsite_b->ID
)) ))
)); ));
} }
} }

View File

@ -1,22 +1,25 @@
<?php <?php
class SearchVariantVersionedTest extends SapphireTest { class SearchVariantVersionedTest extends SapphireTest
{
private static $index = null; private static $index = null;
protected $extraDataObjects = array( protected $extraDataObjects = array(
'SearchVariantVersionedTest_Item' 'SearchVariantVersionedTest_Item'
); );
function setUp() { public function setUp()
{
parent::setUp(); parent::setUp();
// Check versioned available // Check versioned available
if(!class_exists('Versioned')) { if (!class_exists('Versioned')) {
return $this->markTestSkipped('The versioned decorator is not installed'); return $this->markTestSkipped('The versioned decorator is not installed');
} }
if (self::$index === null) self::$index = singleton('SearchVariantVersionedTest_Index'); if (self::$index === null) {
self::$index = singleton('SearchVariantVersionedTest_Index');
}
SearchUpdater::bind_manipulation_capture(); SearchUpdater::bind_manipulation_capture();
@ -30,13 +33,15 @@ class SearchVariantVersionedTest extends SapphireTest {
SearchUpdater::clear_dirty_indexes(); SearchUpdater::clear_dirty_indexes();
} }
function tearDown() { public function tearDown()
{
Config::unnest(); Config::unnest();
parent::tearDown(); parent::tearDown();
} }
function testPublishing() { public function testPublishing()
{
// Check that write updates Stage // Check that write updates Stage
$item = new SearchVariantVersionedTest_Item(array('TestText' => 'Foo')); $item = new SearchVariantVersionedTest_Item(array('TestText' => 'Foo'));
@ -71,7 +76,8 @@ class SearchVariantVersionedTest extends SapphireTest {
)); ));
} }
function testExcludeVariantState() { public function testExcludeVariantState()
{
$index = singleton('SearchVariantVersionedTest_IndexNoStage'); $index = singleton('SearchVariantVersionedTest_IndexNoStage');
FullTextSearch::force_index_list($index); FullTextSearch::force_index_list($index);
@ -91,22 +97,27 @@ class SearchVariantVersionedTest extends SapphireTest {
} }
} }
class SearchVariantVersionedTest_Item extends SiteTree implements TestOnly { class SearchVariantVersionedTest_Item extends SiteTree implements TestOnly
{
// TODO: Currently theres a failure if you addClass a non-table class // TODO: Currently theres a failure if you addClass a non-table class
private static $db = array( private static $db = array(
'TestText' => 'Varchar' 'TestText' => 'Varchar'
); );
} }
class SearchVariantVersionedTest_Index extends SearchIndex_Recording { class SearchVariantVersionedTest_Index extends SearchIndex_Recording
function init() { {
public function init()
{
$this->addClass('SearchVariantVersionedTest_Item'); $this->addClass('SearchVariantVersionedTest_Item');
$this->addFilterField('TestText'); $this->addFilterField('TestText');
} }
} }
class SearchVariantVersionedTest_IndexNoStage extends SearchIndex_Recording { class SearchVariantVersionedTest_IndexNoStage extends SearchIndex_Recording
function init() { {
public function init()
{
$this->addClass('SearchVariantVersionedTest_Item'); $this->addClass('SearchVariantVersionedTest_Item');
$this->addFilterField('TestText'); $this->addFilterField('TestText');
$this->excludeVariantState(array('SearchVariantVersioned' => 'Stage')); $this->excludeVariantState(array('SearchVariantVersioned' => 'Stage'));

View File

@ -3,24 +3,27 @@
/** /**
* Test solr 4.0 compatibility * Test solr 4.0 compatibility
*/ */
class Solr4ServiceTest extends SapphireTest { class Solr4ServiceTest extends SapphireTest
{
/** /**
* *
* @return Solr4ServiceTest_RecordingService * @return Solr4ServiceTest_RecordingService
*/ */
protected function getMockService() { protected function getMockService()
{
return new Solr4ServiceTest_RecordingService(); return new Solr4ServiceTest_RecordingService();
} }
protected function getMockDocument($id) { protected function getMockDocument($id)
{
$document = new Apache_Solr_Document(); $document = new Apache_Solr_Document();
$document->setField('id', $id); $document->setField('id', $id);
$document->setField('title', "Item $id"); $document->setField('title', "Item $id");
return $document; return $document;
} }
public function testAddDocument() { public function testAddDocument()
{
$service = $this->getMockService(); $service = $this->getMockService();
$sent = $service->addDocument($this->getMockDocument('A'), false); $sent = $service->addDocument($this->getMockDocument('A'), false);
$this->assertEquals( $this->assertEquals(
@ -34,7 +37,8 @@ class Solr4ServiceTest extends SapphireTest {
); );
} }
public function testAddDocuments() { public function testAddDocuments()
{
$service = $this->getMockService(); $service = $this->getMockService();
$sent = $service->addDocuments(array( $sent = $service->addDocuments(array(
$this->getMockDocument('C'), $this->getMockDocument('C'),
@ -55,12 +59,15 @@ class Solr4ServiceTest extends SapphireTest {
} }
} }
class Solr4ServiceTest_RecordingService extends Solr4Service_Core { class Solr4ServiceTest_RecordingService extends Solr4Service_Core
protected function _sendRawPost($url, $rawPost, $timeout = FALSE, $contentType = 'text/xml; charset=UTF-8') { {
protected function _sendRawPost($url, $rawPost, $timeout = false, $contentType = 'text/xml; charset=UTF-8')
{
return $rawPost; return $rawPost;
} }
protected function _sendRawGet($url, $timeout = FALSE) { protected function _sendRawGet($url, $timeout = false)
{
return $url; return $url;
} }
} }

View File

@ -1,13 +1,17 @@
<?php <?php
class SolrIndexTest extends SapphireTest { class SolrIndexTest extends SapphireTest
{
function setUpOnce() { public function setUpOnce()
{
parent::setUpOnce(); parent::setUpOnce();
if (class_exists('Phockito')) Phockito::include_hamcrest(); if (class_exists('Phockito')) {
Phockito::include_hamcrest();
}
} }
function setUp() { public function setUp()
{
if (!class_exists('Phockito')) { if (!class_exists('Phockito')) {
$this->markTestSkipped("These tests need the Phockito module installed to run"); $this->markTestSkipped("These tests need the Phockito module installed to run");
$this->skipTest = true; $this->skipTest = true;
@ -16,7 +20,8 @@ class SolrIndexTest extends SapphireTest {
parent::setUp(); parent::setUp();
} }
function testFieldDataHasOne() { public function testFieldDataHasOne()
{
$index = new SolrIndexTest_FakeIndex(); $index = new SolrIndexTest_FakeIndex();
$data = $index->fieldData('HasOneObject.Field1'); $data = $index->fieldData('HasOneObject.Field1');
$data = $data['SearchUpdaterTest_Container_HasOneObject_Field1']; $data = $data['SearchUpdaterTest_Container_HasOneObject_Field1'];
@ -26,7 +31,8 @@ class SolrIndexTest extends SapphireTest {
$this->assertEquals('SearchUpdaterTest_HasOne', $data['class']); $this->assertEquals('SearchUpdaterTest_HasOne', $data['class']);
} }
function testFieldDataHasMany() { public function testFieldDataHasMany()
{
$index = new SolrIndexTest_FakeIndex(); $index = new SolrIndexTest_FakeIndex();
$data = $index->fieldData('HasManyObjects.Field1'); $data = $index->fieldData('HasManyObjects.Field1');
$data = $data['SearchUpdaterTest_Container_HasManyObjects_Field1']; $data = $data['SearchUpdaterTest_Container_HasManyObjects_Field1'];
@ -36,7 +42,8 @@ class SolrIndexTest extends SapphireTest {
$this->assertEquals('SearchUpdaterTest_HasMany', $data['class']); $this->assertEquals('SearchUpdaterTest_HasMany', $data['class']);
} }
function testFieldDataManyMany() { public function testFieldDataManyMany()
{
$index = new SolrIndexTest_FakeIndex(); $index = new SolrIndexTest_FakeIndex();
$data = $index->fieldData('ManyManyObjects.Field1'); $data = $index->fieldData('ManyManyObjects.Field1');
$data = $data['SearchUpdaterTest_Container_ManyManyObjects_Field1']; $data = $data['SearchUpdaterTest_Container_ManyManyObjects_Field1'];
@ -49,7 +56,8 @@ class SolrIndexTest extends SapphireTest {
/** /**
* Test boosting on SearchQuery * Test boosting on SearchQuery
*/ */
function testBoostedQuery() { public function testBoostedQuery()
{
$serviceMock = $this->getServiceMock(); $serviceMock = $this->getServiceMock();
Phockito::when($serviceMock)->search(anything(), anything(), anything(), anything(), anything())->return($this->getFakeRawSolrResponse()); Phockito::when($serviceMock)->search(anything(), anything(), anything(), anything(), anything())->return($this->getFakeRawSolrResponse());
@ -70,7 +78,8 @@ class SolrIndexTest extends SapphireTest {
/** /**
* Test boosting on field schema (via queried fields parameter) * Test boosting on field schema (via queried fields parameter)
*/ */
public function testBoostedField() { public function testBoostedField()
{
$serviceMock = $this->getServiceMock(); $serviceMock = $this->getServiceMock();
Phockito::when($serviceMock) Phockito::when($serviceMock)
->search(anything(), anything(), anything(), anything(), anything()) ->search(anything(), anything(), anything(), anything(), anything())
@ -92,7 +101,8 @@ class SolrIndexTest extends SapphireTest {
->search('+term', anything(), anything(), $matcher, anything()); ->search('+term', anything(), anything(), $matcher, anything());
} }
function testHighlightQueryOnBoost() { public function testHighlightQueryOnBoost()
{
$serviceMock = $this->getServiceMock(); $serviceMock = $this->getServiceMock();
Phockito::when($serviceMock)->search(anything(), anything(), anything(), anything(), anything())->return($this->getFakeRawSolrResponse()); Phockito::when($serviceMock)->search(anything(), anything(), anything(), anything(), anything())->return($this->getFakeRawSolrResponse());
@ -134,7 +144,8 @@ class SolrIndexTest extends SapphireTest {
); );
} }
function testIndexExcludesNullValues() { public function testIndexExcludesNullValues()
{
$serviceMock = $this->getServiceMock(); $serviceMock = $this->getServiceMock();
$index = new SolrIndexTest_FakeIndex(); $index = new SolrIndexTest_FakeIndex();
$index->setService($serviceMock); $index->setService($serviceMock);
@ -157,7 +168,8 @@ class SolrIndexTest extends SapphireTest {
$this->assertEquals('2010-12-30T00:00:00Z', $value['value'], 'Writes non-NULL dates'); $this->assertEquals('2010-12-30T00:00:00Z', $value['value'], 'Writes non-NULL dates');
} }
function testAddFieldExtraOptions() { public function testAddFieldExtraOptions()
{
Config::inst()->nest(); Config::inst()->nest();
Config::inst()->update('Director', 'environment_type', 'live'); // dev mode sets stored=true for everything Config::inst()->update('Director', 'environment_type', 'live'); // dev mode sets stored=true for everything
@ -175,7 +187,8 @@ class SolrIndexTest extends SapphireTest {
Config::inst()->unnest(); Config::inst()->unnest();
} }
function testAddAnalyzer() { public function testAddAnalyzer()
{
$index = new SolrIndexTest_FakeIndex(); $index = new SolrIndexTest_FakeIndex();
$defs = simplexml_load_string('<fields>' . $index->getFieldDefinitions() . '</fields>'); $defs = simplexml_load_string('<fields>' . $index->getFieldDefinitions() . '</fields>');
@ -191,7 +204,8 @@ class SolrIndexTest extends SapphireTest {
$this->assertEquals('solr.HTMLStripCharFilterFactory', $analyzers[0]->charFilter[0]['class']); $this->assertEquals('solr.HTMLStripCharFilterFactory', $analyzers[0]->charFilter[0]['class']);
} }
function testAddCopyField() { public function testAddCopyField()
{
$index = new SolrIndexTest_FakeIndex(); $index = new SolrIndexTest_FakeIndex();
$index->addCopyField('sourceField', 'destField'); $index->addCopyField('sourceField', 'destField');
@ -205,7 +219,8 @@ class SolrIndexTest extends SapphireTest {
/** /**
* Tests the setting of the 'stored' flag * Tests the setting of the 'stored' flag
*/ */
public function testStoredFields() { public function testStoredFields()
{
// Test two fields // Test two fields
$index = new SolrIndexTest_FakeIndex2(); $index = new SolrIndexTest_FakeIndex2();
$index->addStoredField('Field1'); $index->addStoredField('Field1');
@ -238,18 +253,21 @@ class SolrIndexTest extends SapphireTest {
/** /**
* @return Solr3Service * @return Solr3Service
*/ */
protected function getServiceMock() { protected function getServiceMock()
{
return Phockito::mock('Solr3Service'); return Phockito::mock('Solr3Service');
} }
protected function getServiceSpy() { protected function getServiceSpy()
{
$serviceSpy = Phockito::spy('Solr3Service'); $serviceSpy = Phockito::spy('Solr3Service');
Phockito::when($serviceSpy)->_sendRawPost()->return($this->getFakeRawSolrResponse()); Phockito::when($serviceSpy)->_sendRawPost()->return($this->getFakeRawSolrResponse());
return $serviceSpy; return $serviceSpy;
} }
protected function getFakeRawSolrResponse() { protected function getFakeRawSolrResponse()
{
return new Apache_Solr_Response( return new Apache_Solr_Response(
new Apache_Solr_HttpTransport_Response( new Apache_Solr_HttpTransport_Response(
null, null,
@ -260,8 +278,10 @@ class SolrIndexTest extends SapphireTest {
} }
} }
class SolrIndexTest_FakeIndex extends SolrIndex { class SolrIndexTest_FakeIndex extends SolrIndex
function init() { {
public function init()
{
$this->addClass('SearchUpdaterTest_Container'); $this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('Field1'); $this->addFilterField('Field1');
@ -273,14 +293,16 @@ class SolrIndexTest_FakeIndex extends SolrIndex {
} }
class SolrIndexTest_FakeIndex2 extends SolrIndex { class SolrIndexTest_FakeIndex2 extends SolrIndex
{
protected function getStoredDefault() { protected function getStoredDefault()
{
// Override isDev defaulting to stored // Override isDev defaulting to stored
return 'false'; return 'false';
} }
function init() { public function init()
{
$this->addClass('SearchUpdaterTest_Container'); $this->addClass('SearchUpdaterTest_Container');
$this->addFilterField('MyDate', 'Date'); $this->addFilterField('MyDate', 'Date');
$this->addFilterField('HasOneObject.Field1'); $this->addFilterField('HasOneObject.Field1');
@ -290,18 +312,19 @@ class SolrIndexTest_FakeIndex2 extends SolrIndex {
} }
class SolrIndexTest_BoostedIndex extends SolrIndex { class SolrIndexTest_BoostedIndex extends SolrIndex
{
protected function getStoredDefault() { protected function getStoredDefault()
{
// Override isDev defaulting to stored // Override isDev defaulting to stored
return 'false'; return 'false';
} }
function init() { public function init()
{
$this->addClass('SearchUpdaterTest_Container'); $this->addClass('SearchUpdaterTest_Container');
$this->addAllFulltextFields(); $this->addAllFulltextFields();
$this->setFieldBoosting('SearchUpdaterTest_Container_Field1', 1.5); $this->setFieldBoosting('SearchUpdaterTest_Container_Field1', 1.5);
$this->addBoostedField('Field2', null, array(), 2.1); $this->addBoostedField('Field2', null, array(), 2.1);
} }
} }

View File

@ -1,9 +1,11 @@
<?php <?php
if (class_exists('Phockito')) Phockito::include_hamcrest(); if (class_exists('Phockito')) {
Phockito::include_hamcrest();
class SolrIndexVersionedTest extends SapphireTest { }
class SolrIndexVersionedTest extends SapphireTest
{
protected $oldMode = null; protected $oldMode = null;
protected static $index = null; protected static $index = null;
@ -12,8 +14,8 @@ class SolrIndexVersionedTest extends SapphireTest {
'SearchVariantVersionedTest_Item' 'SearchVariantVersionedTest_Item'
); );
public function setUp() { public function setUp()
{
parent::setUp(); parent::setUp();
if (!class_exists('Phockito')) { if (!class_exists('Phockito')) {
@ -22,12 +24,14 @@ class SolrIndexVersionedTest extends SapphireTest {
} }
// Check versioned available // Check versioned available
if(!class_exists('Versioned')) { if (!class_exists('Versioned')) {
$this->skipTest = true; $this->skipTest = true;
return $this->markTestSkipped('The versioned decorator is not installed'); return $this->markTestSkipped('The versioned decorator is not installed');
} }
if (self::$index === null) self::$index = singleton('SolrVersionedTest_Index'); if (self::$index === null) {
self::$index = singleton('SolrVersionedTest_Index');
}
SearchUpdater::bind_manipulation_capture(); SearchUpdater::bind_manipulation_capture();
@ -44,23 +48,27 @@ class SolrIndexVersionedTest extends SapphireTest {
Versioned::reading_stage('Stage'); Versioned::reading_stage('Stage');
} }
public function tearDown() { public function tearDown()
{
Versioned::set_reading_mode($this->oldMode); Versioned::set_reading_mode($this->oldMode);
Config::unnest(); Config::unnest();
parent::tearDown(); parent::tearDown();
} }
protected function getServiceMock() { protected function getServiceMock()
{
return Phockito::mock('Solr3Service'); return Phockito::mock('Solr3Service');
} }
protected function getExpectedDocumentId($id, $stage) { protected function getExpectedDocumentId($id, $stage)
{
// Prevent subsites from breaking tests // Prevent subsites from breaking tests
$subsites = class_exists('Subsite') ? '"SearchVariantSubsites":"0",' : ''; $subsites = class_exists('Subsite') ? '"SearchVariantSubsites":"0",' : '';
return $id.'-SiteTree-{'.$subsites.'"SearchVariantVersioned":"'.$stage.'"}'; return $id.'-SiteTree-{'.$subsites.'"SearchVariantVersioned":"'.$stage.'"}';
} }
public function testPublishing() { public function testPublishing()
{
// Setup mocks // Setup mocks
$serviceMock = $this->getServiceMock(); $serviceMock = $this->getServiceMock();
@ -92,7 +100,8 @@ class SolrIndexVersionedTest extends SapphireTest {
Phockito::verify($serviceMock)->addDocument($doc); Phockito::verify($serviceMock)->addDocument($doc);
} }
public function testDelete() { public function testDelete()
{
// Setup mocks // Setup mocks
$serviceMock = $this->getServiceMock(); $serviceMock = $this->getServiceMock();
@ -130,36 +139,45 @@ class SolrIndexVersionedTest extends SapphireTest {
} }
class SolrVersionedTest_Index extends SolrIndex { class SolrVersionedTest_Index extends SolrIndex
function init() { {
public function init()
{
$this->addClass('SearchVariantVersionedTest_Item'); $this->addClass('SearchVariantVersionedTest_Item');
$this->addFilterField('TestText'); $this->addFilterField('TestText');
} }
} }
if (!class_exists('Phockito')) return; if (!class_exists('Phockito')) {
return;
class SolrDocumentMatcher extends Hamcrest_BaseMatcher { }
class SolrDocumentMatcher extends Hamcrest_BaseMatcher
{
protected $properties; protected $properties;
public function __construct($properties) { public function __construct($properties)
{
$this->properties = $properties; $this->properties = $properties;
} }
public function describeTo(\Hamcrest_Description $description) { public function describeTo(\Hamcrest_Description $description)
{
$description->appendText('Apache_Solr_Document with properties '.var_export($this->properties, true)); $description->appendText('Apache_Solr_Document with properties '.var_export($this->properties, true));
} }
public function matches($item) { public function matches($item)
{
if (! ($item instanceof Apache_Solr_Document)) {
return false;
}
if(! ($item instanceof Apache_Solr_Document)) return false; foreach ($this->properties as $key => $value) {
if ($item->{$key} != $value) {
foreach($this->properties as $key => $value) { return false;
if($item->{$key} != $value) return false; }
} }
return true; return true;
} }
} }

View File

@ -3,8 +3,8 @@
/** /**
* Additional tests of solr reindexing processes when run with queuedjobs * Additional tests of solr reindexing processes when run with queuedjobs
*/ */
class SolrReindexQueuedTest extends SapphireTest { class SolrReindexQueuedTest extends SapphireTest
{
protected $usesDatabase = true; protected $usesDatabase = true;
protected $extraDataObjects = array( protected $extraDataObjects = array(
@ -25,7 +25,8 @@ class SolrReindexQueuedTest extends SapphireTest {
*/ */
protected $service = null; protected $service = null;
public function setUp() { public function setUp()
{
parent::setUp(); parent::setUp();
if (!class_exists('Phockito')) { if (!class_exists('Phockito')) {
@ -33,7 +34,7 @@ class SolrReindexQueuedTest extends SapphireTest {
return $this->markTestSkipped("These tests need the Phockito module installed to run"); return $this->markTestSkipped("These tests need the Phockito module installed to run");
} }
if(!interface_exists('QueuedJob')) { if (!interface_exists('QueuedJob')) {
$this->skipTest = true; $this->skipTest = true;
return $this->markTestSkipped("These tests need the QueuedJobs module installed to run"); return $this->markTestSkipped("These tests need the QueuedJobs module installed to run");
} }
@ -59,14 +60,15 @@ class SolrReindexQueuedTest extends SapphireTest {
* *
* @param int $number Number of records to create in each variant * @param int $number Number of records to create in each variant
*/ */
protected function createDummyData($number) { protected function createDummyData($number)
{
// Populate dataobjects. Use truncate to generate predictable IDs // Populate dataobjects. Use truncate to generate predictable IDs
DB::query('TRUNCATE "SolrReindexTest_Item"'); DB::query('TRUNCATE "SolrReindexTest_Item"');
// Note that we don't create any records in variant = 2, to represent a variant // Note that we don't create any records in variant = 2, to represent a variant
// that should be cleared without any re-indexes performed // that should be cleared without any re-indexes performed
foreach(array(0, 1) as $variant) { foreach (array(0, 1) as $variant) {
for($i = 1; $i <= $number; $i++) { for ($i = 1; $i <= $number; $i++) {
$item = new SolrReindexTest_Item(); $item = new SolrReindexTest_Item();
$item->Variant = $variant; $item->Variant = $variant;
$item->Title = "Item $variant / $i"; $item->Title = "Item $variant / $i";
@ -80,11 +82,13 @@ class SolrReindexQueuedTest extends SapphireTest {
* *
* @return SolrService * @return SolrService
*/ */
protected function getServiceMock() { protected function getServiceMock()
{
return Phockito::mock('Solr4Service'); return Phockito::mock('Solr4Service');
} }
public function tearDown() { public function tearDown()
{
FullTextSearch::force_index_list(); FullTextSearch::force_index_list();
SolrReindexTest_Variant::disable(); SolrReindexTest_Variant::disable();
parent::tearDown(); parent::tearDown();
@ -95,14 +99,16 @@ class SolrReindexQueuedTest extends SapphireTest {
* *
* @return SolrReindexHandler * @return SolrReindexHandler
*/ */
protected function getHandler() { protected function getHandler()
{
return Injector::inst()->get('SolrReindexHandler'); return Injector::inst()->get('SolrReindexHandler');
} }
/** /**
* @return SolrReindexQueuedTest_Service * @return SolrReindexQueuedTest_Service
*/ */
protected function getQueuedJobService() { protected function getQueuedJobService()
{
return singleton('SolrReindexQueuedTest_Service'); return singleton('SolrReindexQueuedTest_Service');
} }
@ -110,7 +116,8 @@ class SolrReindexQueuedTest extends SapphireTest {
* Test that reindex will generate a top top level queued job, and executing this will perform * Test that reindex will generate a top top level queued job, and executing this will perform
* the necessary initialisation of the grouped queued jobs * the necessary initialisation of the grouped queued jobs
*/ */
public function testReindexSegmentsGroups() { public function testReindexSegmentsGroups()
{
$this->createDummyData(18); $this->createDummyData(18);
// Create pre-existing jobs // Create pre-existing jobs
@ -163,7 +170,8 @@ class SolrReindexQueuedTest extends SapphireTest {
/** /**
* Test index processing on individual groups * Test index processing on individual groups
*/ */
public function testRunGroup() { public function testRunGroup()
{
$this->createDummyData(18); $this->createDummyData(18);
// Just do what the SolrReindexQueuedJob would do to create each sub // Just do what the SolrReindexQueuedJob would do to create each sub
@ -197,23 +205,25 @@ class SolrReindexQueuedTest extends SapphireTest {
$this->assertNotEmpty(preg_match('/^Updated (?<ids>[,\d]+)/i', $idMessage[0], $matches)); $this->assertNotEmpty(preg_match('/^Updated (?<ids>[,\d]+)/i', $idMessage[0], $matches));
$ids = array_unique(explode(',', $matches['ids'])); $ids = array_unique(explode(',', $matches['ids']));
$this->assertEquals(6, count($ids)); $this->assertEquals(6, count($ids));
foreach($ids as $id) { foreach ($ids as $id) {
// Each id should be % 3 == 0 // Each id should be % 3 == 0
$this->assertEquals(0, $id % 3, "ID $id Should match pattern ID % 3 = 0"); $this->assertEquals(0, $id % 3, "ID $id Should match pattern ID % 3 = 0");
} }
} }
} }
if(!class_exists('QueuedJobService')) return; if (!class_exists('QueuedJobService')) {
return;
class SolrReindexQueuedTest_Service extends QueuedJobService implements TestOnly { }
class SolrReindexQueuedTest_Service extends QueuedJobService implements TestOnly
{
/** /**
* @return QueuedJob * @return QueuedJob
*/ */
public function getNextJob() { public function getNextJob()
{
$job = $this->getNextPendingJob(); $job = $this->getNextPendingJob();
return $this->initialiseJob($job); return $this->initialiseJob($job);
} }
} }

View File

@ -5,10 +5,12 @@ use Monolog\Handler\HandlerInterface;
use Monolog\Logger; use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
if (class_exists('Phockito')) Phockito::include_hamcrest(); if (class_exists('Phockito')) {
Phockito::include_hamcrest();
class SolrReindexTest extends SapphireTest { }
class SolrReindexTest extends SapphireTest
{
protected $usesDatabase = true; protected $usesDatabase = true;
protected $extraDataObjects = array( protected $extraDataObjects = array(
@ -29,7 +31,8 @@ class SolrReindexTest extends SapphireTest {
*/ */
protected $service = null; protected $service = null;
public function setUp() { public function setUp()
{
parent::setUp(); parent::setUp();
if (!class_exists('Phockito')) { if (!class_exists('Phockito')) {
@ -58,14 +61,15 @@ class SolrReindexTest extends SapphireTest {
* *
* @param int $number Number of records to create in each variant * @param int $number Number of records to create in each variant
*/ */
protected function createDummyData($number) { protected function createDummyData($number)
{
// Populate dataobjects. Use truncate to generate predictable IDs // Populate dataobjects. Use truncate to generate predictable IDs
DB::query('TRUNCATE "SolrReindexTest_Item"'); DB::query('TRUNCATE "SolrReindexTest_Item"');
// Note that we don't create any records in variant = 2, to represent a variant // Note that we don't create any records in variant = 2, to represent a variant
// that should be cleared without any re-indexes performed // that should be cleared without any re-indexes performed
foreach(array(0, 1) as $variant) { foreach (array(0, 1) as $variant) {
for($i = 1; $i <= $number; $i++) { for ($i = 1; $i <= $number; $i++) {
$item = new SolrReindexTest_Item(); $item = new SolrReindexTest_Item();
$item->Variant = $variant; $item->Variant = $variant;
$item->Title = "Item $variant / $i"; $item->Title = "Item $variant / $i";
@ -79,11 +83,13 @@ class SolrReindexTest extends SapphireTest {
* *
* @return SolrService * @return SolrService
*/ */
protected function getServiceMock() { protected function getServiceMock()
{
return Phockito::mock('Solr4Service'); return Phockito::mock('Solr4Service');
} }
public function tearDown() { public function tearDown()
{
FullTextSearch::force_index_list(); FullTextSearch::force_index_list();
SolrReindexTest_Variant::disable(); SolrReindexTest_Variant::disable();
parent::tearDown(); parent::tearDown();
@ -94,14 +100,16 @@ class SolrReindexTest extends SapphireTest {
* *
* @return SolrReindexHandler * @return SolrReindexHandler
*/ */
protected function getHandler() { protected function getHandler()
{
return Injector::inst()->get('SolrReindexHandler'); return Injector::inst()->get('SolrReindexHandler');
} }
/** /**
* Ensure the test variant is up and running properly * Ensure the test variant is up and running properly
*/ */
public function testVariant() { public function testVariant()
{
// State defaults to 0 // State defaults to 0
$variant = SearchVariant::current_state(); $variant = SearchVariant::current_state();
$this->assertEquals( $this->assertEquals(
@ -147,7 +155,8 @@ class SolrReindexTest extends SapphireTest {
* *
* Test should work fine with any variants (versioned, subsites, etc) specified * Test should work fine with any variants (versioned, subsites, etc) specified
*/ */
public function testReindexSegmentsGroups() { public function testReindexSegmentsGroups()
{
$this->createDummyData(120); $this->createDummyData(120);
// Initiate re-index // Initiate re-index
@ -197,7 +206,8 @@ class SolrReindexTest extends SapphireTest {
/** /**
* Test index processing on individual groups * Test index processing on individual groups
*/ */
public function testRunGroup() { public function testRunGroup()
{
$this->createDummyData(120); $this->createDummyData(120);
$logger = new SolrReindexTest_RecordingLogger(); $logger = new SolrReindexTest_RecordingLogger();
@ -220,7 +230,7 @@ class SolrReindexTest extends SapphireTest {
// Test that items in this variant / group are re-indexed // Test that items in this variant / group are re-indexed
// 120 divided into 6 groups should be 20 at least (max 21) // 120 divided into 6 groups should be 20 at least (max 21)
$this->assertEquals(21, count($ids), 'Group size is about 20', 1); $this->assertEquals(21, count($ids), 'Group size is about 20', 1);
foreach($ids as $id) { foreach ($ids as $id) {
// Each id should be % 6 == 2 // Each id should be % 6 == 2
$this->assertEquals(2, $id % 6, "ID $id Should match pattern ID % 6 = 2"); $this->assertEquals(2, $id % 6, "ID $id Should match pattern ID % 6 = 2");
} }
@ -229,13 +239,14 @@ class SolrReindexTest extends SapphireTest {
/** /**
* Test that running all groups covers the entire range of dataobject IDs * Test that running all groups covers the entire range of dataobject IDs
*/ */
public function testRunAllGroups() { public function testRunAllGroups()
{
$this->createDummyData(120); $this->createDummyData(120);
$logger = new SolrReindexTest_RecordingLogger(); $logger = new SolrReindexTest_RecordingLogger();
// Test that running all groups covers the complete set of ids // Test that running all groups covers the complete set of ids
$state = array('SolrReindexTest_Variant' => '1'); $state = array('SolrReindexTest_Variant' => '1');
for($i = 0; $i < 6; $i++) { for ($i = 0; $i < 6; $i++) {
// See testReindexSegmentsGroups for test that each of these states is invoked during a full reindex // See testReindexSegmentsGroups for test that each of these states is invoked during a full reindex
$this $this
->getHandler() ->getHandler()
@ -244,7 +255,7 @@ class SolrReindexTest extends SapphireTest {
// Count all ids updated // Count all ids updated
$ids = array(); $ids = array();
foreach($logger->filterMessages('Updated ') as $message) { foreach ($logger->filterMessages('Updated ') as $message) {
$this->assertNotEmpty(preg_match('/^Updated (?<ids>[,\d]+)/', $message, $matches)); $this->assertNotEmpty(preg_match('/^Updated (?<ids>[,\d]+)/', $message, $matches));
$ids = array_unique(array_merge($ids, explode(',', $matches['ids']))); $ids = array_unique(array_merge($ids, explode(',', $matches['ids'])));
} }
@ -276,8 +287,8 @@ class SolrReindexTest extends SapphireTest {
/** /**
* Provides a wrapper for testing SolrReindexBase * Provides a wrapper for testing SolrReindexBase
*/ */
class SolrReindexTest_TestHandler extends SolrReindexBase { class SolrReindexTest_TestHandler extends SolrReindexBase
{
public function processGroup( public function processGroup(
LoggerInterface $logger, SolrIndex $indexInstance, $state, $class, $groups, $group, $taskName LoggerInterface $logger, SolrIndex $indexInstance, $state, $class, $groups, $group, $taskName
) { ) {
@ -286,15 +297,17 @@ class SolrReindexTest_TestHandler extends SolrReindexBase {
$logger->info("Called processGroup with {$indexName}, {$stateName}, {$class}, group {$group} of {$groups}"); $logger->info("Called processGroup with {$indexName}, {$stateName}, {$class}, group {$group} of {$groups}");
} }
public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null) { public function triggerReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
{
$logger->info("Called triggerReindex"); $logger->info("Called triggerReindex");
} }
} }
class SolrReindexTest_Index extends SolrIndex implements TestOnly { class SolrReindexTest_Index extends SolrIndex implements TestOnly
public function init() { {
public function init()
{
$this->addClass('SolrReindexTest_Item'); $this->addClass('SolrReindexTest_Item');
$this->addAllFulltextFields(); $this->addAllFulltextFields();
} }
@ -303,8 +316,8 @@ class SolrReindexTest_Index extends SolrIndex implements TestOnly {
/** /**
* Does not have any variant extensions * Does not have any variant extensions
*/ */
class SolrReindexTest_Item extends DataObject implements TestOnly { class SolrReindexTest_Item extends DataObject implements TestOnly
{
private static $extensions = array( private static $extensions = array(
'SolrReindexTest_ItemExtension' 'SolrReindexTest_ItemExtension'
); );
@ -313,23 +326,23 @@ class SolrReindexTest_Item extends DataObject implements TestOnly {
'Title' => 'Varchar(255)', 'Title' => 'Varchar(255)',
'Variant' => 'Int(0)' 'Variant' => 'Int(0)'
); );
} }
/** /**
* Select only records in the current variant * Select only records in the current variant
*/ */
class SolrReindexTest_ItemExtension extends DataExtension implements TestOnly { class SolrReindexTest_ItemExtension extends DataExtension implements TestOnly
{
/** /**
* Filter records on the current variant * Filter records on the current variant
* *
* @param SQLQuery $query * @param SQLQuery $query
* @param DataQuery $dataQuery * @param DataQuery $dataQuery
*/ */
public function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) { public function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null)
{
$variant = SolrReindexTest_Variant::get_current(); $variant = SolrReindexTest_Variant::get_current();
if($variant !== null && !$query->filtersOnID()) { if ($variant !== null && !$query->filtersOnID()) {
$sqlVariant = Convert::raw2sql($variant); $sqlVariant = Convert::raw2sql($variant);
$query->addWhere("\"Variant\" = '{$sqlVariant}'"); $query->addWhere("\"Variant\" = '{$sqlVariant}'");
} }
@ -342,8 +355,8 @@ class SolrReindexTest_ItemExtension extends DataExtension implements TestOnly {
* *
* Variant states are 0 and 1, or null if disabled * Variant states are 0 and 1, or null if disabled
*/ */
class SolrReindexTest_Variant extends SearchVariant implements TestOnly { class SolrReindexTest_Variant extends SearchVariant implements TestOnly
{
/** /**
* Value of this variant (either null, 0, or 1) * Value of this variant (either null, 0, or 1)
* *
@ -354,7 +367,8 @@ class SolrReindexTest_Variant extends SearchVariant implements TestOnly {
/** /**
* Activate this variant * Activate this variant
*/ */
public static function enable() { public static function enable()
{
self::disable(); self::disable();
self::$current = 0; self::$current = 0;
@ -366,14 +380,16 @@ class SolrReindexTest_Variant extends SearchVariant implements TestOnly {
/** /**
* Disable this variant and reset * Disable this variant and reset
*/ */
public static function disable() { public static function disable()
{
self::$current = null; self::$current = null;
self::$variants = null; self::$variants = null;
self::$class_variants = array(); self::$class_variants = array();
self::$call_instances = array(); self::$call_instances = array();
} }
public function activateState($state) { public function activateState($state)
{
self::set_current($state); self::set_current($state);
} }
@ -382,7 +398,8 @@ class SolrReindexTest_Variant extends SearchVariant implements TestOnly {
* *
* @param int $current 0, 1, 2, or null (disabled) * @param int $current 0, 1, 2, or null (disabled)
*/ */
public static function set_current($current) { public static function set_current($current)
{
self::$current = $current; self::$current = $current;
} }
@ -391,14 +408,16 @@ class SolrReindexTest_Variant extends SearchVariant implements TestOnly {
* *
* @return string|null * @return string|null
*/ */
public static function get_current() { public static function get_current()
{
// Always use string values for states for consistent json_encode value // Always use string values for states for consistent json_encode value
if(isset(self::$current)) { if (isset(self::$current)) {
return (string)self::$current; return (string)self::$current;
} }
} }
function alterDefinition($base, $index) { public function alterDefinition($base, $index)
{
$self = get_class($this); $self = get_class($this);
$index->filterFields['_testvariant'] = array( $index->filterFields['_testvariant'] = array(
@ -412,43 +431,48 @@ class SolrReindexTest_Variant extends SearchVariant implements TestOnly {
); );
} }
public function alterQuery($query, $index) { public function alterQuery($query, $index)
{
// I guess just calling it _testvariant is ok? // I guess just calling it _testvariant is ok?
$query->filter('_testvariant', $this->currentState()); $query->filter('_testvariant', $this->currentState());
} }
public function appliesTo($class, $includeSubclasses) { public function appliesTo($class, $includeSubclasses)
{
return $class === 'SolrReindexTest_Item' || return $class === 'SolrReindexTest_Item' ||
($includeSubclasses && is_subclass_of($class, 'SolrReindexTest_Item', true)); ($includeSubclasses && is_subclass_of($class, 'SolrReindexTest_Item', true));
} }
public function appliesToEnvironment() { public function appliesToEnvironment()
{
// Set to null to disable // Set to null to disable
return self::$current !== null; return self::$current !== null;
} }
public function currentState() { public function currentState()
{
return self::get_current(); return self::get_current();
} }
public function reindexStates() { public function reindexStates()
{
// Always use string values for states for consistent json_encode value // Always use string values for states for consistent json_encode value
return array('0', '1', '2'); return array('0', '1', '2');
} }
} }
/** /**
* Test logger for recording messages * Test logger for recording messages
*/ */
class SolrReindexTest_RecordingLogger extends Logger implements TestOnly { class SolrReindexTest_RecordingLogger extends Logger implements TestOnly
{
/** /**
* @var SolrReindexTest_Handler * @var SolrReindexTest_Handler
*/ */
protected $testHandler = null; protected $testHandler = null;
public function __construct($name = 'testlogger', array $handlers = array(), array $processors = array()) { public function __construct($name = 'testlogger', array $handlers = array(), array $processors = array())
{
parent::__construct($name, $handlers, $processors); parent::__construct($name, $handlers, $processors);
$this->testHandler = new SolrReindexTest_Handler(); $this->testHandler = new SolrReindexTest_Handler();
@ -458,14 +482,16 @@ class SolrReindexTest_RecordingLogger extends Logger implements TestOnly {
/** /**
* @return array * @return array
*/ */
public function getMessages() { public function getMessages()
{
return $this->testHandler->getMessages(); return $this->testHandler->getMessages();
} }
/** /**
* Clear all messages * Clear all messages
*/ */
public function clear() { public function clear()
{
$this->testHandler->clear(); $this->testHandler->clear();
} }
@ -475,10 +501,11 @@ class SolrReindexTest_RecordingLogger extends Logger implements TestOnly {
* @param string $containing * @param string $containing
* @return array Filtered array * @return array Filtered array
*/ */
public function filterMessages($containing) { public function filterMessages($containing)
{
return array_values(array_filter( return array_values(array_filter(
$this->getMessages(), $this->getMessages(),
function($content) use ($containing) { function ($content) use ($containing) {
return stripos($content, $containing) !== false; return stripos($content, $containing) !== false;
} }
)); ));
@ -490,8 +517,9 @@ class SolrReindexTest_RecordingLogger extends Logger implements TestOnly {
* @param string $containing Message to filter by * @param string $containing Message to filter by
* @return int * @return int
*/ */
public function countMessages($containing = null) { public function countMessages($containing = null)
if($containing) { {
if ($containing) {
$messages = $this->filterMessages($containing); $messages = $this->filterMessages($containing);
} else { } else {
$messages = $this->getMessages(); $messages = $this->getMessages();
@ -503,8 +531,8 @@ class SolrReindexTest_RecordingLogger extends Logger implements TestOnly {
/** /**
* Logger for recording messages for later retrieval * Logger for recording messages for later retrieval
*/ */
class SolrReindexTest_Handler extends AbstractProcessingHandler implements TestOnly { class SolrReindexTest_Handler extends AbstractProcessingHandler implements TestOnly
{
/** /**
* Messages * Messages
* *
@ -517,15 +545,18 @@ class SolrReindexTest_Handler extends AbstractProcessingHandler implements TestO
* *
* @return array * @return array
*/ */
public function getMessages() { public function getMessages()
{
return $this->messages; return $this->messages;
} }
public function clear() { public function clear()
{
$this->messages = array(); $this->messages = array();
} }
protected function write(array $record) { protected function write(array $record)
{
$this->messages[] = $record['message']; $this->messages[] = $record['message'];
} }
} }