Overview

Namespaces

  • Alpha
    • Controller
      • Front
    • Exception
    • Model
      • Type
    • Task
    • Util
      • Backup
      • Cache
      • Code
        • Highlight
        • Metric
      • Config
      • Convertor
      • Email
      • Extension
      • Feed
      • File
      • Graph
      • Helper
      • Http
        • Filter
        • Session
      • Image
      • Logging
      • Search
      • Security
    • View
      • Renderer
        • Html
        • Json
      • Widget

Classes

  • SearchProviderFactory
  • SearchProviderTags

Interfaces

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