<?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; } }