fzengin19/laravel-approval

Laravel iΓ§in onay sistemi paketi

Fund package maintenance!
:vendor_name

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

Language:HTML

pkg:composer/fzengin19/laravel-approval

v1.0.0 2025-07-25 09:27 UTC

This package is auto-updated.

Last update: 2026-01-08 11:12:54 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads Code Coverage Status

A comprehensive approval system package for Laravel. Provides a powerful and flexible solution for managing approval statuses of your models. Developed with TDD approach, reliable code with high test coverage.

✨ Features

  • πŸš€ Easy Integration: Integrate your models with approval system by just adding a trait.
  • βš™οΈ Flexible Configuration: Two different modes (insert/upsert) and customizable settings.
  • πŸ’‘ Configurable Initial State: Define a default status for models that haven't been through an approval process yet.
  • 🧠 Smart Rejection Handling: Intelligent categorization of rejection reasons with predefined and custom options.
  • πŸ” Global Scope: Automatically show only approved records with configurable behavior.
  • πŸ”” Comprehensive Event System: 6 different events (pre/post status changes) using modern PHP 8.1+ features.
  • πŸ“Š Statistics Service: Built-in statistics calculation with percentages and model-specific data.
  • 🎭 Facade Support: Static API for easy usage with full IDE support.
  • πŸ–₯️ Artisan Commands: View statistics via CLI with table formatting.
  • πŸ”§ Repository Pattern: Clean data access layer with insert/upsert modes.
  • πŸ›‘οΈ Validation System: Input validation with business rule enforcement.
  • 🌐 Webhook Support: Configurable webhook endpoints for external integrations.
  • 🎯 Model-specific Configuration: Override settings per model with inheritance.
  • πŸ”„ Auto Pending: Automatically set models to pending status on creation.
  • πŸ”— Polymorphic Causer: Approval actions can be caused by any model, not just Users.
  • ✨ Modern PHP: Utilizes modern PHP features like Enums for type-safe statuses.
  • πŸ“ˆ Performance Optimized: Indexed database fields and efficient queries.

πŸš€ Quick Start

1. Installation

composer require fzengin19/laravel-approval

2. Publish and Migrate

php artisan vendor:publish --provider="LaravelApproval\LaravelApprovalServiceProvider"
php artisan migrate

3. Add Trait to Your Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use LaravelApproval\Contracts\ApprovableInterface;
use LaravelApproval\Traits\Approvable;

class Post extends Model implements ApprovableInterface
{
    use HasFactory, Approvable;

    protected $fillable = [
        'title',
        'content',
    ];
}

4. Start Using

use LaravelApproval\Enums\ApprovalStatus;

// Create a post
$post = Post::create([
    'title' => 'My First Post',
    'content' => 'This is my content...',
]);

// Set to pending
$post->setPending(1); // 1 = user ID

// Approve
$post->approve(1);

// Reject with reason
$post->reject(1, 'spam', 'This is spam content');

// Check status
$post->isApproved(); // true
$post->isPending();  // false
$post->isRejected(); // false
$post->getApprovalStatus(); // Returns ApprovalStatus::APPROVED enum case

// Use query scopes
$approvedPosts = Post::approved()->get();
$pendingPosts = Post::pending()->get();
$rejectedPosts = Post->rejected()->get();

// Get statistics
$stats = Approval::getStatistics(Post::class);

That's it! Your model now has full approval functionality. πŸŽ‰

πŸ“¦ Installation

Requirements

  • PHP 8.1 or higher
  • Laravel 9 or higher

Install via Composer

composer require fzengin19/laravel-approval

Publish Configuration

# Publish migrations and config
php artisan vendor:publish --provider="LaravelApproval\LaravelApprovalServiceProvider"

# Or publish only config
php artisan vendor:publish --tag="laravel-approval-config"

Run Migrations

php artisan migrate

βš™οΈ Configuration

The published config file (config/approvals.php) contains comprehensive configuration options. Here are the key sections:

return [
    // The user model that is responsible for approval actions.
    'user_model' => config('auth.providers.users.model'),

    'default' => [
        // Core Settings
        'mode' => 'insert',                    // 'insert' or 'upsert'
        'auto_pending_on_create' => false,     // Auto pending when model is created
        'show_only_approved_by_default' => false, // Is global scope active?
        'auto_scope' => true,                  // Automatically add global scope
        'default_status_for_unaudited' => null, // Default status for models with no approval record. Can be: null, 'pending', 'approved', 'rejected'.

        // Event Settings
        'events_enabled' => true,              // Enable event system
        'events_logging' => true,              // Log events
        'events_webhooks_enabled' => false,    // Enable webhooks
        'events_webhooks_endpoints' => [],     // Webhook endpoints
        'events_custom_actions' => [           // Custom event actions
            'model_approved' => [
                // Example: function($event) { /* custom logic */ }
            ],
            // ... other events
        ],
        
        // Rejection Settings
        'allow_custom_reasons' => false,       // Allow custom rejection reasons
        'rejection_reasons' => [
            'inappropriate_content' => 'Inappropriate Content',
            'spam' => 'Spam',
            'other' => 'Other',
        ],
    ],
    
    'models' => [
        // Model specific settings (override defaults)
        'App\Models\Post' => [
            'mode' => 'upsert',
            'auto_pending_on_create' => true,
        ],
    ],
];

Note: Approval statuses are managed by the LaravelApproval\Enums\ApprovalStatus enum:

  • ApprovalStatus::PENDING
  • ApprovalStatus::APPROVED
  • ApprovalStatus::REJECTED

πŸ“– Usage Guide

Basic Operations

use LaravelApproval\Enums\ApprovalStatus;

// Create a model
$post = Post::create(['title' => 'New Post']);

// Check current status
// By default, a model with no approval record has a `null` status.
$post->isPending();    // false
$post->isApproved();   // false
$post->isRejected();   // false
$post->getApprovalStatus(); // null

// You can change this behavior with `default_status_for_unaudited` config.
// If config is set to 'pending':
// config(['approvals.default.default_status_for_unaudited' => 'pending']);
// $post->isPending(); // true, returns true
// $post->getApprovalStatus(); // ApprovalStatus::PENDING, returns enum case

// Set to pending
$post->setPending(1);  // 1 = approving user ID

// Approve
$post->approve(1);

// Reject with predefined reason
$post->reject(1, 'spam', 'Additional details');

// Reject with custom reason (automatically categorized as 'other' if not in predefined list)
$post->reject(1, 'Custom reason', 'Additional details');

// Get approval history
$allApprovals = $post->approvals; // All approval records (MorphMany)
$latestApproval = $post->latestApproval; // Current approval record (MorphOne)

// Get who caused the approval
$causer = $post->latestApproval->causer; // Returns the User model instance (or other causer model)

Query Scopes

// Get only approved posts
$approvedPosts = Post::approved()->get();

// Get only pending posts
$pendingPosts = Post::pending()->get();

// Get only rejected posts
$rejectedPosts = Post::rejected()->get();

// Get with approval status (eager load latest approval)
$posts = Post::withApprovalStatus()->get();

// Include unapproved posts (when global scope is active)
$allPosts = Post::withUnapproved()->get();

// Check if global scope is active
$posts = Post::all(); // Only approved if global scope is enabled

Modes

Insert Mode (Default)

Creates a new approval record for each status change. Ideal for history tracking and audit trails.

Configure in your config/approvals.php:

'default' => [
    'mode' => 'insert',  // Default mode
    // ... other settings
],

Usage:

$post->setPending(1);  // New record
$post->approve(1);     // New record
$post->reject(1);      // New record
// Total: 3 records (full history)

Upsert Mode

Updates existing approval record. Ideal for keeping single record and current status only.

Configure in your config/approvals.php:

'default' => [
    'mode' => 'upsert',  // Or set per model
    // ... other settings
],

Usage:

$post->setPending(1);  // New record
$post->approve(1);     // Update existing record
$post->reject(1);      // Update existing record
// Total: 1 record (current status only)

Note: Mode can be configured per model in the config file.

Smart Rejection Handling

The reject() method intelligently handles rejection reasons with automatic categorization:

// Using predefined reason
$post->reject(1, 'spam', 'Additional details');
// Result: rejection_reason = 'spam', rejection_comment = 'Additional details'

// Using custom reason (when allowed)
$post->reject(1, 'copyright_violation', 'Image belongs to Getty Images');
// Result: rejection_reason = 'copyright_violation', rejection_comment = 'Image belongs to Getty Images'

// Using custom reason (when not allowed)
$post->reject(1, 'custom_reason', 'Custom rejection reason');
// Result: rejection_reason = 'other', rejection_comment = 'custom_reason - Custom rejection reason'

Validation Rules:

  • reason field: maximum 255 characters (string field limit)
  • comment field: no length limit (TEXT field)
  • Custom reasons can be enabled/disabled per model

Custom Rejection Reasons

Control whether custom rejection reasons are allowed in your config/approvals.php:

'default' => [
    'allow_custom_reasons' => false,  // Set to true to allow globally
    // ... other settings
],

'models' => [
    'App\Models\Post' => [
        'allow_custom_reasons' => true,  // Allow custom reasons for Post model
        // ... other settings
    ],
    'App\Models\Comment' => [
        'allow_custom_reasons' => false, // Only predefined reasons for Comment model
        // ... other settings
    ],
],

Global Scope

When global scope is active, only approved records are visible by default:

// Only approved posts (when global scope is enabled)
$posts = Post::all();

// To see all posts (bypass global scope)
$allPosts = Post::withUnapproved()->get();

// Check if global scope is active for this model
$showOnlyApproved = config('approvals.models.' . Post::class . '.show_only_approved_by_default', 
                          config('approvals.default.show_only_approved_by_default', false));

Auto Scope Configuration

Control whether the global scope is automatically applied in your config/approvals.php:

'default' => [
    'auto_scope' => true,  // Set to false to disable globally
    // ... other settings
],

'models' => [
    'App\Models\Post' => [
        'auto_scope' => false,  // Disable for specific model
        // ... other settings
    ],
],

Or manually apply the scope when needed:

use LaravelApproval\Scopes\ApprovableScope;

$approvedPosts = Post::withGlobalScope('approvable', new ApprovableScope)->get();

Auto Pending

Automatically set to pending status when model is created. Configure in your config/approvals.php:

'default' => [
    'auto_pending_on_create' => false,  // Set to true to enable globally
    // ... other settings
],

'models' => [
    'App\Models\Post' => [
        'auto_pending_on_create' => true,  // Enable for specific model
        // ... other settings
    ],
],

Usage:

$post = Post::create(['title' => 'Test']);
// Will automatically be in pending status if auto_pending_on_create is enabled

Model-Specific Configuration

Override default settings for specific models in your config/approvals.php. Only specify the settings you want to override:

'models' => [
    'App\Models\Post' => [
        'mode' => 'upsert',                    // This model uses upsert mode
        'auto_pending_on_create' => true,      // Auto pending for this model
        'show_only_approved_by_default' => true, // Global scope active for this model
        'events_enabled' => false,             // No events for this model
        'allow_custom_reasons' => true,        // Allow custom rejection reasons
        'rejection_reasons' => [               // Custom rejection reasons
            'inappropriate_content' => 'Inappropriate Content',
            'spam' => 'Spam',
            'duplicate' => 'Duplicate',
            'incomplete' => 'Incomplete',
            'copyright_violation' => 'Copyright Violation',
            'other' => 'Other',
        ],
    ],
    'App\Models\Comment' => [
        'mode' => 'insert',                    // This model uses insert mode
        'auto_pending_on_create' => false,     // No auto pending for this model
        'events_enabled' => true,              // Events enabled for this model
        'events_logging' => false,             // No logging for this model
        'allow_custom_reasons' => false,       // Only predefined reasons
        'rejection_reasons' => [               // Different rejection reasons
            'spam' => 'Spam',
            'harassment' => 'Harassment',
            'inappropriate' => 'Inappropriate',
            'offensive' => 'Offensive',
            'other' => 'Other',
        ],
    ],
    'App\Models\Product' => [
        'mode' => 'upsert',
        'auto_pending_on_create' => true,
        'events_webhooks_enabled' => true,     // Webhooks enabled for this model
        'events_webhooks_endpoints' => [
            [
                'url' => 'https://api.example.com/webhooks/product-approval',
                'headers' => ['Authorization' => 'Bearer token'],
                'events' => ['model_approved', 'model_rejected'],
            ],
        ],
        'rejection_reasons' => [
            'inappropriate_content' => 'Inappropriate Content',
            'spam' => 'Spam',
            'duplicate' => 'Duplicate',
            'incomplete' => 'Incomplete',
            'pricing_violation' => 'Pricing Violation',
            'category_mismatch' => 'Category Mismatch',
            'other' => 'Other',
        ],
    ],
],

πŸ”” Events

Listen to events on status changes. The package provides 6 different events with rich context:

Available Events

Pre-events (Triggered before status change):

  • LaravelApproval\Events\ModelApproving
  • LaravelApproval\Events\ModelRejecting
  • LaravelApproval\Events\ModelSettingPending

Post-events (Triggered after status change):

  • LaravelApproval\Events\ModelApproved
  • LaravelApproval\Events\ModelRejected
  • LaravelApproval\Events\ModelPending

Event Usage

use Illuminate\Support\Facades\Event;
use LaravelApproval\Events\ModelApproved;
use LaravelApproval\Events\ModelRejected;

Event::listen(ModelApproved::class, function (ModelApproved $event) {
    // Access event properties directly (they are public readonly)
    $model = $event->model;
    $approval = $event->approval;
    $context = $event->context;
    
    // Actions to take when approved
    \Log::info("Model approved: " . $model->id);
});

Event::listen(ModelRejected::class, function (ModelRejected $event) {
    $model = $event->model;
    $approval = $event->approval;
    
    // Actions to take when rejected
    \Log::info('Model rejected', [
        'model' => get_class($model),
        'reason' => $event->reason,
        'comment' => $event->comment,
    ]);
});

Event Configuration

Configure events globally or per model in your config/approvals.php:

'default' => [
    // Event Settings
    'events_enabled' => true,              // Enable event system
    'events_logging' => true,              // Log events
    'events_webhooks_enabled' => false,    // Enable webhooks
    'events_webhooks_endpoints' => [],     // Webhook endpoints
    'events_custom_actions' => [           // Custom event actions
        'model_approved' => [
            // Example: function(ModelApproved $event) { /* custom logic */ }
        ],
        'model_rejected' => [
            // Example: function(ModelRejected $event) { /* custom logic */ }
        ],
        'model_pending' => [
            // Example: function(ModelPending $event) { /* custom logic */ }
        ],
        'model_approving' => [
            // Example: function(ModelApproving $event) { /* custom logic */ }
        ],
        'model_rejecting' => [
            // Example: function(ModelRejecting $event) { /* custom logic */ }
        ],
        'model_setting_pending' => [
            // Example: function(ModelSettingPending $event) { /* custom logic */ }
        ],
    ],
    // ... other settings
],

'models' => [
    'App\Models\Post' => [
        'events_enabled' => false,              // No events for this model
        // ... other settings
    ],
    'App\Models\Comment' => [
        'events_enabled' => true,
        'events_logging' => false,              // No logging for this model
        // ... other settings
    ],
            'App\Models\Product' => [
            'events_webhooks_enabled' => true,      // Webhooks enabled
            'events_webhooks_endpoints' => [
                [
                    'url' => 'https://api.example.com/webhooks/product-approval',
                    'headers' => ['Authorization' => 'Bearer token'],
                    'events' => ['model_approved', 'model_rejected'],
                ],
            ],
            'rejection_reasons' => [
                'inappropriate_content' => 'Inappropriate Content',
                'spam' => 'Spam',
                'duplicate' => 'Duplicate',
                'incomplete' => 'Incomplete',
                'pricing_violation' => 'Pricing Violation',
                'category_mismatch' => 'Category Mismatch',
                'other' => 'Other',
            ],
            // ... other settings
        ],
],

Event Properties

Each event object contains public readonly properties for easy access:

  • model: The model that triggered the event (ApprovableInterface&Model)
  • approval: The approval record (Approval)
  • reason: Rejection reason (?string, if applicable)
  • comment: Rejection comment (?string, if applicable)
  • userId: User ID who performed the action (?int)
  • context: Additional context data (array)

🎭 Facade Usage

use LaravelApproval\Facades\Approval;

// Assuming $post is an Approvable model instance
// and $user is the user performing the action.

// Approve model
Approval::approve($post, $user->id);

// Reject model
Approval::reject($post, $user->id, 'Invalid content', 'Description');

// Set to pending status
Approval::setPending($post, $user->id);

// Get statistics for specific model
$stats = Approval::getStatistics(\App\Models\Post::class);
// [
//     'total' => 10,
//     'approved' => 7,
//     'pending' => 2,
//     'rejected' => 1,
//     'approved_percentage' => 70.0,
//     'pending_percentage' => 20.0,
//     'rejected_percentage' => 10.0,
// ]

// Get all statistics for configured models
$allStats = Approval::getAllStatistics();
// [
//     'App\Models\Post' => [...],
//     'App\Models\Comment' => [...],
//     'App\Models\Product' => [...],
// ]

πŸ–₯️ Artisan Commands

# Statistics for all configured models
php artisan approval:status

# Statistics for specific model
php artisan approval:status --model="App\Models\Post"

Command Output Examples

All Models Statistics: +-------------------+-------+----------+--------+----------+-------------+ | Model | Total | Approved | Pending| Rejected | Approved % | +-------------------+-------+----------+--------+----------+-------------+ | App\Models\Post | 100 | 75 | 15 | 10 | 75.00% | | App\Models\Comment| 50 | 40 | 5 | 5 | 80.00% | +-------------------+-------+----------+--------+----------+-------------+

Single Model Statistics: +----------+-------+------------+ | Metric | Count | Percentage | +----------+-------+------------+ | Total | 100 | 100% | | Approved | 75 | 75.00% | | Pending | 15 | 15.00% | | Rejected | 10 | 10.00% | +----------+-------+------------+


## πŸ—οΈ Architecture Overview

### Package Structure

src/
β”œβ”€β”€ Core/                    # Core business logic
β”‚   β”œβ”€β”€ ApprovalManager.php  # Main approval logic
β”‚   β”œβ”€β”€ ApprovalRepository.php # Data access layer
β”‚   β”œβ”€β”€ ApprovalValidator.php # Validation logic
β”‚   └── ApprovalEventDispatcher.php # Event dispatching
β”œβ”€β”€ Models/                  # Database models
β”‚   └── Approval.php         # Approval model
β”œβ”€β”€ Traits/                  # Model traits
β”‚   β”œβ”€β”€ Approvable.php       # Main trait (composite)
β”‚   β”œβ”€β”€ HasApprovals.php     # Relationship management
β”‚   β”œβ”€β”€ ApprovalScopes.php   # Query scopes
β”‚   └── ApprovalActions.php  # Action methods
β”œβ”€β”€ Events/                  # Event classes
β”‚   β”œβ”€β”€ ModelApproved.php    # Post-approval event
β”‚   β”œβ”€β”€ ModelRejected.php    # Post-rejection event
β”‚   └── ...                  # Other events
β”œβ”€β”€ Listeners/               # Event listeners
β”‚   β”œβ”€β”€ BaseApprovalListener.php # Base listener
β”‚   β”œβ”€β”€ HandleModelApproved.php  # Approval handler
β”‚   └── ...                  # Other handlers
β”œβ”€β”€ Services/                # Service classes
β”‚   β”œβ”€β”€ ApprovalService.php  # Main service
β”‚   └── StatisticsService.php # Statistics service
β”œβ”€β”€ Facades/                 # Facade classes
β”‚   └── Approval.php         # Main facade
β”œβ”€β”€ Commands/                # Artisan commands
β”‚   └── ApprovalStatusCommand.php # Status command
β”œβ”€β”€ Scopes/                  # Query scopes
β”‚   └── ApprovableScope.php  # Global scope
β”œβ”€β”€ Contracts/               # Interfaces
β”‚   └── ApprovableInterface.php # Main interface
└── Exceptions/              # Custom exceptions
    β”œβ”€β”€ ApprovalException.php
    └── UnauthorizedApprovalException.php

### Design Patterns

- **Repository Pattern**: Clean data access layer
- **Service Pattern**: Business logic encapsulation
- **Event-Driven Architecture**: Loose coupling via events
- **Trait Composition**: Modular functionality
- **Facade Pattern**: Simplified API access
- **Observer Pattern**: Event listening system

### Data Flow

1. **Model Action** β†’ ApprovalActions trait
2. **Validation** β†’ ApprovalValidator
3. **Business Logic** β†’ ApprovalManager
4. **Data Persistence** β†’ ApprovalRepository
5. **Event Dispatching** β†’ Event system
6. **Response** β†’ Model with updated status

## πŸ§ͺ Testing

```bash
composer test

Test Coverage

The package includes comprehensive tests with high code coverage, ensuring reliability and stability.

  • Unit Tests: Individual component testing
  • Integration Tests: End-to-end workflow testing
  • Feature Tests: Complete feature testing
  • Database Tests: Migration and factory testing
  • Architecture Tests: Enforcing clean architecture rules using Pest Arch

Test Structure

tests/
β”œβ”€β”€ ArchTest.php             # Architecture tests
β”œβ”€β”€ Commands/                # Artisan command tests
β”œβ”€β”€ Core/                    # Core service tests
β”œβ”€β”€ Events/                  # Event class tests
β”œβ”€β”€ Exceptions/              # Exception tests
β”œβ”€β”€ Facades/                 # Facade tests
β”œβ”€β”€ Integration/             # Integration tests
β”œβ”€β”€ Listeners/               # Listener tests
β”œβ”€β”€ Models/                  # Model tests
β”œβ”€β”€ Services/                # Service tests
β”œβ”€β”€ Traits/                  # Trait tests
β”œβ”€β”€ ExampleTest.php
└── TestCase.php

πŸ“ Changelog

Please see CHANGELOG for more information on what has changed recently.

🀝 Contributing

Please see CONTRIBUTING for details.

πŸ”’ Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

οΏ½οΏ½β€πŸ’» Credits

πŸ“„ License

The MIT License (MIT). Please see License File for more information. bunu incele ve türkçe olarak bana anlat değerlendir