<?php
/**
* Provides storage for content entries.
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
class P4Cms_Content extends P4Cms_Record_PubSubRecord
{
const TYPE_FIELD = 'contentType';
const OWNER_FIELD = 'contentOwner';
protected static $_storageSubPath = 'content';
protected static $_topic = 'p4cms.content.record';
protected static $_typeCache = array();
protected static $_fileContentField = 'file';
protected static $_fields = array(
self::TYPE_FIELD => array(
'mutator' => 'setContentType'
),
self::OWNER_FIELD => array(
'accessor' => 'getOwner',
'mutator' => 'setOwner'
)
);
protected static $_uriCallback = null;
/**
* Pub/sub topic documentation for parent fetchAll and count methods.
*
* @publishes p4cms.content.record.query
* Adjust the passed query to influence the results of P4Cms_Content's
* fetch/fetchAll/count results (e.g. to exclude unpublished content).
* P4Cms_Record_Query $query The query that is about to be used to
* retrieve records from storage.
* P4Cms_Record_Adapter $adapter The current storage connection adapter.
*/
/**
* Set the id of this content.
* Extends parent to use ContentId validator instead of RecordId validator.
*
* @param string|int|null $id the identifier of this entry.
* @return P4Cms_Content provides fluent interface.
*/
public function setId($id)
{
// normalize empty strings to null; this is helpful for form input data
if (is_string($id) && !strlen($id)) {
$id = null;
}
$validator = new P4Cms_Validate_ContentId;
if ($id !== null && !$validator->isValid($id)) {
throw new InvalidArgumentException("Cannot set id. Given id is invalid.");
}
return parent::setId($id);
}
/**
* Set the content type definition to use for this content entry.
*
* @param string|P4Cms_Content_Type $type either a string for the content type id
* of an instance of a content type.
*/
public function setContentType($type)
{
if ($type instanceof P4Cms_Content_Type) {
$type = $type->getId();
}
return $this->_setValue(static::TYPE_FIELD, $type);
}
/**
* Get the content type id for this entry.
*
* @return string the id of this entry's content type.
*/
public function getContentTypeId()
{
return $this->_getValue(static::TYPE_FIELD);
}
/**
* Get the content type definition for this content entry.
*
* @return P4Cms_Content_Type instance of the content type for this content entry.
*/
public function getContentType()
{
return static::_getContentType($this->getContentTypeId(), $this->getAdapter());
}
/**
* Determine if this content entry has a valid content type.
*
* @return bool true if the entry has a valid content type.
*/
public function hasValidContentType()
{
try {
$type = $this->getContentType();
if ($type->hasValidElements()) {
return true;
} else {
return false;
}
} catch (P4Cms_Content_Exception $e) {
return false;
}
}
/**
* Get all of the field names for this content entry.
* Adds the fields defined by the content type.
*
* @return array a list of field names for this spec.
*/
public function getFields()
{
$fields = array_flip(parent::getFields());
// add fields from the content type.
if ($this->hasValidContentType()) {
$fields = $this->getContentType()->getElements() + $fields;
}
return array_keys($fields);
}
/**
* Get the title for this piece of content.
* If the content has no title, returns the id.
*
* @return string the title or the content id if no title is set.
*/
public function getTitle()
{
$title = $this->hasField('title')
? trim($this->_getValue('title'))
: null;
return $title ?: $this->getId();
}
/**
* Get the time that this content entry was last modified (submitted).
*
* @return int the modification timestamp.
*/
public function getModTime()
{
return $this->_getP4File()->getStatus('headTime');
}
/**
* Get an excerpt of the content. If the content entry has an 'excerpt' field,
* it will be used; otherwise, the body field will be truncated. If there is
* no body field, returns null.
*
* @param int $length optional - the maximum length of the excerpt
* set to zero to turn off truncating.
* defaults to 100 characters.
* @param array $options Options influencing the returned excerpt. Valid options:
* bool filterHtml optional - set to true to convert HTML to text.
* defaults to true
* bool fullExcerpt optional - set to true to get full excerpt field; excerptField still truncated.
* defaults to false
* bool keepEntities optional - set to true to keep HTML entities intact during HTML filtering.
* defaults to false
* string excerptField optional - alternate field to use for excerpt if excerpt field does not exist.
* defaults to body
* @return string the excerpt text
*/
public function getExcerpt($length = 100, array $options = array())
{
// setup option defaults
$options = array_merge(
array(
'filterHtml' => true,
'fullExcerpt' => false,
'keepEntities' => false,
'excerptField' => 'body'
),
$options
);
if ($this->hasField('excerpt')) {
if ($options['fullExcerpt']) {
$length = 0;
}
$excerpt = $this->getValue('excerpt');
} else if ($this->hasField($options['excerptField'])) {
$excerpt = $this->getValue($options['excerptField']);
} else if ($this->hasField('body')) {
$excerpt = $this->getValue('body');
} else {
return null;
}
// convert to plain-text.
if ($options['filterHtml']) {
$filter = new P4Cms_Filter_HtmlToText(array('keepEntities' => $options['keepEntities']));
$excerpt = $filter->filter($excerpt);
}
// truncate excerpt.
if ($length !== 0) {
$truncate = new P4Cms_View_Helper_Truncate;
$excerpt = $truncate->truncate($excerpt, $length, null, false);
}
return $excerpt;
}
/**
* Get the URI to this content entry.
* If an action is provided, will return a URI to perform the given action.
*
* @param string $action optional - action to perform - defaults to 'view'.
* @param array $params optional - additional params to add to the uri.
* @return string the uri of the content entry.
*/
public function getUri($action = 'view', $params = array())
{
return call_user_func(static::getUriCallback(), $this, $action, $params);
}
/**
* Cast this content entry to a lucene document for indexing purposes.
*
* @return Zend_Search_Lucene_Document a lucene document suitable for indexing.
*/
public function toLuceneDocument()
{
// create lucene document from this content entry.
return new P4Cms_Content_LuceneDocument($this);
}
/**
* Save this record. If record does not have an id, this will create one.
* Extends save() to add to search index.
*
* @param string $description optional - a description of the change.
* @param null|string|array $options optional - resolve flags, to be used if conflict
* occurs. See P4_File::resolve() for details.
* @return P4Cms_Record provides a fluent interface
*
* @publishes p4cms.search.update
* Perform search indexing related operations with the passed document. Called when
* content is saved, indicating it has been added or edited.
* Zend_Search_Lucene_Document|P4Cms_Content $document The entry being updated.
*
* @publishes p4cms.content.record.preSave
* Perform operations on a content entry just prior to its being saved.
* P4Cms_Content $entry The content entry that is about to be saved.
*
* @publishes p4cms.content.record.postSave
* Perform operations on a content entry just after it's saved (but before the
* batch it is in gets committed).
* P4Cms_Content $entry The content entry that has just been saved.
*/
public function save($description = null, $options = null)
{
parent::save($description, $options);
// update search index.
P4Cms_PubSub::publish("p4cms.search.update", $this);
return $this;
}
/**
* Delete this record.
* Extends delete() to remove from search index.
*
* @param string $description optional - a description of the change.
* @return P4Cms_Record provides fluent interface.
*
* @publishes p4cms.search.delete
* Perform operations when an entry is deleted from the search-index. Note: Updates
* to existing entries are accomplished via delete/add.
* Zend_Search_Lucene_Document|P4Cms_Content $document The entry being deleted.
*
* @publishes p4cms.content.record.delete
* Perform operations on a content entry just prior to deletion.
* P4Cms_Content $entry The content entry that is about to be deleted.
*/
public function delete($description = null)
{
parent::delete($description);
// remove from search index.
P4Cms_PubSub::publish("p4cms.search.delete", $this);
return $this;
}
/**
* Get a field's value after output/display filters are applied.
*
* @param string $field the name of the field to get the filtered value of.
* @return string the filtered value of hte field.
*/
public function getFilteredValue($field)
{
$type = $this->getContentType();
$value = $this->getExpandedValue($field);
// apply element's display filters.
foreach ($type->getDisplayFilters($field) as $filter) {
$value = $filter->filter($value);
}
return $value;
}
/**
* Get a field's expanded value. If a field contains
* macros and macros are enabled for the field, this will
* return the field value with macros evaluated.
*
* @param string $field the name of the field to get the expanded value of.
* @return string the expanded value of the field.
*/
public function getExpandedValue($field)
{
$type = $this->getContentType();
$element = $type->getElement($field);
$value = $this->getValue($field);
// if macros are enabled, invoke them.
if (isset($element['options']['macros']['enabled'])) {
$filter = new P4Cms_Filter_Macro;
$filter->setContext(array('content' => $this, 'element' => $element));
$value = $filter->filter($value);
}
return $value;
}
/**
* Get a field's display value. The display value is the result
* of rendering a field's display decorators. If a field element
* has no decorators, the plain (expanded) value is returned.
*
* @param string $field the name of the field to get the display value of.
* @param array $options optional - display options
* @return string the display value of the field.
*/
public function getDisplayValue($field, array $options = array())
{
$type = $this->getContentType();
$element = clone $type->getFormElement($field);
$value = $this->getFilteredValue($field);
// set the associated content record (if possible) on the element
// for decorators to access - requires enhanced element.
if ($element instanceof P4Cms_Content_EnhancedElementInterface) {
$element->setContentRecord($this);
}
// get decorators to render the element from options param or from
// the content type.
$decorators = isset($options['decorators'])
? $element->setDecorators($options['decorators'])->getDecorators()
: $type->getDisplayDecorators($element);
// if no decorators, just return the plain field value.
if (empty($decorators)) {
return $value;
}
// we have already applied display filters above, clear any
// element input filters as we don't want them in this context.
$element->clearFilters();
// set the field value on the element for decorators to access
// note, some elements (e.g. file/image) will ignore attempts to
// set a value; therefore, decorators will not be able to retrieve
// the field value from such elements directly.
$element->setValue($value);
// render display value using decorators.
$content = '';
foreach ($decorators as $decorator) {
$decorator->setElement($element);
if ($decorator instanceof P4Cms_Content_EnhancedDecoratorInterface) {
$decorator->setContentRecord($this);
}
$content = $decorator->render($content);
}
return $content;
}
/**
* Get the owner of this content entry.
*
* @return string id of owner user.
*/
public function getOwner()
{
return $this->_getValue(static::OWNER_FIELD);
}
/**
* Set the owner of this content entry.
*
* @param P4Cms_User|string|null $user user to set as this content entry owner.
* @return P4Cms_Content provides fluent interface.
*/
public function setOwner($user)
{
if ($user instanceof P4Cms_User) {
$user = $user->getId();
} else if (!is_string($user) && !is_null($user)) {
throw new InvalidArgumentException(
"User must be an instance of P4Cms_User, a string or null."
);
}
return $this->_setValue(static::OWNER_FIELD, $user);
}
/**
* Set a field value to the contents of the given file.
* Extended to capture file metadata such as mime-type and image size.
*
* @param string $field the field to set the value of.
* @param string $file the full path to the file to read from.
* @param string $name optionally provide an explicit name
* if none is given, it will be basename of file.
* @param string $type optionally provide an explicit mime-type
* if none is given, it will be auto-detected.
* @return P4Cms_Record provides fluent interface.
* @throws InvalidArgumentException if the given file does not exist.
*/
public function setValueFromFile($field, $file, $name = null, $type = null)
{
parent::setValueFromFile($field, $file);
// attempt to capture file metadata - note, image size is expected
// to fail (silently) for non-images or unsupported image formats.
$metadata = array(
'mimeType' => $type ?: P4Cms_FileUtility::getMimeType($file),
'filename' => $name ?: basename($file),
'fileSize' => filesize($file)
);
$dimensions = @getimagesize($file);
if (is_array($dimensions)) {
$metadata['dimensions'] = array('width' => $dimensions[0], 'height' => $dimensions[1]);
}
$this->setFieldMetadata($field, $metadata);
return $this;
}
/**
* Set the function to use when generating URI's for content entries.
*
* @param null|callback $function The callback function for URI generation. The
* function should expect three parameters:
* - $content (P4Cms_Content)
* - $action (string)
* - $params (array)
* Returns a string (the uri).
*/
public static function setUriCallback($function)
{
if (!is_callable($function) && $function !== null) {
throw new InvalidArgumentException(
'Cannot set URI callback. Expected a callable function or null.'
);
}
static::$_uriCallback = $function;
}
/**
* Determines if a valid URI callback has been set.
*
* @return bool True if valid URI callback set, False otherwise.
*/
public static function hasUriCallback()
{
return is_callable(static::$_uriCallback);
}
/**
* Returns the current URI callback if one has been set.
*
* @return callback The current URI callback.
* @throws P4Cms_Content_Exception If no URI callback has been set.
*/
public static function getUriCallback()
{
if (!static::hasUriCallback()) {
throw new P4Cms_Content_Exception(
'Cannot get URI callback, no URI callback has been set.'
);
}
return static::$_uriCallback;
}
/**
* Clear the static type cache.
* If a valid adapter is passed, only that connections cache will be cleared; otherwise
* all cached types are cleared on all connections.
*
* @param P4Cms_Record_Adapter $adapter optional - adapter to clear on or null for all
*/
public static function clearTypeCache(P4Cms_Record_Adapter $adapter = null)
{
if (!$adapter) {
static::$_typeCache = array();
return;
}
$cacheKey = spl_object_hash($adapter);
if (array_key_exists($cacheKey, static::$_typeCache)) {
unset(static::$_typeCache[$cacheKey]);
}
}
/**
* Get the set of all content types in storage.
* Caches and indexes (by id) the results of P4Cms_Content_Type::fetchAll().
*
* @param P4Cms_Record_Adapter $adapter the adapter in use.
* @return array all content types indexed by content type id.
*/
protected static function _getContentTypes(P4Cms_Record_Adapter $adapter)
{
// cache must be divided by storage adapter.
$cacheKey = spl_object_hash($adapter);
// load the content types (but only fetch them once).
if (!array_key_exists($cacheKey, static::$_typeCache)) {
$query = new P4Cms_Record_Query;
$query->setIncludeDeleted(true);
$types = P4Cms_Content_Type::fetchAll($query, $adapter);
// cache the content types indexed by id.
if ($types->count()) {
$types = array_combine(
$types->invoke('getId'),
$types->toArray(true)
);
}
static::$_typeCache[$cacheKey] = $types;
}
return static::$_typeCache[$cacheKey];
}
/**
* Get a specific content type instance.
* Utilizes _getContentTypes() to benefit from cache.
*
* @param string $id a string for the content type id
* @param P4Cms_Record_Adapter $adapter the adapter in use.
* @return P4Cms_Content_Type an instance of the requested content type.
*/
protected static function _getContentType($id, P4Cms_Record_Adapter $adapter)
{
$types = static::_getContentTypes($adapter);
$type = $id && isset($types[$id]) ? $types[$id] : null;
// create a in-memory type if we couldn't locate one
if (!$type) {
$type = new P4Cms_Content_Type;
$type->setLabel("Missing Type" . ($id ? " ($id)" : ""));
}
return $type;
}
/**
* Extends parent to pull defaults from content type definition.
*
* @param string $field the name of the field to get the value of.
* @return mixed the default value of the field - null for no default.
*/
protected function _getDefaultValue($field)
{
// attempt to query content type for default.
if ($field !== static::TYPE_FIELD) {
try {
$type = $this->getContentType();
$element = $type->getFormElement($field);
$value = $element->getValue();
if ($value !== null) {
return $value;
}
} catch (Exception $e) {
// intentionally ignore errors fetching content type values
}
}
return parent::_getDefaultValue($field);
}
}