/** * Saves an entry. * * @param EntryModel $entry * @throws Exception * @return bool */ public function saveEntry(EntryModel $entry) { $isNewEntry = !$entry->id; // Entry data if (!$isNewEntry) { $entryRecord = EntryRecord::model()->with('element')->findById($entry->id); if (!$entryRecord) { throw new Exception(Craft::t('No entry exists with the ID “{id}”', array('id' => $entry->id))); } $elementRecord = $entryRecord->element; // if entry->sectionId is null and there is an entryRecord sectionId, we assume this is a front-end edit. if ($entry->sectionId === null && $entryRecord->sectionId) { $entry->sectionId = $entryRecord->sectionId; } } else { $entryRecord = new EntryRecord(); $elementRecord = new ElementRecord(); $elementRecord->type = ElementType::Entry; } $section = craft()->sections->getSectionById($entry->sectionId); if (!$section) { throw new Exception(Craft::t('No section exists with the ID “{id}”', array('id' => $entry->sectionId))); } $sectionLocales = $section->getLocales(); if (!isset($sectionLocales[$entry->locale])) { throw new Exception(Craft::t('The section “{section}” is not enabled for the locale {locale}', array('section' => $section->name, 'locale' => $entry->locale))); } $entryRecord->sectionId = $entry->sectionId; $entryRecord->authorId = $entry->authorId; $entryRecord->postDate = $entry->postDate; $entryRecord->expiryDate = $entry->expiryDate; if ($entry->enabled && !$entryRecord->postDate) { // Default the post date to the current date/time $entryRecord->postDate = $entry->postDate = DateTimeHelper::currentUTCDateTime(); } $entryRecord->validate(); $entry->addErrors($entryRecord->getErrors()); $elementRecord->enabled = $entry->enabled; $elementRecord->validate(); $entry->addErrors($elementRecord->getErrors()); // Entry locale data if ($entry->id) { $entryLocaleRecord = EntryLocaleRecord::model()->findByAttributes(array('entryId' => $entry->id, 'locale' => $entry->locale)); // If entry->slug is null and there is an entryLocaleRecord slug, we assume this is a front-end edit. if ($entry->slug === null && $entryLocaleRecord->slug) { $entry->slug = $entryLocaleRecord->slug; } } if (empty($entryLocaleRecord)) { $entryLocaleRecord = new EntryLocaleRecord(); $entryLocaleRecord->sectionId = $entry->sectionId; $entryLocaleRecord->locale = $entry->locale; } if ($entryLocaleRecord->isNewRecord() || $entry->slug != $entryLocaleRecord->slug) { $this->_generateEntrySlug($entry); $entryLocaleRecord->slug = $entry->slug; } $entryLocaleRecord->validate(); $entry->addErrors($entryLocaleRecord->getErrors()); // Element locale data if ($entry->id) { $elementLocaleRecord = ElementLocaleRecord::model()->findByAttributes(array('elementId' => $entry->id, 'locale' => $entry->locale)); } if (empty($elementLocaleRecord)) { $elementLocaleRecord = new ElementLocaleRecord(); $elementLocaleRecord->locale = $entry->locale; } if ($section->hasUrls && $entry->enabled) { // Make sure the section's URL format is valid. This shouldn't be possible due to section validation, // but it's not enforced by the DB, so anything is possible. $urlFormat = $sectionLocales[$entry->locale]->urlFormat; if (!$urlFormat || strpos($urlFormat, '{slug}') === false) { throw new Exception(Craft::t('The section “{section}” doesn’t have a valid URL Format.', array('section' => Craft::t($section->name)))); } $elementLocaleRecord->uri = craft()->templates->renderObjectTemplate($urlFormat, $entry); } else { $elementLocaleRecord->uri = null; } $elementLocaleRecord->validate(); $entry->addErrors($elementLocaleRecord->getErrors()); // Entry content $fieldLayout = $section->getFieldLayout(); $content = craft()->content->prepElementContentForSave($entry, $fieldLayout); $content->validate(); $entry->addErrors($content->getErrors()); if (!$entry->hasErrors()) { // Save the element record first $elementRecord->save(false); // Now that we have an element ID, save it on the other stuff if (!$entry->id) { $entry->id = $elementRecord->id; $entryRecord->id = $entry->id; } $entryRecord->save(false); $entryLocaleRecord->entryId = $entry->id; $elementLocaleRecord->elementId = $entry->id; $content->elementId = $entry->id; // Save the other records $entryLocaleRecord->save(false); $elementLocaleRecord->save(false); craft()->content->saveContent($content, false); // Update the search index craft()->search->indexElementAttributes($entry, $entry->locale); // Save a new version if (Craft::hasPackage(CraftPackage::PublishPro)) { craft()->entryRevisions->saveVersion($entry); } // Perform some post-save operations craft()->content->postSaveOperations($entry, $content); // Fire an 'onSaveEntry' event $this->onSaveEntry(new Event($this, array('entry' => $entry, 'isNewEntry' => $isNewEntry))); return true; } else { return false; } }
/** * 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; }