Perforce Chronicle 2012.2/486814
API Documentation

Workflow_Module Class Reference

Integrate the workflow module with the rest of the application. More...

Inheritance diagram for Workflow_Module:
P4Cms_Module_Integration

List of all members.

Static Public Member Functions

static clearPluginLoaders ()
 Reset the workflow plugin loaders.
static getPluginLoader ($type)
 Get a plugin loader for instantiating workflow conditions or actions.
static init ()
 Perform early integration work (before load).

Public Attributes

const TRANSITION_ARROW = "\xe2\x9e\x9c"

Static Protected Attributes

static $_pluginLoaders = array()

Detailed Description

Integrate the workflow module with the rest of the application.

Copyright:
2011-2012 Perforce Software. All rights reserved
License:
Please see LICENSE.txt in top-level folder of this distribution.
Version:
2012.2/486814

Member Function Documentation

static Workflow_Module::clearPluginLoaders ( ) [static]

Reset the workflow plugin loaders.

Useful for testing.

    {
        static::$_pluginLoaders = array();
    }
static Workflow_Module::getPluginLoader ( type) [static]

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.

Parameters:
string$typethe plugin loader to get ('condition' or 'action')
Returns:
Zend_Loader_PluginLoader the loader to use with plugins of this 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;
    }
static Workflow_Module::init ( ) [static]

Perform early integration work (before load).

Reimplemented from P4Cms_Module_Integration.

    {
        // 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       = 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));
            }
        );
    }

Member Data Documentation

Workflow_Module::$_pluginLoaders = array() [static, protected]
const Workflow_Module::TRANSITION_ARROW = "\xe2\x9e\x9c"

The documentation for this class was generated from the following file: