<?php /** * This class provides access to the definition for a Perforce spec type. * This includes: field names, field types, field options, preset values, * comments, etc. * * Fields with the dataType 'text' have several issues with whitespace: * - Any trailing whitespace will be stripped * - A trailing new-line will be added if not present * - Any leading/intermediate lines will have trailing whitespace removed * - Any line with non-whitespace content will preserve all trailing whitespace * * @copyright 2011 Perforce Software. All rights reserved. * @license Please see LICENSE.txt in top-level folder of this distribution. * @version <release>/<patch> */ namespace P4\Spec; use P4; use P4\Spec\Exception\Exception; use P4\Connection\ConnectionInterface; use P4\Model\Connected\ConnectedAbstract; class Definition extends ConnectedAbstract { protected $type = null; protected $data = array(); protected $isPopulated = false; protected static $cache = array(); /** * Get the type of spec that this defines. * * @return string the type of spec this defines. */ public function getType() { return $this->type; } /** * Set the type of spec this defines. * * @param string $type the type of spec to define. * @return Definition provides a fluent interface */ public function setType($type) { if (!is_string($type)) { throw new \InvalidArgumentException("Type must be a string."); } $this->type = $type; return $this; } /** * Get the definition for a given spec type from Perforce. * * @param string $type the type of the spec to get the definition for. * @param ConnectionInterface $connection optional - a specific connection to use. * @returns Definition instance containing details about this spec type. */ public static function fetch($type, ConnectionInterface $connection = null) { // if no connection given, use default. $connection = $connection ?: static::getDefaultConnection(); // cache is per-server - ensure cache initialized for this server. $port = $connection->getPort(); if (!array_key_exists($port, static::$cache)) { static::$cache[$port] = array(); } // create and populate spec definition if not cached. if (!array_key_exists($type, static::$cache[$port]) || !static::$cache[$port][$type] instanceof Definition) { // construct spec def instance. $definition = new static($connection); $definition->setType($type); // call get fields to force a populate and ensure type is valid. $definition->getFields(); static::$cache[$port][$type] = $definition; } // return cloned copy so that changes don't pollute the cache. return clone static::$cache[$port][$type]; } /** * Get multi-dimensional array of detailed field information for this spec type. * * @return array detailed field information for this spec type. */ public function getFields() { // only populate if fields array is unset. if (!array_key_exists('fields', $this->data)) { $this->populate(); } return $this->data['fields']; } /** * Get array of detailed information about a particular field. * * @param string $field the field to get information about. * @return array detailed field information for this spec type. * @throws Exception if the field does not exist. */ public function getField($field) { // verify field exists. if (!$this->hasField($field)) { throw new Exception("Can't get field '$field'. Field does not exist."); } $fields = $this->getFields(); return $fields[$field]; } /** * Check if this spec definition has a particular field. * * @param string $field the field to check for the existence of. * @return boolean true if the spec has the named field, false otherwise. */ public function hasField($field) { $fields = array_keys($this->getFields()); return in_array((string)$field, $fields); } /** * Determine if the given field is required. * * @param string $field the field to check if required. * @return bool true if the field is required, false otherwise. */ public function isRequiredField($field) { $field = $this->getField($field); if ($field['fieldType'] === 'required' || $field['fieldType'] === 'key') { return true; } else { return false; } } /** * Determine if the given field is read-only. * * @param string $field the field to check if required. * @return bool true if the field is read-only, false otherwise. */ public function isReadOnlyField($field) { $field = $this->getField($field); if ($field['fieldType'] == 'once') { return true; } else { return false; } } /** * Set the fields that define this specification. * * @param array $fields multi-dimensional array of detailed field information. * @return Definition provides a fluent interface * @todo better validate fields array format. */ public function setFields($fields) { if (!is_array($fields)) { throw new \InvalidArgumentException("Fields must be an array."); } $this->data['fields'] = $fields; return $this; } /** * Get comments for this spec type. * * @return string comments describing this spec type. */ public function getComments() { // only populate if comments are unset. if (!array_key_exists('comments', $this->data)) { $this->populate(); } return $this->data['comments']; } /** * Set the comments for this specification. * * Comments are stored as 'text' fields which causes whitespace issues. * See Definition for details. * * @param string $comments comments describing this spec type. * @return Definition provides a fluent interface * @todo validate comments format more thoroughly. */ public function setComments($comments) { if (!is_string($comments)) { throw new \InvalidArgumentException("Comments must be a string."); } $this->data['comments'] = $comments; return $this; } /** * Save this spec definition to Perforce. * * @return Definition provides a fluent interface */ public function save() { // save spec definition in Perforce. $connection = static::getDefaultConnection(); $result = $connection->run( 'spec', array("-i", $this->getType()), $this->toSpecArray($this->data) ); $this->clearCache(); return $this; } /** * Given a field name this function will return the associated field code. * * @param string $name String representing the field's name. * @return int The field code associated with the passed name. */ public function fieldNameToCode($name) { $field = $this->getField($name); return (int) $field['code']; } /** * Given a field code this function will return the associated field name. * * @param int|string $code Int or string representing code * @return string The field name associated with the passed code * @throws \InvalidArgumentException If passed an invalid or non-existent field code */ public function fieldCodeToName($code) { // if we are passed a string, and casting through int doesn't change it, // it is purely numeric, cast to an int. if (is_string($code) && $code === (string)(int)$code) { $code = (int)$code; } // if we made it this far, fail unless we have an int if (!is_int($code)) { throw new \InvalidArgumentException('Field must be a purely numeric string or int.'); } $fields = $this->getFields(); foreach ($fields as $name => $field) { if ($field['code'] == $code) { return $name; } } throw new \InvalidArgumentException('Specified field code does not exist.'); } /** * Clear the shared 'fetch' class and also clear this instances fields/comments. * * @todo If clearCache is called and this instance is subsequently populated, the shared * fetch cache won't be updated. If fetch is later called an additional populate will * be executed. This could be optimized but is a fairly narrow case. */ public function clearCache() { $type = $this->getType(); // Remove the static cache; helps with future 'fetch' calls unset(static::$cache[$this->getConnection()->getPort()][$type]); // Remove our instances values $this->data = array(); // Ensure our instance will re-populate $this->isPopulated = false; } /** * Expand preset values that are expected to be interpreted client side. * For example, '$user' should be set to the name of the current Perforce * user. '$now' should be set to the current time. See 'p4 help undoc' * for additional details. * * @param string $default the default value to be expanded. * @param ConnectionInterface $connection optional - a specific connection to use * when expanding default values. * @return string|null the expanded default value. * @throws \InvalidArgumentException if default value is not a string * @todo job specs have additional 'expansions'; there may be more outside jobs too. */ public static function expandDefault($default, ConnectionInterface $connection = null) { if (!is_string($default)) { throw new \InvalidArgumentException('Default value must be a string.'); } // if no connection given, use default. $connection = $connection ?: static::getDefaultConnection(); switch ($default) { case '$user': return $connection->getUser(); break; case '$blank': return null; break; default: return $default; break; } } /** * Get the spec definition from Perforce if not already populated. */ protected function populate() { // only populate once. if ($this->isPopulated) { return; } // query perforce to get spec definition. $connection = $this->getConnection(); $result = $connection->run( 'spec', array("-o", $this->getType()) ); // ensure all sequences are expanded into arrays $result->expandSequences(); // ensure spec output is an array. $spec = $result->getData(-1); if (!is_array($spec) || empty($spec)) { throw new Exception( "Failed to populate spec definition. Perforce result invalid." ); } // convert spec to internal format. $data = $this->fromSpecArray($spec); // don't clobber fields/comments if already set. if (!array_key_exists('fields', $this->data)) { $this->data['fields'] = $data['fields']; } if (!array_key_exists('comments', $this->data)) { $this->data['comments'] = $data['comments']; } // flag as populated. $this->isPopulated = true; } /** * Convert 'p4 spec -o' output into the format of the internal data structure * used by this class. * * The data structure groups field metadata by field name and breaks multi-word * fields into their component parts. For example: * * array ( * 'fields' => array ( * 'Field1' => array ( * 'code' => '310', * 'dataType' => 'select', * 'displayLength' => '12', * 'fieldType' => 'optional', * 'order' => '0', * 'position' => 'L', * 'options' => array ( * 0 => 'local', * 1 => 'unix', * ) * ), * 'Field2' => array ( * 'code' => '311', * 'dataType' => 'wlist', * 'displayLength' => '64', * 'fieldType' => 'optional', * 'wordCount' => '2' * ) * ), * 'comments' => '# Comments for this spec.' * ) * * @param array $spec the raw output from 'p4 spec -o' * @return array the converted spec definition data structure. */ protected function fromSpecArray($spec) { $data = array( 'fields' => array(), 'comments' => null ); // break apart fields word-list in spec array. foreach ($spec['Fields'] as $fieldInfo) { list($code, $name, $dataType, $length, $fieldType) = explode(" ", $fieldInfo); $data['fields'][$name] = array( 'code' => $code, 'dataType' => $dataType, 'displayLength' => $length, 'fieldType' => $fieldType ); // hack because Perforce doesn't provide word count // for single column wlist's. if ($dataType == 'wlist') { $data['fields'][$name]['wordCount'] = 1; } } // add word count information for multi-word fields. if (isset($spec['Words']) && is_array($spec['Words'])) { foreach ($spec['Words'] as $wordInfo) { list($fieldName, $wordCount) = explode(" ", $wordInfo); $data['fields'][$fieldName]['wordCount'] = $wordCount; } } // add format information. if (isset($spec['Formats']) && is_array($spec['Formats'])) { foreach ($spec['Formats'] as $formatInfo) { list($fieldName, $order, $position) = explode(" ", $formatInfo); $data['fields'][$fieldName]['order'] = $order; $data['fields'][$fieldName]['position'] = $position; } } // add options for select fields. if (isset($spec['Values']) && is_array($spec['Values'])) { foreach ($spec['Values'] as $selectInfo) { list($fieldName, $options) = explode(" ", $selectInfo); $data['fields'][$fieldName]['options'] = explode('/', $options); } } // add default field values. if (isset($spec['Presets']) && is_array($spec['Presets'])) { foreach ($spec['Presets'] as $defaultInfo) { list($fieldName, $default) = explode(" ", $defaultInfo); $data['fields'][$fieldName]['default'] = $default; } } // add spec comments to data structure. if (isset($spec['Comments']) && is_string($spec['Comments'])) { $data['comments'] = $spec['Comments']; } return $data; } /** * Convert the internal data structure of this class into a 'p4 spec -i' * compatible array of comments and field details. See fromSpecArray for * data structure format. * * @param array $data an internal spec definition data structure. * @return array spec definition array suitable for 'p4 spec -i'. */ protected function toSpecArray($data) { if (!is_array($data) || !array_key_exists('fields', $data) || !array_key_exists('comments', $data)) { throw InvalidArgumentException("Data must be array with fields and comments."); } $spec = array( 'Fields' => array(), 'Words' => array(), 'Formats' => array(), 'Values' => array(), 'Presets' => array(), 'Comments' => null ); // convert fields back into spec array format. foreach ($data['fields'] as $name => $field) { $spec['Fields'][] = implode( " ", array( $field['code'], $name, $field['dataType'], $field['displayLength'], $field['fieldType'] ) ); // only include word count if > 1. if (isset($field['wordCount']) && $field['wordCount'] > 1) { $spec['Words'][] = implode(" ", array($name, $field['wordCount'])); } if (isset($field['order'], $field['position'])) { $spec['Formats'][] = implode(" ", array($name, $field['order'], $field['position'])); } if (isset($field['options']) && is_array($field['options'])) { $spec['Values'][] = implode(" ", array($name, implode("/", $field['options']))); } if (isset($field['default'])) { $spec['Presets'][] = implode(" ", array($name, $field['default'])); } } // remove empty elements. foreach ($spec as $key => $value) { if (empty($value)) { unset($spec[$key]); } } // add comments to spec array. // Perforce will keep existing value if no comments entry is present. // We ensure it is at least blank to force an update. if (isset($data['comments']) && is_string($data['comments'])) { $spec['Comments'] = $data['comments']; } return $spec; } }