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;
}
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}"
);
}
}
}
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('
// 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");
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
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.