<?php /** * P4PHP Perforce connection implementation. * * This client implementation provides access to the P4PHP extension in a way * that conforms to P4\Connection\ConnectionInterface. This allows the P4PHP extension * and the Perforce Command-Line Client wrapper to be used interchangeably. * * @copyright 2011 Perforce Software. All rights reserved. * @license Please see LICENSE.txt in top-level folder of this distribution. * @version <release>/<patch> */ namespace P4\Connection; use P4; use P4\Connection\CommandResult; class Extension extends AbstractConnection { protected $instance; /** * Constructs a P4 connection object. * * @param string $port optional - the port to connect to. * @param string $user optional - the user to connect as. * @param string $client optional - the client spec to use. * @param string $password optional - the password to use. * @param string $ticket optional - a ticket to use. * @throws P4\Exception if P4PHP is not loaded */ public function __construct( $port = null, $user = null, $client = null, $password = null, $ticket = null ) { // ensure that p4-php is installed. if (!extension_loaded('perforce')) { throw new P4\Exception( 'Cannot create P4 API extension instance. Perforce extension not loaded.' ); } // create an instance of p4-php. $this->instance = new P4; // disable automatic sequence expansion (call expandSequences on result object if desired) $this->instance->expand_sequences = false; // prevent command exceptions from being thrown by P4. // we throw our own so that we can attach the result. $this->instance->exception_level = 0; parent::__construct($port, $user, $client, $password, $ticket); } /** * Disconnect from the Perforce Server. * * @return ConnectionInterface provides fluent interface. */ public function disconnect() { // call parent to run disconnect callbacks. parent::disconnect(); if ($this->isConnected()) { $this->instance->disconnect(); } return $this; } /** * Check connected state. * * @return bool true if connected, false otherwise. */ public function isConnected() { return $this->instance->connected(); } /** * Extends parent to set our instance's password to the returned * ticket value if login succeeds. * * @param bool|null $all get a ticket valid for all hosts (false by default) * @return string|null the ticket issued by the server or null if * no ticket issued (ie. user has no password). * @throws Exception\LoginException if login fails. */ public function login($all = false) { $ticket = parent::login($all); if ($ticket) { $this->instance->password = $ticket; } return $ticket; } /** * Extend set port to update p4-php. * * @param string $port the port to connect to. * @return ConnectionInterface provides fluent interface. */ public function setPort($port) { parent::setPort($port); $this->instance->port = $this->getPort(); return $this; } /** * Extend set user to update p4-php. * * @param string $user the user to connect as. * @return ConnectionInterface provides fluent interface. */ public function setUser($user) { parent::setUser($user); $this->instance->user = $this->getUser(); return $this; } /** * Extend set client to update p4-php. * * @param string $client the name of the client workspace to use. * @return ConnectionInterface provides fluent interface. */ public function setClient($client) { parent::setClient($client); // if no client is specified, normally the host name is used. // this can collide with an existing depot or client name, so // we use a temp id to avoid errors. $this->instance->client = $this->getClient() ?: P4\Spec\Client::makeTempId(); return $this; } /** * Extend set password to update p4-php. * * @param string $password the password to use as authentication. * @return ConnectionInterface provides fluent interface. */ public function setPassword($password) { parent::setPassword($password); $this->instance->password = $this->getPassword(); return $this; } /** * Extend set ticket to update p4-php. * Note: the ticket is stored in the password field in p4-php. * * @param string $ticket the ticket to use as authentication. * @return ConnectionInterface provides fluent interface. */ public function setTicket($ticket) { parent::setTicket($ticket); if ($ticket) { $this->instance->password = $this->getTicket(); } return $this; } /** * Extended to set charset in p4-php. * Sets the character set to use for this perforce connection. * * You should only set a character set when connecting to a * 'unicode enabled' server, or when setting the special value * of 'none'. * * @param string $charset the charset to use (e.g. 'utf8'). * @return ConnectionInterface provides fluent interface. */ public function setCharset($charset) { $this->instance->charset = $charset; return parent::setCharset($charset); } /** * Extended to set host name in p4-php. * Sets the client host name overriding the environment. * * @param string|null $host the host name to use. * @return ConnectionInterface provides fluent interface. */ public function setHost($host) { $this->instance->host = $host; return parent::setHost($host); } /** * Extended to set app name in p4-php. * Set the name of the application that is using this connection. * * @param string|null $name the app name to report to the server. * @return ConnectionInterface provides fluent interface. */ public function setAppName($name) { $this->instance->set_protocol('app', (string) $name); return parent::setAppName($name); } /** * Extended to set program name in p4-php. * Set the name of the program that is using this connection. * * @param string|null $name the program name to report to the server. * @return ConnectionInterface provides fluent interface. */ public function setProgName($name) { $this->instance->prog = (string) $name; return parent::setProgName($name); } /** * Extended to set program version in p4-php. * Set the version of the program that is using this connection. * * @param string|null $version the program version to report to the server. * @return ConnectionInterface provides fluent interface. */ public function setProgVersion($version) { $this->instance->version = (string) $version; return parent::setProgVersion($version); } /** * Get the identity of this Connection implementation. * * Resulting array will contain: * - name * - platform * - version (p4-php version) * - build (p4-php build) * - apiversion (p4-api version) * - apibuild (p4-api build) * - date * - original (all text following 'Rev. ' from original response) * * @return array an array of client Connection information * @throws P4\Exception if the returned version string is invalid */ public function getConnectionIdentity() { // obtain the extension's identification $output = $this->instance->identify(); // extract the version string and split into components preg_match('/\nRev. (.*)\.$/', $output, $matches); $parts = isset($matches[1]) ? preg_split('/\/| \(| API\) \(|\)/', $matches[1]) : null; if (count($parts) < 8) { $message = 'p4php returned an invalid version string'; throw new P4\Exception($message); } // build identity array of version components, including original string $identity = array( 'name' => $parts[0], 'platform' => $parts[1], 'version' => $parts[2], 'build' => $parts[3], 'apiversion' => $parts[4], 'apibuild' => $parts[5], 'date' => $parts[6] . '/' . $parts[7] . '/' . $parts[8], 'original' => $matches[1] ); return $identity; } /** * Runs the specified command using the passed output handler. * Ensures the output handler is turned back off at completion. * * If the handler has a 'reset' method it will be called. This is intended * to give the handler an opportunity to prepare itself for a fresh run. * * @param P4_OutputHandlerAbstract $handler the output handler to use * @param string $command the command to run. * @param array|string $params optional - one or more arguments. * @param array|string $input optional - input for the command - should be provided * in array form when writing perforce spec records. * @param boolean $tagged optional - true/false to enable/disable tagged output. * defaults to true. * @param boolean $ignoreErrors optional - true/false to ignore errors - default false * @return P4\Connection\CommandResult the perforce result object. * @throws \Exception if the command produced an exception */ public function runHandler( $handler, $command, $params = array(), $input = null, $tagged = true, $ignoreErrors = false ) { // if the handler has a 'reset' method call it to ensure its ready if (method_exists($handler, 'reset')) { $handler->reset(); } // set the handler and run our command. $this->instance->handler = $handler; try { $result = $this->run($command, $params, $input, $tagged, $ignoreErrors); } catch (\Exception $e) { // just catch the exception for now; we'll rethrow later } // if the handler 'cancelled' the command, there is a chance the connection has been severed // we run a test command to check the connection and disconnect if that fails // there are a number of oddities to be aware of: // - the handler might not have a wasCancelled() method, in which case we assume it cancelled // - running any command on a severed connection produces no errors/exceptions/data (bug in P4PHP) // - we run 'p4 help' because it locks no tables, produces no errors and has modest output // - by explicitly disconnecting any future commands will automatically reconnect if ((!method_exists($handler, 'wasCancelled') || $handler->wasCancelled()) && !$this->doRun('help')->hasData() ) { $this->disconnect(); } $this->instance->handler = null; // if an exception occurred rethrow it now that we've cleared the handler if (isset($e)) { throw $e; } return $result; } /** * Actually issues a command. Called by run() to perform the dirty work. * * @param string $command the command to run. * @param array $params optional - arguments. * @param array|string $input optional - input for the command - should be provided * in array form when writing perforce spec records. * @param boolean $tagged optional - true/false to enable/disable tagged output. * defaults to true. * @return CommandResult the perforce result object. */ protected function doRun($command, $params = array(), $input = null, $tagged = true) { // push command to front of parameters array array_unshift($params, $command); // set input for the command. if ($input !== null) { $this->instance->input = $input; } // toggle tagged output. $this->instance->tagged = (bool) $tagged; // establish connection to perforce server. if (!$this->isConnected()) { $this->connect(); } // run command. $data = call_user_func_array(array($this->instance, "run"), $params); // collect data in result object and ensure output is in array form. $result = new CommandResult($command, $data, $tagged); $result->setErrors($this->instance->errors); $result->setWarnings($this->instance->warnings); return $result; } /** * Prepare input for passing to the p4 extension. * Ensure input is either a string or an array of strings. * * @param string|array $input the input to prepare for p4. * @param string $command the command to prepare input for. * @return string|array the prepared input. */ protected function prepareInput($input, $command) { // if input is not an array, cast to string and return. if (!is_array($input)) { return (string) $input; } // ensure each element of array is a string. $stringify = function (&$input) { $input = (string) $input; }; array_walk_recursive($input, $stringify); return $input; } /** * Does real work of establishing connection. Called by connect(). * * @throws Exception\ConnectException if the connection fails. */ protected function doConnect() { // temporarily enable exceptions to catch connection failure. $this->instance->exception_level = 1; try { $this->instance->connect(); $this->instance->exception_level = 0; } catch (\P4_Exception $e) { $this->instance->exception_level = 0; throw new Exception\ConnectException( "Connect failed: " . $e->getMessage() ); } } }