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