If you familiar with Jetstream, you will noticed app/Actions
directory in your project. This post intended to write a simple and reusable action class.
Let's put the outline what's our action should do:
- Able to accept inputs
- Able to validate
- Able to update / create
- The usage should only need to extend the base class, define rules and model going to use.
With 4 above rules:
<?php
namespace App\Actions;
use App\Contracts\Execute;
use App\Exceptions\ActionException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
abstract class AbstractAction implements Execute
{
protected array $constrainedBy = [];
protected Model $record;
abstract public function rules(): array;
public function __construct(protected array $inputs)
{
}
public function setInputs(array $inputs): self
{
$this->inputs = $inputs;
return $this;
}
public function setConstrainedBy(array $constrainedBy): self
{
$this->constrainedBy = $constrainedBy;
return $this;
}
public function getConstrainedBy(): array
{
return $this->constrainedBy;
}
public function hasConstrained(): bool
{
return count($this->getConstrainedBy()) > 0;
}
public function getInputs(): array
{
return $this->inputs;
}
public function model(): string
{
if (! property_exists($this, 'model')) {
throw ActionException::missingModelProperty(__CLASS__);
}
return $this->model;
}
public function execute()
{
Validator::make(
array_merge(
$this->getConstrainedBy(),
$this->getInputs()
),
$this->rules()
)->validate();
return $this->record = DB::transaction(function () {
return $this->hasConstrained()
? $this->model::updateOrCreate($this->getConstrainedBy(), $this->getInputs())
: $this->model::create($this->getInputs());
});
}
public function getRecord(): Model
{
return $this->record;
}
}
And the custom exception:
<?php
namespace App\Exceptions;
use Exception;
class ActionException extends Exception
{
public static function missingModelProperty($class)
{
return new self("Missing model property in class $class");
}
}
And a contract:
<?php
namespace App\Contracts;
interface Execute
{
public function execute();
}
Now, let's take a look on how to use it. Create a class extend the abstract class:
<?php
namespace App\Actions\User;
use App\Actions\AbstractAction as Action;
use App\Actions\Fortify\PasswordValidationRules;
use App\Models\User;
class UpdateOrCreate extends Action
{
use PasswordValidationRules;
public $model = User::class;
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
];
}
}
The usage:
use \App\Actions\User\UpdateOrCreate as UpdateOrCreateUser;
$data = [
'name' => 'Nasrul Hazim',
'email' => 'nasrul@somewhere.com',
'password' => 'password',
'password_confirmation' => 'password',
];
(new UpdateOrCreateUser($data))->execute();
Another example:
<?php
namespace App\Actions\Ushot;
use App\Actions\AbstractAction as Action;
use App\Models\Project;
class CreateOrUpdateProject extends Action
{
public $model = Project::class;
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
];
}
}
The usage:
$project = (new Project(['name' => 'dev.to']))->execute();
// do something with $project->getRecord();
Do take note, you may want to handle:
- Encryption / Decryption
- Hashing
- Any data transformation prior to validation / insert / update the records.
Top comments (1)
i am updating my laravel knowledge. this helps!