- <?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 store filter internally as a filter - don't cast to string.
- */
-
- namespace P4\File;
-
- use P4;
- use P4\Spec\Change;
-
- class Query
- {
- const QUERY_FILTER = 'filter';
- const QUERY_SORT_BY = 'sortBy';
- const QUERY_SORT_REVERSE = 'reverseOrder';
- const QUERY_LIMIT_FIELDS = 'limitFields';
- const QUERY_LIMIT_TO_CHANGELIST = 'limitToChangelist';
- const QUERY_LIMIT_TO_NEEDS_RESOLVE = 'limitToNeedsResolve';
- const QUERY_LIMIT_TO_OPENED = 'limitToOpened';
- const QUERY_MAX_FILES = 'maxFiles';
- const QUERY_START_ROW = 'startRow';
- const QUERY_FILESPECS = 'filespecs';
-
- const ADD_MODE_AND = 'add';
- const ADD_MODE_OR = 'or';
-
- 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 SORT_CLAUSE_COUNT_2010_2 = 2;
-
- protected $options = null;
- protected $validFieldNames = null;
-
- /**
- * Constructor that accepts an array of options for immediate population.
- * Note that options are validated and can throw exceptions.
- *
- * @param array $options query options
- */
- public function __construct($options = array())
- {
- $this->reset();
- if (isset($options) && 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 query options
- * @return Query
- */
- public static function create($options = array())
- {
- return new static($options);
- }
-
- /**
- * Reset the current query object to its default state.
- *
- * @return Query provide a fluent interface.
- */
- public function reset()
- {
- $this->options = array(
- static::QUERY_FILTER => null,
- static::QUERY_SORT_BY => null,
- static::QUERY_SORT_REVERSE => false,
- static::QUERY_LIMIT_FIELDS => null,
- static::QUERY_LIMIT_TO_CHANGELIST => null,
- static::QUERY_LIMIT_TO_NEEDS_RESOLVE => false,
- static::QUERY_LIMIT_TO_OPENED => false,
- static::QUERY_MAX_FILES => null,
- static::QUERY_START_ROW => null,
- static::QUERY_FILESPECS => null,
- );
-
- $this->validFieldNames = array(
- static::SORT_DATE => true,
- static::SORT_HEAD_REV => true,
- static::SORT_HAVE_REV => true,
- static::SORT_FILE_TYPE => true,
- static::SORT_FILE_SIZE => true,
- );
-
- return $this;
- }
-
- /**
- * Provide all of the current options as an array.
- *
- * @return array The current query options as an array.
- */
- public function toArray()
- {
- return $this->options;
- }
-
- /**
- * Retrieve the current filter object.
- * Null means no filtering will take place.
- *
- * @return string The current filter expression.
- */
- public function getFilter()
- {
- return $this->options[static::QUERY_FILTER];
- }
-
- /**
- * Set the filter to limit the returned set of files.
- * See 'p4 help fstat' and 'p4 help jobview' for more information on
- * the filter format. Accepts a Filter or string for input,
- * or null to remove any filter.
- *
- * @param string|Filter|null $filter The desired filter.
- * @return Query provide a fluent interface.
- */
- public function setFilter($filter = null)
- {
- if (is_string($filter)) {
- $filter = new Filter($filter);
- }
-
- if (!$filter instanceof Filter && !is_null($filter)) {
- throw new \InvalidArgumentException(
- 'Cannot set filter; argument must be a P4\File\Filter, a string, or null.'
- );
- }
-
- $this->options[static::QUERY_FILTER] = $filter;
- return $this;
- }
-
- /**
- * Get the current sort field.
- * Null means default sorting will take place.
- *
- * @return array|null The current list of sort options, or null if not set.
- */
- public function getSortBy()
- {
- return $this->options[static::QUERY_SORT_BY];
- }
-
- /**
- * Set the file field(s) which will be used to sort results.
- *
- * $sortBy can be an array containing, either strings for the field name to sort
- * (where the default option will be SORT_ASCENDING), or the field name as a key
- * with an options array as value.
- *
- * For convenience, setSortBy() can accept $sortBy as a string for the field name
- * and an options array.
- *
- * Valid sort fields are any valid attribute name, or one of the constants:
- * SORT_DATE, SORT_HEAD_REV, SORT_HAVE_REV, SORT_FILE_TYPE, SORT_FILE_SIZE
- *
- * The available sorting options include: SORT_ASCENDING and SORT_DESCENDING.
- * Future server versions may provide other alternatives.
- *
- * Specify null to receive files 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 Query provide a fluent interface.
- */
- public function setSortBy($sortBy = null, $options = null)
- {
- // handle variations of parameter passing
- $clauses = array();
- if (is_array($sortBy)) {
- $maxClauses = $this->getSortClauseCount();
- if (count($sortBy) > $maxClauses) {
- throw new \InvalidArgumentException(
- "Cannot set sort by; argument contains more than $maxClauses clauses."
- );
- }
-
- // normalize sortBy clauses
- foreach ($sortBy as $field => $options) {
- if (is_integer($field) && is_string($options)) {
- $field = $options;
- $options = null;
- }
-
- if (!is_string($field)) {
- throw new \InvalidArgumentException(
- 'Cannot set sort by; invalid sort clause provided.'
- );
- }
-
- $clauses[$field] = $options;
- }
- } elseif (is_string($sortBy)) {
- $clauses = array($sortBy => $options);
- } elseif (isset($sortBy)) {
- throw new \InvalidArgumentException(
- 'Cannot set sort by; argument must be an array, string, or null.'
- );
- }
-
- // validate clauses
- if (isset($clauses)) {
- $counter = 0;
- foreach ($clauses as $field => $options) {
- $counter++;
- if (!$this->isValidSortField($field)) {
- throw new \InvalidArgumentException(
- "Cannot set sort by; invalid field name in clause #$counter."
- );
- }
- if (!$this->isValidSortOptions($options)) {
- throw new \InvalidArgumentException(
- "Cannot set sort by; invalid sort options in clause #$counter."
- );
- }
- }
- }
-
- $this->options[static::QUERY_SORT_BY] = !empty($clauses) ? $clauses : null;
- return $this;
- }
-
- /**
- * Validate sort field name.
- *
- * @param string $field Field name to validate.
- * @return boolean true if field name is valid, false otherwise.
- */
- protected function isValidSortField($field)
- {
- if (!isset($field) || !is_string($field) || !strlen($field)) {
- return false;
- }
-
- if ($this->isInternalSortField($field)) {
- return true;
- }
-
- $validator = new P4\Validate\AttributeName();
- if ($validator->isValid($field)) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Determine whether the field provided is a server-specific sort field.
- *
- * @param string $field The field name to check against list of internal fields.
- * @return boolean true if the supplied field name is an internal field.
- */
- protected function isInternalSortField($field)
- {
- return (array_key_exists($field, $this->validFieldNames)) ? true : false;
- }
-
- /**
- * Validate sort options.
- *
- * @param array|null $options Options to validate.
- * @return boolean true if options are valid, false otherwise.
- */
- protected function isValidSortOptions($options)
- {
- // null is valid; we'll apply a default later.
- if (!isset($options)) {
- return true;
- }
-
- if (!is_array($options)) {
- return false;
- }
-
- // validate each option
- $seenDirection = false;
- foreach ($options as $option) {
- if ($option === static::SORT_ASCENDING
- || $option === static::SORT_DESCENDING
- ) {
- if ($seenDirection) {
- return false;
- }
- $seenDirection = true;
- continue;
- }
- return false;
- }
-
- return true;
- }
-
- /**
- * Retrieve the field count available for sort clauses, which is server dependent.
- * In the future, this method may return different values depending on the server
- * version in use.
- *
- * @return int The number of sort clauses the server supports.
- */
- public function getSortClauseCount()
- {
- return static::SORT_CLAUSE_COUNT_2010_2;
- }
-
- /**
- * 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->options[static::QUERY_SORT_REVERSE];
- }
-
- /**
- * Set the flag indicating whether the results will be returned in reverse order.
- *
- * @param boolean $reverse Set to true to reverse sort order.
- * @return Query provide a fluent interface.
- */
- public function setReverseOrder($reverse = false)
- {
- $this->options[static::QUERY_SORT_REVERSE] = (bool) $reverse;
- return $this;
- }
-
- /**
- * Retrieve the list of file fields to return in server responses.
- * Null means all fields will be returned.
- *
- * @return array|null The current list of file fields.
- */
- public function getLimitFields()
- {
- return $this->options[static::QUERY_LIMIT_FIELDS];
- }
-
- /**
- * Set the list of fields to include in the response from the server.
- *
- * @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 Query provide a fluent interface.
- */
- public function setLimitFields($fields = array())
- {
- // accept strings for a single filespec, for convenience.
- if (is_string($fields)) {
- $fields = array($fields);
- }
- if (isset($fields) && !is_array($fields)) {
- throw new \InvalidArgumentException(
- 'Cannot set limiting fields; argument must be a string, an array, or null.'
- );
- }
-
- $this->options[static::QUERY_LIMIT_FIELDS] = $fields;
- return $this;
- }
-
- /**
- * Retrieve the needs resolve flag.
- * True if only files needing resolve should be returned.
- *
- * @return boolean True if only files needing resolve should be returned.
- */
- public function getLimitToNeedsResolve()
- {
- return $this->options[static::QUERY_LIMIT_TO_NEEDS_RESOLVE];
- }
-
- /**
- * Sets the flag that will limit files to those that need resolve.
- * True means only files that need resolve will be included.
- *
- * @param boolean $limit Set to true if only files needing resolve should be returned.
- * @return Query provide a fluent interface.
- */
- public function setLimitToNeedsResolve($limit = false)
- {
- // accept numbers or numeric string values, for convenience.
- if (is_numeric($limit) || is_string($limit)) {
- $limit = (bool) (int) $limit;
- }
- if (!is_bool($limit)) {
- throw new \InvalidArgumentException('Cannot set limit to needs resolve; argument must be a boolean.');
- }
-
- $this->options[static::QUERY_LIMIT_TO_NEEDS_RESOLVE] = $limit;
- return $this;
- }
-
- /**
- * Retrieve the opened files flag.
- * True if only opened files should be returned.
- *
- * @return boolean True if only opened files should be returned.
- */
- public function getLimitToOpened()
- {
- return $this->options[static::QUERY_LIMIT_TO_OPENED];
- }
-
- /**
- * Sets the flag that will limit files to those that are opened.
- * True means only files that are opened will be included.
- *
- * @param boolean $limit Set to true if only opened files should be returned.
- * @return Query provide a fluent interface.
- */
- public function setLimitToOpened($limit = false)
- {
- // accept numbers or numeric string values, for convenience.
- if (is_numeric($limit) || is_string($limit)) {
- $limit = (bool) (int) $limit;
- }
- if (!is_bool($limit)) {
- throw new \InvalidArgumentException('Cannot set limit to opened files; argument must be a boolean.');
- }
-
- $this->options[static::QUERY_LIMIT_TO_OPENED] = $limit;
- return $this;
- }
-
- /**
- * Retrieve the changelist with which to limit returned files.
- * Null means all restriction to changelist is not in effect.
- *
- * @return string|int The current limiting changelist.
- */
- public function getLimitToChangelist()
- {
- return $this->options[static::QUERY_LIMIT_TO_CHANGELIST];
- }
-
- /**
- * Set to a valid changelist to limit returned files, or
- * null to remove the limit.
- *
- * @param boolean $changelist A valid changelist.
- * @return Query provide a fluent interface.
- */
- public function setLimitToChangelist($changelist = null)
- {
- // accept numeric string values, for convenience.
- if (is_string($changelist)) {
- if ($changelist !== Change::DEFAULT_CHANGE) {
- $changelist = (int) $changelist;
- }
- }
- if ($changelist instanceof Change) {
- $changelist = $changelist->getId();
- }
- $validator = new P4\Validate\ChangeNumber;
- if (isset($changelist) && !$validator->isValid($changelist)) {
- throw new \InvalidArgumentException(
- 'Cannot set limit to changelist; argument must be a changelist id, a P4\Spec\Change object, or null.'
- );
- }
-
- $this->options[static::QUERY_LIMIT_TO_CHANGELIST] = $changelist;
- return $this;
- }
-
- /**
- * Return the starting row for matching files.
- * Null means all matching files will be returned.
- *
- * @return int|null The starting row.
- */
- public function getStartRow()
- {
- return $this->options[static::QUERY_START_ROW];
- }
-
- /**
- * Set the starting row to return from matching files,
- * or null to return all matching files.
- *
- * @param int|null $row The starting row.
- * @return Query provide a fluent interface.
- */
- public function setStartRow($row = null)
- {
- // accept numeric string values, for convenience.
- if (is_string($row)) {
- $row = (int) $row;
- }
- if (isset($row) && (!is_integer($row) || $row < 0)) {
- throw new \InvalidArgumentException('Cannot set start row; argument must be a positive integer or null.');
- }
- if ($row === 0) {
- $row = null;
- }
-
- $this->options[static::QUERY_START_ROW] = $row;
- return $this;
- }
-
- /**
- * Retrieve the maximum number of files to include in results.
- * 0 or null means unlimited.
- *
- * @return integer The maximum number of files to include in results.
- */
- public function getMaxFiles()
- {
- return $this->options[static::QUERY_MAX_FILES];
- }
-
- /**
- * Set to limit the number of matching files returned, or null
- * to return all matching files.
- *
- * @param int|null $max The maximum number of files to return.
- * @return Query provide a fluent interface.
- */
- public function setMaxFiles($max = null)
- {
- // accept numeric string values, for convenience.
- if (is_string($max)) {
- $max = (int) $max;
- }
- if (isset($max) && (!is_integer($max) || $max < 0)) {
- throw new \InvalidArgumentException('Cannot set max files; argument must be a positive integer or null.');
- }
- if ($max === 0) {
- $max = null;
- }
-
- $this->options[static::QUERY_MAX_FILES] = $max;
- return $this;
- }
-
- /**
- * Retrieve the list of filespecs to fetch.
- * Null means no filespecs will be fetched; aka query cannot run.
- *
- * @return array The list of filespecs.
- */
- public function getFilespecs()
- {
- return $this->options[static::QUERY_FILESPECS];
- }
-
- /**
- * Set the list of filespecs to be fetched, or null to empty the array.
- *
- * The filespecs may be in any one of depot, client or local file syntax with wildcards
- * (e.g. '//depot/...'). Note: perforce applies options such as maxFiles and sortBy to
- * each filespec individually.
- *
- * @param array|null $filespecs The filespecs to fetch.
- * @return Query provide a fluent interface.
- */
- public function setFilespecs($filespecs = null)
- {
- // accept a string for a single filespec, for convenience.
- if (is_string($filespecs)) {
- $filespecs = array($filespecs);
- }
- if (isset($filespecs) && !is_array($filespecs)) {
- throw new \InvalidArgumentException('Cannot set filespecs; argument must be a string, an array, or null.');
- }
-
- $this->options[static::QUERY_FILESPECS] = isset($filespecs) ? array_values($filespecs) : null;
- return $this;
- }
-
- /**
- * Add a single filespec to be fetched.
- *
- * The filespec may be in any one of depot, client or local file syntax with wildcards
- * (e.g. '//depot/...'). Note: perforce applies options such as maxFiles and sortBy to
- * each filespec individually.
- *
- * @param string $filespec The filespec to add.
- * @return Query provide a fluent interface.
- */
- public function addFilespec($filespec)
- {
- if (!isset($filespec) || !is_string($filespec)) {
- throw new \InvalidArgumentException('Cannot add filespec; argument must be a string.');
- }
-
- return $this->addFilespecs(array($filespec));
- }
-
- /**
- * Add a list of filespecs to be fetched.
- *
- * The filespecs may be in any one of depot, client or local file syntax with wildcards
- * (e.g. '//depot/...'). Note: perforce applies options such as maxFiles and sortBy to
- * each filespec individually.
- *
- * @param array $filespecs The array of filespecs to add.
- * @return Query provide a fluent interface.
- */
- public function addFilespecs($filespecs = array())
- {
- if (!isset($filespecs) || !is_array($filespecs)) {
- throw new \InvalidArgumentException('Cannot add filespecs; argument must be an array.');
- }
-
- $this->options[static::QUERY_FILESPECS] = (isset($this->options[static::QUERY_FILESPECS]))
- ? array_merge($this->options[static::QUERY_FILESPECS], array_values($filespecs))
- : $filespecs;
-
- return $this;
- }
-
- /**
- * Produce set of flags for the fstat command based on current options.
- *
- * @return array set of flags suitable for passing to fstat command.
- */
- public function getFstatFlags()
- {
- $flags = array();
-
- $filter = $this->getFilter();
- $filter = is_null($filter) ? '' : $filter->getExpression();
-
- // if start row set, apply rowNumber filter.
- if ($this->getStartRow()) {
- $filter = $filter ? '(' . $filter . ') & ' : '';
- $filter = $filter . 'rowNumber > ' . $this->getStartRow();
- }
-
- // filter option.
- if ($filter) {
- $flags[] = '-F';
- $flags[] = $filter;
- }
-
- // subset of fields option.
- if (count($this->getLimitFields())) {
- $flags[] = '-T';
- $flags[] = implode(' ', $this->getLimitFields());
- }
-
- // maximum results option.
- if ($this->getMaxFiles() !== null) {
- $flags[] = '-m';
- $flags[] = $this->getMaxFiles();
- }
-
- // files in change option.
- if ($this->getLimitToChangelist() !== null) {
- $flags[] = '-e';
- $flags[] = $this->getLimitToChangelist();
-
- // for the default change, we want to fetch opened files
- if ($this->getLimitToChangelist() === Change::DEFAULT_CHANGE) {
- $this->setLimitToOpened(true);
- }
- }
-
- // only opened files option.
- if ($this->getLimitToOpened()) {
- $flags[] = '-Ro';
- }
-
- // only files that need resolve option.
- if ($this->getLimitToNeedsResolve()) {
- $flags[] = "-Ru";
- }
-
- // sort options.
- if ($this->getSortBy() !== null) {
- $handled = false;
- $clauses = $this->getSortBy();
- if (count($clauses) == 1) {
- list ($field, $options) = each($clauses);
- if ($this->isInternalSortField($field) && !isset($options)) {
- $handled = true;
- switch ($field) {
- case static::SORT_DATE:
- $flags[] = '-Sd';
- break;
- case static::SORT_HEAD_REV:
- $flags[] = '-Sr';
- break;
- case static::SORT_HAVE_REV:
- $flags[] = '-Sh';
- break;
- case static::SORT_FILE_TYPE:
- $flags[] = '-St';
- break;
- case static::SORT_FILE_SIZE:
- $flags[] = '-Ss';
- break;
- }
- }
- }
-
- if (!$handled) {
- $expressions = array();
- foreach ($clauses as $field => $options) {
- if (strpos($field, '#') !== false) {
- $field = preg_replace('/#/', '', $field);
- } else {
- $field = "attr-$field";
- }
- if (!isset($options)) {
- $options = array(static::SORT_ASCENDING);
- }
- $expressions[] = "$field=". join('', $options);
- }
- $flags[] = '-S';
- $flags[] = join(',', $expressions);
- }
-
- }
-
- // reverse sort option.
- if ($this->getReverseOrder()) {
- $flags[] = '-r';
- }
-
- // standard options.
- $flags[] = '-Oal';
-
- return $flags;
- }
- }