- <?php
- /**
- * Abstracts operations against Perforce clients/workspaces.
- *
- * @copyright 2011 Perforce Software. All rights reserved.
- * @license Please see LICENSE.txt in top-level folder of this distribution.
- * @version <release>/<patch>
- * @todo Add support for 'p4 opened'
- * @todo Deal with converting updated/created date/times from listing format
- * @todo Return DateTime objects as appropriate
- */
-
- namespace P4\Spec;
-
- use P4\Validate;
- use P4\Connection\ConnectionInterface;
-
- class Client extends PluralAbstract
- {
- const SPEC_TYPE = 'client';
- const ID_FIELD = 'Client';
-
- const FETCH_BY_NAME = 'name';
- const FETCH_BY_OWNER = 'owner';
- const FETCH_BY_STREAM = 'stream';
-
- protected $fields = array(
- 'Update' => array(
- 'accessor' => 'getUpdateDateTime'
- ),
- 'Access' => array(
- 'accessor' => 'getAccessDateTime'
- ),
- 'Owner' => array(
- 'accessor' => 'getOwner',
- 'mutator' => 'setOwner'
- ),
- 'Host' => array(
- 'accessor' => 'getHost',
- 'mutator' => 'setHost'
- ),
- 'Description' => array(
- 'accessor' => 'getDescription',
- 'mutator' => 'setDescription'
- ),
- 'Root' => array(
- 'accessor' => 'getRoot',
- 'mutator' => 'setRoot'
- ),
- 'Options' => array(
- 'accessor' => 'getOptions',
- 'mutator' => 'setOptions'
- ),
- 'SubmitOptions' => array(
- 'accessor' => 'getSubmitOptions',
- 'mutator' => 'setSubmitOptions'
- ),
- 'LineEnd' => array(
- 'accessor' => 'getLineEnd',
- 'mutator' => 'setLineEnd'
- ),
- 'View' => array(
- 'accessor' => 'getView',
- 'mutator' => 'setView'
- ),
- 'Stream' => array(
- 'accessor' => 'getStream',
- 'mutator' => 'setStream'
- )
- );
-
- /**
- * Get all Clients 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_NAME - set to client name pattern (e.g. 'pc*').
- * FETCH_BY_OWNER - set to owner's username (e.g. 'jdoe').
- * FETCH_BY_STREAM - set to stream name (e.g. '//depotname/string').
- *
- * @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)
- {
- // simply return parent - method exists to document options.
- return parent::fetchAll($options, $connection);
- }
-
- /**
- * Save this spec to Perforce.
- * Extends parent to blank out the 'View' if a stream is specified. You cannot
- * edit the view on a stream spec but leaving it can cause errors.
- *
- * @return SpecAbstract provides a fluent interface
- */
- public function save()
- {
- if ($this->get('Stream')) {
- $this->set('View', array());
- }
-
- return parent::save();
- }
-
- /**
- * Remove this client. Extends parent to offer force delete.
- *
- * @param boolean $force pass true to force delete this client.
- * @return Client provides fluent interface.
- */
- public function delete($force = false)
- {
- return parent::delete($force ? array('-f') : null);
- }
-
- /**
- * Determine if the given client id exists.
- *
- * @param string $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 client.
- */
- public static function exists($id, ConnectionInterface $connection = null)
- {
- // check id for valid format
- if (!static::isValidId($id)) {
- return false;
- }
-
- $clients = static::fetchAll(
- array(
- static::FETCH_BY_NAME => $id,
- static::FETCH_MAXIMUM => 1
- ),
- $connection
- );
-
- return (bool) count($clients);
- }
-
- /**
- * Extends the parent temp cleanup callback to try reverting any
- * files prior to client deletion. This won't always be successful,
- * but it will reduce the number of temp clients that cannot be
- * deleted due to open files.
- *
- * @return callable A callback function with the signature function($entry)
- */
- protected static function getTempCleanupCallback()
- {
- $parentCallback = parent::getTempCleanupCallback();
-
- return function ($entry) use ($parentCallback) {
- $p4 = $entry->getConnection();
- $original = $p4->getClient();
- $p4->setClient($entry->getId());
-
- // try to revert any open files - if this fails we
- // want to carry on so the original client gets restored
- // and we still attempt to delete the client spec.
- try {
- $p4->run('revert', array('-k', '//...'));
- } catch (\Exception $e) {
- // carry on!
- }
-
- // restore the original client
- $p4->setClient($original);
-
- // let parent delete the spec entry.
- return $parentCallback($entry);
- };
- }
-
- /**
- * Get the last update time for this client spec.
- * This value is read only, no setUpdateTime function is provided.
- *
- * If this is a brand new spec, null will be returned in lieu of a time.
- *
- * @return string|null Date/Time of last update, formatted "2009/11/23 12:57:06" or null
- */
- public function getUpdateDateTime()
- {
- return $this->getRawValue('Update');
- }
-
- /**
- * Get the last access time for this client spec.
- * This value is read only, no setAccessTime function is provided.
- *
- * If this is a brand new spec, null will be returned in lieu of a time.
- *
- * @return string|null Date/Time of last access, formatted "2009/11/23 12:57:06" or null
- */
- public function getAccessDateTime()
- {
- return $this->getRawValue('Access');
- }
-
- /**
- * Get the owner of this client.
- *
- * @return string|null User who owns this record.
- */
- public function getOwner()
- {
- return $this->getRawValue('Owner');
- }
-
- /**
- * Set the owner of this client to passed value.
- *
- * @param string|null $owner A string containing username
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException Owner is incorrect type.
- */
- public function setOwner($owner)
- {
- if (!is_string($owner) && !is_null($owner)) {
- throw new \InvalidArgumentException('Owner must be a string or null.');
- }
-
- return $this->setRawValue('Owner', $owner);
- }
-
- /**
- * Get the host setting for this client.
- *
- * @return string|null Host name set for this client, empty string for any.
- */
- public function getHost()
- {
- return $this->getRawValue('Host');
- }
-
- /**
- * If set, restricts access to the named host. Specify a blank string or null
- * to allow access from all hosts.
- *
- * @param string|null $host Host name for this client, empty string or null for any
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException Host is incorrect type.
- */
- public function setHost($host)
- {
- if (!is_string($host) && !is_null($host)) {
- throw new \InvalidArgumentException('Host must be a string or null.');
- }
-
- return $this->setRawValue('Host', $host);
- }
-
- /**
- * Get the description for this client.
- *
- * @return string|null description for this client.
- */
- public function getDescription()
- {
- return $this->getRawValue('Description');
- }
-
- /**
- * Set a description for this client.
- *
- * @param string|null $description description for this client.
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException Description is incorrect type.
- */
- public function setDescription($description)
- {
- if (!is_string($description) && !is_null($description)) {
- throw new \InvalidArgumentException('Description must be a string or null.');
- }
-
- return $this->setRawValue('Description', $description);
- }
-
- /**
- * Get the base directory of the client workspace.
- *
- * @return string|null Base directory of the client workspace.
- */
- public function getRoot()
- {
- return $this->getRawValue('Root');
- }
-
- /**
- * Set the base directory of the client workspace.
- *
- * @param string|null $root Base directory for the client workspace.
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException Root is incorrect type.
- */
- public function setRoot($root)
- {
- if (!is_string($root) && !is_null($root)) {
- throw new \InvalidArgumentException('Root must be a string or null.');
- }
-
- return $this->setRawValue('Root', $root);
- }
-
- /**
- * Get options for this client.
- * Returned array will contain one option per element e.g.:
- * array (
- * 0 => 'noallwrite',
- * 1 => 'noclobber',
- * 2 => 'nocompress',
- * 3 => 'unlocked',
- * 4 => 'nomodtime',
- * 5 => 'rmdir'
- * )
- *
- * @return array options which are set on this client.
- */
- public function getOptions()
- {
- $options = $this->getRawValue('Options');
- $options = explode(' ', $options);
-
- // Explode will set key 0 to null for empty input; clean it up.
- if (count($options) == 1 && empty($options[0])) {
- $options = array();
- }
-
- return $options;
- }
-
- /**
- * Set the options for this client.
- * Accepts an array, format detailed in getOptions, or a single string containing
- * a space seperated list of options.
- *
- * @param array|string $options options to set on this client in array or string.
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException Options are incorrect type.
- */
- public function setOptions($options)
- {
- if (is_array($options)) {
- $options = implode(' ', $options);
- }
-
- if (!is_string($options)) {
- throw new \InvalidArgumentException('Options must be an array or string');
- }
-
- return $this->setRawValue('Options', $options);
- }
-
- /**
- * Get the submit options for this client.
- * Returned array will contain one option per element e.g.:
- * array (
- * 0 => 'submitunchanged'
- * )
- *
- * @return array submit options which are set on this client.
- */
- public function getSubmitOptions()
- {
- $options = $this->getRawValue('SubmitOptions');
- $options = explode(' ', $options);
-
- // Explode will set key 0 to null for empty input; clean it up.
- if (count($options) == 1 && empty($options[0])) {
- $options = array();
- }
-
- return $options;
- }
-
- /**
- * Set the submit options for this client.
- * Accepts an array, format detailed in getSubmitOptions, or a single string
- * containing a space seperated list of options.
- *
- * @param array|string $options submit options to set on this client in array or string
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException Submit Options are incorrect type.
- */
- public function setSubmitOptions($options)
- {
- if (is_array($options)) {
- $options = implode(' ', $options);
- }
-
- if (!is_string($options)) {
- throw new \InvalidArgumentException('Submit Options must be an array or string');
- }
-
- return $this->setRawValue('SubmitOptions', $options);
- }
-
- /**
- * Get the line ending setting for this client.
- * Will be one of: local/unix/mac/win/share
- *
- * @return string|null Line ending setting for this client.
- */
- public function getLineEnd()
- {
- return $this->getRawValue('LineEnd');
- }
-
- /**
- * Set the line ending setting for this client.
- * See getLineEnd for available options.
- *
- * @param string|null $lineEnd Line ending setting for this client.
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException lineEnd is incorrect type.
- */
- public function setLineEnd($lineEnd)
- {
- if (!is_string($lineEnd) && !is_null($lineEnd)) {
- throw new \InvalidArgumentException('Line End must be a string or null.');
- }
-
- return $this->setRawValue('LineEnd', $lineEnd);
- }
-
- /**
- * Get the view for this client.
- * View entries will be returned as an array with 'depot' and 'client' entries, e.g.:
- * array (
- * 0 => array (
- * 'depot' => '//depot/example/with space/...',
- * 'client' => '//client.name/...'
- * )
- * )
- *
- * @return array list view entries for this client.
- */
- public function getView()
- {
- // The raw view data is formatted as:
- // array (
- // 0 => '"//depot/example/with space/..." //client.name/...',
- // )
- //
- // We split this into 'depot' and 'client' components via the str_getcsv function
- // and key the two resulting entries as 'depot' and 'client'
- $view = array();
- // The ?: translates empty views into an empty array
- foreach ($this->getRawValue('View') ?: array() as $entry) {
- $entry = str_getcsv($entry, ' ');
- $view[] = array_combine(array('depot','client'), $entry);
- }
-
- return $view;
- }
-
- /**
- * Set the view for this client.
- * View is passed as an array of view entries. Each view entry can be an array with
- * 'depot' and 'client' entries or a raw string.
- *
- * @param array $view View entries, formatted into depot/client sub-arrays.
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException View array, or a view entry, is incorrect type.
- */
- public function setView($view)
- {
- if (!is_array($view)) {
- throw new \InvalidArgumentException('View must be passed as array.');
- }
-
- // The View array contains either:
- // - Child arrays keyed on depot/client which we glue together
- // - Raw strings which we simply leave as is
- // The below foreach run will normalize the whole thing for storage
- $parsedView = array();
- foreach ($view as $entry) {
- if (is_array($entry) &&
- isset($entry['depot'], $entry['client']) &&
- is_string($entry['depot']) &&
- is_string($entry['client'])) {
- $entry = '"'. $entry['depot'] .'" "'. $entry['client'] .'"';
- }
-
- if (!is_string($entry)) {
- throw new \InvalidArgumentException(
- "Each view entry must be a 'depot' and 'client' array or a string."
- );
- }
-
- $validate = str_getcsv($entry, ' ');
- if (count($validate) != 2 || trim($validate[0]) === '' || trim($validate[1]) === '') {
- throw new \InvalidArgumentException(
- "Each view entry must contain two paths, no more, no less."
- );
- }
-
- $parsedView[] = $entry;
- };
-
- return $this->setRawValue('View', $parsedView);
- }
-
- /**
- * Add a view mapping to this client.
- *
- * @param string $depot the depot half of the view mapping.
- * @param string $client the client half of the view mapping.
- * @return Client provides a fluent interface.
- */
- public function addView($depot, $client)
- {
- $mappings = $this->getView();
- $mappings[] = array("depot" => $depot, "client" => $client);
-
- return $this->setView($mappings);
- }
-
- /**
- * Updates the 'client' half of the view to ensure the
- * current client ID is used.
- *
- * @return Client provides a fluent interface.
- */
- public function touchUpView()
- {
- $view = $this->getView();
- foreach ($view as &$mapping) {
- $mapping['client'] = preg_replace(
- "#//[^/]*/#",
- '//' . $this->getId() . '/',
- $mapping['client']
- );
- }
- $this->setView($view);
-
- return $this;
- }
-
- /**
- * Get the stream this client is dedicated to.
- *
- * @return string|null Stream setting for this client.
- */
- public function getStream()
- {
- return $this->getRawValue('Stream');
- }
-
- /**
- * Set the stream this client is dedicated to.
- *
- * @param string|null $stream stream setting for this client.
- * @return Client provides a fluent interface.
- * @throws \InvalidArgumentException stream is incorrect type.
- * @todo Validate stream id
- */
- public function setStream($stream)
- {
- if (!is_string($stream) && !is_null($stream)) {
- throw new \InvalidArgumentException('Stream must be a string or null.');
- }
-
- return $this->setRawValue('Stream', $stream);
- }
-
- /**
- * Check if the given id is in a valid format for this spec type.
- *
- * @param string $id the id to check
- * @return bool true if id is valid, false otherwise
- */
- protected static function isValidId($id)
- {
- $validator = new Validate\SpecName;
- $validator->allowRelative(true);
- $validator->allowPercent(false);
- $validator->allowCommas(false);
- return $validator->isValid($id);
- }
-
- /**
- * 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_NAME])) {
- $name = $options[static::FETCH_BY_NAME];
-
- if (!is_string($name) || trim($name) === '') {
- throw new \InvalidArgumentException(
- 'Filter by Name expects a non-empty string as input'
- );
- }
-
- $flags[] = '-e';
- $flags[] = $name;
- }
-
- if (isset($options[static::FETCH_BY_OWNER])) {
- $owner = $options[static::FETCH_BY_OWNER];
-
- // We allow empty values as this returns clients with no owner
- if (!is_string($owner) || trim($owner) === '') {
- throw new \InvalidArgumentException(
- 'Filter by Owner expects a non-empty string as input'
- );
- }
-
- $flags[] = '-u';
- $flags[] = $owner;
- }
-
- if (isset($options[static::FETCH_BY_STREAM])) {
- $stream = $options[static::FETCH_BY_STREAM];
-
- if (!is_string($stream) || trim($stream) === '') {
- throw new \InvalidArgumentException(
- 'Filter by Stream expects a non-empty string as input'
- );
- }
-
- $flags[] = '-S';
- $flags[] = $stream;
- }
-
- return $flags;
- }
- }