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.

Top comments (1)

Collapse
 
nuzulfikrie profile image
nuzulfikrie

i am updating my laravel knowledge. this helps!

DEV

Thank you.

 
Thanks for visiting DEV, we’ve worked really hard to cultivate this great community and would love to have you join us. If you’d like to create an account, you can sign up here.