(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60232 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-09 06:40:50 +00:00
parent 4ec93162a0
commit 75f2cf2654
24 changed files with 342 additions and 107 deletions

View File

@ -27,6 +27,15 @@ abstract class DataFormatter extends Object {
*/ */
public $relationDepth = 1; public $relationDepth = 1;
/**
* Allows overriding of the fields which are rendered for the
* processed dataobjects. By default, this includes all
* fields in {@link DataObject::inheritedDatabaseFields()}.
*
* @var array
*/
protected $customFields = null;
/** /**
* Get a DataFormatter object suitable for handling the given file extension * Get a DataFormatter object suitable for handling the given file extension
*/ */
@ -46,6 +55,50 @@ abstract class DataFormatter extends Object {
} }
} }
/**
* @param array $fields
*/
public function setCustomFields($fields) {
$this->customFields = $fields;
}
/**
* @return array
*/
public function getCustomFields() {
return $this->customFields;
}
/**
* Returns all fields on the object which should be shown
* in the output. Can be customised through {@link self::setCustomFields()}.
*
* @todo Allow for custom getters on the processed object (currently filtered through inheritedDatabaseFields)
* @todo Field level permission checks
*
* @param DataObject $obj
* @return array
*/
protected function getFieldsForObj($obj) {
$dbFields = array();
// if custom fields are specified, only select these
if($this->customFields) {
foreach($this->customFields as $fieldName) {
// @todo Possible security risk by making methods accessible - implement field-level security
if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) $dbFields[$fieldName] = $fieldName;
}
} else {
// by default, all database fields are selected
$dbFields = $obj->inheritedDatabaseFields();
}
// add default required fields
$dbFields = array_merge($dbFields, array('ID'=>'Int'));
return $dbFields;
}
/** /**
* Return an array of the extensions that this data formatter supports * Return an array of the extensions that this data formatter supports
*/ */
@ -54,13 +107,11 @@ abstract class DataFormatter extends Object {
/** /**
* Convert a single data object to this format. Return a string. * Convert a single data object to this format. Return a string.
* @todo Add parameters for things like selecting output columns
*/ */
abstract function convertDataObject(DataObjectInterface $do); abstract function convertDataObject(DataObjectInterface $do);
/** /**
* Convert a data object set to this format. Return a string. * Convert a data object set to this format. Return a string.
* @todo Add parameters for things like selecting output columns
*/ */
abstract function convertDataObjectSet(DataObjectSet $set); abstract function convertDataObjectSet(DataObjectSet $set);

View File

@ -22,8 +22,7 @@ class JSONDataFormatter extends DataFormatter {
$id = $obj->ID; $id = $obj->ID;
$json = "{\n className : \"$className\",\n"; $json = "{\n className : \"$className\",\n";
$dbFields = array_merge($obj->inheritedDatabaseFields(), array('ID'=>'Int')); foreach($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
foreach($dbFields as $fieldName => $fieldType) {
if(is_object($obj->$fieldName)) { if(is_object($obj->$fieldName)) {
$jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON(); $jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON();
} else { } else {

View File

@ -23,11 +23,19 @@
* - DELETE /api/v1/(ClassName)/(ID)/(Relation)/(ForeignID) - remove the relationship between two database records, but don't actually delete the foreign object (NOT IMPLEMENTED YET) * - DELETE /api/v1/(ClassName)/(ID)/(Relation)/(ForeignID) - remove the relationship between two database records, but don't actually delete the foreign object (NOT IMPLEMENTED YET)
* *
* - POST /api/v1/(ClassName)/(ID)/(MethodName) - executes a method on the given object (e.g, publish) * - POST /api/v1/(ClassName)/(ID)/(MethodName) - executes a method on the given object (e.g, publish)
*
* @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
* *
* @package sapphire * You can trigger searches based on the fields specified on {@link DataObject::searchable_fields} and passed
* @subpackage api * through {@link DataObject::getDefaultSearchContext()}. Just add a key-value pair with the search-term
* to the url, e.g. /api/v1/(ClassName)/?Title=mytitle
*
* Other url-modifiers:
* - &limit=<numeric>: Limit the result set
* - &relationdepth=<numeric>: Displays links to existing has-one and has-many relationships to a certain depth (Default: 1)
* - &fields=<string>: Comma-separated list of fields on the output object (defaults to all database-columns)
*
* @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
* @todo Make SearchContext specification customizeable for each class
* @todo Allow for range-searches (e.g. on Created column)
*/ */
class RestfulServer extends Controller { class RestfulServer extends Controller {
static $url_handlers = array( static $url_handlers = array(
@ -74,6 +82,9 @@ class RestfulServer extends Controller {
if(!$extension) $extension = "xml"; if(!$extension) $extension = "xml";
$formatter = DataFormatter::for_extension($extension); //$this->dataFormatterFromMime($contentType); $formatter = DataFormatter::for_extension($extension); //$this->dataFormatterFromMime($contentType);
if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields));
$relationDepth = $this->request->getVar('relationdepth');
if(is_numeric($relationDepth)) $formatter->relationDepth = (int)$relationDepth;
switch($requestMethod) { switch($requestMethod) {
case 'GET': case 'GET':
@ -169,7 +180,7 @@ class RestfulServer extends Controller {
// show empty serialized result when no records are present // show empty serialized result when no records are present
if(!$obj) $obj = new DataObjectSet(); if(!$obj) $obj = new DataObjectSet();
} }
if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj); if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj);
else return $formatter->convertDataObject($obj); else return $formatter->convertDataObject($obj);
} }

View File

@ -29,8 +29,7 @@ class XMLDataFormatter extends DataFormatter {
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID"); $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
$json = "<$className href=\"$objHref.xml\">\n"; $json = "<$className href=\"$objHref.xml\">\n";
$dbFields = array_merge($obj->inheritedDatabaseFields(), array('ID'=>'Int')); foreach($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
foreach($dbFields as $fieldName => $fieldType) {
if(is_object($obj->$fieldName)) { if(is_object($obj->$fieldName)) {
$json .= $obj->$fieldName->toXML(); $json .= $obj->$fieldName->toXML();
} else { } else {

View File

@ -1261,7 +1261,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
// We need to find the inverse component name // We need to find the inverse component name
$otherManyMany = singleton($candidate)->stat('many_many'); $otherManyMany = singleton($candidate)->stat('many_many');
if(!$otherManyMany) { if(!$otherManyMany) {
Debug::message("Inverse component of $candidate not found"); user_error("Inverse component of $candidate not found ({$this->class})", E_USER_ERROR);
} }
foreach($otherManyMany as $inverseComponentName => $candidateClass) { foreach($otherManyMany as $inverseComponentName => $candidateClass) {
@ -1641,6 +1641,40 @@ class DataObject extends ViewableData implements DataObjectInterface {
} }
} }
/**
* @param Member $member
* @return boolean
*/
public function canView($member = null) {
return true;
}
/**
* @param Member $member
* @return boolean
*/
public function canEdit($member = null) {
return true;
}
/**
* @param Member $member
* @return boolean
*/
public function canDelete($member = null) {
return true;
}
/**
* @todo Should canCreate be a static method?
*
* @param Member $member
* @return boolean
*/
public function canCreate($member = null) {
return true;
}
/** /**
* Debugging used by Debug::show() * Debugging used by Debug::show()
* *

View File

@ -612,6 +612,7 @@ class SiteTree extends DataObject {
* It can be overloaded to customise the security model for an * It can be overloaded to customise the security model for an
* application. * application.
* *
* @param Member $member
* @return boolean True if the current user can delete this page. * @return boolean True if the current user can delete this page.
*/ */
public function canDelete($member = null) { public function canDelete($member = null) {
@ -637,6 +638,7 @@ class SiteTree extends DataObject {
* It can be overloaded to customise the security model for an * It can be overloaded to customise the security model for an
* application. * application.
* *
* @param Member $member
* @return boolean True if the current user can create pages on this * @return boolean True if the current user can create pages on this
* class. * class.
*/ */
@ -663,6 +665,7 @@ class SiteTree extends DataObject {
* It can be overloaded to customise the security model for an * It can be overloaded to customise the security model for an
* application. * application.
* *
* @param Member $member
* @return boolean True if the current user can edit this page. * @return boolean True if the current user can edit this page.
*/ */
public function canEdit($member = null) { public function canEdit($member = null) {
@ -693,6 +696,7 @@ class SiteTree extends DataObject {
* It can be overloaded to customise the security model for an * It can be overloaded to customise the security model for an
* application. * application.
* *
* @param Member $member
* @return boolean True if the current user can publish this page. * @return boolean True if the current user can publish this page.
*/ */
public function canPublish($member = null) { public function canPublish($member = null) {

View File

@ -0,0 +1,17 @@
<?php
/**
* @package sapphire
* @subpackage model
*/
/**
* Represents a US zip code.
*
* Can either be 5 or 9 digits.
*/
class USZipcode extends Varchar {
}
?>

View File

@ -107,10 +107,11 @@ class Debug {
* @param mixed $val * @param mixed $val
*/ */
static function dump($val) { static function dump($val) {
echo '<pre style="background-color:#ccc;padding:5px;">'; echo '<pre style="background-color:#ccc;padding:5px;font-size:14px;line-height:18px;">';
$caller = Debug::caller(); $caller = Debug::caller();
echo "<span style=\"font-size: 60%\">Line $caller[line] of " . basename($caller['file']) . "</span>\n"; echo "<span style=\"font-size: 12px;color:#666;\">Line $caller[line] of " . basename($caller['file']) . ":</span>\n";
print_r($val); if (is_string($val)) print_r(wordwrap($val, 100));
else print_r($val);
echo '</pre>'; echo '</pre>';
} }
@ -260,7 +261,7 @@ class Debug {
echo "ERROR:Error $errno: $errstr\n At l$errline in $errfile\n"; echo "ERROR:Error $errno: $errstr\n At l$errline in $errfile\n";
Debug::backtrace(); Debug::backtrace();
} else { } else {
$reporter = new DebugReporter(); $reporter = new SapphireDebugReporter();
$reporter->writeHeader(); $reporter->writeHeader();
echo '<div class="info">'; echo '<div class="info">';
echo "<h1>" . strip_tags($errstr) . "</h1>"; echo "<h1>" . strip_tags($errstr) . "</h1>";
@ -588,8 +589,9 @@ class SapphireDebugReporter implements DebugReporter {
echo 'pre { margin-left:18px; }'; echo 'pre { margin-left:18px; }';
echo 'pre span { color:#999;}'; echo 'pre span { color:#999;}';
echo 'pre .error { color:#f00; }'; echo 'pre .error { color:#f00; }';
echo '.pass { padding:2px 20px 2px 40px; color:#006600; background:#E2F9E3 url('.Director::absoluteBaseURL() .'cms/images/alert-good.gif) no-repeat scroll 7px 50%; border:1px solid #8DD38D; }'; echo '.pass { margin-top:18px; padding:2px 20px 2px 40px; color:#006600; background:#E2F9E3 url('.Director::absoluteBaseURL() .'cms/images/alert-good.gif) no-repeat scroll 7px 50%; border:1px solid #8DD38D; }';
echo '.fail { padding:2px 20px 2px 40px; color:#C80700; background:#FFE9E9 url('.Director::absoluteBaseURL() .'cms/images/alert-bad.gif) no-repeat scroll 7px 50%; }'; echo '.fail { margin-top:18px; padding:2px 20px 2px 40px; color:#C80700; background:#FFE9E9 url('.Director::absoluteBaseURL() .'cms/images/alert-bad.gif) no-repeat scroll 7px 50%; border:1px solid #C80700; }';
echo '.failure span { color:#C80700; font-weight:bold; }';
echo '</style></head>'; echo '</style></head>';
echo '<body>'; echo '<body>';
echo '<div class="header"><img src="'. Director::absoluteBaseURL() .'cms/images/mainmenu/logo.gif" width="26" height="23"></div>'; echo '<div class="header"><img src="'. Director::absoluteBaseURL() .'cms/images/mainmenu/logo.gif" width="26" height="23"></div>';

View File

@ -17,7 +17,7 @@ class DevelopmentAdmin extends Controller {
); );
function index() { function index() {
$renderer = new DebugView(); $renderer = new SapphireDebugReporter();
$renderer->writeHeader(); $renderer->writeHeader();
echo <<<HTML echo <<<HTML
<div class="info"><h1>Sapphire Development Tools</h1></div> <div class="info"><h1>Sapphire Development Tools</h1></div>

View File

@ -50,6 +50,10 @@ class TestRunner extends Controller {
if (!self::$default_reporter) self::set_reporter('SapphireDebugReporter'); if (!self::$default_reporter) self::set_reporter('SapphireDebugReporter');
} }
public function Link() {
return Controller::join_links(Director::absoluteBaseURL(), 'dev/tests/');
}
/** /**
* Run all test classes * Run all test classes
*/ */
@ -69,10 +73,19 @@ class TestRunner extends Controller {
* Browse all enabled test cases in the environment * Browse all enabled test cases in the environment
*/ */
function browse() { function browse() {
self::$default_reporter->writeHeader();
echo '<div class="info">';
echo '<h1>Available Tests</h1>';
echo '</div>';
echo '<div class="trace">';
$tests = ClassInfo::subclassesFor('SapphireTest'); $tests = ClassInfo::subclassesFor('SapphireTest');
echo "<h3><a href=\"" . $this->Link() . "all\">Run all " . count($tests) . " tests</a></h3>";
echo "<br />";
foreach ($tests as $test) { foreach ($tests as $test) {
echo "<h3><a href=\"$test\">$test</a></h3>"; echo "<h3><a href=\"" . $this->Link() . "$test\">Run $test</a></h3>";
} }
echo '</div>';
self::$default_reporter->writeFooter();
} }
function coverage() { function coverage() {
@ -109,7 +122,6 @@ class TestRunner extends Controller {
echo "<p>Running test cases: " . implode(", ", $classList) . "</p>"; echo "<p>Running test cases: " . implode(", ", $classList) . "</p>";
} else { } else {
echo "<h1>{$classList[0]}</h1>"; echo "<h1>{$classList[0]}</h1>";
echo "<p>Running test case:</p>";
} }
echo "</div>"; echo "</div>";
echo '<div class="trace">'; echo '<div class="trace">';

View File

@ -743,35 +743,6 @@ class TableField_Item extends TableListField_Item {
return $content; return $content;
} }
function Can($mode) {
return $this->parent->Can($mode);
}
function Parent() {
return $this->parent;
}
/**
* Create the base link for the call below.
*/
function BaseLink() {
$parent = $this->parent;
$action = $parent->FormAction();
if(substr($action, -1, 1) !== '&'){
$action = $action."&";
}
$action = str_replace('&', '&amp;', $action);
return $action . "action_callfieldmethod=1&amp;fieldName=". $parent->Name() . "&amp;childID=" . $this->ID;
}
/**
* Runs the delete() method on the Tablefield parent.
* Allows the deletion of objects via ajax
*/
function DeleteLink() {
return $this->BaseLink() . "&amp;methodName=delete";
}
} }
?> ?>

View File

@ -166,8 +166,6 @@ class SearchContext extends Object {
$searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields')); $searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields'));
$query = $this->getQuery($searchParams, $sort, $limit); $query = $this->getQuery($searchParams, $sort, $limit);
//Debug::dump($query->sql());
// use if a raw SQL query is needed // use if a raw SQL query is needed
$results = new DataObjectSet(); $results = new DataObjectSet();
foreach($query->execute() as $row) { foreach($query->execute() as $row) {
@ -198,7 +196,7 @@ class SearchContext extends Object {
* @param SQLQuery $query * @param SQLQuery $query
*/ */
protected function processFilters(SQLQuery $query, $searchParams) { protected function processFilters(SQLQuery $query, $searchParams) {
$conditions = array(); /*$conditions = array();
foreach($this->filters as $field => $filter) { foreach($this->filters as $field => $filter) {
if (strstr($field, '.')) { if (strstr($field, '.')) {
$path = explode('.', $field); $path = explode('.', $field);
@ -207,7 +205,7 @@ class SearchContext extends Object {
} }
} }
$query->where = $conditions; $query->where = $conditions;
return $query; return $query;*/
} }
/** /**
@ -255,31 +253,5 @@ class SearchContext extends Object {
$this->fields = $fields; $this->fields = $fields;
} }
/**
* Placeholder, until I figure out the rest of the SQLQuery stuff
* and link the $searchable_fields array to the SearchContext
*
* @deprecated in favor of getResults
*/
public function getResultSet($fields) {
$filter = "";
$current = 1;
$fields = array_filter($fields, array($this,'clearEmptySearchFields'));
$length = count($fields);
foreach($fields as $key=>$val) {
// Array values come from more complex fields - for now let's just disable searching on them
if (!is_array($val) && $val != '') {
$filter .= "`$key`='$val'";
} else {
$length--;
}
if ($current < $length) {
$filter .= " AND ";
}
$current++;
}
return DataObject::get($this->modelClass, $filter);
}
} }
?> ?>

View File

@ -1,17 +1,20 @@
<?php <?php
/**
* @package sapphire
* @subpackage search
*/
/** /**
* Checks if a value is in a given set. * Checks if a value is in a given set.
* SQL syntax used: Column IN ('val1','val2') * SQL syntax used: Column IN ('val1','val2')
* *
* @todo Add negation (NOT IN)6 * @todo Add negation (NOT IN)6
*
* @author Silverstripe Ltd., Ingo Schommer (<firstname>@silverstripe.com)
*/ */
class CollectionFilter extends SearchFilter { class CollectionFilter extends SearchFilter {
public function apply(SQLQuery $query) { public function apply(SQLQuery $query) {
$query = $this->applyRelation($query); $query = $this->applyRelation($query);
$values = explode(',',$this->value); $values = explode(',',$this->getValue());
if(!$values) return false; if(!$values) return false;
for($i=0; $i<count($values); $i++) { for($i=0; $i<count($values); $i++) {

View File

@ -0,0 +1,32 @@
<?php
/**
* @package sapphire
* @subpackage search
*/
/**
* Matches textual content with a substring match on a text fragment leading
* to the end of the string.
*
* <code>
* "abcdefg" => "defg" # true
* "abcdefg" => "abcd" # false
* </code>
*
* @package sapphire
* @subpackage search
*/
class EndsWithFilter extends SearchFilter {
/**
* Applies a match on the trailing characters of a field value.
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
$query->where($this->getName(), "RLIKE", "{$this->getValue()}$");
}
}
?>

View File

@ -1,6 +1,11 @@
<?php <?php
/** /**
* Matches textual content with a columnname = 'keyword' construct * @package sapphire
* @subpackage search
*/
/**
* Selects textual content with an exact match between columnname and keyword.
* *
* @todo case sensitivity switch * @todo case sensitivity switch
* @todo documentation * @todo documentation
@ -17,7 +22,7 @@ class ExactMatchFilter extends SearchFilter {
*/ */
public function apply(SQLQuery $query) { public function apply(SQLQuery $query) {
$query = $this->applyRelation($query); $query = $this->applyRelation($query);
return $query->where("{$this->getName()} = '{$this->value}'"); return $query->where("{$this->getName()} = '{$this->getValue()}'");
} }
} }

View File

@ -1,4 +1,9 @@
<?php <?php
/**
* @package sapphire
* @subpackage search
*/
/** /**
* Filters by full-text matching on the given field. * Filters by full-text matching on the given field.
* *
@ -23,7 +28,8 @@
class FulltextFilter extends SearchFilter { class FulltextFilter extends SearchFilter {
public function apply(SQLQuery $query) { public function apply(SQLQuery $query) {
return ""; $query->where("MATCH ({$this->getName()} AGAINST ('{$this->getValue()}')");
return $query;
} }
} }

View File

@ -13,7 +13,7 @@
class NegationFilter extends SearchFilter { class NegationFilter extends SearchFilter {
public function apply(SQLQuery $query) { public function apply(SQLQuery $query) {
return $query->where("{$this->name} != '{$this->value}'"); return $query->where("{$this->name} != '{$this->getValue()}'");
} }
} }

View File

@ -1,4 +1,9 @@
<?php <?php
/**
* @package sapphire
* @subpackage search
*/
/** /**
* Matches textual content with a LIKE '%keyword%' construct. * Matches textual content with a LIKE '%keyword%' construct.
* *
@ -9,7 +14,7 @@ class PartialMatchFilter extends SearchFilter {
public function apply(SQLQuery $query) { public function apply(SQLQuery $query) {
$query = $this->applyRelation($query); $query = $this->applyRelation($query);
return $query->where("{$this->getName()} LIKE '%{$this->value}%'"); return $query->where("{$this->getName()} LIKE '%{$this->getValue()}%'");
} }
} }

View File

@ -1,4 +1,9 @@
<?php <?php
/**
* @package sapphire
* @subpackage search
*/
/** /**
* @todo documentation * @todo documentation
* *
@ -17,12 +22,48 @@ abstract class SearchFilter extends Object {
$this->value = $value; $this->value = $value;
} }
/**
* Called by constructor to convert a string pathname into
* a well defined relationship sequence.
*
* @param unknown_type $name
*/
protected function addRelation($name) {
if (strstr($name, '.')) {
$parts = explode('.', $name);
$this->name = array_pop($parts);
$this->relation = $parts;
} else {
$this->name = $name;
}
}
/**
* Set the root model class to be selected by this
* search query.
*
* @param string $className
*/
public function setModel($className) {
$this->model = $className;
}
/**
* Set the current value to be filtered on.
*
* @param string $value
*/
public function setValue($value) { public function setValue($value) {
$this->value = $value; $this->value = $value;
} }
public function setModel($className) { /**
$this->model = $className; * Accessor for the current value to be filtered on.
*
* @return string
*/
public function getValue() {
return $this->value;
} }
/** /**
@ -42,16 +83,6 @@ abstract class SearchFilter extends Object {
return $candidateClass . "." . $this->name; return $candidateClass . "." . $this->name;
} }
protected function addRelation($name) {
if (strstr($name, '.')) {
$parts = explode('.', $name);
$this->name = array_pop($parts);
$this->relation = $parts;
} else {
$this->name = $name;
}
}
/** /**
* Applies multiple-table inheritance to straight joins on the data objects * Applies multiple-table inheritance to straight joins on the data objects
* *
@ -66,11 +97,10 @@ abstract class SearchFilter extends Object {
/** /**
* Traverse the relationship fields, and add the table * Traverse the relationship fields, and add the table
* mappings to the query object state. * mappings to the query object state.
*
* @todo move join specific crap into SQLQuery
* *
* @param unknown_type $query * @todo try to make this implicitly triggered so it doesn't have to be manually called in child filters
* @return unknown * @param SQLQuery $query
* @return SQLQuery
*/ */
protected function applyRelation($query) { protected function applyRelation($query) {
if (is_array($this->relation)) { if (is_array($this->relation)) {
@ -84,6 +114,8 @@ abstract class SearchFilter extends Object {
$model = singleton($component); $model = singleton($component);
$this->applyJoin($query, $model, $component); $this->applyJoin($query, $model, $component);
$this->model = $component; $this->model = $component;
} elseif ($component = $model->many_many($rel)) {
Debug::dump("Many-Many traversals not implemented");
} }
} }
} }
@ -94,6 +126,7 @@ abstract class SearchFilter extends Object {
* Apply filter criteria to a SQL query. * Apply filter criteria to a SQL query.
* *
* @param SQLQuery $query * @param SQLQuery $query
* @return SQLQuery
*/ */
abstract public function apply(SQLQuery $query); abstract public function apply(SQLQuery $query);

View File

@ -0,0 +1,32 @@
<?php
/**
* @package sapphire
* @subpackage search
*/
/**
* Matches textual content with a substring match from the beginning
* of the string.
*
* <code>
* "abcdefg" => "defg" # false
* "abcdefg" => "abcd" # true
* </code>
*
* @package sapphire
* @subpackage search
*/
class StartsWithFilter extends SearchFilter {
/**
* Applies a substring match on a field value.
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
$query->where("LOCATE('{$this->getValue()}', {$this->getName()}) = 1");
}
}
?>

View File

@ -1,4 +1,9 @@
<?php <?php
/**
* @package sapphire
* @subpackage search
*/
/** /**
* Uses a substring match against content in column rows. * Uses a substring match against content in column rows.
* *
@ -8,7 +13,7 @@
class SubstringFilter extends SearchFilter { class SubstringFilter extends SearchFilter {
public function apply(SQLQuery $query) { public function apply(SQLQuery $query) {
return $query->where("LOCATE({$this->name}, $value)"); return $query->where("LOCATE('{$this->getValue()}', {$this->getName()}) != 0");
} }
} }

View File

@ -0,0 +1,34 @@
<?php
/**
* @package sapphire
* @subpackage search
*/
/**
* Incomplete.
*
* @todo add to tests
*
* @package sapphire
* @subpackage search
*/
class WithinRangeFilter extends SearchFilter {
private $min;
private $max;
function setMin($min) {
$this->min = $min;
}
function setMax($max) {
$this->max = $max;
}
function apply(SQLQuery $query) {
$query->where("{$this->getName()} >= {$this->min} AND {$this->getName()} <= {$this->max}");
}
}
?>

View File

@ -101,6 +101,8 @@ class SearchContextTest extends SapphireTest {
"PartialMatch" => "partially", "PartialMatch" => "partially",
"Negation" => "undisclosed", "Negation" => "undisclosed",
"CollectionMatch" => "ExistingCollectionValue,NonExistingCollectionValue,4,Inline'Quotes'", "CollectionMatch" => "ExistingCollectionValue,NonExistingCollectionValue,4,Inline'Quotes'",
"StartsWith" => "12345",
"EndsWith" => "ijkl"
); );
$results = $context->getResults($params); $results = $context->getResults($params);
@ -209,8 +211,10 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly {
"PartialMatch" => "Text", "PartialMatch" => "Text",
"Negation" => "Text", "Negation" => "Text",
"SubstringMatch" => "Text", "SubstringMatch" => "Text",
"HiddenValue" => "Text",
"CollectionMatch" => "Text", "CollectionMatch" => "Text",
"StartsWith" => "Text",
"EndsWith" => "Text",
"HiddenValue" => "Text"
); );
static $searchable_fields = array( static $searchable_fields = array(
@ -219,6 +223,8 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly {
"Negation" => "NegationFilter", "Negation" => "NegationFilter",
"SubstringMatch" => "SubstringFilter", "SubstringMatch" => "SubstringFilter",
"CollectionMatch" => "CollectionFilter", "CollectionMatch" => "CollectionFilter",
"StartsWith" => "StartsWithFilter",
"EndsWith" => "EndsWithFilter"
); );
} }

View File

@ -62,4 +62,6 @@ SearchContextTest_AllFilterTypes:
PartialMatch: Match me partially PartialMatch: Match me partially
Negation: Shouldnt match me Negation: Shouldnt match me
HiddenValue: Filtered value HiddenValue: Filtered value
CollectionMatch: ExistingCollectionValue CollectionMatch: ExistingCollectionValue
StartsWith: 12345-6789
EndsWith: abcd-efgh-ijkl