Reusable Action Class
Tags: laravel, action
Published: 2022-09-10 17:20:27

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
<?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
<?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
<?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
<?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:


```php
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
<?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:

```php 

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

Do take note, you may want to handle:

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