/** * Handles all of the routine tasks that go along with saving elements. * * Those tasks include: * * - Validating its content (if $validateContent is `true`, or it’s left as `null` and the element is enabled) * - Ensuring the element has a title if its type {@link BaseElementType::hasTitles() has titles}, and giving it a * default title in the event that $validateContent is set to `false` * - Saving a row in the `elements` table * - Assigning the element’s ID on the element model, if it’s a new element * - Assigning the element’s ID on the element’s content model, if there is one and it’s a new set of content * - Updating the search index with new keywords from the element’s content * - Setting a unique URI on the element, if it’s supposed to have one. * - Saving the element’s row(s) in the `elements_i18n` and `content` tables * - Deleting any rows in the `elements_i18n` and `content` tables that no longer need to be there * - Calling the field types’ {@link BaseFieldType::onAfterElementSave() onAfterElementSave()} methods * - Cleaing any template caches that the element was involved in * * This method should be called by a service’s “saveX()” method, _after_ it is done validating any attributes on * the element that are of particular concern to its element type. For example, if the element were an entry, * saveElement() should be called only after the entry’s sectionId and typeId attributes had been validated to * ensure that they point to valid section and entry type IDs. * * @param BaseElementModel $element The element that is being saved * @param bool|null $validateContent Whether the element's content should be validated. If left 'null', it * will depend on whether the element is enabled or not. * * @throws Exception|\Exception * @return bool */ public function saveElement(BaseElementModel $element, $validateContent = null) { $elementType = $this->getElementType($element->getElementType()); $isNewElement = !$element->id; // Validate the content first if ($elementType->hasContent()) { if ($validateContent === null) { $validateContent = (bool) $element->enabled; } if ($validateContent && !craft()->content->validateContent($element)) { $element->addErrors($element->getContent()->getErrors()); return false; } else { // Make sure there's a title if ($elementType->hasTitles()) { $fields = array('title'); $content = $element->getContent(); $content->setRequiredFields($fields); if (!$content->validate($fields) && $content->hasErrors('title')) { // Just set *something* on it if ($isNewElement) { $content->title = 'New ' . $element->getClassHandle(); } else { $content->title = $element->getClassHandle() . ' ' . $element->id; } } } } } // Get the element record if (!$isNewElement) { $elementRecord = ElementRecord::model()->findByAttributes(array('id' => $element->id, 'type' => $element->getElementType())); if (!$elementRecord) { throw new Exception(Craft::t('No element exists with the ID “{id}”.', array('id' => $element->id))); } } else { $elementRecord = new ElementRecord(); $elementRecord->type = $element->getElementType(); } // Set the attributes $elementRecord->enabled = (bool) $element->enabled; $elementRecord->archived = (bool) $element->archived; $transaction = craft()->db->getCurrentTransaction() === null ? craft()->db->beginTransaction() : null; try { // Fire an 'onBeforeSaveElement' event $event = new Event($this, array('element' => $element, 'isNewElement' => $isNewElement)); $this->onBeforeSaveElement($event); // Is the event giving us the go-ahead? if ($event->performAction) { // Save the element record first $success = $elementRecord->save(false); if ($success) { if ($isNewElement) { // Save the element id on the element model, in case {id} is in the URL format $element->id = $elementRecord->id; if ($elementType->hasContent()) { $element->getContent()->elementId = $element->id; } } // Save the content if ($elementType->hasContent()) { craft()->content->saveContent($element, false, (bool) $element->id); } // Update the search index craft()->search->indexElementAttributes($element); // Update the locale records and content // We're saving all of the element's locales here to ensure that they all exist and to update the URI in // the event that the URL format includes some value that just changed $localeRecords = array(); if (!$isNewElement) { $existingLocaleRecords = ElementLocaleRecord::model()->findAllByAttributes(array('elementId' => $element->id)); foreach ($existingLocaleRecords as $record) { $localeRecords[$record->locale] = $record; } } $mainLocaleId = $element->locale; $locales = $element->getLocales(); $localeIds = array(); if (!$locales) { throw new Exception('All elements must have at least one locale associated with them.'); } foreach ($locales as $localeId => $localeInfo) { if (is_numeric($localeId) && is_string($localeInfo)) { $localeId = $localeInfo; $localeInfo = array(); } $localeIds[] = $localeId; if (!isset($localeInfo['enabledByDefault'])) { $localeInfo['enabledByDefault'] = true; } if (isset($localeRecords[$localeId])) { $localeRecord = $localeRecords[$localeId]; } else { $localeRecord = new ElementLocaleRecord(); $localeRecord->elementId = $element->id; $localeRecord->locale = $localeId; $localeRecord->enabled = $localeInfo['enabledByDefault']; } // Is this the main locale? $isMainLocale = $localeId == $mainLocaleId; if ($isMainLocale) { $localizedElement = $element; } else { // Copy the element for this locale $localizedElement = $element->copy(); $localizedElement->locale = $localeId; if ($localeRecord->id) { // Keep the original slug $localizedElement->slug = $localeRecord->slug; } else { // Default to the main locale's slug $localizedElement->slug = $element->slug; } } if ($elementType->hasContent()) { if (!$isMainLocale) { $content = null; if (!$isNewElement) { // Do we already have a content row for this locale? $content = craft()->content->getContent($localizedElement); } if (!$content) { $content = craft()->content->createContent($localizedElement); $content->setAttributes($element->getContent()->getAttributes()); $content->id = null; $content->locale = $localeId; } $localizedElement->setContent($content); } if (!$localizedElement->getContent()->id) { craft()->content->saveContent($localizedElement, false, false); } } // Capture the original slug, in case it's entirely composed of invalid characters $originalSlug = $localizedElement->slug; // Clean up the slug ElementHelper::setValidSlug($localizedElement); // If the slug was entirely composed of invalid characters, it will be blank now. if ($originalSlug && !$localizedElement->slug) { $localizedElement->slug = $originalSlug; $element->addError('slug', Craft::t('{attribute} is invalid.', array('attribute' => Craft::t('Slug')))); // Don't bother with any of the other locales $success = false; break; } ElementHelper::setUniqueUri($localizedElement); $localeRecord->slug = $localizedElement->slug; $localeRecord->uri = $localizedElement->uri; if ($isMainLocale) { $localeRecord->enabled = (bool) $element->localeEnabled; } $success = $localeRecord->save(); if (!$success) { // Pass any validation errors on to the element $element->addErrors($localeRecord->getErrors()); // Don't bother with any of the other locales break; } } if ($success) { if (!$isNewElement) { // Delete the rows that don't need to be there anymore craft()->db->createCommand()->delete('elements_i18n', array('and', 'elementId = :elementId', array('not in', 'locale', $localeIds)), array(':elementId' => $element->id)); if ($elementType->hasContent()) { craft()->db->createCommand()->delete($element->getContentTable(), array('and', 'elementId = :elementId', array('not in', 'locale', $localeIds)), array(':elementId' => $element->id)); } } // Call the field types' onAfterElementSave() methods $fieldLayout = $element->getFieldLayout(); if ($fieldLayout) { foreach ($fieldLayout->getFields() as $fieldLayoutField) { $field = $fieldLayoutField->getField(); if ($field) { $fieldType = $field->getFieldType(); if ($fieldType) { $fieldType->element = $element; $fieldType->onAfterElementSave(); } } } } // Finally, delete any caches involving this element. (Even do this for new elements, since they // might pop up in a cached criteria.) craft()->templateCache->deleteCachesByElement($element); } } } else { $success = false; } // Commit the transaction regardless of whether we saved the user, in case something changed // in onBeforeSaveElement if ($transaction !== null) { $transaction->commit(); } } catch (\Exception $e) { if ($transaction !== null) { $transaction->rollback(); } throw $e; } if ($success) { // Fire an 'onSaveElement' event $this->onSaveElement(new Event($this, array('element' => $element, 'isNewElement' => $isNewElement))); } else { if ($isNewElement) { $element->id = null; if ($elementType->hasContent()) { $element->getContent()->id = null; $element->getContent()->elementId = null; } } } return $success; }