Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named Transitions with on classes #22

Open
captnCC opened this issue Jun 8, 2021 · 0 comments
Open

Named Transitions with on classes #22

captnCC opened this issue Jun 8, 2021 · 0 comments

Comments

@captnCC
Copy link

captnCC commented Jun 8, 2021

Summary

For small state machines the current API Design works good, but in my case I has 8 States and ~15 Transitions that all need some sort of transitions guards/validation and hooks (pre/after).

This bloats the state machine class extremely to an unreadable state. It needs way to much if clauses to check $from and $to for all the possibilities.

Proposal

I introduced classes per Transition to help organise all the logic. Each class defines for which state machines, from states and to states its applicable. These classes are registered to the state machine which selects the first applicable Transition class for the transition to be run.

The transition class itself has methods for validation, pre-hook and a post-hook.

class Transition
{

    /**
     * @var \Asantibanez\LaravelEloquentStateMachines\StateMachines\StateMachine
     */
    protected StateMachine $stateMachine;

    /**
     * @var string[]
     */
    protected static array $allowedFor = [];

    /** @var string[] */
    protected static array $allowedFrom = [];

    /** @var string[] */
    protected static array $allowedTo = [];

    public function __construct(
      StateMachine $stateMachine,
      protected string $from,
      protected string $to,
      protected Model $model,
      protected array $customProperties,
      protected mixed $responsible
    ) {
        if (count(static::$allowedFor) > 0 && !in_array($stateMachine::class, static::$allowedFor, false)) {
            throw new InvalidArgumentException("This transition isn't applicable to the statemachine");
        }
    }

    public function validate(): ?Validator {
        return null;
    }

    /**
     * @param  string  $from
     * @param  string  $to
     * @param  \Asantibanez\LaravelEloquentStateMachines\StateMachines\StateMachine  $stateMachine
     *
     * @return bool
     */
    public static function isApplicable(
      string $from,
      string $to,
      StateMachine $stateMachine
    ): bool {
        return
          in_array($from, static::$allowedFrom, false)
          && in_array($to, static::$allowedTo, false)
          && in_array($stateMachine::class, static::$allowedFor, false);
    }

    public function preTransition(): void
    {

    }

    public function postTransition(): void
    {

    }

}

The StateMachine class it self needs just a little modifications an supports the 'old' way of managing transitions as well.
The main points are the transitionClasses array and some additions to the transitionTo method, which now selects the first applicable Transition class and runs its hooks after the main StateMachine hooks.

class StateMachine
{

    public function transitionClasses(): array
    {
        return [
          Transition::class,
        ];
    }

    /**
     * @param $from
     * @param $to
     * @param  array  $customProperties
     * @param  null|mixed  $responsible
     *
     * @throws \Asantibanez\LaravelEloquentStateMachines\Exceptions\TransitionNotAllowedException
     * @throws \Illuminate\Validation\ValidationException
     */
    public function transitionTo(
      $from,
      $to,
      $customProperties = [],
      $responsible = null
    ): void {
        if (!$this->canBe($from, $to)) {
            throw new TransitionNotAllowedException();
        }

        $transitionClass = collect($this->transitionClasses())
          ->filter(function ($class) use (
            $from,
            $to
          ) {
              return $class::isApplicable($from, $to, $this);
          })->first() ?: Transition::class;

        /** @var Transition $transition */
        $transition = new $transitionClass($this, $from, $to, $this->model,
          $customProperties,
          $responsible);


        $validator = $transition->validate() ?: $this->validatorForTransition($from,
          $to, $this->model);
        if ($validator !== null && $validator->fails()) {
            throw new ValidationException($validator);
        }

        $beforeTransitionHooks = $this->beforeTransitionHooks()[$from] ?? [];

        collect($beforeTransitionHooks)
          ->each(function ($callable) use (
            $to,
            $customProperties,
            $responsible
          ) {
              $callable($to, $this->model, $customProperties, $responsible);
          });
        $transition->preTransition();

        $field = $this->field;
        $this->model->$field = $to;

        $changedAttributes = $this->model->getChangedAttributes();

        $this->model->save();

        if ($this->recordHistory()) {
            $responsible = $responsible ?? auth()->user();

            $this->model->recordState($field, $from, $to, $customProperties,
              $responsible, $changedAttributes);
        }

        $afterTransitionHooks = $this->afterTransitionHooks()[$to] ?? [];


        collect($afterTransitionHooks)
          ->each(function ($callable) use ($from) {
              $callable($from, $this->model);
          });

        $transition->postTransition();

        $this->cancelAllPendingTransitions();
    }

}

If there is interest in this kind of feature I could provide a PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant