<?php
/**
* Widgets are configurations of a widget-controller in a region.
* The widget model provides read/write access to an individual
* widget's configuration information.
*
* Each widget has an id that is unique within it's region. The id is
* is determined by the order that that the widget was defined in.
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
class P4Cms_Widget extends P4Cms_Record_Config
{
protected static $_storageSubPath = 'widgets';
protected static $_fields = array(
'id',
'region',
'type',
'title',
'showTitle',
'order' => array(
'default' => 0
),
'class',
'asynchronous' => array(
'default' => false
),
'config',
'addTime'
);
protected static $_idField = 'id';
protected $_error = null;
protected $_exception = null;
/**
* Create a new instance of a widget by specifying a widget type.
* The type can be a type instance or a type id.
*
* If caller provides values, they will be merged with the widget
* type defaults (given values win).
*
* @param string|P4Cms_Widget_Type $type the type of widget to create.
* @param array $values optional - widget values to use
* @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
* @return P4Cms_Widget a newly created widget instance.
* @throws P4Cms_Widget_Exception if the specified type is invalid.
*/
public static function factory($type, array $values = null, P4Cms_Record_Adapter $adapter = null)
{
// lookup type model if id given.
if (is_string($type) && P4Cms_Widget_Type::exists($type)) {
$type = P4Cms_Widget_Type::fetch($type);
}
// validate type.
if (!$type instanceof P4Cms_Widget_Type || !$type->isValid()) {
throw new P4Cms_Widget_Exception(
"Cannot create widget. The given widget type is invalid."
);
}
// merge caller provided values with type defaults.
$values = $values ?: array();
$values = array_merge(
array(
'title' => $type->label,
'config' => $type->getDefaults()
),
$values
);
// instantiate widget setting type and defaults.
return static::create($values, $adapter)->setType($type);
}
/**
* Get the widgets contained in a given region.
*
* Widgets have a 'region' attribute which we utilize to locate them.
*
* @param string $id the id of the region
* @param P4Cms_Record_Query|array|null $query optional - query options to augment result.
* @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
* @return P4Cms_Record the requested widget(s)
*/
public static function fetchByRegion($id, $query = null, P4Cms_Record_Adapter $adapter = null)
{
if (empty($id)) {
throw new InvalidArgumentException(
'Cannot fetch by region. Region id must be specified.'
);
}
$query = static::_normalizeQuery($query);
$query->addFilter(P4Cms_Record_Filter::create()->add('region', $id));
$widgets = new P4Cms_Model_Iterator;
foreach (static::fetchAll($query, $adapter) as $widget) {
$type = $widget->getValue('type');
if (P4Cms_Widget_Type::exists($type) && P4Cms_Widget_Type::fetch($type)->isValid()) {
$widgets[$widget->getId()] = $widget;
}
}
// put widgets in order. we do this client side as the server lacks
// a purely numeric sort (negative order for example would be a problem)
$widgets->sortBy(
array(
'order' => array(P4Cms_Model_Iterator::SORT_NUMERIC),
'addTime' => array(P4Cms_Model_Iterator::SORT_NUMERIC)
)
);
return $widgets;
}
/**
* Run this widget and return the output.
*
* @param bool $throwExceptions optional - defaults to false to prevent widgets from
* halting execution - if you want to permit exceptions
* pass true for this argument.
* @return string the output produced by the widget.
*/
public function run($throwExceptions = false)
{
// try to run the widget controller.
// suppress exceptions unless throw exceptions is true.
$output = '';
try {
$view = Zend_Layout::getMvcInstance()->getView();
$type = $this->getType();
$params = $type->getRouteParams();
// when an action parameter is included in a page request (e.g. ?action=foo),
// the action will be retrieved by dispatcher->getActionMethod() because
// $params['action'] is null -- it's not defined in the module.ini file.
// we provide a default 'index' action here to avoid the problem.
$output = $view->action(
$params['action'] ?: 'index',
$params['controller'],
$params['module'],
array('widget' => $this)
);
} catch (Exception $e) {
P4Cms_Log::logException("Failed to run widget.", $e);
$this->_exception = $e;
$this->_error = $e->getMessage();
if ($throwExceptions) {
throw $e;
}
}
return $output;
}
/**
* Determine if this widget suffered an error during run.
*
* @return bool true if an error occurred.
*/
public function hasError()
{
return is_string($this->_error) && strlen($this->_error);
}
/**
* Get the error message if an error occurred during run.
*
* @return string the error message.
* @throw P4Cms_Widget_Exception if no error occured.
*/
public function getError()
{
if (!$this->hasError()) {
throw new P4Cms_Widget_Exception(
"Cannot get error. No error occurred."
);
}
return $this->_error;
}
/**
* Determine if an exception occurred during run.
*
* @return bool true if an exception occurred.
*/
public function hasException()
{
return $this->_exception instanceof Exception;
}
/**
* Get the exception if one occurred.
*
* @return Exception the exception that occurred.
* @throws P4Cms_Widget_Exception if no exception occurred.
*/
public function getException()
{
if (!$this->hasException()) {
throw new P4Cms_Widget_Exception(
"Cannot get exception. No exception occurred."
);
}
return $this->_exception;
}
/**
* Set the type of this widget.
*
* @param null|string|P4Cms_Widget_Type $type the type of widget (either id or instance).
*/
public function setType($type)
{
// normalize type instance to id string.
if ($type instanceof P4Cms_Widget_Type) {
$type = $type->getId();
}
if (!is_string($type) && $type !== null) {
throw new InvalidArgumentException(
"Widget type must be a string, a widget type instance or null."
);
}
return $this->_setValue('type', $type);
}
/**
* Get the type of this widget.
*
* @return P4Cms_Widget_Type an instance of this widget's type.
* @throws P4Cms_Widget_Exception if no valid type is set.
*/
public function getType()
{
$type = $this->_getValue('type');
// ensure type id is set.
if (!is_string($type) || !strlen($type)) {
throw new P4Cms_Widget_Exception(
"Cannot get widget type. The type has not been set."
);
}
return P4Cms_Widget_Type::fetch($type);
}
/**
* Whether or not to load this widget asynchronously.
* If true, widget should not be run during initial page
* rendering process, but rather via a subsequent http
* request.
*
* @return bool true if widget should be loaded asynchronously.
*/
public function isAsynchronous()
{
return (bool) $this->asynchronous;
}
/**
* Save this widget.
* Extends parent to keep record of the time that the widget is added.
*
* @param string $description optional - a description of the change.
* @return P4Cms_Record provides a fluent interface
*/
public function save($description = null)
{
if (!$this->getValue('addTime')) {
$this->setValue('addTime', microtime(true));
}
return parent::save($description);
}
/**
* Collect all default widgets and install any that are missing.
*
* @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
*/
public static function installDefaults(P4Cms_Record_Adapter $adapter = null)
{
// clear the module/theme cache
P4Cms_Module::clearCache();
P4Cms_Theme::clearCache();
$packages = P4Cms_Module::fetchAllEnabled();
// add the active theme to the packages list
if (P4Cms_Theme::hasActive()) {
$packages[] = P4Cms_Theme::fetchActive();
}
// install default widgets for each package
foreach ($packages as $package) {
static::installPackageDefaults($package, $adapter);
}
}
/**
* Collect all default widgets from a package and install any that are missing.
*
* @param P4Cms_PackageAbstract $package the package whose widgets are to be installed.
* @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
* @param boolean $restoreDelete optional - restore the deleted widget.
*/
public static function installPackageDefaults(
P4Cms_PackageAbstract $package,
P4Cms_Record_Adapter $adapter = null,
$restoreDelete = false)
{
// if no adapter given, use default.
$adapter = $adapter ?: static::getDefaultAdapter();
// collect default widgets that have not been configured,
// and instantiate the defined widgets
$regionWidgetConfig = $package->getWidgetConfig();
foreach ($regionWidgetConfig as $regionId => $widgetConfigs) {
$widgets = array();
foreach ($widgetConfigs as $id => $widgetConfig) {
// create predictable uuid formatted id from package name, region id and the given id.
$widgetId = $package->getName() . '-' . $regionId . '-' . $id;
$widgetId = P4Cms_Uuid::fromMd5(md5($widgetId))->get();
// skip existing widgets
if (P4Cms_Widget::exists($widgetId, null, $adapter)) {
continue;
}
// Restore the widget if it has been deleted and $restoreDelete is set
if ($restoreDelete &&
P4Cms_Widget::exists($widgetId, array('includeDeleted' => true), $adapter)
) {
$widget = P4Cms_Widget::fetch($widgetId, array('includeDeleted' => true), $adapter);
$widget->save();
} else {
// try to create and save the widget.
try {
$type = isset($widgetConfig['type']) ? $widgetConfig['type'] : null;
$widget = P4Cms_Widget::factory($type, $widgetConfig, $adapter);
$widget->setId($widgetId)
->setValue('region', $regionId)
->save();
} catch (P4Cms_Widget_Exception $e) {
continue;
}
}
}
}
}
/**
* Remove widgets provided by a package.
*
* @param P4Cms_PackageAbstract $package the package whose widgets to be removed
* @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
*/
public static function removePackageDefaults(
P4Cms_PackageAbstract $package,
P4Cms_Record_Adapter $adapter = null)
{
// if no adapter given, use default.
$adapter = $adapter ?: static::getDefaultAdapter();
// collect default regions from the package
$regionWidgetConfig = $package->getWidgetConfig();
foreach ($regionWidgetConfig as $regionId => $widgetConfigs) {
foreach ($widgetConfigs as $id => $widgetConfig) {
// create predictable uuid formatted id from package name, region id and the given id.
$widgetId = $package->getName() . '-' . $regionId . '-' . $id;
$widgetId = P4Cms_Uuid::fromMd5(md5($widgetId))->get();
// delete the widget
try {
P4Cms_Widget::remove($widgetId, $adapter);
} catch (Exception $e) {
// we can't do much if the delete fails.
continue;
}
}
}
}
}