<?php
/**
* Integrate the workflow module with the rest of the application.
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
class Workflow_Module extends P4Cms_Module_Integration
{
const TRANSITION_ARROW = "\xe2\x9e\x9c";
/**
* Static storage for the workflow plugin loaders.
* This is so that we only have to configure the loaders
* once. Can be cleared via clearPluginLoaders().
*
* @var array list of plugin loaders.
*/
protected static $_pluginLoaders = array();
/**
* Perform early integration work (before load).
*
* @todo hook into content save to fire off transition actions
*/
public static function init()
{
// participate in content editing by providing a subform.
// we place the workflow sub-form under the save sub-form
// so that the user is prompted for workflow on save.
P4Cms_PubSub::subscribe('p4cms.content.form',
function(Content_Form_Content $form)
{
// if save subform doesn't exist, nothing to do.
$saveSubForm = $form->getSubForm('save');
if (!$saveSubForm) {
return;
}
// if the content entry has no workflow, nothing to do.
$entry = $form->getEntry();
try {
$workflow = Workflow_Model_Workflow::fetchByContent($entry);
} catch (Workflow_Exception $e) {
return;
}
// content type has workflow, add workflow sub-form so
// editor can change state of content.
$workflowForm = new Workflow_Form_EditContent(
array(
'idPrefix' => $form->getIdPrefix(),
'entry' => $entry,
'workflow' => $workflow,
'order' => -10,
'dojoType' => 'p4cms.workflow.ContentSubForm',
'formName' => 'workflow',
'class' => 'workflow-sub-form'
)
);
// normalize workflow sub-form and add it as a content-save sub-form
Content_Form_Content::normalizeSubForm($workflowForm);
$saveSubForm->addSubForm($workflowForm, 'workflow');
}
);
// populate workflow sub-form when editing a content
P4Cms_PubSub::subscribe('p4cms.content.form.populate',
function(Content_Form_Content $form, array $values)
{
// if save subform doesn't exist, nothing to do.
$saveSubForm = $form->getSubForm('save');
if (!$saveSubForm) {
return;
}
// if workflow subform doesn't exist, nothing to do also.
$workflowSubForm = $saveSubForm->getSubForm('workflow');
if (!$workflowSubForm) {
return;
}
// there are 2 different data sources the content form is populated from:
// request data and content entry values
// below we check which case occurs and populate workflow sub-form either
// from passed $values (this happens when form data are contained in the
// request, typically when form was previously submitted) or from content
// entry values (if form data are not present in the request, typically
// when form initializes)
$state = $workflowSubForm->getElement('state');
if (isset($values['workflow']['state'])
&& array_key_exists($values['workflow']['state'], $state->getMultiOptions())
) {
$data = $values['workflow'] + array(
'scheduled' => null,
'scheduledDate' => null,
'scheduledTime' => null
);
// set scheduled to 'false' if it contains whatever else then 'true'
if ($data['scheduled'] !== 'true') {
$data['scheduled'] = 'false';
}
} else {
// get values from entry
$entry = $form->getEntry();
$workflow = Workflow_Model_Workflow::fetchByContent($entry);
$scheduledState = $workflow->getScheduledStateOf($entry);
$scheduledTime = $workflow->getScheduledTimeOf($entry);
$isScheduled = $scheduledState !== null;
$selectedState = $isScheduled
? $scheduledState->getId()
: $workflow->getStateOf($entry)->getId();
$data = array(
'state' => $selectedState,
'scheduled' => $isScheduled ? 'true' : 'false',
'scheduledDate' => $isScheduled ? date('Y-m-d', $scheduledTime) : null,
'scheduledTime' => $isScheduled ? date('H:i', $scheduledTime) : null
);
}
// populate the workflow sub-form with prepared data
$workflowSubForm->populate($data);
}
);
// re-evaluate 'valid' transitions in light of pending data.
P4Cms_PubSub::subscribe('p4cms.content.form.preValidate',
function(Content_Form_Content $form, array $values)
{
// if save subform doesn't exist, nothing to do.
$saveSubForm = $form->getSubForm('save');
if (!$saveSubForm) {
return;
}
// if workflow subform doesn't exist, nothing to do also.
$workflowSubForm = $saveSubForm->getSubForm('workflow');
if (!$workflowSubForm) {
return;
}
$state = $workflowSubForm->getElement('state');
$state->setMultiOptions($workflowSubForm->getStateOptions($values));
}
);
// connect to content pre-save event to use the workflow model's method
// of storing the workflow state (validates state and stores it as a
// first-class attribute - otherwise state would be an array and hard
// to query).
P4Cms_PubSub::subscribe('p4cms.content.record.preSave',
function(P4Cms_Record $entry)
{
$workflow = $entry->getValue('workflow');
$entry->unsetValue('workflow');
// if workflow is not an array, nothing to work with.
if (!is_array($workflow)) {
return;
}
// grab the workflow model for this content entry
// if no workflow, nothing to do.
try {
$workflowModel = Workflow_Model_Workflow::fetchByContent($entry);
} catch (Workflow_Exception $e) {
return;
}
// set current state or scheduled state and time if transition is scheduled
if (isset($workflow['state'])) {
// set scheduled state/time if scheduled option was selected and there
// is a transition (i.e. other than current state was selected),
// otherwise set current state
$currentState = $workflowModel->getStateOf($entry)->getId();
if ($workflow['state'] !== $currentState && isset($workflow['scheduled'])
&& $workflow['scheduled'] === 'true'
) {
$time = strtotime(
$workflow['scheduledDate'] . ' ' . $workflow['scheduledTime']
);
$workflowModel->setScheduledStateOf($entry, $workflow['state'], $time);
} else {
$workflowModel->setStateOf($entry, $workflow['state']);
}
}
}
);
// connect to content post-save event to detect workflow
// transitions and invoke any transition actions.
P4Cms_PubSub::subscribe('p4cms.content.record.postSave',
function(P4Cms_Record $entry)
{
// grab the workflow model for this content entry
// if no workflow, nothing to do.
try {
$workflowModel = Workflow_Model_Workflow::fetchByContent($entry);
} catch (Workflow_Exception $e) {
return;
}
// detect workflow transition and invoke actions.
$transition = $workflowModel->detectTransitionOn($entry);
if ($transition) {
$transition->invokeActionsOn($entry);
}
}
);
// connect to content query generation to filter unpublished
// content from users that don't have permission to see it.
P4Cms_PubSub::subscribe('p4cms.content.record.query',
function(P4Cms_Record_Query $query, P4Cms_Record_Adapter $adapter)
{
$user = P4Cms_User::fetchActive();
if (!$user->isAllowed('content', 'access-unpublished')) {
$filter = Workflow_Model_Workflow::makePublishedContentFilter();
// add filter to allow accessing own content (as long as user is not anonymous)
if (!$user->isAnonymous()) {
$filter->add(
P4Cms_Content::OWNER_FIELD,
$user->getId(),
P4Cms_Record_Filter::COMPARE_EQUAL,
P4Cms_Record_Filter::CONNECTIVE_OR
);
}
$query->addFilter($filter);
}
}
);
// provide form to filter content by workflow state.
P4Cms_PubSub::subscribe('p4cms.content.grid.form.subForms',
function(Zend_Form $form)
{
// provide the form only if user can access unpublished content
$user = P4Cms_User::fetchActive();
if (!$user->isAllowed('content', 'access-unpublished')) {
return;
}
return new Workflow_Form_GridStateFilter;
}
);
// filter content query by selected states.
P4Cms_PubSub::subscribe('p4cms.content.grid.populate',
function(P4Cms_Record_Query $query, Zend_Form $form)
{
// get workflow sub-form
$workflowForm = $form->getSubForm('workflow');
if (!$workflowForm instanceof Workflow_Form_GridStateFilter) {
return;
}
// early exit if no workflow filters selected
$workflow = $workflowForm->getValue('workflow');
if (!$workflow) {
return;
}
// get list of target states where filters should be applied to: current, scheduled or either
$target = $workflowForm->getValue('targetState');
if ($target === 'current') {
$targets = array(false);
} else if ($target === 'scheduled') {
$targets = array(true);
} else if ($target === 'either') {
$targets = array(false, true);
} else {
$targets = array();
}
$filter = new P4Cms_Record_Filter;
foreach ($targets as $scheduled) {
// create subfilter depending on selected workflow options and target states
switch ($workflow) {
case Workflow_Form_GridStateFilter::OPTION_ONLY_PUBLISHED:
$subFilter = Workflow_Model_Workflow::makePublishedContentFilter($scheduled);
break;
case Workflow_Form_GridStateFilter::OPTION_ONLY_UNPUBLISHED:
$subFilter = Workflow_Model_Workflow::makeUnpublishedContentFilter($scheduled);
break;
case Workflow_Form_GridStateFilter::OPTION_USER_SELECTED:
$subFilter = Workflow_Model_Workflow::makeStatesContentFilter(
$workflowForm->getSelectedStates(), $scheduled
);
break;
default:
return;
}
// append subfilter to the record filter
$filter->addSubFilter($subFilter, P4Cms_Record_Filter::CONNECTIVE_OR);
}
$query->addFilter($filter);
}
);
// provide form to filter content history list by workflow state.
P4Cms_PubSub::subscribe('p4cms.history.grid.form.subForms',
function(Zend_Form $form)
{
// get record the history grid was constructed for from the form
// if it is not a content record, we have no interest in it
$record = $form->getRecord();
if (!$record instanceof P4Cms_Content) {
return;
}
$workflow = $record->getContentType()->workflow;
if (!Workflow_Model_Workflow::exists($workflow)) {
return;
}
$workflow = Workflow_Model_Workflow::fetch($workflow);
$states = $workflow->getStateModels();
$stateOptions = array_combine($states->invoke('getId'), $states->invoke('getLabel'));
// add all states that are not governed by the current workflow but appear in the grid
$extraStates = array();
$filename = $record->toP4File()->getDepotFilename();
foreach ($form->getChanges() as $change) {
$file = $change->getFileObject($filename);
$entry = P4Cms_Content::fromP4File($file);
$extraStates[] = $entry->getValue(Workflow_Model_State::RECORD_FIELD);
$extraStates[] = $entry->getValue(Workflow_Model_State::RECORD_SCHEDULED_FIELD);
}
$extraStates = array_diff(
array_unique(array_filter($extraStates)),
array_keys($stateOptions)
);
// don't show sub-form if there is less than 2 states
if (count($stateOptions) + count($extraStates) < 2) {
return;
}
// create the form to filter grid by workflow states
$form = new P4Cms_Form_SubForm;
$form->setName('workflow')
->setAttrib('class', 'states-form')
->setOrder(40);
// add select box with options the filters will be applied to
$form->addElement(
'Select',
'targetState',
array(
'label' => 'Workflow',
'multiOptions' => array(
'current' => 'Current Status',
'scheduled' => 'Scheduled Status',
'either' => 'Current or Scheduled Status'
),
'autoApply' => true,
'order' => 40
)
);
// add checkboxes with existing states
if (count($stateOptions)) {
$form->addElement(
'MultiCheckbox', 'validStates',
array(
'multiOptions' => $stateOptions,
'autoApply' => true,
'order' => 41
)
);
}
// add checkboxes with extra states sorted alphabetically
if (count($extraStates)) {
natcasesort($extraStates);
$form->addElement(
'MultiCheckbox', 'extraStates',
array(
'multiOptions' => array_combine($extraStates, $extraStates),
'autoApply' => true
)
);
// put extra states into a display group so it can be styled separately
$form->addDisplayGroup(
array('extraStates'),
'extraStatesGroup',
array(
'order' => 42
)
);
}
return $form;
}
);
// filter history grid by selected states.
P4Cms_PubSub::subscribe('p4cms.history.grid.populate',
function(P4_Model_Iterator $changes, Zend_Form $form)
{
$values = $form->getValues();
$workflow = isset($values['workflow']) ? $values['workflow'] : array();
// extract states from workflow options
$states = array_merge(
isset($workflow['validStates']) ? $workflow['validStates'] : array(),
isset($workflow['extraStates']) ? $workflow['extraStates'] : array()
);
// get entry field the filters will be applied to
$applyTo = isset($values['workflow']['targetState'])
? $values['workflow']['targetState']
: null;
// early exit if no states selected or not specified where to apply the filters
if (!count($states) || !$applyTo) {
return;
}
// get record the history grid was constructed for from the form
$record = $form->getRecord();
if (!$record instanceof P4Cms_Content) {
return;
}
// filter entries to keep only revisions with one of the selected workflow states
$filename = $record->toP4File()->getDepotFilename();
$changes->filterByCallback(
function($change) use ($states, $filename, $applyTo)
{
$file = $change->getFileObject($filename);
$entry = P4Cms_Content::fromP4File($file);
$current = $entry->getValue(Workflow_Model_State::RECORD_FIELD);
$scheduled = $entry->getValue(Workflow_Model_State::RECORD_SCHEDULED_FIELD);
$inCurrent = in_array($current, $states);
$inScheduled = in_array($scheduled, $states);
return ($applyTo === 'current' && $inCurrent)
|| ($applyTo === 'scheduled' && $inScheduled)
|| ($applyTo === 'either' && ($inCurrent || $inScheduled));
}
);
}
);
// add state field into the dojo data passed to the content data grid
$workflows = null;
$contentTypes = null;
P4Cms_PubSub::subscribe('p4cms.content.grid.data.item',
function(array $data, P4Cms_Content $content, $helper) use (&$workflows, &$contentTypes)
{
// get the workflow used by this content entry's type - we use
// references for the workflows and content types for performance.
$workflows = $workflows ?: Workflow_Model_Workflow::fetchAll();
$types = $contentTypes ?: P4Cms_Content_Type::fetchAll();
$stateField = Workflow_Model_State::RECORD_FIELD;
$type = $content->getContentTypeId();
$type = $type && isset($types[$type]) ? $types[$type] : null;
$workflow = $type ? $type->workflow : null;
$workflow = $workflow && isset($workflows[$workflow]) ? $workflows[$workflow] : null;
// deal with the three types of output
// a) The type specifies a valid workflow, output the current state
// b) The type specifies no workflow, implicitly published
// c) The type or workflow are invalid empty output
if ($type && $type->workflow && $workflow) {
$state = $workflow->getStateOf($content);
$data[$stateField] = $state->getLabel();
$data['workflow'] = $workflow->getLabel() . ': ' . $state->getLabel();
$data['workflowId'] = $workflow->getId();
// if there is scheduled transition, append info about scheduled state and time
$scheduledState = $workflow->getScheduledStateOf($content);
if ($scheduledState !== null) {
$timestamp = $workflow->getScheduledTimeOf($content);
$data[$stateField] .= ' ' . Workflow_Module::TRANSITION_ARROW
. ' ' . $scheduledState->getLabel();
$data['workflow'] .= '<br>'
. $state->getTransitionModel($scheduledState->getId())->getLabel()
. ' on ' . date('M j, Y', $timestamp)
. ' at ' . date('g:i A T', $timestamp);
}
} else if ($type && !$type->workflow) {
$data[$stateField] = ucfirst(Workflow_Model_State::PUBLISHED);
$data['workflow'] = 'No workflow: content automatically published';
$data['workflowId'] = '';
} else {
$data[$stateField] = '';
$data['workflow'] = 'Unknown workflow state. Content type and/or workflow are missing.';
$data['workflowId'] = '';
}
return $data;
}
);
// add state column into the content data grid
P4Cms_PubSub::subscribe('p4cms.content.grid.render',
function($helper)
{
$attributes = array(
'order' => 35,
'width' => '20%',
'label' => 'Workflow',
'formatter' => 'p4cms.workflow.contentGridFormatters.state'
);
$helper->addColumn(Workflow_Model_State::RECORD_FIELD, $attributes, false);
// attach tooltip dialog to this columns to show workflow details
$tooltips = $helper->getAttrib('fieldTooltips') ?: array();
$tooltips[] = array(
'sourceField' => 'workflow',
'attachField' => Workflow_Model_State::RECORD_FIELD
);
$helper->setAttrib('fieldTooltips', $tooltips);
}
);
// add button to the footer for changing workflow state on selected entries
P4Cms_PubSub::subscribe('p4cms.content.grid.render',
function($helper)
{
// only add button if user can edit content and the delete button is showing.
// if the delete button is showing, that is indicative of an editing context.
$user = P4Cms_User::fetchActive();
if (!$helper->view->showDeleteButton || !$user->isAllowed('content', 'edit')) {
return;
}
$helper->addButton(
'Workflow',
array(
'attribs' => array(
'onclick' => 'p4cms.workflow.content.grid.Utility.openWorkflowDialog();',
'class' => 'workflow-button'
),
'order' => 20
)
);
}
);
// add state field into the dojo data passed to the history data grid
P4Cms_PubSub::subscribe('p4cms.history.grid.data.item',
function(array $data, P4_Change $change, $helper)
{
// we are only interested in the content history grid
if (!$helper->view->record instanceof P4Cms_Content) {
return;
}
$revspec = isset($data['version']) ? $data['version'] : null;
$recordId = isset($data['recordId']) ? $data['recordId'] : null;
// get workflow state of given content entry at #revspec
if ($revspec && $recordId) {
$entry = P4Cms_Content::fetch(
$recordId . '#' . $revspec,
array('includeDeleted' => true)
);
// if entry has no workflow, nothing to do
try {
$workflow = Workflow_Model_Workflow::fetchByContent($entry);
} catch (Workflow_Exception $e) {
return $data;
}
// add state to data as array with state label/id and flag whether it exists or not
$stateId = $entry->getValue(Workflow_Model_State::RECORD_FIELD);
$scheduledStateId = $entry->getValue(Workflow_Model_State::RECORD_SCHEDULED_FIELD);
if ($workflow->hasState($stateId)) {
$state = array(
'state' => $workflow->getStateModel($stateId)->getLabel(),
'exists' => true
);
// append scheduled state if entry has one
if ($scheduledStateId && $workflow->hasState($scheduledStateId)) {
$state['state'] .= ' ' . Workflow_Module::TRANSITION_ARROW . ' '
. $workflow->getStateModel($scheduledStateId)->getLabel();
}
} else {
$state = array(
'state' => $stateId,
'exists' => false
);
// append scheduled state if entry has one
if ($scheduledStateId) {
$state['state'] .= ' ' . Workflow_Module::TRANSITION_ARROW . ' '
. $scheduledStateId;
}
}
$data['state'] = $state;
}
return $data;
}
);
// add workflow column into the history data grid
P4Cms_PubSub::subscribe('p4cms.history.grid.render',
function($helper)
{
// do not show column if content is not under workflow
if (!P4cms_Content::exists($helper->view->id)
|| !P4cms_Content::fetch($helper->view->id)->getContentType()->workflow
) {
return;
}
$attributes = array(
'order' => 35,
'width' => '20%',
'label' => 'Workflow',
'formatter' => 'p4cms.workflow.contentHistoryGridFormatters.state'
);
$helper->addColumn('state', $attributes, false);
}
);
// provide workflow grid actions
P4Cms_PubSub::subscribe('p4cms.workflow.grid.actions',
function($actions)
{
$actions->addPages(
array(
array(
'label' => 'Edit',
'onClick' => 'p4cms.workflow.grid.Actions.onClickEdit();',
'order' => '10'
),
array(
'label' => 'Delete',
'onClick' => 'p4cms.workflow.grid.Actions.onClickDelete();',
'order' => '20'
)
)
);
}
);
// provide content grid actions
P4Cms_PubSub::subscribe('p4cms.content.grid.actions',
function($actions)
{
$actions->addPages(
array(
array(
'label' => 'Change Status',
'onClick' => 'p4cms.workflow.content.grid.Actions.onClickChangeStatus();',
'onShow' => 'p4cms.workflow.content.grid.Actions.onShowChangeStatus(this);',
'order' => '100',
'resource' => 'content',
'privilege' => 'edit'
)
)
);
}
);
// provide form to search workflows
P4Cms_PubSub::subscribe('p4cms.workflow.grid.form.subForms',
function(Zend_Form $form)
{
return new Ui_Form_GridSearch;
}
);
// filter workflows by keyword search
P4Cms_PubSub::subscribe('p4cms.workflow.grid.populate',
function(P4Cms_Model_Iterator $workflows, Zend_Form $form)
{
$values = $form->getValues();
// extract search query.
$query = isset($values['search']['query'])
? $values['search']['query']
: null;
// early exit if no query.
if (!$query) {
return null;
}
// remove workflows that don't match search query.
return $workflows->search(
array('label'),
$query
);
}
);
// provide form to filter workflows by associated content types
P4Cms_PubSub::subscribe('p4cms.workflow.grid.form.subForms',
function(Zend_Form $form)
{
return new Content_Form_GridTypeFilter;
}
);
// filter workflows by selected content types
P4Cms_PubSub::subscribe('p4cms.workflow.grid.populate',
function(P4Cms_Model_Iterator $workflows, Zend_Form $form)
{
// get type sub-form.
$typeForm = $form->getSubForm('type');
if (!$typeForm instanceof Content_Form_GridTypeFilter) {
return;
}
// filter for selected types.
$types = $typeForm->getElement('types')->getNormalizedTypes();
if (count($types)) {
// get list of workflows of all selected types
$typeWorkflows = P4Cms_Content_Type::fetchAll(array('ids' => $types))
->invoke('getValue', array('workflow'));
// filter workflows to keep only those associated with selected content types
$workflows->filter('id', array_unique($typeWorkflows));
}
}
);
// update workflows when a site is created.
P4Cms_PubSub::subscribe('p4cms.site.created',
function(P4Cms_Site $site)
{
$adapter = $site->getStorageAdapter();
Workflow_Model_Workflow::installDefaultWorkflows($adapter);
}
);
// update workflows when a module/theme is enabled.
$installDefaults = function(P4Cms_Site $site, P4Cms_PackageAbstract $package)
{
$adapter = $site->getStorageAdapter();
Workflow_Model_Workflow::installPackageDefaults($package, $adapter);
};
P4Cms_PubSub::subscribe('p4cms.site.module.enabled', $installDefaults);
P4Cms_PubSub::subscribe('p4cms.site.theme.enabled', $installDefaults);
// update workflows when a module/theme is disabled
$removeDefaults = function(P4Cms_Site $site, P4Cms_PackageAbstract $package)
{
$adapter = $site->getStorageAdapter();
Workflow_Model_Workflow::removePackageDefaults($package, $adapter);
};
P4Cms_PubSub::subscribe('p4cms.site.module.disabled', $removeDefaults);
P4Cms_PubSub::subscribe('p4cms.site.theme.disabled', $removeDefaults);
// add workflow drop-down to content type form.
P4Cms_PubSub::subscribe('p4cms.content.type.form',
function(P4Cms_Form_PubSubForm $form)
{
// collect available workflows.
$options = array('' => 'No Workflow (Always Published)');
$workflows = Workflow_Model_Workflow::fetchAll();
foreach ($workflows as $workflow) {
$states = $workflow->getStateModels()->invoke('getLabel');
$states = implode(', ', $states);
$helper = $form->getView()->getHelper('truncate');
$states = $helper->truncate($states, 50, '...');
$label = $workflow->getLabel() . " ($states)";
$options[$workflow->getId()] = $label;
}
$form->addElement(
'select',
'workflow',
array(
'label' => 'Workflow',
'multiOptions' => $options,
'description' => 'Select a workflow to control the process of creating '
. 'and publishing content of this type.',
'order' => 6
)
);
}
);
// connect to search prepare document event to add the workflow state
P4Cms_PubSub::subscribe('p4cms.search.prepareDocument',
function($document, $original)
{
// we only care about lucene documents and content records.
if (!$document instanceof Zend_Search_Lucene_Document
|| !$original instanceof P4Cms_Content
) {
return $document;
}
// add the workflow state, but don't index it.
$document->addField(
Zend_Search_Lucene_Field::unIndexed(
Workflow_Model_State::RECORD_FIELD,
$original->getValue(Workflow_Model_State::RECORD_FIELD)
)
);
return $document;
}
);
// connect to search results event to filter unpublished content
$workflows = null;
$contentTypes = null;
P4Cms_PubSub::subscribe('p4cms.search.results',
function($results) use (&$workflows, &$contentTypes)
{
// nothing to do if current user can access unpublished content.
$user = P4Cms_User::fetchActive();
if ($user->isAllowed('content', 'access-unpublished')) {
return $results;
}
// populate the workflows and content types if needed - we use
// references for the workflows and content types for performance.
$workflows = $workflows ?: Workflow_Model_Workflow::fetchAll();
$types = $contentTypes ?: P4Cms_Content_Type::fetchAll();
// exclude hits that are not published
foreach ($results as $key => $result) {
$document = $result->getDocument();
$fields = $document->getFieldNames();
// only consider results that appear to reference content.
if (!in_array('contentType', $fields)) {
continue;
}
$type = $document->contentType;
$type = isset($types[$type]) ? $types[$type] : null;
$workflow = $type ? $type->workflow : null;
$workflow = $workflow && isset($workflows[$workflow]) ? $workflows[$workflow] : null;
// only check state on content types under workflow.
if ($type && !$type->workflow) {
continue;
}
// remove any entries with invalid type or workflow settings
if (!$type || !$workflow) {
unset($results[$key]);
continue;
}
// remove unpublished content entries
$state = in_array(Workflow_Model_State::RECORD_FIELD, $fields)
? $document->getFieldValue(Workflow_Model_State::RECORD_FIELD)
: null;
if (!$workflow->hasState($state) || $state !== Workflow_Model_State::PUBLISHED) {
unset($results[$key]);
}
}
return $results;
}
);
// process scheduled transitions
// @todo should we clear scheduled data on entries that fail when changing the state?
// they will most likely fail on the next run as well
P4Cms_PubSub::subscribe('p4cms.cron.hourly',
function()
{
// elevate privileges of current (cron) user to grant all content privileges
P4Cms_User::fetchActive()->allow('content');
// get record filter to keep only entries with scheduled transitions
// where scheduled time is in the past
$filter = Workflow_Model_Workflow::makeScheduledContentFilter();
// iterate over filtered entries and process scheduled transitions
$query = P4Cms_Record_Query::create()->addFilter($filter);
$report = array();
foreach (P4Cms_Content::fetchAll($query) as $entry) {
$id = $entry->getId();
try {
// get the governing workflow of the entry
$workflow = Workflow_Model_Workflow::fetchByContent($entry);
// update the state of workflow for the entry according to the
// scheduled transition
$fromState = $workflow->getStateOf($entry);
$toState = $workflow->getScheduledStateOf($entry);
if (!$toState) {
throw new Exception("Scheduled state not found.");
}
$workflow->setStateOf($entry, $toState->getId());
$entry->save(
"Processed scheduled transition: "
. $fromState->getLabel()
. " " . Workflow_Module::TRANSITION_ARROW . " "
. $toState->getLabel() . "."
);
} catch (Exception $e) {
$message = "Cannot process scheduled transition for entry id '$id': "
. $e->getMessage();
P4Cms_Log::log($message, P4Cms_Log::ERR);
$report['error'][] = $message;
continue;
}
$message = "Processed scheduled transition for content entry id '$id'"
. " (from state: " . $fromState->getLabel()
. ", to state: " . $toState->getLabel() . ").";
P4Cms_Log::log($message, P4Cms_Log::NOTICE);
$report['notice'][] = $message;
}
return $report;
}
);
// organize workflow under configuration group for pull operations.
P4Cms_PubSub::subscribe(
'p4cms.site.branch.pull.groupPaths',
function($paths, $source, $target, $result)
{
$paths->addSubGroup(
array(
'label' => 'Workflows',
'basePaths' => $target->getId() . '/workflows/...',
'inheritPaths' => $target->getId() . '/workflows/...',
'pullByDefault' => true,
'details' =>
function($paths) use ($source, $target)
{
$pathsById = array();
foreach ($paths as $path) {
if (strpos($path->depotFile, $target->getId() . '/workflows/') === 0) {
$id = Workflow_Model_Workflow::depotFileToId($path->depotFile);
$pathsById[$id] = $path;
}
}
$details = new P4Cms_Model_Iterator;
$entries = Site_Model_PullPathGroup::fetchRecords(
array_keys($pathsById), 'Workflow_Model_Workflow', $source, $target
);
foreach ($entries as $entry) {
$path = $pathsById[$entry->getId()];
$details[] = new P4Cms_Model(
array(
'conflict' => $path->conflict,
'action' => $path->action,
'label' => $entry->getLabel()
)
);
}
$details->setProperty(
'columns',
array('label' => 'Workflow', 'action' => 'Action')
);
return $details;
}
)
);
}
);
// help organize content-related records by workflow when pulling changes.
P4Cms_PubSub::subscribe(
'p4cms.site.branch.pull.groupPaths',
function($paths, $source, $target, $result)
{
// try to find the content entries group
$content = $paths->getSubGroup('Content');
$entries = $content ? $content->getSubGroup('Entries') : null;
if (!$entries) {
return;
}
// all paths will be in target syntax. we need to convert any paths
// we are not deleting to source syntax to check their status.
$paths = array();
foreach ($entries->getPaths() as $path) {
if ($path->action != 'delete') {
$paths[] = $source->getId() . substr($path->depotFile, strlen($target->getId()));
} else {
$paths[] = $path->depotFile;
}
}
// determine which paths, if any, represent published content entries
$filter = Workflow_Model_Workflow::makePublishedContentFilter(false, $source->getStorageAdapter());
$query = P4_File_Query::create()
->addFilespecs($paths)
->setLimitFields(array('depotFile'))
->setFilter($filter);
$published = $paths
? P4_File::fetchAll($query, $source->getStorageAdapter()->getConnection())
: new P4_Model_Iterator;
// translate any source syntax results back to target syntax.
$paths = array();
foreach ($published->invoke('getValue', array('depotFile')) as $path) {
if (strpos($path, $source->getId()) === 0) {
$path = $target->getId() . substr($path, strlen($source->getId()));
}
$paths[] = $path;
}
$entries->addSubGroup(
array(
'label' => 'Published Entries',
'inheritPaths' => $paths,
'pullByDefault' => true,
'order' => -100,
'details' => $entries->getDetailsCallback()
)
);
// move the remaining paths to an un-published content group
$entries->addSubGroup(
array(
'label' => 'Unpublished Entries',
'inheritPaths' => $entries->getPaths(),
'pullByDefault' => true,
'order' => -90,
'details' => $entries->getDetailsCallback()
)
);
// move our published/unpublished group (and any others) up to
// the content group instead of being under entries
foreach ($entries->getSubGroups() as $group) {
$content->addSubGroup($group);
}
// remove the now empty entries group as we are done with it
$content->getSubGroups()
->filter('label', 'Entries', array(P4Cms_Model_Iterator::FILTER_INVERSE));
}
);
}
/**
* Get a plugin loader for instantiating workflow conditions or actions.
*
* This loader is configured with appropriate prefixes and paths for
* all enabled modules that include workflow plugins of the given type.
* This allows plugins to be loaded via their short name and overridden
* by later modules.
*
* @param string $type the plugin loader to get ('condition' or 'action')
* @return Zend_Loader_PluginLoader the loader to use with plugins of this type
*/
public static function getPluginLoader($type)
{
$types = array(
'action' => array('/workflows/actions', '_Workflow_Action'),
'condition' => array('/workflows/conditions', '_Workflow_Condition')
);
if (!$type || !isset($types[$type])) {
throw new InvalidArgumentException(
"Cannot get plugin loader. Invalid plugin type specified."
);
}
// return cached copy if present.
if (isset(static::$_pluginLoaders[$type])) {
return static::$_pluginLoaders[$type];
}
// make a new plugin loader and add paths for all
// modules containing workflow plugins of given type.
$loader = new Zend_Loader_PluginLoader;
foreach (P4Cms_Module::fetchAllEnabled() as $module) {
$path = $module->getPath() . $types[$type][0];
if (is_dir($path)) {
$loader->addPrefixPath(
$module->getName() . $types[$type][1],
$path
);
}
}
static::$_pluginLoaders[$type] = $loader;
return $loader;
}
/**
* Reset the workflow plugin loaders. Useful for testing.
*/
public static function clearPluginLoaders()
{
static::$_pluginLoaders = array();
}
}