strata-mvc/strata

View on GitHub
src/Model/CustomPostType/Query.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php

namespace Strata\Model\CustomPostType;

use Strata\Strata;
use WP_Query;

/**
 * The query is a cache of filters that will be eventually
 * sent to WP_Query. It allows chaining and object-oriented
 * manipulations of database queries.
 */
class Query
{
    /**
     * A list of WP_Query filters. Defaults to a list of published posts
     * ordered by name alphabetically unpaged.
     * @var array
     */
    protected $filters = array(
        'orderby'          => 'title',
        'order'            => 'ASC',
        'post_status'      => 'any',
        'nopaging'         => true,
        'suppress_filters' => true,

        // This field, though not used by Wordpress, will
        // allow some flexibility with combining AND and OR query
        // relations.
        'strata_relations' => array(
            'meta_query' => array("AND" => array(), "OR" => array()),
            'tax_query' => array("AND" => array(), "OR" => array()),
        ),
    );

    /**
     * @var integer Query timer
     */
    private $executionStart = 0;

    /**
     * Fetches the set matching the current state of the filters.
     * @return array An array of WP_Post objects
     */
    public function fetch()
    {
        $query = $this->query();
        return $query->posts;
    }

    /**
     * Executes the query using the current filters.
     * @return array
     */
    public function query()
    {
        $this->carryOverIncompatibleQueries();
        $this->relationsToQueries();
        return $this->executeFilteredQuery();
    }

    /**
     * Returns a custom listing based on the search
     * results.
     * @param  string $key
     * @param  string $label
     * @return array
     */
    public function listing($key, $label)
    {
        $data = array();
        foreach ($this->fetch() as $entity) {
            $data[$entity->{$key}] = $entity->{$label};
        }
        return $data;
    }

    /**
     * Adds a 'data_query' condition
     * @param  array $dateQuery
     * @return Query
     */
    public function date($dateQuery)
    {
        $this->filters['date_query'] = $dateQuery;
        return $this;
    }

    /**
     * Adds an 'orderby' condition.
     * @param  string $orderBy
     * @return Query
     */
    public function orderby($orderBy)
    {
        $this->filters['orderby'] = $orderBy;
        return $this;
    }

    /**
     * Adds an 'order' condition
     * @param  string $order
     * @return Query
     */
    public function direction($order)
    {
        $this->filters['order'] = $order;
        return $this;
    }

    /**
     * Adds a 'post_type' condition
     * @param  string $type
     * @return Query
     */
    public function type($type = null)
    {
        if (is_null($type)) {
            unset($this->filters['post_type']);
        } else {
            $this->filters['post_type'] = $type;
        }
        return $this;
    }

    /**
     * Adds a 'post_status' condition
     * @param  string $status
     * @return Query
     */
    public function status($status = null)
    {
        if (is_null($status)) {
            unset($this->filters['post_status']);
        } else {
            $this->filters['post_status'] = $status;
        }

        return $this;
    }

    /**
     * Adds any type of condition.
     * @param  string $field
     * @param  string $value
     * @return Query
     */
    public function where($field, $value)
    {
        if (strtolower($field) === "meta_query") {
            return $this->metaWhere($field, $value);
        } elseif (strtolower($field) === "tax_query") {
            return $this->taxWhere($field, $value);
        }

        $this->filters[$field] = $value;
        return $this;
    }

    /**
     * Allows for branching of query conditions.
     * @param  string $field
     * @param  string $value
     * @return Query
     */
    public function orWhere($field, $value)
    {
        if (strtolower($field) === "meta_query") {
            return $this->metaWhere($field, $value, 'OR');
        } elseif (strtolower($field) === "tax_query") {
            return $this->taxWhere($field, $value, 'OR');
        }

        $this->filters[$field] = $value;
        return $this;
    }

    /**
     * Paginates the current query using paginate_links()
     * @param  array  $config
     * @return string
     */
    public function paginate($config = array())
    {
        $query = $this->query();
        $count = $query->post_count;
        $postsPerPage = $query->query_vars['posts_per_page'];
        $offset = (int)get_query_var('paged', 1);

        $this->limit($postsPerPage);

        if ($offset > 1) {
            $this->offset(($offset-1) * $postsPerPage);
        }

        if ($count > $postsPerPage) {
            $config +=  array(
                'mid-size' => 1,
                'current' => $offset === 0 ? 1 : $offset,
                'total' => ceil($count / $postsPerPage),
                'prev_next' => true,
                'prev_text' => __('Previous', 'strata'),
                'next_text' => __('Next', 'strata')
            );

            return paginate_links($config);
        }

        return "";
    }

    /**
     * Sets the 'post_per_page' condition on the current query.
     * @param  integer $qty
     * @return Query
     */
    public function limit($qty)
    {
        $this->filters['posts_per_page']   = $qty;
        $this->filters['nopaging']         = false;
        return $this;
    }

    /**
     * Sets the 'offset' condition on the current query.
     * @param  integer $idx
     * @return Query
     */
    public function offset($idx)
    {
        $this->filters['offset']   = $idx;
        return $this;
    }

    /**
     * Gets the list of active filters.
     * @return array
     */
    public function getFilters()
    {
        return (array)$this->filters;
    }

    /**
     * Applies multiple filters to the current query
     * in one pass.
     * @param  array $filters
     * @return Query
     */
    public function applyFilters($filters)
    {
        foreach ($filters as $key => $value) {
            $this->filters[$key] = $value;
        }

        return $this;
    }

    /**
     * Executes the query based on the current filters.
     * @return mixed The result of the "new WP_Query()"
     */
    protected function executeFilteredQuery()
    {
        $this->logQueryStart();
        $result = new WP_Query($this->getFilters());
        $this->logQueryCompletion($result->request);
        return $result;
    }

    /**
     * If previous meta queries are set, they may prevent the combination of
     * both 'AND' and 'OR' relation. Carries over the exclusive fields as a list of
     * IDs to apply in a Query with a new meta_query relation type.
     * @return Query
     */
    protected function carryOverIncompatibleQueries()
    {
        foreach ($this->filters['strata_relations'] as $queryType => $queryDetails) {
            if ($this->hasRelationQuery($queryType, 'AND') && $this->hasRelationQuery($queryType, 'OR')) {
                $this->andRelationToPostIn($queryType);
            }
        }

        return $this;
    }

    /**
     * Interprets the branching queries into resultsets that will be
     * applied to the main query
     * @return Query
     */
    protected function relationsToQueries()
    {
        foreach ($this->filters['strata_relations'] as $queryType => $queryDetails) {
            // At this point, there should only be exclusive AND or OR query groups
            $metaQueries = null;
            $relationTypes = array_keys($queryDetails);
            foreach ($relationTypes as $relationType) {
                if ($this->hasRelationQuery($queryType, $relationType)) {
                    $metaQueries = $this->getRelationQuery($queryType, $relationType);
                    $this->setQueryRelation($queryType, $relationType);
                    $this->resetQueryRelation($queryType, $relationType);
                }
            }

            if (!is_null($metaQueries)) {
                foreach ($metaQueries as $query) {
                    $this->addRelationQuery($queryType, $query);
                }
            }
        }

        return $this;
    }

    /**
     * Takes in a resultsets and maps it to a 'post__in'
     * condition to port the results to the main query.
     * @param  string $query_type
     * @return Query
     */
    protected function andRelationToPostIn($query_type)
    {
        $andQuery = new Query();

        // Copy the current Query but remove the OR conditions.
        // They will be looked up as this instance goes on with
        // the process.
        $filterCopy = $this->filters;

        unset($filterCopy["limit"]);
        unset($filterCopy["posts_per_page"]);
        $filterCopy[$query_type]["OR"] = array();
        $filterCopy['nopaging'] = true;

        $andQuery->applyFilters($filterCopy);
        $andQuery->resetQueryRelation($query_type, 'OR');

        // This forces the AND relationships to be loaded before
        // comparing the or parameters
        $andIds = $andQuery->listing("ID", "ID");

        $this->where('post__in', array_values($andIds));
        $this->resetQueryRelation($query_type, 'AND');

        return $this;
    }

    /**
     * Removes the query relationships that matter only to Strata.
     * @param  string $which
     * @param  string $type
     * @return Query
     */
    public function resetQueryRelation($which, $type)
    {
        $this->filters['strata_relations'][$which][$type] = array();
        return $this;
    }

    /**
     * Fetches a special relation query
     * @param  string $which
     * @param  string $type
     * @return Query
     */
    public function getRelationQuery($which, $type)
    {
        return $this->filters['strata_relations'][$which][$type];
    }

    /**
     * Confirms if there is a special filter for a relation query.
     * @param  string  $which
     * @param  straing  $type
     * @return boolean
     */
    public function hasRelationQuery($which, $type)
    {
        return count($this->getRelationQuery($which, $type)) > 0;
    }

    /**
     * Sets a new relationship query.
     * @param string $type
     * @param string $which
     * @return Query
     */
    public function setQueryRelation($type, $which)
    {
        $this->prepareRelationFilter($type);
        $this->filters[$type]['relation'] = $which;
        return $this;
    }

    /**
     * Prepopulates a special relation query
     * @param  string $type
     * @return Query
     */
    public function prepareRelationFilter($type)
    {
        if (!array_key_exists($type, $this->filters) || !is_array($this->filters[$type])) {
            $this->filters[$type] = array();
        }
        return $this;
    }

    /**
     * This does not actually set the meta_query parameter. It is
     * used to build a more complex AND/OR logical fetch.
     * @return Query
     */
    protected function metaWhere($field, $value, $compare = 'AND')
    {
        $this->filters['strata_relations']['meta_query'][$compare][] = $value;
        return $this;
    }

    /**
     * This does not actually set the meta_query parameter. It is
     * used to build a more complex AND/OR logical fetch.
     * @return Query
     */
    protected function taxWhere($field, $value, $compare = 'AND')
    {
        $this->filters['strata_relations']['tax_query'][$compare][] = $value;
        return $this;
    }

    /**
     * Adds a new relation filter
     * @param string $type
     * @param string $value
     * @return Query
     */
    private function addRelationQuery($type, $value)
    {
        $this->prepareRelationFilter($type);
        $this->filters[$type][] = $value;
        return $this;
    }

    /**
     * Sets the beginning microtime of when the query has begun reading the
     * database.
     */
    protected function logQueryStart()
    {
        $this->executionStart = microtime(true);
    }

    /**
     * Logs the completion time of the database query.
     * @param  string $sql
     */
    protected function logQueryCompletion($sql)
    {
        $executionTime = microtime(true) - $this->executionStart;
        $timer = sprintf(" (Done in %s seconds)", round($executionTime, 4));

        $oneLine = preg_replace('/\s+/', ' ', trim($sql));
        Strata::app()->log($oneLine . $timer, "<magenta>Query</magenta>");
    }
}