From 0d0f005c2467c858eab651f1fd7b34eef0345442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmar=20K=C3=BChlmann?= <sk@cebe.cloud> Date: Tue, 17 Dec 2024 13:43:12 +0100 Subject: [PATCH 1/5] callable scenarios for actions: create, update, delete --- src/actions/CreateAction.php | 23 ++++++++++++++++++----- src/actions/DeleteAction.php | 16 ++++++++++++++-- src/actions/UpdateAction.php | 16 ++++++++++++++-- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/actions/CreateAction.php b/src/actions/CreateAction.php index 3ac00c9..d79701d 100644 --- a/src/actions/CreateAction.php +++ b/src/actions/CreateAction.php @@ -29,9 +29,10 @@ class CreateAction extends JsonApiAction { use HasResourceTransformer; use HasParentAttributes; + /** * @var array - * * Configuration for attaching relationships + * Configuration for attaching relationships * Should contains key - relation name and array with * idType - php type of resource ids for validation * validator = callback for custom id validation @@ -44,12 +45,24 @@ class CreateAction extends JsonApiAction * $relatedModels = Relation::find()->where(['id' => $ids])->andWhere([additional conditions])->all(); * if(count($relatedModels) < $ids) { * throw new HttpException(422, 'Invalid photos ids'); - * }], + * }}, * ] - **/ + **/ + public $allowedRelations = []; + /** - * @var string the scenario to be assigned to the new model before it is validated and saved. + * @var string|callable + * string - the scenario to be assigned to the model before it is validated and saved. + * callable - a PHP callable that will be executed during the action. + * It must return a string representing the scenario to be assigned to the model before it is validated and saved. + * The signature of the callable should be as follows, + * ```php + * function ($action, $model = null) { + * // $model is the requested model instance. + * // If null, it means no specific model (e.g. CreateAction) + * } + * ``` */ public $scenario = Model::SCENARIO_DEFAULT; @@ -98,7 +111,7 @@ public function run() /* @var $model \yii\db\ActiveRecord */ $model = new $this->modelClass([ - 'scenario' => $this->scenario, + 'scenario' => is_callable($this->scenario) ? call_user_func($this->scenario, $this->id) : $this->scenario, ]); RelationshipManager::validateRelationships($model, $this->getResourceRelationships(), $this->allowedRelations); $model->load($this->getResourceAttributes(), ''); diff --git a/src/actions/DeleteAction.php b/src/actions/DeleteAction.php index d9341a3..bae39b4 100644 --- a/src/actions/DeleteAction.php +++ b/src/actions/DeleteAction.php @@ -23,7 +23,17 @@ class DeleteAction extends JsonApiAction use HasParentAttributes; /** - * @var string the scenario to be assigned to the new model before it is validated and saved. + * @var string|callable + * string - the scenario to be assigned to the model before it is validated and saved. + * callable - a PHP callable that will be executed during the action. + * It must return a string representing the scenario to be assigned to the model before it is validated and saved. + * The signature of the callable should be as follows, + * ```php + * function ($action, $model = null) { + * // $model is the requested model instance. + * // If null, it means no specific model (e.g. CreateAction) + * } + * ``` */ public $scenario = Model::SCENARIO_DEFAULT; @@ -56,7 +66,9 @@ public function run($id):void throw new ForbiddenHttpException('Update with relationships not supported yet'); } $model = $this->isParentRestrictionRequired() ? $this->findModelForParent($id) : $this->findModel($id); - $model->setScenario($this->scenario); + $model->setScenario(is_callable($this->scenario) ? + call_user_func($this->scenario, $this->id, $model) : $this->scenario + ); if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id, $model); } diff --git a/src/actions/UpdateAction.php b/src/actions/UpdateAction.php index 528daf1..4efa519 100644 --- a/src/actions/UpdateAction.php +++ b/src/actions/UpdateAction.php @@ -51,8 +51,19 @@ class UpdateAction extends JsonApiAction * ] **/ public $allowedRelations = []; + /** - * @var string the scenario to be assigned to the model before it is validated and updated. + * @var string|callable + * string - the scenario to be assigned to the model before it is validated and updated. + * callable - a PHP callable that will be executed during the action. + * It must return a string representing the scenario to be assigned to the model before it is validated and updated. + * The signature of the callable should be as follows, + * ```php + * function ($action, $model = null) { + * // $model is the requested model instance. + * // If null, it means no specific model (e.g. CreateAction) + * } + * ``` */ public $scenario = Model::SCENARIO_DEFAULT; /** @@ -88,7 +99,8 @@ public function run($id):Item { /* @var $model ActiveRecord */ $model = $this->isParentRestrictionRequired() ? $this->findModelForParent($id) : $this->findModel($id); - $model->scenario = $this->scenario; + $model->scenario = is_callable($this->scenario) ? + call_user_func($this->scenario, $this->id, $model) : $this->scenario; if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id, $model); } From d26aba87e8667b37329c1420e0bb13d71d52360e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmar=20K=C3=BChlmann?= <sk@cebe.cloud> Date: Tue, 17 Dec 2024 14:36:00 +0100 Subject: [PATCH 2/5] update callable scenario for action create --- src/actions/CreateAction.php | 10 +++++----- src/actions/DeleteAction.php | 3 +-- src/actions/UpdateAction.php | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/actions/CreateAction.php b/src/actions/CreateAction.php index d79701d..4e949b6 100644 --- a/src/actions/CreateAction.php +++ b/src/actions/CreateAction.php @@ -58,9 +58,8 @@ class CreateAction extends JsonApiAction * It must return a string representing the scenario to be assigned to the model before it is validated and saved. * The signature of the callable should be as follows, * ```php - * function ($action, $model = null) { + * function ($action, $model) { * // $model is the requested model instance. - * // If null, it means no specific model (e.g. CreateAction) * } * ``` */ @@ -110,9 +109,10 @@ public function run() } /* @var $model \yii\db\ActiveRecord */ - $model = new $this->modelClass([ - 'scenario' => is_callable($this->scenario) ? call_user_func($this->scenario, $this->id) : $this->scenario, - ]); + $model = new $this->modelClass(); + $model->setScenario(is_callable($this->scenario) ? + call_user_func($this->scenario, $this->id, $model) : $this->scenario + ); RelationshipManager::validateRelationships($model, $this->getResourceRelationships(), $this->allowedRelations); $model->load($this->getResourceAttributes(), ''); if ($this->isParentRestrictionRequired()) { diff --git a/src/actions/DeleteAction.php b/src/actions/DeleteAction.php index bae39b4..9107759 100644 --- a/src/actions/DeleteAction.php +++ b/src/actions/DeleteAction.php @@ -29,9 +29,8 @@ class DeleteAction extends JsonApiAction * It must return a string representing the scenario to be assigned to the model before it is validated and saved. * The signature of the callable should be as follows, * ```php - * function ($action, $model = null) { + * function ($action, $model) { * // $model is the requested model instance. - * // If null, it means no specific model (e.g. CreateAction) * } * ``` */ diff --git a/src/actions/UpdateAction.php b/src/actions/UpdateAction.php index 4efa519..673ecf5 100644 --- a/src/actions/UpdateAction.php +++ b/src/actions/UpdateAction.php @@ -59,9 +59,8 @@ class UpdateAction extends JsonApiAction * It must return a string representing the scenario to be assigned to the model before it is validated and updated. * The signature of the callable should be as follows, * ```php - * function ($action, $model = null) { + * function ($action, $model) { * // $model is the requested model instance. - * // If null, it means no specific model (e.g. CreateAction) * } * ``` */ From 2c2175e8fb9a07fdf7c3b27b40873176274150aa Mon Sep 17 00:00:00 2001 From: Carsten Brandt <mail@cebe.cc> Date: Wed, 18 Dec 2024 10:07:02 +0100 Subject: [PATCH 3/5] Apply suggestions from code review --- src/actions/CreateAction.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/actions/CreateAction.php b/src/actions/CreateAction.php index 4e949b6..3ad3484 100644 --- a/src/actions/CreateAction.php +++ b/src/actions/CreateAction.php @@ -45,9 +45,10 @@ class CreateAction extends JsonApiAction * $relatedModels = Relation::find()->where(['id' => $ids])->andWhere([additional conditions])->all(); * if(count($relatedModels) < $ids) { * throw new HttpException(422, 'Invalid photos ids'); - * }}, + * } + * }, * ] - **/ + */ public $allowedRelations = []; From e121018ef0e22f6731ded02e4deac3cc92a34b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmar=20K=C3=BChlmann?= <sk@cebe.cloud> Date: Wed, 18 Dec 2024 10:48:31 +0100 Subject: [PATCH 4/5] scenario check type string first --- src/actions/CreateAction.php | 14 +++++++++++--- src/actions/DeleteAction.php | 15 ++++++++++++--- src/actions/UpdateAction.php | 14 ++++++++++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/actions/CreateAction.php b/src/actions/CreateAction.php index 3ad3484..3782f42 100644 --- a/src/actions/CreateAction.php +++ b/src/actions/CreateAction.php @@ -16,6 +16,7 @@ use yii\base\Model; use yii\db\ActiveRecordInterface; use yii\helpers\Url; +use yii\base\InvalidConfigException; use yii\web\ServerErrorHttpException; use function array_keys; use function call_user_func; @@ -111,9 +112,16 @@ public function run() /* @var $model \yii\db\ActiveRecord */ $model = new $this->modelClass(); - $model->setScenario(is_callable($this->scenario) ? - call_user_func($this->scenario, $this->id, $model) : $this->scenario - ); + + if (is_string($this->scenario)) { + $scenario = $this->scenario; + } elseif (is_callable($this->scenario)) { + $scenario = call_user_func($this->scenario, $this->id, $model); + } else { + throw new InvalidConfigException('The "scenario" property must be defined either as a string or as a callable.'); + } + $model->setScenario($scenario); + RelationshipManager::validateRelationships($model, $this->getResourceRelationships(), $this->allowedRelations); $model->load($this->getResourceAttributes(), ''); if ($this->isParentRestrictionRequired()) { diff --git a/src/actions/DeleteAction.php b/src/actions/DeleteAction.php index 9107759..f408491 100644 --- a/src/actions/DeleteAction.php +++ b/src/actions/DeleteAction.php @@ -10,6 +10,7 @@ use Closure; use Yii; use yii\base\Model; +use yii\base\InvalidConfigException; use yii\web\ForbiddenHttpException; use yii\web\ServerErrorHttpException; @@ -65,12 +66,20 @@ public function run($id):void throw new ForbiddenHttpException('Update with relationships not supported yet'); } $model = $this->isParentRestrictionRequired() ? $this->findModelForParent($id) : $this->findModel($id); - $model->setScenario(is_callable($this->scenario) ? - call_user_func($this->scenario, $this->id, $model) : $this->scenario - ); + + if (is_string($this->scenario)) { + $scenario = $this->scenario; + } elseif (is_callable($this->scenario)) { + $scenario = call_user_func($this->scenario, $this->id, $model); + } else { + throw new InvalidConfigException('The "scenario" property must be defined either as a string or as a callable.'); + } + $model->setScenario($scenario); + if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id, $model); } + if ($model->delete() === false) { throw new ServerErrorHttpException('Failed to delete the object for unknown reason.'); } diff --git a/src/actions/UpdateAction.php b/src/actions/UpdateAction.php index 673ecf5..f0517cd 100644 --- a/src/actions/UpdateAction.php +++ b/src/actions/UpdateAction.php @@ -15,6 +15,7 @@ use Yii; use yii\base\Model; use yii\db\ActiveRecord; +use yii\base\InvalidConfigException; use yii\web\ServerErrorHttpException; /** @@ -98,11 +99,20 @@ public function run($id):Item { /* @var $model ActiveRecord */ $model = $this->isParentRestrictionRequired() ? $this->findModelForParent($id) : $this->findModel($id); - $model->scenario = is_callable($this->scenario) ? - call_user_func($this->scenario, $this->id, $model) : $this->scenario; + + if (is_string($this->scenario)) { + $scenario = $this->scenario; + } elseif (is_callable($this->scenario)) { + $scenario = call_user_func($this->scenario, $this->id, $model); + } else { + throw new InvalidConfigException('The "scenario" property must be defined either as a string or as a callable.'); + } + $model->setScenario($scenario); + if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id, $model); } + $originalModel = clone $model; RelationshipManager::validateRelationships($model, $this->getResourceRelationships(), $this->allowedRelations); if (empty($this->getResourceAttributes()) && $this->hasResourceRelationships()) { From 8ee5446154a245781bb0599ea9c90c165888e9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmar=20K=C3=BChlmann?= <sk@cebe.cloud> Date: Wed, 18 Dec 2024 10:54:36 +0100 Subject: [PATCH 5/5] createAction adjust comment --- src/actions/CreateAction.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/actions/CreateAction.php b/src/actions/CreateAction.php index 3782f42..6f965f9 100644 --- a/src/actions/CreateAction.php +++ b/src/actions/CreateAction.php @@ -40,15 +40,15 @@ class CreateAction extends JsonApiAction * Keep it empty for disable this ability * @see https://jsonapi.org/format/#crud-creating * @example - * 'allowedRelations' => [ - * 'author' => ['idType' => 'integer'], - * 'photos' => ['idType' => 'integer', 'validator' => function($model, array $ids) { - * $relatedModels = Relation::find()->where(['id' => $ids])->andWhere([additional conditions])->all(); - * if(count($relatedModels) < $ids) { - * throw new HttpException(422, 'Invalid photos ids'); - * } - * }, - * ] + * 'allowedRelations' => [ + * 'author' => ['idType' => 'integer'], + * 'photos' => ['idType' => 'integer', 'validator' => function($model, array $ids) { + * $relatedModels = Relation::find()->where(['id' => $ids])->andWhere([additional conditions])->all(); + * if (count($relatedModels) < $ids) { + * throw new HttpException(422, 'Invalid photos ids'); + * } + * }, + * ] */ public $allowedRelations = [];