* $model = User::model()->find(); * $behavior = new ASolrSearchable(); * $behavior->attributes = array( * "name", "skills", "country.name" * ); * $model->attachBehavior("ASolrSearchable", $behavior); * $model->index(); // adds the user to the solr index * * $model->name = "Test User"; * $model->save(); // document is automatically reindexed * $model->address = "123 Fake Street"; * $model->save(); // document is not reindexed because we don't care about the address field * * $criteria = new ASolrCriteria; * $criteria->query = "name:'Test User'"; * $users = $model->findAllBySolr($criteria); // all the users with the name "Test User" * * $model->delete(); // document is automatically deleted from solr after the model is deleted * * @package packages.solr * @author Charles Pick / PeoplePerHour.com */ class ASolrSearchable extends CActiveRecordBehavior { /** * The class name of the solr document to instantiate * * @var string */ public $documentClass = "ASolrDocument"; /** * Whether to automatically index or reindex the document when it changes. * Defaults to true. * You can also pass a method that will be evaluated in order to decide if * the current model should be indexed or not * eg: autoIndex => function() { return true; }, * while in the definition of attributes in the models behaviors() method * * @var boolean | callable */ public $autoIndex = true; /** * Whether to be smart about when to reindex documents. * If this is true, changes will be pushed to solr only if attributes that * we care about have changed. * * @var boolean */ public $smartIndex = true; /** * The configuration for the associated ASolrDocument class * * @var array */ public $solrDocumentConfig = array (); /** * The solr document associated with this model instance * * @var ASolrDocument */ protected $_solrDocument; /** * The solr criteria associated with this model instance * * @var ASolrCriteria */ protected $_solrCriteria; /** * The attributes that should be indexed in solr * * @var array */ protected $_attributes; /** * Stores the attributes of the model after it is found. * Used to determine whether any of the attributes we care about have changed or not * * @var array */ protected $_oldAttributes = array (); /** * Sets the attributes that should be indexed in solr * * @param array $attributes */ public function setAttributes($attributes) { $a = array (); foreach ( $attributes as $key => $value ) { if (is_integer ( $key )) { $key = $value; } $a [$key] = $value; } $this->_attributes = $a; } /** * Gets the attributes that should be indexed in solr * * @return array */ public function getAttributes() { if ($this->_attributes === null) { $names = $this->getOwner ()->attributeNames (); $this->_attributes = array_combine ( $names, $names ); } return $this->_attributes; } /** * Gets a list of objects and attributes that * * @return array a multidimensional array of objects and attributes */ protected function resolveAttributes() { $names = array (); foreach ( $this->getAttributes () as $modelAttribute => $docAttribute ) { if (! strstr ( $modelAttribute, "." )) { $names [$modelAttribute] = array ( $this->getOwner (), $modelAttribute ); continue; } $reference = $this->getOwner (); /* @var CActiveRecord $reference */ $pointers = explode ( ".", $modelAttribute ); $lastItem = array_pop ( $pointers ); foreach ( $pointers as $pointer ) { $reference = $reference->{$pointer}; } $names [$modelAttribute] = array ( $reference, $lastItem ); } return $names; } /** * Resolves the attribute name to the field name on solr. * Default implementation replaces "." with "__" (double underscore) *
* echo $behavior->resolveAttributeName("name"); // "name"
* echo $behavior->resolveAttributeName("country.name"); // "country__name"
*
*
* @param
* $attributeName
* @return mixed
*/
protected function resolveAttributeName($attributeName) {
$attributes = $this->getAttributes ();
$attributeName = $attributes [$attributeName];
return str_replace ( ".", "__", $attributeName );
}
/**
* Resolves the value of an attribute on an owner object.
*
* @param mixed $owner
* the object or array of objects that the attribute belongs to
* @param string $attribute
* the name of the attribute to get the value for
* @return mixed the attribute value
*/
protected function resolveAttributeValue($owner, $attribute) {
if (is_array ( $owner )) {
$value = array ();
foreach ( $owner as $item )
if (is_array ( $item ) && isset ( $item [$attribute] ))
$value [] = $item [$attribute];
else if (is_object ( $item ) && isset ( $item->{$attribute} ))
$value [] = $item->{$attribute};
return $value;
}
return isset ( $owner->{$attribute} ) ? $owner->{$attribute} : null;
}
/**
* Sets the solr document associated with this model instance
*
* @param ASolrDocument $solrDocument
* the solr document
*/
public function setSolrDocument($solrDocument) {
$this->_solrDocument = $solrDocument;
}
/**
* Gets the solr document associated with this model instance.
*
* @param boolean $refresh
* whether to refresh the document, defaults to false
* @return ASolrDocument the solr document
*/
public function getSolrDocument($refresh = false) {
if ($this->_solrDocument === null || $refresh) {
$config = $this->solrDocumentConfig;
$config ['class'] = $this->documentClass;
$this->_solrDocument = Yii::createComponent ( $config );
foreach ( $this->resolveAttributes () as $attribute => $item ) {
list ( $object, $property ) = $item;
$resolvedAttributeName = $this->resolveAttributeName ( $attribute );
$this->_solrDocument->{$resolvedAttributeName} = $this->resolveAttributeValue ( $object, $property );
}
}
return $this->_solrDocument;
}
/**
* Adds the solr document to the index
*
* @return boolean true if the document was indexed successfully
*/
public function index() {
if (! $this->isIndexable ())
return true;
$document = $this->getSolrDocument ( true );
if (! $document->save ()) {
return false;
}
if ($this->smartIndex) {
$this->_oldAttributes = array ();
foreach ( $this->resolveAttributes () as $key => $item ) {
list ( $object, $property ) = $item;
$this->_oldAttributes [$key] = $this->resolveAttributeValue ( $object, $property );
}
}
return true;
}
/**
* Triggered after the attached model is found.
* Stores the current state of attributes we care about to see if they have changed.
*
* @param CEvent $event
* the event raised
*/
public function afterFind($event) {
if ($this->smartIndex) {
$this->_oldAttributes = array ();
foreach ( $this->resolveAttributes () as $key => $item ) {
list ( $object, $property ) = $item;
$this->_oldAttributes [$key] = $this->resolveAttributeValue ( $object, $property );
}
}
}
/**
* Deletes the relevant document from the solr index after the model is deleted
*
* @param CEvent $event
* the event raised
*/
public function afterDelete($event) {
$this->getSolrDocument ()->delete ();
}
/**
* Adds the relevant document to the solr index after the model is saved if $this->autoIndex is true.
* For existing records, the document will only be re-indexed if attributes we care about have changed.
*
* @param CEvent $event
* the event raised
*/
public function afterSave($event) {
if (! $this->isIndexable () || ! $this->getIsModified ()) {
return;
}
$this->index ();
}
/**
* Finds an active record that matches the given criteria using solr
*
* @param ASolrCriteria $criteria
* the solr criteria to use for searching
* @return CActiveRecord|null the found record, or null if nothing was found
*/
public function findBySolr($criteria = null) {
$c = new ASolrCriteria ();
$c->mergeWith ( $this->getSolrCriteria () );
if ($criteria !== null) {
$c->mergeWith ( $criteria );
}
if ($c->getQuery () == "") {
$c->setQuery ( "*:*" );
}
$document = $this->getSolrDocument ()->find ( $c );
if (! is_object ( $document )) {
return null;
}
return $this->populateFromSolr ( $document, false );
}
/**
* Finds all active records that matches the given criteria using solr
*
* @param ASolrCriteria $criteria
* the solr criteria to use for searching
* @return CActiveRecord[] an array of results
*/
public function findAllBySolr($criteria = null) {
$c = new ASolrCriteria ();
$c->mergeWith ( $this->getSolrCriteria () );
if ($criteria !== null) {
$c->mergeWith ( $criteria );
}
if ($c->getQuery () == "") {
$c->setQuery ( "*:*" );
}
return $this->populateFromSolr ( $this->getSolrDocument ()->findAll ( $c ), true );
}
/**
* Populates active record objects from solr
*
* @param ASolrDocument|array $document
* the document(s) to populate the records from
* @param boolean $all
* whether to populate a list of records instead of just one, defaults to false
* @return CActiveRecord|array the active record(s) populated from solr
*/
public function populateFromSolr($document, $all = false) {
if ($all) {
$results = array ();
foreach ( $document as $doc ) {
$results [] = $this->populateFromSolr ( $doc, false );
}
return $results;
}
$relations = $this->getOwner ()->getMetaData ()->relations;
$attributes = array ();
$relationAttributes = array ();
foreach ( $this->getAttributes () as $modelAttribute => $docAttribute ) {
$resolved = $this->resolveAttributeName ( $modelAttribute );
if (! strstr ( $modelAttribute, "." )) {
$attributes [$modelAttribute] = $document->{$resolved};
continue;
}
$reference = &$relationAttributes;
$pointers = explode ( ".", $modelAttribute );
$last = array_pop ( $pointers );
foreach ( $pointers as $pointer ) {
if (! isset ( $reference [$pointer] )) {
$reference [$pointer] = array ();
}
$reference = & $reference [$pointer];
}
$reference [$last] = $document->{$resolved};
}
$modelClass = get_class ( $this->getOwner () );
$model = $modelClass::model ()->populateRecord ( $attributes );
if (count ( $relationAttributes )) {
foreach ( $relationAttributes as $relationName => $attributes ) {
$relationClass = $relations [$relationName]->className;
$model->{$relationName} = $relationClass::model ()->populateRecord ( $attributes );
}
}
return $model;
}
/**
* Determines whether any attributes that we care about on the model have been modified or not.
*
* @return boolean true if the item has been modified, otherwise false
*/
public function getIsModified() {
if (! $this->smartIndex || count ( $this->_oldAttributes ) == 0) {
return true;
}
foreach ( $this->resolveAttributes () as $key => $item ) {
if (! isset ( $this->_oldAttributes [$key] )) {
return true;
}
list ( $object, $property ) = $item;
if ($this->_oldAttributes [$key] != $this->resolveAttributeValue ( $object, $property )) {
return true;
}
}
return false;
}
/**
* Resets the scope
*
* @return ASolrSearchable $this with the scope reset
*/
public function resetScope() {
$this->_solrCriteria = null;
return $this;
}
/**
* Sets the solr criteria associated with this model
*
* @param ASolrCriteria $solrCriteria
* the solr criteria
*/
public function setSolrCriteria($solrCriteria) {
$this->_solrCriteria = $solrCriteria;
}
/**
* Gets the solr criteria associated with this model
*
* @return ASolrCriteria the solr criteria
*/
public function getSolrCriteria() {
if ($this->_solrCriteria === null) {
$this->_solrCriteria = new ASolrCriteria ();
}
return $this->_solrCriteria;
}
/**
* Checks whether the current model should be indexed or not.
*
* @return bool
*/
protected function isIndexable() {
return is_callable ( $this->autoIndex ) ? call_user_func ( $this->autoIndex ) : $this->autoIndex;
}
}