Query.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4Cms/
  • Record/
  • Query.php
  • View
  • Commits
  • Open Download .zip Download (23 KB)
<?php
/**
 * Provides a container for query options suitable for passing to fetchAll.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 * @todo        add support for limiting returned fields.
 */
class P4Cms_Record_Query
{
    const QUERY_INCLUDE_DELETED = 'includeDeleted';
    const QUERY_RECORD_CLASS    = 'recordClass';
    const QUERY_LIMIT_FIELDS    = 'limitFields';
    const QUERY_MAX_DEPTH       = 'maxDepth';
    const QUERY_MAX_ROWS        = 'maxRows';
    const QUERY_PATHS           = 'paths';
    const QUERY_IDS             = 'ids';

    const SORT_DATE             = '#REdate';
    const SORT_HEAD_REV         = '#RErev';
    const SORT_HAVE_REV         = '#NEhrev';
    const SORT_FILE_TYPE        = '#NEtype';
    const SORT_FILE_SIZE        = '#REsize';
    const SORT_ASCENDING        = 'a';
    const SORT_DESCENDING       = 'd';

    const RECORD_BASE_CLASS     = 'P4Cms_Record';

    protected $_options         = null;
    protected $_query           = null;
    protected $_recordClass     = null;

    /**
     * Constructor that accepts an array of query options
     *
     * @param array $options Optional array of options to populate
     */
    public function __construct($options = array())
    {
        $this->reset();

        // some options that are valid for both this and file query as well,
        // may not be directly passed to the file query constructor
        $fileQueryOptions = array_diff($options, array(static::QUERY_LIMIT_FIELDS));
        $this->_query     = new P4_File_Query($fileQueryOptions);

        if (isset($options) and is_array($options)) {
            foreach ($options as $key => $value) {
                if (array_key_exists($key, $this->_options)) {
                    $method = 'set'. ucfirst($key);
                    $this->$method($value);
                }
            }
        }
    }

    /**
     * Creates and returns a new Query class. Useful for working
     * around PHP's lack of new chaining.
     *
     * @param   array  $options  Optional array of options to populate for new Query class
     * @return  P4Cms_Record_Query
     */
    public static function create($options = array())
    {
        return new static($options);
    }

    /**
     * Reset the current query object to its default state.
     *
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function reset()
    {
        $this->_options = array(
            static::QUERY_INCLUDE_DELETED   => false,
            static::QUERY_RECORD_CLASS      => null,
            static::QUERY_LIMIT_FIELDS      => null,
            static::QUERY_MAX_DEPTH         => null,
            static::QUERY_MAX_ROWS          => null,
            static::QUERY_PATHS             => null,
            static::QUERY_IDS               => null
        );
        $this->_query = new P4_File_Query;
        return $this;
    }

    /**
     * Provide all of the current options as an array.
     *
     * @return  array  The current query options as an array.
     */
    public function toArray()
    {
        $array = $this->_query->toArray();

        // hide filespecs option (superseded by paths)
        unset($array[P4_File_Query::QUERY_FILESPECS]);
        
        // hide max-files option (superseded by max-rows)
        unset($array[P4_File_Query::QUERY_MAX_FILES]);

        return array_merge($array, $this->_options);
    }

    /**
     * Retrieve the current filter expression.
     * Null means no filtering will take place.
     *
     * @return  P4Cms_Record_Filter|null  The current filter object or null.
     */
    public function getFilter()
    {
        return $this->_query->getFilter();
    }

    /**
     * Add a filter to this query.
     *
     * @param   P4Cms_Record_Filter     $filter     the filter to add.
     * @return  P4Cms_Record_Query      provides fluent interface.
     */
    public function addFilter(P4Cms_Record_Filter $filter)
    {
        $currentFilter = $this->getFilter();
        if (!$currentFilter) {
            return $this->setFilter($filter);
        }

        $currentFilter->addSubFilter($filter);
        return $this;
    }

    /**
     * Set the "filter expression" to limit the returned set of records.
     * See 'p4 help fstat' and 'p4 help jobview' for more information on
     * the filter format. Accepts a P4Cms_Record_Filter or string for input,
     * or null to remove any filter.
     *
     * @param   string|array|P4Cms_Record_Filter|null  $filter  The desired filter expression.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function setFilter($filter = null)
    {
        if (is_string($filter) || is_array($filter)) {
            $filter = new P4Cms_Record_Filter($filter);
        }
        
        if (!$filter instanceof P4Cms_Record_Filter && !is_null($filter)) {
            throw new InvalidArgumentException(
                'Cannot set filter; argument must be a P4Cms_Record_Filter, an array, a string, or null.'
            );
        }
        $this->_query->setFilter($filter);
        return $this;
    }

    /**
     * Get the current sort field.
     * Null means default sorting will take place.
     *
     * @return  string  The current sort field, or null if not set.
     */
    public function getSortBy()
    {
        return $this->_query->getSortBy();
    }

    /**
     * Set the record field which will be used to sort results. Valid sort fields are:
     * SORT_DATE, SORT_HEAD_REV, SORT_HAVE_REV, SORT_FILE_TYPE, SORT_FILE_SIZE.
     * Specify null to receive records in the default order.
     *
     * @param   array|string|null   $sortBy   An array of fields or field => options, a string field,
     *                                        or default null.
     * @param   array|null          $options  Sorting options, only used when sortBy is a string.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     * @see     P4_File_Query
     */
    public function setSortBy($sortBy = null, $options = null)
    {
        $this->_query->setSortBy($sortBy, $options);
        return $this;
    }

    /**
     * Retrieve the current reverse order flag setting.
     * True means that the sort order will be reversed.
     *
     * @return  boolean  true if the sort order will be reversed.
     */
    public function getReverseOrder()
    {
        return $this->_query->getReverseOrder();
    }

    /**
     * Set the flag indicating whether the results will be returned in reverse order.
     *
     * @param   boolean  $reverse  Set to true to reverse sort order.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function setReverseOrder($reverse = false)
    {
        $this->_query->setReverseOrder($reverse);
        return $this;
    }

    /**
     * Retrieve the flag indicating whether deleted files will be included in results.
     * True means deleted files will be included.
     *
     * @return  boolean  True indicates deleted files included.
     */
    public function getIncludeDeleted()
    {
        return $this->_options[static::QUERY_INCLUDE_DELETED];
    }

    /**
     * Set the flag indicating whether deleted files will be included in results.
     * True means deleted files should be included.
     *
     * @param   boolean  $include  Flag to include deleted files.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function setIncludeDeleted($include = false)
    {
        $this->_options[static::QUERY_INCLUDE_DELETED] = (bool) $include;
        return $this;
    }

    /**
     * Return the starting row for matching records.
     * Null means all matching records will be returned.
     *
     * @return  int|null  The starting row.
     */
    public function getStartRow()
    {
        return $this->_query->getStartRow();
    }

    /**
     * Set the starting row to return from matching records,
     * or null to return all matching records.
     *
     * @param   int|null  $row   The starting row.
     * @return  P4Cms_Record_Query    provide a fluent interface.
     */
    public function setStartRow($row = null)
    {
        $this->_query->setStartRow($row);
        return $this;
    }

    /**
     * Retrieve the maximum number of records to include in results.
     * 0 or null means unlimited.
     *
     * @return  integer  The maximum number of records to include in results.
     */
    public function getMaxRows()
    {
        return $this->_query->getMaxFiles();
    }

    /**
     * Set to limit the number of matching records returned, or null
     * to return all matching records.
     *
     * @param   int|null  $max  The maximum number of records to return.
     * @return  P4_File_Query   provide a fluent interface.
     */
    public function setMaxRows($max = null)
    {
        $this->_options[static::QUERY_MAX_ROWS] = $max;
        $this->_query->setMaxFiles($max);
        return $this;
    }

    /**
     * Retrieve the list of record fields to return in server responses.
     * Null means all fields will be returned.
     *
     * @return  array|null  The current list of record fields.
     */
    public function getLimitFields()
    {
        return $this->_options[static::QUERY_LIMIT_FIELDS];
    }

    /**
     * Set the list of fields to include in the response from the server.
     *
     * Record limitFields are converted to the file fields (i.e. file attributes) when
     * this query is converted to the file query as we need to know the record class to
     * determine id and file content fields that have to be skipped.
     *
     * @param   string|array|null   $fields     The list of desired fields. Supply a string to specify
     *                                          one field, or supply a null to retrieve all fields.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function setLimitFields($fields = array())
    {
        $this->_options[static::QUERY_LIMIT_FIELDS] = $fields;
        return $this;
    }

    /**
     * Retrieve the maximum path depth of records to include in results.
     * 0 means include records only at the current depth. Null means
     * unlimited.
     *
     * @return  integer  The maximum depth of records to include in results.
     */
    public function getMaxDepth()
    {
        return $this->_options[static::QUERY_MAX_DEPTH];
    }

    /**
     * Set the maximum path depth for records to be included in results.
     * 0 means include records only at the current depth. Null means
     * unlimited.
     *
     * @param   integer  $depth  The maximum depth of records to include in results.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function setMaxDepth($depth = null)
    {
        // accept numeric string values, for convenience.
        if (is_string($depth)) {
            $depth = (int) $depth;
        }
        if (isset($depth) and (!is_integer($depth) or $depth < 0)) {
            throw new InvalidArgumentException(
                'Cannot set maximum depth; argument must be a non-negative integer or null.'
            );
        }

        $this->_options[static::QUERY_MAX_DEPTH] = $depth;
        return $this;
    }

    /**
     * Retrieve the IDs to be used to fetch records.
     *
     * @return  array|null  The list of ids to be used to fetch records.
     */
    public function getIds()
    {
        return $this->_options[static::QUERY_IDS];
    }

    /**
     * Set the list of IDs to be used to fetch records.
     *
     * @param   array|null  $ids    The IDs to be used to fetch records.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function setIds($ids = null)
    {
        if (!is_array($ids) && !is_null($ids)) {
            throw new InvalidArgumentException("IDs should be an array or null");
        }

        $this->_options[static::QUERY_IDS] = $ids;
        return $this;
    }

    /**
     * Add a single ID to the list of IDs to be used to fetch records.
     *
     * @param   string|int  $id         An ID to be added.
     * @return  P4Cms_Record_Query      provide a fluent interface.
     */
    public function addId($id)
    {
        if (!is_string($id) && !is_int($id)) {
            throw new InvalidArgumentException('Cannot add ID; argument must be a string or int.');
        }

        return $this->addIds(array($id));
    }

    /**
     * Add IDs to the list of IDs to be used to fetch records.
     *
     * @param   array   $ids            set of string or int IDs to be added.
     * @return  P4Cms_Record_Query      provide a fluent interface.
     */
    public function addIds($ids)
    {
        if (!is_array($ids)) {
            throw new InvalidArgumentException('Cannot add IDs; argument must be an array.');
        }

        $this->setIds(array_merge($this->getIds() ?: array(), $ids));
        
        return $this;
    }

    /**
     * Remove a single ID from the list to be used to fetch records.
     *
     * @param   string|int  $id     ID to remove from list
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function removeId($id)
    {
        if (!is_string($id) && !is_int($id)) {
            throw new InvalidArgumentException('Cannot remove id; argument must be a string or int.');
        }

        return $this->removeIds(array($id));
    }

    /**
     * Remove IDs from the list to be used to fetch records.
     *
     * @param   array  $ids         The IDs to be removed.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function removeIds($ids = array())
    {
        if (!is_array($ids)) {
            throw new InvalidArgumentException('Cannot remove IDs; argument must be an array.');
        }

        $currentIds = $this->getIds() ?: array();
        foreach ($ids as $id) {
            $index = array_search($id, $currentIds);
            if ($index !== false) {
                unset($currentIds[$index]);
            }
        }
        $this->setIds($currentIds);

        return $this;
    }

    /**
     * Retrieve the paths to be used to fetch records.
     *
     * @return  array|null  The list of paths to be used to fetch records.
     */
    public function getPaths()
    {
        return $this->_query->getFilespecs();
    }

    /**
     * Set the list of paths to be used to fetch records.
     *
     * @param   array|null  $paths  The paths to be used to fetch records.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function setPaths($paths = null)
    {
        $this->_query->setFilespecs($paths);
        $this->_options[static::QUERY_PATHS] = $paths;
        return $this;
    }

    /**
     * Add a single path to the list of paths to be used to fetch records.
     *
     * @param   string  $path           A path to be added.
     * @param   bool    $intersect      optional - defaults to false - intersect with existing paths.
     * @return  P4Cms_Record_Query      provide a fluent interface.
     */
    public function addPath($path, $intersect = false)
    {
        if (!isset($path) or !is_string($path)) {
            throw new InvalidArgumentException('Cannot add path; argument must be a string.');
        }

        return $this->addPaths(array($path), $intersect);
    }

    /**
     * Add paths to the list of paths to be used to fetch records.
     *
     * @param   array   $paths          set of paths to be added.
     * @param   bool    $intersect      optional - defaults to false - intersect with existing paths.
     * @return  P4Cms_Record_Query      provide a fluent interface.
     */
    public function addPaths($paths = array(), $intersect = false)
    {
        if (!isset($paths) or !is_array($paths)) {
            throw new InvalidArgumentException('Cannot add paths; argument must be an array.');
        }

        // if paths are currently null, no need to combine.
        if ($this->getPaths() === null) {
            $this->setPaths($paths);
            return $this;
        }

        // combine arrays appropriately
        if ($intersect) {
            $this->setPaths(array_intersect($this->getPaths(), $paths));
        } else {
            $this->setPaths(array_merge($this->getPaths(), $paths));
        }

        return $this;
    }

    /**
     * Remove a single path from the list to be used to fetch records.
     * 
     * @param   string  $path       path to remove from list
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function removePath($path)
    {
        if (!is_string($path)) {
            throw new InvalidArgumentException('Cannot remove path; argument must be a string.');
        }

        return $this->removePaths(array($path));
    }

    /**
     * Remove paths from the list to be used to fetch records.
     *
     * @param   array  $paths  The paths to be removed.
     * @return  P4Cms_Record_Query  provide a fluent interface.
     */
    public function removePaths($paths = array())
    {
        if (!isset($paths) or !is_array($paths)) {
            throw new InvalidArgumentException('Cannot remove paths; argument must be an array.');
        }

        $currentPaths = $this->getPaths() ?: array();
        foreach ($paths as $path) {
            $index = array_search($path, $currentPaths);
            if ($index !== false) {
                array_splice($currentPaths, $index, 1);
            }
        }
        $this->setPaths($currentPaths);

        return $this;
    }

    /**
     * Set the record class to use when preparing the query for execution.
     * If no record class is set uses the base record class.
     * 
     * @param   object|string|null  $class  instance or name of a record class - null to clear.
     * @return  P4Cms_Record_Query          provides fluent interface. 
     * @throws  InvalidArgumentException    if the given class is not a valid record class.
     */
    public function setRecordClass($class)
    {
        $class = is_object($class) ? get_class($object) : $class;
        
        // only validate if class is not null.
        if ($class) {
            $this->_validateRecordClass($class);
        }

        $this->_recordClass = $class;

        return $this;
    }
    
    /**
     * Get the record class to use when preparing the query.
     * If no record class has been set, returns the base record class.
     * 
     * @return  string  the name of the record class to use.
     */
    public function getRecordClass()
    {
        return $this->_recordClass ?: static::RECORD_BASE_CLASS;
    }
    
    /**
     * Compose a P4_File_Query to be used in P4_File::fetchAll() calls.
     *
     * @param   string                  $recordClass    optional - a specific record class to influence storage paths.
     * @param   P4Cms_Record_Adapter    $adapter        optional - storage adapter to use.
     * @return  P4_File_Query           A query object for use with P4_File::fetchAll()
     */
    public function toFileQuery($recordClass = null, P4Cms_Record_Adapter $adapter = null)
    {
        // validate record class.
        $recordClass = $recordClass ?: $this->getRecordClass();
        $this->_validateRecordClass($recordClass);

        // if no adapter given, use default.
        $adapter = $adapter ?: $recordClass::getDefaultAdapter();

        // determine location of records in depot.
        $depotStoragePath = $recordClass::getDepotStoragePath($adapter);
        $storagePath      = $recordClass::getStoragePath($adapter);

        // clone the current query options, so that later on we
        // don't have to undo the modifications below.
        $query = clone $this->_query;

        // update the filter to remove deleted records, unless include deleted is true
        if (!$this->getIncludeDeleted()) {
            $filter = "^headAction=...delete";
            $filter = $query->getFilter() !== null && (string) $query->getFilter() !== ''
                ? '('. $query->getFilter() .') & '. $filter
                : $filter;

            $query->setFilter($filter);
        }

        // set limit fields - we have to do it here as we need to know recordClass
        // to ignore id and file content fields
        if ($this->_options[static::QUERY_LIMIT_FIELDS]) {
            // always include depotFile field
            $limitFields = array('depotFile');

            $fields = (array) $this->_options[static::QUERY_LIMIT_FIELDS];
            foreach ($fields as $field) {
                // ignore id and file content fields
                if ($field === $recordClass::getIdField() 
                    || ($recordClass::hasFileContentField() && $field === $recordClass::getFileContentField())
                ) {
                    continue;
                }

                $limitFields[] = 'attr-' . $field;
            }

            $query->setLimitFields($limitFields);
        }

        // modify the filter to limit results by depth, if required
        if ($this->getMaxDepth() !== null) {
            // restrict depth by filtering depot paths deeper than max depth.
            $filter = '^depotFile=' . $depotStoragePath
                    . str_repeat('/*', $this->getMaxDepth() + 1) .'/...';
            $filter = $query->getFilter() !== null
                ? '('. $query->getFilter() . ') & '. $filter
                : $filter;

            $query->setFilter($filter);
        }

        $filespecs = array();

        // collect all paths, prepending storage path
        foreach ($this->getPaths() ?: array() as $path) {
            $filespecs[] = $storagePath . "/" . $path;
        }

        // collect all IDs, translating to filespec
        foreach ($this->getIds() ?: array() as $id) {
            $filespecs[] = $recordClass::idToFilespec($id, $adapter);
        }

        // do any required global touchup
        foreach ($filespecs as &$filespec) {
            if (!P4_File::hasRevspec($filespec)) {
                $filespec .= '#head';
            }
        }

        // four cases for handling path filters:
        //            null - set to entire storage path and append #head to hide pending adds.
        //     empty array - leave empty (no results desired).
        //        one path - simply set the one filespec directly.
        //  multiple paths - put paths in a temp label to limit results.
        if ($this->getPaths() === null && $this->getIds() === null) {
            $query->setFilespecs($storagePath . "/...#head");
        } else {
            switch (count($filespecs)) {
                case 0:
                case 1:
                    $query->setFilespecs($filespecs);
                    break;
                default:
                    $label = P4_Label::makeTemp(
                        array('View' => array($depotStoragePath . '/...')),
                        null,
                        $adapter->getConnection()
                    );
                    $label->tag($filespecs);
                    $query->setFilespecs($storagePath . '/...@' . $label->getId());
            }
        }

        return $query;
    }
    
    /**
     * Verify that the given class name is a valid record class.
     * 
     * @param   string  $class              the class name to verify.
     * @throws  InvalidArgumentException    if the class does not exist or is not a record. 
     */
    protected function _validateRecordClass($class) 
    {
        $base = static::RECORD_BASE_CLASS;
        
        // ensure class exists and is a valid record class.
        if (!class_exists($class) 
            || (!is_subclass_of($class, $base) && $class !== $base)
        ) {
            throw new InvalidArgumentException("Invalid record class given.");
        }
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4Cms/Record/Query.php
#1 8972 Matt Attaway Initial add of the Chronicle source code