[ Index ]

PHP Cross Reference of Joomla 2.5.4 DE

title

Body

[close]

/components/com_finder/models/ -> search.php (source)

   1  <?php
   2  /**
   3   * @package     Joomla.Site
   4   * @subpackage  com_finder
   5   *
   6   * @copyright   Copyright (C) 2005 - 2012 Open Source Matters, Inc. All rights reserved.
   7   * @license     GNU General Public License version 2 or later; see LICENSE
   8   */
   9  
  10  defined('_JEXEC') or die;
  11  
  12  // Register dependent classes.
  13  define('FINDER_PATH_INDEXER', JPATH_ADMINISTRATOR . '/components/com_finder/helpers/indexer');
  14  JLoader::register('FinderIndexerHelper', FINDER_PATH_INDEXER . '/helper.php');
  15  JLoader::register('FinderIndexerQuery', FINDER_PATH_INDEXER . '/query.php');
  16  JLoader::register('FinderIndexerResult', FINDER_PATH_INDEXER . '/result.php');
  17  
  18  jimport('joomla.application.component.modellist');
  19  
  20  /**
  21   * Search model class for the Finder package.
  22   *
  23   * @package     Joomla.Site
  24   * @subpackage  com_finder
  25   * @since       2.5
  26   */
  27  class FinderModelSearch extends JModelList
  28  {
  29      /**
  30       * Context string for the model type
  31       *
  32       * @var    string
  33       * @since  2.5
  34       */
  35      protected $context = 'com_finder.search';
  36  
  37      /**
  38       * The query object is an instance of FinderIndexerQuery which contains and
  39       * models the entire search query including the text input; static and
  40       * dynamic taxonomy filters; date filters; etc.
  41       *
  42       * @var    FinderIndexerQuery
  43       * @since  2.5
  44       */
  45      protected $query;
  46  
  47      /**
  48       * An array of all excluded terms ids.
  49       *
  50       * @var    array
  51       * @since  2.5
  52       */
  53      protected $excludedTerms = array();
  54  
  55      /**
  56       * An array of all included terms ids.
  57       *
  58       * @var    array
  59       * @since  2.5
  60       */
  61      protected $includedTerms = array();
  62  
  63      /**
  64       * An array of all required terms ids.
  65       *
  66       * @var    array
  67       * @since  2.5
  68       */
  69      protected $requiredTerms = array();
  70  
  71      /**
  72       * Method to get the results of the query.
  73       *
  74       * @return  array  An array of FinderIndexerResult objects.
  75       *
  76       * @since   2.5
  77       * @throws  Exception on database error.
  78       */
  79  	public function getResults()
  80      {
  81          // Check if the search query is valid.
  82          if (empty($this->query->search))
  83          {
  84              return null;
  85          }
  86  
  87          // Check if we should return results.
  88          if (empty($this->includedTerms) && (empty($this->query->filters) || !$this->query->empty))
  89          {
  90              return null;
  91          }
  92  
  93          // Get the store id.
  94          $store = $this->getStoreId('getResults');
  95  
  96          // Use the cached data if possible.
  97          if ($this->retrieve($store))
  98          {
  99              return $this->retrieve($store);
 100          }
 101  
 102          // Get the row data.
 103          $items = $this->getResultsData();
 104  
 105          // Check the data.
 106          if (empty($items))
 107          {
 108              return null;
 109          }
 110  
 111          // Create the query to get the search results.
 112          $db = $this->getDbo();
 113          $query = $db->getQuery(true);
 114  
 115          $query->select($db->quoteName('link_id') . ', ' . $db->quoteName('object'));
 116          $query->from($db->quoteName('#__finder_links'));
 117          $query->where($db->quoteName('link_id') . ' IN (' . implode(',', array_keys($items)) . ')');
 118  
 119          // Load the results from the database.
 120          $db->setQuery($query);
 121          $rows = $db->loadObjectList('link_id');
 122  
 123          // Check for a database error.
 124          if ($db->getErrorNum())
 125          {
 126              throw new Exception($db->getErrorMsg(), 500);
 127          }
 128  
 129          // Set up our results container.
 130          $results = $items;
 131  
 132          // Convert the rows to result objects.
 133          foreach ($rows as $rk => $row)
 134          {
 135              // Build the result object.
 136              $result = unserialize($row->object);
 137              $result->weight = $results[$rk];
 138              $result->link_id = $rk;
 139  
 140              // Add the result back to the stack.
 141              $results[$rk] = $result;
 142          }
 143  
 144          // Switch to a non-associative array.
 145          $results = array_values($results);
 146  
 147          // Push the results into cache.
 148          $this->store($store, $results);
 149  
 150          // Return the results.
 151          return $this->retrieve($store);
 152      }
 153  
 154      /**
 155       * Method to get the total number of results.
 156       *
 157       * @return  integer  The total number of results.
 158       *
 159       * @since   2.5
 160       * @throws  Exception on database error.
 161       */
 162  	public function getTotal()
 163      {
 164          // Check if the search query is valid.
 165          if (empty($this->query->search))
 166          {
 167              return null;
 168          }
 169  
 170          // Check if we should return results.
 171          if (empty($this->includedTerms) && (empty($this->query->filters) || !$this->query->empty))
 172          {
 173              return null;
 174          }
 175  
 176          // Get the store id.
 177          $store = $this->getStoreId('getTotal');
 178  
 179          // Use the cached data if possible.
 180          if ($this->retrieve($store))
 181          {
 182              return $this->retrieve($store);
 183          }
 184  
 185          // Get the results total.
 186          $total = $this->getResultsTotal();
 187  
 188          // Push the total into cache.
 189          $this->store($store, $total);
 190  
 191          // Return the total.
 192          return $this->retrieve($store);
 193      }
 194  
 195      /**
 196       * Method to get the query object.
 197       *
 198       * @return  FinderIndexerQuery  A query object.
 199       *
 200       * @since   2.5
 201       */
 202  	public function getQuery()
 203      {
 204          // Get the state in case it isn't loaded.
 205          $state = $this->getState();
 206  
 207          // Return the query object.
 208          return $this->query;
 209      }
 210  
 211      /**
 212       * Method to build a database query to load the list data.
 213       *
 214       * @return  JDatabaseQuery  A database query.
 215       *
 216       * @since   2.5
 217       */
 218  	protected function getListQuery()
 219      {
 220          // Get the store id.
 221          $store = $this->getStoreId('getListQuery');
 222  
 223          // Use the cached data if possible.
 224          if ($this->retrieve($store, false))
 225          {
 226              return clone($this->retrieve($store, false));
 227          }
 228  
 229          // Set variables
 230          $user = JFactory::getUser();
 231          $groups = implode(',', $user->getAuthorisedViewLevels());
 232  
 233          // Create a new query object.
 234          $db = $this->getDbo();
 235          $query = $db->getQuery(true);
 236  
 237          $query->select('l.link_id');
 238          $query->from($db->quoteName('#__finder_links') . ' AS l');
 239          $query->where($db->quoteName('l.access') . ' IN (' . $groups . ')');
 240          $query->where($db->quoteName('l.state') . ' = 1');
 241  
 242          // Get the null date and the current date, minus seconds.
 243          $nullDate = $db->quote($db->getNullDate());
 244          $nowDate = $db->quote(substr_replace(JFactory::getDate()->toSQL(), '00', -2));
 245  
 246          // Add the publish up and publish down filters.
 247          $query->where('(' . $db->quoteName('l.publish_start_date') . ' = ' . $nullDate . ' OR ' . $db->quoteName('l.publish_start_date') . ' <= ' . $nowDate . ')');
 248          $query->where('(' . $db->quoteName('l.publish_end_date') . ' = ' . $nullDate . ' OR ' . $db->quoteName('l.publish_end_date') . ' >= ' . $nowDate . ')');
 249  
 250          /*
 251           * Add the taxonomy filters to the query. We have to join the taxonomy
 252           * map table for each group so that we can use AND clauses across
 253           * groups. Within each group there can be an array of values that will
 254           * use OR clauses.
 255           */
 256          if (!empty($this->query->filters))
 257          {
 258              // Convert the associative array to a numerically indexed array.
 259              $groups = array_values($this->query->filters);
 260  
 261              // Iterate through each taxonomy group and add the join and where.
 262              for ($i = 0, $c = count($groups); $i < $c; $i++)
 263              {
 264                  // We use the offset because each join needs a unique alias.
 265                  $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t' . $i . ' ON t' . $i . '.link_id = l.link_id');
 266                  $query->where('t' . $i . '.node_id IN (' . implode(',', $groups[$i]) . ')');
 267              }
 268          }
 269  
 270          // Add the start date filter to the query.
 271          if (!empty($this->query->date1))
 272          {
 273              // Escape the date.
 274              $date1 = $db->quote($this->query->date1);
 275  
 276              // Add the appropriate WHERE condition.
 277              if ($this->query->when1 == 'before')
 278              {
 279                  $query->where($db->quoteName('l.start_date') . ' <= ' . $date1);
 280              }
 281              elseif ($this->query->when1 == 'after')
 282              {
 283                  $query->where($db->quoteName('l.start_date') . ' >= ' . $date1);
 284              }
 285              else
 286              {
 287                  $query->where($db->quoteName('l.start_date') . ' = ' . $date1);
 288              }
 289          }
 290  
 291          // Add the end date filter to the query.
 292          if (!empty($this->query->date2))
 293          {
 294              // Escape the date.
 295              $date2 = $db->quote($this->query->date2);
 296  
 297              // Add the appropriate WHERE condition.
 298              if ($this->query->when2 == 'before')
 299              {
 300                  $query->where($db->quoteName('l.start_date') . ' <= ' . $date2);
 301              }
 302              elseif ($this->query->when2 == 'after')
 303              {
 304                  $query->where($db->quoteName('l.start_date') . ' >= ' . $date2);
 305              }
 306              else
 307              {
 308                  $query->where($db->quoteName('l.start_date') . ' = ' . $date2);
 309              }
 310          }
 311          // Filter by language
 312          if ($this->getState('filter.language')) {
 313              $query->where('l.language in ('.$db->quote(JFactory::getLanguage()->getTag()).','.$db->quote('*').')');
 314          }
 315          // Push the data into cache.
 316          $this->store($store, $query, false);
 317  
 318          // Return a copy of the query object.
 319          return clone($this->retrieve($store, false));
 320      }
 321  
 322      /**
 323       * Method to get the total number of results for the search query.
 324       *
 325       * @return  integer  The results total.
 326       *
 327       * @since   2.5
 328       * @throws  Exception on database error.
 329       */
 330  	protected function getResultsTotal()
 331      {
 332          // Get the store id.
 333          $store = $this->getStoreId('getResultsTotal', false);
 334  
 335          // Use the cached data if possible.
 336          if ($this->retrieve($store))
 337          {
 338              return $this->retrieve($store);
 339          }
 340  
 341          // Get the base query and add the ordering information.
 342          $base = $this->getListQuery();
 343          $base->select('0 AS ordering');
 344  
 345          // Get the maximum number of results.
 346          $limit = (int) $this->getState('match.limit');
 347  
 348          /*
 349           * If there are no optional or required search terms in the query,
 350           * we can get the result total in one relatively simple database query.
 351           */
 352          if (empty($this->includedTerms))
 353          {
 354              // Adjust the query to join on the appropriate mapping table.
 355              $sql = clone($base);
 356              $sql->clear('select');
 357              $sql->select('COUNT(DISTINCT l.link_id)');
 358  
 359              // Get the total from the database.
 360              $this->_db->setQuery($sql);
 361              $total = $this->_db->loadResult();
 362  
 363              // Check for a database error.
 364              if ($this->_db->getErrorNum())
 365              {
 366                  throw new Exception($this->_db->getErrorMsg(), 500);
 367              }
 368  
 369              // Push the total into cache.
 370              $this->store($store, min($total, $limit));
 371  
 372              // Return the total.
 373              return $this->retrieve($store);
 374          }
 375  
 376          /*
 377           * If there are optional or required search terms in the query, the
 378           * process of getting the result total is more complicated.
 379           */
 380          $start = 0;
 381          $total = 0;
 382          $more = false;
 383          $items = array();
 384          $sorted = array();
 385          $maps = array();
 386          $excluded = $this->getExcludedLinkIds();
 387  
 388          /*
 389           * Iterate through the included search terms and group them by mapping
 390           * table suffix. This ensures that we never have to do more than 16
 391           * queries to get a batch. This may seem like a lot but it is rarely
 392           * anywhere near 16 because of the improved mapping algorithm.
 393           */
 394          foreach ($this->includedTerms as $token => $ids)
 395          {
 396              // Get the mapping table suffix.
 397              $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
 398  
 399              // Initialize the mapping group.
 400              if (!array_key_exists($suffix, $maps))
 401              {
 402                  $maps[$suffix] = array();
 403              }
 404              // Add the terms to the mapping group.
 405              $maps[$suffix] = array_merge($maps[$suffix], $ids);
 406          }
 407  
 408          /*
 409           * When the query contains search terms we need to find and process the
 410           * result total iteratively using a do-while loop.
 411           */
 412          do
 413          {
 414              // Create a container for the fetched results.
 415              $results = array();
 416              $more = false;
 417  
 418              /*
 419               * Iterate through the mapping groups and load the total from each
 420               * mapping table.
 421               */
 422              foreach ($maps as $suffix => $ids)
 423              {
 424                  // Create a storage key for this set.
 425                  $setId = $this->getStoreId('getResultsTotal:' . serialize(array_values($ids)) . ':' . $start . ':' . $limit);
 426  
 427                  // Use the cached data if possible.
 428                  if ($this->retrieve($setId))
 429                  {
 430                      $temp = $this->retrieve($setId);
 431                  }
 432                  // Load the data from the database.
 433                  else
 434                  {
 435                      // Adjust the query to join on the appropriate mapping table.
 436                      $sql = clone($base);
 437                      $sql->join('INNER', '#__finder_links_terms' . $suffix . ' AS m ON m.link_id = l.link_id');
 438                      $sql->where('m.term_id IN (' . implode(',', $ids) . ')');
 439  
 440                      // Load the results from the database.
 441                      $this->_db->setQuery($sql, $start, $limit);
 442                      $temp = $this->_db->loadObjectList();
 443  
 444                      // Check for a database error.
 445                      if ($this->_db->getErrorNum())
 446                      {
 447                          throw new Exception($this->_db->getErrorMsg(), 500);
 448                      }
 449  
 450                      // Set the more flag to true if any of the sets equal the limit.
 451                      $more = (count($temp) === $limit) ? true : false;
 452  
 453                      // We loaded the data unkeyed but we need it to be keyed for later.
 454                      $junk = $temp;
 455                      $temp = array();
 456  
 457                      // Convert to an associative array.
 458                      for ($i = 0, $c = count($junk); $i < $c; $i++)
 459                      {
 460                          $temp[$junk[$i]->link_id] = $junk[$i];
 461                      }
 462  
 463                      // Store this set in cache.
 464                      $this->store($setId, $temp);
 465                  }
 466  
 467                  // Merge the results.
 468                  $results = array_merge($results, $temp);
 469              }
 470  
 471              // Check if there are any excluded terms to deal with.
 472              if (count($excluded))
 473              {
 474                  // Remove any results that match excluded terms.
 475                  for ($i = 0, $c = count($results); $i < $c; $i++)
 476                  {
 477                      if (in_array($results[$i]->link_id, $excluded))
 478                      {
 479                          unset($results[$i]);
 480                      }
 481                  }
 482  
 483                  // Reset the array keys.
 484                  $results = array_values($results);
 485              }
 486  
 487              // Iterate through the set to extract the unique items.
 488              for ($i = 0, $c = count($results); $i < $c; $i++)
 489              {
 490                  if (!isset($sorted[$results[$i]->link_id]))
 491                  {
 492                      $sorted[$results[$i]->link_id] = $results[$i]->ordering;
 493                  }
 494              }
 495  
 496              /*
 497               * If the query contains just optional search terms and we have
 498               * enough items for the page, we can stop here.
 499               */
 500              if (empty($this->requiredTerms))
 501              {
 502                  // If we need more items and they're available, make another pass.
 503                  if ($more && count($sorted) < $limit)
 504                  {
 505                      // Increment the batch starting point and continue.
 506                      $start += $limit;
 507                      continue;
 508                  }
 509  
 510                  // Push the total into cache.
 511                  $this->store($store, min(count($sorted), $limit));
 512  
 513                  // Return the total.
 514                  return $this->retrieve($store);
 515              }
 516  
 517              /*
 518               * The query contains required search terms so we have to iterate
 519               * over the items and remove any items that do not match all of the
 520               * required search terms. This is one of the most expensive steps
 521               * because a required token could theoretically eliminate all of
 522               * current terms which means we would have to loop through all of
 523               * the possibilities.
 524               */
 525              foreach ($this->requiredTerms as $token => $required)
 526              {
 527                  // Create a storage key for this set.
 528                  $setId = $this->getStoreId('getResultsTotal:required:' . serialize(array_values($required)) . ':' . $start . ':' . $limit);
 529  
 530                  // Use the cached data if possible.
 531                  if ($this->retrieve($setId))
 532                  {
 533                      $reqTemp = $this->retrieve($setId);
 534                  }
 535                      // Check if the token was matched.
 536                  elseif (empty($required))
 537                  {
 538                      return null;
 539                  }
 540                      // Load the data from the database.
 541                  else
 542                  {
 543                      // Setup containers in case we have to make multiple passes.
 544                      $reqMore = false;
 545                      $reqStart = 0;
 546                      $reqTemp = array();
 547  
 548                      do
 549                      {
 550                          // Get the map table suffix.
 551                          $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
 552  
 553                          // Adjust the query to join on the appropriate mapping table.
 554                          $sql = clone($base);
 555                          $sql->join('INNER', '#__finder_links_terms' . $suffix . ' AS m ON m.link_id = l.link_id');
 556                          $sql->where('m.term_id IN (' . implode(',', $required) . ')');
 557  
 558                          // Load the results from the database.
 559                          $this->_db->setQuery($sql, $reqStart, $limit);
 560                          $temp = $this->_db->loadObjectList('link_id');
 561  
 562                          // Check for a database error.
 563                          if ($this->_db->getErrorNum())
 564                          {
 565                              throw new Exception($this->_db->getErrorMsg(), 500);
 566                          }
 567  
 568                          // Set the required token more flag to true if the set equal the limit.
 569                          $reqMore = (count($temp) === $limit) ? true : false;
 570  
 571                          // Merge the matching set for this token.
 572                          $reqTemp = $reqTemp + $temp;
 573  
 574                          // Increment the term offset.
 575                          $reqStart += $limit;
 576                      }
 577                      while ($reqMore == true);
 578  
 579                      // Store this set in cache.
 580                      $this->store($setId, $reqTemp);
 581                  }
 582  
 583                  // Remove any items that do not match the required term.
 584                  $sorted = array_intersect_key($sorted, $reqTemp);
 585              }
 586  
 587              // If we need more items and they're available, make another pass.
 588              if ($more && count($sorted) < $limit)
 589              {
 590                  // Increment the batch starting point.
 591                  $start += $limit;
 592  
 593                  // Merge the found items.
 594                  $items = $items + $sorted;
 595  
 596                  continue;
 597              }
 598              // Otherwise, end the loop.
 599              {
 600                  // Merge the found items.
 601                  $items = $items + $sorted;
 602  
 603                  $more = false;
 604              }
 605              // End do-while loop.
 606          }
 607          while ($more === true);
 608  
 609          // Set the total.
 610          $total = count($items);
 611          $total = min($total, $limit);
 612  
 613          // Push the total into cache.
 614          $this->store($store, $total);
 615  
 616          // Return the total.
 617          return $this->retrieve($store);
 618      }
 619  
 620      /**
 621       * Method to get the results for the search query.
 622       *
 623       * @return  array  An array of result data objects.
 624       *
 625       * @since   2.5
 626       * @throws  Exception on database error.
 627       */
 628  	protected function getResultsData()
 629      {
 630          // Get the store id.
 631          $store = $this->getStoreId('getResultsData', false);
 632  
 633          // Use the cached data if possible.
 634          if ($this->retrieve($store))
 635          {
 636              return $this->retrieve($store);
 637          }
 638  
 639          // Get the result ordering and direction.
 640          $ordering = $this->getState('list.ordering', 'l.start_date');
 641          $direction = $this->getState('list.direction', 'DESC');
 642  
 643          // Get the base query and add the ordering information.
 644          $base = $this->getListQuery();
 645          $base->select($this->_db->escape($ordering) . ' AS ordering');
 646          $base->order($this->_db->escape($ordering) . ' ' . $this->_db->escape($direction));
 647  
 648          /*
 649           * If there are no optional or required search terms in the query, we
 650           * can get the results in one relatively simple database query.
 651           */
 652          if (empty($this->includedTerms))
 653          {
 654              // Get the results from the database.
 655              $this->_db->setQuery($base, (int) $this->getState('list.start'), (int) $this->getState('list.limit'));
 656              $return = $this->_db->loadObjectList('link_id');
 657  
 658              // Check for a database error.
 659              if ($this->_db->getErrorNum())
 660              {
 661                  throw new Exception($this->_db->getErrorMsg(), 500);
 662              }
 663  
 664              // Get a new store id because this data is page specific.
 665              $store = $this->getStoreId('getResultsData', true);
 666  
 667              // Push the results into cache.
 668              $this->store($store, $return);
 669  
 670              // Return the results.
 671              return $this->retrieve($store);
 672          }
 673  
 674          /*
 675           * If there are optional or required search terms in the query, the
 676           * process of getting the results is more complicated.
 677           */
 678          $start = 0;
 679          $limit = (int) $this->getState('match.limit');
 680          $more = false;
 681          $items = array();
 682          $sorted = array();
 683          $maps = array();
 684          $excluded = $this->getExcludedLinkIds();
 685  
 686          /*
 687           * Iterate through the included search terms and group them by mapping
 688           * table suffix. This ensures that we never have to do more than 16
 689           * queries to get a batch. This may seem like a lot but it is rarely
 690           * anywhere near 16 because of the improved mapping algorithm.
 691           */
 692          foreach ($this->includedTerms as $token => $ids)
 693          {
 694              // Get the mapping table suffix.
 695              $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
 696  
 697              // Initialize the mapping group.
 698              if (!array_key_exists($suffix, $maps))
 699              {
 700                  $maps[$suffix] = array();
 701              }
 702  
 703              // Add the terms to the mapping group.
 704              $maps[$suffix] = array_merge($maps[$suffix], $ids);
 705          }
 706  
 707          /*
 708           * When the query contains search terms we need to find and process the
 709           * results iteratively using a do-while loop.
 710           */
 711          do
 712          {
 713              // Create a container for the fetched results.
 714              $results = array();
 715              $more = false;
 716  
 717              /*
 718               * Iterate through the mapping groups and load the results from each
 719               * mapping table.
 720               */
 721              foreach ($maps as $suffix => $ids)
 722              {
 723                  // Create a storage key for this set.
 724                  $setId = $this->getStoreId('getResultsData:' . serialize(array_values($ids)) . ':' . $start . ':' . $limit);
 725  
 726                  // Use the cached data if possible.
 727                  if ($this->retrieve($setId))
 728                  {
 729                      $temp = $this->retrieve($setId);
 730                  }
 731                  // Load the data from the database.
 732                  else
 733                  {
 734                      // Adjust the query to join on the appropriate mapping table.
 735                      $sql = clone($base);
 736                      $sql->join('INNER', $this->_db->quoteName('#__finder_links_terms' . $suffix) . ' AS m ON m.link_id = l.link_id');
 737                      $sql->where('m.term_id IN (' . implode(',', $ids) . ')');
 738  
 739                      // Load the results from the database.
 740                      $this->_db->setQuery($sql, $start, $limit);
 741                      $temp = $this->_db->loadObjectList('link_id');
 742  
 743                      // Check for a database error.
 744                      if ($this->_db->getErrorNum())
 745                      {
 746                          throw new Exception($this->_db->getErrorMsg(), 500);
 747                      }
 748  
 749                      // Store this set in cache.
 750                      $this->store($setId, $temp);
 751  
 752                      // The data is keyed by link_id to ease caching, we don't need it till later.
 753                      $temp = array_values($temp);
 754                  }
 755  
 756                  // Set the more flag to true if any of the sets equal the limit.
 757                  $more = (count($temp) === $limit) ? true : false;
 758  
 759                  // Merge the results.
 760                  $results = array_merge($results, $temp);
 761              }
 762  
 763              // Check if there are any excluded terms to deal with.
 764              if (count($excluded))
 765              {
 766                  // Remove any results that match excluded terms.
 767                  for ($i = 0, $c = count($results); $i < $c; $i++)
 768                  {
 769                      if (in_array($results[$i]->link_id, $excluded))
 770                      {
 771                          unset($results[$i]);
 772                      }
 773                  }
 774  
 775                  // Reset the array keys.
 776                  $results = array_values($results);
 777              }
 778  
 779              /*
 780               * If we are ordering by relevance we have to add up the relevance
 781               * scores that are contained in the ordering field.
 782               */
 783              if ($ordering === 'm.weight')
 784              {
 785                  // Iterate through the set to extract the unique items.
 786                  for ($i = 0, $c = count($results); $i < $c; $i++)
 787                  {
 788                      // Add the total weights for all included search terms.
 789                      if (isset($sorted[$results[$i]->link_id]))
 790                      {
 791                          $sorted[$results[$i]->link_id] += (float) $results[$i]->ordering;
 792                      }
 793                      else
 794                      {
 795                          $sorted[$results[$i]->link_id] = (float) $results[$i]->ordering;
 796                      }
 797                  }
 798              }
 799              /*
 800               * If we are ordering by start date we have to add convert the
 801               * dates to unix timestamps.
 802               */
 803              elseif ($ordering === 'l.start_date')
 804              {
 805                  // Iterate through the set to extract the unique items.
 806                  for ($i = 0, $c = count($results); $i < $c; $i++)
 807                  {
 808                      if (!isset($sorted[$results[$i]->link_id]))
 809                      {
 810                          $sorted[$results[$i]->link_id] = strtotime($results[$i]->ordering);
 811                      }
 812                  }
 813              }
 814              /*
 815               * If we are not ordering by relevance or date, we just have to add
 816               * the unique items to the set.
 817               */
 818              else
 819              {
 820                  // Iterate through the set to extract the unique items.
 821                  for ($i = 0, $c = count($results); $i < $c; $i++)
 822                  {
 823                      if (!isset($sorted[$results[$i]->link_id]))
 824                      {
 825                          $sorted[$results[$i]->link_id] = $results[$i]->ordering;
 826                      }
 827                  }
 828              }
 829  
 830              // Sort the results.
 831              if ($direction === 'ASC')
 832              {
 833                  natcasesort($items);
 834              }
 835              else
 836              {
 837                  natcasesort($items);
 838                  $items = array_reverse($items, true);
 839              }
 840  
 841              /*
 842               * If the query contains just optional search terms and we have
 843               * enough items for the page, we can stop here.
 844               */
 845              if (empty($this->requiredTerms))
 846              {
 847                  // If we need more items and they're available, make another pass.
 848                  if ($more && count($sorted) < ($this->getState('list.start') + $this->getState('list.limit')))
 849                  {
 850                      // Increment the batch starting point and continue.
 851                      $start += $limit;
 852                      continue;
 853                  }
 854  
 855                  // Push the results into cache.
 856                  $this->store($store, $sorted);
 857  
 858                  // Return the requested set.
 859                  return array_slice($this->retrieve($store), (int) $this->getState('list.start'), (int) $this->getState('list.limit'), true);
 860              }
 861  
 862              /*
 863               * The query contains required search terms so we have to iterate
 864               * over the items and remove any items that do not match all of the
 865               * required search terms. This is one of the most expensive steps
 866               * because a required token could theoretically eliminate all of
 867               * current terms which means we would have to loop through all of
 868               * the possibilities.
 869               */
 870              foreach ($this->requiredTerms as $token => $required)
 871              {
 872                  // Create a storage key for this set.
 873                  $setId = $this->getStoreId('getResultsData:required:' . serialize(array_values($required)) . ':' . $start . ':' . $limit);
 874  
 875                  // Use the cached data if possible.
 876                  if ($this->retrieve($setId))
 877                  {
 878                      $reqTemp = $this->retrieve($setId);
 879                  }
 880                  // Check if the token was matched.
 881                  elseif (empty($required))
 882                  {
 883                      return null;
 884                  }
 885                  // Load the data from the database.
 886                  else
 887                  {
 888                      // Setup containers in case we have to make multiple passes.
 889                      $reqMore = false;
 890                      $reqStart = 0;
 891                      $reqTemp = array();
 892  
 893                      do
 894                      {
 895                          // Get the map table suffix.
 896                          $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
 897  
 898                          // Adjust the query to join on the appropriate mapping table.
 899                          $sql = clone($base);
 900                          $sql->join('INNER', $this->_db->quoteName('#__finder_links_terms' . $suffix) . ' AS m ON m.link_id = l.link_id');
 901                          $sql->where('m.term_id IN (' . implode(',', $required) . ')');
 902  
 903                          // Load the results from the database.
 904                          $this->_db->setQuery($sql, $reqStart, $limit);
 905                          $temp = $this->_db->loadObjectList('link_id');
 906  
 907                          // Check for a database error.
 908                          if ($this->_db->getErrorNum())
 909                          {
 910                              throw new Exception($this->_db->getErrorMsg(), 500);
 911                          }
 912  
 913                          // Set the required token more flag to true if the set equal the limit.
 914                          $reqMore = (count($temp) === $limit) ? true : false;
 915  
 916                          // Merge the matching set for this token.
 917                          $reqTemp = $reqTemp + $temp;
 918  
 919                          // Increment the term offset.
 920                          $reqStart += $limit;
 921                      }
 922                      while ($reqMore == true);
 923  
 924                      // Store this set in cache.
 925                      $this->store($setId, $reqTemp);
 926                  }
 927  
 928                  // Remove any items that do not match the required term.
 929                  $sorted = array_intersect_key($sorted, $reqTemp);
 930              }
 931  
 932              // If we need more items and they're available, make another pass.
 933              if ($more && count($sorted) < ($this->getState('list.start') + $this->getState('list.limit')))
 934              {
 935                  // Increment the batch starting point.
 936                  $start += $limit;
 937  
 938                  // Merge the found items.
 939                  $items = array_merge($items, $sorted);
 940  
 941                  continue;
 942              }
 943              // Otherwise, end the loop.
 944              else
 945              {
 946                  // Set the found items.
 947                  $items = $sorted;
 948  
 949                  $more = false;
 950              }
 951          // End do-while loop.
 952          }
 953          while ($more === true);
 954  
 955          // Push the results into cache.
 956          $this->store($store, $items);
 957  
 958          // Return the requested set.
 959          return array_slice($this->retrieve($store), (int) $this->getState('list.start'), (int) $this->getState('list.limit'), true);
 960      }
 961  
 962      /**
 963       * Method to get an array of link ids that match excluded terms.
 964       *
 965       * @return  array  An array of links ids.
 966       *
 967       * @since   2.5
 968       * @throws  Exception on database error.
 969       */
 970  	protected function getExcludedLinkIds()
 971      {
 972          // Check if the search query has excluded terms.
 973          if (empty($this->excludedTerms))
 974          {
 975              return array();
 976          }
 977  
 978          // Get the store id.
 979          $store = $this->getStoreId('getExcludedLinkIds', false);
 980  
 981          // Use the cached data if possible.
 982          if ($this->retrieve($store))
 983          {
 984              return $this->retrieve($store);
 985          }
 986  
 987          // Initialize containers.
 988          $links = array();
 989          $maps = array();
 990  
 991          /*
 992           * Iterate through the excluded search terms and group them by mapping
 993           * table suffix. This ensures that we never have to do more than 16
 994           * queries to get a batch. This may seem like a lot but it is rarely
 995           * anywhere near 16 because of the improved mapping algorithm.
 996           */
 997          foreach ($this->excludedTerms as $token => $id)
 998          {
 999              // Get the mapping table suffix.
1000              $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
1001  
1002              // Initialize the mapping group.
1003              if (!array_key_exists($suffix, $maps))
1004              {
1005                  $maps[$suffix] = array();
1006              }
1007  
1008              // Add the terms to the mapping group.
1009              $maps[$suffix][] = (int) $id;
1010          }
1011  
1012          /*
1013           * Iterate through the mapping groups and load the excluded links ids
1014           * from each mapping table.
1015           */
1016          foreach ($maps as $suffix => $ids)
1017          {
1018              // Create a new query object.
1019              $db = $this->getDbo();
1020              $query = $db->getQuery(true);
1021  
1022              // Create the query to get the links ids.
1023              $query->select('link_id');
1024              $query->from($db->quoteName('#__finder_links_terms' . $suffix));
1025              $query->where($db->quoteName('term_id') . ' IN (' . implode(',', $ids) . ')');
1026              $query->group($db->quoteName('link_id'));
1027  
1028              // Load the link ids from the database.
1029              $db->setQuery($query);
1030              $temp = $db->loadColumn();
1031  
1032              // Check for a database error.
1033              if ($db->getErrorNum())
1034              {
1035                  throw new Exception($db->getErrorMsg(), 500);
1036              }
1037  
1038              // Merge the link ids.
1039              $links = array_merge($links, $temp);
1040          }
1041  
1042          // Sanitize the link ids.
1043          $links = array_unique($links);
1044          JArrayHelper::toInteger($links);
1045  
1046          // Push the link ids into cache.
1047          $this->store($store, $links);
1048  
1049          return $links;
1050      }
1051  
1052      /**
1053       * Method to get a subquery for filtering link ids mapped to specific
1054       * terms ids.
1055       *
1056       * @param   array  $terms  An array of search term ids.
1057       *
1058       * @return  JDatabaseQuery  A database object.
1059       *
1060       * @since   2.5
1061       */
1062  	protected function getTermsQuery($terms)
1063      {
1064          // Create the SQL query to get the matching link ids.
1065          //@TODO: Impact of removing SQL_NO_CACHE?
1066          $db = $this->getDbo();
1067          $query = $db->getQuery(true);
1068          $query->select('SQL_NO_CACHE link_id');
1069          $query->from('#__finder_links_terms');
1070          $query->where('term_id IN (' . implode(',', $terms) . ')');
1071  
1072          return $query;
1073      }
1074  
1075      /**
1076       * Method to get a store id based on model the configuration state.
1077       *
1078       * This is necessary because the model is used by the component and
1079       * different modules that might need different sets of data or different
1080       * ordering requirements.
1081       *
1082       * @param   string   $id    An identifier string to generate the store id. [optional]
1083       * @param   boolean  $page  True to store the data paged, false to store all data. [optional]
1084       *
1085       * @return  string  A store id.
1086       *
1087       * @since   2.5
1088       */
1089  	protected function getStoreId($id = '', $page = true)
1090      {
1091          // Get the query object.
1092          $query = $this->getQuery();
1093  
1094          // Add the search query state.
1095          $id .= ':' . $query->input;
1096          $id .= ':' . $query->language;
1097          $id .= ':' . $query->filter;
1098          $id .= ':' . serialize($query->filters);
1099          $id .= ':' . $query->date1;
1100          $id .= ':' . $query->date2;
1101          $id .= ':' . $query->when1;
1102          $id .= ':' . $query->when2;
1103  
1104          if ($page)
1105          {
1106              // Add the list state for page specific data.
1107              $id .= ':' . $this->getState('list.start');
1108              $id .= ':' . $this->getState('list.limit');
1109              $id .= ':' . $this->getState('list.ordering');
1110              $id .= ':' . $this->getState('list.direction');
1111          }
1112  
1113          return parent::getStoreId($id);
1114      }
1115  
1116      /**
1117       * Method to auto-populate the model state.  Calling getState in this method will result in recursion.
1118       *
1119       * @param   string  $ordering   An optional ordering field. [optional]
1120       * @param   string  $direction  An optional direction. [optional]
1121       *
1122       * @return  void
1123       *
1124       * @since   2.5
1125       */
1126  	protected function populateState($ordering = null, $direction = null)
1127      {
1128          // Get the configuration options.
1129          $app = JFactory::getApplication();
1130          $input = $app->input;
1131          $params = $app->getParams();
1132          $user = JFactory::getUser();
1133          $filter = JFilterInput::getInstance();
1134  
1135          $this->setState('filter.language', $app->getLanguageFilter());
1136  
1137          // Setup the stemmer.
1138          if ($params->get('stem', 1) && $params->get('stemmer', 'porter_en'))
1139          {
1140              FinderIndexerHelper::$stemmer = FinderIndexerStemmer::getInstance($params->get('stemmer', 'porter_en'));
1141          }
1142  
1143          // Initialize variables.
1144          $request = $input->request;
1145          $options = array();
1146  
1147          // Get the query string.
1148          $options['input'] = !is_null($request->get('q')) ? $request->get('q', '', 'string') : $params->get('q');
1149          $options['input'] = $filter->clean($options['input'], 'string');
1150  
1151          // Get the empty query setting.
1152          $options['empty'] = $params->get('allow_empty_query', 0);
1153  
1154          // Get the query language.
1155          $options['language'] = !is_null($request->get('l')) ? $request->get('l', '', 'cmd') : $params->get('l');
1156          $options['language'] = $filter->clean($options['language'], 'cmd');
1157  
1158          // Get the static taxonomy filters.
1159          $options['filter'] = !is_null($request->get('f')) ? $request->get('f', '', 'int') : $params->get('f');
1160          $options['filter'] = $filter->clean($options['filter'], 'int');
1161  
1162          // Get the dynamic taxonomy filters.
1163          $options['filters'] = !is_null($request->get('t')) ? $request->get('t', '', 'array') : $params->get('t');
1164          $options['filters'] = $filter->clean($options['filters'], 'array');
1165          JArrayHelper::toInteger($options['filters']);
1166  
1167          // Get the start date and start date modifier filters.
1168          $options['date1'] = !is_null($request->get('d1')) ? $request->get('d1', '', 'string') : $params->get('d1');
1169          $options['date1'] = $filter->clean($options['date1'], 'string');
1170          $options['when1'] = !is_null($request->get('w1')) ? $request->get('w1', '', 'string') : $params->get('w1');
1171          $options['when1'] = $filter->clean($options['when1'], 'string');
1172  
1173          // Get the end date and end date modifier filters.
1174          $options['date2'] = !is_null($request->get('d2')) ? $request->get('d2', '', 'string') : $params->get('d2');
1175          $options['date2'] = $filter->clean($options['date2'], 'string');
1176          $options['when2'] = !is_null($request->get('w2')) ? $request->get('w2', '', 'string') : $params->get('w2');
1177          $options['when2'] = $filter->clean($options['when2'], 'string');
1178  
1179          // Load the query object.
1180          $this->query = new FinderIndexerQuery($options);
1181  
1182          // Load the query token data.
1183          $this->excludedTerms = $this->query->getExcludedTermIds();
1184          $this->includedTerms = $this->query->getIncludedTermIds();
1185          $this->requiredTerms = $this->query->getRequiredTermIds();
1186  
1187          // Load the list state.
1188          $this->setState('list.start', $input->get('limitstart', 0, 'int'));
1189          $this->setState('list.limit', $input->get('limit', $app->getCfg('list_limit', 20), 'int'));
1190  
1191          // Load the sort ordering.
1192          $order = $params->get('sort_order', 'relevance');
1193          switch ($order)
1194          {
1195              case 'date':
1196                  $this->setState('list.ordering', 'l.start_date');
1197                  break;
1198  
1199              case 'price':
1200                  $this->setState('list.ordering', 'l.list_price');
1201                  break;
1202  
1203              default:
1204              case ($order == 'relevance' && !empty($this->includedTerms)):
1205                  $this->setState('list.ordering', 'm.weight');
1206                  break;
1207          }
1208  
1209          // Load the sort direction.
1210          $dirn = $params->get('sort_direction', 'desc');
1211          switch ($dirn) {
1212              case 'asc':
1213                  $this->setState('list.direction', 'ASC');
1214                  break;
1215  
1216              default:
1217              case 'desc':
1218                  $this->setState('list.direction', 'DESC');
1219                  break;
1220          }
1221  
1222          // Set the match limit.
1223          $this->setState('match.limit', 1000);
1224  
1225          // Load the parameters.
1226          $this->setState('params', $params);
1227  
1228          // Load the user state.
1229          $this->setState('user.id', (int) $user->get('id'));
1230          $this->setState('user.groups', $user->getAuthorisedViewLevels());
1231      }
1232  
1233      /**
1234       * Method to retrieve data from cache.
1235       *
1236       * @param   string   $id          The cache store id.
1237       * @param   boolean  $persistent  Flag to enable the use of external cache. [optional]
1238       *
1239       * @return  mixed  The cached data if found, null otherwise.
1240       *
1241       * @since   2.5
1242       */
1243  	protected function retrieve($id, $persistent = true)
1244      {
1245          $data = null;
1246  
1247          // Use the internal cache if possible.
1248          if (isset($this->cache[$id]))
1249          {
1250              return $this->cache[$id];
1251          }
1252  
1253          // Use the external cache if data is persistent.
1254          if ($persistent)
1255          {
1256              $data = JFactory::getCache($this->context, 'output')->get($id);
1257              $data = $data ? unserialize($data) : null;
1258          }
1259  
1260          // Store the data in internal cache.
1261          if ($data)
1262          {
1263              $this->cache[$id] = $data;
1264          }
1265  
1266          return $data;
1267      }
1268  
1269      /**
1270       * Method to store data in cache.
1271       *
1272       * @param   string   $id          The cache store id.
1273       * @param   mixed    $data        The data to cache.
1274       * @param   boolean  $persistent  Flag to enable the use of external cache. [optional]
1275       *
1276       * @return  boolean  True on success, false on failure.
1277       *
1278       * @since   2.5
1279       */
1280  	protected function store($id, $data, $persistent = true)
1281      {
1282          // Store the data in internal cache.
1283          $this->cache[$id] = $data;
1284  
1285          // Store the data in external cache if data is persistent.
1286          if ($persistent)
1287          {
1288              return JFactory::getCache($this->context, 'output')->store(serialize($data), $id);
1289          }
1290  
1291          return true;
1292      }
1293  }


Generated: Tue Apr 3 11:40:28 2012 Cross-referenced by PHPXref 0.7.1