Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
pixelworxio/livewire-workflows
claude-code
cursor
workflow
Forms
laravel
multistep
livewire

This document provides AI assistants with comprehensive information about the Livewire Workflows codebase structure, development workflows, and key conventions.

Install
php artisan boost:add-skill pixelworxio/livewire-workflows

Save this content to: AGENTS.md

# AGENTS.md - AI Assistant Guide for Livewire Workflows

This document provides AI assistants with comprehensive information about the Livewire Workflows codebase structure, development workflows, and key conventions.

## Table of Contents

1. [Project Overview](#project-overview)
2. [Architecture & Design Patterns](#architecture--design-patterns)
3. [Directory Structure](#directory-structure)
4. [Core Components](#core-components)
5. [Development Workflow](#development-workflow)
6. [Coding Standards](#coding-standards)
7. [Testing Conventions](#testing-conventions)
8. [Key Concepts](#key-concepts)
9. [Common Tasks](#common-tasks)
10. [Troubleshooting](#troubleshooting)

---

## Project Overview

**Livewire Workflows** is a Laravel package that enables developers to build multi-step workflows with zero boilerplate. It provides an expressive DSL for defining complex user journeys (onboarding, checkouts, surveys) with automatic route registration, guard-based navigation, state persistence, and full Livewire integration.

### Key Information

- **Package Name**: `pixelworxio/livewire-workflows`
- **Namespace**: `Pixelworxio\LivewireWorkflows`
- **Requirements**: PHP 8.3+, Laravel 11.x/12.x/13.x, Livewire 3.x/4.x
- **License**: MIT
- **Test Framework**: Pest v5
- **Code Quality**: PHPStan (level 2), Laravel Pint

### Primary Features

1. **Auto-generated routes** from DSL definitions
2. **Guard-based navigation** with positive semantics
3. **State persistence** (session, database, or null)
4. **History tracking** for back navigation
5. **Progress tracking** API
6. **Event-driven extensibility**
7. **Signed resume links** for Eloquent-backed workflows
8. **Livewire 3 and 4 integration**

---

## Architecture & Design Patterns

### Design Patterns Used

#### 1. Fluent Builder Pattern
- **Classes**: `FlowBuilder`, `StepBuilder`
- **Purpose**: Provide expressive DSL for workflow definition
- **Result**: Immutable DTOs (`WorkflowDefinition`, `StepDefinition`)

```php
Workflow::flow('onboarding')
    ->entersAt(name: 'onboarding.start', path: '/onboarding')
    ->finishesAt('dashboard')
    ->step('verify-email')
        ->goTo(VerifyEmail::class)
        ->unlessPasses(EmailVerifiedGuard::class)
        ->order(10);
```

#### 2. Repository Pattern
- **Contract**: `WorkflowStateRepository`
- **Implementations**: `SessionWorkflowStateRepository`, `EloquentWorkflowStateRepository`, `NullStateRepository`
- **Purpose**: Abstract state persistence mechanism

#### 3. Strategy Pattern
- **Usage**: Swap state repositories via configuration
- **Binding**: Service container resolves correct implementation

#### 4. Pipeline Pattern
- **Location**: `WorkflowEngine`
- **Purpose**: Sequential guard evaluation using Laravel's Pipeline

#### 5. Facade Pattern
- **Class**: `Workflow` facade
- **Target**: `WorkflowRegistrar`
- **Purpose**: Static access to workflow registration

#### 6. Registry Pattern
- **Class**: `WorkflowRegistrar`
- **Purpose**: Central registry of all workflows with lazy finalization

#### 7. DTO Pattern
- **Classes**: `WorkflowDefinition`, `StepDefinition`
- **Characteristics**: Immutable, validated on construction

#### 8. Trait Composition
- **Trait**: `InteractsWithWorkflows`
- **Purpose**: Reusable functionality for Livewire components

#### 9. Observer Pattern (Events)
- **Events**: `WorkflowAdvanced`, `WorkflowCompleted`, `WorkflowStateClearing`
- **Purpose**: Extensibility and analytics hooks

---

## Directory Structure

```
livewire-workflows/
├── config/
│   └── livewire-workflows.php          # Package configuration
├── database/
│   └── migrations/                     # Database migration stubs
├── resources/                          # Package resources (images, etc.)
├── src/
│   ├── Attributes/                     # PHP 8 Attributes
│   │   ├── WorkflowName.php           # Workflow name declaration attribute
│   │   ├── WorkflowState.php          # State persistence attribute
│   │   └── WorkflowStep.php           # Step metadata attribute
│   ├── Commands/                       # Artisan commands
│   │   ├── MakeWorkflowCommand.php
│   │   ├── MakeWorkflowStepCommand.php
│   │   ├── MakeWorkflowGuardCommand.php
│   │   ├── WorkflowsInstallCommand.php
│   │   └── WorkflowsScanCommand.php
│   │   ├── WorkflowsAuditCommand.php
│   │   └── WorkflowsUpgradeCommand.php
│   ├── Contracts/                      # Interfaces
│   │   ├── GuardContract.php
│   │   └── WorkflowStateRepository.php
│   ├── Events/                         # Laravel Events
│   │   ├── WorkflowAdvanced.php
│   │   ├── WorkflowCompleted.php
│   │   └── WorkflowStateClearing.php
│   ├── Exceptions/                     # Custom exceptions
│   │   ├── WorkflowNotFoundException.php
│   │   └── InvalidWorkflowConfigurationException.php
│   ├── Facades/                        # Laravel Facades
│   │   └── Workflow.php
│   ├── Http/Controllers/               # HTTP Controllers
│   │   └── WorkflowEntryController.php
│   │   └── WorkflowResumeController.php
│   ├── Livewire/Concerns/             # Livewire Traits
│   │   └── InteractsWithWorkflows.php
│   ├── Registrar/                      # DSL Implementation
│   │   ├── WorkflowRegistrar.php      # Central registry
│   │   ├── FlowBuilder.php            # Flow builder
│   │   └── StepBuilder.php            # Step builder
│   ├── StateRepositories/             # State Persistence
│   │   ├── NullStateRepository.php
│   │   ├── SessionWorkflowStateRepository.php
│   │   └── EloquentWorkflowStateRepository.php
│   ├── Support/                       # Core Support Classes
│   │   ├── WorkflowDefinition.php     # Workflow DTO
│   │   ├── StepDefinition.php         # Step DTO
│   │   ├── WorkflowEngine.php         # Core execution logic
│   │   ├── WorkflowResolver.php       # High-level API
│   │   ├── WorkflowInstance.php       # Fluent workflow interface
│   │   └── RouteRegistrar.php         # Auto-route registration
│   ├── LivewireWorkflowsServiceProvider.php
│   └── helpers.php                    # Global helper functions
├── stubs/                             # Code generation templates
│   └── workflows.php.stub
├── tests/
│   ├── Feature/                       # Integration tests
│   ├── Unit/                          # Unit tests
│   ├── Support/                       # Test fixtures
│   ├── TestCase.php                   # Base test case
│   ├── Pest.php                       # Pest configuration
│   └── ArchTest.php                   # Architecture tests
├── CHANGELOG.md                       # Version history
├── CONTRIBUTING.md                    # Contribution guidelines
├── README.md                          # User documentation
├── STATE_MANAGEMENT.md                # State management guide
├── UPGRADE.md                         # Upgrade guide
├── composer.json
├── phpunit.xml.dist
└── phpstan.neon.dist
```

---

## Core Components

### 1. Workflow Engine (`src/Support/WorkflowEngine.php`)

**Purpose**: Core workflow execution logic

**Responsibilities**:
- Evaluates guards using Laravel Pipeline
- Determines next/previous steps
- Handles workflow completion
- Calculates progress tracking
- Fires workflow events
- Manages state clearing

**Key Methods**:
- `nextStep(WorkflowDefinition $workflow, Request $request): ?string`
- `previousStep(WorkflowDefinition $workflow, string $currentKey, Request $request): ?string`
- `advanceTo(WorkflowDefinition $workflow, string $toKey, Request $request): void`
- `complete(WorkflowDefinition $workflow, Request $request): void`
- `progress(WorkflowDefinition $workflow, Request $request): array`

### 2. Workflow Registrar (`src/Registrar/WorkflowRegistrar.php`)

**Purpose**: Central registry for all workflows

**Responsibilities**:
- Manages pending builders and finalized definitions
- Lazy finalization pattern
- Provides workflow lookup

**Key Methods**:
- `flow(string $name): FlowBuilder` - Start a new workflow definition
- `get(string $name): WorkflowDefinition` - Get finalized workflow
- `has(string $name): bool` - Check if workflow exists
- `all(): array` - Get all workflows

### 3. Flow Builder (`src/Registrar/FlowBuilder.php`)

**Purpose**: Fluent builder for workflow definitions

**Key Methods**:
- `entersAt(string $name, string $path): self` - Define entry route
- `finishesAt(string $routeName): self` - Define completion route
- `historyMode(string $mode): self` - Set history mode ('none' or 'stack')
- `step(string $key): StepBuilder` - Add a step and return StepBuilder
- `build(): WorkflowDefinition` - Finalize and validate

**Auto-Build**: Builder auto-builds in destructor if `finishesAt()` was called.

### 4. Step Builder (`src/Registrar/StepBuilder.php`)

**Purpose**: Fluent builder for individual steps

**Key Methods**:
- `goTo(string $componentClass): self` - Set Livewire component
- `unlessPasses(string $guardClass): self` - Set guard class
- `order(int $order): self` - Set execution order
- Proxies unknown methods to `FlowBuilder` for chaining

### 5. Workflow Definition (`src/Support/WorkflowDefinition.php`)

**Purpose**: Immutable DTO for workflow configuration

**Properties**:
- `flow` - Workflow identifier
- `entryRouteName`, `entryPath` - Entry route details
- `finishRoute` - Completion route
- `historyMode` - History tracking mode
- `steps` - Array of StepDefinitions

**Key Methods**:
- `getOrderedSteps(): array` - Steps sorted by order
- `getPreviousStep(string $currentKey): ?StepDefinition`
- `getStepRouteName(string $stepKey): string`

### 6. InteractsWithWorkflows Trait (`src/Livewire/Concerns/InteractsWithWorkflows.php`)

**Purpose**: Core trait for Livewire components

**Lifecycle Hooks**:
- `bootInteractsWithWorkflows()` - Read WorkflowName attribute or auto-detect workflow name from route
- `mountInteractsWithWorkflows()` - Hydrate state from repository
- `updatedInteractsWithWorkflows(string $propertyName, mixed $value)` - Proactively sync state when properties change
- `dehydrateInteractsWithWorkflows()` - Sync state to repository as fallback

**State Persistence Strategy**:
The trait now uses a **dual-layer persistence** approach for maximum reliability:

1. **Proactive Syncing** - Properties marked with `#[WorkflowState]` are immediately persisted when changed via Livewire's `updated()` hook
   - Ensures state is saved even if dehydration fails
   - Provides immediate persistence for wire:model bindings
   - Tracks dirty properties to optimize syncing

2. **Dehydration Fallback** - The dehydrate hook still runs to catch any property changes not captured by the updated hook
   - Handles direct property assignments in methods
   - Ensures complete state snapshot at end of request
   - Backward compatible with existing code

This approach addresses edge cases where:
- Request fails before dehydration completes
- Properties are modified outside Livewire's tracking
- Complex component interactions require immediate persistence

**Navigation Methods**:
- `continue(string $flow): void` - Move to next step
- `back(string $flow, string $currentKey): void` - Return to previous step
- `syncState(): void` - Manually persist state

**State Methods**:
- `getWorkflowState(string $key, mixed $default = null): mixed`
- `putWorkflowState(string $key, mixed $value): void`
- `hasWorkflowState(string $key): bool`
- `forgetWorkflowState(string $key): void`
- `clearWorkflowState(?string $namespace = null): void`
- `allWorkflowState(): array`

**User Key Resolution**:
1. Authenticated user ID (if logged in)
2. Session ID (if session available)
3. Guest fingerprint: `guest-{md5(IP + UserAgent)}`

### 7. Guard Contract (`src/Contracts/GuardContract.php`)

**Purpose**: Interface for step guards

**Critical Understanding - Positive Semantics**:
- `passes() = true` → Step is SKIPPED
- `passes() = false` → Step is SHOWN
- Used with `unlessPasses()` in DSL: "Show step UNLESS guard passes"

**Methods**:
- `passes(Request $request): bool` - Determine if step can be skipped
- `onEnter(Request $request): void` - Hook when entering step
- `onExit(Request $request): void` - Hook when exiting step
- `onPass(Request $request): void` - Hook when guard passes
- `onFail(Request $request): void` - Hook when guard fails

### 8. State Repositories

#### WorkflowStateRepository Contract
**Methods**:
- `getCurrentStep(string $flow, string|int|null $userKey): ?string`
- `setCurrentStep(string $flow, string|int|null $userKey, string $stepKey): void`
- `getHistory(string $flow, string|int|null $userKey): array`
- `pushHistory(string $flow, string|int|null $userKey, string $stepKey): void`
- `popHistory(string $flow, string|int|null $userKey): ?string`
- `clear(string $flow, string|int|null $userKey): void`
- `getMetadata(string $flow, string|int|null $userKey): array`
- `setMetadata(string $flow, string|int|null $userKey, array $metadata): void`
- `getState(string $flow, string|int|null $userKey, string $key): mixed`
- `setState(string $flow, string|int|null $userKey, string $key, mixed $value): void`
- `hasState(string $flow, string|int|null $userKey, string $key): bool`
- `forgetState(string $flow, string|int|null $userKey, string $key): void`
- `clearState(string $flow, string|int|null $userKey, ?string $namespace = null): void`
- `getAllState(string $flow, string|int|null $userKey): array`

#### SessionWorkflowStateRepository
- Session-based storage
- Keys: `workflows.{flow}.{userKey}.{suffix}`
- Suitable for guests and simple flows

#### EloquentWorkflowStateRepository
- Database-backed (`workflow_states` table)
- Production-ready with persistence
- Unique constraint on `[workflow_name, user_key]`

#### NullStateRepository
- No-op implementation for stateless workflows

### 9. Attributes

#### WorkflowName Attribute (`src/Attributes/WorkflowName.php`)

**Purpose**: Declare the workflow name on a Livewire component class

**Usage**:
```php
use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName;

#[WorkflowName('onboarding')]
class VerifyEmailStep extends Component
{
    use InteractsWithWorkflows;
    // No need to set protected ?string $workflowName = 'onboarding';
}
```

**Benefits**:
- Eliminates boilerplate of setting `$workflowName` property
- Makes workflow association explicit and declarative
- Auto-detected during `bootInteractsWithWorkflows()`
- Falls back to route-based auto-detection if not present

**When to Use**:
- When you want explicit workflow association
- When component might be used outside workflow routes
- When you want to avoid route-based auto-detection

#### WorkflowState Attribute (`src/Attributes/WorkflowState.php`)

**Purpose**: Mark component properties for automatic state persistence

**Parameters**:
- `encrypt` (bool) - Whether to encrypt the value in storage (default: false)
- `namespace` (string|null) - Optional namespace for grouping state keys (default: null)

**Usage**: See "State Management with Attributes" section above

#### WorkflowStep Attribute (`src/Attributes/WorkflowStep.php`)

**Purpose**: Mark Livewire components as workflow steps (for validation/scanning)

**Note**: Not used for auto-registration (DSL is source of truth), but can be used by the `workflows:scan` command for validation

---

## Development Workflow

### Initial Setup

```bash
# Clone and install
git clone https://github.com/pixelworxio/livewire-workflows.git
cd livewire-workflows
composer install

# Run tests
composer test

# Static analysis
./vendor/bin/phpstan analyse

# Format code
./vendor/bin/pint
```

### Branch Strategy

1. Preserve unrelated working-tree changes.
2. Make changes following coding standards.
3. Write or update tests for all changes.
4. Run the release checks.
5. Do not stage, commit, push, tag, or publish unless the user explicitly approves that action.

### Pre-Commit Checklist

- [ ] All tests pass: `composer test`
- [ ] PHPStan analysis passes: `./vendor/bin/phpstan analyse`
- [ ] Code formatted: `./vendor/bin/pint`
- [ ] New tests added for changes
- [ ] Documentation updated (README, STATE_MANAGEMENT, etc.)
- [ ] CHANGELOG.md updated

---

## Coding Standards

### General Principles

1. **PSR-12 Compliance**: Follow PHP coding standards
2. **SOLID Principles**: Keep classes focused, follow SRP and DIP
3. **DRY**: Don't repeat yourself
4. **Type Safety**: Use strict types and proper type hints
5. **Immutability**: Favor immutable objects where appropriate
6. **Early Returns**: Use guard clauses

### File Structure

```php
<?php

declare(strict_types=1);  // ALWAYS include

namespace Pixelworxio\LivewireWorkflows\Example;

use Illuminate\Http\Request;  // Imports sorted alphabetically

/**
 * Clear class-level docblock explaining purpose.
 */
class ExampleClass
{
    // Constructor with promoted properties (PHP 8+)
    public function __construct(
        protected readonly SomeDependency $dependency,
    ) {}

// Public methods with full type hints
    public function exampleMethod(Request $request): string
    {
        // Use early returns for guard clauses
        if (! $this->shouldProceed($request)) {
            return 'early';
        }

        // Favor readability over brevity
        $result = $this->dependency->process($request);

        return $result;
    }

    // Protected/private methods at bottom
    protected function shouldProceed(Request $request): bool
    {
        return $request->has('required_field');
    }
}
```

### Key Conventions

#### Strict Types
**ALWAYS** start files with:
```php
<?php

declare(strict_types=1);
```

#### Constructor Property Promotion
Use PHP 8 promoted properties:
```php
public function __construct(
    protected readonly WorkflowEngine $engine,
    protected readonly WorkflowStateRepository $repository,
) {}
```

#### Readonly Properties
Use `readonly` for immutability when possible:
```php
public function __construct(
    public readonly string $flow,
    public readonly string $entryRouteName,
) {}
```

#### Named Arguments
Use named arguments for clarity (especially in DSL):
```php
->entersAt(name: 'onboarding.start', path: '/onboarding')
```

#### Validation at Construction
Validate in `__construct()` or `build()`:
```php
public function __construct(
    public readonly string $flow,
    public readonly string $componentClass,
) {
    if (! class_exists($componentClass)) {
        throw new InvalidWorkflowConfigurationException(
            "Component class {$componentClass} does not exist."
        );
    }
}
```

### Naming Conventions

- **Classes**: PascalCase (`WorkflowEngine`)
- **Methods**: camelCase (`nextStep`)
- **Variables**: camelCase (`$currentStep`)
- **Constants**: UPPER_SNAKE_CASE (`HISTORY_MODE_STACK`)
- **Route Names**: snake.case with dots (`onboarding.verify-email`)
- **Step Keys**: kebab-case (`verify-email`)
- **Workflow Names**: kebab-case (`employee-onboarding`)

### Route Conventions

**Auto-Generated Pattern**:
- Entry: `{path}` → `{flow}.start`
- Steps: `{path}/{step-key}` → `{flow}.{step-key}`

**Example**:
```php
->entersAt(name: 'checkout.start', path: '/checkout')
->step('shipping')  // → /checkout/shipping → checkout.shipping
```

**Dynamic Routes with Parameters**:

Workflows support dynamic route parameters and route model binding:

```php
->entersAt(name: 'checkout.start', path: '/user/{user}/product/{product}')
->step('shipping')  // → /user/{user}/product/{product}/shipping
```

**Key Implementation Details**:
- Route parameters are parsed from `entryPath` and stored in `WorkflowDefinition::$routeParameters`
- Parameters are automatically extracted from the current request and passed through navigation
- `continue()` and `back()` methods preserve route parameters seamlessly
- Supports Laravel's route model binding syntax: `{user:id}`, `{product:slug}`, etc.
- `WorkflowDefinition::parseRouteParameters()` extracts parameter names using regex
- `WorkflowResolver::extractRouteParameters()` filters current route parameters to only include workflow-defined parameters
- `InteractsWithWorkflows::extractWorkflowRouteParameters()` provides the same functionality for Livewire components

**Example with Model Binding**:
```php
Workflow::flow('order-review')
    ->entersAt(name: 'order-review.start', path: '/organization/{organization:slug}/order/{order:id}')
    ->finishesAt('dashboard')
    ->step('review')
        ->goTo(ReviewOrder::class)
        ->unlessPasses(OrderNotReviewed::class)
        ->order(10);
```

---

## Testing Conventions

### Test Framework: Pest v5

### Test Structure

```php
<?php

declare(strict_types=1);

use Pixelworxio\LivewireWorkflows\Facades\Workflow;

beforeEach(function () {
    // Setup code runs before each test
    Workflow::flow('test')
        ->entersAt(name: 'test.start', path: '/test')
        ->finishesAt('done')
        ->step('step-one')
            ->goTo(TestStepOneComponent::class)
            ->unlessPasses(TestStepOneGuard::class)
            ->order(10);
});

test('descriptive test name in sentence case', function () {
    // Arrange
    TestStepOneGuard::$shouldPass = false;

    // Act
    $result = workflow('test')->nextRouteNameFor(request());

    // Assert
    expect($result)->toBe('test.step-one');
});
```

### Test Organization

- **Feature Tests** (`tests/Feature/`): Integration and end-to-end tests
- **Unit Tests** (`tests/Unit/`): Isolated business logic tests
- **Support** (`tests/Support/`): Test fixtures, stubs, helpers

### Test Fixtures

**Test Components** (e.g., `TestStepOneComponent.php`):
```php
class TestStepOneComponent extends Component
{
    use InteractsWithWorkflows;

    public function render()
    {
        return '<div>Test Step</div>';
    }
}
```

**Test Guards** (e.g., `TestStepOneGuard.php`):
```php
class TestStepOneGuard implements GuardContract
{
    public static bool $shouldPass = false;

    public function passes(Request $request): bool
    {
        return self::$shouldPass;
    }

public function onEnter(Request $request): void {}
    public function onExit(Request $request): void {}
    public function onPass(Request $request): void {}
    public function onFail(Request $request): void {}
}
```

### TestCase Configuration (`tests/TestCase.php`)

**Key Setup**:
- Extends `Orchestra\Testbench\TestCase`
- Registers `LivewireServiceProvider` and `LivewireWorkflowsServiceProvider`
- Configures in-memory SQLite for tests
- Provides `loadWorkflowMigrations()` helper

### Running Tests

```bash
# All tests
composer test
# or
./vendor/bin/pest

# Specific file
./vendor/bin/pest tests/Unit/WorkflowRegistrarTest.php

# With coverage
./vendor/bin/pest --coverage

# With coverage minimum
./vendor/bin/pest --coverage --min=80
```

### Test Coverage Goals

- Maintain **>80%** code coverage
- Test edge cases and error conditions
- Feature tests for integration scenarios
- Unit tests for business logic

### Architecture Tests (`tests/ArchTest.php`)

```php
arch('no debug functions in production code')
    ->expect('Pixelworxio\LivewireWorkflows')
    ->not->toUse(['dd', 'dump', 'ray']);
```

---

## Key Concepts

### 1. Positive Guard Semantics

**Critical Understanding**: Guards use **positive semantics** - opposite of what you might expect.

| Guard Result | Step Behavior |
|--------------|---------------|
| `passes() = true` | **Skip** this step |
| `passes() = false` | **Show** this step |

**Rationale**: Natural language with `unlessPasses()`:
```php
->unlessPasses(EmailVerifiedGuard::class)
// "Show step UNLESS guard passes"
```

**Example**:
```php
class ProfileCompleteGuard implements GuardContract
{
    public function passes(Request $request): bool
    {
        // Return true when profile IS complete → skip step
        return $request->user()->profile_completed;
    }
}
```

### 2. Workflow Navigation Flow

1. User visits **entry route** (`/onboarding`)
2. Package evaluates guards in **order**
3. Redirects to **first failing guard's step**
4. User completes step, calls `$this->continue('onboarding')`
5. Re-evaluates from entry → **next unmet step** or **finish**

### 3. History Modes

```php
->historyMode('none')   // No tracking (default)
->historyMode('stack')  // Full history stack for back navigation
```

**Stack Mode**: Enables `$this->back()` to return to previous steps.

### 4. State Scoping

**State is scoped per**:
- Workflow name
- User key (auth ID > session ID > guest fingerprint)

**No cross-workflow sharing** - by design for data isolation.

### 5. Auto-Route Registration

Routes are automatically registered from DSL:

```php
Workflow::flow('checkout')
    ->entersAt(name: 'checkout.start', path: '/checkout')
    ->step('shipping')  // Auto: /checkout/shipping
    ->step('payment')   // Auto: /checkout/payment
```

**Generated Routes**:
- `checkout.start` → `GET /checkout`
- `checkout.shipping` → `GET /checkout/shipping`
- `checkout.payment` → `GET /checkout/payment`

### 6. State Management with Attributes

**Setting Workflow Name**:
```php
use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName;

#[WorkflowName('onboarding')]
class ProfileStep extends Component
{
    use InteractsWithWorkflows;

    // No need to set protected ?string $workflowName = 'onboarding';
}
```

The `#[WorkflowName]` attribute eliminates the need to manually set `$workflowName` property. The trait automatically reads the attribute during boot. If no attribute is present, it falls back to auto-detection from the route name.

**Automatic Persistence**:
```php
use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName;
use Pixelworxio\LivewireWorkflows\Attributes\WorkflowState;

#[WorkflowName('onboarding')]
class ProfileStep extends Component
{
    use InteractsWithWorkflows;

    #[WorkflowState]
    public ?string $email = null;

    #[WorkflowState(encrypt: true)]
    public ?string $password = null;

#[WorkflowState(namespace: 'profile')]
    public ?string $name = null;
}
```

Properties are:
- **Hydrated** on mount (from repository)
- **Synced immediately** when changed via wire:model or `$this->set()`
- **Synced on dehydrate** as fallback to catch all changes
- **Scoped** to workflow and user

**Dual-Layer Persistence**:
- **Layer 1**: Proactive syncing via `updatedInteractsWithWorkflows()` - catches wire:model and explicit property updates
- **Layer 2**: Dehydration fallback via `dehydrateInteractsWithWorkflows()` - catches direct assignments in methods

This ensures state is persisted reliably throughout the component lifecycle, not just at the end of the request.

### 7. Lazy Builder Finalization

Builders are stored as "pending" and finalized lazily:
- On explicit `get()` call
- In destructor if `finishesAt()` was called
- Allows flexible definition order

### 8. Convention Over Configuration

**Default Conventions**:
- Route names: `{flow}.{step-key}`
- Route paths: `{entryPath}/{step-key}`
- Session keys: `workflows.{flow}.{userKey}.{suffix}`
- Database table: `workflow_states` (fixed)
- Middleware: `['web']` (configurable)

---

## Common Tasks

### Adding a New Feature

1. Write failing tests first when practical.
2. Implement the feature following coding standards.
3. Ensure tests pass: `composer test`.
4. Run static analysis: `./vendor/bin/phpstan analyse`.
5. Format code: `./vendor/bin/pint`.
6. Update relevant documentation and `CHANGELOG.md`.
7. Leave the work uncommitted for review unless the user explicitly asks for Git actions.

### Adding a New Command

1. Create command class in `src/Commands/`
2. Register in `LivewireWorkflowsServiceProvider::registerCommands()`
3. Add stub template in `stubs/` if needed
4. Write tests in `tests/Feature/Commands/`
5. Update README with command documentation

### Adding a New Event

1. Create event class in `src/Events/`
2. Fire event in appropriate location (likely `WorkflowEngine`)
3. Document in README
4. Add test coverage

### Adding a New State Repository

1. Implement `WorkflowStateRepository` contract
2. Add to `StateRepositories/` directory
3. Register binding in service provider
4. Add configuration option
5. Write tests
6. Document in STATE_MANAGEMENT.md

### Modifying the DSL

1. Update builder classes (`FlowBuilder`, `StepBuilder`)
2. Update definition DTOs if needed
3. Ensure backward compatibility or document breaking change
4. Write comprehensive tests
5. Update README examples
6. Update UPGRADE.md if breaking

### Debugging Workflow Issues

**Enable Workflow Debugging**:
```php
// In routes/workflows.php or component
dd(workflow('onboarding')->progressFor(request()));

// Check current state
dd(app(WorkflowStateRepository::class)->getCurrentStep('onboarding', 'user-123'));

// See all state
dd($this->allWorkflowState());
```

**Common Issues**:
- Guard logic inverted (remember: `passes() = true` → skip)
- Missing `workflowName` property in component
- Workflow not registered in `routes/workflows.php`
- Route middleware blocking access

---

## Troubleshooting

### Guards Not Working

**Problem**: Steps always shown/skipped incorrectly

**Solution**:
- Remember positive semantics: `passes() = true` → SKIP
- Check guard class exists and implements `GuardContract`
- Verify guard is properly registered: `->unlessPasses(YourGuard::class)`
- Debug with `dd()` in guard's `passes()` method

### State Not Persisting

**Problem**: Component properties not saving

**Solution**:
- Ensure `workflowName` is set in component
- Verify `#[WorkflowState]` attribute is on public properties
- Check repository configuration in `config/livewire-workflows.php`
- For eloquent: ensure migration ran (`workflow_states` table exists)
- Verify `InteractsWithWorkflows` trait is used

### State Not Hydrating

**Problem**: Properties remain null on mount

**Solution**:
- Confirm `#[WorkflowState]` attribute exists
- Ensure `bootInteractsWithWorkflows()` is called (automatic with trait)
- Check property visibility (public properties work best)
- Verify data exists in repository

### Routes Not Registered

**Problem**: 404 errors on workflow routes

**Solution**:
- Ensure `routes/workflows.php` exists and is loaded
- Check `WorkflowRegistrar` is registering workflow
- Verify `RouteRegistrar` is being called in service provider
- Run `php artisan route:list` to see registered routes
- Check middleware isn't blocking

### Workflow Not Redirecting

**Problem**: Stays on entry route instead of redirecting to first step

**Solution**:
- Verify at least one guard returns `false` (step should be shown)
- Check all guards are returning `true` (all steps complete)
- Ensure `finishRoute` names a registered route
- Debug with: `dd(workflow('name')->nextRouteNameFor(request()))`

### Back Navigation Not Working

**Problem**: `$this->back()` does nothing

**Solution**:
- Ensure `historyMode('stack')` is set on workflow
- Verify history is being tracked (check repository)
- Confirm previous step exists in history
- Check you're passing correct flow name and current key

### Encryption Errors

**Problem**: Error when encrypting state

**Solution**:
- Ensure `APP_KEY` is set in `.env`
- Verify Laravel's encryption works: `Crypt::encrypt('test')`
- Check encrypted values aren't being double-encrypted

### Tests Failing After Changes

**Problem**: Tests fail after modifying code

**Solution**:
- Check breaking changes in DTOs (immutable properties)
- Verify test fixtures still valid (guards, components)
- Clear test database: `rm database/database.sqlite` (if using)
- Reset static properties in guards between tests
- Use `beforeEach()` to reset state

### PHPStan Errors

**Problem**: Static analysis failing

**Solution**:
- Add proper type hints to all methods
- Use PHPDoc comments for complex types
- Fix mixed types where possible
- Add to baseline if necessary: `./vendor/bin/phpstan analyse --generate-baseline`

---

## API Reference Quick Guide

### Workflow DSL

```php
Workflow::flow(string $name)
    ->entersAt(name: string, path: string)
    ->finishesAt(string $routeName)
    ->historyMode('none' | 'stack')
    ->step(string $key)
        ->goTo(string $componentClass)
        ->unlessPasses(string $guardClass)
        ->order(int $order);
```

### Livewire Component Methods

```php
use InteractsWithWorkflows;

// Navigation
$this->continue(string $flow): void
$this->back(string $flow, string $currentKey): void
$this->syncState(): void

// State management
$this->getWorkflowState(string $key, mixed $default = null): mixed
$this->putWorkflowState(string $key, mixed $value): void
$this->hasWorkflowState(string $key): bool
$this->forgetWorkflowState(string $key): void
$this->clearWorkflowState(?string $namespace = null): void
$this->allWorkflowState(): array
```

### Helper Functions

```php
// Get workflow instance
workflow(string $flow)->redirect(Request $request, ?string $doneRoute = null)
workflow(string $flow)->nextRouteNameFor(Request $request, ?string $doneRoute = null)
workflow(string $flow)->previousRouteNameFor(string $currentKey, Request $request)
workflow(string $flow)->progressFor(Request $request)
```

### Progress Tracking

```php
$progress = workflow('onboarding')->progressFor($request);
// Returns:
[
    'total' => 3,
    'completed' => 1,
    'remaining' => 2,
    'percentage' => 33.33,
    'current_step' => 'verify-email',
    'next_step' => 'profile',
    'is_complete' => false,
]
```

### Artisan Commands

```bash
# Installation
php artisan workflows:install
php artisan workflows:upgrade

# Generation
php artisan make:workflow {name}
php artisan make:workflow-guard {name}
php artisan make:workflow-step {workflow} {step} [--component] [--guard] [--order]

# Validation
php artisan workflows:scan
```

---

## Configuration Reference

### Package Configuration (`config/livewire-workflows.php`)

```php
return [
    // State persistence: 'null', 'session', or 'eloquent'
    'repository' => env('WORKFLOWS_REPOSITORY', 'eloquent'),

    // Middleware applied to all workflow routes
    'middleware' => ['web'],

    'resume' => [
        'default_expires_minutes' => env('WORKFLOWS_RESUME_EXPIRES', 1440),
    ],
];
```

### State Repository Options

| Repository | Use Case | Persistence |
|------------|----------|-------------|
| `null` | Stateless workflows | None |
| `session` | Short-lived anonymous flows (deprecated) | Session lifetime |
| `eloquent` | Authenticated users, production | Database |

---

## Important Architectural Decisions

### Why Positive Guard Semantics?

Natural language with `unlessPasses()`: "Show step UNLESS guard passes"

### Why Immutable Definitions?

Thread safety, predictability, prevents accidental mutations

### Why Lazy Builder Finalization?

Allows flexible definition order, auto-cleanup in destructor

### Why State Scoping?

Data isolation, security, simplicity (no cross-workflow contamination)

### Why Auto-Route Registration?

Zero boilerplate, DRY principle, convention over configuration

### Why Events?

Extensibility without modifying package (analytics, logging, etc.)

---

## Resources

- **README.md**: User-facing documentation
- **STATE_MANAGEMENT.md**: Comprehensive state management guide
- **CONTRIBUTING.md**: Contribution guidelines and standards
- **CHANGELOG.md**: Version history
- **UPGRADE.md**: Breaking changes and upgrade instructions
- **GitHub Issues**: Bug reports and feature requests
- **Tests**: Living documentation of expected behavior

---

## Quick Reference for AI Assistants

### When Making Changes:

1. ✅ **ALWAYS** include `declare(strict_types=1);`
2. ✅ **ALWAYS** write tests for new features
3. ✅ **ALWAYS** update relevant documentation
4. ✅ **ALWAYS** run `composer test` and `phpstan` before committing
5. ✅ **ALWAYS** use readonly properties for immutability
6. ✅ **ALWAYS** validate in constructors/builders
7. ✅ Use constructor property promotion
8. ✅ Use named arguments in DSL
9. ✅ Follow PSR-12 standards
10. ✅ Remember: `passes() = true` means **SKIP** the step

### When Adding Features:

1. Check if it breaks existing functionality
2. Ensure backward compatibility or document breaking change
3. Add to CHANGELOG.md
4. Update UPGRADE.md if breaking
5. Consider event-driven approach for extensibility
6. Prefer composition over inheritance
7. Keep classes focused (SRP)

### When Fixing Bugs:

1. Write failing test first (reproduce bug)
2. Fix the bug
3. Ensure test passes
4. Check for similar issues elsewhere
5. Add regression test
6. Document fix in CHANGELOG.md

---

## Version Information

This AGENTS.md was created for version: **v1.x** (check CHANGELOG.md for current version)

Last updated: 2025-11-14

---

*This document is maintained for AI assistants working on the Livewire Workflows codebase. Keep it updated as the project evolves.*

AGENTS.md - AI Assistant Guide for Livewire Workflows

This document provides AI assistants with comprehensive information about the Livewire Workflows codebase structure, development workflows, and key conventions.

Table of Contents

  1. Project Overview
  2. Architecture & Design Patterns
  3. Directory Structure
  4. Core Components
  5. Development Workflow
  6. Coding Standards
  7. Testing Conventions
  8. Key Concepts
  9. Common Tasks
  10. Troubleshooting

Project Overview

Livewire Workflows is a Laravel package that enables developers to build multi-step workflows with zero boilerplate. It provides an expressive DSL for defining complex user journeys (onboarding, checkouts, surveys) with automatic route registration, guard-based navigation, state persistence, and full Livewire integration.

Key Information

  • Package Name: pixelworxio/livewire-workflows
  • Namespace: Pixelworxio\LivewireWorkflows
  • Requirements: PHP 8.3+, Laravel 11.x/12.x/13.x, Livewire 3.x/4.x
  • License: MIT
  • Test Framework: Pest v5
  • Code Quality: PHPStan (level 2), Laravel Pint

Primary Features

  1. Auto-generated routes from DSL definitions
  2. Guard-based navigation with positive semantics
  3. State persistence (session, database, or null)
  4. History tracking for back navigation
  5. Progress tracking API
  6. Event-driven extensibility
  7. Signed resume links for Eloquent-backed workflows
  8. Livewire 3 and 4 integration

Architecture & Design Patterns

Design Patterns Used

1. Fluent Builder Pattern

  • Classes: FlowBuilder, StepBuilder
  • Purpose: Provide expressive DSL for workflow definition
  • Result: Immutable DTOs (WorkflowDefinition, StepDefinition)
Workflow::flow('onboarding')
    ->entersAt(name: 'onboarding.start', path: '/onboarding')
    ->finishesAt('dashboard')
    ->step('verify-email')
        ->goTo(VerifyEmail::class)
        ->unlessPasses(EmailVerifiedGuard::class)
        ->order(10);

2. Repository Pattern

  • Contract: WorkflowStateRepository
  • Implementations: SessionWorkflowStateRepository, EloquentWorkflowStateRepository, NullStateRepository
  • Purpose: Abstract state persistence mechanism

3. Strategy Pattern

  • Usage: Swap state repositories via configuration
  • Binding: Service container resolves correct implementation

4. Pipeline Pattern

  • Location: WorkflowEngine
  • Purpose: Sequential guard evaluation using Laravel's Pipeline

5. Facade Pattern

  • Class: Workflow facade
  • Target: WorkflowRegistrar
  • Purpose: Static access to workflow registration

6. Registry Pattern

  • Class: WorkflowRegistrar
  • Purpose: Central registry of all workflows with lazy finalization

7. DTO Pattern

  • Classes: WorkflowDefinition, StepDefinition
  • Characteristics: Immutable, validated on construction

8. Trait Composition

  • Trait: InteractsWithWorkflows
  • Purpose: Reusable functionality for Livewire components

9. Observer Pattern (Events)

  • Events: WorkflowAdvanced, WorkflowCompleted, WorkflowStateClearing
  • Purpose: Extensibility and analytics hooks

Directory Structure

livewire-workflows/
├── config/
│   └── livewire-workflows.php          # Package configuration
├── database/
│   └── migrations/                     # Database migration stubs
├── resources/                          # Package resources (images, etc.)
├── src/
│   ├── Attributes/                     # PHP 8 Attributes
│   │   ├── WorkflowName.php           # Workflow name declaration attribute
│   │   ├── WorkflowState.php          # State persistence attribute
│   │   └── WorkflowStep.php           # Step metadata attribute
│   ├── Commands/                       # Artisan commands
│   │   ├── MakeWorkflowCommand.php
│   │   ├── MakeWorkflowStepCommand.php
│   │   ├── MakeWorkflowGuardCommand.php
│   │   ├── WorkflowsInstallCommand.php
│   │   └── WorkflowsScanCommand.php
│   │   ├── WorkflowsAuditCommand.php
│   │   └── WorkflowsUpgradeCommand.php
│   ├── Contracts/                      # Interfaces
│   │   ├── GuardContract.php
│   │   └── WorkflowStateRepository.php
│   ├── Events/                         # Laravel Events
│   │   ├── WorkflowAdvanced.php
│   │   ├── WorkflowCompleted.php
│   │   └── WorkflowStateClearing.php
│   ├── Exceptions/                     # Custom exceptions
│   │   ├── WorkflowNotFoundException.php
│   │   └── InvalidWorkflowConfigurationException.php
│   ├── Facades/                        # Laravel Facades
│   │   └── Workflow.php
│   ├── Http/Controllers/               # HTTP Controllers
│   │   └── WorkflowEntryController.php
│   │   └── WorkflowResumeController.php
│   ├── Livewire/Concerns/             # Livewire Traits
│   │   └── InteractsWithWorkflows.php
│   ├── Registrar/                      # DSL Implementation
│   │   ├── WorkflowRegistrar.php      # Central registry
│   │   ├── FlowBuilder.php            # Flow builder
│   │   └── StepBuilder.php            # Step builder
│   ├── StateRepositories/             # State Persistence
│   │   ├── NullStateRepository.php
│   │   ├── SessionWorkflowStateRepository.php
│   │   └── EloquentWorkflowStateRepository.php
│   ├── Support/                       # Core Support Classes
│   │   ├── WorkflowDefinition.php     # Workflow DTO
│   │   ├── StepDefinition.php         # Step DTO
│   │   ├── WorkflowEngine.php         # Core execution logic
│   │   ├── WorkflowResolver.php       # High-level API
│   │   ├── WorkflowInstance.php       # Fluent workflow interface
│   │   └── RouteRegistrar.php         # Auto-route registration
│   ├── LivewireWorkflowsServiceProvider.php
│   └── helpers.php                    # Global helper functions
├── stubs/                             # Code generation templates
│   └── workflows.php.stub
├── tests/
│   ├── Feature/                       # Integration tests
│   ├── Unit/                          # Unit tests
│   ├── Support/                       # Test fixtures
│   ├── TestCase.php                   # Base test case
│   ├── Pest.php                       # Pest configuration
│   └── ArchTest.php                   # Architecture tests
├── CHANGELOG.md                       # Version history
├── CONTRIBUTING.md                    # Contribution guidelines
├── README.md                          # User documentation
├── STATE_MANAGEMENT.md                # State management guide
├── UPGRADE.md                         # Upgrade guide
├── composer.json
├── phpunit.xml.dist
└── phpstan.neon.dist

Core Components

1. Workflow Engine (src/Support/WorkflowEngine.php)

Purpose: Core workflow execution logic

Responsibilities:

  • Evaluates guards using Laravel Pipeline
  • Determines next/previous steps
  • Handles workflow completion
  • Calculates progress tracking
  • Fires workflow events
  • Manages state clearing

Key Methods:

  • nextStep(WorkflowDefinition $workflow, Request $request): ?string
  • previousStep(WorkflowDefinition $workflow, string $currentKey, Request $request): ?string
  • advanceTo(WorkflowDefinition $workflow, string $toKey, Request $request): void
  • complete(WorkflowDefinition $workflow, Request $request): void
  • progress(WorkflowDefinition $workflow, Request $request): array

2. Workflow Registrar (src/Registrar/WorkflowRegistrar.php)

Purpose: Central registry for all workflows

Responsibilities:

  • Manages pending builders and finalized definitions
  • Lazy finalization pattern
  • Provides workflow lookup

Key Methods:

  • flow(string $name): FlowBuilder - Start a new workflow definition
  • get(string $name): WorkflowDefinition - Get finalized workflow
  • has(string $name): bool - Check if workflow exists
  • all(): array - Get all workflows

3. Flow Builder (src/Registrar/FlowBuilder.php)

Purpose: Fluent builder for workflow definitions

Key Methods:

  • entersAt(string $name, string $path): self - Define entry route
  • finishesAt(string $routeName): self - Define completion route
  • historyMode(string $mode): self - Set history mode ('none' or 'stack')
  • step(string $key): StepBuilder - Add a step and return StepBuilder
  • build(): WorkflowDefinition - Finalize and validate

Auto-Build: Builder auto-builds in destructor if finishesAt() was called.

4. Step Builder (src/Registrar/StepBuilder.php)

Purpose: Fluent builder for individual steps

Key Methods:

  • goTo(string $componentClass): self - Set Livewire component
  • unlessPasses(string $guardClass): self - Set guard class
  • order(int $order): self - Set execution order
  • Proxies unknown methods to FlowBuilder for chaining

5. Workflow Definition (src/Support/WorkflowDefinition.php)

Purpose: Immutable DTO for workflow configuration

Properties:

  • flow - Workflow identifier
  • entryRouteName, entryPath - Entry route details
  • finishRoute - Completion route
  • historyMode - History tracking mode
  • steps - Array of StepDefinitions

Key Methods:

  • getOrderedSteps(): array - Steps sorted by order
  • getPreviousStep(string $currentKey): ?StepDefinition
  • getStepRouteName(string $stepKey): string

6. InteractsWithWorkflows Trait (src/Livewire/Concerns/InteractsWithWorkflows.php)

Purpose: Core trait for Livewire components

Lifecycle Hooks:

  • bootInteractsWithWorkflows() - Read WorkflowName attribute or auto-detect workflow name from route
  • mountInteractsWithWorkflows() - Hydrate state from repository
  • updatedInteractsWithWorkflows(string $propertyName, mixed $value) - Proactively sync state when properties change
  • dehydrateInteractsWithWorkflows() - Sync state to repository as fallback

State Persistence Strategy: The trait now uses a dual-layer persistence approach for maximum reliability:

  1. Proactive Syncing - Properties marked with #[WorkflowState] are immediately persisted when changed via Livewire's updated() hook

    • Ensures state is saved even if dehydration fails
    • Provides immediate persistence for wire:model bindings
    • Tracks dirty properties to optimize syncing
  2. Dehydration Fallback - The dehydrate hook still runs to catch any property changes not captured by the updated hook

    • Handles direct property assignments in methods
    • Ensures complete state snapshot at end of request
    • Backward compatible with existing code

This approach addresses edge cases where:

  • Request fails before dehydration completes
  • Properties are modified outside Livewire's tracking
  • Complex component interactions require immediate persistence

Navigation Methods:

  • continue(string $flow): void - Move to next step
  • back(string $flow, string $currentKey): void - Return to previous step
  • syncState(): void - Manually persist state

State Methods:

  • getWorkflowState(string $key, mixed $default = null): mixed
  • putWorkflowState(string $key, mixed $value): void
  • hasWorkflowState(string $key): bool
  • forgetWorkflowState(string $key): void
  • clearWorkflowState(?string $namespace = null): void
  • allWorkflowState(): array

User Key Resolution:

  1. Authenticated user ID (if logged in)
  2. Session ID (if session available)
  3. Guest fingerprint: guest-{md5(IP + UserAgent)}

7. Guard Contract (src/Contracts/GuardContract.php)

Purpose: Interface for step guards

Critical Understanding - Positive Semantics:

  • passes() = true → Step is SKIPPED
  • passes() = false → Step is SHOWN
  • Used with unlessPasses() in DSL: "Show step UNLESS guard passes"

Methods:

  • passes(Request $request): bool - Determine if step can be skipped
  • onEnter(Request $request): void - Hook when entering step
  • onExit(Request $request): void - Hook when exiting step
  • onPass(Request $request): void - Hook when guard passes
  • onFail(Request $request): void - Hook when guard fails

8. State Repositories

WorkflowStateRepository Contract

Methods:

  • getCurrentStep(string $flow, string|int|null $userKey): ?string
  • setCurrentStep(string $flow, string|int|null $userKey, string $stepKey): void
  • getHistory(string $flow, string|int|null $userKey): array
  • pushHistory(string $flow, string|int|null $userKey, string $stepKey): void
  • popHistory(string $flow, string|int|null $userKey): ?string
  • clear(string $flow, string|int|null $userKey): void
  • getMetadata(string $flow, string|int|null $userKey): array
  • setMetadata(string $flow, string|int|null $userKey, array $metadata): void
  • getState(string $flow, string|int|null $userKey, string $key): mixed
  • setState(string $flow, string|int|null $userKey, string $key, mixed $value): void
  • hasState(string $flow, string|int|null $userKey, string $key): bool
  • forgetState(string $flow, string|int|null $userKey, string $key): void
  • clearState(string $flow, string|int|null $userKey, ?string $namespace = null): void
  • getAllState(string $flow, string|int|null $userKey): array

SessionWorkflowStateRepository

  • Session-based storage
  • Keys: workflows.{flow}.{userKey}.{suffix}
  • Suitable for guests and simple flows

EloquentWorkflowStateRepository

  • Database-backed (workflow_states table)
  • Production-ready with persistence
  • Unique constraint on [workflow_name, user_key]

NullStateRepository

  • No-op implementation for stateless workflows

9. Attributes

WorkflowName Attribute (src/Attributes/WorkflowName.php)

Purpose: Declare the workflow name on a Livewire component class

Usage:

use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName;

#[WorkflowName('onboarding')]
class VerifyEmailStep extends Component
{
    use InteractsWithWorkflows;
    // No need to set protected ?string $workflowName = 'onboarding';
}

Benefits:

  • Eliminates boilerplate of setting $workflowName property
  • Makes workflow association explicit and declarative
  • Auto-detected during bootInteractsWithWorkflows()
  • Falls back to route-based auto-detection if not present

When to Use:

  • When you want explicit workflow association
  • When component might be used outside workflow routes
  • When you want to avoid route-based auto-detection

WorkflowState Attribute (src/Attributes/WorkflowState.php)

Purpose: Mark component properties for automatic state persistence

Parameters:

  • encrypt (bool) - Whether to encrypt the value in storage (default: false)
  • namespace (string|null) - Optional namespace for grouping state keys (default: null)

Usage: See "State Management with Attributes" section above

WorkflowStep Attribute (src/Attributes/WorkflowStep.php)

Purpose: Mark Livewire components as workflow steps (for validation/scanning)

Note: Not used for auto-registration (DSL is source of truth), but can be used by the workflows:scan command for validation


Development Workflow

Initial Setup

# Clone and install
git clone https://github.com/pixelworxio/livewire-workflows.git
cd livewire-workflows
composer install

# Run tests
composer test

# Static analysis
./vendor/bin/phpstan analyse

# Format code
./vendor/bin/pint

Branch Strategy

  1. Preserve unrelated working-tree changes.
  2. Make changes following coding standards.
  3. Write or update tests for all changes.
  4. Run the release checks.
  5. Do not stage, commit, push, tag, or publish unless the user explicitly approves that action.

Pre-Commit Checklist

  • All tests pass: composer test
  • PHPStan analysis passes: ./vendor/bin/phpstan analyse
  • Code formatted: ./vendor/bin/pint
  • New tests added for changes
  • Documentation updated (README, STATE_MANAGEMENT, etc.)
  • CHANGELOG.md updated

Coding Standards

General Principles

  1. PSR-12 Compliance: Follow PHP coding standards
  2. SOLID Principles: Keep classes focused, follow SRP and DIP
  3. DRY: Don't repeat yourself
  4. Type Safety: Use strict types and proper type hints
  5. Immutability: Favor immutable objects where appropriate
  6. Early Returns: Use guard clauses

File Structure

<?php

declare(strict_types=1);  // ALWAYS include

namespace Pixelworxio\LivewireWorkflows\Example;

use Illuminate\Http\Request;  // Imports sorted alphabetically

/**
 * Clear class-level docblock explaining purpose.
 */
class ExampleClass
{
    // Constructor with promoted properties (PHP 8+)
    public function __construct(
        protected readonly SomeDependency $dependency,
    ) {}

// Public methods with full type hints
    public function exampleMethod(Request $request): string
    {
        // Use early returns for guard clauses
        if (! $this->shouldProceed($request)) {
            return 'early';
        }

        // Favor readability over brevity
        $result = $this->dependency->process($request);

        return $result;
    }

    // Protected/private methods at bottom
    protected function shouldProceed(Request $request): bool
    {
        return $request->has('required_field');
    }
}

Key Conventions

Strict Types

ALWAYS start files with:

<?php

declare(strict_types=1);

Constructor Property Promotion

Use PHP 8 promoted properties:

public function __construct(
    protected readonly WorkflowEngine $engine,
    protected readonly WorkflowStateRepository $repository,
) {}

Readonly Properties

Use readonly for immutability when possible:

public function __construct(
    public readonly string $flow,
    public readonly string $entryRouteName,
) {}

Named Arguments

Use named arguments for clarity (especially in DSL):

->entersAt(name: 'onboarding.start', path: '/onboarding')

Validation at Construction

Validate in __construct() or build():

public function __construct(
    public readonly string $flow,
    public readonly string $componentClass,
) {
    if (! class_exists($componentClass)) {
        throw new InvalidWorkflowConfigurationException(
            "Component class {$componentClass} does not exist."
        );
    }
}

Naming Conventions

  • Classes: PascalCase (WorkflowEngine)
  • Methods: camelCase (nextStep)
  • Variables: camelCase ($currentStep)
  • Constants: UPPER_SNAKE_CASE (HISTORY_MODE_STACK)
  • Route Names: snake.case with dots (onboarding.verify-email)
  • Step Keys: kebab-case (verify-email)
  • Workflow Names: kebab-case (employee-onboarding)

Route Conventions

Auto-Generated Pattern:

  • Entry: {path} → {flow}.start
  • Steps: {path}/{step-key} → {flow}.{step-key}

Example:

->entersAt(name: 'checkout.start', path: '/checkout')
->step('shipping')  // → /checkout/shipping → checkout.shipping

Dynamic Routes with Parameters:

Workflows support dynamic route parameters and route model binding:

->entersAt(name: 'checkout.start', path: '/user/{user}/product/{product}')
->step('shipping')  // → /user/{user}/product/{product}/shipping

Key Implementation Details:

  • Route parameters are parsed from entryPath and stored in WorkflowDefinition::$routeParameters
  • Parameters are automatically extracted from the current request and passed through navigation
  • continue() and back() methods preserve route parameters seamlessly
  • Supports Laravel's route model binding syntax: {user:id}, {product:slug}, etc.
  • WorkflowDefinition::parseRouteParameters() extracts parameter names using regex
  • WorkflowResolver::extractRouteParameters() filters current route parameters to only include workflow-defined parameters
  • InteractsWithWorkflows::extractWorkflowRouteParameters() provides the same functionality for Livewire components

Example with Model Binding:

Workflow::flow('order-review')
    ->entersAt(name: 'order-review.start', path: '/organization/{organization:slug}/order/{order:id}')
    ->finishesAt('dashboard')
    ->step('review')
        ->goTo(ReviewOrder::class)
        ->unlessPasses(OrderNotReviewed::class)
        ->order(10);

Testing Conventions

Test Framework: Pest v5

Test Structure

<?php

declare(strict_types=1);

use Pixelworxio\LivewireWorkflows\Facades\Workflow;

beforeEach(function () {
    // Setup code runs before each test
    Workflow::flow('test')
        ->entersAt(name: 'test.start', path: '/test')
        ->finishesAt('done')
        ->step('step-one')
            ->goTo(TestStepOneComponent::class)
            ->unlessPasses(TestStepOneGuard::class)
            ->order(10);
});

test('descriptive test name in sentence case', function () {
    // Arrange
    TestStepOneGuard::$shouldPass = false;

    // Act
    $result = workflow('test')->nextRouteNameFor(request());

    // Assert
    expect($result)->toBe('test.step-one');
});

Test Organization

  • Feature Tests (tests/Feature/): Integration and end-to-end tests
  • Unit Tests (tests/Unit/): Isolated business logic tests
  • Support (tests/Support/): Test fixtures, stubs, helpers

Test Fixtures

Test Components (e.g., TestStepOneComponent.php):

class TestStepOneComponent extends Component
{
    use InteractsWithWorkflows;

    public function render()
    {
        return '<div>Test Step</div>';
    }
}

Test Guards (e.g., TestStepOneGuard.php):

class TestStepOneGuard implements GuardContract
{
    public static bool $shouldPass = false;

    public function passes(Request $request): bool
    {
        return self::$shouldPass;
    }

public function onEnter(Request $request): void {}
    public function onExit(Request $request): void {}
    public function onPass(Request $request): void {}
    public function onFail(Request $request): void {}
}

TestCase Configuration (tests/TestCase.php)

Key Setup:

  • Extends Orchestra\Testbench\TestCase
  • Registers LivewireServiceProvider and LivewireWorkflowsServiceProvider
  • Configures in-memory SQLite for tests
  • Provides loadWorkflowMigrations() helper

Running Tests

# All tests
composer test
# or
./vendor/bin/pest

# Specific file
./vendor/bin/pest tests/Unit/WorkflowRegistrarTest.php

# With coverage
./vendor/bin/pest --coverage

# With coverage minimum
./vendor/bin/pest --coverage --min=80

Test Coverage Goals

  • Maintain >80% code coverage
  • Test edge cases and error conditions
  • Feature tests for integration scenarios
  • Unit tests for business logic

Architecture Tests (tests/ArchTest.php)

arch('no debug functions in production code')
    ->expect('Pixelworxio\LivewireWorkflows')
    ->not->toUse(['dd', 'dump', 'ray']);

Key Concepts

1. Positive Guard Semantics

Critical Understanding: Guards use positive semantics - opposite of what you might expect.

Guard Result Step Behavior
passes() = true Skip this step
passes() = false Show this step

Rationale: Natural language with unlessPasses():

->unlessPasses(EmailVerifiedGuard::class)
// "Show step UNLESS guard passes"

Example:

class ProfileCompleteGuard implements GuardContract
{
    public function passes(Request $request): bool
    {
        // Return true when profile IS complete → skip step
        return $request->user()->profile_completed;
    }
}

2. Workflow Navigation Flow

  1. User visits entry route (/onboarding)
  2. Package evaluates guards in order
  3. Redirects to first failing guard's step
  4. User completes step, calls $this->continue('onboarding')
  5. Re-evaluates from entry → next unmet step or finish

3. History Modes

->historyMode('none')   // No tracking (default)
->historyMode('stack')  // Full history stack for back navigation

Stack Mode: Enables $this->back() to return to previous steps.

4. State Scoping

State is scoped per:

  • Workflow name
  • User key (auth ID > session ID > guest fingerprint)

No cross-workflow sharing - by design for data isolation.

5. Auto-Route Registration

Routes are automatically registered from DSL:

Workflow::flow('checkout')
    ->entersAt(name: 'checkout.start', path: '/checkout')
    ->step('shipping')  // Auto: /checkout/shipping
    ->step('payment')   // Auto: /checkout/payment

Generated Routes:

  • checkout.start → GET /checkout
  • checkout.shipping → GET /checkout/shipping
  • checkout.payment → GET /checkout/payment

6. State Management with Attributes

Setting Workflow Name:

use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName;

#[WorkflowName('onboarding')]
class ProfileStep extends Component
{
    use InteractsWithWorkflows;

    // No need to set protected ?string $workflowName = 'onboarding';
}

The #[WorkflowName] attribute eliminates the need to manually set $workflowName property. The trait automatically reads the attribute during boot. If no attribute is present, it falls back to auto-detection from the route name.

Automatic Persistence:

use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName;
use Pixelworxio\LivewireWorkflows\Attributes\WorkflowState;

#[WorkflowName('onboarding')]
class ProfileStep extends Component
{
    use InteractsWithWorkflows;

    #[WorkflowState]
    public ?string $email = null;

    #[WorkflowState(encrypt: true)]
    public ?string $password = null;

#[WorkflowState(namespace: 'profile')]
    public ?string $name = null;
}

Properties are:

  • Hydrated on mount (from repository)
  • Synced immediately when changed via wire:model or $this->set()
  • Synced on dehydrate as fallback to catch all changes
  • Scoped to workflow and user

Dual-Layer Persistence:

  • Layer 1: Proactive syncing via updatedInteractsWithWorkflows() - catches wire:model and explicit property updates
  • Layer 2: Dehydration fallback via dehydrateInteractsWithWorkflows() - catches direct assignments in methods

This ensures state is persisted reliably throughout the component lifecycle, not just at the end of the request.

7. Lazy Builder Finalization

Builders are stored as "pending" and finalized lazily:

  • On explicit get() call
  • In destructor if finishesAt() was called
  • Allows flexible definition order

8. Convention Over Configuration

Default Conventions:

  • Route names: {flow}.{step-key}
  • Route paths: {entryPath}/{step-key}
  • Session keys: workflows.{flow}.{userKey}.{suffix}
  • Database table: workflow_states (fixed)
  • Middleware: ['web'] (configurable)

Common Tasks

Adding a New Feature

  1. Write failing tests first when practical.
  2. Implement the feature following coding standards.
  3. Ensure tests pass: composer test.
  4. Run static analysis: ./vendor/bin/phpstan analyse.
  5. Format code: ./vendor/bin/pint.
  6. Update relevant documentation and CHANGELOG.md.
  7. Leave the work uncommitted for review unless the user explicitly asks for Git actions.

Adding a New Command

  1. Create command class in src/Commands/
  2. Register in LivewireWorkflowsServiceProvider::registerCommands()
  3. Add stub template in stubs/ if needed
  4. Write tests in tests/Feature/Commands/
  5. Update README with command documentation

Adding a New Event

  1. Create event class in src/Events/
  2. Fire event in appropriate location (likely WorkflowEngine)
  3. Document in README
  4. Add test coverage

Adding a New State Repository

  1. Implement WorkflowStateRepository contract
  2. Add to StateRepositories/ directory
  3. Register binding in service provider
  4. Add configuration option
  5. Write tests
  6. Document in STATE_MANAGEMENT.md

Modifying the DSL

  1. Update builder classes (FlowBuilder, StepBuilder)
  2. Update definition DTOs if needed
  3. Ensure backward compatibility or document breaking change
  4. Write comprehensive tests
  5. Update README examples
  6. Update UPGRADE.md if breaking

Debugging Workflow Issues

Enable Workflow Debugging:

// In routes/workflows.php or component
dd(workflow('onboarding')->progressFor(request()));

// Check current state
dd(app(WorkflowStateRepository::class)->getCurrentStep('onboarding', 'user-123'));

// See all state
dd($this->allWorkflowState());

Common Issues:

  • Guard logic inverted (remember: passes() = true → skip)
  • Missing workflowName property in component
  • Workflow not registered in routes/workflows.php
  • Route middleware blocking access

Troubleshooting

Guards Not Working

Problem: Steps always shown/skipped incorrectly

Solution:

  • Remember positive semantics: passes() = true → SKIP
  • Check guard class exists and implements GuardContract
  • Verify guard is properly registered: ->unlessPasses(YourGuard::class)
  • Debug with dd() in guard's passes() method

State Not Persisting

Problem: Component properties not saving

Solution:

  • Ensure workflowName is set in component
  • Verify #[WorkflowState] attribute is on public properties
  • Check repository configuration in config/livewire-workflows.php
  • For eloquent: ensure migration ran (workflow_states table exists)
  • Verify InteractsWithWorkflows trait is used

State Not Hydrating

Problem: Properties remain null on mount

Solution:

  • Confirm #[WorkflowState] attribute exists
  • Ensure bootInteractsWithWorkflows() is called (automatic with trait)
  • Check property visibility (public properties work best)
  • Verify data exists in repository

Routes Not Registered

Problem: 404 errors on workflow routes

Solution:

  • Ensure routes/workflows.php exists and is loaded
  • Check WorkflowRegistrar is registering workflow
  • Verify RouteRegistrar is being called in service provider
  • Run php artisan route:list to see registered routes
  • Check middleware isn't blocking

Workflow Not Redirecting

Problem: Stays on entry route instead of redirecting to first step

Solution:

  • Verify at least one guard returns false (step should be shown)
  • Check all guards are returning true (all steps complete)
  • Ensure finishRoute names a registered route
  • Debug with: dd(workflow('name')->nextRouteNameFor(request()))

Back Navigation Not Working

Problem: $this->back() does nothing

Solution:

  • Ensure historyMode('stack') is set on workflow
  • Verify history is being tracked (check repository)
  • Confirm previous step exists in history
  • Check you're passing correct flow name and current key

Encryption Errors

Problem: Error when encrypting state

Solution:

  • Ensure APP_KEY is set in .env
  • Verify Laravel's encryption works: Crypt::encrypt('test')
  • Check encrypted values aren't being double-encrypted

Tests Failing After Changes

Problem: Tests fail after modifying code

Solution:

  • Check breaking changes in DTOs (immutable properties)
  • Verify test fixtures still valid (guards, components)
  • Clear test database: rm database/database.sqlite (if using)
  • Reset static properties in guards between tests
  • Use beforeEach() to reset state

PHPStan Errors

Problem: Static analysis failing

Solution:

  • Add proper type hints to all methods
  • Use PHPDoc comments for complex types
  • Fix mixed types where possible
  • Add to baseline if necessary: ./vendor/bin/phpstan analyse --generate-baseline

API Reference Quick Guide

Workflow DSL

Workflow::flow(string $name)
    ->entersAt(name: string, path: string)
    ->finishesAt(string $routeName)
    ->historyMode('none' | 'stack')
    ->step(string $key)
        ->goTo(string $componentClass)
        ->unlessPasses(string $guardClass)
        ->order(int $order);

Livewire Component Methods

use InteractsWithWorkflows;

// Navigation
$this->continue(string $flow): void
$this->back(string $flow, string $currentKey): void
$this->syncState(): void

// State management
$this->getWorkflowState(string $key, mixed $default = null): mixed
$this->putWorkflowState(string $key, mixed $value): void
$this->hasWorkflowState(string $key): bool
$this->forgetWorkflowState(string $key): void
$this->clearWorkflowState(?string $namespace = null): void
$this->allWorkflowState(): array

Helper Functions

// Get workflow instance
workflow(string $flow)->redirect(Request $request, ?string $doneRoute = null)
workflow(string $flow)->nextRouteNameFor(Request $request, ?string $doneRoute = null)
workflow(string $flow)->previousRouteNameFor(string $currentKey, Request $request)
workflow(string $flow)->progressFor(Request $request)

Progress Tracking

$progress = workflow('onboarding')->progressFor($request);
// Returns:
[
    'total' => 3,
    'completed' => 1,
    'remaining' => 2,
    'percentage' => 33.33,
    'current_step' => 'verify-email',
    'next_step' => 'profile',
    'is_complete' => false,
]

Artisan Commands

# Installation
php artisan workflows:install
php artisan workflows:upgrade

# Generation
php artisan make:workflow {name}
php artisan make:workflow-guard {name}
php artisan make:workflow-step {workflow} {step} [--component] [--guard] [--order]

# Validation
php artisan workflows:scan

Configuration Reference

Package Configuration (config/livewire-workflows.php)

return [
    // State persistence: 'null', 'session', or 'eloquent'
    'repository' => env('WORKFLOWS_REPOSITORY', 'eloquent'),

    // Middleware applied to all workflow routes
    'middleware' => ['web'],

    'resume' => [
        'default_expires_minutes' => env('WORKFLOWS_RESUME_EXPIRES', 1440),
    ],
];

State Repository Options

Repository Use Case Persistence
null Stateless workflows None
session Short-lived anonymous flows (deprecated) Session lifetime
eloquent Authenticated users, production Database

Important Architectural Decisions

Why Positive Guard Semantics?

Natural language with unlessPasses(): "Show step UNLESS guard passes"

Why Immutable Definitions?

Thread safety, predictability, prevents accidental mutations

Why Lazy Builder Finalization?

Allows flexible definition order, auto-cleanup in destructor

Why State Scoping?

Data isolation, security, simplicity (no cross-workflow contamination)

Why Auto-Route Registration?

Zero boilerplate, DRY principle, convention over configuration

Why Events?

Extensibility without modifying package (analytics, logging, etc.)


Resources

  • README.md: User-facing documentation
  • STATE_MANAGEMENT.md: Comprehensive state management guide
  • CONTRIBUTING.md: Contribution guidelines and standards
  • CHANGELOG.md: Version history
  • UPGRADE.md: Breaking changes and upgrade instructions
  • GitHub Issues: Bug reports and feature requests
  • Tests: Living documentation of expected behavior

Quick Reference for AI Assistants

When Making Changes:

  1. ✅ ALWAYS include declare(strict_types=1);
  2. ✅ ALWAYS write tests for new features
  3. ✅ ALWAYS update relevant documentation
  4. ✅ ALWAYS run composer test and phpstan before committing
  5. ✅ ALWAYS use readonly properties for immutability
  6. ✅ ALWAYS validate in constructors/builders
  7. ✅ Use constructor property promotion
  8. ✅ Use named arguments in DSL
  9. ✅ Follow PSR-12 standards
  10. ✅ Remember: passes() = true means SKIP the step

When Adding Features:

  1. Check if it breaks existing functionality
  2. Ensure backward compatibility or document breaking change
  3. Add to CHANGELOG.md
  4. Update UPGRADE.md if breaking
  5. Consider event-driven approach for extensibility
  6. Prefer composition over inheritance
  7. Keep classes focused (SRP)

When Fixing Bugs:

  1. Write failing test first (reproduce bug)
  2. Fix the bug
  3. Ensure test passes
  4. Check for similar issues elsewhere
  5. Add regression test
  6. Document fix in CHANGELOG.md

Version Information

This AGENTS.md was created for version: v1.x (check CHANGELOG.md for current version)

Last updated: 2025-11-14


This document is maintained for AI assistants working on the Livewire Workflows codebase. Keep it updated as the project evolves.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log