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