/ */ class P4Cms_Record_Volatile extends P4Cms_Record_Connected { const CLIENT = 'volatileClient'; protected $_clientMasquerade = null; protected $_depotFile = null; protected $_originalClient = null; protected $_originalHost = null; protected $_storageSubPath = null; /** * Fetch a stored volatile record by id. * * @param string $id the id of the record to fetch * @param P4Cms_Record_Adapter $adapter the storage adapter to use * @param P4_Client|string|null $masquerade the client to masquerade as * @return P4Cms_Record_Volatile the populated volatile record * @throws P4Cms_Record_NotFoundException if no matching record can be found. */ public static function fetch($id, $adapter, $masquerade = null) { $record = new static; $record->setId($id) ->setAdapter($adapter) ->setClientMasquerade($masquerade) ->_populate(); return $record; } /** * Check for the existance of a volatile record by id. * * @param string $id the id of the record to lookup * @param P4Cms_Record_Adapter $adapter the storage adapter to use * @param P4_Client|string|null $masquerade the client to masquerade as * @return bool true if the record exists; false otherwise. */ public static function exists($id, $adapter, $masquerade = null) { try { static::fetch($id, $adapter, $masquerade); return true; } catch (P4Cms_Record_NotFoundException $e) { return false; } } /** * Set the client workspace to masquerade as. * * @param P4_Client|string|null $client the client workspace to masquerade as * @return P4Cms_Record_Volatile provides fluent interface. */ public function setClientMasquerade($client) { // upgrade client to a client object. $client = (!$client instanceof P4_Client && !is_null($client)) ? P4_Client::fetch($client, $this->getAdapter()->getConnection()) : $client; $this->_clientMasquerade = $client; return $this; } /** * Get the client workspace we are set to masquerade as. * If no masquerade client has been explicitly set we will * fetch/create one based on the presence of a 'volatileClient' * property on the storage adapter. * * @return P4_Client|null the client workspace to masquerade as. */ public function getClientMasquerade() { // if we already have a client that we're masquerading as, return it. if ($this->_clientMasquerade) { return $this->_clientMasquerade; } // the adapter may specify a volatile client property (the site // branch object does this), if we don't have one early exit. $adapter = $this->getAdapter(); if (!$adapter->hasProperty(static::CLIENT)) { return null; } $p4 = $adapter->getConnection(); $clientId = $adapter->getProperty(static::CLIENT); // try to fetch the volatile client - if it does not exist, create one // based on the adapter's existing client (ie. same view/stream). try { $client = P4_Client::fetch($clientId, $p4); } catch (P4_Spec_NotFoundException $e) { $client = P4_Client::fetch($p4->getClient(), $p4); $client->setId($clientId) ->setDescription("Chronicle generated 'volatile' record client.") ->setRoot(P4_Environment::isWindows() ? "NUL" : "/dev/null") ->touchUpView() ->save(); } $this->_clientMasquerade = $client; return $this->_clientMasquerade; } /** * Save the values in this volatile record to Perforce. * Note that the values are not submitted, only pended. * * @return P4Cms_Record_Volatile provides fluent interface. */ public function save() { $file = $this->_getDepotFile(); // masquerade as another client $this->_beginCharade(); try { // ensure pending file is opened for add (or edit) // we use flush and -t/k to avoid touching files on disk. $this->_runFree('flush', $file); $this->_runFree('add', array('-t', 'text', $file)); $this->_runFree('edit', array('-kt', 'text', $file)); $params = array(); foreach ($this->_values as $key => $value) { $params[] = "-n"; $params[] = $key; $params[] = "-v"; $params[] = (string) $value; } $params[] = $file; $this->getAdapter()->getConnection()->run('attribute', $params); } catch (Exception $e) { // look away! } // restore original client. $this->_endCharade(); // if an exception occurred, throw it now. if (isset($e)) { throw $e; } return $this; } /** * Remove the volatile record. Since volatile records are never * submitted, this is actually a 'p4 revert' operation. * * @return P4Cms_Record_Volatile provides fluent interface. */ public function delete() { $file = $this->_getDepotFile(); // masquerade as another client $this->_beginCharade(); try { $this->getAdapter()->getConnection()->run('revert', array('-k', $file)); } catch (Exception $e) { // look away! } // restore original client. $this->_endCharade(); // if an exception occurred, throw it now. if (isset($e)) { throw $e; } return $this; } /** * Set the id of this record. * Extended to clear depotFile anytime id changes. * * @param mixed $id the value of the id of this record. * @return P4Cms_Record_Volatile provides a fluent interface */ public function setId($id) { $this->_depotFile = null; return parent::setId($id); } /** * Load values into this record from Perforce. * Clobbers any existing in-memory values. * * @return P4Cms_Record_Volatile provides fluent interface. */ protected function _populate() { $file = $this->_getDepotFile(); // masquerade as another client $this->_beginCharade(); try { $result = $this->getAdapter()->getConnection()->run('fstat', array('-Oa', $file)); // extract information if no warings (ie. file exists). if (!$result->hasWarnings()) { $this->_values = array(); foreach ((array) $result->getData(0) as $key => $value) { $parts = explode('-', $key, 2); if (count($parts) !== 2 || $parts[0] !== 'openattr') { continue; } $this->_setValue($parts[1], $value); } } else { // warnings indicate no such record. $e = new P4Cms_Record_NotFoundException( "Cannot fetch record: " . $this->getId() . ". No matching record." ); } } catch (Exception $e) { // look away! } // restore original client. $this->_endCharade(); // if an exception occurred, throw it now. if (isset($e)) { throw $e; } return $this; } /** * Run a Perforce command with no exceptions. * * @param string $command the command to run. * @param array|string $params optional - one or more arguments. * @return bool|P4_Result command result or false if it failed. */ protected function _runFree($command, $params) { $adapter = $this->getAdapter(); $p4 = $adapter->getConnection(); try { return $p4->run($command, $params); } catch (P4_Exception $e) { return false; } } /** * Begin masquerading as another client/host. */ protected function _beginCharade() { // nothing to do if not masquerading. $masquerade = $this->getClientMasquerade(); if (!$masquerade) { return; } $adapter = $this->getAdapter(); $p4 = $adapter->getConnection(); // clobber the current client and host, but remember // them so we can restore them afterwards. $this->_originalClient = $p4->getClient(); $this->_originalHost = $p4->getHost(); $p4->setClient($masquerade->getId() ?: $p4->getClient()); $p4->setHost($masquerade->getHost() ?: $p4->getHost()); } /** * Stop masquerading - restore original client/host. */ protected function _endCharade() { // nothing to do if not masquerading. if (!$this->_originalClient) { return; } $adapter = $this->getAdapter(); $p4 = $adapter->getConnection(); $p4->setClient($this->_originalClient); $p4->setHost($this->_originalHost); $this->_originalClient = null; $this->_originalHost = null; } /** * Get the (depot-syntax formatted) path to this record in Perforce. * * @return string the path to this record in depot-syntax. */ protected function _getDepotFile() { // must have an id to get depot file. if (!$this->getId()) { throw new P4Cms_Record_Exception("Cannot get record file path without an id."); } // convert id to depot file syntax if we haven't already. if (!$this->_depotFile) { $adapter = $this->getAdapter(); $subPath = trim($this->_storageSubPath, '/\\'); $subPath = $subPath ? '/' . $subPath . '/' : '/'; $file = $adapter->getBasePath() . $subPath . $this->getId(); $result = $adapter->getConnection()->run('where', $file); $this->_depotFile = $result->getData(0, 'depotFile'); } return $this->_depotFile; } }