/ */ class P4Cms_Record extends P4Cms_Record_Connected { const ENCODING_METADATA_KEY = "_encoding"; const ENCODING_FORMAT_JSON = "json"; const SAVE_THROW_CONFLICT = "throw"; const FROM_FILE_IMPORT = 'import'; protected $_id = null; protected $_p4File = null; protected $_metadata = array(); protected $_needsPopulate = false; protected $_needsFilePopulate = false; protected static $_whereCache = array(); protected static $_hasValidFields = null; /** * All records should have an id field. */ protected static $_idField = 'id'; /** * Optionally, bin2hex encode identifiers when converting * to/from depot filespecs to permit non-standard characters. */ protected static $_encodeIds = false; /** * Specifies the array of fields that the current Record class wishes to use. * The implementing class MUST set this property. */ protected static $_fields = array(); /** * Specifies the sub-path to use for storage of records. * This is used in combination with the records path (provided * by the storage adapter) to construct the full storage path. * The implementing class MUST set this property. */ protected static $_storageSubPath = null; /** * Specifies the name of the record field which will be * persisted in the file used to store the records. * If desired, the implementing class needs to set this property * to match an entry defined in the $_fields array. * If left null, all fields will persist as file attributes. */ protected static $_fileContentField = null; /** * Create a new record instance, using optional field values, in a chainable fashion. * * @param array $values associative array of keyed field values * to load into the model. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. */ public static function create($values = null, P4Cms_Record_Adapter $adapter = null) { return new static($values, $adapter); } /** * Determine if id is valid identifier for this record. * * @param string $id record identifier * @return boolean true if valid, otherwise false */ public function isValidId($id) { $id = static::$_encodeIds ? static::_encodeId($id) : $id; $validator = new P4Cms_Validate_RecordId; return $validator->isValid($id); } /** * Get the id of this record. * Extended to always return a string or null. * * @return string|null the value of the id field. */ public function getId() { $id = parent::getId(); // cast non-null ids to strings. return $id === null ? null : (string) $id; } /** * Set the id of this record. * * @param string|int|null $id the identifier of this record. * @return P4Cms_Record provides fluent interface. */ public function setId($id) { if ($id !== null && !$this->isValidId($id)) { throw new InvalidArgumentException("Cannot set id. Given id is invalid."); } // if populate was deferred, caller expects it // to have been populated already. $this->_populate(); // if id has changed, clear associated p4 file. if ($id !== $this->getId()) { $this->_p4File = null; } return parent::setId($id); } /** * Get all of the model field names. * Extends parent to populate first and throw an exception * if it encounters any invalid field names. * * @return array a list of field names for this model. * @throws P4Cms_Record_Exception if any of the predefined field names are invalid. */ public function getFields() { // populate but skip getting the file contents at this point $this->_populate(true); // validate predefined fields on first access. if (static::$_hasValidFields === null) { static::$_hasValidFields = true; $validator = new P4Cms_Validate_RecordField; foreach ($this->getDefinedFields() as $field) { if (!$validator->isValid($field)) { static::$_hasValidFields = false; } } } // if fields are invalid, throw exception. if (static::$_hasValidFields === false) { throw new P4Cms_Record_Exception( "Cannot get fields. Record has one or more fields with invalid names." ); } // let parent do its thing. $fields = parent::getFields(); // ensure file content field is present if defined. if (!empty(static::$_fileContentField) && !in_array(static::$_fileContentField, $fields)) { $fields[] = static::$_fileContentField; } return $fields; } /** * Set a particular field value. * Extends parent to validate names of new fields. * * @param string $field the name of the field to set the value of. * @param mixed $value the value to set in the field. * @return P4Cms_Model provides a fluent interface * @throws P4Cms_Model_Exception if the field does not exist. * @throws P4Cms_Record_Exception if the field name is invalid. */ public function setValue($field, $value) { // if field is new, validate field name. if (!$this->hasField($field)) { $validator = new P4Cms_Validate_RecordField; if (!$validator->isValid($field)) { throw new P4Cms_Record_Exception( "Cannot set value. Field '$field' is not a valid field name." ); } } return parent::setValue($field, $value); } /** * Set all of the model's values at once. * Extends parent to support passing a form object. * * Accepting a form object permits special handling of certain form * elements via the P4Cms_Record_EnhancedElementInterface. This interface * requires a populateRecord() method which allows the element to make * decisions and modify other aspects of the record object. * * @param Zend_Form|array|null $values form or array of values to set on record. * @param bool $filter optional - if true, ignores values for unknown fields. * @return P4Cms_Record provides a fluent interface */ public function setValues($values, $filter = false) { // let parent deal with non-form input. if (!$values instanceof Zend_Form) { return parent::setValues($values, $filter); } // set values from form input. $form = $values; $values = $form->getValues(); foreach ($values as $field => $value) { // skip read-only fields. if ($this->isReadOnlyField($field)) { continue; } // skip filtered fields. if ($filter && !$this->hasField($field)) { continue; } // handle record-aware elements. $element = $form->getElement($field); if ($element instanceof P4Cms_Record_EnhancedElementInterface) { $element->populateRecord($this); } else { $this->setValue($field, $value); } } return $this; } /** * Set a field value to the contents of the given file. * * @param string $field the field to set the value of. * @param string $file the full path to the file to read from. * @return P4Cms_Record provides fluent interface. * @throws InvalidArgumentException if the given file does not exist. */ public function setValueFromFile($field, $file) { if (!is_file($file)) { throw new InvalidArgumentException("Cannot set value from file. File does not exist."); } $this->setValue($field, file_get_contents($file)); return $this; } /** * Check if a record with the given id exists. * * Query options may, optionally, be passed. Any paths/ids present * in the options will ignored. * * @param string|int $id the id of the record to fetch. * @param P4Cms_Record_Query|array|null $query optional - query options to augment result. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return bool true if the record exists, false otherwise. */ public static function exists( $id, $query = null, P4Cms_Record_Adapter $adapter = null) { $query = static::_normalizeQuery($query); // if no id given, return false. if (!strlen($id)) { return false; } // clobber any existing IDs with our own and clear any paths on query. $query->setIds(array($id))->setPaths(array()); return static::count($query, $adapter) > 0; } /** * Get a specific record by id. * A revision specifier may be, optionally, included in the id field. * Rev Specifiers will influence the data returned but will not be * present in the id of the returned record. * * Query options may, optionally, be passed. Any paths/ids present * in the options will ignored. * * @param string|int $id the id of the record to fetch. * @param P4Cms_Record_Query|array|null $query optional - query options to augment result. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return P4Cms_Record the requested record. * @throws P4Cms_Record_NotFoundException if the requested record can't be found. */ public static function fetch($id, $query = null, P4Cms_Record_Adapter $adapter = null) { $query = static::_normalizeQuery($query); // clobber any existing IDs with our own and clear any paths on options. $query->setIds(array($id))->setPaths(array()); $results = static::fetchAll($query, $adapter); if (!count($results)) { throw new P4Cms_Record_NotFoundException( "Cannot fetch record '$id'. Record does not exist." ); } return $results->first(); } /** * Get all records under the record storage path. * Results can be limited by providing a query object or array. * * @param P4Cms_Record_Query|array|null $query optional - query options to augment result. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return P4Cms_Model_Iterator all records of this type. */ public static function fetchAll($query = null, P4Cms_Record_Adapter $adapter = null) { $query = static::_normalizeQuery($query); // if no adapter given, use default. $adapter = $adapter ?: static::getDefaultAdapter(); // convert record query to a p4 file query. $query = $query->toFileQuery(get_called_class(), $adapter); // early exit if no filespecs in query, return empty iterator. if (is_array($query->getFilespecs()) && !count($query->getFilespecs())) { return new P4Cms_Model_Iterator; } // fetch files from perforce. $files = P4_File::fetchAll($query, $adapter->getConnection()); // convert files to records. $records = new P4Cms_Model_Iterator; foreach ($files as $file) { $record = static::fromP4File($file, null, $adapter); $records[$record->getId()] = $record; } return $records; } /** * Count all records matching the given query. * * @param P4Cms_Record_Query|array|null $query optional - query options to augment result. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return integer The count of all matching records */ public static function count( P4Cms_Record_Query $query = null, P4Cms_Record_Adapter $adapter = null) { $query = static::_normalizeQuery($query); // if no adapter given, use default. $adapter = $adapter ?: static::getDefaultAdapter(); // convert record query to a p4 file query. $query = $query->toFileQuery(get_called_class(), $adapter); // early exit if no filespecs in query, return zero. if (is_array($query->getFilespecs()) && !count($query->getFilespecs())) { return 0; } // only fetch a single field - use headRev because it's tiny. $query->setLimitFields('headRev'); // fetch count from perforce. return P4_File::count($query, $adapter->getConnection()); } /** * Save this record. If the record does not have an id, a new * UUID will be assigned to identify the record. * * @param string $description optional - a description of the change. * @param null|string|array $options optional - passing the SAVE_THROW_CONFLICTS * flag will cause exceptions on conflict; default * behaviour is to crush any conflicts. * Note this flag has no effect in batches. * @return P4Cms_Record provides a fluent interface */ public function save($description = null, $options = null) { // if we are in a batch, pend the record to the // changelist identified by the batch id. $adapter = $this->getAdapter(); $change = ($adapter->inBatch()) ? $adapter->getBatchId() : null; // if this record has an id, attempt to flush and edit the file. // if it has no id, generate a new UUID to identify the record. if ($this->getId()) { $file = $this->_getP4File(); try { // if our file isn't deleted simply attempt to sync and edit. // perforce doesn't let you edit a deleted revision (you can't // 'have' a deleted revision) so if it is deleted, sync to the // previous revision and attempt to edit that. if (!$file->isDeleted()) { $file->sync(); $file->edit($change); } else { // if we are deleted, sync to the previous revision // we create a new file object because we want to sync/edit // the previous version without changing this record object's // file instance (which would have negative side-effects). $revSpec = '#' . ((int)$file->getStatus('headRev') - 1); $previousFile = P4_File::fetch( $file->getFilespec(true) . $revSpec, $file->getConnection() ); $previousFile->sync(); // attempt to open for edit. if the file is deleted at the head // revision this will fail and we will open for add later. $previousFile->edit($change); // clear file's status cache so it can be aware of changes made // by previousFile (e.g. 'isOpened' check will be acurate) $file->clearStatusCache(); } } catch (P4_File_Exception $e) { // edit failed, but that's ok - we'll attempt to add below. } catch (P4_Connection_CommandException $e) { // if command failed due to a chmod error, just eat the exception; // file will get created later. normally this problem should not // occur, but if a virtual integrate or copy was performed, it can. if (!stripos($e->getMessage(), "Command failed: chmod: ") === 0) { throw $e; } } } else { $this->setId((string) new P4Cms_Uuid); $file = $this->_getP4File(); } // write file content field to file contents. // if we don't have a file content field we // simply touch the file to ensure it's on disk if (static::hasFileContentField()) { $field = static::getFileContentField(); // we avoid reading the file into memory if possible // but there are situations where we have to: // - if this is an add write the value to persist the default // - if this is an edit the file should already exist but if its missing // make a go of reading its current value and writing it back out // - lastly if we have a value in memory we need to write it to persist it if (!$file->isOpened() || !file_exists($file->getLocalFilename()) || array_key_exists($field, $this->_values) ) { $value = $this->_encodeFieldValue($field, $this->_getValue($field)); $file->setLocalContents($value); } } else { $file->touchLocalFile(); } // if file is not yet opened, add it now - we do this after // the file is written so perforce can detect the file type. if (!$file->isOpened()) { $file->add($change); } // write field values and metadata as file attributes. // we clear any attributes we don't know about (ie. the field was // explicitly unset, or this record was not fetched, but happens // to collide with a file in perforce that has attributes) $clear = array(); $ignore = array(); $attributes = array(); // collect field values to set as attributes. foreach ($this->getFields() as $field) { if ($field != static::$_idField && $field != static::$_fileContentField) { $attributes[$field] = $this->_encodeFieldValue($field, $this->_getValue($field)); } } // collect metadata to set or ignore -- if we were unable to decode // certain metadata when reading it, we set it to false to indicate it // should be left alone (could be third-party data for example) foreach ($this->_metadata as $field => $data) { $field = "_" . $field; if (!empty($data)) { $attributes[$field] = $this->_encodeMetadata($data); } else if ($data === false) { $ignore[] = $field; } } // determine the fields to clear - as above, we clear any attributes // we don't know about so long as they aren't listed as ignored. foreach ($file->getAttributes() as $key => $value) { if (!array_key_exists($key, $attributes) && !in_array($key, $ignore)) { $clear[] = $key; } } $file->setAttributes($attributes); $file->clearAttributes($clear); // if we're not in a batch, submit file to perforce if (!$adapter->inBatch()) { if (!$description) { $description = $this->_generateSubmitDescription(); } // default option is to 'accept yours' but we switch to // null if SAVE_THROW_CONFLICTS flag is passed. $resolveFlag = P4_File::RESOLVE_ACCEPT_YOURS; if (in_array(static::SAVE_THROW_CONFLICT, (array)$options)) { $resolveFlag = null; } $file->submit($description, $resolveFlag); } return $this; } /** * Store a record. Equivalent to the instance method save(), but offered * as a static method for convenience. * * @param array|string|null $values optional - list of values for the new record * if a string is given, it will be taken as the * record identifier - if no id given, a new * UUID will be assigned. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return P4Cms_Record provides a fluent interface. */ public static function store($values = array(), P4Cms_Record_Adapter $adapter = null) { // normalize values to an array. if (!is_array($values)) { $values = array(static::$_idField => $values); } $record = static::create($values, $adapter); $record->save(); return $record; } /** * Delete this record. * * @param string $description optional - a description of the change. * @return P4Cms_Record provides fluent interface. */ public function delete($description = null) { // if we are in a batch, pend the record to the // changelist identified by the batch id. $adapter = $this->getAdapter(); $change = ($adapter->inBatch()) ? $adapter->getBatchId() : null; // open depot file for delete. $file = $this->_getP4File(); try { $file->delete($change); } catch (P4_File_Exception $e) { // ignore exception if file was open for add - otherwise rethrow. if (!$file->isOpened() || $file->getStatus('action') !== 'add') { throw $e; } } // ensure local file deleted. if (file_exists($file->getLocalFilename())) { $file->deleteLocalFile(); } // if we're not in a batch, submit file to perforce if (!$adapter->inBatch()) { if (!$description) { $description = "Deleted '" . static::$_storageSubPath . "' record."; } $file->submit($description); } return $this; } /** * Remove a record from storage. Equivalent to delete but class-based for convenience. * * @param string $id the id of the record to remove. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return P4Cms_Record provides fluent interface. */ public static function remove($id, P4Cms_Record_Adapter $adapter = null) { // if no adapter given, use default. $adapter = $adapter ?: static::getDefaultAdapter(); $record = new static; $record->setId($id) ->setAdapter($adapter) ->delete(); return $record; } /** * Override parent to clear associated p4 file if adapter has changed to ensure the file * will get the connection from the new adapter. * * @param P4Cms_Record_Adapter $adapter the adapter to use for this instance. * @return P4Cms_Record provides fluent interface. */ public function setAdapter(P4Cms_Record_Adapter $adapter) { // if adapter has changed, clear associated p4 file. if ($adapter !== $this->_adapter) { $this->_p4File = null; } return parent::setAdapter($adapter); } /** * Get the Perforce path used for the storage of this class of records. * The storage path is a combination of the records path (provided by the record * storage adapter) and the sub-path (defined by the record class). * * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return string the path used to store this class of records. */ public static function getStoragePath(P4Cms_Record_Adapter $adapter = null) { // if no adapter given, use default. $adapter = $adapter ?: static::getDefaultAdapter(); // normalize the path components. $basePath = rtrim($adapter->getBasePath(), '/'); $subPath = rtrim(static::$_storageSubPath, '/'); // return basePath w. subPath (if set). return strlen($subPath) ? $basePath . '/' . $subPath : $basePath; } /** * Determine if this record class has a field mapped to the file contents. * * @return bool true if the class has a file content field; false otherwise. */ public static function hasFileContentField() { return isset(static::$_fileContentField); } /** * Get the name of the field that is mapped to the file contents. * * @return string the name of the file content field. * @throws P4Cms_Record_Exception if there is no file content field. */ public static function getFileContentField() { if (!static::hasFileContentField()) { throw new P4Cms_Record_Exception( "Cannot get the file content field. No field is mapped to the file." ); } return static::$_fileContentField; } /** * Get metadata for the given field. * * Field metadata is stored in a file attribute named for the * field, but with a leading underscore (e.g. '_field-name'). * * @param string $field the field to get metadata for. * @return array the metadata for the given field. * @throws P4Cms_Record_Exception if the field does not exist. */ public function getFieldMetadata($field) { // populate but skip getting the file contents at this point $this->_populate(true); if (!$this->hasField($field)) { throw new P4Cms_Record_Exception( "Cannot get field metadata for a non-existant field." ); } return $this->_getFieldMetadata($field); } /** * Set metadata for the given field. * * Field metadata is stored in a file attribute named for the * field, but with a leading underscore (e.g. '_field-name'). * * @param string $field the field to set metadata for. * @param array|null $data the metadata to store for the field. * @return P4Cms_Record provides fluent interface. * @throws P4Cms_Record_Exception if the field does not exist. */ public function setFieldMetadata($field, array $data = null) { if (!$this->hasField($field)) { throw new P4Cms_Record_Exception( "Cannot set field metadata for a non-existant field." ); } $this->_metadata[$field] = $data; return $this; } /** * Test if this record is deleted in perforce. * * @return boolean true if record is deleted or doesn't have an id, * otherwise returns true. */ public function isDeleted() { return $this->getId() && $this->_getP4File()->isDeleted(); } /** * Provides access to a copy of the p4_file object which is underlying the * current record instance. By default it returns a cloned copy, pass true * to get a reference. * * @param bool $reference optional - pass true to get a reference to the file * @return P4_File the file associated with this record instance. */ public function toP4File($reference = false) { return $reference ? $this->_getP4File() : clone $this->_getP4File(); } /** * Given a p4 file instance, produce a record instance with id * adapter and associated p4 file object all set appropriately. * * Under normal operation a reference is maintained to the given * file and the id of the record is derived from the filespec. * If the import option is specified, only the values are taken * from the file. No reference is maintained and the resulting * record will have a null id. * * @param P4_File $file a p4 file instance to convert into a record. * @param string|array|null $options options to influence the operation: * FROM_FILE_IMPORT - only the file's values are * used, the id is ignored * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return P4Cms_Record the record instance generated from the file. */ public static function fromP4File($file, $options = null, P4Cms_Record_Adapter $adapter = null) { // if no adapter given, use default. $import = in_array(static::FROM_FILE_IMPORT, (array)$options); $adapter = $adapter ?: static::getDefaultAdapter(); $id = $import ? null : static::depotFileToId($file->getDepotFilename(), $adapter); $record = new static(); $record->setId($id) ->setAdapter($adapter) ->_setP4File($file) ->_deferPopulate(); // if we are doing an import force the record to read in // values then clear any reference to the passed file. if ($import) { $record->_populate() ->_setP4File(null); } return $record; } /** * Get the depot-syntax form of the perforce path used for the storage of * this class of records. The path returned by getStoragePath() is in an * unknown form. It could be in depot, client or local file-system syntax. * * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return string the depot path used to store this class of records. */ public static function getDepotStoragePath(P4Cms_Record_Adapter $adapter = null) { // if no adapter given, use default. $adapter = $adapter ?: static::getDefaultAdapter(); // get the storage path (in unknown form). $storagePath = static::getStoragePath($adapter); // we cache the depot-syntax version on a per-path, per-adapter basis. // to avoid running 'p4 where' everytime we need to get the depot storage path. if (isset(static::$_whereCache[spl_object_hash($adapter)][$storagePath])) { return static::$_whereCache[spl_object_hash($adapter)][$storagePath]; } // convert to depot-syntax. $result = $adapter->getConnection()->run('where', $storagePath . '/...'); if ($result->hasWarnings()) { throw new P4Cms_Record_Exception( "Cannot get the depot storage path. Storage path is not in client view." ); } $depotPath = substr($result->getData(0, 'depotFile'), 0, -4); // cache per adapter/path. static::$_whereCache[spl_object_hash($adapter)][$storagePath] = $depotPath; return $depotPath; } /** * Given a record id, determine the corresponding filespec. * * @param string $id the record id to get the filespec for. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return string the filespec for a given record id. */ public static function idToFilespec($id, P4Cms_Record_Adapter $adapter = null) { // if no adapter given, use default. $adapter = $adapter ?: static::getDefaultAdapter(); // id is required. if (!strlen($id)) { throw new InvalidArgumentException("Cannot get filespec for an empty id."); } // optionally encode id for storage. if (static::$_encodeIds) { $id = static::_encodeId($id); } return static::getStoragePath($adapter) . '/' . $id; } /** * Return name of the id field. * * @return string name of id field. */ public static function getIdField() { return static::$_idField; } /** * Given a record filespec in depotFile syntax, determine the id. * * @param string $depotFile a record depotFile. * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use. * @return string|int the id portion of the depotFile file spec. */ public static function depotFileToId( $depotFile, P4Cms_Record_Adapter $adapter = null) { // if no adapter given, use default. $adapter = $adapter ?: static::getDefaultAdapter(); // strip the depot storage path from the depotFile to produce the id. $depotBasePath = static::getDepotStoragePath($adapter) . '/'; if (strpos($depotFile, $depotBasePath) === 0) { $id = substr($depotFile, strlen($depotBasePath)); // optionally decode stored id. if (static::$_encodeIds) { $id = static::_decodeId($id); } } else { throw new P4Cms_Record_Exception( "Cannot determine record id for a file outside of the record storage path." ); } return $id; } /** * Set the corresponding P4 File object instance. * Used when fetching records to prime the record object. * * @param P4_File|null $file the corresponding P4_File object. * @return P4Cms_Record provides fluent interface. * @throws Record_Exception if the file is not a valid P4_File object. */ protected function _setP4File($file) { if (!$file instanceof P4_File && !is_null($file)) { throw new P4Cms_Record_Exception( 'Cannot set P4 File. The given file is not a valid P4_File object.' ); } $this->_p4File = $file; return $this; } /** * Get the P4 File object that corresponds to this record. * * @return P4_File corresponding P4 File instance. */ protected function _getP4File() { // create corresponding p4 file instance if necessary. if (!$this->_p4File instanceof P4_File) { $filespec = static::idToFilespec($this->getId(), $this->getAdapter()); $this->_p4File = new P4_File; $this->_p4File->setFilespec($filespec) ->setConnection($this->getAdapter()->getConnection()); } return $this->_p4File; } /** * Encode metadata for storage (using JSON). * * @param mixed $data The data to be encoded. * @return string The 'encoded' data. */ protected function _encodeMetadata($data) { return Zend_Json::encode($data); } /** * Decode metadata (presumably from storage). * * @param string $data The data to be decoded. * @return mixed The 'decoded' data. */ protected function _decodeMetadata($data) { return strlen($data) ? Zend_Json::decode($data) : null; } /** * Encode field's value as JSON if its not string or numeric. * Updates field metadata to record encoding. * * @param string $field the field to encode the value for. * @param mixed $value the value to encode. * @return string the encoded value. */ protected function _encodeFieldValue($field, $value) { $metadata = $this->_getFieldMetadata($field); if (is_numeric($value)) { $value = (string) $value; } if (isset($value) && !is_string($value)) { // json encode $value = Zend_Json::encode($value); $metadata[self::ENCODING_METADATA_KEY] = self::ENCODING_FORMAT_JSON; } else if (array_key_exists(self::ENCODING_METADATA_KEY, $metadata)) { unset($metadata[self::ENCODING_METADATA_KEY]); } $this->setFieldMetadata($field, $metadata); return $value; } /** * Decode field's value if it is encoded (checks field metadata). * * @param string $field the field we are decoding * @param string $value the encoded value. * @return mixed the decoded value (could be string or array). */ protected function _decodeFieldValue($field, $value) { $metadata = $this->_getFieldMetadata($field); if (strlen($value) && isset($metadata[self::ENCODING_METADATA_KEY]) && self::ENCODING_FORMAT_JSON === $metadata[self::ENCODING_METADATA_KEY] ) { try { return Zend_Json::decode($value); } catch (Exception $e) { P4Cms_Log::logException("Failed to decode field value", $e); } } // convert empty strings to null. // this is done so that null values round-trip correctly // this prevents empty strings from round-tripping, but // that was deemed a reasonable trade-off. if (!strlen($value)) { return null; } return $value; } /** * Overrides parent to populate the record first. * Get a raw (but decoded) field value. Does not use custom accessor methods. * If idField is specified; will utilize 'getId' function. * * @param string $field the name of the field to get the value of. * @return mixed the value of the field. * @throws P4Cms_Model_Exception if the field does not exist. */ protected function _getValue($field) { $excludeFile = ($field !== static::$_fileContentField); $this->_populate($excludeFile); return parent::_getValue($field); } /** * Schedule populate to run when data is requested (lazy-load). * * @return P4Cms_Record provides fluent interface. */ protected function _deferPopulate() { $this->_needsPopulate = true; $this->_needsFilePopulate = true; return $this; } /** * Get the values for this record from Perforce and set them * in the instance. Won't clobber existing values. * * @param bool $excludeFile optional - skip populating file content * @return P4Cms_Record provides fluent interface. */ protected function _populate($excludeFile = false) { // if record has no id and no file, we can't pull from storage. if (!$this->hasId() && !$this->_p4File) { return $this; } if ($this->_needsPopulate) { // clear needsPopulate flag. $this->_needsPopulate = false; // get file attributes from associated p4 file. $file = $this->_getP4File(); try { $attributes = $file->getAttributes(); } catch (P4_File_Exception $e) { // no matching file in storage, nothing to populate from. return $this; } // set field metadata first from file attributes. foreach ($attributes as $key => $value) { if ($key[0] === '_') { $field = substr($key, 1); if (!array_key_exists($field, $this->_metadata)) { try { $this->_metadata[$field] = $this->_decodeMetadata($value); } catch (Exception $e) { // we failed to decode the metadata entry -- we set it to // false to tell save that the attribute should be ignored. $this->_metadata[$field] = false; } } } } // set field values from file attributes. $validator = new P4Cms_Validate_RecordField; foreach ($attributes as $key => $value) { if ($validator->isValid($key)) { if (!array_key_exists($key, $this->_values)) { $this->_values[$key] = $this->_decodeFieldValue($key, $value); } } } } if ($this->_needsFilePopulate && !$excludeFile) { // clear needsPopulate flag. $this->_needsFilePopulate = false; // set file content field if record has one. $fileField = static::$_fileContentField; if (strlen($fileField) && !array_key_exists($fileField, $this->_values)) { $file = $this->_getP4File(); try { if (!$file->isDeleted()) { $contents = $file->getDepotContents(); } else { // if we are deleted, pull the file content from the previous revision $revSpec = '#' . ((int)$file->getStatus('headRev') - 1); $contents = P4_File::fetch( $file->getFilespec(true) . $revSpec, $file->getConnection() )->getDepotContents(); } $this->_values[$fileField] = $this->_decodeFieldValue($fileField, $contents); } catch (P4_File_Exception $e) { // presumably no depot file content to get. } } } return $this; } /** * Get metadata for the given field - doesn't populate or check field existance. * * Field metadata is stored in a file attribute named for the * field, but with a leading underscore (e.g. '_field-name'). * * @param string $field the field to get metadata for. * @return array the metadata for the given field. */ protected function _getFieldMetadata($field) { if (array_key_exists($field, $this->_metadata)) { return (array) $this->_metadata[$field]; } else { return array(); } } /** * Encode id for storage (via bin2hex). * * @param string $id the id to encode. * @return string the encoded id. */ protected function _encodeId($id) { return bin2hex($id); } /** * Decode stored id (reverse bin2hex). * * @param string $id the id to decode. * @return string the decoded id. */ protected function _decodeId($id) { return pack("H*", $id); } /** * Generate a save description for this record. * * @return string a default submit description. */ protected function _generateSubmitDescription() { return static::$_storageSubPath ? "Saved '" . static::$_storageSubPath . "' record." : "Saved record."; } /** * Queries arguments (e.g. to fetch/fetchAll) can be given as a * query object, an array or null. This helper method normalizes * the input to a query object and throws on invalid arguments. * * @param P4Cms_Record_Query|array|null $query optional - query options to augment result. * * @return P4Cms_Record_Query the query input normalized to a query object. * @throws InvalidArgumentException if the query input is not valid. */ protected static function _normalizeQuery($query) { if (!$query instanceof P4Cms_Record_Query && !is_array($query) && !is_null($query)) { throw new InvalidArgumentException( 'Query must be a P4Cms_Record_Query, array or null' ); } // normalize array input to a query if (is_array($query)) { $query = new P4Cms_Record_Query($query); } // if null query given, make a new one. $query = $query ?: new P4Cms_Record_Query; return $query; } }