├── .gitignore ├── AutoNumber.php ├── AutonumberValidator.php ├── Behavior.php ├── Bootstrap.php ├── README.md ├── composer.json └── migrations └── m140527_084418_auto_number.php /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/* 2 | /vendor 3 | /composer.lock -------------------------------------------------------------------------------- /AutoNumber.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 1.0 17 | */ 18 | class AutoNumber extends ActiveRecord 19 | { 20 | 21 | /** 22 | * @inheritdoc 23 | */ 24 | public static function tableName() 25 | { 26 | return '{{%auto_number}}'; 27 | } 28 | 29 | /** 30 | * @inheritdoc 31 | */ 32 | public function rules() 33 | { 34 | return [ 35 | [['optimistic_lock', 'number'], 'default', 'value' => 1], 36 | [['group'], 'required'], 37 | [['number'], 'integer'], 38 | [['group'], 'string'] 39 | ]; 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function attributeLabels() 46 | { 47 | return [ 48 | 'template' => 'Template Num', 49 | 'number' => 'Number', 50 | ]; 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | public function optimisticLock() 57 | { 58 | return 'optimistic_lock'; 59 | } 60 | 61 | /** 62 | * 63 | * @param string $format value inner `{}` will evaluate as date. Number of digit represented as number of `?`. 64 | * @param bool $alnum if true will generate alfanumeric value. If false generate only number. 65 | * @param int $digit For compatibility purpose. 66 | * @param array $group For compatibility purpose. 67 | * @return string 68 | */ 69 | public static function generate($format, $alnum = false, $digit = null, array $group = []) 70 | { 71 | if ($format) { 72 | $format = preg_replace_callback('/\{([^\}]+)\}/', function($matchs) { 73 | return date($matchs[1]); 74 | }, $format); 75 | } 76 | 77 | if (empty($group) && strlen($format) < 32) { 78 | $key = (string)$format; 79 | } else { 80 | $group['value'] = $format; 81 | $key = md5(serialize($group)); 82 | } 83 | 84 | $command = \Yii::$app->db->createCommand(); 85 | $command->setSql('SELECT [[number]] FROM {{%auto_number}} WHERE [[group]]=:key'); 86 | $counter = $command->bindValue(':key', $key)->queryScalar() + 1; 87 | $command->upsert('{{%auto_number}}', ['group' => $key, 'number' => $counter, 'optimistic_lock' => 1, 'update_time' => time()])->execute(); 88 | $number = $alnum ? strtoupper(base_convert($counter, 10, 36)) : (string) $counter; 89 | 90 | if ($format === null) { 91 | $result = $number; 92 | } elseif ($digit) { 93 | $number = str_pad($number, $digit, '0', STR_PAD_LEFT); 94 | $result = str_replace('?', $number, $format); 95 | } else { 96 | $places = []; 97 | $total = 0; 98 | $result = preg_replace_callback('/\?+/', function($matchs) use(&$places, &$total) { 99 | $n = strlen($matchs[0]); 100 | $i = count($places); 101 | $places[] = $n; 102 | $total += $n; 103 | return "<[~{$i}~]>"; 104 | }, $format); 105 | 106 | if ($total > 1) { 107 | $number = str_pad($number, $total, '0', STR_PAD_LEFT); 108 | $parts = []; 109 | for ($i = count($places) - 1; $i >= 0; $i--) { 110 | if ($i == 0) { 111 | $parts[0] = $number; 112 | } else { 113 | $parts[$i] = substr($number, -$places[$i]); 114 | $number = substr($number, 0, -$places[$i]); 115 | } 116 | } 117 | $result = preg_replace_callback('/<\[~(\d+)~\]>/', function($matchs) use(&$parts) { 118 | $i = $matchs[1]; 119 | return $parts[$i]; 120 | }, $result); 121 | } else { 122 | $result = str_replace('?', $number, $format); 123 | } 124 | } 125 | return $result; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /AutonumberValidator.php: -------------------------------------------------------------------------------- 1 | 'SA.'.date('Ymd').'?'], 18 | * ... 19 | * ] 20 | * ~~~ 21 | * 22 | * @author Misbahul D Munir 23 | * @since 1.0 24 | */ 25 | class AutonumberValidator extends Validator 26 | { 27 | /** 28 | * @var mixed the default value or a PHP callable that returns the default value which will 29 | * be assigned to the attributes being validated if they are empty. The signature of the PHP callable 30 | * should be as follows, 31 | * 32 | * ```php 33 | * function foo($model, $attribute) { 34 | * // compute value 35 | * return $value; 36 | * } 37 | * ``` 38 | * 39 | * @see [[Behavior::$value]] 40 | */ 41 | public $format; 42 | 43 | /** 44 | * @var integer digit number of auto number 45 | */ 46 | public $digit; 47 | 48 | /** 49 | * @var mixed 50 | */ 51 | public $group; 52 | 53 | /** 54 | * @var bool 55 | */ 56 | public $alnum; 57 | 58 | /** 59 | * @var boolean 60 | */ 61 | public $unique = true; 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public $skipOnEmpty = false; 67 | 68 | /** 69 | * @var boolean 70 | */ 71 | public $throwIsStale = false; 72 | 73 | /** 74 | * @var array 75 | */ 76 | private static $_executed = []; 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function validateAttribute($object, $attribute) 82 | { 83 | if ($this->isEmpty($object->$attribute)) { 84 | $eventId = uniqid(); 85 | $object->on(ActiveRecord::EVENT_BEFORE_INSERT, [$this, 'beforeSave'], [$eventId, $attribute]); 86 | $object->on(ActiveRecord::EVENT_BEFORE_UPDATE, [$this, 'beforeSave'], [$eventId, $attribute]); 87 | } 88 | } 89 | 90 | /** 91 | * Handle for [[\yii\db\ActiveRecord::EVENT_BEFORE_INSERT]] and [[\yii\db\ActiveRecord::EVENT_BEFORE_UPDATE]] 92 | * @param \yii\base\ModelEvent $event 93 | */ 94 | public function beforeSave($event) 95 | { 96 | list($id, $attribute) = $event->data; 97 | if (isset(self::$_executed[$id])) { 98 | return; 99 | } 100 | 101 | /* @var $object \yii\db\ActiveRecord */ 102 | $object = $event->sender; 103 | if (is_string($this->format) && method_exists($object, $this->format)) { 104 | $value = call_user_func([$object, $this->format], $object, $attribute); 105 | } else { 106 | $value = is_callable($this->format) ? call_user_func($this->format, $object, $attribute) : $this->format; 107 | } 108 | 109 | $group = [ 110 | 'class' => $this->unique ? get_class($object) : false, 111 | 'group' => $this->group, 112 | 'attribute' => $attribute, 113 | ]; 114 | $object->$attribute = AutoNumber::generate($value, $this->alnum, $this->digit, $group); 115 | self::$_executed[$id] = true; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Behavior.php: -------------------------------------------------------------------------------- 1 | 'mdm\autonumber\Behavior', 19 | * 'value' => 'INV-{Ymd}-????', // ? will replace with generated number 20 | * ] 21 | * ] 22 | * } 23 | * ~~~ 24 | * 25 | * @author Misbahul D Munir 26 | * @since 1.0 27 | */ 28 | class Behavior extends AttributeBehavior 29 | { 30 | /** 31 | * @var integer digit number of auto number 32 | */ 33 | public $digit; 34 | 35 | /** 36 | * @var mixed Optional. 37 | */ 38 | public $group; 39 | 40 | /** 41 | * @var boolean If set `true` number will genarate unique for owner classname. 42 | * Default `true`. 43 | */ 44 | public $unique = true; 45 | 46 | /** 47 | * @var string 48 | */ 49 | public $attribute; 50 | 51 | /** 52 | * 53 | * @var bool If set `true` formated number will return alfabet and numeric. 54 | */ 55 | public $alnum = false; 56 | 57 | /** 58 | * @inheritdoc 59 | */ 60 | public function init() 61 | { 62 | if ($this->attribute !== null) { 63 | $this->attributes[BaseActiveRecord::EVENT_BEFORE_INSERT][] = $this->attribute; 64 | } 65 | parent::init(); 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | protected function getValue($event) 72 | { 73 | if (is_string($this->value) && method_exists($this->owner, $this->value)) { 74 | $value = call_user_func([$this->owner, $this->value], $event); 75 | } else { 76 | $value = is_callable($this->value) ? call_user_func($this->value, $event) : $this->value; 77 | } 78 | $group = [ 79 | 'class' => $this->unique ? get_class($this->owner) : false, 80 | 'group' => $this->group, 81 | 'attribute' => $this->attribute, 82 | ]; 83 | return AutoNumber::generate($value, $this->alnum, $this->digit, $group); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Bootstrap.php: -------------------------------------------------------------------------------- 1 | 12 | * @since 1.0 13 | */ 14 | class Bootstrap implements BootstrapInterface 15 | { 16 | /** 17 | * @inheritdoc 18 | */ 19 | public function bootstrap($app) 20 | { 21 | Validator::$builtInValidators['nextValue'] = __NAMESPACE__ . '\AutonumberValidator'; 22 | Validator::$builtInValidators['autonumber'] = __NAMESPACE__ . '\AutonumberValidator'; 23 | } 24 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Auto Number Extension for Yii 2 2 | =============================== 3 | 4 | Yii2 extension to genarete formated autonumber. It can be used for generate 5 | document number. 6 | 7 | Installation 8 | ------------ 9 | 10 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 11 | 12 | Either run 13 | 14 | ``` 15 | php composer.phar require --prefer-dist mdmsoft/yii2-autonumber "~1.0" 16 | ``` 17 | 18 | or add 19 | 20 | ``` 21 | "mdmsoft/yii2-autonumber": "~1.0" 22 | ``` 23 | 24 | to the require section of your `composer.json` file. 25 | 26 | 27 | Usage 28 | ----- 29 | 30 | Prepare required table by execute yii migrate. 31 | 32 | ``` 33 | yii migrate --migrationPath=@mdm/autonumber/migrations 34 | ``` 35 | 36 | if wantn't use db migration. you can create required table manually. 37 | 38 | ```sql 39 | CREATE TABLE auto_number ( 40 | "group" varchar(32) NOT NULL, 41 | "number" int, 42 | optimistic_lock int, 43 | update_time int, 44 | PRIMARY KEY ("group") 45 | ); 46 | ``` 47 | 48 | Once the extension is installed, simply modify your ActiveRecord class: 49 | 50 | ```php 51 | public function behaviors() 52 | { 53 | return [ 54 | [ 55 | 'class' => 'mdm\autonumber\Behavior', 56 | 'attribute' => 'sales_num', // required 57 | 'group' => $this->id_branch, // optional 58 | 'value' => 'SA.'.date('Y-m-d').'.?' , // format auto number. '?' will be replaced with generated number 59 | 'digit' => 4 // optional, default to null. 60 | ], 61 | ]; 62 | } 63 | 64 | // it will set value $model->sales_num as 'SA.2014-06-25.0001' 65 | ``` 66 | 67 | Instead of behavior, you can use this extension as validator 68 | 69 | ```php 70 | public function rules() 71 | { 72 | return [ 73 | [['sales_num'], 'autonumber', 'format'=>'SA.'.date('Y-m-d').'.?'], 74 | ... 75 | ]; 76 | } 77 | ``` 78 | 79 | New Format 80 | ---------- 81 | 82 | Since version 1.5 we introduce new format of number. Now we use `{}` to evaluate as date and number of digit represented as number of `?`. 83 | 84 | ```php 85 | public function rules() 86 | { 87 | return [ 88 | [['sales_num'], 'autonumber', 'format' => 'SA/{Y/m}/?.???'], 89 | ... 90 | ]; 91 | } 92 | 93 | // it will set value $model->sales_num as 'SA/2019/10/0.001' 94 | 95 | 96 | public function behaviors() 97 | { 98 | return [ 99 | [ 100 | 'class' => 'class' => 'mdm\autonumber\Behavior', 101 | 'attribute' => 'sales_num', // required 102 | 'value' => 'SA/{Y/m}/?.???' 103 | ] 104 | ]; 105 | } 106 | 107 | // another usage 108 | 109 | public function actionCreate() 110 | { 111 | $model = new Sales() 112 | $model->load(Yii::$app->request->post()); 113 | $model->sales_num = mdm\autonumber\AutoNumber::generate('SA/{Y/m}/?.???'); 114 | ... 115 | } 116 | ``` 117 | 118 | 119 | - [Api Documentation](http://mdmsoft.github.io/yii2-autonumber/index.html) 120 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdmsoft/yii2-autonumber", 3 | "description": "Auto number extension for the Yii framework", 4 | "keywords": ["yii2", " extension", " autonumber", " behavior"], 5 | "license": "BSD-3-Clause", 6 | "type": "yii2-extension", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Misbahul D Munir", 11 | "email": "misbahuldmunir@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "yiisoft/yii2": "~2.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "mdm\\autonumber\\": "" 20 | } 21 | }, 22 | "extra": { 23 | "bootstrap": "mdm\\autonumber\\Bootstrap", 24 | "branch-alias": { 25 | "dev-master": "1.x-dev" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /migrations/m140527_084418_auto_number.php: -------------------------------------------------------------------------------- 1 | 8 | * @since 1.0 9 | */ 10 | class m140527_084418_auto_number extends \yii\db\Migration 11 | { 12 | 13 | /** 14 | * @inheritdoc 15 | */ 16 | public function safeUp() 17 | { 18 | $tableOptions = null; 19 | if ($this->db->driverName === 'mysql') { 20 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'; 21 | } 22 | 23 | $this->createTable('{{%auto_number}}', [ 24 | 'group' => Schema::TYPE_STRING . '(32) NOT NULL', 25 | 'number' => Schema::TYPE_INTEGER, 26 | 'optimistic_lock' => Schema::TYPE_INTEGER, 27 | 'update_time' => Schema::TYPE_INTEGER, 28 | 'PRIMARY KEY ([[group]])' 29 | ], $tableOptions); 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function safeDown() 36 | { 37 | $this->dropTable('{{%auto_number}}'); 38 | } 39 | } 40 | --------------------------------------------------------------------------------