<?php
/**
* Abstracts operations against Perforce streams.
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
* @todo add accessor/mutator for 'Paths'
*/
class P4_Stream extends P4_Spec_PluralAbstract
{
const FETCH_BY_PATH = 'path';
const FETCH_BY_FILTER = 'filter';
const SORT_RECURSIVE = 'sort';
protected static $_specType = 'stream';
protected static $_idField = 'Stream';
protected static $_accessors = array(
'Update' => 'getUpdateDateTime',
'Access' => 'getAccessDateTime',
'Owner' => 'getOwner',
'Name' => 'getName',
'Parent' => 'getParent',
'Type' => 'getType',
'Description' => 'getDescription',
'Options' => 'getOptions',
'Paths' => 'getPaths'
);
protected static $_mutators = array(
'Owner' => 'setOwner',
'Name' => 'setName',
'Parent' => 'setParent',
'Type' => 'setType',
'Description' => 'setDescription',
'Options' => 'setOptions',
'Paths' => 'setPaths'
);
protected $_cache = array();
/**
* Get all Streams from Perforce. Adds the following filter options:
* FETCH_BY_PATH - limits results to streams matching path (can include wildcards).
* FETCH_BY_FILTER - a 'jobview' style expression to limit results.
* SORT_RECURSIVE - optionally sort the results recusively
*
* @param array $options optional - options to augment fetch behavior.
* @param P4_Connection_Interface $connection optional - a specific connection to use.
* @return P4_Model_Iterator all records of this type matching options.
*/
public static function fetchAll($options = array(), P4_Connection_Interface $connection = null)
{
// try/catch parent to deal with the exception we get on non-existend depots
try {
$streams = parent::fetchAll($options, $connection);
// set the 'parent' stream on each entry if we have a copy
foreach ($streams as $stream) {
// skip any streams without parents
if (!$stream->getParent()) {
continue;
}
// attempt to locate the parent object in our result set and set it
$parent = $streams->filter('Stream', $stream->getParent(), P4_Iterator::FILTER_COPY)
->first();
if ($parent) {
$stream->_cache['parentObject'] = $parent;
}
}
// apply sorting if it has been requested
if (isset($options[static::SORT_RECURSIVE]) && $options[static::SORT_RECURSIVE]) {
$streams = static::_sortRecursively($streams);
}
return $streams;
} catch (P4_Connection_CommandException $e) {
// if the 'depot' has been interpreted as an invalid client, just return no matches
if (preg_match("/Command failed: .+ - must refer to client/", $e->getMessage())) {
return new P4_Model_Iterator;
}
// unexpected error; rethrow it
throw $e;
}
}
/**
* Determine if the given stream id exists.
*
* @param string $id the id to check for.
* @param P4_Connection_Interface $connection optional - a specific connection to use.
* @return bool true if the given id matches an existing stream.
*/
public static function exists($id, P4_Connection_Interface $connection = null)
{
// check id for valid format
if (!static::_isValidId($id)) {
return false;
}
$streams = static::fetchAll(array(static::FETCH_BY_FILTER => "Stream=$id"), $connection);
return (bool) count($streams);
}
/**
* Get the last update time for this stream.
* This value is read only, no setUpdateDateTime function is provided.
*
* If this is a brand new stream, null will be returned in lieu of a time.
*
* @return string|null Date/Time of last update, formatted "2009/11/23 12:57:06" or null
*/
public function getUpdateDateTime()
{
return $this->_getValue('Update');
}
/**
* Get the last access time for this stream.
* This value is read only, no setAccessDateTime function is provided.
*
* If this is a brand new spec, null will be returned in lieu of a time.
*
* @return string|null Date/Time of last access, formatted "2009/11/23 12:57:06" or null
*/
public function getAccessDateTime()
{
return $this->_getValue('Access');
}
/**
* Get the owner of this stream.
*
* @return string|null User who owns this record.
*/
public function getOwner()
{
return $this->_getValue('Owner');
}
/**
* Set the owner of this stream to passed value.
*
* @param string|null $owner A string containing username
* @return P4_Stream provides a fluent interface.
* @throws InvalidArgumentException Owner is incorrect type.
*/
public function setOwner($owner)
{
if (!is_string($owner) && !is_null($owner)) {
throw new InvalidArgumentException('Owner must be a string or null.');
}
return $this->_setValue('Owner', $owner);
}
/**
* Get the name setting for this stream.
*
* @return string|null Name set for this stream or null.
*/
public function getName()
{
return $this->_getValue('Name');
}
/**
* Set the name for this stream.
*
* @param string|null $name Name for this stream or null
* @return P4_Stream provides a fluent interface.
* @throws InvalidArgumentException Name is incorrect type.
*/
public function setName($name)
{
if (!is_string($name) && !is_null($name)) {
throw new InvalidArgumentException('Name must be a string or null.');
}
return $this->_setValue('Name', $name);
}
/**
* Get the parent setting for this stream.
*
* @return string|null Parent set for this stream.
*/
public function getParent()
{
$parent = $this->_getValue('Parent');
return $parent == 'none' ? null : $parent;
}
/**
* Set the parent for this stream.
*
* @param string|null $parent Parent for this stream or null
* @return P4_Stream provides a fluent interface.
* @throws InvalidArgumentException Parent is incorrect type.
*/
public function setParent($parent)
{
if (!is_string($parent) && !is_null($parent)) {
throw new InvalidArgumentException('Parent must be a string or null.');
}
// clear cache as parent may have changed
$this->_cache = array();
return $this->_setValue('Parent', $parent);
}
/**
* Get the parent asociated with this stream in P4_Stream format.
*
* @return P4_Stream|null this streams parent object or null if none
*/
public function getParentObject()
{
if (!$this->getParent()) {
return null;
}
if (!isset($this->_cache['parentObject'])
|| !$this->_cache['parentObject'] instanceof P4_Stream
) {
$this->_cache['parentObject'] = P4_Stream::fetch(
$this->getParent(),
$this->getConnection()
);
}
return clone $this->_cache['parentObject'];
}
/**
* Returns the depth of this stream. Assumes all parent objects
* are returnable.
*
* @return int the depth of this stream.
*/
public function getDepth()
{
$stream = $this;
for ($i = 0; $stream = $stream->getParentObject(); $i++);
return $i;
}
/**
* Get the type setting for this stream.
*
* @return string|null Type set for this stream.
*/
public function getType()
{
return $this->_getValue('Type');
}
/**
* Set the type for this stream.
*
* @param string|null $type Type for this stream or null
* @return P4_Stream provides a fluent interface.
* @throws InvalidArgumentException Type is incorrect type.
*/
public function setType($type)
{
if (!is_string($type) && !is_null($type)) {
throw new InvalidArgumentException('Type must be a string or null.');
}
return $this->_setValue('Type', $type);
}
/**
* Get the description for this stream.
*
* @return string|null description for this stream.
*/
public function getDescription()
{
return $this->_getValue('Description');
}
/**
* Set a description for this stream.
*
* @param string|null $description description for this stream.
* @return P4_Stream provides a fluent interface.
* @throws InvalidArgumentException Description is incorrect type.
*/
public function setDescription($description)
{
if (!is_string($description) && !is_null($description)) {
throw new InvalidArgumentException('Description must be a string or null.');
}
return $this->_setValue('Description', $description);
}
/**
* Get options for this stream.
* Returned array will contain one option per element e.g.:
* array (
* 0 => 'allsubmit',
* 1 => 'toparent',
* 2 => 'unlocked'
* )
*
* @return array options which are set on this stream.
*/
public function getOptions()
{
$options = $this->_getValue('Options');
$options = explode(' ', $options);
// Explode will set key 0 to null for empty input; clean it up.
if (count($options) == 1 && empty($options[0])) {
$options = array();
}
return $options;
}
/**
* Set the options for this stream.
* Accepts an array, format detailed in getOptions, or a single string containing
* a space seperated list of options.
*
* @param array|string $options options to set on this stream in array or string.
* @return P4_Steam provides a fluent interface.
* @throws InvalidArgumentException Options are incorrect type.
*/
public function setOptions($options)
{
if (is_array($options)) {
$options = implode(' ', $options);
}
if (!is_string($options)) {
throw new InvalidArgumentException('Options must be an array or string');
}
return $this->_setValue('Options', $options);
}
/**
* Get the paths for this stream.
* Path entries will be returned as an array with 'type', 'view' and 'depot' entries, e.g.:
* array (
* 0 => array (
* 'type' => 'share',
* 'view' => 'src/...',
* 'depot' => null
* )
* 1 => array (
* 'type' => 'import',
* 'view' => 'src/...',
* 'depot' => '//over/there/src/...'
* )
* )
*
* @return array list path entries for this stream.
*/
public function getPaths()
{
// The raw path data is formatted as:
// array (
// 0 => 'share ...',
// 1 => 'import imp/ //depot/other/local/...'
// )
$paths = array();
foreach ($this->_getValue('Paths') ?: array() as $entry) {
$entry = str_getcsv($entry, ' ');
$paths[] = array_combine(
array('type', 'view', 'depot'),
$entry + array(null, null, null)
);
}
return $paths;
}
/**
* Set the paths for this stream.
* Paths are passed as an array of path entries. Each patj entry can be an array with
* 'type', 'view' and, optionally, 'depot' entries or a raw string.
*
* @param array|string $paths Path entries, formatted as sub-arrays or strings.
* @return P4_Stream provides a fluent interface.
* @throws InvalidArgumentException Paths array, or a path entry, is incorrect type.
*/
public function setPaths($paths)
{
// we let the caller pass in a single path and normalize it below
if (is_string($paths) || isset($paths['type'], $paths['view'])) {
$paths = array($paths);
}
if (!is_array($paths)) {
throw new InvalidArgumentException('Paths must be passed as array or string.');
}
// The Paths array contains either:
// - Child arrays keyed on type/view/[depot] which we glue together
// - Raw strings which we simply leave as is
// The below foreach run will normalize the whole thing for storage
$parsedPaths = array();
foreach ($paths as $path) {
if (is_array($path)
&& isset($path['type'], $path['view'])
&& is_string($path['type'])
&& is_string($path['view'])
&& (!isset($path['depot']) || is_string($path['depot']))
) {
// stringify the path quoting paths to be safe
$string = $path['type'] . ' "' . $path['view'] . '"';
if (isset($path['depot']) && strlen($path['depot'])) {
$string .= ' "' . $path['depot'] . '"';
}
$path = $string;
}
if (!is_string($path)) {
throw new InvalidArgumentException(
"Each path entry must be an array with type/view (and optionally depot) or a string."
);
}
$validate = str_getcsv($path, ' ');
if (count($validate) < 2 || count($validate) > 3
|| trim($validate[0]) === '' || trim($validate[1]) === ''
) {
throw new InvalidArgumentException(
"Each path entry must contain between two and three entries."
);
}
$parsedPaths[] = $path;
};
return $this->_setValue('Paths', $parsedPaths);
}
/**
* Add a path to this stream.
*
* @param string $type the path type (share/isolate/import/exclude)
* @param string $view the view path
* @param string|null $depot the depot path (only used for import type)
* @return P4_Stream provides a fluent interface.
*/
public function addPath($type, $view, $depot = null)
{
$paths = $this->getPaths();
$paths[] = array("type" => $type, "view" => $view, "depot" => $depot);
return $this->setPaths($paths);
}
/**
* Save this spec to Perforce.
* Extends parent to provide a default value of 'none' for parent.
*
* @return P4_Stream provides a fluent interface
*/
public function save()
{
if (!$this->getValue('Parent')) {
$this->setValue('Parent', 'none');
}
return parent::save();
}
/**
* Remove this stream. Extend parent to remove all clients dedicated to this
* stream first.
*
* @param boolean $force pass true to force delete this stream, additionally
* attempts to delete any clients using this stream
* by default stream will only be deleted if there are
* no clients current using the stream and current user
* is the stream owner or the stream is unlocked.
* @return P4_Stream provides fluent interface.
*/
public function delete($force = false)
{
// remove clients dedicated to this stream if force is true
if ($force) {
P4_Client::fetchAll(
array(P4_Client::FETCH_BY_STREAM => $this->getId())
)->invoke('delete', array($force));
}
return parent::delete($force ? array('-f') : null);
}
/**
* Check if the given id is in a valid format for this spec type.
*
* @param string $id the id to check
* @return bool true if id is valid, false otherwise
*/
protected static function _isValidId($id)
{
$validator = new P4_Validate_StreamName;
return $validator->isValid($id);
}
/**
* Produce set of flags for the spec list command, given fetch all options array.
* Extends parent to add support for filter option.
*
* @param array $options array of options to augment fetch behavior.
* see fetchAll for documented options.
* @return array set of flags suitable for passing to spec list command.
*/
protected static function _getFetchAllFlags($options)
{
$flags = parent::_getFetchAllFlags($options);
if (isset($options[static::FETCH_BY_FILTER])) {
$filter = $options[static::FETCH_BY_FILTER];
if (!is_string($filter) || trim($filter) === '') {
throw new InvalidArgumentException(
'Filter expects a non-empty string as input'
);
}
$flags[] = '-F';
$flags[] = $filter;
}
if (isset($options[static::FETCH_BY_PATH])) {
$flags[] = $options[static::FETCH_BY_PATH];
}
return $flags;
}
/**
* Given a spec entry from spec list output (p4 streams), produce
* an instance of this spec with field values set where possible.
*
* @param array $listEntry a single spec entry from spec list output.
* @param array $flags the flags that were used for this 'fetchAll' run.
* @param P4_Connection_Interface $connection a specific connection to use.
* @return P4_Stream a (partially) populated instance of this spec class.
*/
protected static function _fromSpecListEntry($listEntry, $flags, P4_Connection_Interface $connection)
{
// update/access time are return as longs. Unset to avoid figuring out timezone
// for a proper conversion.
unset($listEntry['Update']);
unset($listEntry['Access']);
// move the description into place
$listEntry['Description'] = $listEntry['desc'];
unset($listEntry['desc']);
return parent::_fromSpecListEntry($listEntry, $flags, $connection);
}
/**
* This method will ensure the list of streams is in the proper order
* with children listed after their associated parents.
*
* @param P4_Model_Iterator $streams the streams to sort
* @param string|null $parent the parent id (used for recursion)
* @return P4_Model_Iterator The recursively sorted result
*/
protected static function _sortRecursively($streams, $parent = null)
{
// get branches with given parent and sort them
$children = $streams->filter('Parent', array($parent), P4_Model_Iterator::FILTER_COPY)
->sortBy('Name', array(P4_Model_Iterator::SORT_NATURAL));
// assemble list and append sorted sub-entries below their parent
$sorted = new P4_Model_Iterator;
foreach ($children as $stream) {
$sorted[] = $stream;
foreach (static::_sortRecursively($streams, $stream->getId()) as $sub) {
$sorted[] = $sub;
}
}
return $sorted;
}
}