<?php
/**
* Abstracts operations against Perforce jobs.
*
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
* @todo Add support for the following commands:
* fix
* fixes
*/
namespace P4\Spec;
use P4\Connection\ConnectionInterface;
use P4\Model\Fielded\Iterator as FieldedIterator;
use P4\Spec\Exception\Exception;
use P4\Spec\Exception\NotFoundException;
use P4\Validate;
class Job extends PluralAbstract
{
const SPEC_TYPE = 'job';
const ID_FIELD = 'Job';
const FETCH_BY_FILTER = 'filter';
const FETCH_DESCRIPTION = 'descriptions';
const FETCH_BY_IDS = 'ids';
const FETCH_INSENSITIVE = 'insensitive';
const FETCH_REVERSE = 'reverse';
protected $cache = array();
protected $fields = array(
102 => array(
'accessor' => 'getStatus',
'mutator' => 'setStatus'
),
103 => array(
'accessor' => 'getUser',
'mutator' => 'setUser',
),
104 => array(
'accessor' => 'getDate'
),
105 => array(
'accessor' => 'getDescription',
'mutator' => 'setDescription'
)
);
/**
* Extend parent to clear any cached fixed changes.
*
* @param null|string $id the id of this entry - pass null to clear.
* @return PluralAbstract provides a fluent interface
* @throws \InvalidArgumentException if id does not pass validation.
*/
public function setId($id)
{
$this->cache = array();
return parent::setId($id);
}
/**
* Get field value. If a custom field accessor exists, it will be used.
* Extends parent to add support for accessors keyed on field code instead of name.
*
* @param string|null $field the name of the field to get the value of or null for all
* @return mixed the value of the field(s).
* @throws Exception if the field does not exist.
*/
public function get($field = null)
{
// allow parent to deal with requests for array format
if ($field === null) {
return parent::get($field);
}
// if field has custom accessor based on field code, use it.
$fieldCode = $this->getSpecDefinition()->fieldNameToCode($field);
if (isset($this->fields[$fieldCode]['accessor'])) {
return $this->{$this->fields[$fieldCode]['accessor']}();
}
return parent::get($field);
}
/**
* Set field value. If a custom field mutator exists, it will be used.
* Extends parent to add support for mutators keyed on field code instead of name.
*
* @param string $field the name of the field to set the value of.
* @param mixed $value the value to set in the field.
* @return SpecAbstract provides a fluent interface
*/
public function set($field, $value)
{
// if field has custom mutator based on field code, use it.
$fieldCode = $this->getSpecDefinition()->fieldNameToCode($field);
if (isset($this->fields[$fieldCode]['mutator'])) {
return $this->{$this->fields[$fieldCode]['mutator']}($value);
}
return parent::set($field, $value);
}
/**
* Get all Jobs from Perforce. Adds filtering options.
*
* @param array $options optional - array of options to augment fetch behavior.
* supported options are:
*
* FETCH_MAXIMUM - set to integer value to limit to the
* first 'max' number of entries.
* FETCH_BY_FILTER - set to jobview filter
* FETCH_DESCRIPTION - description will be fetched if true,
* left for later lazy loading if false.
* * defaults to true if not specified
* FETCH_BY_IDS - pass a list of ids to fetch
* not compatible with FETCH_BY_FILTER
* FETCH_INSENSITIVE - only applies to FETCH_BY_IDS, makes
* id matches case insensitive
* @param ConnectionInterface $connection optional - a specific connection to use.
* @return \P4\Model\Fielded\Iterator all records of this type.
*/
public static function fetchAll($options = array(), ConnectionInterface $connection = null)
{
// if fetch by ids was passed by is an empty array just return an empty result
// otherwise the caller would actually get all jobs back erroneously.
$options += array(static::FETCH_BY_IDS => null, static::FETCH_INSENSITIVE => null);
$ids = $options[static::FETCH_BY_IDS];
if (is_array($ids) && !count($ids)) {
return new FieldedIterator;
}
$result = parent::fetchAll($options, $connection);
// if we received fetch by ids, ensure the results are accurate.
// if the id foo was requested we can also see results for entries
// such as foo-bar without this step. its rare in reality though.
if ($options[static::FETCH_BY_IDS]) {
$result->filter(
'Job',
$options[static::FETCH_BY_IDS],
$options[static::FETCH_INSENSITIVE] ? array($result::FILTER_NO_CASE) : array()
);
}
return $result;
}
/**
* Determine if the given job id exists.
*
* @param string|int $id the id to check for.
* @param ConnectionInterface $connection optional - a specific connection to use.
* @return bool true if the given id matches an existing job.
*/
public static function exists($id, ConnectionInterface $connection = null)
{
// check id for valid format
if (!static::isValidId($id)) {
return false;
}
$jobs = static::fetchAll(
array(
static::FETCH_BY_IDS => $id,
static::FETCH_DESCRIPTION => false,
static::FETCH_MAXIMUM => 1
),
$connection
);
return (bool) count($jobs);
}
/**
* Get the requested job entry from Perforce.
*
* @param string $id the id of the job to fetch.
* @param ConnectionInterface $connection optional - a specific connection to use.
* @return Change instance of the requested job.
* @throws \InvalidArgumentException if no id is given.
* @throws NotFoundException if no such job exists.
*/
public static function fetch($id, ConnectionInterface $connection = null)
{
// ensure a valid id is provided.
if (!static::isValidId($id)) {
throw new \InvalidArgumentException("Must supply a valid id to fetch.");
}
$job = static::fetchAll(
array(
static::FETCH_BY_IDS => $id,
static::FETCH_MAXIMUM => 1
),
$connection
)->first();
if (!$job || $job->getId() != $id) {
throw new NotFoundException(
"Cannot fetch " . static::SPEC_TYPE . " $id. Record does not exist."
);
}
return $job;
}
/**
* Override parent to set id to 'new' if unset and capture id returned by save.
*
* @return Job provides a fluent interface
*/
public function save()
{
$values = $this->getRawValues();
if ($this->getId() === null) {
$values[static::ID_FIELD] = "new";
}
// ensure all required fields have values.
$this->validateRequiredFields($values);
$result = $this->getConnection()->run(static::SPEC_TYPE, "-i", $values);
// Saved job Id is returned as a string, capture it.
$matches = false;
foreach ($result->getData() as $data) {
if (preg_match('/^Job ([^ ]+) saved\./', $data, $matches)) {
break;
}
}
if (!$matches) {
throw new Exception('Cannot find ID for saved Job.');
}
// Store the retrieved ID
$this->setId($matches[1]);
// should re-populate (server may change values).
$this->deferPopulate(true);
return $this;
}
/**
* Returns the status of this job. This will return the value of field 102 even if the
* field name has been changed in the jobspec.
*
* Out of the box valid status options are: open/suspended/closed or null. Modifying the
* jobspec can change the list of valid options.
*
* @return string|null Status of this job or null if unset.
*/
public function getStatus()
{
return $this->getRawValue($this->getSpecDefinition()->fieldCodeToName(102));
}
/**
* Update the status of this job. This will update the value of field 102 even if the
* field name has been changed in the jobspec.
*
* @param string|null $status Status of this job or null
* @return Job provides a fluent interface.
* @throws \InvalidArgumentException For input which isn't a string or null
*/
public function setStatus($status)
{
if (!is_string($status) && !is_null($status)) {
throw new \InvalidArgumentException('Status must be a string or null');
}
return $this->setRawValue($this->getSpecDefinition()->fieldCodeToName(102), $status);
}
/**
* Returns the user who created this job. This will return the value of field 103
* even if the field name has been changed in the jobspec.
*
* @return string|null User who created this job or null if unset.
*/
public function getUser()
{
return $this->getRawValue($this->getSpecDefinition()->fieldCodeToName(103));
}
/**
* Update the user who created this job. This will update the value of field 103
* even if the field name has been changed in the jobspec.
*
* @param string|User|null $user User who created this job, or null
* @return Job provides a fluent interface.
* @throws \InvalidArgumentException For input which isn't a string, User or null
*/
public function setUser($user)
{
if ($user instanceof User) {
$user = $user->getId();
}
if (!is_null($user) && !is_string($user)) {
throw new \InvalidArgumentException('User must be a string, P4\Spec\User or null');
}
return $this->setRawValue($this->getSpecDefinition()->fieldCodeToName(103), $user);
}
/**
* Returns the date this job was created. This will return the value of field 104
* even if the field name has been changed in the jobspec.
*
* @return string|null Date this job was created or null if unset.
*/
public function getDate()
{
return $this->getRawValue($this->getSpecDefinition()->fieldCodeToName(104));
}
/**
* Get the unixtime this job was created on the server.
*
* @return int|null the unixtime this job was created on the server,
* or null if the job does not exist on the server.
*/
public function getTime()
{
return $this->getAsTime($this->getSpecDefinition()->fieldCodeToName(104)) ?: null;
}
/**
* Convenience function to get a given field as unixtime accounting for server's current timezone.
*
* @param string $field the name of the field
* @return int|false date in unix timestamp of false if unable to convert
*/
public function getAsTime($field)
{
return static::dateToTime($this->getRawValue($field), $this->getConnection());
}
/**
* Returns the description for this job. This will return the value of field 105
* even if the field name has been changed in the jobspec.
*
* @return string|null Description for this job or null if unset.
*/
public function getDescription()
{
return $this->getRawValue($this->getSpecDefinition()->fieldCodeToName(105));
}
/**
* Update the decription for this job. This will update the value of field 105
* even if the field name has been changed in the jobspec.
*
* @param string|null $description Description for this job, or null
* @return Job provides a fluent interface.
* @throws \InvalidArgumentException For input which isn't a string or null
*/
public function setDescription($description)
{
if (!is_null($description) && !is_string($description)) {
throw new \InvalidArgumentException('Description must be a string or null');
}
return $this->setRawValue($this->getSpecDefinition()->fieldCodeToName(105), $description);
}
/**
* Get the changes fixed by this job.
*
* @return array the list of changes fixed by this job.
*/
public function getChanges()
{
// if no id is set; just return an empty array
if (!$this->getId()) {
return array();
}
// fetch the list of changes if we don't already have it
if (!isset($this->cache['changes']) || !is_array($this->cache['changes'])) {
$this->cache['changes'] = array();
$data = $this->getConnection()->run('fixes', array('-j', $this->getId()))->getData();
foreach ($data as $fix) {
$this->cache['changes'][] = $fix['Change'];
}
}
return $this->cache['changes'];
}
/**
* Get the change objects fixed by this job.
*
* @return FieldedIterator list of Changes fixed by this job
*/
public function getChangeObjects()
{
// just skip to an empty iterator if we have no fixes
if (!$this->getChanges()) {
return new FieldedIterator;
}
if (!isset($this->cache['changeObjects'])
|| !$this->cache['changeObjects'] instanceof FieldedIterator
) {
$this->cache['changeObjects'] = Change::fetchAll(
array(Change::FETCH_BY_IDS => $this->getChanges()),
$this->getConnection()
);
}
return clone $this->cache['changeObjects'];
}
/**
* Determine if this job has a created date field
*
* @return bool true if job has a created date field; false otherwise.
*/
public function hasCreatedDateField()
{
try {
$this->getCreatedDateField();
return true;
} catch (Exception $e) {
return false;
}
}
/**
* Get the name of this job's created date field.
*
* @return string the name of the created date field.
* @throws Exception if there is no created date field.
*/
public function getCreatedDateField()
{
$spec = $this->getSpecDefinition();
$fields = $spec->getFields();
foreach ($fields as $key => $field) {
if (isset($field['fieldType']) && $field['fieldType'] === 'once'
&& isset($field['default']) && $field['default'] === '$now'
) {
return $key;
}
}
throw new Exception("Job has no created date field.");
}
/**
* Determine if this job has a modified date field
*
* @return bool true if job has a modified date field; false otherwise.
*/
public function hasModifiedDateField()
{
try {
$this->getModifiedDateField();
return true;
} catch (Exception $e) {
return false;
}
}
/**
* Get the name of this job's modified date field.
*
* @return string the name of the modified date field.
* @throws Exception if there is no modified date field.
*/
public function getModifiedDateField()
{
$spec = $this->getSpecDefinition();
$fields = $spec->getFields();
foreach ($fields as $key => $field) {
if (isset($field['fieldType']) && $field['fieldType'] === 'always'
&& isset($field['default']) && $field['default'] === '$now'
) {
return $key;
}
}
throw new Exception("Job has no modified date field.");
}
/**
* Determine if this job has a created by field
*
* @return bool true if job has a created by field; false otherwise.
*/
public function hasCreatedByField()
{
try {
$this->getCreatedByField();
return true;
} catch (Exception $e) {
return false;
}
}
/**
* Get the name of this job's created by field.
*
* @return string the name of the created by field.
* @throws Exception if there is no created by field.
*/
public function getCreatedByField()
{
$spec = $this->getSpecDefinition();
$fields = $spec->getFields();
foreach ($fields as $key => $field) {
if (isset($field['fieldType']) && $field['fieldType'] !== 'always'
&& isset($field['default']) && $field['default'] === '$user'
) {
return $key;
}
}
throw new Exception("Job has no created By field.");
}
/**
* Determine if this job has a modified by field
*
* @return bool true if job has a modified by field; false otherwise.
*/
public function hasModifiedByField()
{
try {
$this->getModifiedByField();
return true;
} catch (Exception $e) {
return false;
}
}
/**
* Get the name of this job's modified by field.
*
* @return string the name of the modified by field.
* @throws Exception if there is no modified by field.
*/
public function getModifiedByField()
{
$spec = $this->getSpecDefinition();
$fields = $spec->getFields();
foreach ($fields as $key => $field) {
if (isset($field['fieldType']) && $field['fieldType'] === 'always'
&& isset($field['default']) && $field['default'] === '$user'
) {
return $key;
}
}
throw new Exception("Job has no modified By field.");
}
/**
* 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]) &&
!(isset($options[static::FETCH_BY_IDS]) && $options[static::FETCH_BY_IDS])
) {
$filter = $options[static::FETCH_BY_FILTER];
if (!is_string($filter) || trim($filter) === "") {
throw new \InvalidArgumentException(
'Fetch by Filter expects a non-empty string as input'
);
}
$flags[] = '-e';
$flags[] = $filter;
}
if (isset($options[static::FETCH_BY_IDS]) && $options[static::FETCH_BY_IDS]) {
// escape and concat job ids
$jobs = array();
foreach ((array)$options[static::FETCH_BY_IDS] as $id) {
$jobs[] = preg_replace('/([^\w])/', '\\\\$1', $id);
}
$flags[] = '-e';
$flags[] = static::ID_FIELD . "="
. implode("|" . static::ID_FIELD . "=", $jobs);
}
// if they have not specified FETCH_DESCRIPTION or
// they have and its true; include full descriptions
if (!isset($options[static::FETCH_DESCRIPTION]) ||
$options[static::FETCH_DESCRIPTION]) {
$flags[] = '-l';
}
// sort in reverse order if so instructed
if (isset($options[static::FETCH_REVERSE]) && $options[static::FETCH_REVERSE]) {
$flags[] = '-r';
}
return $flags;
}
/**
* Check if the given id is in a valid format for this spec type.
*
* @param string|int $id the id to check
* @return bool true if id is valid, false otherwise
*/
protected static function isValidId($id)
{
$validator = new Validate\SpecName;
$validator->allowSlashes(true);
$validator->allowRelative(true);
$validator->allowPurelyNumeric(true);
return $validator->isValid($id);
}
/**
* Extends parent to control description inclusion based on FETCH options.
*
* @param array $listEntry a single spec entry from spec list output.
* @param array $flags the flags that were used for this 'fetchAll' run.
* @param ConnectionInterface $connection a specific connection to use.
* @return Job a (partially) populated instance of this spec class.
*/
protected static function fromSpecListEntry($listEntry, $flags, ConnectionInterface $connection)
{
// discard the description if it isn't the 'long' version
if (!in_array('-l', $flags)) {
unset($listEntry['Description']);
}
$job = parent::fromSpecListEntry($listEntry, $flags, $connection);
// jobs are fully populated when -l is used.
// empty fields are not returned by p4 jobs and would
// otherwise cause a needless populate on get(null)
if (in_array('-l', $flags)) {
$job->needsPopulate = false;
}
return $job;
}
}