/** * Makes user adopting current role. * * @param user $user user to adopt current role * @return $this * @throws \Exception on failing to adopt role */ public function makeAdoptedBy(user $user) { $role = $this; if (!$this->_source->transaction()->wrap(function (datasource\connection $db) use($role, $user) { $userID = $user->getUUID(); $roleID = $role->id; $count = $db->createQuery('user_role')->addCondition('user_uuid=?', true, $userID)->addCondition('role_id=?', true, $roleID)->execute(true)->cell(); if ($count > 0) { // role has been adopted before return true; } $qSet = $db->qualifyDatasetName('user_role'); return $db->test('INSERT INTO ' . $qSet . ' (user_uuid,role_id) VALUES (?,?)', $userID, $roleID) !== false; })) { throw new \RuntimeException(sprintf('adopting role %s by user %s failed', $this->label, $user->getName())); } return $this; }
/** * Removes current instance of model from datasource. * * @note On deleting current instance all tightly related instances are * deleted implicitly. * * @note On deleting current instance this object is released and can't be * used for managing model instance, anymore. * * @throws \RuntimeException * @throws datasource_exception */ public function delete() { if (!is_array($this->_id)) { throw new \RuntimeException('item does not exist (anymore)'); } /* * wrap deletion of item and its tight relations in a transaction */ $item = $this; $set = static::set(); $relations = static::$relations; $class = get_called_class(); if (!$this->_source->transaction()->wrap(function (connection $connection) use($item, $set, $relations, $class) { // cache information on item to delete $idValues = array_values($item->id()); $record = count($relations) ? $item->load() : array(); /* * step 1) actually delete current item */ $qSet = $connection->qualifyDatasetName($set); if ($connection->test(sprintf('DELETE FROM %s WHERE %s', $qSet, $item->filter()), $idValues) === false) { throw new datasource_exception($connection, 'failed to delete requested model instance'); } /* * step 2) update all related items */ // on deleting item relations have to be updated // - always null references on current item to be deleted foreach ($relations as $relationName => $relationSpec) { // detect if relation is "tight" $isTightlyBound = in_array('tight', (array) @$relationSpec['options']) || @$relationSpec['options']['tight']; // get first reference in relation /** @var model_relation $relation */ $relation = call_user_func(array($class, "relation"), $relationName); $firstNode = $relation->nodeAtIndex(0); $secondNode = $relation->nodeAtIndex(1); $secondNodeSet = $secondNode->getName(true); // prepare collection of information on second node $onSecond = array('null' => array(), 'filter' => array('properties' => array(), 'values' => array())); // extract reusable code to prepare filter for selecting record // of second node actually related to deleted item $getFilter = function () use(&$onSecond, $connection, $firstNode, $secondNode, $record) { // retrieve _qualified and quoted_ names of predecessor's properties $onSecond['filter']['properties'] = $secondNode->getPredecessorNames($connection); foreach ($firstNode->getSuccessorNames() as $property) { $onSecond['filter']['values'][] = @$record[$property]; } }; // inspect type of relationship between first and second node of // current relation if ($secondNode->canBindOnPredecessor()) { // second node in relation is referencing first one // -> there are items of second node's model referring to // item removed above // -> find records of all those items ... $getFilter(); // ... at least for nulling their references on deleted item $onSecond['null'] = $onSecond['filter']['properties']; } else { // first node in relation is referencing second one // -> deleted item was referencing item of second node's model // -> there is basically no need to update any foreign // references on deleted item if ($isTightlyBound) { // relation is marked as "tight" // -> need to delete item referenced by deleted item $getFilter(); } } // convert filtering properties of second node into set of assignments $filter = array_map(function ($name) { return "{$name}=?"; }, $onSecond['filter']['properties']); if ($isTightlyBound) { // in tight relation immediately related elements are // deleted as well $secondModel = $secondNode->getModel(); if ($secondModel->isVirtual()) { // second model is virtual, only // -> it's okay to simply delete matching records in datasource $qSet = $connection->qualifyDatasetName($secondNodeSet); $term = implode(' AND ', $filter); if (!$connection->test("DELETE FROM {$qSet} WHERE {$term}", $onSecond['filter']['values'])) { throw new datasource_exception($connection, 'failed to delete instances of tightly related items in relation ' . $relationName); } // TODO: add support for tightly bound relation in opposite reference of this virtual node } else { // query data source for IDs of all tightly related items $query = $connection->createQuery($secondNodeSet); // - select related items using properties involved in relation foreach ($onSecond['filter']['properties'] as $index => $name) { $query->addFilter("{$name}=?", true, $onSecond['filter']['values'][$index]); } // - fetch all properties used to identify items $ids = $secondModel->getIdProperties(); foreach ($ids as $index => $name) { $query->addProperty($connection->quoteName($name), "i{$index}"); } // iterate over all matches for deleting every one $matches = $query->execute(); $iCount = count($ids); while ($match = $matches->row()) { // extract properly sorted ID from matching record $id = array(); for ($i = 0; $i < $iCount; $i++) { $id[$ids[$i]] = $match["i{$i}"]; } // select item of model and delete it $secondModel->selectInstance($connection, $id)->delete(); } } } else { if (count($onSecond['null'])) { // need to null foreign references on deleted item $values = array_merge(array_pad(array(), count($onSecond['filter']['values']), null), $onSecond['filter']['values']); $qSet = $connection->qualifyDatasetName($secondNodeSet); $matching = implode(' AND ', $filter); $setting = implode(',', $filter); if (!$connection->test("UPDATE {$qSet} SET {$setting} WHERE {$matching}", $values)) { throw new datasource_exception($connection, 'failed to null references on deleted item in relation ' . $relationName); } } } } return true; })) { throw new datasource_exception($this->_source, 'failed to completely delete item and its tightly bound relations'); } // drop that item now ... $this->_id = null; }
/** * Processes input on current editor. * * @param callable $validatorCallback * @return bool|string false on input failures requiring user action, * "saved" on input successfully saved to data source, * "cancel" on user pressing cancel button, * "delete" on user deleting record */ public function processInput($validatorCallback = null) { if ($this->hasInput()) { switch (input::vget('_cmd')) { case 'cancel': // permit closing editor due to user requesting to cancel editing return 'cancel'; case 'delete': // delete current edited item if ($this->may['delete'] && $this->item) { $ctx = $this; $item = $this->item; $fields = $this->fields; $this->datasource->transaction()->wrap(function () use($ctx, $item, $fields) { foreach ($fields as $field) { /** @var model_editor_field $field */ $field->type()->onDeleting($ctx, $item, $field); } $item->delete(); return true; }); $this->item = null; return 'delete'; } view::flash(\de\toxa\txf\_L('You must not delete this item.'), 'error'); return false; case 'save': // extract some protected properties from current instance to be used in transaction-wrapped callback $ctx = $this; $class = $this->class; $source = $this->datasource; $fields = $this->fields; $enabled = $this->enabled; $item = $this->item; $fixed = $this->getFixed(); $errors = array(); $this->onCreating = !$this->hasItem(); if (!$this->onCreating && !$this->may['edit']) { view::flash(\de\toxa\txf\_L('You must not edit this item.'), 'error'); return false; } // wrap modification on model in transaction $success = $source->transaction()->wrap(function () use($ctx, $class, $source, $fields, $enabled, $fixed, &$item, &$errors, $validatorCallback) { $properties = array(); foreach ($fields as $property => $definition) { /** @var model_editor_field $definition */ if (!count($enabled) || !@$enabled[$property]) { try { // normalize input $input = call_user_func(array($definition->type(), 'normalize'), $ctx->getValue($property, $definition->isCustom()), $property, $ctx); // validate input $success = call_user_func(array($definition->type(), 'validate'), $input, $property, $ctx); // save input if valid if ($success) { $properties[$property] = $input; } else { $errors[$property] = \de\toxa\txf\_L('Your input is invalid.'); } } catch (\Exception $e) { $errors[$property] = $e->getMessage(); } } } if (count($errors)) { return false; } if (is_callable($validatorCallback)) { // provide opportunity to qualify properties for validation $qualified = $properties; foreach ($fields as $field) { /** @var model_editor_field $field */ $qualified = $field->type()->beforeValidating($ctx, $item, $qualified, $field); } // invoke custom callback given those qualified copy of properties for validating $localErrors = call_user_func($validatorCallback, $qualified, $errors, $item ? $item->id() : null); if ($localErrors === false || is_string($localErrors) || is_array($localErrors) && count($localErrors)) { if (is_array($localErrors)) { $errors = array_merge($errors, $localErrors); } else { if (is_string($localErrors)) { view::flash($localErrors, 'error'); } } return false; } } if ($item) { // on updating item -> don't adjust values of // properties marked as fixed foreach ($fixed as $name => $value) { unset($properties[$name]); } } else { // creating new item -> ensure to use fixed initial // values provided additionally foreach ($fixed as $name => $value) { $properties[$name] = $value; } } // optionally pre-process saving properties of item foreach ($fields as $field) { /** @var model_editor_field $field */ $properties = $field->type()->beforeStoring($ctx, $item, $properties, $field); } if ($item) { // update properties of existing item foreach ($properties as $name => $value) { $item->__set($name, $value); } } else { // create new item $item = $class->getMethod('create')->invoke(null, $source, $properties); // tell all elements to have item now foreach ($fields as $field) { /** @var model_editor_field $field */ $field->type()->onSelectingItem($ctx, $item, $field); } } // optionally post-process saving properties of item foreach ($fields as $field) { /** @var model_editor_field $field */ $item = $field->type()->afterStoring($ctx, $item, $properties, $field); } return true; }); // transfer adjusted properties back to protected scope of current instance $this->errors = $errors; // write back item created or probably replaced by afterStoring() call in transaction $this->item = $item; if ($success) { // permit closing editor after having saved all current input view::flash(\de\toxa\txf\_L('Your changes have been saved.')); return 'saved'; } view::flash(\de\toxa\txf\_L('Failed to save your changes.'), 'error'); } } // don't close editor return false; }