__set * * @var string */ protected $propertyName; public function __construct($values = null) { if (null !== $values) { $this->override($values); } } public static function forIcingaObject(IcingaObject $object, $propertyName) { $set = new static; $set->object = $object; $set->propertyName = $propertyName; if ($object->hasBeenLoadedFromDb()) { $set->loadFromDb(); } return $set; } public function set($set) { if (null === $set) { $this->reset(); return $this; } elseif (is_array($set) || is_string($set)) { $this->reset(); $this->override($set); } elseif (is_object($set)) { $this->reset(); foreach (['override', 'extend', 'blacklist'] as $method) { if (property_exists($set, $method)) { $this->$method($set->$method); } } } else { throw new ProgrammingError( 'ExtensibleSet::set accepts only plain arrays or objects' ); } return $this; } public function isEmpty() { return $this->ownValues === null && empty($this->plusValues) && empty($this->minusValues); } public function toPlainObject() { if ($this->ownValues !== null) { if (empty($this->minusValues) && empty($this->plusValues)) { return $this->ownValues; } } $plain = (object) []; if ($this->ownValues !== null) { $plain->override = $this->ownValues; } if (! empty($this->plusValues)) { $plain->extend = $this->plusValues; } if (! empty($this->minusValues)) { $plain->blacklist = $this->minusValues; } return $plain; } public function getPlainUnmodifiedObject() { if ($this->fromDb === null) { return null; } $old = $this->fromDb; if ($old['override'] !== null) { if (empty($old['blacklist']) && empty($old['extend'])) { return $old['override']; } } $plain = (object) []; if ($old['override'] !== null) { $plain->override = $old['override']; } if (! empty($old['extend'])) { $plain->extend = $old['extend']; } if (! empty($old['blacklist'])) { $plain->blacklist = $old['blacklist']; } return $plain; } public function hasBeenLoadedFromDb() { return $this->fromDb !== null; } public function hasBeenModified() { if ($this->hasBeenLoadedFromDb()) { if ($this->ownValues !== $this->fromDb['override']) { return true; } if ($this->plusValues !== $this->fromDb['extend']) { return true; } if ($this->minusValues !== $this->fromDb['blacklist']) { return true; } return false; } else { if ($this->ownValues === null && empty($this->plusValues) && empty($this->minusValues) ) { return false; } else { return true; } } } protected function loadFromDb() { $db = $this->object->getDb(); $query = $db->select()->from($this->tableName(), [ 'property', 'merge_behaviour' ])->where($this->foreignKey() . ' = ?', $this->object->get('id')); $byBehaviour = [ 'override' => [], 'extend' => [], 'blacklist' => [], ]; foreach ($db->fetchAll($query) as $row) { if (! array_key_exists($row->merge_behaviour, $byBehaviour)) { throw new ProgrammingError( 'Got unknown merge_behaviour "%s". Schema change?', $row->merge_behaviour ); } $byBehaviour[$row->merge_behaviour][] = $row->property; } foreach ($byBehaviour as $method => &$values) { if (empty($values)) { continue; } sort($values); $this->$method($values); } if (empty($byBehaviour['override'])) { $byBehaviour['override'] = null; } $this->fromDb = $byBehaviour; return $this; } protected function foreignKey() { return $this->object->getShortTableName() . '_id'; } protected function tableName() { return implode('_', [ $this->object->getTableName(), $this->propertyName, 'set' ]); } public function getObject() { return $this->object; } public function store() { if (null === $this->object) { throw new ProgrammingError( 'Cannot store ExtensibleSet with no assigned object' ); } if (! $this->hasBeenModified()) { return false; } $this->storeToDb(); return true; } protected function storeToDb() { $db = $this->object->getDb(); if ($db === null) { throw new ProgrammingError( 'Cannot store a set for an unstored related object' ); } $table = $this->tableName(); $props = [ $this->foreignKey() => $this->object->get('id') ]; $db->delete( $this->tableName(), $db->quoteInto( $this->foreignKey() . ' = ?', $this->object->get('id') ) ); if ($this->ownValues !== null) { $props['merge_behaviour'] = 'override'; foreach ($this->ownValues as $value) { $db->insert( $table, array_merge($props, ['property' => $value]) ); } } if (! empty($this->plusValues)) { $props['merge_behaviour'] = 'extend'; foreach ($this->plusValues as $value) { $db->insert( $table, array_merge($props, ['property' => $value]) ); } } if (! empty($this->minusValues)) { $props['merge_behaviour'] = 'blacklist'; foreach ($this->minusValues as $value) { $db->insert( $table, array_merge($props, ['property' => $value]) ); } } $this->setBeingLoadedFromDb(); } public function setBeingLoadedFromDb() { $this->fromDb = [ 'override' => $this->ownValues ?: [], 'extend' => $this->plusValues ?: [], 'blacklist' => $this->minusValues ?: [], ]; } public function override($values) { $this->ownValues = []; $this->inheritedValues = []; $this->addValuesTo($this->ownValues, $values); return $this->addResolvedValues($values); } public function extend($values) { $this->addValuesTo($this->plusValues, $values); return $this->addResolvedValues($values); } public function blacklist($values) { $this->addValuesTo($this->minusValues, $values); if ($this->hasBeenResolved()) { $this->removeValuesFrom($this->resolvedValues, $values); } return $this; } public function getResolvedValues() { if (! $this->hasBeenResolved()) { $this->recalculate(); } sort($this->resolvedValues); return $this->resolvedValues; } public function inheritFrom(ExtensibleSet $parent) { if ($this->ownValues !== null) { return $this; } if ($this->hasBeenResolved()) { $this->resolvedValues = null; } $this->inheritedValues = []; $this->addValuesTo( $this->inheritedValues, $this->stripBlacklistedValues($parent->getResolvedValues()) ); return $this->recalculate(); } public function forgetInheritedValues() { $this->inheritedValues = []; return $this; } protected function renderArray($array) { $safe = []; foreach ($array as $value) { $safe[] = c::alreadyRendered($value); } return c::renderArray($safe); } public function renderAs($key, $prefix = ' ') { $parts = []; // TODO: It would be nice if we could use empty arrays to override // inherited ones // if ($this->ownValues !== null) { if (!empty($this->ownValues)) { $parts[] = c::renderKeyValue( $key, $this->renderArray($this->ownValues), $prefix ); } if (!empty($this->plusValues)) { $parts[] = c::renderKeyOperatorValue( $key, '+=', $this->renderArray($this->plusValues), $prefix ); } if (!empty($this->minusValues)) { $parts[] = c::renderKeyOperatorValue( $key, '-=', $this->renderArray($this->minusValues), $prefix ); } return implode('', $parts); } public function isRestricted() { return $this->allowedValues === null; } public function enumAllowedValues() { if ($this->isRestricted()) { throw new ProgrammingError( 'No allowed value set available, this set is not restricted' ); } if (empty($this->allowedValues)) { return []; } return array_combine($this->allowedValues, $this->allowedValues); } protected function hasBeenResolved() { return $this->resolvedValues !== null; } protected function stripBlacklistedValues($array) { $this->removeValuesFrom($array, $this->minusValues); return $array; } protected function assertValidValue($value) { if (null === $this->allowedValues) { return $this; } if (in_array($value, $this->allowedValues)) { return $this; } throw new InvalidPropertyException( 'Got invalid property "%s", allowed are: (%s)', $value, implode(', ', $this->allowedValues) ); } protected function addValuesTo(&$array, $values) { foreach ($this->wantArray($values) as $value) { // silently ignore null or empty strings if (strlen($value) === 0) { continue; } $this->addTo($array, $value); } return $this; } protected function addResolvedValues($values) { if (! $this->hasBeenResolved()) { $this->resolvedValues = []; } return $this->addValuesTo( $this->resolvedValues, $this->stripBlacklistedValues($this->wantArray($values)) ); } protected function removeValuesFrom(&$array, $values) { foreach ($this->wantArray($values) as $value) { $this->removeFrom($array, $value); } return $this; } protected function addTo(&$array, $value) { if (! in_array($value, $array)) { $this->assertValidValue($value); $array[] = $value; } return $this; } protected function removeFrom(&$array, $value) { if (false !== ($pos = array_search($value, $array))) { unset($array[$pos]); } return $this; } protected function recalculate() { $this->resolvedValues = []; if ($this->ownValues === null) { $this->addValuesTo($this->resolvedValues, $this->inheritedValues); } else { $this->addValuesTo($this->resolvedValues, $this->ownValues); } $this->addValuesTo($this->resolvedValues, $this->plusValues); $this->removeFrom($this->resolvedValues, $this->minusValues); return $this; } protected function reset() { $this->ownValues = null; $this->plusValues = []; $this->minusValues = []; $this->resolvedValues = null; $this->inheritedValues = []; return $this; } protected function translate($string) { return mt('director', $string); } protected function wantArray($values) { if (is_array($values)) { return $values; } return [$values]; } }