Subversion Repositories web.creative

Rev

Blame | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire Page Paths
 *
 * Keeps a cache of page paths to improve performance and 
 * make paths more queryable by selectors.
 *
 * 
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 * 
 *
 */

class PagePaths extends WireData implements Module {

  public static function getModuleInfo() {
    return array(
      'title' => 'Page Paths', 
      'version' => 1, 
      'summary' => "Enables page paths/urls to be queryable by selectors. Also offers potential for improved load performance. Builds an index at install (may take time on a large site). Currently supports only single languages sites.",
      'singular' => true, 
      'autoload' => true, 
      );
  }

  /**
   * Table created by this module
   *
   */
  const dbTableName = 'pages_paths';

  /**
   * @var Languages|false
   *
   */
  protected $languages = null;

  /**
   * Initialize the hooks
   *
   */
  public function init() {
    $this->pages->addHook('moved', $this, 'hookPageMoved'); 
    $this->pages->addHook('renamed', $this, 'hookPageMoved'); 
    $this->pages->addHook('added', $this, 'hookPageMoved'); 
    $this->pages->addHook('deleted', $this, 'hookPageDeleted');
  }
  
  public function ready() {
    $page = $this->wire('page');
    if($page->template == 'admin' && $page->name == 'module') {
      $this->wire('modules')->addHookAfter('refresh', $this, 'hookModulesRefresh');
    }
  }

  /**
   * Returns Languages object or false if not available
   *
   * @return Languages|null
   *
   */
  public function getLanguages() {
    if(!is_null($this->languages)) return $this->languages;
    $languages = $this->wire('languages');
    if(!$languages) return null;
    if(!$this->wire('modules')->isInstalled('LanguageSupportPageNames')) {
      $this->languages = false;
    } else {
      $this->languages = $this->wire('languages');
    }
    return $this->languages;
  }

  /**
   * Hook to ProcessModule::refresh
   * 
   * @param HookEvent $event
   * 
   */
  public function hookModulesRefresh(HookEvent $event) {
    if($event) {} // ignore
    if($this->getLanguages()) {
      $this->wire('session')->warning(
        $this->_('Please uninstall the Core > PagePaths module (it is not compatible with LanguageSupportPageNames)')
      );
    }
  }

  /**
   * Hook called when a page is moved or renamed
   * 
   * @param HookEvent $event
   *
   */
  public function hookPageMoved(HookEvent $event) {
    $page = $event->arguments[0];
    $this->updatePagePath($page->id, $page->path); 
  }


  /**
   * When a page is deleted
   * 
   * @param HookEvent $event
   *
   */
  public function hookPageDeleted(HookEvent $event) {
    $page = $event->arguments[0];
    $database = $this->wire('database');
    $query = $database->prepare("DELETE FROM " . self::dbTableName . " WHERE pages_id=:pages_id"); 
    $query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
    $query->execute();
  }

  /**
   * Given a page ID, return the page path, NULL if not found, or boolean false if cannot be determined.
   *
   * @param int $id
   * @return string|null|false
   *
   */
  public function getPath($id) {
    if($this->getLanguages()) return false; // we do not support multi-language yet for this module
    $table = self::dbTableName;
    $database = $this->wire('database');
    $query = $database->prepare("SELECT path FROM `$table` WHERE pages_id=:pages_id"); 
    $query->bindValue(":pages_id", $id, \PDO::PARAM_INT);
    $query->execute();
    if(!$query->rowCount()) return null;
    $path = $query->fetchColumn();
    $path = strlen($path) ? $this->wire('sanitizer')->pagePathName("/$path/", Sanitizer::toUTF8) : "/";
    return $path;
  }

  /**
   * Given a page path, return the page ID or NULL if not found.
   *
   * @param string $path
   * @return int|null
   *
   */
  public function getID($path) {
    $table = self::dbTableName;
    $database = $this->wire('database');
    $path = $this->wire('sanitizer')->pagePathName($path, Sanitizer::toAscii);
    $path = trim($path, '/');
    $query = $database->prepare("SELECT pages_id FROM $table WHERE path=:path");
    $query->bindValue(":path", $path); 
    $query->execute();
    if(!$query->rowCount()) return null;
    $id = $query->fetchColumn(); 
    return (int) $id;
  }

  /**
   * Perform a path match for use by PageFinder
   *
   * @param DatabaseQuerySelect $query
   * @param Selector $selector
   * @throws PageFinderSyntaxException
   *
   */
  public function getMatchQuery(DatabaseQuerySelect $query, Selector $selector) {

    static $n = 0;
    $n++;
    $table = self::dbTableName;
    $alias = "$table$n";
    $value = $selector->value;
    // $joinType = $selector->not ? 'leftjoin' : 'join';

    $query->join("$table AS $alias ON pages.id=$alias.pages_id"); 

    if(in_array($selector->operator, array('=', '!=', '<>', '>', '<', '>=', '<='))) {
      if(!is_array($value)) $value = array($value);
      $where = '';
      foreach($value as $path) {
        if($where) $where .= $selector->not ? " AND " : " OR ";
        $path = $this->wire('sanitizer')->pagePathName($path, Sanitizer::toAscii);
        $path = $this->wire('database')->escapeStr(trim($path, '/')); 
        $where .= ($selector->not ? "NOT " : "") . "$alias.path{$selector->operator}'$path'";
      }
      $query->where("($where)");

    } else {
      if(is_array($value)) {
        $error = "Multi value using '|' is not supported with path/url and '$selector->operator' operator";
        throw new PageFinderSyntaxException($error);
      }
      if($selector->not) {
        $error = "NOT mode isn't yet supported with path/url and '$selector->operator' operator";
        throw new PageFinderSyntaxException($error);
      }
      /** @var DatabaseQuerySelectFulltext $ft */
      $ft = $this->wire(new DatabaseQuerySelectFulltext($query));
      $ft->match($alias, 'path', $selector->operator, trim($value, '/'));
    }
  }

  /**
   * Updates path for $page and all children
   *
   * @param int $id 
   * @param string $path
   * @param bool $hasChildren Omit if true or unknown
   * @param int $level Recursion level, you should omit this param
   * @return int Number of paths updated
   *
   */
  protected function updatePagePath($id, $path, $hasChildren = true, $level = 0) {

    $table = self::dbTableName;
    $id = (int) $id;
    $database = $this->wire('database');
    $path = $this->wire('sanitizer')->pagePathName($path, Sanitizer::toAscii);
    $path = trim($path, '/');
    $_path = $database->escapeStr($path);
    $numUpdated = 1;

    $sql =  "INSERT INTO $table (pages_id, path) VALUES(:id, :path) " . 
        "ON DUPLICATE KEY UPDATE pages_id=VALUES(pages_id), path=VALUES(path)"; 
  
    $query = $database->prepare($sql);
    $query->bindValue(":id", $id, \PDO::PARAM_INT); 
    $query->bindValue(":path", $_path); 
    $query->execute();

    if($hasChildren) {

      $sql =  "SELECT pages.id, pages.name, COUNT(children.id) FROM pages " . 
          "LEFT JOIN pages AS children ON children.id=pages.parent_id " . 
          "WHERE pages.parent_id=:id " . 
          "GROUP BY pages.id ";
    
      $query = $database->prepare($sql);
      $query->bindValue(":id", $id, \PDO::PARAM_INT);
      $query->execute();  
      
      while($row = $query->fetch(\PDO::FETCH_NUM)) {
        list($id, $name, $numChildren) = $row;
        $numUpdated += $this->updatePagePath($id, "$path/$name", $numChildren > 0, $level+1);
      }
    }

    if(!$level) $this->message(sprintf($this->_n('Updated %d path', 'Updated %d paths', $numUpdated), $numUpdated)); 

    return $numUpdated;
  }

  /**
   * Install the module
   *
   */
  public function ___install() {
    
    $table = self::dbTableName;
    $database = $this->wire('database');
    $engine = $this->wire('config')->dbEngine;
    $charset = $this->wire('config')->dbCharset;

    $database->query("DROP TABLE IF EXISTS $table"); 

    $sql =  "CREATE TABLE $table (" . 
        "pages_id int(10) unsigned NOT NULL, " . 
        "path text CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, " . 
        "PRIMARY KEY pages_id (pages_id), " .
        "UNIQUE KEY path (path(500)), " . 
        "FULLTEXT KEY path_fulltext (path)" . 
        ") ENGINE=$engine DEFAULT CHARSET=$charset";

    $database->query($sql); 
    $numUpdated = $this->updatePagePath(1, '/');
    if($numUpdated) {} // ignore
  }

  /**
   * Uninstall the module
   *
   */
  public function ___uninstall() {
    $this->wire('database')->query("DROP TABLE " . self::dbTableName); 
  }

}