<?php
/**
* Abstracts operations against Perforce counters.
*
* This class is somewhat unique as calling set will immediately write the new value
* to perforce; no separate save step is required.
* When reading values out we do attempt to use cached results, to ensure you read
* out the value directly from perforce set $force to true when calling get.
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace P4\Counter;
use P4\Connection\ConnectionInterface;
use P4\Connection\Exception\CommandException;
use P4\Counter\Exception\NotFoundException;
use P4\Model\Connected;
use P4\Exception;
use P4\OutputHandler\Limit;
abstract class AbstractCounter extends Connected\ConnectedAbstract
{
const FETCH_MAXIMUM = 'maximum';
const FETCH_BY_NAME = 'name';
const FETCH_AFTER = 'after';
// flags can be specified by the implementer. they will be included for
// all calls to 'p4 counter' or 'p4 counters' (e.g. -u to swap to keys)
protected static $flags = array();
protected $id = null;
protected $value = null;
/**
* Get the id of this counter.
*
* @return null|string the id of this entry.
*/
public function getId()
{
return $this->id;
}
/**
* Set the id of this counter. Id must be in a valid format or null.
*
* @param null|string $id the id of this entry - pass null to clear.
* @return Counter provides a fluent interface
* @throws \InvalidArgumentException if id does not pass validation.
*/
public function setId($id)
{
if ($id !== null && !static::isValidId($id)) {
throw new \InvalidArgumentException("Cannot set id. Id is invalid.");
}
$this->id = $id;
$this->value = null;
return $this;
}
/**
* Determine if the given counter 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 counter.
*/
public static function exists($id, ConnectionInterface $connection = null)
{
// check id for valid format
if (!static::isValidId($id)) {
return false;
}
$counters = static::fetchAll(
array(static::FETCH_BY_NAME => $id),
$connection
);
return in_array($id, $counters->invoke('getId'));
}
/**
* Get the requested counter from Perforce.
*
* @param string $id the id of the counter to fetch.
* @param ConnectionInterface $connection optional - a specific connection to use.
* @return Counter instace of the requested counter.
* @throws \InvalidArgumentException if invalid id is given.
* @throws NotFoundException if record cannot be located
*/
public static function fetch($id, ConnectionInterface $connection = null)
{
// ensure a valid id is provided.
if (!static::isValidId($id)) {
throw new \InvalidArgumentException("Must supply a valid id to fetch.");
}
// if no connection given, use default.
$connection = $connection ?: static::getDefaultConnection();
$counters = static::fetchAll(
array(static::FETCH_BY_NAME => $id),
$connection
);
// be defensive; we only expect one result but ensure its the correct id
foreach ($counters as $counter) {
if ($counter->getId() == $id) {
return $counter;
}
}
// if we made it here we couldn't find the counter so throw
throw new NotFoundException(
"Cannot fetch entry. Id does not exist."
);
}
/**
* Get all Counters from Perforce.
*
* @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.
* Note: Max limit is imposed client side on <2013.1.
* FETCH_BY_NAME - set to string value to limit to counters
* matching the given name/pattern.
* FETCH_AFTER - set to an id _after_ which we start collecting
* @param ConnectionInterface $connection optional - a specific connection to use.
* @return Connected\Iterator all counters matching passed option(s).
*/
public static function fetchAll($options = array(), ConnectionInterface $connection = null)
{
// if no connection given, use default.
$connection = $connection ?: static::getDefaultConnection();
// check if the server supports filtering counters (-e) or max (-m)
$supportsFilter = $connection->isServerMinVersion('2010.1');
$supportsMax = $connection->isServerMinVersion('2013.1');
// normalize options and pull them out
$options += array(static::FETCH_MAXIMUM => 0, static::FETCH_BY_NAME => null, static::FETCH_AFTER => null);
$max = (int) $options[static::FETCH_MAXIMUM];
$after = $options[static::FETCH_AFTER];
$isAfter = false;
$filter = $options[static::FETCH_BY_NAME];
$pattern = false;
// configure params starting with default flags for this model
$params = static::$flags;
// use server max limiting if we have no after/filters to interfere and its supported
if ($max && !$after && (!$filter || $supportsFilter) && $supportsMax) {
$params[] = '-m';
$params[] = $max;
}
// user server side filtering if possible, fall back to defining regex
if ($filter && $supportsFilter) {
$params[] = '-e';
$params[] = $options[static::FETCH_BY_NAME];
} elseif ($filter) {
$pattern = preg_quote($options[static::FETCH_BY_NAME]);
$pattern = '/^' . str_replace('\*', '.*', $pattern) . '$/';
}
// configure a handler to enforce limit, after and filter
$handler = new Limit;
$handler->setMax($max);
$handler->setFilterCallback(
function ($data) use ($pattern, $after, &$isAfter) {
// skip entries which fail our pattern
if ($pattern && !preg_match($pattern, $data['counter'])) {
return false;
}
// if we have an 'after' and haven't seen it check and skip
if ($after && !$isAfter) {
$isAfter = ($after == $data['counter']);
return false;
}
// made it this far its a good entry don't filter it
return true;
}
);
// convert result data to counter objects.
$result = $connection->runHandler($handler, 'counters', $params);
$counters = new Connected\Iterator;
foreach ($result->getData() as $data) {
// populate a counter and add it to the iterator
try {
$counter = new static($connection);
$counter->setId($data['counter']);
$counter->value = $data['value'];
} catch (\InvalidArgumentException $e) {
// assume id was invalid - ignore.
continue;
}
$counters[] = $counter;
}
return $counters;
}
/**
* Get counter's value.
*
* If a cached value is available it will, by default, be used. If you pass
* true as the $force param you can force the current value to always be
* read out from the perforce server.
*
* @param bool $force optional - false (default) allow cached value
* true ensure current value is read from p4d
* @return mixed the value of the counter.
*/
public function get($force = false)
{
// if we have a cached value and the caller allows it simply return
if (!$force && $this->value !== null) {
return $this->value;
}
$id = $this->getId();
$connection = $this->getConnection();
// if the ID is not set or the ID doesn't exist in perforce, return null
if ($id === null || !static::exists($id, $connection)) {
return null;
}
$params = static::$flags;
$params[] = $id;
$result = $connection->run('counter', $params);
$data = $result->getData();
$value = $data[0]['value'];
// cache the value for later
$this->value = $value;
return $value;
}
/**
* Increment counters value by 1. If the counter doesn't exist it will be
* created and assigned the value 1.
* The update is carried out atomically by the server.
*
* @return string The counters new value
* @throws Exception If the current value is non-numeric
*/
public function increment()
{
$id = $this->getId();
if ($id === null) {
throw new Exception("Cannot increment value. No id has been set.");
}
$params = static::$flags;
$params[] = '-i';
$params[] = $id;
$result = $this->getConnection()->run('counter', $params);
$data = $result->getData();
$value = $data[0]['value'];
// update our value cache
$this->value = $value;
return $value;
}
/**
* Delete this counter entry. We intend implementor to provide an
* actual 'delete' method at which point they can decide if they
* wan't to expose 'force' or not.
*
*
* @param bool $force optional - force delete the counter.
* @return Counter provides a fluent interface
* @throws Exception if no id has been set.
*/
protected function doDelete($force = false)
{
$id = $this->getId();
if ($id === null) {
throw new Exception("Cannot delete. No id has been set.");
}
// setup counter command args.
$params = static::$flags;
if ($force) {
$params[] = '-f';
}
$params[] = "-d";
$params[] = $id;
try {
$this->getConnection()->run('counter', $params);
} catch (CommandException $e) {
if (strpos($e->getMessage(), 'No such counter')) {
throw new NotFoundException(
"Cannot delete entry. Id does not exist."
);
}
throw $e;
}
// clear our cached value
$this->value = null;
return $this;
}
/**
* Set counters value. The value will be immediately written to perforce.
* We intend implementor to provide an actual 'set' method at which
* point they can decide if they wan't to expose 'force' or not.
*
* @param mixed $value the value to set in the counter.
* @param bool $force optional - force set the counter.
* @return Counter provides a fluent interface
* @throws Exception if no Id has been set
*/
protected function doSet($value, $force = false)
{
$id = $this->getId();
if ($id === null) {
throw new Exception("Cannot set value. No id has been set.");
}
// setup counter command args.
$params = static::$flags;
if ($force) {
$params[] = '-f';
}
$params[] = $id;
$params[] = $value;
$this->getConnection()->run('counter', $params);
// update our value cache
$this->value = $value;
return $this;
}
/**
* Check if the given id is in a valid format.
*
* @param string $id the id to check
* @return bool true if id is valid, false otherwise
*/
protected static function isValidId($id)
{
$validator = new \P4\Validate\CounterName;
return $validator->isValid($id);
}
}