PageRenderTime 102ms CodeModel.GetById 36ms RepoModel.GetById 7ms app.codeStats 0ms

/craft/app/services/ContentService.php

https://gitlab.com/madebycloud/derekman
PHP | 447 lines | 267 code | 68 blank | 112 comment | 28 complexity | f1f2353ea6565f9b1f98d64ca00c9215 MD5 | raw file
  1. <?php
  2. namespace Craft;
  3. /**
  4. * Class ContentService
  5. *
  6. * @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
  7. * @copyright Copyright (c) 2014, Pixel & Tonic, Inc.
  8. * @license http://buildwithcraft.com/license Craft License Agreement
  9. * @see http://buildwithcraft.com
  10. * @package craft.app.services
  11. * @since 1.0
  12. */
  13. class ContentService extends BaseApplicationComponent
  14. {
  15. // Properties
  16. // =========================================================================
  17. /**
  18. * @var string
  19. */
  20. public $contentTable = 'content';
  21. /**
  22. * @var string
  23. */
  24. public $fieldColumnPrefix = 'field_';
  25. /**
  26. * @var string
  27. */
  28. public $fieldContext = 'global';
  29. // Public Methods
  30. // =========================================================================
  31. /**
  32. * Returns the content model for a given element.
  33. *
  34. * @param BaseElementModel $element The element whose content we're looking for.
  35. *
  36. * @return ContentModel|null The element's content or `null` if no content has been saved for the element.
  37. */
  38. public function getContent(BaseElementModel $element)
  39. {
  40. if (!$element->id || !$element->locale)
  41. {
  42. return;
  43. }
  44. $originalContentTable = $this->contentTable;
  45. $originalFieldColumnPrefix = $this->fieldColumnPrefix;
  46. $originalFieldContext = $this->fieldContext;
  47. $this->contentTable = $element->getContentTable();
  48. $this->fieldColumnPrefix = $element->getFieldColumnPrefix();
  49. $this->fieldContext = $element->getFieldContext();
  50. $row = craft()->db->createCommand()
  51. ->from($this->contentTable)
  52. ->where(array(
  53. 'elementId' => $element->id,
  54. 'locale' => $element->locale
  55. ))
  56. ->queryRow();
  57. if ($row)
  58. {
  59. $row = $this->_removeColumnPrefixesFromRow($row);
  60. $content = new ContentModel($row);
  61. }
  62. else
  63. {
  64. $content = null;
  65. }
  66. $this->contentTable = $originalContentTable;
  67. $this->fieldColumnPrefix = $originalFieldColumnPrefix;
  68. $this->fieldContext = $originalFieldContext;
  69. return $content;
  70. }
  71. /**
  72. * Instantiates a new content model for a given element.
  73. *
  74. * @param BaseElementModel $element The element for which we should create a new content model.
  75. *
  76. * @return ContentModel The new content model.
  77. */
  78. public function createContent(BaseElementModel $element)
  79. {
  80. $originalContentTable = $this->contentTable;
  81. $originalFieldColumnPrefix = $this->fieldColumnPrefix;
  82. $originalFieldContext = $this->fieldContext;
  83. $this->contentTable = $element->getContentTable();
  84. $this->fieldColumnPrefix = $element->getFieldColumnPrefix();
  85. $this->fieldContext = $element->getFieldContext();
  86. $content = new ContentModel();
  87. $content->elementId = $element->id;
  88. $content->locale = $element->locale;
  89. $this->contentTable = $originalContentTable;
  90. $this->fieldColumnPrefix = $originalFieldColumnPrefix;
  91. $this->fieldContext = $originalFieldContext;
  92. return $content;
  93. }
  94. /**
  95. * Saves an element's content.
  96. *
  97. * @param BaseElementModel $element The element whose content we're saving.
  98. * @param bool $validate Whether the element's content should be validated first.
  99. * @param bool $updateOtherLocales Whether any non-translatable fields' values should be copied to the
  100. * element's other locales.
  101. *
  102. * @throws Exception
  103. * @return bool Whether the content was saved successfully. If it wasn't, any validation errors will be saved on the
  104. * element and its content model.
  105. */
  106. public function saveContent(BaseElementModel $element, $validate = true, $updateOtherLocales = true)
  107. {
  108. if (!$element->id)
  109. {
  110. throw new Exception(Craft::t('Cannot save the content of an unsaved element.'));
  111. }
  112. $originalContentTable = $this->contentTable;
  113. $originalFieldColumnPrefix = $this->fieldColumnPrefix;
  114. $originalFieldContext = $this->fieldContext;
  115. $this->contentTable = $element->getContentTable();
  116. $this->fieldColumnPrefix = $element->getFieldColumnPrefix();
  117. $this->fieldContext = $element->getFieldContext();
  118. $content = $element->getContent();
  119. if (!$validate || $this->validateContent($element))
  120. {
  121. $this->_saveContentRow($content);
  122. $fieldLayout = $element->getFieldLayout();
  123. if ($fieldLayout)
  124. {
  125. if ($updateOtherLocales && craft()->isLocalized())
  126. {
  127. $this->_duplicateNonTranslatableFieldValues($element, $content, $fieldLayout, $nonTranslatableFields, $otherContentModels);
  128. }
  129. $this->_updateSearchIndexes($element, $content, $fieldLayout, $nonTranslatableFields, $otherContentModels);
  130. }
  131. $success = true;
  132. }
  133. else
  134. {
  135. $element->addErrors($content->getErrors());
  136. $success = false;
  137. }
  138. $this->contentTable = $originalContentTable;
  139. $this->fieldColumnPrefix = $originalFieldColumnPrefix;
  140. $this->fieldContext = $originalFieldContext;
  141. return $success;
  142. }
  143. /**
  144. * Validates some content with a given field layout.
  145. *
  146. * @param BaseElementModel $element The element whose content should be validated.
  147. *
  148. * @return bool Whether the element's content validates.
  149. */
  150. public function validateContent(BaseElementModel $element)
  151. {
  152. $elementType = craft()->elements->getElementType($element->getElementType());
  153. $fieldLayout = $element->getFieldLayout();
  154. $content = $element->getContent();
  155. // Set the required fields from the layout
  156. $attributesToValidate = array('id', 'elementId', 'locale');
  157. $requiredFields = array();
  158. if ($elementType->hasTitles())
  159. {
  160. $requiredFields[] = 'title';
  161. $attributesToValidate[] = 'title';
  162. }
  163. if ($fieldLayout)
  164. {
  165. foreach ($fieldLayout->getFields() as $fieldLayoutField)
  166. {
  167. $field = $fieldLayoutField->getField();
  168. if ($field)
  169. {
  170. $attributesToValidate[] = $field->handle;
  171. if ($fieldLayoutField->required)
  172. {
  173. $requiredFields[] = $field->id;
  174. }
  175. }
  176. }
  177. }
  178. if ($requiredFields)
  179. {
  180. $content->setRequiredFields($requiredFields);
  181. }
  182. return $content->validate($attributesToValidate);
  183. }
  184. /**
  185. * Fires an 'onSaveContent' event.
  186. *
  187. * @param Event $event
  188. *
  189. * @return null
  190. */
  191. public function onSaveContent(Event $event)
  192. {
  193. $this->raiseEvent('onSaveContent', $event);
  194. }
  195. // Private Methods
  196. // =========================================================================
  197. /**
  198. * Saves a content model to the database.
  199. *
  200. * @param ContentModel $content
  201. *
  202. * @return bool
  203. */
  204. private function _saveContentRow(ContentModel $content)
  205. {
  206. $values = array(
  207. 'id' => $content->id,
  208. 'elementId' => $content->elementId,
  209. 'locale' => $content->locale,
  210. );
  211. $excludeColumns = array_keys($values);
  212. $excludeColumns = array_merge($excludeColumns, array_keys(DbHelper::getAuditColumnConfig()));
  213. $fullContentTableName = craft()->db->addTablePrefix($this->contentTable);
  214. $contentTableSchema = craft()->db->schema->getTable($fullContentTableName);
  215. foreach ($contentTableSchema->columns as $columnSchema)
  216. {
  217. if ($columnSchema->allowNull && !in_array($columnSchema->name, $excludeColumns))
  218. {
  219. $values[$columnSchema->name] = null;
  220. }
  221. }
  222. // If the element type has titles, than it's required and will be set. Otherwise, no need to include it (it
  223. // might not even be a real column if this isn't the 'content' table).
  224. if ($content->title)
  225. {
  226. $values['title'] = $content->title;
  227. }
  228. foreach (craft()->fields->getFieldsWithContent() as $field)
  229. {
  230. $handle = $field->handle;
  231. $value = $content->$handle;
  232. $values[$this->fieldColumnPrefix.$field->handle] = ModelHelper::packageAttributeValue($value, true);
  233. }
  234. $isNewContent = !$content->id;
  235. if (!$isNewContent)
  236. {
  237. $affectedRows = craft()->db->createCommand()->update($this->contentTable, $values, array('id' => $content->id));
  238. }
  239. else
  240. {
  241. $affectedRows = craft()->db->createCommand()->insert($this->contentTable, $values);
  242. }
  243. if ($affectedRows)
  244. {
  245. if ($isNewContent)
  246. {
  247. // Set the new ID
  248. $content->id = craft()->db->getLastInsertID();
  249. }
  250. // Fire an 'onSaveContent' event
  251. $this->onSaveContent(new Event($this, array(
  252. 'content' => $content,
  253. 'isNewContent' => $isNewContent
  254. )));
  255. return true;
  256. }
  257. else
  258. {
  259. return false;
  260. }
  261. }
  262. /**
  263. * Copies the new values of any non-translatable fields across the element's
  264. * other locales.
  265. *
  266. * @param BaseElementModel $element
  267. * @param ContentModel $content
  268. * @param FieldLayoutModel $fieldLayout
  269. * @param array &$nonTranslatableFields
  270. * @param array &$otherContentModels
  271. *
  272. * @return null
  273. */
  274. private function _duplicateNonTranslatableFieldValues(BaseElementModel $element, ContentModel $content, FieldLayoutModel $fieldLayout, &$nonTranslatableFields, &$otherContentModels)
  275. {
  276. // Get all of the non-translatable fields
  277. $nonTranslatableFields = array();
  278. foreach ($fieldLayout->getFields() as $fieldLayoutField)
  279. {
  280. $field = $fieldLayoutField->getField();
  281. if ($field && !$field->translatable)
  282. {
  283. if ($field->hasContentColumn())
  284. {
  285. $nonTranslatableFields[$field->id] = $field;
  286. }
  287. }
  288. }
  289. if ($nonTranslatableFields)
  290. {
  291. // Get the other locales' content
  292. $rows = craft()->db->createCommand()
  293. ->from($this->contentTable)
  294. ->where(
  295. array('and', 'elementId = :elementId', 'locale != :locale'),
  296. array(':elementId' => $element->id, ':locale' => $content->locale))
  297. ->queryAll();
  298. // Remove the column prefixes
  299. foreach ($rows as $i => $row)
  300. {
  301. $rows[$i] = $this->_removeColumnPrefixesFromRow($row);
  302. }
  303. $otherContentModels = ContentModel::populateModels($rows);
  304. foreach ($otherContentModels as $otherContentModel)
  305. {
  306. foreach ($nonTranslatableFields as $field)
  307. {
  308. $handle = $field->handle;
  309. $otherContentModel->$handle = $content->$handle;
  310. }
  311. $this->_saveContentRow($otherContentModel);
  312. }
  313. }
  314. }
  315. /**
  316. * Updates the search indexes based on the new content values.
  317. *
  318. * @param BaseElementModel $element
  319. * @param ContentModel $content
  320. * @param FieldLayoutModel $fieldLayout
  321. * @param array|null &$nonTranslatableFields
  322. * @param array|null &$otherContentModels
  323. *
  324. * @return null
  325. */
  326. private function _updateSearchIndexes(BaseElementModel $element, ContentModel $content, FieldLayoutModel $fieldLayout, &$nonTranslatableFields = null, &$otherContentModels = null)
  327. {
  328. $searchKeywordsByLocale = array();
  329. foreach ($fieldLayout->getFields() as $fieldLayoutField)
  330. {
  331. $field = $fieldLayoutField->getField();
  332. if ($field)
  333. {
  334. $fieldType = $field->getFieldType();
  335. if ($fieldType)
  336. {
  337. $fieldType->element = $element;
  338. $handle = $field->handle;
  339. // Set the keywords for the content's locale
  340. $fieldSearchKeywords = $fieldType->getSearchKeywords($element->getFieldValue($handle));
  341. $searchKeywordsByLocale[$content->locale][$field->id] = $fieldSearchKeywords;
  342. // Should we queue up the other locales' new keywords too?
  343. if (isset($nonTranslatableFields[$field->id]))
  344. {
  345. foreach ($otherContentModels as $otherContentModel)
  346. {
  347. $searchKeywordsByLocale[$otherContentModel->locale][$field->id] = $fieldSearchKeywords;
  348. }
  349. }
  350. }
  351. }
  352. }
  353. foreach ($searchKeywordsByLocale as $localeId => $keywords)
  354. {
  355. craft()->search->indexElementFields($element->id, $localeId, $keywords);
  356. }
  357. }
  358. /**
  359. * Removes the column prefixes from a given row.
  360. *
  361. * @param array $row
  362. *
  363. * @return array
  364. */
  365. private function _removeColumnPrefixesFromRow($row)
  366. {
  367. $fieldColumnPrefixLength = strlen($this->fieldColumnPrefix);
  368. foreach ($row as $column => $value)
  369. {
  370. if (strncmp($column, $this->fieldColumnPrefix, $fieldColumnPrefixLength) === 0)
  371. {
  372. $fieldHandle = substr($column, $fieldColumnPrefixLength);
  373. $row[$fieldHandle] = $value;
  374. unset($row[$column]);
  375. }
  376. }
  377. return $row;
  378. }
  379. }