Skip to content

Instantly share code, notes, and snippets.

@raoul2000
Last active October 2, 2015 06:53
Show Gist options
  • Save raoul2000/c38165b7e6b5f12a6945 to your computer and use it in GitHub Desktop.
Save raoul2000/c38165b7e6b5f12a6945 to your computer and use it in GitHub Desktop.
yii2-workflow - WorkflowDbSource prototype
--
-- schema screenshot : http://s172418307.onlinehome.fr/public/raoul2000/static/workflowDbSource-schema.png
--
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
-- -----------------------------------------------------
-- Schema yii2_workflow_test
-- -----------------------------------------------------
-- -----------------------------------------------------
-- Schema yii2_workflow_test
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `yii2_workflow_test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ;
USE `yii2_workflow_test` ;
-- -----------------------------------------------------
-- Table `yii2_workflow_test`.`sw_status`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `yii2_workflow_test`.`sw_status` (
`id` VARCHAR(20) NOT NULL,
`workflow_id` VARCHAR(20) NOT NULL,
`label` VARCHAR(45) NULL,
PRIMARY KEY (`id`, `workflow_id`),
INDEX `fk_sw_status_sw_workflow1_idx` (`workflow_id` ASC),
CONSTRAINT `fk_sw_status_sw_workflow1`
FOREIGN KEY (`workflow_id`)
REFERENCES `yii2_workflow_test`.`sw_workflow` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `yii2_workflow_test`.`sw_workflow`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `yii2_workflow_test`.`sw_workflow` (
`id` VARCHAR(20) NOT NULL,
`initial_status_id` VARCHAR(20) NULL,
PRIMARY KEY (`id`),
INDEX `fk_sw_workflow_sw_status_idx` (`initial_status_id` ASC),
CONSTRAINT `fk_sw_workflow_sw_status`
FOREIGN KEY (`initial_status_id`)
REFERENCES `yii2_workflow_test`.`sw_status` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `yii2_workflow_test`.`sw_transition`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `yii2_workflow_test`.`sw_transition` (
`start_status_id` VARCHAR(20) NOT NULL,
`start_status_workflow_id` VARCHAR(20) NOT NULL,
`end_status_id` VARCHAR(20) NOT NULL,
`end_status_workflow_id` VARCHAR(20) NOT NULL,
PRIMARY KEY (`start_status_id`, `start_status_workflow_id`, `end_status_id`, `end_status_workflow_id`),
INDEX `fk_sw_transition_sw_status1_idx` (`end_status_id` ASC, `end_status_workflow_id` ASC),
CONSTRAINT `fk_start_status`
FOREIGN KEY (`start_status_id` , `start_status_workflow_id`)
REFERENCES `yii2_workflow_test`.`sw_status` (`id` , `workflow_id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_end_status`
FOREIGN KEY (`end_status_id` , `end_status_workflow_id`)
REFERENCES `yii2_workflow_test`.`sw_status` (`id` , `workflow_id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "{{%sw_status}}".
*
* @property string $id
* @property string $workflow_id
* @property string $label
*
* @property SwWorkflow $workflow
* @property SwTransition[] $swTransitions
* @property SwWorkflow[] $swWorkflows
*/
class SwStatus extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return '{{%sw_status}}';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'workflow_id'], 'required'],
[['id', 'workflow_id'], 'string', 'max' => 20],
[['label'], 'string', 'max' => 45]
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'workflow_id' => 'Workflow ID',
'label' => 'Label',
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getWorkflow()
{
return $this->hasOne(SwWorkflow::className(), ['id' => 'workflow_id']);
}
/**
* @return \yii\db\ActiveQuery
*/
public function getTransitions()
{
return $this->hasMany(SwTransition::className(), ['end_status_id' => 'id', 'end_status_workflow_id' => 'workflow_id']);
}
}
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "{{%sw_transition}}".
*
* @property string $start_status_id
* @property string $start_status_workflow_id
* @property string $end_status_id
* @property string $end_status_workflow_id
*
* @property SwStatus $startStatus
* @property SwStatus $endStatus
*/
class SwTransition extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return '{{%sw_transition}}';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['start_status_id', 'start_status_workflow_id', 'end_status_id', 'end_status_workflow_id'], 'required'],
[['start_status_id', 'start_status_workflow_id', 'end_status_id', 'end_status_workflow_id'], 'string', 'max' => 20]
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'start_status_id' => 'Start Status ID',
'start_status_workflow_id' => 'Start Status Workflow ID',
'end_status_id' => 'End Status ID',
'end_status_workflow_id' => 'End Status Workflow ID',
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getStartStatus()
{
return $this->hasOne(SwStatus::className(), ['id' => 'start_status_id', 'workflow_id' => 'start_status_workflow_id']);
}
/**
* @return \yii\db\ActiveQuery
*/
public function getEndStatus()
{
return $this->hasOne(SwStatus::className(), ['id' => 'end_status_id', 'workflow_id' => 'end_status_workflow_id']);
}
}
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "{{%sw_workflow}}".
*
* @property string $id
* @property string $initial_status_id
*
* @property SwStatus[] $swStatuses
* @property SwStatus $initialStatus
*/
class SwWorkflow extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return '{{%sw_workflow}}';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id'], 'required'],
[['id', 'initial_status_id'], 'string', 'max' => 20]
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'initial_status_id' => 'Initial Status ID',
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getStatuses()
{
return $this->hasMany(SwStatus::className(), ['workflow_id' => 'id']);
}
/**
* @return \yii\db\ActiveQuery
*/
public function getInitialStatus()
{
return $this->hasOne(SwStatus::className(), ['id' => 'initial_status_id']);
}
}
<?php
namespace app\components;
use Yii;
use yii\base\Object;
use yii\base\InvalidConfigException;
use yii\helpers\Inflector;
use yii\helpers\VarDumper;
use raoul2000\workflow\base\Status;
use raoul2000\workflow\base\Transition;
use raoul2000\workflow\base\Workflow;
use raoul2000\workflow\base\WorkflowException;
use raoul2000\workflow\source\IWorkflowSource;
use yii\helpers\ArrayHelper;
use raoul2000\workflow\base\WorkflowValidationException;
use app\models\SwStatus;
use app\models\SwWorkflow;
use app\models\SwTransition;
/**
* This is a draft WorkflowDbSource component dedicated to read workflow
* definition from DB.
*
* IT IS FAR FROM COMPLETE AND MUST NOT BE USED FOR ANY OTHER PURPOSE THAN
* TEST AND PROTOTYPE.
*
* It doesn't implement many features available in the WorkflowFileSource component
* released with yii2-workflow but can be used as a starting point to develop
* a production ready component.
*
* Among missing features :
* - Status, Transition and Workllow class mapping to allow usage of custom
* classes for those objects
* - metadata
* - short id usage (in this version canonical status ids must be used)
* - robust test for method arguments (in particular ids)
*
* The underlying DB schema is also very simple and only include those columns
* which are required by yii2-workflow.
*
* @author Raoul
*
*/
class WorkflowDbSource extends Object implements IWorkflowSource
{
const SEPARATOR_STATUS_NAME = '/';
/**
* @var Workflow[] list of workflow instances indexed by workflow id
*/
private $_w = [];
/**
* @var Status[] list status instances indexed by their id
*/
private $_s = [];
private $_allStatusLoaded = false;
/**
* @var Transition[] list of out-going Transition instances indexed by the start status id
*/
private $_t = [];
/**
* @see \raoul2000\workflow\source\IWorkflowSource::getStatus()
*/
public function getStatus($id, $model = null) {
list($wId, $stId) = $this->parseStatusId($id);
$canonicalStId = $wId . self::SEPARATOR_STATUS_NAME . $stId;
// TODO : implement status class map
if ( ! array_key_exists($canonicalStId, $this->_s) ) {
$statusModel = SwStatus::findOne([
'workflow_id' => $wId,
'id' => $stId
]);
if( $statusModel == null) {
throw new WorkflowException('No status found with id '. $id);
}
$this->_s[$canonicalStId] = Yii::createObject([
'class' => 'raoul2000\workflow\base\Status',
'id' => $canonicalStId,
'workflowId' => $statusModel->workflow_id,
'label' => isset($statusModel->label) ? $statusModel->label : Inflector::camel2words($stId, true),
'source' => $this
]);
}
return $this->_s[$canonicalStId];
}
/**
* @see \raoul2000\workflow\source\IWorkflowSource::getAllStatuses()
*/
public function getAllStatuses($workflowId) {
if ( ! $this->_allStatusLoaded ) {
$loadedStatusIds = array_keys($this->_s);
$dbStatus = SwStatus::find()
->where(['workflow_id' => $workflowId])
->andWhere(['NOT IN', 'id', $loadedStatusIds])
->all();
foreach($dbStatus as $status){
$canonicalStId = $status->workflow_id.self::SEPARATOR_STATUS_NAME.$status->id;
$this->_s[$canonicalStId] = Yii::createObject([
'class' => 'raoul2000\workflow\base\Status',
'id' => $canonicalStId,
'workflowId' => $status->workflow_id,
'label' => isset($status->label) ? $status->label : Inflector::camel2words($status->id, true),
'source' => $this
]);
}
$this->_allStatusLoaded = true;
}
return $this->_s;
}
/**
* @see \raoul2000\workflow\source\IWorkflowSource::getTransitions()
*/
public function getTransitions($startStatusId, $model = null) {
list($wId, $stId) = $this->parseStatusId($startStatusId);
$startId = $wId.self::SEPARATOR_STATUS_NAME.$stId;
if ( ! array_key_exists($startId, $this->_t) ) {
$transInstance = [];
$transitions = SwTransition::findAll([
'start_status_id' => $stId,
'start_status_workflow_id' => $wId
]);
foreach($transitions as $transition) {
// TODO : implement transition class map
$endId = $transition->end_status_workflow_id.self::SEPARATOR_STATUS_NAME.$transition->end_status_id;
$transInstance[] = Yii::createObject([
'class' => 'raoul2000\workflow\base\Transition',
'start' => $this->getStatus($startId),
'end' => $this->getStatus($endId),
'source' => $this
]);
}
$this->_t[$startId] = $transInstance;
}
return $this->_t[$startId];
}
/**
* @see \raoul2000\workflow\source\IWorkflowSource::getTransition()
*/
public function getTransition($startId, $endId, $defaultWorkflowId = null) {
$tr = $this->getTransitions($startId, $defaultWorkflowId);
if ( count($tr) > 0 ) {
foreach ($tr as $aTransition) {
if ($aTransition->getEndStatus()->getId() == $endId) {
return $aTransition;
}
}
}
return null;
}
/**
* @see \raoul2000\workflow\source\IWorkflowSource::getWorkflow()
*/
public function getWorkflow($id) {
// TODO : validate that initial status is valid
// TODO : implement status class map
if ( ! array_key_exists($id, $this->_w) ) {
$workflowModel = SwWorkflow::findOne([
'id' => $id
]);
if( $workflowModel == null) {
throw new WorkflowException('No workflow found with id '. $id);
}
$initialStatusId = $workflowModel->id.self::SEPARATOR_STATUS_NAME.$workflowModel->initial_status_id;
$this->_w[$id] = Yii::createObject([
'class' => 'raoul2000\workflow\base\Workflow',
'id' => $id,
'initialStatusId' => $initialStatusId,
'source' => $this
]);
}
return $this->_w[$id];
}
/**
*
* @param string $val canonical id (e.g. myWorkflow/myStatus)
* @return array:
*/
public function parseStatusId($val) {
// TODO : validate $val and once splitted in workflow_id and status_id
// ensure they are both valid
$tokens = array_map('trim', explode(self::SEPARATOR_STATUS_NAME, $val));
return $tokens;
}
}
@philippfrenzel
Copy link

Migrate:

<?php

use yii\db\Schema;
use yii\db\Migration;

class m000000_000001_workflow_init extends Migration
{
    public function up()
    {
        switch (Yii::$app->db->driverName) {
            case 'mysql':
              $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
              break;
            case 'pgsql':
              $tableOptions = null;
              break;
            case 'mssql':
              $tableOptions = null;
              break;            
            default:
              throw new RuntimeException('Your database is not supported!');
        }

        $this->createTable('{{%sw_status}}',array(
            'id'                    => Schema::TYPE_STRING . '(20) NOT NULL',            
            'label'                 => Schema::TYPE_STRING . '(45) NOT NULL',

            // blamable
            'created_by'            => Schema::TYPE_INTEGER . ' NULL',
            'updated_by'            => Schema::TYPE_INTEGER . ' NULL',

            // timestamps
            'created_at'            => Schema::TYPE_INTEGER . ' NOT NULL',
            'updated_at'            => Schema::TYPE_INTEGER . ' NOT NULL',
            'deleted_at'            => Schema::TYPE_INTEGER . ' DEFAULT NULL',

            //foreign keys
            'workflow_id'           => Schema::TYPE_STRING . '(20) NOT NULL',
        ),$tableOptions);

        $this->addPrimaryKey('PK_SW_Status','{{%sw_status}}',['id','workflow_id']);

        $this->createTable('{{%sw_workflow}}',array(
            'id'                    => Schema::TYPE_STRING . '(20) NOT NULL',            

            // blamable
            'created_by'            => Schema::TYPE_INTEGER . ' NULL',
            'updated_by'            => Schema::TYPE_INTEGER . ' NULL',

            // timestamps
            'created_at'            => Schema::TYPE_INTEGER . ' NOT NULL',
            'updated_at'            => Schema::TYPE_INTEGER . ' NOT NULL',
            'deleted_at'            => Schema::TYPE_INTEGER . ' DEFAULT NULL',

            //foreign keys
            'initial_status_id'     => Schema::TYPE_STRING . '(20) NULL',
        ),$tableOptions);

        $this->addPrimaryKey('PK_SW_Workflow','{{%sw_workflow}}','id');
        $this->addForeignKey('FK_SW_Workflow_SW_Status','{{%sw_workflow}}','initial_status_id','{{%sw_status}}','id');

        $this->createTable('{{%sw_transition}}',array(
            'start_status_id'           => Schema::TYPE_STRING . '(20) NOT NULL',
            'start_status_workflow_id'  => Schema::TYPE_STRING . '(20) NOT NULL',
            'end_status_id'             => Schema::TYPE_STRING . '(20) NOT NULL',
            'end_status_workflow_id'    => Schema::TYPE_STRING . '(20) NOT NULL',

            // blamable
            'created_by'            => Schema::TYPE_INTEGER . ' NULL',
            'updated_by'            => Schema::TYPE_INTEGER . ' NULL',

            // timestamps
            'created_at'            => Schema::TYPE_INTEGER . ' NOT NULL',
            'updated_at'            => Schema::TYPE_INTEGER . ' NOT NULL',
            'deleted_at'            => Schema::TYPE_INTEGER . ' DEFAULT NULL',

        ),$tableOptions);

        $this->addPrimaryKey('PK_SW_Status','{{%sw_transition}}',['start_status_id','start_status_workflow_id','end_status_id','end_status_workflow_id']);

        $this->addForeignKey('FK_SW_Transition_SW_Status_Start','{{%sw_workflow}}','start_status_id','{{%sw_status}}','id');
        $this->addForeignKey('FK_SW_Transition_SW_Status_End','{{%sw_workflow}}','end_status_id','{{%sw_status}}','id');
    }

    public function down()
    {
        echo "m000000_000001_workflow_init cannot be reverted.\n";

        return false;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment