├── .gitignore ├── README.md ├── RelationTrait.php ├── composer.json ├── composer.lock └── messages ├── config.php ├── en └── mtrelt.php └── id-ID └── mtrelt.php /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-relation-trait 2 | Yii 2 Models add functionality for load with relation (loadAll($POST)), & transactional save with relation (saveAll()) 3 | 4 | PLUS soft delete/restore feature! 5 | 6 | Best work with [mootensai/yii2-enhanced-gii](https://github.com/mootensai/yii2-enhanced-gii) 7 | 8 | [![Latest Stable Version](https://poser.pugx.org/mootensai/yii2-relation-trait/v/stable)](https://packagist.org/packages/mootensai/yii2-relation-trait) 9 | [![License](https://poser.pugx.org/mootensai/yii2-relation-trait/license)](https://packagist.org/packages/mootensai/yii2-relation-trait) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/mootensai/yii2-relation-trait.svg?style=flat-square)](https://packagist.org/packages/mootensai/yii2-relation-trait) 11 | [![Monthly Downloads](https://poser.pugx.org/mootensai/yii2-relation-trait/d/monthly)](https://packagist.org/packages/mootensai/yii2-relation-trait) 12 | [![Daily Downloads](https://poser.pugx.org/mootensai/yii2-relation-trait/d/daily)](https://packagist.org/packages/mootensai/yii2-relation-trait) 13 | [![Join the chat at https://gitter.im/mootensai/yii2-relation-trait](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mootensai/yii2-relation-trait?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 14 | 15 | ## Support 16 | 17 | [![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.3.0/dist/gratipay.svg)](https://gratipay.com/mootensai/) 18 | 19 | https://www.paypal.me/yohanesc 20 | 21 | Endorse me on LinkedIn 22 | 23 | https://www.linkedin.com/in/yohanes-candrajaya-b68394102/ 24 | 25 | ## Installation 26 | 27 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 28 | 29 | Either run 30 | 31 | ```bash 32 | $ composer require 'mootensai/yii2-relation-trait:dev-master' 33 | ``` 34 | 35 | or add 36 | 37 | ``` 38 | "mootensai/yii2-relation-trait": "*" 39 | ``` 40 | 41 | to the `require` section of your `composer.json` file. 42 | 43 | 44 | ## Usage At Model 45 | 46 | ```php 47 | class MyModel extends ActiveRecord{ 48 | use \mootensai\relation\RelationTrait; 49 | } 50 | ``` 51 | 52 | ## Array Input & Usage At Controller 53 | 54 | It takes a normal array of POST. This is the example 55 | ```php 56 | Array ( 57 | $_POST['ParentClass'] => Array 58 | ( 59 | [attr1] => value1 60 | [attr2] => value2 61 | // has many 62 | [relationName] => Array 63 | ( 64 | [0] => Array 65 | ( 66 | [relAttr] => relValue1 67 | ) 68 | [1] => Array 69 | ( 70 | [relAttr] => relValue1 71 | ) 72 | ) 73 | // has one 74 | [relationName] => Array 75 | ( 76 | [relAttr1] => relValue1 77 | [relAttr2] => relValue2 78 | ) 79 | ) 80 | ) 81 | 82 | OR 83 | 84 | Array ( 85 | $_POST['ParentClass'] => ['attr1' => 'value1','attr2' => 'value2'], 86 | // Has One 87 | $_POST['RelatedClass'] => ['relAttr1' => 'value1','relAttr2' => 'value2'], 88 | // Has Many 89 | $_POST['RelatedClass'] => Array 90 | ( 91 | [0] => Array 92 | ( 93 | [attr1] => value1 94 | [attr2] => value2 95 | ) 96 | [1] => Array 97 | ( 98 | [attr1] => value1 99 | [attr2] => value2 100 | ) 101 | ) 102 | ) 103 | ``` 104 | 105 | ```php 106 | // sample at controller 107 | if($model->loadAll(Yii:$app->request->post()) && $model->saveAll()){ 108 | return $this->redirect(['view', 'id' => $model->id, 'created' => $model->created]); 109 | } 110 | ``` 111 | 112 | # Features 113 | 114 | ## Array Output 115 | 116 | ```php 117 | // I use this to send model & related through JSON / Serialize 118 | print_r($model->getAttributesWithRelatedAsPost()); 119 | ``` 120 | 121 | ``` 122 | Array 123 | ( 124 | [MainClass] => Array 125 | ( 126 | [attr1] => value1 127 | [attr2] => value2 128 | ) 129 | 130 | [RelatedClass] => Array 131 | ( 132 | [0] => Array 133 | ( 134 | [attr1] => value1 135 | [attr2] => value2 136 | ) 137 | ) 138 | 139 | ) 140 | ``` 141 | 142 | ```php 143 | print_r($model->getAttributesWithRelated()); 144 | ``` 145 | 146 | ``` 147 | Array 148 | ( 149 | [attr1] => value1 150 | [attr2] => value2 151 | [relationName] => Array 152 | ( 153 | [0] => Array 154 | ( 155 | [attr1] => value1 156 | [attr2] => value2 157 | ) 158 | ) 159 | ) 160 | ``` 161 | 162 | ## Use Transaction 163 | 164 | So your data will be atomic 165 | (see : http://en.wikipedia.org/wiki/ACID) 166 | 167 | ## Use Normal Save 168 | 169 | So your behaviors still works 170 | 171 | ## Add Validation At Main Model 172 | 173 | ```php 174 | $form->errorSummary($model); 175 | ``` 176 | 177 | will give you 178 | 179 | ``` 180 | <> #<> : <> 181 | My Related Model #1 : Attribute is required 182 | ``` 183 | 184 | ## It Works On Auto Incremental PK Or Not (I Have Tried Use UUID) 185 | 186 | See here if you want to use my behavior : 187 | 188 | https://github.com/mootensai/yii2-uuid-behavior 189 | 190 | ## Soft Delete 191 | 192 | Add this line to your Model to enable soft delete 193 | 194 | ```php 195 | private $_rt_softdelete; 196 | 197 | function __construct(){ 198 | $this->_rt_softdelete = [ 199 | '' => 200 | // multiple row marker column example 201 | 'isdeleted' => 1, 202 | 'deleted_by' => \Yii::$app->user->id, 203 | 'deleted_at' => date('Y-m-d H:i:s') 204 | ]; 205 | } 206 | ``` 207 | 208 | Add this line to your Model to enable soft restore 209 | 210 | ```php 211 | private $_rt_softrestore; 212 | 213 | function __construct(){ 214 | $this->_rt_softrestore = [ 215 | '' => 216 | // multiple row marker column example 217 | 'isdeleted' => 0, 218 | 'deleted_by' => 0, 219 | 'deleted_at' => 'NULL' 220 | ]; 221 | } 222 | ``` 223 | 224 | ### Should work on Yii's supported DB 225 | 226 | It use all Yii's Active Query or Active Record to execute DB command 227 | 228 | 229 | ### I'm open for any improvement 230 | Please create issue if you got a problem or an idea for enhancement 231 | 232 | #### ~ SDG ~ 233 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /RelationTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @since 1.0 8 | */ 9 | 10 | namespace mootensai\relation; 11 | 12 | use Yii; 13 | use yii\db\ActiveQuery; 14 | use \yii\db\ActiveRecord; 15 | use \yii\db\Exception; 16 | use yii\db\IntegrityException; 17 | use \yii\helpers\Inflector; 18 | use \yii\helpers\StringHelper; 19 | use yii\helpers\ArrayHelper; 20 | 21 | /* 22 | * add this line to your Model to enable soft delete 23 | * 24 | * private $_rt_softdelete; 25 | * 26 | * function __construct(){ 27 | * $this->_rt_softdelete = [ 28 | * '' => 29 | * // multiple row marker column example 30 | * 'isdeleted' => 1, 31 | * 'deleted_by' => \Yii::$app->user->id, 32 | * 'deleted_at' => date('Y-m-d H:i:s') 33 | * ]; 34 | * } 35 | * add this line to your Model to enable soft restore 36 | * private $_rt_softrestore; 37 | * 38 | * function __construct(){ 39 | * $this->_rt_softrestore = [ 40 | * '' => 41 | * // multiple row marker column example 42 | * 'isdeleted' => 0, 43 | * 'deleted_by' => 0, 44 | * 'deleted_at' => 'NULL' 45 | * ]; 46 | * } 47 | */ 48 | 49 | trait RelationTrait 50 | { 51 | 52 | /** 53 | * Load all attribute including related attribute 54 | * @param $POST 55 | * @param array $skippedRelations 56 | * @return bool 57 | */ 58 | public function loadAll($POST, $skippedRelations = []) 59 | { 60 | if ($this->load($POST)) { 61 | $shortName = StringHelper::basename(get_class($this)); 62 | $relData = $this->getRelationData(); 63 | foreach ($POST as $model => $attr) { 64 | if (is_array($attr)) { 65 | if ($model == $shortName) { 66 | foreach ($attr as $relName => $relAttr) { 67 | if (is_array($relAttr)) { 68 | $isHasMany = !ArrayHelper::isAssociative($relAttr); 69 | if (in_array($relName, $skippedRelations) || !array_key_exists($relName, $relData)) { 70 | continue; 71 | } 72 | 73 | $this->loadToRelation($isHasMany, $relName, $relAttr); 74 | } 75 | } 76 | } else { 77 | $isHasMany = is_array($attr) && is_array(current($attr)); 78 | $relName = ($isHasMany) ? lcfirst(Inflector::pluralize($model)) : lcfirst($model); 79 | if (in_array($relName, $skippedRelations) || !array_key_exists($relName, $relData)) { 80 | continue; 81 | } 82 | 83 | $this->loadToRelation($isHasMany, $relName, $attr); 84 | } 85 | } 86 | } 87 | return true; 88 | } 89 | return false; 90 | } 91 | 92 | /** 93 | * Refactored from loadAll() function 94 | * @param $isHasMany 95 | * @param $relName 96 | * @param $v 97 | * @return bool 98 | */ 99 | private function loadToRelation($isHasMany, $relName, $v) 100 | { 101 | /* @var $AQ ActiveQuery */ 102 | /* @var $this ActiveRecord */ 103 | /* @var $relObj ActiveRecord */ 104 | $AQ = $this->getRelation($relName); 105 | /* @var $relModelClass ActiveRecord */ 106 | $relModelClass = $AQ->modelClass; 107 | $relPKAttr = $relModelClass::primaryKey(); 108 | $isManyMany = count($relPKAttr) > 1; 109 | 110 | if ($isManyMany) { 111 | $container = []; 112 | foreach ($v as $relPost) { 113 | if (array_filter($relPost)) { 114 | $condition = []; 115 | $condition[$relPKAttr[0]] = $this->primaryKey; 116 | foreach ($relPost as $relAttr => $relAttrVal) { 117 | if (in_array($relAttr, $relPKAttr)) { 118 | $condition[$relAttr] = $relAttrVal; 119 | } 120 | } 121 | $relObj = $relModelClass::findOne($condition); 122 | if (is_null($relObj)) { 123 | $relObj = new $relModelClass; 124 | } 125 | $relObj->load($relPost, ''); 126 | $container[] = $relObj; 127 | } 128 | } 129 | $this->populateRelation($relName, $container); 130 | } else if ($isHasMany) { 131 | $container = []; 132 | foreach ($v as $relPost) { 133 | if (array_filter($relPost)) { 134 | /* @var $relObj ActiveRecord */ 135 | $relObj = (empty($relPost[$relPKAttr[0]])) ? new $relModelClass() : $relModelClass::findOne($relPost[$relPKAttr[0]]); 136 | if (is_null($relObj)) { 137 | $relObj = new $relModelClass(); 138 | } 139 | $relObj->load($relPost, ''); 140 | $container[] = $relObj; 141 | } 142 | } 143 | $this->populateRelation($relName, $container); 144 | } else { 145 | $relObj = (empty($v[$relPKAttr[0]])) ? new $relModelClass : $relModelClass::findOne($v[$relPKAttr[0]]); 146 | $relObj->load($v, ''); 147 | $this->populateRelation($relName, $relObj); 148 | } 149 | return true; 150 | } 151 | 152 | /** 153 | * Save model including all related model already loaded 154 | * @param array $skippedRelations 155 | * @return bool 156 | * @throws Exception 157 | */ 158 | public function saveAll($skippedRelations = []) 159 | { 160 | /* @var $this ActiveRecord */ 161 | $db = $this->getDb(); 162 | $trans = $db->beginTransaction(); 163 | $isNewRecord = $this->isNewRecord; 164 | $isSoftDelete = isset($this->_rt_softdelete); 165 | try { 166 | if ($this->save()) { 167 | $error = false; 168 | if (!empty($this->relatedRecords)) { 169 | /* @var $records ActiveRecord | ActiveRecord[] */ 170 | foreach ($this->relatedRecords as $name => $records) { 171 | if (in_array($name, $skippedRelations)) 172 | continue; 173 | 174 | $AQ = $this->getRelation($name); 175 | $link = $AQ->link; 176 | if (!empty($records)) { 177 | $notDeletedPK = []; 178 | $notDeletedFK = []; 179 | $relPKAttr = ($AQ->multiple) ? $records[0]->primaryKey() : $records->primaryKey(); 180 | $isManyMany = (count($relPKAttr) > 1); 181 | if ($AQ->multiple) { 182 | /* @var $relModel ActiveRecord */ 183 | foreach ($records as $index => $relModel) { 184 | foreach ($link as $key => $value) { 185 | $relModel->$key = $this->$value; 186 | $notDeletedFK[$key] = $this->$value; 187 | } 188 | 189 | //GET PK OF REL MODEL 190 | if ($isManyMany) { 191 | $mainPK = array_keys($link)[0]; 192 | foreach ($relModel->primaryKey as $attr => $value) { 193 | if ($attr != $mainPK) { 194 | $notDeletedPK[$attr][] = $value; 195 | } 196 | } 197 | } else { 198 | $notDeletedPK[] = $relModel->primaryKey; 199 | } 200 | 201 | } 202 | 203 | if (!$isNewRecord) { 204 | //DELETE WITH 'NOT IN' PK MODEL & REL MODEL 205 | if ($isManyMany) { 206 | // Many Many 207 | $query = ['and', $notDeletedFK]; 208 | foreach ($notDeletedPK as $attr => $value) { 209 | $notIn = ['not in', $attr, $value]; 210 | array_push($query, $notIn); 211 | } 212 | try { 213 | if ($isSoftDelete) { 214 | $relModel->updateAll($this->_rt_softdelete, $query); 215 | } else { 216 | $relModel->deleteAll($query); 217 | } 218 | } catch (IntegrityException $exc) { 219 | $this->addError($name, "Data can't be deleted because it's still used by another data."); 220 | $error = true; 221 | } 222 | } else { 223 | // Has Many 224 | $query = ['and', $notDeletedFK, ['not in', $relPKAttr[0], $notDeletedPK]]; 225 | if (!empty($notDeletedPK)) { 226 | try { 227 | if ($isSoftDelete) { 228 | $relModel->updateAll($this->_rt_softdelete, $query); 229 | } else { 230 | $relModel->deleteAll($query); 231 | } 232 | } catch (IntegrityException $exc) { 233 | $this->addError($name, "Data can't be deleted because it's still used by another data."); 234 | $error = true; 235 | } 236 | } 237 | } 238 | } 239 | 240 | foreach ($records as $index => $relModel) { 241 | $relSave = $relModel->save(); 242 | 243 | if (!$relSave || !empty($relModel->errors)) { 244 | $relModelWords = Yii::t('app', Inflector::camel2words(StringHelper::basename($AQ->modelClass))); 245 | $index++; 246 | foreach ($relModel->errors as $validation) { 247 | foreach ($validation as $errorMsg) { 248 | $this->addError($name, "$relModelWords #$index : $errorMsg"); 249 | } 250 | } 251 | $error = true; 252 | } 253 | } 254 | } else { 255 | //Has One 256 | foreach ($link as $key => $value) { 257 | $records->$key = $this->$value; 258 | } 259 | $relSave = $records->save(); 260 | if (!$relSave || !empty($records->errors)) { 261 | $recordsWords = Yii::t('app', Inflector::camel2words(StringHelper::basename($AQ->modelClass))); 262 | foreach ($records->errors as $validation) { 263 | foreach ($validation as $errorMsg) { 264 | $this->addError($name, "$recordsWords : $errorMsg"); 265 | } 266 | } 267 | $error = true; 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | //No Children left 275 | $relAvail = array_keys($this->relatedRecords); 276 | $relData = $this->getRelationData(); 277 | $allRel = array_keys($relData); 278 | $noChildren = array_diff($allRel, $relAvail); 279 | 280 | foreach ($noChildren as $relName) { 281 | /* @var $relModel ActiveRecord */ 282 | if (empty($relData[$relName]['via']) && !in_array($relName, $skippedRelations)) { 283 | $relModel = new $relData[$relName]['modelClass']; 284 | $condition = []; 285 | $isManyMany = count($relModel->primaryKey()) > 1; 286 | if ($isManyMany) { 287 | foreach ($relData[$relName]['link'] as $k => $v) { 288 | $condition[$k] = $this->$v; 289 | } 290 | try { 291 | if ($isSoftDelete) { 292 | $relModel->updateAll($this->_rt_softdelete, ['and', $condition]); 293 | } else { 294 | $relModel->deleteAll(['and', $condition]); 295 | } 296 | } catch (IntegrityException $exc) { 297 | $this->addError($relData[$relName]['name'], Yii::t('mtrelt', "Data can't be deleted because it's still used by another data.")); 298 | $error = true; 299 | } 300 | } else { 301 | if ($relData[$relName]['ismultiple']) { 302 | foreach ($relData[$relName]['link'] as $k => $v) { 303 | $condition[$k] = $this->$v; 304 | } 305 | try { 306 | if ($isSoftDelete) { 307 | $relModel->updateAll($this->_rt_softdelete, ['and', $condition]); 308 | } else { 309 | $relModel->deleteAll(['and', $condition]); 310 | } 311 | } catch (IntegrityException $exc) { 312 | $this->addError($relData[$relName]['name'], Yii::t('mtrelt', "Data can't be deleted because it's still used by another data.")); 313 | $error = true; 314 | } 315 | } 316 | } 317 | } 318 | } 319 | 320 | 321 | if ($error) { 322 | $trans->rollback(); 323 | $this->isNewRecord = $isNewRecord; 324 | return false; 325 | } 326 | $trans->commit(); 327 | return true; 328 | } else { 329 | return false; 330 | } 331 | } catch (Exception $exc) { 332 | $trans->rollBack(); 333 | $this->isNewRecord = $isNewRecord; 334 | throw $exc; 335 | } 336 | } 337 | 338 | 339 | /** 340 | * Deleted model row with all related records 341 | * @param array $skippedRelations 342 | * @return bool 343 | * @throws Exception 344 | */ 345 | public function deleteWithRelated($skippedRelations = []) 346 | { 347 | /* @var $this ActiveRecord */ 348 | $db = $this->getDb(); 349 | $trans = $db->beginTransaction(); 350 | $isSoftDelete = isset($this->_rt_softdelete); 351 | try { 352 | $error = false; 353 | $relData = $this->getRelationData(); 354 | foreach ($relData as $data) { 355 | $array = []; 356 | if ($data['ismultiple'] && !in_array($data['name'], $skippedRelations)) { 357 | $link = $data['link']; 358 | if (count($this->{$data['name']})) { 359 | foreach ($link as $key => $value) { 360 | if (isset($this->$value)) { 361 | $array[$key] = $this->$value; 362 | } 363 | } 364 | if ($isSoftDelete) { 365 | $error = !$this->{$data['name']}[0]->updateAll($this->_rt_softdelete, ['and', $array]); 366 | } else { 367 | $error = !$this->{$data['name']}[0]->deleteAll(['and', $array]); 368 | } 369 | } 370 | } 371 | } 372 | if ($error) { 373 | $trans->rollback(); 374 | return false; 375 | } 376 | if ($isSoftDelete) { 377 | $this->attributes = array_merge($this->attributes, $this->_rt_softdelete); 378 | if ($this->save(false)) { 379 | $trans->commit(); 380 | return true; 381 | } else { 382 | $trans->rollBack(); 383 | } 384 | } else { 385 | if ($this->delete()) { 386 | $trans->commit(); 387 | return true; 388 | } else { 389 | $trans->rollBack(); 390 | } 391 | } 392 | } catch (Exception $exc) { 393 | $trans->rollBack(); 394 | throw $exc; 395 | } 396 | } 397 | 398 | /** 399 | * Restore soft deleted row including all related records 400 | * @param array $skippedRelations 401 | * @return bool 402 | * @throws Exception 403 | */ 404 | public function restoreWithRelated($skippedRelations = []) 405 | { 406 | if (!isset($this->_rt_softrestore)) { 407 | return false; 408 | } 409 | 410 | /* @var $this ActiveRecord */ 411 | $db = $this->getDb(); 412 | $trans = $db->beginTransaction(); 413 | try { 414 | $error = false; 415 | $relData = $this->getRelationData(); 416 | foreach ($relData as $data) { 417 | $array = []; 418 | if ($data['ismultiple'] && !in_array($data['name'], $skippedRelations)) { 419 | $link = $data['link']; 420 | if (count($this->{$data['name']})) { 421 | foreach ($link as $key => $value) { 422 | if (isset($this->$value)) { 423 | $array[$key] = $this->$value; 424 | } 425 | } 426 | $error = !$this->{$data['name']}[0]->updateAll($this->_rt_softrestore, ['and', $array]); 427 | } 428 | } 429 | } 430 | if ($error) { 431 | $trans->rollback(); 432 | return false; 433 | } 434 | $this->attributes = array_merge($this->attributes, $this->_rt_softrestore); 435 | if ($this->save(false)) { 436 | $trans->commit(); 437 | return true; 438 | } else { 439 | $trans->rollBack(); 440 | } 441 | } catch (Exception $exc) { 442 | $trans->rollBack(); 443 | throw $exc; 444 | } 445 | } 446 | 447 | public function getRelationData() 448 | { 449 | $stack = []; 450 | if (method_exists($this, 'relationNames')) { 451 | foreach ($this->relationNames() as $name) { 452 | /* @var $rel ActiveQuery */ 453 | $rel = $this->getRelation($name); 454 | $stack[$name]['name'] = $name; 455 | $stack[$name]['method'] = 'get' . ucfirst($name); 456 | $stack[$name]['ismultiple'] = $rel->multiple; 457 | $stack[$name]['modelClass'] = $rel->modelClass; 458 | $stack[$name]['link'] = $rel->link; 459 | $stack[$name]['via'] = $rel->via; 460 | } 461 | } else { 462 | $ARMethods = get_class_methods('\yii\db\ActiveRecord'); 463 | $modelMethods = get_class_methods('\yii\base\Model'); 464 | $reflection = new \ReflectionClass($this); 465 | /* @var $method \ReflectionMethod */ 466 | foreach ($reflection->getMethods() as $method) { 467 | if (in_array($method->name, $ARMethods) || in_array($method->name, $modelMethods)) { 468 | continue; 469 | } 470 | if ($method->name === 'getRelationData') { 471 | continue; 472 | } 473 | if ($method->name === 'getAttributesWithRelatedAsPost') { 474 | continue; 475 | } 476 | if ($method->name === 'getAttributesWithRelated') { 477 | continue; 478 | } 479 | if (strpos($method->name, 'get') !== 0) { 480 | continue; 481 | } 482 | if($method->getNumberOfParameters() > 0) { 483 | continue; 484 | } 485 | if((string)$method->getReturnType() !== ActiveQueryInterface::class){ 486 | continue; 487 | } 488 | try { 489 | $rel = call_user_func(array($this, $method->name)); 490 | if ($rel instanceof ActiveQuery) { 491 | $name = lcfirst(preg_replace('/^get/', '', $method->name)); 492 | $stack[$name]['name'] = lcfirst(preg_replace('/^get/', '', $method->name)); 493 | $stack[$name]['method'] = $method->name; 494 | $stack[$name]['ismultiple'] = $rel->multiple; 495 | $stack[$name]['modelClass'] = $rel->modelClass; 496 | $stack[$name]['link'] = $rel->link; 497 | $stack[$name]['via'] = $rel->via; 498 | } 499 | } catch (\Exception $exc) { 500 | //if method name can't be called, 501 | } 502 | } 503 | } 504 | return $stack; 505 | } 506 | 507 | /** 508 | * This function is deprecated! 509 | * Return array like this 510 | * Array 511 | * ( 512 | * [MainClass] => Array 513 | * ( 514 | * [attr1] => value1 515 | * [attr2] => value2 516 | * ) 517 | * 518 | * [RelatedClass] => Array 519 | * ( 520 | * [0] => Array 521 | * ( 522 | * [attr1] => value1 523 | * [attr2] => value2 524 | * ) 525 | * ) 526 | * ) 527 | * @return array 528 | */ 529 | public function getAttributesWithRelatedAsPost() 530 | { 531 | $return = []; 532 | $shortName = StringHelper::basename(get_class($this)); 533 | $return[$shortName] = $this->attributes; 534 | foreach ($this->relatedRecords as $name => $records) { 535 | $AQ = $this->getRelation($name); 536 | if ($AQ->multiple) { 537 | foreach ($records as $index => $record) { 538 | $return[$name][$index] = $record->attributes; 539 | } 540 | } else { 541 | $return[$name] = $records->attributes; 542 | } 543 | 544 | } 545 | return $return; 546 | } 547 | 548 | /** 549 | * return array like this 550 | * Array 551 | * ( 552 | * [attr1] => value1 553 | * [attr2] => value2 554 | * [relationName] => Array 555 | * ( 556 | * [0] => Array 557 | * ( 558 | * [attr1] => value1 559 | * [attr2] => value2 560 | * ) 561 | * ) 562 | * ) 563 | * @return array 564 | */ 565 | public function getAttributesWithRelated() 566 | { 567 | /* @var $this ActiveRecord */ 568 | $return = $this->attributes; 569 | foreach ($this->relatedRecords as $name => $records) { 570 | $AQ = $this->getRelation($name); 571 | if ($AQ->multiple) { 572 | foreach ($records as $index => $record) { 573 | $return[$name][$index] = $record->attributes; 574 | } 575 | } else { 576 | $return[$name] = $records->attributes; 577 | } 578 | } 579 | return $return; 580 | } 581 | 582 | /** 583 | * TranslationTrait manages methods for all translations used in Krajee extensions 584 | * 585 | * @author Kartik Visweswaran 586 | * @since 1.8.8 587 | * Yii i18n messages configuration for generating translations 588 | * source : https://github.com/kartik-v/yii2-krajee-base/blob/master/TranslationTrait.php 589 | * Edited by : Yohanes Candrajaya 590 | * 591 | * 592 | * @return void 593 | */ 594 | public function initI18N() 595 | { 596 | $reflector = new \ReflectionClass(get_class($this)); 597 | $dir = dirname($reflector->getFileName()); 598 | 599 | Yii::setAlias("@mtrelt", $dir); 600 | $config = [ 601 | 'class' => 'yii\i18n\PhpMessageSource', 602 | 'basePath' => "@mtrelt/messages", 603 | 'forceTranslation' => true 604 | ]; 605 | $globalConfig = ArrayHelper::getValue(Yii::$app->i18n->translations, "mtrelt*", []); 606 | if (!empty($globalConfig)) { 607 | $config = array_merge($config, is_array($globalConfig) ? $globalConfig : (array)$globalConfig); 608 | } 609 | Yii::$app->i18n->translations["mtrelt*"] = $config; 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mootensai/yii2-relation-trait", 3 | "type": "yii2-extension", 4 | "description": "Yii 2 Models load with relation, & transaction save with relation", 5 | "keywords": ["Yii2","relation","load","save","transaction","loadwithrelation", "savewithrelation", "related", "saveall", "loadall"], 6 | "homepage": "http://github.com/mootensai/yii2-relation-trait", 7 | "license": "BSD-3-Clause", 8 | "support": { 9 | "issues": "https://github.com/mootensai/yii2-relation-trait/issues", 10 | "source": "https://github.com/mootensai/yii2-relation-trait" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Yohanes Candrajaya", 15 | "email": "moo.tensai@gmail.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.4.0", 20 | "yiisoft/yii2": ">=2.0.38" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "mootensai\\relation\\": "" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "575850dd6075d5ccbccf7b6b61fdfdfb", 8 | "packages": [ 9 | { 10 | "name": "bower-asset/jquery", 11 | "version": "2.1.4", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/jquery/jquery.git", 15 | "reference": "7751e69b615c6eca6f783a81e292a55725af6b85" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/jquery/jquery/zipball/7751e69b615c6eca6f783a81e292a55725af6b85", 20 | "reference": "7751e69b615c6eca6f783a81e292a55725af6b85", 21 | "shasum": "" 22 | }, 23 | "require-dev": { 24 | "bower-asset/qunit": "1.14.0", 25 | "bower-asset/requirejs": "2.1.10", 26 | "bower-asset/sinon": "1.8.1", 27 | "bower-asset/sizzle": "2.1.1-patch2" 28 | }, 29 | "type": "bower-asset-library", 30 | "extra": { 31 | "bower-asset-main": "dist/jquery.js", 32 | "bower-asset-ignore": [ 33 | "**/.*", 34 | "build", 35 | "dist/cdn", 36 | "speed", 37 | "test", 38 | "*.md", 39 | "AUTHORS.txt", 40 | "Gruntfile.js", 41 | "package.json" 42 | ] 43 | }, 44 | "license": [ 45 | "MIT" 46 | ], 47 | "keywords": [ 48 | "javascript", 49 | "jquery", 50 | "library" 51 | ] 52 | }, 53 | { 54 | "name": "bower-asset/jquery.inputmask", 55 | "version": "3.1.63", 56 | "source": { 57 | "type": "git", 58 | "url": "https://github.com/RobinHerbots/jquery.inputmask.git", 59 | "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48" 60 | }, 61 | "dist": { 62 | "type": "zip", 63 | "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/c40c7287eadc31e341ebbf0c02352eb55b9cbc48", 64 | "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48", 65 | "shasum": "" 66 | }, 67 | "require": { 68 | "bower-asset/jquery": ">=1.7" 69 | }, 70 | "type": "bower-asset-library", 71 | "extra": { 72 | "bower-asset-main": [ 73 | "./dist/inputmask/jquery.inputmask.js", 74 | "./dist/inputmask/jquery.inputmask.extensions.js", 75 | "./dist/inputmask/jquery.inputmask.date.extensions.js", 76 | "./dist/inputmask/jquery.inputmask.numeric.extensions.js", 77 | "./dist/inputmask/jquery.inputmask.phone.extensions.js", 78 | "./dist/inputmask/jquery.inputmask.regex.extensions.js" 79 | ], 80 | "bower-asset-ignore": [ 81 | "**/.*", 82 | "qunit/", 83 | "nuget/", 84 | "tools/", 85 | "js/", 86 | "*.md", 87 | "build.properties", 88 | "build.xml", 89 | "jquery.inputmask.jquery.json" 90 | ] 91 | }, 92 | "license": [ 93 | "http://opensource.org/licenses/mit-license.php" 94 | ], 95 | "description": "jquery.inputmask is a jquery plugin which create an input mask.", 96 | "keywords": [ 97 | "form", 98 | "input", 99 | "inputmask", 100 | "jquery", 101 | "mask", 102 | "plugins" 103 | ] 104 | }, 105 | { 106 | "name": "bower-asset/punycode", 107 | "version": "v1.3.2", 108 | "source": { 109 | "type": "git", 110 | "url": "https://github.com/bestiejs/punycode.js.git", 111 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" 112 | }, 113 | "dist": { 114 | "type": "zip", 115 | "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", 116 | "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", 117 | "shasum": "" 118 | }, 119 | "type": "bower-asset-library", 120 | "extra": { 121 | "bower-asset-main": "punycode.js", 122 | "bower-asset-ignore": [ 123 | "coverage", 124 | "tests", 125 | ".*", 126 | "component.json", 127 | "Gruntfile.js", 128 | "node_modules", 129 | "package.json" 130 | ] 131 | } 132 | }, 133 | { 134 | "name": "bower-asset/yii2-pjax", 135 | "version": "v2.0.4", 136 | "source": { 137 | "type": "git", 138 | "url": "https://github.com/yiisoft/jquery-pjax.git", 139 | "reference": "3f20897307cca046fca5323b318475ae9dac0ca0" 140 | }, 141 | "dist": { 142 | "type": "zip", 143 | "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/3f20897307cca046fca5323b318475ae9dac0ca0", 144 | "reference": "3f20897307cca046fca5323b318475ae9dac0ca0", 145 | "shasum": "" 146 | }, 147 | "require": { 148 | "bower-asset/jquery": ">=1.8" 149 | }, 150 | "type": "bower-asset-library", 151 | "extra": { 152 | "bower-asset-main": "./jquery.pjax.js", 153 | "bower-asset-ignore": [ 154 | ".travis.yml", 155 | "Gemfile", 156 | "Gemfile.lock", 157 | "vendor/", 158 | "script/", 159 | "test/" 160 | ] 161 | }, 162 | "license": [ 163 | "MIT" 164 | ] 165 | }, 166 | { 167 | "name": "cebe/markdown", 168 | "version": "1.1.0", 169 | "source": { 170 | "type": "git", 171 | "url": "https://github.com/cebe/markdown.git", 172 | "reference": "54a2c49de31cc44e864ebf0500a35ef21d0010b2" 173 | }, 174 | "dist": { 175 | "type": "zip", 176 | "url": "https://api.github.com/repos/cebe/markdown/zipball/54a2c49de31cc44e864ebf0500a35ef21d0010b2", 177 | "reference": "54a2c49de31cc44e864ebf0500a35ef21d0010b2", 178 | "shasum": "" 179 | }, 180 | "require": { 181 | "lib-pcre": "*", 182 | "php": ">=5.4.0" 183 | }, 184 | "require-dev": { 185 | "cebe/indent": "*", 186 | "facebook/xhprof": "*@dev", 187 | "phpunit/phpunit": "4.1.*" 188 | }, 189 | "bin": [ 190 | "bin/markdown" 191 | ], 192 | "type": "library", 193 | "extra": { 194 | "branch-alias": { 195 | "dev-master": "1.1.x-dev" 196 | } 197 | }, 198 | "autoload": { 199 | "psr-4": { 200 | "cebe\\markdown\\": "" 201 | } 202 | }, 203 | "notification-url": "https://packagist.org/downloads/", 204 | "license": [ 205 | "MIT" 206 | ], 207 | "authors": [ 208 | { 209 | "name": "Carsten Brandt", 210 | "email": "mail@cebe.cc", 211 | "homepage": "http://cebe.cc/", 212 | "role": "Creator" 213 | } 214 | ], 215 | "description": "A super fast, highly extensible markdown parser for PHP", 216 | "homepage": "https://github.com/cebe/markdown#readme", 217 | "keywords": [ 218 | "extensible", 219 | "fast", 220 | "gfm", 221 | "markdown", 222 | "markdown-extra" 223 | ], 224 | "time": "2015-03-06 05:28:07" 225 | }, 226 | { 227 | "name": "ezyang/htmlpurifier", 228 | "version": "v4.6.0", 229 | "source": { 230 | "type": "git", 231 | "url": "https://github.com/ezyang/htmlpurifier.git", 232 | "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd" 233 | }, 234 | "dist": { 235 | "type": "zip", 236 | "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6f389f0f25b90d0b495308efcfa073981177f0fd", 237 | "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd", 238 | "shasum": "" 239 | }, 240 | "require": { 241 | "php": ">=5.2" 242 | }, 243 | "type": "library", 244 | "autoload": { 245 | "psr-0": { 246 | "HTMLPurifier": "library/" 247 | }, 248 | "files": [ 249 | "library/HTMLPurifier.composer.php" 250 | ] 251 | }, 252 | "notification-url": "https://packagist.org/downloads/", 253 | "license": [ 254 | "LGPL" 255 | ], 256 | "authors": [ 257 | { 258 | "name": "Edward Z. Yang", 259 | "email": "admin@htmlpurifier.org", 260 | "homepage": "http://ezyang.com" 261 | } 262 | ], 263 | "description": "Standards compliant HTML filter written in PHP", 264 | "homepage": "http://htmlpurifier.org/", 265 | "keywords": [ 266 | "html" 267 | ], 268 | "time": "2013-11-30 08:25:19" 269 | }, 270 | { 271 | "name": "yiisoft/yii2", 272 | "version": "2.0.5", 273 | "source": { 274 | "type": "git", 275 | "url": "https://github.com/yiisoft/yii2-framework.git", 276 | "reference": "ea8c13b9f5cd437bd7bf73cad8a3457a155f3727" 277 | }, 278 | "dist": { 279 | "type": "zip", 280 | "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/ea8c13b9f5cd437bd7bf73cad8a3457a155f3727", 281 | "reference": "ea8c13b9f5cd437bd7bf73cad8a3457a155f3727", 282 | "shasum": "" 283 | }, 284 | "require": { 285 | "bower-asset/jquery": "2.1.*@stable | 1.11.*@stable", 286 | "bower-asset/jquery.inputmask": "3.1.*", 287 | "bower-asset/punycode": "1.3.*", 288 | "bower-asset/yii2-pjax": ">=2.0.1", 289 | "cebe/markdown": "~1.0.0 | ~1.1.0", 290 | "ext-mbstring": "*", 291 | "ezyang/htmlpurifier": "4.6.*", 292 | "lib-pcre": "*", 293 | "php": ">=5.4.0", 294 | "yiisoft/yii2-composer": "*" 295 | }, 296 | "bin": [ 297 | "yii" 298 | ], 299 | "type": "library", 300 | "extra": { 301 | "branch-alias": { 302 | "dev-master": "2.0.x-dev" 303 | } 304 | }, 305 | "autoload": { 306 | "psr-4": { 307 | "yii\\": "" 308 | } 309 | }, 310 | "notification-url": "https://packagist.org/downloads/", 311 | "license": [ 312 | "BSD-3-Clause" 313 | ], 314 | "authors": [ 315 | { 316 | "name": "Qiang Xue", 317 | "email": "qiang.xue@gmail.com", 318 | "homepage": "http://www.yiiframework.com/", 319 | "role": "Founder and project lead" 320 | }, 321 | { 322 | "name": "Alexander Makarov", 323 | "email": "sam@rmcreative.ru", 324 | "homepage": "http://rmcreative.ru/", 325 | "role": "Core framework development" 326 | }, 327 | { 328 | "name": "Maurizio Domba", 329 | "homepage": "http://mdomba.info/", 330 | "role": "Core framework development" 331 | }, 332 | { 333 | "name": "Carsten Brandt", 334 | "email": "mail@cebe.cc", 335 | "homepage": "http://cebe.cc/", 336 | "role": "Core framework development" 337 | }, 338 | { 339 | "name": "Timur Ruziev", 340 | "email": "resurtm@gmail.com", 341 | "homepage": "http://resurtm.com/", 342 | "role": "Core framework development" 343 | }, 344 | { 345 | "name": "Paul Klimov", 346 | "email": "klimov.paul@gmail.com", 347 | "role": "Core framework development" 348 | } 349 | ], 350 | "description": "Yii PHP Framework Version 2", 351 | "homepage": "http://www.yiiframework.com/", 352 | "keywords": [ 353 | "framework", 354 | "yii2" 355 | ], 356 | "time": "2015-07-11 02:37:59" 357 | }, 358 | { 359 | "name": "yiisoft/yii2-composer", 360 | "version": "2.0.3", 361 | "source": { 362 | "type": "git", 363 | "url": "https://github.com/yiisoft/yii2-composer.git", 364 | "reference": "ca8d23707ae47d20b0454e4b135c156f6da6d7be" 365 | }, 366 | "dist": { 367 | "type": "zip", 368 | "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/ca8d23707ae47d20b0454e4b135c156f6da6d7be", 369 | "reference": "ca8d23707ae47d20b0454e4b135c156f6da6d7be", 370 | "shasum": "" 371 | }, 372 | "require": { 373 | "composer-plugin-api": "1.0.0" 374 | }, 375 | "type": "composer-plugin", 376 | "extra": { 377 | "class": "yii\\composer\\Plugin", 378 | "branch-alias": { 379 | "dev-master": "2.0.x-dev" 380 | } 381 | }, 382 | "autoload": { 383 | "psr-4": { 384 | "yii\\composer\\": "" 385 | } 386 | }, 387 | "notification-url": "https://packagist.org/downloads/", 388 | "license": [ 389 | "BSD-3-Clause" 390 | ], 391 | "authors": [ 392 | { 393 | "name": "Qiang Xue", 394 | "email": "qiang.xue@gmail.com" 395 | } 396 | ], 397 | "description": "The composer plugin for Yii extension installer", 398 | "keywords": [ 399 | "composer", 400 | "extension installer", 401 | "yii2" 402 | ], 403 | "time": "2015-03-01 06:22:44" 404 | } 405 | ], 406 | "packages-dev": [], 407 | "aliases": [], 408 | "minimum-stability": "stable", 409 | "stability-flags": [], 410 | "prefer-stable": false, 411 | "prefer-lowest": false, 412 | "platform": { 413 | "php": ">=5.4.0" 414 | }, 415 | "platform-dev": [] 416 | } 417 | -------------------------------------------------------------------------------- /messages/config.php: -------------------------------------------------------------------------------- 1 | __DIR__ . DIRECTORY_SEPARATOR . '..', 6 | // string, required, root directory containing message translations. 7 | 'messagePath' => __DIR__, 8 | // array, required, list of language codes that the extracted messages 9 | // should be translated to. For example, ['zh-CN', 'de']. 10 | 'languages' => ['en', 'id-ID'], 11 | // string, the name of the function for translating messages. 12 | // Defaults to 'Yii::t'. This is used as a mark to find the messages to be 13 | // translated. You may use a string for single function name or an array for 14 | // multiple function names. 15 | 'translator' => 'Yii::t', 16 | // boolean, whether to sort messages by keys when merging new messages 17 | // with the existing ones. Defaults to false, which means the new (untranslated) 18 | // messages will be separated from the old (translated) ones. 19 | 'sort' => false, 20 | // boolean, whether the message file should be overwritten with the merged messages 21 | 'overwrite' => true, 22 | // boolean, whether to remove messages that no longer appear in the source code. 23 | // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. 24 | 'removeUnused' => false, 25 | // array, list of patterns that specify which files/directories should NOT be processed. 26 | // If empty or not set, all files/directories will be processed. 27 | // A path matches a pattern if it contains the pattern string at its end. For example, 28 | // '/a/b' will match all files and directories ending with '/a/b'; 29 | // the '*.svn' will match all files and directories whose name ends with '.svn'. 30 | // and the '.svn' will match all files and directories named exactly '.svn'. 31 | // Note, the '/' characters in a pattern matches both '/' and '\'. 32 | // See helpers/FileHelper::findFiles() description for more details on pattern matching rules. 33 | 'only' => ['*.php'], 34 | // array, list of patterns that specify which files (not directories) should be processed. 35 | // If empty or not set, all files will be processed. 36 | // Please refer to "except" for details about the patterns. 37 | // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. 38 | 'except' => [ 39 | '.svn', 40 | '.git', 41 | '.gitignore', 42 | '.gitkeep', 43 | '.hgignore', 44 | '.hgkeep', 45 | '/messages', 46 | ], 47 | // Generated file format. Can be either "php", "po" or "db". 48 | 'format' => 'php', 49 | // When format is "db", you may specify the following two options 50 | //'db' => 'db', 51 | //'sourceMessageTable' => '{{%source_message}}', 52 | ]; 53 | -------------------------------------------------------------------------------- /messages/en/mtrelt.php: -------------------------------------------------------------------------------- 1 | 'Data can\'t be deleted because it\'s still used by another data.', 21 | ]; 22 | -------------------------------------------------------------------------------- /messages/id-ID/mtrelt.php: -------------------------------------------------------------------------------- 1 | 'Data tidak bisa dihapus karena masih digunakan oleh data lain.', 21 | ]; 22 | --------------------------------------------------------------------------------