diff --git a/.gitignore b/.gitignore index c3adeeb..2a69cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/nbproject/* \ No newline at end of file +/nbproject/* +.idea +vendor \ No newline at end of file diff --git a/README.md b/README.md index 6110179..8c78e19 100644 --- a/README.md +++ b/README.md @@ -1,236 +1,151 @@ # yii2-relation-trait -Yii 2 Models add functionality for load with relation (loadAll($POST)), & transactional save with relation (saveAll()) -PLUS soft delete/restore feature! +> **Note**: This is **not** the official extension by [@mootensai](https://github.com/mootensai). +> I am not the creator of the original extension. I have made bug fixes and improvements that suit my use case. +> Feel free to use it or refer to the official package +> at [mootensai/yii2-relation-trait](https://github.com/mootensai/yii2-relation-trait). -Best work with [mootensai/yii2-enhanced-gii](https://github.com/mootensai/yii2-enhanced-gii) +Yii 2 Models add functionality for loading related models via `loadAll($POST)` and transactional saving via +`saveAll()`. +It also supports **soft delete** and **soft restore** features. -[![Latest Stable Version](https://poser.pugx.org/mootensai/yii2-relation-trait/v/stable)](https://packagist.org/packages/mootensai/yii2-relation-trait) -[![License](https://poser.pugx.org/mootensai/yii2-relation-trait/license)](https://packagist.org/packages/mootensai/yii2-relation-trait) -[![Total Downloads](https://img.shields.io/packagist/dt/mootensai/yii2-relation-trait.svg?style=flat-square)](https://packagist.org/packages/mootensai/yii2-relation-trait) -[![Monthly Downloads](https://poser.pugx.org/mootensai/yii2-relation-trait/d/monthly)](https://packagist.org/packages/mootensai/yii2-relation-trait) -[![Daily Downloads](https://poser.pugx.org/mootensai/yii2-relation-trait/d/daily)](https://packagist.org/packages/mootensai/yii2-relation-trait) -[![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) +Works best with [mootensai/yii2-enhanced-gii](https://github.com/mootensai/yii2-enhanced-gii). -## Support +## Badges -[![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.3.0/dist/gratipay.svg)](https://gratipay.com/mootensai/) - -https://www.paypal.me/yohanesc - -Endorse me on LinkedIn - -https://www.linkedin.com/in/yohanes-candrajaya-b68394102/ +[![Latest Stable Version](https://poser.pugx.org/deadmantfa/yii2-relation-trait/v/stable)](https://packagist.org/packages/deadmantfa/yii2-relation-trait) +[![License](https://poser.pugx.org/deadmantfa/yii2-relation-trait/license)](https://packagist.org/packages/deadmantfa/yii2-relation-trait) +[![Total Downloads](https://img.shields.io/packagist/dt/deadmantfa/yii2-relation-trait.svg?style=flat-square)](https://packagist.org/packages/deadmantfa/yii2-relation-trait) +[![Monthly Downloads](https://poser.pugx.org/deadmantfa/yii2-relation-trait/d/monthly)](https://packagist.org/packages/deadmantfa/yii2-relation-trait) +[![Daily Downloads](https://poser.pugx.org/deadmantfa/yii2-relation-trait/d/daily)](https://packagist.org/packages/deadmantfa/yii2-relation-trait) ## Installation -The preferred way to install this extension is through [composer](http://getcomposer.org/download/). +The preferred way to install this extension is through [Composer](http://getcomposer.org/download/). Either run ```bash -$ composer require 'mootensai/yii2-relation-trait:dev-master' +composer require deadmantfa/yii2-relation-trait ``` or add +```php +"deadmantfa/yii2-relation-trait": "^2.0.0" ``` -"mootensai/yii2-relation-trait": "*" -``` - -to the `require` section of your `composer.json` file. +to the require section of your application's ```composer.json``` file. -## Usage At Model +## Usage in the Model ```php -class MyModel extends ActiveRecord{ - use \mootensai\relation\RelationTrait; -} -``` - -## Array Input & Usage At Controller +use deadmantfa\relation\RelationTrait; -It takes a normal array of POST. This is the example -```php -Array ( - $_POST['ParentClass'] => Array - ( - [attr1] => value1 - [attr2] => value2 - // has many - [relationName] => Array - ( - [0] => Array - ( - [relAttr] => relValue1 - ) - [1] => Array - ( - [relAttr] => relValue1 - ) - ) - // has one - [relationName] => Array - ( - [relAttr1] => relValue1 - [relAttr2] => relValue2 - ) - ) -) - -OR - -Array ( - $_POST['ParentClass'] => ['attr1' => 'value1','attr2' => 'value2'], - // Has One - $_POST['RelatedClass'] => ['relAttr1' => 'value1','relAttr2' => 'value2'], - // Has Many - $_POST['RelatedClass'] => Array - ( - [0] => Array - ( - [attr1] => value1 - [attr2] => value2 - ) - [1] => Array - ( - [attr1] => value1 - [attr2] => value2 - ) - ) -) -``` +class MyModel extends \yii\db\ActiveRecord +{ + use RelationTrait; -```php -// sample at controller -if($model->loadAll(Yii:$app->request->post()) && $model->saveAll()){ - return $this->redirect(['view', 'id' => $model->id, 'created' => $model->created]); + // ... } ``` -# Features +## Controller Usage -## Array Output +The extension expects a **normal array of POST** data. For example: ```php -// I use this to send model & related through JSON / Serialize -print_r($model->getAttributesWithRelatedAsPost()); -``` - -``` -Array -( - [MainClass] => Array - ( - [attr1] => value1 - [attr2] => value2 - ) - - [RelatedClass] => Array - ( - [0] => Array - ( - [attr1] => value1 - [attr2] => value2 - ) - ) - -) -``` +[ + $_POST['ParentClass'] => [ + 'attr1' => 'value1', + 'attr2' => 'value2', + // Has many + 'relationName' => [ + [ 'relAttr' => 'relValue1' ], + [ 'relAttr' => 'relValue2' ] + ], + // Has one + 'relationName' => [ + 'relAttr1' => 'relValue1', + 'relAttr2' => 'relValue2' + ] + ] +]; +``` + +In your controller: ```php -print_r($model->getAttributesWithRelated()); -``` - -``` -Array -( - [attr1] => value1 - [attr2] => value2 - [relationName] => Array - ( - [0] => Array - ( - [attr1] => value1 - [attr2] => value2 - ) - ) -) -``` - -## Use Transaction - -So your data will be atomic -(see : http://en.wikipedia.org/wiki/ACID) - -## Use Normal Save - -So your behaviors still works - -## Add Validation At Main Model - -```php -$form->errorSummary($model); -``` - -will give you - -``` -<> #<> : <> -My Related Model #1 : Attribute is required +$model = new ParentClass(); +if ($model->loadAll(Yii::$app->request->post()) && $model->saveAll()) { + return $this->redirect(['view', 'id' => $model->id]); +} ``` -## It Works On Auto Incremental PK Or Not (I Have Tried Use UUID) - -See here if you want to use my behavior : - -https://github.com/mootensai/yii2-uuid-behavior - -## Soft Delete - -Add this line to your Model to enable soft delete +Features + +1. Transaction Support + a. Your data changes are atomic (ACID compliant). +2. Normal ```save()``` + a. Behaviors still work as usual since it’s built on top of Yii’s ```ActiveRecord```. +3. Validation + a. Errors from related models appear via ```errorSummary()```, e.g. + ```text + MyRelatedClass #2: [Error message] + ``` +4. UUID or Auto-Increment + Works with any PK strategy, + including [mootensai/yii2-uuid-behavior](https://github.com/mootensai/yii2-uuid-behavior). +5. Soft Delete + By defining ```$_rt_softdelete``` in your model constructor (and ```$_rt_softrestore``` for restoring), you can + softly mark rows as deleted instead of physically removing them. + ```php + private $_rt_softdelete; + private $_rt_softrestore; + + public function __construct($config = []) + { + parent::__construct($config); + + $this->_rt_softdelete = [ + 'is_deleted' => 1, + 'deleted_by' => Yii::$app->user->id, + 'deleted_at' => date('Y-m-d H:i:s'), + ]; + + $this->_rt_softrestore = [ + 'is_deleted' => 0, + 'deleted_by' => null, + 'deleted_at' => null, + ]; + } + ``` + +## Array Outputs ```php -private $_rt_softdelete; - -function __construct(){ - $this->_rt_softdelete = [ - '' => - // multiple row marker column example - 'isdeleted' => 1, - 'deleted_by' => \Yii::$app->user->id, - 'deleted_at' => date('Y-m-d H:i:s') - ]; -} +print_r($model->getAttributesWithRelatedAsPost()); ``` -Add this line to your Model to enable soft restore +Produces a POST-like structure with the main model and related arrays. ```php -private $_rt_softrestore; - -function __construct(){ - $this->_rt_softrestore = [ - '' => - // multiple row marker column example - 'isdeleted' => 0, - 'deleted_by' => 0, - 'deleted_at' => 'NULL' - ]; -} +print_r($model->getAttributesWithRelated()); ``` -### Should work on Yii's supported DB - -It use all Yii's Active Query or Active Record to execute DB command - - -### I'm open for any improvement -Please create issue if you got a problem or an idea for enhancement +Produces a nested structure under ```[relationName] => [...]```. -#### ~ SDG ~ +## Contributing or Reporting Issues +Please open an [issue](https://github.com/deadmantfa/yii2-relation-trait/pulls) or submit a PR if you find a bug or have +an improvement idea. +--- +### Disclaimer +This package is a **fork** or an alternative +to [mootensai/yii2-relation-trait](https://github.com/mootensai/yii2-relation-trait). +**All credit** to [@mootensai](https://github.com/mootensai) for the initial code. +**This is not meant to replace** the original package but rather provide bug fixes and enhancements under a different +namespace. \ No newline at end of file diff --git a/RelationTrait.php b/RelationTrait.php index e3c9aa6..cf003fa 100644 --- a/RelationTrait.php +++ b/RelationTrait.php @@ -1,333 +1,420 @@ - * @since 1.0 - */ - -namespace mootensai\relation; +namespace deadmantfa\relation; +use ReflectionClass; +use Throwable; use Yii; +use yii\base\Model; use yii\db\ActiveQuery; -use \yii\db\ActiveRecord; -use \yii\db\Exception; +use yii\db\ActiveQueryInterface; +use yii\db\ActiveRecord; +use yii\db\Exception; use yii\db\IntegrityException; -use \yii\helpers\Inflector; -use \yii\helpers\StringHelper; use yii\helpers\ArrayHelper; +use yii\helpers\Inflector; +use yii\helpers\StringHelper; +use yii\i18n\PhpMessageSource; -/* - * add this line to your Model to enable soft delete - * - * private $_rt_softdelete; - * - * function __construct(){ - * $this->_rt_softdelete = [ - * '' => - * // multiple row marker column example - * 'isdeleted' => 1, - * 'deleted_by' => \Yii::$app->user->id, - * 'deleted_at' => date('Y-m-d H:i:s') - * ]; - * } - * add this line to your Model to enable soft restore - * private $_rt_softrestore; +/** + * Trait RelationTrait * - * function __construct(){ - * $this->_rt_softrestore = [ - * '' => - * // multiple row marker column example - * 'isdeleted' => 0, - * 'deleted_by' => 0, - * 'deleted_at' => 'NULL' - * ]; - * } + * Provides methods for mass-loading related models via POST data and saving/deleting + * them in a single transaction. Includes optional soft-delete/restore functionality. */ - trait RelationTrait { - /** - * Load all attribute including related attribute - * @param $POST - * @param array $skippedRelations - * @return bool + * Load model attributes including related attributes from POST data. + * + * @param array $POST The POST data (likely Yii::$app->request->post()). + * @param array $skippedRelations Relations to exclude from load. */ - public function loadAll($POST, $skippedRelations = []) + public function loadAll(array $POST, array $skippedRelations = []): bool { - if ($this->load($POST)) { - $shortName = StringHelper::basename(get_class($this)); - $relData = $this->getRelationData(); - foreach ($POST as $model => $attr) { - if (is_array($attr)) { - if ($model == $shortName) { - foreach ($attr as $relName => $relAttr) { - if (is_array($relAttr)) { - $isHasMany = !ArrayHelper::isAssociative($relAttr); - if (in_array($relName, $skippedRelations) || !array_key_exists($relName, $relData)) { - continue; - } + if (!$this->load($POST)) { + return false; + } - $this->loadToRelation($isHasMany, $relName, $relAttr); - } - } - } else { - $isHasMany = is_array($attr) && is_array(current($attr)); - $relName = ($isHasMany) ? lcfirst(Inflector::pluralize($model)) : lcfirst($model); - if (in_array($relName, $skippedRelations) || !array_key_exists($relName, $relData)) { - continue; - } + $shortName = StringHelper::basename(static::class); + $relData = $this->getRelationData(); + + // We assume the top-level array in $POST is named after $shortName or the related classes + foreach ($POST as $model => $attr) { + if (!is_array($attr)) { + continue; + } - $this->loadToRelation($isHasMany, $relName, $attr); + // If the POST key matches our model short name + if ($model === $shortName) { + // e.g. $POST['MyModel'] => ['someRelation' => [...], 'anotherRelation' => [...]] + foreach ($attr as $relName => $relAttr) { + if (!is_array($relAttr)) { + continue; + } + $isHasMany = !ArrayHelper::isAssociative($relAttr); + if (in_array($relName, $skippedRelations, true) || !array_key_exists($relName, $relData)) { + continue; } + $this->loadToRelation($isHasMany, $relName, $relAttr); } + } else { + // If $model is the name of a related class, guess that the relation name + // is pluralized or singularized version of $model. + $isHasMany = is_array(current($attr)); + $relName = $isHasMany + ? lcfirst(Inflector::pluralize($model)) + : lcfirst($model); + + if (in_array($relName, $skippedRelations, true) || !array_key_exists($relName, $relData)) { + continue; + } + $this->loadToRelation($isHasMany, $relName, $attr); } - return true; } - return false; + + return true; + } + + /** + * Retrieves information about all relations defined in this model by scanning: + * - 'relationNames()' method if defined in the model (must return an array of relation names as strings), or + * - reflection for 'getXYZ()' methods returning an ActiveQueryInterface. + */ + public function getRelationData(): array + { + $stack = []; + + // If the model has a custom "relationNames()" method, use that to retrieve relation names + if (method_exists($this, 'relationNames')) { + // Expecting something like: ['relationA', 'relationB', ...] + $names = $this->relationNames(); + foreach ($names as $name) { + /** @var ActiveQuery $rel */ + $rel = $this->getRelation($name); + $stack[$name] = [ + 'name' => $name, + 'method' => 'get' . ucfirst($name), + 'ismultiple' => $rel->multiple, + 'modelClass' => $rel->modelClass, + 'link' => $rel->link, + 'via' => $rel->via, + ]; + } + return $stack; + } + + // Otherwise, reflect on all getSomething() methods + $ARMethods = get_class_methods(ActiveRecord::class); + $modelMethods = get_class_methods(Model::class); + $reflection = new ReflectionClass($this); + + foreach ($reflection->getMethods() as $method) { + $methodName = $method->getName(); + + // Skip parent or irrelevant methods + if (in_array($methodName, $ARMethods, true) || + in_array($methodName, $modelMethods, true) || + in_array($methodName, [ + 'getRelationData', + 'getAttributesWithRelatedAsPost', + 'getAttributesWithRelated', + 'getRelatedRecordsTree', + ], true) + ) { + continue; + } + + if (strpos($methodName, 'get') !== 0) { + continue; + } + if ($method->getNumberOfParameters() > 0) { + continue; + } + + try { + $rel = call_user_func([$this, $methodName]); + if (!$rel instanceof ActiveQueryInterface) { + continue; + } + $propName = lcfirst(preg_replace('/^get/', '', $methodName)); + $stack[$propName] = [ + 'name' => $propName, + 'method' => $methodName, + 'ismultiple' => $rel->multiple, + 'modelClass' => $rel->modelClass, + 'link' => $rel->link, + 'via' => $rel->via, + ]; + } catch (Throwable $exc) { + // ignore + } + } + + return $stack; } /** - * Refactored from loadAll() function - * @param $isHasMany - * @param $relName - * @param $v - * @return bool + * Load array data into a single relation (HasOne, HasMany, or ManyMany). + * + * @param bool $isHasMany Whether this relation is plural (HasMany / ManyMany). + * @param string $relName The relation name in the AR model. + * @param array $v The data array for that relation. */ - private function loadToRelation($isHasMany, $relName, $v) + private function loadToRelation(bool $isHasMany, string $relName, array $v): bool { - /* @var $AQ ActiveQuery */ - /* @var $this ActiveRecord */ - /* @var $relObj ActiveRecord */ + /** @var ActiveRecord $this */ $AQ = $this->getRelation($relName); - /* @var $relModelClass ActiveRecord */ $relModelClass = $AQ->modelClass; $relPKAttr = $relModelClass::primaryKey(); - $isManyMany = count($relPKAttr) > 1; + // If there's more than one column in the PK array, we consider it ManyMany. + // (In some advanced pivot scenarios, you might refine this logic further.) + $isManyMany = (count($relPKAttr) > 1); + // Many-to-many if ($isManyMany) { $container = []; foreach ($v as $relPost) { - if (array_filter($relPost)) { - $condition = []; - $condition[$relPKAttr[0]] = $this->primaryKey; - foreach ($relPost as $relAttr => $relAttrVal) { - if (in_array($relAttr, $relPKAttr)) { - $condition[$relAttr] = $relAttrVal; - } - } - $relObj = $relModelClass::findOne($condition); - if (is_null($relObj)) { - $relObj = new $relModelClass; + if (!array_filter($relPost)) { + continue; + } + // Build condition for the pivot table + // Make sure $relPKAttr[0] exists + if (!isset($relPKAttr[0])) { + // No well-defined first PK attribute => skip or handle otherwise + continue; + } + $condition = [$relPKAttr[0] => $this->primaryKey]; + + foreach ($relPost as $relAttr => $relAttrVal) { + if (in_array($relAttr, $relPKAttr, true)) { + $condition[$relAttr] = $relAttrVal; } - $relObj->load($relPost, ''); - $container[] = $relObj; } + + $relObj = $relModelClass::findOne($condition); + if ($relObj === null) { + $relObj = new $relModelClass(); + } + $relObj->load($relPost, ''); + $container[] = $relObj; } $this->populateRelation($relName, $container); - } else if ($isHasMany) { + return true; + } + + // HasMany + if ($isHasMany) { $container = []; foreach ($v as $relPost) { - if (array_filter($relPost)) { - /* @var $relObj ActiveRecord */ - $relObj = (empty($relPost[$relPKAttr[0]])) ? new $relModelClass() : $relModelClass::findOne($relPost[$relPKAttr[0]]); - if (is_null($relObj)) { - $relObj = new $relModelClass(); - } - $relObj->load($relPost, ''); - $container[] = $relObj; + if (!array_filter($relPost)) { + continue; + } + $primaryKeyVal = $relPost[$relPKAttr[0]] ?? null; + $relObj = null; + if ($primaryKeyVal) { + $relObj = $relModelClass::findOne($primaryKeyVal); } + if ($relObj === null) { + $relObj = new $relModelClass(); + } + $relObj->load($relPost, ''); + $container[] = $relObj; } $this->populateRelation($relName, $container); - } else { - $relObj = (empty($v[$relPKAttr[0]])) ? new $relModelClass : $relModelClass::findOne($v[$relPKAttr[0]]); - $relObj->load($v, ''); - $this->populateRelation($relName, $relObj); + return true; + } + + // HasOne + $primaryKeyVal = $v[$relPKAttr[0]] ?? null; + $relObj = null; + if ($primaryKeyVal) { + $relObj = $relModelClass::findOne($primaryKeyVal); } + if ($relObj === null) { + $relObj = new $relModelClass(); + } + $relObj->load($v, ''); + $this->populateRelation($relName, $relObj); + return true; } /** - * Save model including all related model already loaded - * @param array $skippedRelations - * @return bool + * Save model and all related records in a transaction. + * Optionally uses soft-delete if configured. + * + * @param array $skippedRelations Relations to exclude from save. * @throws Exception */ - public function saveAll($skippedRelations = []) + public function saveAll(array $skippedRelations = []): bool { - /* @var $this ActiveRecord */ + /** @var ActiveRecord $this */ $db = $this->getDb(); $trans = $db->beginTransaction(); $isNewRecord = $this->isNewRecord; $isSoftDelete = isset($this->_rt_softdelete); + $error = false; + try { - if ($this->save()) { - $error = false; - if (!empty($this->relatedRecords)) { - /* @var $records ActiveRecord | ActiveRecord[] */ - foreach ($this->relatedRecords as $name => $records) { - if (in_array($name, $skippedRelations)) - continue; + if (!$this->save()) { + $trans->rollBack(); + return false; + } - $AQ = $this->getRelation($name); - $link = $AQ->link; - if (!empty($records)) { - $notDeletedPK = []; - $notDeletedFK = []; - $relPKAttr = ($AQ->multiple) ? $records[0]->primaryKey() : $records->primaryKey(); - $isManyMany = (count($relPKAttr) > 1); - if ($AQ->multiple) { - /* @var $relModel ActiveRecord */ - foreach ($records as $index => $relModel) { - foreach ($link as $key => $value) { - $relModel->$key = $this->$value; - $notDeletedFK[$key] = $this->$value; - } + // Save any loaded related records + if (!empty($this->relatedRecords)) { + foreach ($this->relatedRecords as $name => $records) { + if (in_array($name, $skippedRelations, true)) { + continue; + } - //GET PK OF REL MODEL - if ($isManyMany) { - $mainPK = array_keys($link)[0]; - foreach ($relModel->primaryKey as $attr => $value) { - if ($attr != $mainPK) { - $notDeletedPK[$attr][] = $value; - } - } - } else { - $notDeletedPK[] = $relModel->primaryKey; - } + /** @var ActiveQuery $AQ */ + $AQ = $this->getRelation($name); + $link = $AQ->link; + if (empty($records)) { + continue; + } - } + if ($AQ->multiple) { + // ManyMany or HasMany + $firstRelModel = is_array($records) ? reset($records) : null; + if (!$firstRelModel instanceof ActiveRecord) { + continue; + } + $relPKAttr = $firstRelModel->primaryKey(); + $isManyMany = (count($relPKAttr) > 1); + // Collect PK & FK for leftover deletion + $notDeletedPK = []; + $notDeletedFK = []; + foreach ($records as $index => $relModel) { + if (!$relModel instanceof ActiveRecord) { + continue; + } + // Ensure this child's FK references the parent + foreach ($link as $key => $value) { + $relModel->{$key} = $this->{$value}; + $notDeletedFK[$key] = $this->{$value}; + } - if (!$isNewRecord) { - //DELETE WITH 'NOT IN' PK MODEL & REL MODEL - if ($isManyMany) { - // Many Many - $query = ['and', $notDeletedFK]; - foreach ($notDeletedPK as $attr => $value) { - $notIn = ['not in', $attr, $value]; - array_push($query, $notIn); - } - try { - if ($isSoftDelete) { - $relModel->updateAll($this->_rt_softdelete, $query); - } else { - $relModel->deleteAll($query); - } - } catch (IntegrityException $exc) { - $this->addError($name, "Data can't be deleted because it's still used by another data."); - $error = true; - } - } else { - // Has Many - $query = ['and', $notDeletedFK, ['not in', $relPKAttr[0], $notDeletedPK]]; - if (!empty($notDeletedPK)) { - try { - if ($isSoftDelete) { - $relModel->updateAll($this->_rt_softdelete, $query); - } else { - $relModel->deleteAll($query); - } - } catch (IntegrityException $exc) { - $this->addError($name, "Data can't be deleted because it's still used by another data."); - $error = true; - } - } + // Mark PK for not-deleting + if ($isManyMany) { + $mainPK = array_key_first($link); + // In a truly multi-column pivot, you might need more robust logic. + foreach ($relModel->primaryKey as $attr => $val) { + if ($attr !== $mainPK) { + $notDeletedPK[$attr][] = $val; } } + } else { + $notDeletedPK[] = $relModel->primaryKey; + } + } + // For existing parent, remove leftover children + if (!$isNewRecord) { + $relationLabel = Yii::t('app', Inflector::camel2words(StringHelper::basename($AQ->modelClass))); + $relModel = $firstRelModel; - foreach ($records as $index => $relModel) { - $relSave = $relModel->save(); - - if (!$relSave || !empty($relModel->errors)) { - $relModelWords = Yii::t('app', Inflector::camel2words(StringHelper::basename($AQ->modelClass))); - $index++; - foreach ($relModel->errors as $validation) { - foreach ($validation as $errorMsg) { - $this->addError($name, "$relModelWords #$index : $errorMsg"); - } - } - $error = true; - } + if ($isManyMany) { + // ManyMany leftover cleanup + $query = ['and', $notDeletedFK]; + foreach ($notDeletedPK as $attr => $values) { + $query[] = ['not in', $attr, $values]; } + $this->safeDeleteOrSoftDelete( + $relModel, + $isSoftDelete ? $this->_rt_softdelete : [], + $query, + $relationLabel, + $name, + $error + ); } else { - //Has One - foreach ($link as $key => $value) { - $records->$key = $this->$value; - } - $relSave = $records->save(); - if (!$relSave || !empty($records->errors)) { - $recordsWords = Yii::t('app', Inflector::camel2words(StringHelper::basename($AQ->modelClass))); - foreach ($records->errors as $validation) { - foreach ($validation as $errorMsg) { - $this->addError($name, "$recordsWords : $errorMsg"); - } - } - $error = true; + // HasMany leftover cleanup + $primaryColumn = $relPKAttr[0] ?? null; + if ($primaryColumn !== null && $notDeletedPK !== []) { + $query = ['and', $notDeletedFK, ['not in', $primaryColumn, $notDeletedPK]]; + $this->safeDeleteOrSoftDelete( + $relModel, + $isSoftDelete ? $this->_rt_softdelete : [], + $query, + $relationLabel, + $name, + $error + ); } } } - } - } - - //No Children left - $relAvail = array_keys($this->relatedRecords); - $relData = $this->getRelationData(); - $allRel = array_keys($relData); - $noChildren = array_diff($allRel, $relAvail); - - foreach ($noChildren as $relName) { - /* @var $relModel ActiveRecord */ - if (empty($relData[$relName]['via']) && !in_array($relName, $skippedRelations)) { - $relModel = new $relData[$relName]['modelClass']; - $condition = []; - $isManyMany = count($relModel->primaryKey()) > 1; - if ($isManyMany) { - foreach ($relData[$relName]['link'] as $k => $v) { - $condition[$k] = $this->$v; - } - try { - if ($isSoftDelete) { - $relModel->updateAll($this->_rt_softdelete, ['and', $condition]); - } else { - $relModel->deleteAll(['and', $condition]); + // Now save each related record + foreach ($records as $index => $relModel) { + if (!$relModel->save() || !empty($relModel->errors)) { + $relModelWords = Yii::t('app', Inflector::camel2words(StringHelper::basename($AQ->modelClass))); + $idx = $index + 1; + foreach ($relModel->errors as $validation) { + foreach ($validation as $errorMsg) { + $this->addError($name, "$relModelWords #$idx : $errorMsg"); + } } - } catch (IntegrityException $exc) { - $this->addError($relData[$relName]['name'], Yii::t('mtrelt', "Data can't be deleted because it's still used by another data.")); $error = true; } - } else { - if ($relData[$relName]['ismultiple']) { - foreach ($relData[$relName]['link'] as $k => $v) { - $condition[$k] = $this->$v; - } - try { - if ($isSoftDelete) { - $relModel->updateAll($this->_rt_softdelete, ['and', $condition]); - } else { - $relModel->deleteAll(['and', $condition]); - } - } catch (IntegrityException $exc) { - $this->addError($relData[$relName]['name'], Yii::t('mtrelt', "Data can't be deleted because it's still used by another data.")); - $error = true; + } + } elseif (!is_array($records) && $records instanceof ActiveRecord) { + // HasOne + $relModel = $records; + foreach ($link as $key => $value) { + $relModel->{$key} = $this->{$value}; + } + if (!$relModel->save() || !empty($relModel->errors)) { + $recordsWords = Yii::t('app', Inflector::camel2words(StringHelper::basename($AQ->modelClass))); + foreach ($relModel->errors as $validation) { + foreach ($validation as $errorMsg) { + $this->addError($name, "$recordsWords : $errorMsg"); } } + $error = true; } } } + } + // Remove children for relations not in $this->relatedRecords + $relAvail = array_keys($this->relatedRecords); + $relData = $this->getRelationData(); + $allRel = array_keys($relData); + $noChildren = array_diff($allRel, $relAvail); - if ($error) { - $trans->rollback(); - $this->isNewRecord = $isNewRecord; - return false; + foreach ($noChildren as $relName) { + if (!empty($relData[$relName]['via']) || + in_array($relName, $skippedRelations, true) + ) { + continue; } - $trans->commit(); - return true; - } else { + + $relModelClass = $relData[$relName]['modelClass']; + /** @var ActiveRecord $relModel */ + $relModel = new $relModelClass(); + $condition = $this->buildConditionFromLink($relData[$relName]['link']); + + $relationLabel = Inflector::camel2words(StringHelper::basename($relData[$relName]['modelClass'])); + $this->safeDeleteOrSoftDelete( + $relModel, + $isSoftDelete ? $this->_rt_softdelete : [], + ['and', $condition], + $relationLabel, + $relData[$relName]['name'], + $error + ); + } + + if ($error) { + $trans->rollBack(); + $this->isNewRecord = $isNewRecord; return false; } + + $trans->commit(); + return true; } catch (Exception $exc) { $trans->rollBack(); $this->isNewRecord = $isNewRecord; @@ -335,60 +422,143 @@ public function saveAll($skippedRelations = []) } } + /** + * Unified method to either "soft-delete" (updateAll) or physically delete (deleteAll), + * catching IntegrityException for foreign key constraints. + * + * @param ActiveRecord $modelClassOrInstance Model class or an instance used for static calls. + * @param array $softDeleteData The soft-delete attributes (if any). If empty, do hard delete. + * @param array $condition The query condition (e.g. ['and', [...]]). + * @param string $relationLabel Label for error messages. + * @param string $relationName The relation property name (for $this->addError). + * @param bool $errorRef This is passed by reference—set to true if an IntegrityException occurs. + */ + private function safeDeleteOrSoftDelete( + ActiveRecord $modelClassOrInstance, + array $softDeleteData, + array $condition, + string $relationLabel, + string $relationName, + bool &$errorRef + ): void + { + try { + if ($softDeleteData !== []) { + $modelClassOrInstance->updateAll($softDeleteData, $condition); + } else { + $modelClassOrInstance->deleteAll($condition); + } + } catch (IntegrityException $exc) { + // Optionally append the DB error message for debugging + $dbMsg = $exc->getMessage(); + $errorMsg = Yii::t('app', "Data can't be deleted because it's still used by another data. DB says: {dbErr}", [ + 'dbErr' => $dbMsg, + ]); + $this->addError($relationName, "$relationLabel: $errorMsg"); + $errorRef = true; + } + } + + /** + * A small helper for building a 'where' condition array from a relation link array. + * + * For example, if $link = ['foreign_key_id' => 'id'], we build `['foreign_key_id' => $this->id]`. + */ + private function buildConditionFromLink(array $link): array + { + $condition = []; + foreach ($link as $key => $value) { + if (isset($this->{$value})) { + $condition[$key] = $this->{$value}; + } + } + return $condition; + } /** - * Deleted model row with all related records - * @param array $skippedRelations - * @return bool - * @throws Exception + * Delete this model and all related records. + * If soft-delete is configured, it will only mark them as deleted. + * + * @param array $skippedRelations Relations to exclude from deletion. + * @throws Exception|Throwable */ - public function deleteWithRelated($skippedRelations = []) + public function deleteWithRelated(array $skippedRelations = []): bool { - /* @var $this ActiveRecord */ + /** @var ActiveRecord $this */ $db = $this->getDb(); $trans = $db->beginTransaction(); $isSoftDelete = isset($this->_rt_softdelete); + $error = false; + try { - $error = false; $relData = $this->getRelationData(); foreach ($relData as $data) { - $array = []; - if ($data['ismultiple'] && !in_array($data['name'], $skippedRelations)) { - $link = $data['link']; - if (count($this->{$data['name']})) { - foreach ($link as $key => $value) { - if (isset($this->$value)) { - $array[$key] = $this->$value; - } - } - if ($isSoftDelete) { - $error = !$this->{$data['name']}[0]->updateAll($this->_rt_softdelete, ['and', $array]); - } else { - $error = !$this->{$data['name']}[0]->deleteAll(['and', $array]); - } + if (!$data['ismultiple'] || in_array($data['name'], $skippedRelations, true)) { + continue; + } + $relationName = $data['name']; + + // If the relation is empty, no need to do anything + if (empty($this->{$relationName}) || !is_array($this->{$relationName})) { + continue; + } + + // Build condition + $condition = $this->buildConditionFromLink($data['link']); + if (empty($condition)) { + continue; + } + + /** @var ActiveRecord|bool $firstChild */ + $relationRecords = $this->{$relationName}; + $firstChild = reset($relationRecords); + + // If $firstChild is `false`, it might mean an empty array or non-ActiveRecord entries + if (!$firstChild instanceof ActiveRecord) { + continue; + } + + $relModelClass = get_class($firstChild); + + try { + if ($isSoftDelete) { + $relModelClass::updateAll($this->_rt_softdelete, ['and', $condition]); + } else { + $relModelClass::deleteAll(['and', $condition]); } + } catch (IntegrityException $exc) { + // Optionally include the actual DB exception message: + $message = Yii::t('app', "Cannot delete related data due to foreign key constraints. DB says: {error}", [ + 'error' => $exc->getMessage(), + ]); + $this->addError($relationName, $message); + $error = true; + break; } } + if ($error) { - $trans->rollback(); + $trans->rollBack(); return false; } + + // Finally handle the parent if ($isSoftDelete) { $this->attributes = array_merge($this->attributes, $this->_rt_softdelete); if ($this->save(false)) { $trans->commit(); return true; - } else { - $trans->rollBack(); - } - } else { - if ($this->delete()) { - $trans->commit(); - return true; - } else { - $trans->rollBack(); } + $trans->rollBack(); + return false; } + + if ($this->delete()) { + $trans->commit(); + return true; + } + $trans->rollBack(); + return false; } catch (Exception $exc) { $trans->rollBack(); throw $exc; @@ -396,139 +566,100 @@ public function deleteWithRelated($skippedRelations = []) } /** - * Restore soft deleted row including all related records - * @param array $skippedRelations - * @return bool + * Restore a soft-deleted row, including all related records. + * Requires $_rt_softrestore to be defined in the model. + * + * @param array $skippedRelations Relations to exclude from restore. * @throws Exception */ - public function restoreWithRelated($skippedRelations = []) + public function restoreWithRelated(array $skippedRelations = []): bool { if (!isset($this->_rt_softrestore)) { return false; } - /* @var $this ActiveRecord */ + /** @var ActiveRecord $this */ $db = $this->getDb(); $trans = $db->beginTransaction(); + $error = false; + try { - $error = false; $relData = $this->getRelationData(); foreach ($relData as $data) { - $array = []; - if ($data['ismultiple'] && !in_array($data['name'], $skippedRelations)) { - $link = $data['link']; - if (count($this->{$data['name']})) { - foreach ($link as $key => $value) { - if (isset($this->$value)) { - $array[$key] = $this->$value; - } - } - $error = !$this->{$data['name']}[0]->updateAll($this->_rt_softrestore, ['and', $array]); - } + if (!$data['ismultiple'] || in_array($data['name'], $skippedRelations, true)) { + continue; + } + + $relationName = $data['name']; + if (empty($this->{$relationName}) || !is_array($this->{$relationName})) { + continue; + } + + $condition = $this->buildConditionFromLink($data['link']); + if (empty($condition)) { + continue; + } + + /** @var ActiveRecord|bool $firstChild */ + $firstChild = reset($this->{$relationName}); + if (!$firstChild instanceof ActiveRecord) { + // if it's not a valid AR instance, skip + continue; + } + + $relModelClass = get_class($firstChild); + + try { + $relModelClass::updateAll($this->_rt_softrestore, ['and', $condition]); + } catch (IntegrityException $exc) { + $message = Yii::t('app', "Cannot restore related data due to foreign key constraints. DB says: {error}", [ + 'error' => $exc->getMessage(), + ]); + $this->addError($relationName, $message); + $error = true; + break; } } + if ($error) { - $trans->rollback(); + $trans->rollBack(); return false; } + + // Restore the parent $this->attributes = array_merge($this->attributes, $this->_rt_softrestore); if ($this->save(false)) { $trans->commit(); return true; - } else { - $trans->rollBack(); } + + $trans->rollBack(); + return false; } catch (Exception $exc) { $trans->rollBack(); throw $exc; } } - public function getRelationData() + /** + * Deprecated: Return array structured for form POST data (the "multi-dimensional" approach). + */ + public function getAttributesWithRelatedAsPost(): array { - $stack = []; - if (method_exists($this, 'relationNames')) { - foreach ($this->relationNames() as $name) { - /* @var $rel ActiveQuery */ - $rel = $this->getRelation($name); - $stack[$name]['name'] = $name; - $stack[$name]['method'] = 'get' . ucfirst($name); - $stack[$name]['ismultiple'] = $rel->multiple; - $stack[$name]['modelClass'] = $rel->modelClass; - $stack[$name]['link'] = $rel->link; - $stack[$name]['via'] = $rel->via; - } - } else { - $ARMethods = get_class_methods('\yii\db\ActiveRecord'); - $modelMethods = get_class_methods('\yii\base\Model'); - $reflection = new \ReflectionClass($this); - /* @var $method \ReflectionMethod */ - foreach ($reflection->getMethods() as $method) { - if (in_array($method->name, $ARMethods) || in_array($method->name, $modelMethods)) { - continue; - } - if ($method->name === 'getRelationData') { - continue; - } - if ($method->name === 'getAttributesWithRelatedAsPost') { - continue; - } - if ($method->name === 'getAttributesWithRelated') { - continue; - } - if (strpos($method->name, 'get') !== 0) { - continue; - } - if($method->getNumberOfParameters() > 0) { - continue; - } - try { - $rel = call_user_func(array($this, $method->name)); - if ($rel instanceof ActiveQuery) { - $name = lcfirst(preg_replace('/^get/', '', $method->name)); - $stack[$name]['name'] = lcfirst(preg_replace('/^get/', '', $method->name)); - $stack[$name]['method'] = $method->name; - $stack[$name]['ismultiple'] = $rel->multiple; - $stack[$name]['modelClass'] = $rel->modelClass; - $stack[$name]['link'] = $rel->link; - $stack[$name]['via'] = $rel->via; - } - } catch (\Exception $exc) { - //if method name can't be called, - } - } - } - return $stack; + $shortName = StringHelper::basename(static::class); + $return = [ + $shortName => $this->attributes, + ]; + return $this->getRelatedRecordsTree($return); } /** - * This function is deprecated! - * Return array like this - * Array - * ( - * [MainClass] => Array - * ( - * [attr1] => value1 - * [attr2] => value2 - * ) - * - * [RelatedClass] => Array - * ( - * [0] => Array - * ( - * [attr1] => value1 - * [attr2] => value2 - * ) - * ) - * ) - * @return array + * Builds a nested array representation of the current model's related records. */ - public function getAttributesWithRelatedAsPost() + public function getRelatedRecordsTree(array $return): array { - $return = []; - $shortName = StringHelper::basename(get_class($this)); - $return[$shortName] = $this->attributes; foreach ($this->relatedRecords as $name => $records) { + /** @var ActiveQuery $AQ */ $AQ = $this->getRelation($name); if ($AQ->multiple) { foreach ($records as $index => $record) { @@ -537,72 +668,42 @@ public function getAttributesWithRelatedAsPost() } else { $return[$name] = $records->attributes; } - } return $return; } /** - * return array like this - * Array - * ( - * [attr1] => value1 - * [attr2] => value2 - * [relationName] => Array - * ( - * [0] => Array - * ( - * [attr1] => value1 - * [attr2] => value2 - * ) - * ) - * ) - * @return array + * Return array of attributes including related records in a single tree. */ - public function getAttributesWithRelated() + public function getAttributesWithRelated(): array { - /* @var $this ActiveRecord */ + /** @var ActiveRecord $this */ $return = $this->attributes; - foreach ($this->relatedRecords as $name => $records) { - $AQ = $this->getRelation($name); - if ($AQ->multiple) { - foreach ($records as $index => $record) { - $return[$name][$index] = $record->attributes; - } - } else { - $return[$name] = $records->attributes; - } - } - return $return; + return $this->getRelatedRecordsTree($return); } /** - * TranslationTrait manages methods for all translations used in Krajee extensions - * - * @author Kartik Visweswaran - * @since 1.8.8 - * Yii i18n messages configuration for generating translations - * source : https://github.com/kartik-v/yii2-krajee-base/blob/master/TranslationTrait.php - * Edited by : Yohanes Candrajaya + * Initialize i18n configuration for message translation. * - * - * @return void + * @throws \Exception */ - public function initI18N() + public function initI18N(): void { - $reflector = new \ReflectionClass(get_class($this)); + $reflector = new ReflectionClass(static::class); $dir = dirname($reflector->getFileName()); - Yii::setAlias("@mtrelt", $dir); + Yii::setAlias('@mtrelt', $dir); + $config = [ - 'class' => 'yii\i18n\PhpMessageSource', - 'basePath' => "@mtrelt/messages", - 'forceTranslation' => true + 'class' => PhpMessageSource::class, + 'basePath' => '@mtrelt/messages', + 'forceTranslation' => true, ]; - $globalConfig = ArrayHelper::getValue(Yii::$app->i18n->translations, "mtrelt*", []); + $globalConfig = ArrayHelper::getValue(Yii::$app->i18n->translations, 'mtrelt*', []); + if (!empty($globalConfig)) { $config = array_merge($config, is_array($globalConfig) ? $globalConfig : (array)$globalConfig); } - Yii::$app->i18n->translations["mtrelt*"] = $config; + Yii::$app->i18n->translations['mtrelt*'] = $config; } } diff --git a/composer.json b/composer.json index 3dcf7b1..24b7bc8 100644 --- a/composer.json +++ b/composer.json @@ -1,27 +1,56 @@ { - "name": "mootensai/yii2-relation-trait", - "type": "yii2-extension", - "description": "Yii 2 Models load with relation, & transaction save with relation", - "keywords": ["Yii2","relation","load","save","transaction","loadwithrelation", "savewithrelation", "related", "saveall", "loadall"], - "homepage": "http://github.com/mootensai/yii2-relation-trait", - "license": "BSD-3-Clause", - "support": { - "issues": "https://github.com/mootensai/yii2-relation-trait/issues", - "source": "https://github.com/mootensai/yii2-relation-trait" + "name": "deadmantfa/yii2-relation-trait", + "type": "yii2-extension", + "description": "Yii 2 Models load with relation & transaction save with relation (plus optional soft-delete/restore).", + "keywords": [ + "Yii2", + "relation", + "load", + "save", + "transaction", + "loadwithrelation", + "savewithrelation", + "related", + "saveall", + "loadall" + ], + "homepage": "https://github.com/deadmantfa/yii2-relation-trait", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/deadmantfa/yii2-relation-trait/issues", + "source": "https://github.com/deadmantfa/yii2-relation-trait" + }, + "authors": [ + { + "name": "Yohanes Candrajaya", + "email": "moo.tensai@gmail.com" }, - "authors": [ - { - "name": "Yohanes Candrajaya", - "email": "moo.tensai@gmail.com" - } - ], - "require": { - "php": ">=5.4.0", - "yiisoft/yii2": "~2.0" - }, - "autoload": { - "psr-4": { - "mootensai\\relation\\": "" - } + { + "name": "Wenceslaus Dsilva", + "email": "wenceslausdsilva@gmail.com" + } + ], + "require": { + "php": ">=7.4.0", + "yiisoft/yii2": "^2.0.51" + }, + "autoload": { + "psr-4": { + "deadmantfa\\relation\\": "" + } + }, + "repositories": [ + { + "type": "composer", + "url": "https://asset-packagist.org" + } + ], + "config": { + "allow-plugins": { + "yiisoft/yii2-composer": true } + }, + "require-dev": { + "rector/rector": "^2.0" + } } diff --git a/composer.lock b/composer.lock index 914003d..edbe4bf 100644 --- a/composer.lock +++ b/composer.lock @@ -1,180 +1,98 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "575850dd6075d5ccbccf7b6b61fdfdfb", + "content-hash": "a30601a3188f08f07fdf2a1b95757267", "packages": [ { - "name": "bower-asset/jquery", - "version": "2.1.4", + "name": "bower-asset/inputmask", + "version": "5.0.8", "source": { "type": "git", - "url": "https://github.com/jquery/jquery.git", - "reference": "7751e69b615c6eca6f783a81e292a55725af6b85" + "url": "git@github.com:RobinHerbots/Inputmask.git", + "reference": "e0f39e0c93569c6b494c3a57edef2c59313a6b64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jquery/jquery/zipball/7751e69b615c6eca6f783a81e292a55725af6b85", - "reference": "7751e69b615c6eca6f783a81e292a55725af6b85", - "shasum": "" + "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/e0f39e0c93569c6b494c3a57edef2c59313a6b64", + "reference": "e0f39e0c93569c6b494c3a57edef2c59313a6b64" }, - "require-dev": { - "bower-asset/qunit": "1.14.0", - "bower-asset/requirejs": "2.1.10", - "bower-asset/sinon": "1.8.1", - "bower-asset/sizzle": "2.1.1-patch2" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "dist/jquery.js", - "bower-asset-ignore": [ - "**/.*", - "build", - "dist/cdn", - "speed", - "test", - "*.md", - "AUTHORS.txt", - "Gruntfile.js", - "package.json" - ] + "require": { + "bower-asset/jquery": ">=1.7" }, + "type": "bower-asset", "license": [ - "MIT" - ], - "keywords": [ - "javascript", - "jquery", - "library" + "http://opensource.org/licenses/mit-license.php" ] }, { - "name": "bower-asset/jquery.inputmask", - "version": "3.1.63", + "name": "bower-asset/jquery", + "version": "3.6.4", "source": { "type": "git", - "url": "https://github.com/RobinHerbots/jquery.inputmask.git", - "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48" + "url": "git@github.com:jquery/jquery-dist.git", + "reference": "91ef2d8836342875f2519b5815197ea0f23613cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RobinHerbots/jquery.inputmask/zipball/c40c7287eadc31e341ebbf0c02352eb55b9cbc48", - "reference": "c40c7287eadc31e341ebbf0c02352eb55b9cbc48", - "shasum": "" - }, - "require": { - "bower-asset/jquery": ">=1.7" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": [ - "./dist/inputmask/jquery.inputmask.js", - "./dist/inputmask/jquery.inputmask.extensions.js", - "./dist/inputmask/jquery.inputmask.date.extensions.js", - "./dist/inputmask/jquery.inputmask.numeric.extensions.js", - "./dist/inputmask/jquery.inputmask.phone.extensions.js", - "./dist/inputmask/jquery.inputmask.regex.extensions.js" - ], - "bower-asset-ignore": [ - "**/.*", - "qunit/", - "nuget/", - "tools/", - "js/", - "*.md", - "build.properties", - "build.xml", - "jquery.inputmask.jquery.json" - ] + "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/91ef2d8836342875f2519b5815197ea0f23613cf", + "reference": "91ef2d8836342875f2519b5815197ea0f23613cf" }, + "type": "bower-asset", "license": [ - "http://opensource.org/licenses/mit-license.php" - ], - "description": "jquery.inputmask is a jquery plugin which create an input mask.", - "keywords": [ - "form", - "input", - "inputmask", - "jquery", - "mask", - "plugins" + "MIT" ] }, { "name": "bower-asset/punycode", - "version": "v1.3.2", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/bestiejs/punycode.js.git", - "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" + "url": "https://github.com/mathiasbynens/punycode.js.git", + "reference": "9e1b2cda98d215d3a73fcbfe93c62e021f4ba768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "shasum": "" + "url": "https://api.github.com/repos/mathiasbynens/punycode.js/zipball/9e1b2cda98d215d3a73fcbfe93c62e021f4ba768", + "reference": "9e1b2cda98d215d3a73fcbfe93c62e021f4ba768" }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "punycode.js", - "bower-asset-ignore": [ - "coverage", - "tests", - ".*", - "component.json", - "Gruntfile.js", - "node_modules", - "package.json" - ] - } + "type": "bower-asset" }, { "name": "bower-asset/yii2-pjax", - "version": "v2.0.4", + "version": "2.0.8", "source": { "type": "git", - "url": "https://github.com/yiisoft/jquery-pjax.git", - "reference": "3f20897307cca046fca5323b318475ae9dac0ca0" + "url": "git@github.com:yiisoft/jquery-pjax.git", + "reference": "a9298d57da63d14a950f1b94366a864bc62264fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/3f20897307cca046fca5323b318475ae9dac0ca0", - "reference": "3f20897307cca046fca5323b318475ae9dac0ca0", - "shasum": "" + "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/a9298d57da63d14a950f1b94366a864bc62264fb", + "reference": "a9298d57da63d14a950f1b94366a864bc62264fb" }, "require": { "bower-asset/jquery": ">=1.8" }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "./jquery.pjax.js", - "bower-asset-ignore": [ - ".travis.yml", - "Gemfile", - "Gemfile.lock", - "vendor/", - "script/", - "test/" - ] - }, + "type": "bower-asset", "license": [ "MIT" ] }, { "name": "cebe/markdown", - "version": "1.1.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/cebe/markdown.git", - "reference": "54a2c49de31cc44e864ebf0500a35ef21d0010b2" + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cebe/markdown/zipball/54a2c49de31cc44e864ebf0500a35ef21d0010b2", - "reference": "54a2c49de31cc44e864ebf0500a35ef21d0010b2", + "url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86", + "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86", "shasum": "" }, "require": { @@ -192,7 +110,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -221,37 +139,54 @@ "markdown", "markdown-extra" ], - "time": "2015-03-06 05:28:07" + "support": { + "issues": "https://github.com/cebe/markdown/issues", + "source": "https://github.com/cebe/markdown" + }, + "time": "2018-03-26T11:24:36+00:00" }, { "name": "ezyang/htmlpurifier", - "version": "v4.6.0", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd" + "reference": "cb56001e54359df7ae76dc522d08845dc741621b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6f389f0f25b90d0b495308efcfa073981177f0fd", - "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b", + "reference": "cb56001e54359df7ae76dc522d08845dc741621b", "shasum": "" }, "require": { - "php": ">=5.2" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" }, "type": "library", "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], "psr-0": { "HTMLPurifier": "library/" }, - "files": [ - "library/HTMLPurifier.composer.php" + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL" + "LGPL-2.1-or-later" ], "authors": [ { @@ -265,33 +200,89 @@ "keywords": [ "html" ], - "time": "2013-11-30 08:25:19" + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0" + }, + "time": "2024-11-01T03:51:45+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" }, { "name": "yiisoft/yii2", - "version": "2.0.5", + "version": "2.0.51", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "ea8c13b9f5cd437bd7bf73cad8a3457a155f3727" + "reference": "ea1f112f4dc9a9824e77b788019e2d53325d823c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/ea8c13b9f5cd437bd7bf73cad8a3457a155f3727", - "reference": "ea8c13b9f5cd437bd7bf73cad8a3457a155f3727", + "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/ea1f112f4dc9a9824e77b788019e2d53325d823c", + "reference": "ea1f112f4dc9a9824e77b788019e2d53325d823c", "shasum": "" }, "require": { - "bower-asset/jquery": "2.1.*@stable | 1.11.*@stable", - "bower-asset/jquery.inputmask": "3.1.*", - "bower-asset/punycode": "1.3.*", - "bower-asset/yii2-pjax": ">=2.0.1", - "cebe/markdown": "~1.0.0 | ~1.1.0", + "bower-asset/inputmask": "^5.0.8 ", + "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", + "bower-asset/punycode": "^2.2", + "bower-asset/yii2-pjax": "~2.0.1", + "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", + "ext-ctype": "*", "ext-mbstring": "*", - "ezyang/htmlpurifier": "4.6.*", + "ezyang/htmlpurifier": "^4.17", "lib-pcre": "*", - "php": ">=5.4.0", - "yiisoft/yii2-composer": "*" + "paragonie/random_compat": ">=1", + "php": ">=7.3.0", + "yiisoft/yii2-composer": "~2.0.4" }, "bin": [ "yii" @@ -315,13 +306,13 @@ { "name": "Qiang Xue", "email": "qiang.xue@gmail.com", - "homepage": "http://www.yiiframework.com/", + "homepage": "https://www.yiiframework.com/", "role": "Founder and project lead" }, { "name": "Alexander Makarov", "email": "sam@rmcreative.ru", - "homepage": "http://rmcreative.ru/", + "homepage": "https://rmcreative.ru/", "role": "Core framework development" }, { @@ -332,7 +323,7 @@ { "name": "Carsten Brandt", "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", + "homepage": "https://www.cebe.cc/", "role": "Core framework development" }, { @@ -345,32 +336,68 @@ "name": "Paul Klimov", "email": "klimov.paul@gmail.com", "role": "Core framework development" + }, + { + "name": "Dmitry Naumenko", + "email": "d.naumenko.a@gmail.com", + "role": "Core framework development" + }, + { + "name": "Boudewijn Vahrmeijer", + "email": "info@dynasource.eu", + "homepage": "http://dynasource.eu", + "role": "Core framework development" } ], "description": "Yii PHP Framework Version 2", - "homepage": "http://www.yiiframework.com/", + "homepage": "https://www.yiiframework.com/", "keywords": [ "framework", "yii2" ], - "time": "2015-07-11 02:37:59" + "support": { + "forum": "https://forum.yiiframework.com/", + "irc": "ircs://irc.libera.chat:6697/yii", + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "source": "https://github.com/yiisoft/yii2", + "wiki": "https://www.yiiframework.com/wiki" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2", + "type": "tidelift" + } + ], + "time": "2024-07-18T19:50:00+00:00" }, { "name": "yiisoft/yii2-composer", - "version": "2.0.3", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "ca8d23707ae47d20b0454e4b135c156f6da6d7be" + "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/ca8d23707ae47d20b0454e4b135c156f6da6d7be", - "reference": "ca8d23707ae47d20b0454e4b135c156f6da6d7be", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/94bb3f66e779e2774f8776d6e1bdeab402940510", + "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510", "shasum": "" }, "require": { - "composer-plugin-api": "1.0.0" + "composer-plugin-api": "^1.0 | ^2.0" + }, + "require-dev": { + "composer/composer": "^1.0 | ^2.0@dev", + "phpunit/phpunit": "<7" }, "type": "composer-plugin", "extra": { @@ -392,6 +419,10 @@ { "name": "Qiang Xue", "email": "qiang.xue@gmail.com" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc" } ], "description": "The composer plugin for Yii extension installer", @@ -400,17 +431,157 @@ "extension installer", "yii2" ], - "time": "2015-03-01 06:22:44" + "support": { + "forum": "http://www.yiiframework.com/forum/", + "irc": "irc://irc.freenode.net/yii", + "issues": "https://github.com/yiisoft/yii2-composer/issues", + "source": "https://github.com/yiisoft/yii2-composer", + "wiki": "http://www.yiiframework.com/wiki/" + }, + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-composer", + "type": "tidelift" + } + ], + "time": "2020-06-24T00:04:01+00:00" + } + ], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7", + "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-01-05T16:43:48+00:00" + }, + { + "name": "rector/rector", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "fa0cb009dc3df084bf549032ae4080a0481a2036" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/fa0cb009dc3df084bf549032ae4080a0481a2036", + "reference": "fa0cb009dc3df084bf549032ae4080a0481a2036", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.1" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.0.6" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2025-01-06T10:38:36+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.4.0" + "php": ">=7.4.0" }, - "platform-dev": [] + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..febc08f --- /dev/null +++ b/rector.php @@ -0,0 +1,16 @@ +withPaths([ + __DIR__ . '/messages', + ]) + ->withRootFiles() + // uncomment to reach your current PHP version + // ->withPhpSets() + ->withTypeCoverageLevel(80) + ->withDeadCodeLevel(80) + ->withCodeQualityLevel(80);