Content.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4Cms/
  • Content.php
  • View
  • Commits
  • Open Download .zip Download (22 KB)
<?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);
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4Cms/Content.php
#1 8972 Matt Attaway Initial add of the Chronicle source code