<?php
/**
* Provides a base for singular spec models such as protections,
* triggers, typemap, etc. to extend.
*
* Keyed specs such as changes, jobs, users, etc. should extend
* P4\Spec\PluralAbstract.
*
* When extending this class, be sure to set the SPEC_TYPE const
* to the name of the Perforce Specification Type (e.g. protect,
* typemap, etc.)
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace P4\Spec;
use P4\Spec\Exception\Exception;
use P4\Connection\ConnectionInterface;
use P4\Model\Fielded\FieldedAbstract;
class SingularAbstract extends FieldedAbstract
{
const SPEC_TYPE = null;
protected $needsPopulate = false;
protected $specDefinition = null;
/**
* Get this spec from Perforce.
* Creates a new spec instance and schedules a populate.
*
* @param ConnectionInterface $connection optional - a specific connection to use.
* @return PluralAbstract instace of the requested entry.
* @throws \InvalidArgumentException if no id is given.
*/
public static function fetch(ConnectionInterface $connection = null)
{
$spec = new static($connection);
$spec->deferPopulate();
return $spec;
}
/**
* Gets the definition of this specification from Perforce.
*
* The specification definition provides: field names,
* field types, field options, preset values, comments, etc.
*
* Only fetches it once per instance. Additionally, the spec
* definition object has a per-process (static) cache.
*
* @return Definition instance containing details about this spec type.
*/
public function getSpecDefinition()
{
// load the spec definition if we haven't already done so.
if (!$this->specDefinition instanceof Definition) {
$this->specDefinition = Definition::fetch(
static::SPEC_TYPE,
$this->getConnection()
);
}
return $this->specDefinition;
}
/**
* Get all of the spec field names.
* Extended to pull fields from the spec definition.
*
* @return array a list of field names for this spec.
*/
public function getFields()
{
$fields = $this->getSpecDefinition()->getFields();
return array_keys($fields);
}
/**
* Get all of the required fields.
*
* @return array a list of required fields for this spec.
*/
public function getRequiredFields()
{
$fields = array();
$spec = $this->getSpecDefinition();
foreach ($this->getFields() as $field) {
if ($spec->isRequiredField($field)) {
$fields[] = $field;
}
}
return $fields;
}
/**
* Save this spec to Perforce.
*
* @return SingularAbstract provides a fluent interface
*/
public function save()
{
// ensure all required fields have values.
$this->validateRequiredFields();
$this->getConnection()->run(
static::SPEC_TYPE,
"-i",
$this->getRawValues()
);
// should re-populate (server may change values).
$this->deferPopulate(true);
return $this;
}
/**
* Get a field's raw value (avoids accessor).
*
* Extended to limit to defined fields, to lazy-load values and to
* pull default values from the spec definition for required fields.
*
* @param string $field the name of the field to get the value of.
* @return mixed the value of the field.
* @throws Exception if the field does not exist.
*/
public function getRawValue($field)
{
// if field doesn't exist, throw exception.
if (!$this->hasField($field)) {
throw new Exception("Can't get the value of a non-existant field.");
}
// if field has not been set, populate.
if (!array_key_exists($field, $this->values)) {
$this->populate();
}
// if field has a value, return it.
if (array_key_exists($field, $this->values)) {
return $this->values[$field];
}
// get default value if field is required - return null for
// optional fields so that they don't get values automatically.
// optional field defaults are best handled by the server.
if ($this->getSpecDefinition($this->getConnection())->isRequiredField($field)) {
return $this->getDefaultValue($field);
} else {
return null;
}
}
/**
* Set a field's raw value (avoids mutator).
* Extended to limit to setting defined fields that are not read-only.
*
* @param string $field the name of the field to set the value of.
* @param mixed $value the value to set in the field.
* @return SingularAbstract provides a fluent interface
* @throws Exception if the field does not exist or is read-only
*/
public function setRawValue($field, $value)
{
// if field doesn't exist, throw exception.
if (!$this->hasField($field)) {
throw new Exception("Can't set the value of a non-existant field.");
}
// if field is read-only, throw exception.
if ($this->getSpecDefinition()->isReadOnlyField($field)) {
throw new Exception("Can't set the value of a read-only field.");
}
$this->values[$field] = $value;
return $this;
}
/**
* Schedule populate to run when data is requested (lazy-load).
*
* @param bool $reset optionally clear instance values.
*/
public function deferPopulate($reset = false)
{
$this->needsPopulate = true;
if ($reset) {
$this->values = array();
}
}
/**
* Get the values for this spec from Perforce and set them
* in the instance. Won't clobber existing values.
*/
protected function populate()
{
// early exit if populate not needed.
if (!$this->needsPopulate) {
return;
}
// get spec data from Perforce.
$data = $this->getSpecData();
// ensure fields is an array.
if (!is_array($data)) {
throw new Exception("Failed to populate spec. Perforce result invalid.");
}
// copy field values to instance without clobbering.
foreach ($data as $key => $value) {
if (!array_key_exists($key, $this->values)) {
$this->values[$key] = $value;
}
}
// clear needs populate flag.
$this->needsPopulate = false;
}
/**
* Get a field's default value.
*
* @param string $field the name of the field to get the default value of.
* @return mixed the default value of the field.
*/
protected function getDefaultValue($field)
{
$definition = $this->getSpecDefinition();
$field = $definition->getField($field);
if (isset($field['default'])) {
return $definition::expandDefault($field['default'], $this->getConnection());
} else {
return null;
}
}
/**
* Get raw spec data direct from Perforce. No caching involved.
*
* @return array $data the raw spec output from Perforce.
*/
protected function getSpecData()
{
$result = $this->getConnection()->run(static::SPEC_TYPE, "-o");
return $result->expandSequences()->getData(-1);
}
/**
* Ensure that all required fields have values.
*
* @param array $values optional - set of values to validate against
* defaults to instance values.
* @throws Exception if any required fields are missing values.
*/
protected function validateRequiredFields($values = null)
{
$values = (array) $values ?: $this->getRawValues();
// check that each required field has a value.
foreach ($this->getRequiredFields() as $field) {
$value = isset($values[$field]) ? $values[$field] : null;
// in order to satisfy a required field, array values
// must have elements and all values must have string length.
if ((is_array($value) && !count($value)) || (!is_array($value) && !strlen($value))) {
$missing[] = $field;
}
}
if (isset($missing)) {
throw new Exception(
"Cannot save spec. Missing required fields: " . implode(", ", $missing)
);
}
}
}