diff --git a/src/actions/CreateAction.php b/src/actions/CreateAction.php index 3ac00c9..6f965f9 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; @@ -29,27 +30,40 @@ 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 * 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 = []; + /** - * @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) { + * // $model is the requested model instance. + * } + * ``` */ public $scenario = Model::SCENARIO_DEFAULT; @@ -97,9 +111,17 @@ public function run() } /* @var $model \yii\db\ActiveRecord */ - $model = new $this->modelClass([ - 'scenario' => $this->scenario, - ]); + $model = new $this->modelClass(); + + 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 d9341a3..c6ee7b0 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; @@ -23,10 +24,39 @@ 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) { + * // $model is the requested model instance. + * } + * ``` */ public $scenario = Model::SCENARIO_DEFAULT; + /** + * @var callable|null A PHP callable that will be called to determine + * whether the deletion of a model is allowed. If not set, no deletion + * check will be performed. The callable should have the following signature: + * + * @example + * ```php + * function ($action, $model) { + * // $model is the model instance being deleted. + * + * // If the deletion is not allowed, an error should be thrown. For example: + * if ($model->status !== 'draft') { + * throw new MethodNotAllowedHttpException('The model can only be deleted if its status is "draft".'); + * } + * } + * ``` + */ + public $checkDeleteAllowed; + + /** * @var callable|Closure Callback after save model with all relations * @example @@ -56,10 +86,24 @@ 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); + + 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 ($this->checkDeleteAllowed) { + call_user_func($this->checkDeleteAllowed, $this->id, $model); + } + if ($model->delete() === false) { throw new ServerErrorHttpException('Failed to delete the object for unknown reason.'); } diff --git a/src/actions/JsonApiAction.php b/src/actions/JsonApiAction.php index de6e074..75b61eb 100644 --- a/src/actions/JsonApiAction.php +++ b/src/actions/JsonApiAction.php @@ -61,13 +61,20 @@ class JsonApiAction extends Action public $findModel; /** - * @var callable a PHP callable that will be called when running an action to determine - * if the current user has the permission to execute the action. If not set, the access - * check will not be performed. The signature of the callable should be as follows, + * @var callable|null A PHP callable that will be called when running an action to determine + * whether the current user has permission to execute the action. If not set, no access + * check will be performed. The callable should have the following signature: + * + * @example * ```php * function ($action, $model = null) { * // $model is the requested model instance. - * // If null, it means no specific model (e.g. IndexAction) + * // If null, it indicates no specific model (e.g., IndexAction). + * + * // If the user does not have the required permissions, an error should be thrown. For example: + * if (!Yii::$app->user->can('admin')) { + * throw new ForbiddenHttpException(); + * } * } * ``` */ diff --git a/src/actions/UpdateAction.php b/src/actions/UpdateAction.php index 528daf1..57afd67 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; /** @@ -51,10 +52,40 @@ 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) { + * // $model is the requested model instance. + * } + * ``` */ public $scenario = Model::SCENARIO_DEFAULT; + + /** + * @var callable|null A PHP callable that will be called to determine + * whether the update of a model is allowed. If not set, no update + * check will be performed. The callable should have the following signature: + * + * @example + * ```php + * function ($action, $model) { + * // $model is the model instance being updated. + * + * // If the update is not allowed, an error should be thrown. For example: + * if ($model->status === 'archived') { + * throw new MethodNotAllowedHttpException('The model cannot be updated when its status is "archived".'); + * } + * } + * ``` + */ + public $checkUpdateAllowed; + /** * @var callable|Closure Callback after save model with all relations * @example @@ -63,6 +94,7 @@ class UpdateAction extends JsonApiAction * } */ public $afterSave = null; + /** * @throws \yii\base\InvalidConfigException */ @@ -88,10 +120,24 @@ public function run($id):Item { /* @var $model ActiveRecord */ $model = $this->isParentRestrictionRequired() ? $this->findModelForParent($id) : $this->findModel($id); - $model->scenario = $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 ($this->checkUpdateAllowed) { + call_user_func($this->checkUpdateAllowed, $this->id, $model); + } + $originalModel = clone $model; RelationshipManager::validateRelationships($model, $this->getResourceRelationships(), $this->allowedRelations); if (empty($this->getResourceAttributes()) && $this->hasResourceRelationships()) {