PHP code example of androlax2 / laravel-model-state-graph

1. Go to this page and download the library: Download androlax2/laravel-model-state-graph library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

androlax2 / laravel-model-state-graph example snippets


interface BusinessRule
{
    /**
     * Determine if this rule applies to the current model state
     */
    public function supports(Model $model): bool;
    
    /**
     * Validate the model against this rule
     * @throws BusinessRuleViolationException
     */
    public function validate(Model $model): void;
}

interface FieldRuleSet
{
    /**
     * The field name this rule set validates
     */
    public function getField(): string;
    
    /**
     * All business rules for this field
     * @return BusinessRule[]
     */
    public function getRules(): array;
    
    /**
     * Determine if this rule set should run for the current model state
     */
    public function supports(Model $model): bool;
}

$graph = ModelStateGraph::for(Product::class)
    ->addFieldRuleSet(new QuantityRuleSet())
    ->addFieldRuleSet(new PriceRuleSet());

if ($graph->isValid($model)) {
    // All rules passed
} else {
    $violations = $graph->getViolations($model);
    // Handle violations
}



namespace App\Rules;

use Androlax\LaravelModelStateGraph\Contracts\BusinessRule;
use Androlax\LaravelModelStateGraph\Exceptions\BusinessRuleViolationException;
use App\Models\Product;

class QuantityMustBePositiveRule implements BusinessRule
{
    public function supports(Product $model): bool
    {
        // Only apply when quantity changes
        return $model->isDirty('quantity');
    }
    
    public function validate(Product $model): void
    {
        if ($model->quantity < 0) {
            throw new BusinessRuleViolationException('Quantity must be positive');
        }
    }
}



namespace App\RuleSets;

use Androlax\LaravelModelStateGraph\Contracts\FieldRuleSet;
use App\Models\Product;
use App\Rules\QuantityMustBePositiveRule;

class QuantityRuleSet implements FieldRuleSet
{
    public function getField(): string
    {
        return 'quantity';
    }
    
    public function getRules(): array
    {
        return [
            new QuantityMustBePositiveRule(),
        ];
    }
    
    public function supports(Product $model): bool
    {
        return $model->isDirty('quantity');
    }
}



use Androlax\LaravelModelStateGraph\ModelStateGraph;
use App\RuleSets\QuantityRuleSet;

$product = Product::first();
$product->quantity = -5;

$graph = ModelStateGraph::for(Product::class)
    ->addFieldRuleSet(new QuantityRuleSet());

if ($graph->isValid($product)) {
    $product->save();
} else {
    $violations = $graph->getViolations($product);
    // Output: ["Quantity must be positive"]
}



class QuantityRuleSet implements FieldRuleSet
{
    public function getField(): string
    {
        return 'quantity';
    }
    
    public function getRules(): array
    {
        return [
            new QuantityMustBePositiveRule(),
            new QuantityIncreaseRule(),
            new QuantityDecreaseRule(),
            new QuantityRangeRule(),
        ];
    }
    
    public function supports(Product $model): bool
    {
        return $model->isDirty('quantity');
    }
}



class QuantityIncreaseRule implements BusinessRule
{
    public function supports(Product $model): bool
    {
        // Only when increasing quantity
        return $model->isDirty('quantity') && 
               $model->quantity > $model->getOriginal('quantity');
    }
    
    public function validate(Product $model): void
    {
        $increase = $model->quantity - $model->getOriginal('quantity');
        
        if ($increase > $model->max_daily_increase) {
            throw new BusinessRuleViolationException(
                "Quantity increase of {$increase} exceeds daily limit of {$model->max_daily_increase}"
            );
        }
        
        if (!$this->hasSufficientInventory($increase)) {
            throw new BusinessRuleViolationException('Insufficient inventory for quantity increase');
        }
    }
    
    private function hasSufficientInventory(int $increase): bool
    {
        // Your inventory check logic
        return true;
    }
}

class QuantityDecreaseRule implements BusinessRule
{
    public function supports(Product $model): bool
    {
        // Only when decreasing quantity
        return $model->isDirty('quantity') && 
               $model->quantity < $model->getOriginal('quantity');
    }
    
    public function validate(Product $model): void
    {
        if ($model->quantity < $model->minimum_stock) {
            throw new BusinessRuleViolationException(
                "Quantity cannot decrease below minimum stock level of {$model->minimum_stock}"
            );
        }
    }
}



$graph = ModelStateGraph::for(Product::class)
    ->addFieldRuleSet(new QuantityRuleSet())
    ->addFieldRuleSet(new PriceRuleSet())
    ->addFieldRuleSet(new StatusRuleSet());

$product->fill([
    'quantity' => 25,
    'price' => 99.99,
    'status' => 'active'
]);

if ($graph->isValid($product)) {
    $product->save();
} else {
    $violations = $graph->getViolations($product);
    foreach ($violations as $violation) {
        echo $violation . "\n";
    }
}



class StatusTransitionRule implements BusinessRule
{
    private array $allowedTransitions = [
        'draft' => ['pending', 'cancelled'],
        'pending' => ['approved', 'rejected', 'cancelled'],
        'approved' => ['shipped', 'cancelled'],
        'shipped' => ['delivered'],
        'delivered' => [], // Terminal state
    ];
    
    public function supports(Product $model): bool
    {
        return $model->isDirty('status');
    }
    
    public function validate(Product $model): void
    {
        $from = $model->getOriginal('status');
        $to = $model->status;
        
        $allowed = $this->allowedTransitions[$from] ?? [];
        
        if (!in_array($to, $allowed)) {
            throw new BusinessRuleViolationException(
                "Cannot transition from '{$from}' to '{$to}'. Allowed transitions: " . implode(', ', $allowed)
            );
        }
    }
}



class ConditionalPriceRuleSet implements FieldRuleSet
{
    public function __construct(
        private FeatureFlagService $features,
        private User $currentUser
    ) {}
    
    public function getField(): string
    {
        return 'price';
    }
    
    public function getRules(): array
    {
        $rules = [
            new PriceRangeRule(),
            new PriceChangeLimitRule(),
        ];
        
        // Admin users can override price limits
        if ($this->currentUser->hasRole('admin')) {
            $rules[] = new AdminPriceOverrideRule();
        }
        
        // Add experimental pricing rules when feature is enabled
        if ($this->features->isEnabled('dynamic_pricing')) {
            $rules[] = new DynamicPricingRule();
        }
        
        return $rules;
    }
    
    public function supports(Product $model): bool
    {
        // Skip validation for free products
        return $model->isDirty('price') && $model->category !== 'free';
    }
}

// Usage with dependency injection
$graph = ModelStateGraph::for(Product::class)
    ->addFieldRuleSet(
        new ConditionalPriceRuleSet(
            app(FeatureFlagService::class),
            auth()->user()
        )
    );



class PriceRequiresApprovalRule implements BusinessRule
{
    private const APPROVAL_THRESHOLD_PERCENT = 20;
    
    public function __construct(
        private ApprovalService $approvalService
    ) {}
    
    public function supports(Product $model): bool
    {
        if (!$model->isDirty('price')) {
            return false;
        }
        
        $originalPrice = $model->getOriginal('price');
        $newPrice = $model->price;
        $percentChange = abs(($newPrice - $originalPrice) / $originalPrice * 100);
        
        return $percentChange >= self::APPROVAL_THRESHOLD_PERCENT;
    }
    
    public function validate(Product $model): void
    {
        $approval = $this->approvalService->findPendingApproval($model, 'price_change');
        
        if (!$approval || !$approval->isApproved()) {
            throw new BusinessRuleViolationException(
                'Price changes over 20% 



namespace App\Observers;

use Androlax\LaravelModelStateGraph\ModelStateGraph;
use App\Models\Product;
use App\RuleSets\QuantityRuleSet;
use App\RuleSets\PriceRuleSet;
use App\RuleSets\StatusRuleSet;

class ProductObserver
{
    private ModelStateGraph $graph;
    
    public function __construct()
    {
        $this->graph = ModelStateGraph::for(Product::class)
            ->addFieldRuleSet(new QuantityRuleSet())
            ->addFieldRuleSet(new PriceRuleSet())
            ->addFieldRuleSet(new StatusRuleSet());
    }
    
    public function saving(Product $product): bool
    {
        if (!$this->graph->isValid($product)) {
            $violations = $this->graph->getViolations($product);
            
            // Log violations
            logger()->warning('Product validation failed', [
                'product_id' => $product->id,
                'violations' => $violations,
            ]);
            
            // Prevent save
            return false;
        }
        
        return true;
    }
}



use Androlax\LaravelModelStateGraph\ModelStateGraph;
use Androlax\LaravelModelStateGraph\Exceptions\InvalidFieldException;
use Androlax\LaravelModelStateGraph\Exceptions\DuplicateFieldException;
use Androlax\LaravelModelStateGraph\Exceptions\BusinessRuleViolationException;

try {
    $graph = ModelStateGraph::for(Product::class)
        ->addFieldRuleSet(new QuantityRuleSet())
        ->addFieldRuleSet(new PriceRuleSet());
    
    if (!$graph->isValid($product)) {
        $violations = $graph->getViolations($product);
        
        // Log each violation
        foreach ($violations as $violation) {
            logger()->warning('Business rule violation', [
                'model' => get_class($product),
                'model_id' => $product->id,
                'message' => $violation,
            ]);
        }
        
        // Return to user with errors
        return back()->withErrors([
            'validation' => 'The product state is invalid: ' . implode(', ', $violations)
        ]);
    }
    
    $product->save();
    
} catch (InvalidFieldException $e) {
    // Field doesn't exist on the model
    report($e);
    return back()->withErrors([
        'field' => 'Invalid field configuration: ' . $e->getMessage()
    ]);
    
} catch (DuplicateFieldException $e) {
    // Multiple rule sets defined for the same field
    report($e);
    return back()->withErrors([
        'configuration' => 'Duplicate field rule sets: ' . $e->getMessage()
    ]);
}



use Tests\Fixtures\Product;
use App\Rules\QuantityIncreaseRule;

it('allows quantity increases within limits', function () {
    $product = Product::create([
        'quantity' => 10,
        'max_daily_increase' => 50,
    ]);
    
    $product->quantity = 25; // Increase of 15
    
    $rule = new QuantityIncreaseRule();
    
    expect($rule->supports($product))->toBeTrue();
    
    // Should not throw exception
    $rule->validate($product);
});

it('prevents quantity increases exceeding daily limit', function () {
    $product = Product::create([
        'quantity' => 10,
        'max_daily_increase' => 20,
    ]);
    
    $product->quantity = 50; // Increase of 40, exceeds limit
    
    $rule = new QuantityIncreaseRule();
    
    expect(fn() => $rule->validate($product))
        ->toThrow(BusinessRuleViolationException::class, 'exceeds daily limit');
});



use App\RuleSets\QuantityRuleSet;

it('only supports models with dirty quantity field', function () {
    $product = Product::create(['quantity' => 10]);
    $ruleSet = new QuantityRuleSet();
    
    expect($ruleSet->supports($product))->toBeFalse();
    
    $product->quantity = 20;
    expect($ruleSet->supports($product))->toBeTrue();
});

it('



it('validates complete product updates', function () {
    $product = Product::create([
        'quantity' => 10,
        'price' => 50.00,
        'status' => 'draft',
    ]);
    
    $product->fill([
        'quantity' => 25,
        'price' => 45.00,
        'status' => 'pending',
    ]);
    
    $graph = ModelStateGraph::for(Product::class)
        ->addFieldRuleSet(new QuantityRuleSet())
        ->addFieldRuleSet(new PriceRuleSet())
        ->addFieldRuleSet(new StatusRuleSet());
    
    expect($graph->isValid($product))->toBeTrue();
});

it('catches invalid status transitions', function () {
    $product = Product::create(['status' => 'draft']);
    $product->status = 'shipped'; // Invalid: draft can't go directly to shipped
    
    $graph = ModelStateGraph::for(Product::class)
        ->addFieldRuleSet(new StatusRuleSet());
    
    expect($graph->isValid($product))->toBeFalse();
    expect($graph->getViolations($product))
        ->toContain('Cannot transition from \'draft\' to \'shipped\'');
});

// Good: Focused rule
class QuantityMustBePositiveRule implements BusinessRule { ... }
class QuantityWithinRangeRule implements BusinessRule { ... }

// Bad: Rule doing too much
class QuantityValidationRule implements BusinessRule { ... } // validates everything

public function supports(Product $model): bool
{
    // Quick check: only run when relevant
    if (!$model->isDirty('price')) {
        return false;
    }
    
    // More expensive checks only if needed
    return $model->category === 'premium';
}

// Good: Clear and actionable
throw new BusinessRuleViolationException(
    "Quantity increase of {$increase} exceeds daily limit of {$limit}. Try again tomorrow or request approval."
);

// Bad: Vague
throw new BusinessRuleViolationException("Invalid quantity");

app/
├── Models/
│   ├── Product.php
│   └── Order.php
└── BusinessRules/
    ├── Product/
    │   ├── Quantity/
    │   │   ├── QuantityRuleSet.php
    │   │   ├── QuantityIncreaseRule.php
    │   │   ├── QuantityDecreaseRule.php
    │   │   └── QuantityRangeRule.php
    │   ├── Price/
    │   │   ├── PriceRuleSet.php
    │   │   ├── PriceRangeRule.php
    │   │   └── PriceApprovalRule.php
    │   └── Status/
    │       ├── StatusRuleSet.php
    │       └── StatusTransitionRule.php
    └── Order/
        ├── Status/
        │   ├── StatusRuleSet.php
        │   └── OrderStatusTransitionRule.php
        └── Payment/
            ├── PaymentRuleSet.php
            └── PaymentValidationRule.php

class PriceRuleSet implements FieldRuleSet
{
    public function __construct(
        private PricingService $pricing,
        private ?User $user = null
    ) {
        $this->user ??= auth()->user();
    }
    
    public function getRules(): array
    {
        return [
            new PriceRangeRule($this->pricing),
            new PriceApprovalRule($this->user),
        ];
    }
}

/**
 * Order Status State Machine
 * 
 * draft → pending → approved → shipped → delivered
 *   ↓       ↓         ↓
 * cancelled
 * 
 * Business Rules:
 * - Orders can be cancelled at any stage before delivery
 * - Shipped orders