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 = [];