      1: <?php
      2:
      3: /**
      4:  *
      5:  * Uses the TagObject business oject to store searchable tags in the main
      6:  * application database.
      7:  *
      8:  * @package alpha::util::search
      9:  * @since 1.2.3
     10:  * @author John Collins <dev@alphaframework.org>
     11:  * @version $Id: SearchProviderTags.inc 1815 2014-08-27 21:03:08Z alphadevx $
     12:  * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
     13:  * @copyright Copyright (c) 2014, John Collins (founder of Alpha Framework).
     14:  * All rights reserved.
     15:  *
     16:  * <pre>
     17:  * Redistribution and use in source and binary forms, with or
     18:  * without modification, are permitted provided that the
     19:  * following conditions are met:
     20:  *
     21:  * * Redistributions of source code must retain the above
     22:  *   copyright notice, this list of conditions and the
     23:  *   following disclaimer.
     24:  * * Redistributions in binary form must reproduce the above
     25:  *   copyright notice, this list of conditions and the
     26:  *   following disclaimer in the documentation and/or other
     27:  *   materials provided with the distribution.
     28:  * * Neither the name of the Alpha Framework nor the names
     29:  *   of its contributors may be used to endorse or promote
     30:  *   products derived from this software without specific
     31:  *   prior written permission.
     32:  *
     33:  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
     34:  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
     35:  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
     36:  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     37:  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
     38:  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     39:  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
     40:  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     41:  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     42:  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     43:  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
     44:  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     45:  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     46:  * </pre>
     47:  *
     48:  */
     49: class SearchProviderTags implements SearchProviderInterface {
     50:
     51:     /**
     52:      * Trace logger
     53:      *
     54:      * @var Logger
     55:      * @since 1.2.3
     56:      */
     57:     private static $logger;
     58:
     59:     /**
     60:      * The number of matches found for the current search.
     61:      *
     62:      * @var integer
     63:      * @since 1.2.3
     64:      */
     65:     private $numberFound = 0;
     66:
     67:     /**
     68:      * constructor to set up the object
     69:      *
     70:      * @since 1.2.3
     71:      */
     72:     public function __construct() {
     73:         self::$logger = new Logger('SearchProviderTags');
     74:     }
     75:
     76:     /**
     77:      * {@inheritdoc}
     78:      */
     79:     public function search($query, $returnType = 'all', $start = 0, $limit = 10) {
     80:         global $config;
     81:
     82:         // explode the user's query into a set of tokenized transient TagObjects
     83:         $queryTags = TagObject::tokenize($query, '', '', false);
     84:         $matchingTags = array();
     85:
     86:         // load TagObjects from the DB where content equals the content of one of our transient TagObjects
     87:         foreach($queryTags as $queryTag) {
     88:             $tags = $queryTag->loadAllByAttribute('content', $queryTag->get('content'));
     89:             $matchingTags = array_merge($matchingTags, $tags);
     90:         }
     91:
     92:         // the result objects
     93:         $results = array();
     94:         // matching tags with weights
     95:         $matches = array();
     96:
     97:         if ($config->get('cache.provider.name') != '' && count($queryTags) == 1) { // for now, we are only caching on single tags
     98:             $key = $queryTags[0]->get('content');
     99:             $matches = $this->loadFromCache($key);
    100:         }
    101:
    102:         if (count($matches) == 0) {
    103:             /*
    104:              * Build an array of BOs for the matching tags from the DB:
    105:              * array key = BO ID
    106:              * array value = weight (the amount of tags matching the BO)
    107:              */
    108:             foreach($matchingTags as $tag) {
    109:                 if ($returnType == 'all' || $tag->get('taggedClass') == $returnType) {
    110:
    111:                     $key = $tag->get('taggedClass').'-'.$tag->get('taggedOID');
    112:
    113:                     if(isset($matches[$key])) {
    114:                         // increment the weight if the same BO is tagged more than once
    115:                         $weight = intval($matches[$key]) + 1;
    116:                         $matches[$key] = $weight;
    117:                     }else{
    118:                         $matches[$key] = 1;
    119:                     }
    120:                 }
    121:             }
    122:
    123:             if ($config->get('cache.provider.name') != '' && count($queryTags) == 1) { // for now, we are only caching on single tags
    124:                 $key = $queryTags[0]->get('content');
    125:                 $this->addToCache($key, $matches);
    126:             }
    127:         }
    128:
    129:         // sort the matches based on tag frequency weight
    130:         arsort($matches);
    131:
    132:         $this->numberFound = count($matches);
    133:
    134:         // now paginate
    135:         $matches = array_slice($matches, $start, $limit+5); // the +5 is just some padding in case of orphans
    136:
    137:         // now load each object
    138:         foreach ($matches as $key => $weight) {
    139:             if(count($results) < $limit) {
    140:                 $parts = explode('-', $key);
    141:
    142:                 try {
    143:
    144:                     $BO = new $parts[0];
    145:                     $BO->load($parts[1]);
    146:
    147:                     $results[] = $BO;
    148:
    149:                 }catch(BONotFoundException $e) {
    150:                     self::$logger->warn('Orpaned TagObject detected pointing to a non-existant BO of OID ['.$parts[1].'] and type ['.$parts[0].'].');
    151:                 }
    152:             }
    153:         }
    154:
    155:         return $results;
    156:     }
    157:
    158:     /**
    159:      * {@inheritdoc}
    160:      */
    161:     public function getRelated(AlphaDAO $sourceObject, $returnType = 'all', $start = 0, $limit = 10, $distinct = '') {
    162:
    163:         global $config;
    164:
    165:         // the result objects
    166:         $results = array();
    167:         // matching tags with weights
    168:         $matches = array();
    169:         // only used in conjunction with distinct param
    170:         $distinctValues = array();
    171:
    172:         if ($config->get('cache.provider.name') != '') {
    173:             $key = get_class($sourceObject).'-'.$sourceObject->getOID().'-related'.($distinct == '' ? '' : '-distinct');
    174:             $matches = $this->loadFromCache($key);
    175:         }
    176:
    177:         if (count($matches) == 0) {
    178:             // all the tags on the source object for comparison
    179:             $tags = $sourceObject->getPropObject('tags')->getRelatedObjects();
    180:
    181:             foreach($tags as $tag) {
    182:                 $tagObject = new TagObject();
    183:
    184:                 if ($distinct == '') {
    185:                     $matchingTags = $tagObject->query("SELECT * FROM ".$tagObject->getTableName()." WHERE
    186:                         content='".$tag->get('content')."' AND NOT
    187:                         (taggedOID = '".$sourceObject->getOID()."' AND taggedClass = '".get_class($sourceObject)."');");
    188:                 }else{
    189:                     // filter out results where the source object field is identical to distinct param
    190:                     $matchingTags = $tagObject->query("SELECT * FROM ".$tagObject->getTableName()." WHERE
    191:                         content='".$tag->get('content')."' AND NOT
    192:                         (taggedOID = '".$sourceObject->getOID()."' AND taggedClass = '".get_class($sourceObject)."')
    193:                         AND taggedOID IN (SELECT OID FROM ".$sourceObject->getTableName()." WHERE ".$distinct." != '".addslashes($sourceObject->get($distinct))."');");
    194:                 }
    195:
    196:                 foreach($matchingTags as $matchingTag) {
    197:                     if ($returnType == 'all' || $tag->get('taggedClass') == $returnType) {
    198:
    199:                         $key = $matchingTag['taggedClass'].'-'.$matchingTag['taggedOID'];
    200:
    201:                         // matches on the distinct if defined need to be skipped
    202:                         if ($distinct != '') {
    203:                             try {
    204:
    205:                                 $BO = new $matchingTag['taggedClass'];
    206:                                 $BO->load($matchingTag['taggedOID']);
    207:
    208:                                 // skip where the source object field is identical
    209:                                 if ($sourceObject->get($distinct) == $BO->get($distinct))
    210:                                     continue;
    211:
    212:                                 if(!in_array($BO->get($distinct), $distinctValues)) {
    213:                                     $distinctValues[] = $BO->get($distinct);
    214:                                 }else{
    215:                                     continue;
    216:                                 }
    217:                             }catch (BONotFoundException $e) {
    218:                                 self::$logger->warn('Error loading object ['.$matchingTag['taggedOID'].'] of type ['.$matchingTag['taggedClass'].'], probable orphan');
    219:                             }
    220:                         }
    221:
    222:                         if(isset($matches[$key])) {
    223:                             // increment the weight if the same BO is tagged more than once
    224:                             $weight = intval($matches[$key]) + 1;
    225:                             $matches[$key] = $weight;
    226:                         }else{
    227:                             $matches[$key] = 1;
    228:                         }
    229:                     }
    230:                 }
    231:
    232:                 if ($config->get('cache.provider.name') != '') {
    233:                     $key = get_class($sourceObject).'-'.$sourceObject->getOID().'-related'.($distinct == '' ? '' : '-distinct');
    234:                     $this->addToCache($key, $matches);
    235:                 }
    236:             }
    237:         }
    238:
    239:         // sort the matches based on tag frequency weight
    240:         arsort($matches);
    241:
    242:         $this->numberFound = count($matches);
    243:
    244:         // now paginate
    245:         $matches = array_slice($matches, $start, $limit);
    246:
    247:         // now load each object
    248:         foreach ($matches as $key => $weight) {
    249:             $parts = explode('-', $key);
    250:
    251:             $BO = new $parts[0];
    252:             $BO->load($parts[1]);
    253:
    254:             $results[] = $BO;
    255:         }
    256:
    257:         return $results;
    258:     }
    259:
    260:     /**
    261:      * {@inheritdoc}
    262:      */
    263:     public function index(AlphaDAO $sourceObject) {
    264:         $taggedAttributes = $sourceObject->getTaggedAttributes();
    265:
    266:         foreach($taggedAttributes as $tagged) {
    267:             $tags = TagObject::tokenize($sourceObject->get($tagged), get_class($sourceObject), $sourceObject->getOID());
    268:
    269:             foreach($tags as $tag) {
    270:                 try {
    271:                     $tag->save();
    272:                 }catch(ValidationException $e){
    273:                     /*
    274:                      * The unique key has most-likely been violated because this BO is already tagged with this
    275:                      * value, so we can ignore in this case.
    276:                      */
    277:                 }
    278:             }
    279:         }
    280:     }
    281:
    282:     /**
    283:      * {@inheritdoc}
    284:      */
    285:     public function delete(AlphaDAO $sourceObject) {
    286:         $tags = $sourceObject->getPropObject('tags')->getRelatedObjects();
    287:
    288:         foreach ($tags as $tag)
    289:             $tag->delete();
    290:     }
    291:
    292:     /**
    293:      * {@inheritdoc}
    294:      */
    295:     public function getNumberFound() {
    296:         return $this->numberFound;
    297:     }
    298:
    299:     /**
    300:      * Load the tag search matches from the cache
    301:      *
    302:      * @since 1.2.4
    303:      */
    304:     private function loadFromCache($key) {
    305:         global $config;
    306:
    307:         try {
    308:             $cache = AlphaCacheProviderFactory::getInstance($config->get('cache.provider.name'));
    309:             $matches = $cache->get($key);
    310:
    311:             if(!$matches) {
    312:                 self::$logger->debug('Cache miss on key ['.$key.']');
    313:                 return array();
    314:             }else{
    315:                 self::$logger->debug('Cache hit on key ['.$key.']');
    316:                 return $matches;
    317:             }
    318:         }catch(Exception $e) {
    319:             self::$logger->error('Error while attempting to load a search result from ['.$config->get('cache.provider.name').']
    320:              instance: ['.$e->getMessage().']');
    321:
    322:             return array();
    323:         }
    324:     }
    325:
    326:     /**
    327:      * Add the tag search matches to the cache
    328:      *
    329:      * @since 1.2.4
    330:      */
    331:     public function addToCache($key, $matches) {
    332:         global $config;
    333:
    334:         try {
    335:             $cache = AlphaCacheProviderFactory::getInstance($config->get('cache.provider.name'));
    336:             $cache->set($key, $matches, 86400); // cache search matches for a day
    337:
    338:         }catch(Exception $e) {
    339:             self::$logger->error('Error while attempting to store a search matches array to the ['.$config->get('cache.provider.name').']
    340:                 instance: ['.$e->getMessage().']');
    341:         }
    342:     }
    343: }
    344:
    345: ?>