DEV Community

Nasrul Hazim Bin Mohamad
Nasrul Hazim Bin Mohamad

Posted on

Reusable Action Class

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:

  1. Able to accept inputs
  2. Able to validate
  3. Able to update / create
  4. 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

And a contract:

<?php

namespace App\Contracts;

interface Execute
{
    public function execute();
}
Enter fullscreen mode Exit fullscreen mode

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(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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'],
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

The usage:


$project = (new Project(['name' => 'dev.to']))->execute();
// do something with $project->getRecord();
Enter fullscreen mode Exit fullscreen mode

Do take note, you may want to handle:

  1. Encryption / Decryption
  2. Hashing
  3. Any data transformation prior to validation / insert / update the records.

Oldest comments (1)

Collapse
 
nuzulfikrie profile image
nuzulfikrie

i am updating my laravel knowledge. this helps!