<?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.
*/
class P4_File_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 P4_File_Query
*/
public static function create($options = array())
{
return new static($options);
}
/**
* Reset the current query object to its default state.
*
* @return P4_File_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 P4_File_Filter or string for input,
* or null to remove any filter.
*
* @param string|P4_File_Filter|null $filter The desired filter.
* @return P4_File_Query provide a fluent interface.
*/
public function setFilter($filter = null)
{
if (is_string($filter)) {
$filter = new P4_File_Filter($filter);
}
if (!$filter instanceof P4_File_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 P4_File_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 fort 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 P4_File_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 P4_File_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 P4_File_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 P4_File_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 P4_File_Query provide a fluent interface.
*/
public function setLimitToChangelist($changelist = null)
{
// accept numeric string values, for convenience.
if (is_string($changelist)) {
if ($changelist !== P4_Change::DEFAULT_CHANGE) {
$changelist = (int) $changelist;
}
}
if ($changelist instanceof P4_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_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 P4_File_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 P4_File_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 P4_File_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 P4_File_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 P4_File_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() === P4_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;
}
}