\Search\Model\BehaviorSearchableBehavior

This behavior allows entities to be searchable through an auto-generated list of words.

Using this Behavior

You must indicate which fields can be indexed when attaching this behavior to your tables. For example, when attaching this behavior to Users table:

$this->addBehavior('Search.Searchable', [
    'fields' => ['username', 'email']
]);

In the example above, this behavior will look for words to index in user's "username" and user's "email" properties.

If you need a really special selection of words for each entity is being indexed, then you can set the fields option as a callable which should return a list of words for the given entity. For example:

$this->addBehavior('Search.Searchable', [
    'fields' => function ($user) {
        return "{$user->name} {$user->email}";
    }
]);

You can return either, a plain text of space-separated words, or an array list of words:

$this->addBehavior('Search.Searchable', [
    'fields' => function ($user) {
        return [
            'word 1',
            'word 2',
            'word 3',
        ];
    }
]);

This behaviors will apply a series of filters (converts to lowercase, remove line breaks, etc) to the resulting word list, so you should simply return a RAW string of words and let this behavior do the rest of the job.

Banned Words

You can use the bannedWords option to tell which words should not be indexed by this behavior. For example:

$this->addBehavior('Search.Searchable', [
    'bannedWords' => ['of', 'the', 'and']
]);

If you need to ban a really specific list of words you can set bannedWords option as a callable method that should return true or false to tell if a words should be indexed or not. For example:

$this->addBehavior('Search.Searchable', [
    'bannedWords' => function ($word) {
        return strlen($word) > 3;
    }
]);
  • Returning TRUE indicates that the word is safe for indexing (not banned).
  • Returning FALSE indicates that the word should NOT be indexed (banned).

In the example, above any word of 4 or more characters will be indexed (e.g. "home", "name", "quickapps", etc). Any word of 3 or less characters will be banned (e.g. "and", "or", "the").

Searching Entities

When attaching this behavior, every entity under your table gets a list of indexed words. The idea is you can use this list of words to locate any entity based on a customized search-criteria. A search-criteria looks as follow:

"this phrase" OR -"not this one" AND this

Use wildcard searches to broaden results; asterisk (*) matches any one or more characters, exclamation mark (!) matches any single character:

"this *rase" OR -"not th!! one" AND thi!

Anything containing space (" ") characters must be wrapper between quotation marks:

"this phrase" special_operator:"[100 to 500]" -word -"more words" -word_1 word_2

The search criteria above will be treated as it were composed by the following parts:

  • this phrase
  • special_operator:[100 to 500]
  • -word
  • -more words
  • -word_1
  • word_2

Search criteria allows you to perform complex search conditions in a human-readable way. Allows you, for example, create user-friendly search-forms, or create some RSS feed just by creating a friendly URL using a search-criteria. e.g.: http://example.com/rss/category:art date:>2014-01-01

You must use the search() method to scope any query using a search-criteria. For example, in one controller using Users model:

$criteria = '"this phrase" OR -"not this one" AND this';
$query = $this->Users->find();
$query = $this->Users->search($criteria, $query);

The above will alter the given $query object according to the given criteria. The second argument (query object) is optional, if not provided this Behavior automatically generates a find-query for you. Previous example and the one below are equivalent:

$criteria = '"this phrase" OR -"not this one" AND this';
$query = $this->Users->search($criteria);

Creating Operators

An Operator is a search-criteria command which allows you to perform very specific filter conditions over your queries. An operator has two parts, a name and its arguments, both parts must be separated using the : symbol e.g.:

// operator name is: "author"
// operator arguments are: ">2014-03-01"
date:>2014-03-01

NOTE: Operators names are treated as lowercase_and_underscored, so AuthorName, AUTHOR_NAME or AuThoR_naMe are all treated as: author_name.

You can define custom operators for your table by using the addSearchOperator() method. For example, you might need create a custom operator author which allows you to search a Content entity by author name. A search-criteria using this operator may looks as follow:

// get all contents containing `this phrase` and created by `JohnLocke`
"this phrase" author:JohnLocke

You can define in your table an operator method and register it into this behavior under the author name, a full working example may look as follow:

class MyTable extends Table
{
    public function initialize(array $config)
    {
        // attach the behavior
        $this->addBehavior('Search.Searchable');

        // register a new operator for handling `author:<author_name>` expressions
        $this->addSearchOperator('author', 'operatorAuthor');
    }

    public function operatorAuthor(Query $query, Token $token)
    {
        // $query: The query object to alter
        // $token: Token representing the operator to apply.
        // Scope query using $token information and return.
        return $query;
    }
}

You can also define operator as a callable function:

class MyTable extends Table
{
    public function initialize(array $config)
    {
        $this->addBehavior('Search.Searchable');

        $this->addSearchOperator('author', function(Query $query, Token $token) {
            // Scope query and return.
            return $query;
        });
    }
}

Creating Reusable Operators

If your application has operators that are commonly reused, it is helpful to package those operators into re-usable classes:

// in MyPlugin/Model/Search/CustomOperator.php
namespace MyPlugin\Model\Search;

use Search\Operator;

class CustomOperator extends Operator
{
    public function scope($query, $token)
    {
        // Scope $query
        return $query;
    }
}

// In any table class:

// Add the custom operator,
$this->addSearchOperator('operator_name', 'MyPlugin.Custom', ['opt1' => 'val1', ...]);

// OR passing a constructed operator
use MyPlugin\Model\Search\CustomOperator;
$this->addSearchOperator('operator_name', new CustomOperator($this, ['opt1' => 'val1', ...]));

Fallback Operators

When an operator is detected in the given search criteria but no operator callable was defined using addSearchOperator(), then SearchableBehavior.operator<OperatorName> will be fired, so other plugins may respond to any undefined operator. For example, given the search criteria below, lets suppose date operator was not defined early:

"this phrase" author:JohnLocke date:[2013-06-06..2014-06-06]

The SearchableBehavior.operatorDate event will be fired. A plugin may respond to this call by implementing this event:

// ...

public function implementedEvents() {
    return [
        'SearchableBehavior.operatorDate' => 'operatorDate',
    ];
}

// ...

public function operatorDate($event, $query, $token) {
    // Scope $query object and return it
    return $query;
}

// ...

IMPORTANT:

  • Event handler method should always return the modified $query object.
  • The event's context, that is $event->subject(), is the table instance that fired the event.

Summary

Methods
Properties
Constants
__construct()
afterSave()
beforeDelete()
search()
addSearchOperator()
enableSearchOperator()
disableSearchOperator()
trigger()
eventDispatcher()
triggered()
No public properties found
No constants found
_scopeOperator()
_scopeWords()
_extractEntityWords()
_filterText()
_operatorCallable()
_getTokens()
$_table
$_defaultConfig
N/A
No private methods found
No private properties found
N/A

Properties

$_table

$_table : \Cake\ORM\Table

The table this behavior is attached to.

Type

\Cake\ORM\Table

$_defaultConfig

$_defaultConfig : array

Behavior configuration array.

  • operators: A list of registered operators methods as name => methodName.

  • fields: List of entity fields where to look for words. Or a callable method, it receives and entity as first argument, and it must return a list of words for that entity (as an array list, or a string space-separated words).

  • bannedWords: List of banned words.

  • on: Indicates when to extract words, update when entity is being updated, insert when a new entity is inserted into table. Or both (by default).

Type

array

Methods

__construct()

__construct(\Cake\ORM\Table $table, array $config)

Constructor

Parameters

\Cake\ORM\Table $table

The table this behavior is attached to.

array $config

The config for this behavior.

afterSave()

afterSave(\Cake\Event\Event $event, \Cake\ORM\Entity $entity) : void

Generates a list of words after each entity is saved.

Parameters

\Cake\Event\Event $event

The event that was triggered

\Cake\ORM\Entity $entity

The entity that was saved

beforeDelete()

beforeDelete(\Cake\Event\Event $event, \Cake\ORM\Entity $entity) : boolean

Prepares entity to delete its words-index.

Parameters

\Cake\Event\Event $event

The event that was triggered

\Cake\ORM\Entity $entity

The entity that was removed

Returns

boolean

search()

search(string $criteria, \Cake\ORM\Query|null $query) : \Cake\ORM\Query

Scopes the given query object.

It looks for search-criteria and applies them over the query object. For example, given the criteria below:

"this phrase" -"and not this one"

Alters the query object as follow:

$query->where([
   'indexed_words LIKE' => '%this phrase%',
   'indexed_words NOT LIKE' => '%and not this one%'
]);

The AND & OR keywords are allowed to create complex conditions. For example:

"this phrase" OR -"and not this one" AND "this"

Will produce something like:

$query->where(['indexed_words LIKE' => '%this phrase%'])
    ->orWhere(['indexed_words NOT LIKE' => '%and not this one%']);
    ->andWhere(['indexed_words LIKE' => '%this%']);

Parameters

string $criteria

A search-criteria. e.g. "this phrase" author:username

\Cake\ORM\Query|null $query

The query to scope, or null to create one

Throws

\Search\Model\Behavior\Cake\Error\FatalErrorException

When query gets corrupted while processing tokens

Returns

\Cake\ORM\Query —

Scoped query

addSearchOperator()

addSearchOperator(string $name, mixed $handler, array $options) : void

Registers a new operator method.

Allowed formats are:

$this->addSearchOperator('created', 'operatorCreated');

The above will use Table's operatorCreated() method to handle the "created" operator.


$this->addSearchOperator('created', 'MyPlugin.Limit');

The above will use MyPlugin\Model\Search\LimitOperator class to handle the "limit" operator. Note the Operator suffix.


$this->addSearchOperator('created', 'MyPlugin.Limit', ['my_option' => 'option_value']);

Similar as before, but in this case you can provide some configuration options passing an array as above.


$this->addSearchOperator('created', 'Full\ClassName');

Or you can indicate a full class name to use.


$this->addSearchOperator('created', function ($query, $token) {
    // scope $query
    return $query;
});

You can simply pass a callable function to handle the operator, this callable must return the altered $query object.


$this->addSearchOperator('created', new CreatedOperator($table, $options));

In this case you can directly pass an instance of an operator handler, this object should extends the Search\Operator abstract class.

Parameters

string $name

Underscored operator's name. e.g. author

mixed $handler

A valid handler as described above

array $options

enableSearchOperator()

enableSearchOperator(string $name) : void

Enables a an operator.

Parameters

string $name

Name of the operator to be enabled

disableSearchOperator()

disableSearchOperator(string $name) : void

Disables an operator.

Parameters

string $name

Name of the operator to be disabled

trigger()

trigger(array|string $eventName) : \Cake\Event\Event

Triggers the given event name. This method provides a shortcut for:

$this->trigger('EventName', $arg1, $arg2, ..., $argn);

You can provide a subject to use by passing an array as first arguments where the first element is the event name and the second one is the subject, if no subject is given $this will be used by default:

$this->trigger(['GetTime', new MySubject()], $arg_0, $arg_1, ..., $arg_n);

You can also indicate an EventDispatcher instance to use by prefixing the event name with <InstanceName>::, for instance:

$this->trigger('Blog::EventName', $arg1, $arg2, ..., $argn);

This will use the EventDispacher instance named Blog and will trigger the event EventName within that instance.

Parameters

array|string $eventName

The event name to trigger

Returns

\Cake\Event\Event —

The event object that was fired

eventDispatcher()

eventDispatcher(string $name) : \QuickApps\Event\EventDispatcher

Gets an instance of the given Event Dispatcher name.

Usage:

$this->eventDispatcher('myDispatcher')
    ->trigger('MyEventName', $argument)
    ->result;

Parameters

string $name

Name of the dispatcher to get, defaults to 'default'

Returns

\QuickApps\Event\EventDispatcher

triggered()

triggered(string|null $eventName) : integer|array

Retrieves the number of times an event was triggered, or the complete list of events that were triggered.

Parameters

string|null $eventName

The name of the event, if null returns the entire list of event that were fired

Returns

integer|array

_scopeOperator()

_scopeOperator(\Cake\ORM\Query $query, \Search\Token $token) : \Cake\ORM\Query

Scopes the given query using the given operator token.

Parameters

\Cake\ORM\Query $query

The query to scope

\Search\Token $token

Token describing an operator. e.g -op_name:op_value

Returns

\Cake\ORM\Query —

Scoped query

_scopeWords()

_scopeWords(\Cake\ORM\Query $query, \Search\Token $token) : \Cake\ORM\Query

Scopes the given query using the given words token.

Parameters

\Cake\ORM\Query $query

The query to scope

\Search\Token $token

Token describing a words sequence. e.g this is a phrase

Returns

\Cake\ORM\Query —

Scoped query

_extractEntityWords()

_extractEntityWords(\Cake\Datasource\EntityInterface $entity) : string

Extracts a list of words to by indexed for given entity.

NOTE: Words can be repeated, this allows to search phrases.

Parameters

\Cake\Datasource\EntityInterface $entity

The entity for which generate the list of words

Returns

string —

Space-separated list of words. e.g. cat dog this that

_filterText()

_filterText(string $text) : string

Removes any invalid word from the given text.

Parameters

string $text

The text to filter

Returns

string —

Filtered text

_operatorCallable()

_operatorCallable(string $name) : boolean|callable

Gets the callable method for a given operator method.

Parameters

string $name

Name of the method to get

Returns

boolean|callable —

False if no callback was found for the given operator name. Or the callable if found.

_getTokens()

_getTokens(string $criteria) : array

Extract tokens from search-criteria.

Parameters

string $criteria

A search-criteria

Returns

array —

List of extracted tokens