承接 uzbek/laravel-partition-manager 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

uzbek/laravel-partition-manager

Composer 安装命令:

composer require uzbek/laravel-partition-manager

包简介

A Laravel package for managing PostgreSQL partitioned tables

README 文档

README

A powerful Laravel package for managing PostgreSQL partitioned tables. Supports RANGE, LIST, and HASH partitioning strategies with multi-level sub-partitioning, automatic partition generation, and schema management.

Requirements

  • PHP 8.3 or higher
  • Laravel 12.0 or higher
  • PostgreSQL 14 or higher

Table of Contents

Installation

Install the package via Composer:

composer require uzbek/laravel-partition-manager

Optionally publish the configuration:

php artisan vendor:publish --tag=partition-manager-config

Quick Start

use Uzbek\LaravelPartitionManager\Partition;

// Create a partitioned table with 12 monthly partitions
Partition::create('logs', function($table) {
    $table->id();
    $table->string('level');
    $table->text('message');
    $table->timestamp('created_at');
})
->by('created_at')
->monthly();

// Add partitions to an existing table
Partition::for('events')
    ->by('created_at')
    ->daily(30);

Features

  • Three Partitioning Strategies: RANGE, LIST, and HASH partitioning
  • Automatic Partition Generation: Monthly, yearly, daily, weekly, quarterly partitions
  • Multi-Level Sub-Partitioning: Create hierarchical partition structures
  • Schema Management: Per-partition schema assignment with auto-creation
  • Transaction Safety: All operations wrapped in transactions with automatic rollback
  • Partition Lifecycle Management: Attach, detach, drop, analyze, vacuum operations
  • Advanced Options: Default partitions, check constraints, custom tablespaces, partition pruning
  • Multi-Column Partitioning: Partition by multiple columns or custom expressions
  • Artisan Commands: 8 CLI commands for partition management and maintenance
  • Scheduler Integration: Schedule partition rotation and creation via Laravel's scheduler
  • Partition Templates: Reusable partition configurations with placeholder support
  • Model Trait: Partition-aware Eloquent queries with scopes and helper methods
  • Laravel Octane Support: Proper cache management for long-running processes

Partition Types

Range Partitioning

Divide data based on ranges of values, ideal for date-based partitioning.

// Simple syntax with terminal method
Partition::create('orders', function($table) {
    $table->id();
    $table->decimal('amount');
    $table->date('order_date');
})
->by('order_date')
->monthly(24, '2024-01-01');  // 24 partitions from Jan 2024

// Manual partition definition
Partition::create('orders', function($table) {
    $table->id();
    $table->decimal('amount');
    $table->date('order_date');
})
->by('order_date')
->addRangePartition('orders_2024_q1', '2024-01-01', '2024-04-01')
->addRangePartition('orders_2024_q2', '2024-04-01', '2024-07-01')
->generate();  // Required when using addRangePartition()

List Partitioning

Partition data based on discrete values.

Partition::create('users', function($table) {
    $table->id();
    $table->string('country');
    $table->string('email');
})
->by('country')
->addListPartition('users_us', ['US', 'CA'])
->addListPartition('users_eu', ['DE', 'FR', 'IT', 'ES'])
->addListPartition('users_asia', ['JP', 'CN', 'KR'])
->generate();

// Or use addListPartitions for bulk creation
Partition::create('orders', function($table) {
    $table->id();
    $table->string('status');
})
->by('status')
->addListPartitions(['pending', 'processing', 'shipped', 'delivered'])
->generate();

Hash Partitioning

Distribute data evenly across partitions using a hash function.

Partition::create('events', function($table) {
    $table->id();
    $table->string('event_type');
    $table->jsonb('payload');
})
->by('id')
->addHashPartitions(4)
->generate();

Automatic Partition Generation

Terminal Methods

Terminal methods create the table and partitions immediately (no generate() call needed):

Partition::create('logs', function($table) { /* ... */ })
->by('created_at')
->monthly();           // 12 monthly partitions from current month
->monthly(24);         // 24 monthly partitions
->monthly(12, '2024-01-01');  // From specific date

->yearly();            // 5 yearly partitions from current year
->yearly(10, 2020);    // 10 partitions starting from 2020

->daily();             // 30 daily partitions from today
->daily(7, '2024-01-01');

->weekly();            // 12 weekly partitions
->quarterly();         // 8 quarterly partitions

Flexible Date Input

All date parameters accept multiple formats:

// Integer year
->monthly(12, 2024);           // Starts from 2024-01-01
->yearly(5, 2024);             // Starts from 2024-01-01

// Partial date strings
->monthly(12, '2024');         // Starts from 2024-01-01
->monthly(12, '2024-06');      // Starts from 2024-06-01

// Full date strings
->monthly(12, '2024-06-15');   // Starts from 2024-06-01 (aligned to month)

// DateTime/Carbon objects
->monthly(12, new DateTime('2024-06-15'));
->monthly(12, Carbon::now()->subMonth());

// Start from today explicitly
->monthly(12, fromToday: true);

Quick Generation for Existing Tables

// One-liner methods
Partition::monthly('logs', 'created_at', 12);
Partition::yearly('reports', 'year', 5);
Partition::daily('metrics', 'recorded_at', 30);
Partition::weekly('events', 'event_date', 12);
Partition::quarterly('sales', 'sale_date', 8);

// Using the builder for more control
Partition::for('logs')
    ->by('created_at')
    ->schema('log_partitions')
    ->monthly(24);

// List partitions
Partition::for('regions')
    ->byList('country', [
        'us' => ['US', 'CA'],
        'eu' => ['DE', 'FR', 'ES'],
        'asia' => ['JP', 'CN', 'KR']
    ]);

// Hash partitions
Partition::for('users')
    ->byHash('id', 8);

DateRangeBuilder

For advanced date range configurations:

use Uzbek\LaravelPartitionManager\Builders\DateRangeBuilder;

Partition::create('metrics', function($table) {
    $table->id();
    $table->float('value');
    $table->timestamp('recorded_at');
})
->by('recorded_at')
->dateRange(
    DateRangeBuilder::daily()
        ->from('2024-01-01')
        ->count(30)
        ->defaultSchema('daily_metrics')
)
->generate();

Schema Management

Per-Partition Schemas

// Default schema for all partitions
Partition::create('logs', function($table) { /* ... */ })
->by('logged_at')
->schema('log_partitions')
->monthly();

// Different schemas per partition
Partition::create('logs', function($table) { /* ... */ })
->by('logged_at')
->addRangePartition('logs_archive', '2023-01-01', '2024-01-01', 'archive_schema')
->addRangePartition('logs_current', '2024-01-01', '2025-01-01', 'current_schema')
->generate();

SchemaCreator Service

use Uzbek\LaravelPartitionManager\Services\SchemaCreator;

// Ensure schema exists
SchemaCreator::ensure('my_schema');
SchemaCreator::ensure('my_schema', $connection);

// Ensure schema and return prefixed table name
$fullName = SchemaCreator::ensureAndPrefix('my_table', 'my_schema');
// Returns: 'my_schema.my_table'

PartitionSchemaManager Service

use Uzbek\LaravelPartitionManager\Services\PartitionSchemaManager;

$schemaManager = new PartitionSchemaManager();

$schemaManager->setDefault('default_partitions');
$schemaManager->register('error', 'error_log_schema');
$schemaManager->registerMultiple([
    'active' => 'active_schema',
    'archived' => 'archive_schema'
]);

$schema = $schemaManager->getSchemaFor('error');
$hasSchema = $schemaManager->hasSchemaFor('info');

Sub-Partitioning

Create hierarchical partition structures with support for unlimited nesting levels.

Inline Sub-Partitions (Recommended)

The cleanest way to define sub-partitions - no need to repeat partition names:

use Uzbek\LaravelPartitionManager\Builders\SubPartitionBuilder;

Partition::create('events', function($table) {
    $table->id();
    $table->string('type');
    $table->integer('user_id');
    $table->timestamp('created_at');
})
->by('created_at')
->addRangePartition('events_2024_01', '2024-01-01', '2024-02-01',
    subPartitions: SubPartitionBuilder::list('type')
        ->addListPartition('events_2024_01_user', ['login', 'logout'])
        ->addListPartition('events_2024_01_system', ['error', 'warning'])
)
->generate();

Separate Declaration

You can also declare sub-partitions separately using withSubPartitions():

Partition::create('events', function($table) { /* ... */ })
->by('created_at')
->addRangePartition('events_2024_01', '2024-01-01', '2024-02-01')
->withSubPartitions('events_2024_01',
    SubPartitionBuilder::list('type')
        ->addListPartition('events_2024_01_user', ['login', 'logout'])
        ->addListPartition('events_2024_01_system', ['error', 'warning'])
)
->generate();

Apply Same Sub-Partition to All Partitions

Use addSubPartitionToAll() to apply the same sub-partition configuration to all existing partitions:

Partition::create('logs', function($table) {
    $table->id();
    $table->integer('user_id');
    $table->timestamp('created_at');
})
->by('created_at')
->addMonthlyPartitions(12, '2024-01-01')
->addSubPartitionToAll(
    SubPartitionBuilder::hash('user_id')
        ->addHashPartitions(4, '%_shard_')  // % = partition name
        // Creates: logs_m2024_01_shard_0, logs_m2024_01_shard_1, etc.
)
->generate();

The % placeholder in prefixes is replaced with the parent partition name, making it easy to create hierarchical naming.

Multi-Level Nesting

Sub-partitions can have their own sub-partitions, enabling complex hierarchical structures:

// Example: LIST → HASH → RANGE (3 levels deep)
Partition::create('orders', function($table) {
    $table->id();
    $table->string('status');
    $table->integer('user_id');
    $table->timestamp('created_at');
})
->list()
->by('status')
->addListPartition('orders_active', ['pending', 'processing'],
    subPartitions: SubPartitionBuilder::hash('user_id')
        ->addHashPartition('active_shard_0', 4, 0,
            subPartitions: SubPartitionBuilder::range('created_at')
                ->addMonthlyPartitions('m_', 12)
        )
        ->addHashPartition('active_shard_1', 4, 1,
            subPartitions: SubPartitionBuilder::range('created_at')
                ->addMonthlyPartitions('m_', 12)
        )
        ->addHashPartition('active_shard_2', 4, 2)
        ->addHashPartition('active_shard_3', 4, 3)
)
->addListPartition('orders_completed', ['shipped', 'delivered'])
->generate();

Using Value Objects

use Uzbek\LaravelPartitionManager\ValueObjects\RangePartition;
use Uzbek\LaravelPartitionManager\ValueObjects\ListSubPartition;

$partition = RangePartition::range('data_2024_01')
    ->withRange('2024-01-01', '2024-02-01')
    ->withSchema('monthly_data')
    ->withSubPartitions(
        SubPartitionBuilder::list('status')
            ->add(ListSubPartition::create('data_active')
                ->withValues(['active', 'pending'])
                ->withSchema('active_data'))
    );

$builder->addPartition($partition);

Add Sub-Partitions to Existing Partitions

Convert an existing non-partitioned partition into a partitioned one:

use Uzbek\LaravelPartitionManager\Services\PartitionMaintenance;

// Add sub-partitions to an existing partition (auto-detects partition expression)
PartitionMaintenance::addSubPartitions(
    'orders',                    // parent table
    'orders_active',             // existing partition to sub-partition
    SubPartitionBuilder::hash('user_id')->addHashPartitions('shard_', 8)
);

// Or manually specify the partition expression
PartitionMaintenance::addSubPartitions(
    'orders',
    'orders_active',
    SubPartitionBuilder::hash('user_id')->addHashPartitions('shard_', 8),
    "FOR VALUES IN ('active', 'pending')"  // optional, auto-detected if omitted
);

Partition Management

Runtime Operations

use Uzbek\LaravelPartitionManager\Services\PartitionManager;

$manager = app(PartitionManager::class);

// List all partitions
$partitions = $manager->getPartitions('logs');

// Get partition details
$info = $manager->getPartitionInfo('logs', 'logs_2024_01');

// Get partition strategy
$strategy = $manager->getPartitionStrategy('logs');
// Returns: PartitionType::RANGE, LIST, or HASH

// Check if table is partitioned
if ($manager->isPartitioned('logs')) { /* ... */ }

// Get statistics
$size = $manager->getTableSize('logs');
$count = $manager->getPartitionCount('logs');
$oldest = $manager->getOldestPartition('logs');
$newest = $manager->getNewestPartition('logs');

// Drop old partitions
$dropped = $manager->dropOldPartitions('logs', new DateTime('-6 months'));

// Maintenance
$manager->analyzePartition('logs_2024_01');
$manager->vacuumPartition('logs_2024_01', full: true);

Table Operations

use Uzbek\LaravelPartitionManager\Builders\PostgresPartitionBuilder;

$builder = new PostgresPartitionBuilder('orders');

// Attach existing table as partition
$builder->attachPartition('old_orders', 'orders_2023', '2023-01-01', '2024-01-01');

// Detach partition (optionally with CONCURRENTLY)
$builder->detachPartition('orders_2023', concurrently: true);

// Drop partition
$builder->dropPartition('orders_2023');

// Maintenance
$builder->analyze();
$builder->vacuum();
$builder->vacuum(full: true);

Using Facades

use Uzbek\LaravelPartitionManager\Facades\PartitionManager;

$partitions = PartitionManager::getPartitions('logs');
$isPartitioned = PartitionManager::isPartitioned('logs');
$strategy = PartitionManager::getPartitionStrategy('logs');

Artisan Commands

The package provides 8 Artisan commands for managing partitions from the command line:

partition:list

List all partitions for a table with size and row count:

php artisan partition:list orders

# Output:
# +------------------+------------+----------+
# | Partition        | Rows       | Size     |
# +------------------+------------+----------+
# | orders_m2024_01  | 15,234     | 2.1 MB   |
# | orders_m2024_02  | 18,456     | 2.5 MB   |
# | orders_m2024_03  | 12,890     | 1.8 MB   |
# +------------------+------------+----------+

partition:tree

Display partition hierarchy as a tree:

php artisan partition:tree orders --depth=2

# Output:
# orders (partitioned by RANGE)
# ├── orders_2024_01 [2024-01-01 → 2024-02-01]
# ├── orders_2024_02 [2024-02-01 → 2024-03-01]
# └── orders_2024_03 [2024-03-01 → 2024-04-01]

partition:health

Run a health check on partitions:

php artisan partition:health orders

# Checks for:
# - Gaps between partitions
# - Overlapping ranges
# - Missing indexes
# - Orphan data in default partition

partition:ensure-future

Create future partitions proactively:

# Ensure 3 monthly partitions exist
php artisan partition:ensure-future orders created_at --count=3 --interval=monthly

# With custom schema
php artisan partition:ensure-future orders created_at --count=6 --interval=monthly --schema=archive

partition:drop-old

Drop partitions older than a threshold:

# Drop partitions older than 6 months
php artisan partition:drop-old logs --keep=6

# Preview without dropping
php artisan partition:drop-old logs --keep=6 --dry-run

partition:vacuum

Run VACUUM on partitions:

# Vacuum specific partition
php artisan partition:vacuum orders_2024_01

# Vacuum all partitions of a table
php artisan partition:vacuum orders --all

# Full vacuum (reclaims more space, locks table)
php artisan partition:vacuum orders_2024_01 --full

# Vacuum with analyze
php artisan partition:vacuum orders_2024_01 --analyze

partition:reindex

Rebuild indexes on partitions:

# Reindex specific partition
php artisan partition:reindex orders_2024_01

# Reindex concurrently (non-blocking)
php artisan partition:reindex orders_2024_01 --concurrently

# Reindex all partitions
php artisan partition:reindex orders --all

partition:analyze

Update statistics for query planner:

# Analyze specific partition
php artisan partition:analyze orders_2024_01

# Analyze all partitions
php artisan partition:analyze orders --all

Scheduler Integration

Schedule partition maintenance tasks using Laravel's scheduler with a fluent API:

Basic Usage

In your app/Console/Kernel.php or routes/console.php:

use Illuminate\Console\Scheduling\Schedule;

$schedule->partition('orders', 'created_at')
    ->ensureFuture(3, 'monthly')
    ->daily();

$schedule->partition('logs', 'created_at')
    ->rotate(keep: 12)
    ->monthly();

Combined Operations

Run both creation and rotation in a single scheduled task:

$schedule->partition('events', 'created_at')
    ->ensureFuture(3, 'monthly')
    ->rotate(keep: 24)
    ->schema('event_partitions')
    ->dailyAt('02:00')
    ->withoutOverlapping()
    ->onOneServer();

Available Methods

$schedule->partition(string $table, string $column)
    // Partition operations
    ->ensureFuture(int $count, string $interval = 'monthly')  // Create future partitions
    ->rotate(int $keep, bool $dropSchemas = false)            // Drop old partitions
    ->schema(string $schema)                                   // Schema for new partitions

    // Scheduling options (all Laravel scheduler methods available)
    ->daily()
    ->dailyAt('02:00')
    ->weekly()
    ->weeklyOn(1, '03:00')  // Monday at 3 AM
    ->monthly()
    ->monthlyOn(1, '04:00')
    ->cron('0 2 * * *')
    ->timezone('America/New_York')

    // Constraints
    ->withoutOverlapping()
    ->onOneServer()
    ->evenInMaintenanceMode()
    ->runInBackground()

    // Callbacks
    ->onSuccess(fn($result) => Log::info($result->summary()))
    ->onFailure(fn($e) => Log::error($e->getMessage()))
    ->name('partition-maintenance:orders');

Result Object

The scheduled task returns a PartitionScheduleResult object:

$schedule->partition('orders', 'created_at')
    ->ensureFuture(3, 'monthly')
    ->rotate(keep: 12)
    ->daily()
    ->onSuccess(function (PartitionScheduleResult $result) {
        if ($result->hasChanges()) {
            Log::info($result->summary());
            // Output: "Partition maintenance: created 2 partition(s), dropped 1 partition(s)"
        }

        echo $result->partitionsCreated;      // Number created
        echo count($result->partitionsDropped); // Names of dropped partitions
    });

Partition Templates

Templates provide reusable partition configurations that can be applied to multiple tables. The % placeholder is replaced with the table name.

Configuration

Define templates in config/partition-manager.php:

'templates' => [
    'monthly_archive' => [
        'type' => 'range',
        'column' => 'created_at',
        'interval' => 'monthly',
        'count' => 12,
        'schema' => '%_archive',        // orders → orders_archive
        'default_partition' => true,
        'future_partitions' => 3,
    ],

    'tenant_hash' => [
        'type' => 'hash',
        'column' => 'tenant_id',
        'modulus' => 16,
        'schema' => 'tenants',
    ],

    'status_list' => [
        'type' => 'list',
        'column' => 'status',
        'values' => ['pending', 'active', 'completed', 'cancelled'],
        'default_partition' => true,
    ],
],

Using Templates in Migrations

use Uzbek\LaravelPartitionManager\Partition;

// Apply template from config
Partition::create('orders', function($table) {
    $table->id();
    $table->decimal('amount');
    $table->timestamp('created_at');
})
->fromTemplate('monthly_archive')
->generate();

// Override template settings
Partition::create('events', function($table) {
    $table->id();
    $table->timestamp('created_at');
})
->fromTemplate('monthly_archive', [
    'count' => 24,
    'schema' => 'event_archive',
])
->generate();

Programmatic Templates

Create templates programmatically:

use Uzbek\LaravelPartitionManager\Templates\PartitionTemplate;

$template = PartitionTemplate::define('custom')
    ->range('created_at')
    ->monthly(12)
    ->withSchema('%_partitions')
    ->withDefaultPartition()
    ->withFuturePartitions(3);

Partition::create('logs', fn($t) => $t->id()->timestamps())
    ->fromTemplate($template)
    ->generate();

Template Methods

PartitionTemplate::define(string $name)
    // Partition type
    ->range(string|array $columns)
    ->list(string $column)
    ->hash(string $column, int $modulus)

    // Intervals (for RANGE)
    ->daily(int $count = 30)
    ->weekly(int $count = 12)
    ->monthly(int $count = 12)
    ->yearly(int $count = 5)

    // LIST values
    ->withValues(array $values)

    // Options
    ->withSchema(string $schema)           // Supports % placeholder
    ->withTablespace(string $tablespace)
    ->withPrefix(string $prefix)           // Supports % placeholder
    ->withDefaultPartition(bool $enabled = true)
    ->withFuturePartitions(int $count);

Model Trait

The HasPartitions trait adds partition-aware methods to your Eloquent models:

Basic Setup

use Illuminate\Database\Eloquent\Model;
use Uzbek\LaravelPartitionManager\Traits\HasPartitions;

class Order extends Model
{
    use HasPartitions;

    // Optional: specify the partition column (default: 'created_at')
    protected static string $partitionColumn = 'created_at';
}

Query Scopes

Query specific partitions directly:

// Query a specific partition by name
Order::inPartition('orders_m2024_01')->where('status', 'pending')->get();

// Query by date range (enables partition pruning)
Order::inPartitionRange('2024-01-01', '2024-03-31')->get();

// Query by value (for LIST partitions)
Order::inPartitionValue('active')->get();
Order::inPartitionValues(['pending', 'processing'])->get();

Partition Information

// Check if table is partitioned
if (Order::isPartitioned()) {
    // Get all partitions
    $partitions = Order::getPartitions();

    // Get partition strategy (RANGE, LIST, HASH)
    $strategy = Order::getPartitionStrategy();

    // Get statistics
    $stats = Order::getPartitionStats();

    // Get boundaries
    $boundaries = Order::getPartitionBoundaries();
}

Finding Partitions

// Find partition for a specific date (RANGE)
$partitionName = Order::getPartitionForDate('2024-06-15');
// Returns: 'orders_m2024_06' or null

// Check if partition exists for date
if (Order::hasPartitionForDate('2024-12-01')) {
    // Safe to insert
}

// Find partition for a value (LIST)
$partitionName = Order::getPartitionForValue('active');

Health and Statistics

// Run health check
$health = Order::partitionHealthCheck();
// Returns: ['gaps' => [...], 'overlaps' => [...], 'missing_indexes' => [...], 'orphan_data' => bool]

// Estimate total rows
$totalRows = Order::estimateTotalRows();

// Count rows in specific partition
$count = Order::countInPartition('orders_m2024_01');

// Get oldest/newest partition
$oldest = Order::getOldestPartition();
$newest = Order::getNewestPartition();

Query Analysis

// Explain partition pruning for a query
$query = Order::where('created_at', '>=', '2024-01-01')
    ->where('created_at', '<', '2024-04-01');

$analysis = Order::explainPartitionPruning($query);
// Returns:
// [
//     'partitions_scanned' => ['orders_m2024_01', 'orders_m2024_02', 'orders_m2024_03'],
//     'total_partitions' => 12,
//     'pruning_effective' => true,
//     'plan' => '...'
// ]

// Print partition tree
echo Order::printPartitionTree();

All Trait Methods

// Static methods
Order::getPartitionColumn(): string
Order::isPartitioned(): bool
Order::getPartitions(): array
Order::getPartitionStrategy(): ?string
Order::getPartitionStats(): array
Order::getPartitionBoundaries(): array
Order::partitionHealthCheck(): array
Order::estimateTotalRows(): int
Order::getPartitionForDate(string $date): ?string
Order::getPartitionForValue(mixed $value): ?string
Order::hasPartitionForDate(string $date): bool
Order::explainPartitionPruning(?Builder $query = null): array
Order::printPartitionTree(): string
Order::countInPartition(string $partitionName): int
Order::getOldestPartition(): ?object
Order::getNewestPartition(): ?object

// Query scopes
Order::inPartition(string $partitionName)
Order::inPartitionRange(string $from, string $to)
Order::inPartitionValue(mixed $value)
Order::inPartitionValues(array $values)

Advanced Features

PostgreSQL Enum Types

The package provides a pgEnum Blueprint macro for creating PostgreSQL native enum types:

use Uzbek\LaravelPartitionManager\Partition;

Partition::create('orders', function($table) {
    $table->id();
    $table->pgEnum('status', ['pending', 'processing', 'shipped', 'delivered']);
    $table->pgEnum('priority', ['low', 'medium', 'high'], 'order_priority'); // custom type name
    $table->timestamp('created_at');
})
->list()
->by('status')
->addListPartition('orders_pending', ['pending', 'processing'])
->addListPartition('orders_shipped', ['shipped', 'delivered'])
->generate();

The macro:

  • Creates a PostgreSQL ENUM type with the specified values
  • Registers the type with Laravel's grammar for proper SQL compilation
  • Auto-generates type name as {singular_table}_{column}_enum if not specified
  • Handles duplicate type creation gracefully (won't error if type already exists)

Conditional (Partial) Indexes

The conditionalIndex macro creates PostgreSQL partial indexes with WHERE clauses and optional column ordering. Follows Laravel's convention with columns first and optional auto-generated name:

Schema::table('promo_codes', function (Blueprint $table) {
    // Simple partial index with raw WHERE string (auto-generated name)
    $table->conditionalIndex(['code'], "status = 'void'");
    // Creates: promo_codes_code_idx

    // With custom name
    $table->conditionalIndex(['code'], "status = 'void'", 'idx_pc_void');

    // Laravel-style closure for WHERE clause
    $table->conditionalIndex(['source_type', 'source_id'], function ($query) {
        $query->whereIn('status', ['new', 'dispensed'])
              ->whereNotNull('source_type');
    }, 'idx_pc_source');
});

Schema::table('user_daily_points', function (Blueprint $table) {
    // Index with column ordering (no WHERE clause)
    $table->conditionalIndex(['date', 'points' => 'DESC']);

    // Alternative syntax: ordering in column string
    $table->conditionalIndex(['date ASC', 'points DESC'], name: 'idx_udp_sorted');

    // With closure and ordering
    $table->conditionalIndex(
        ['created_at' => 'DESC', 'score' => 'DESC'],
        fn($q) => $q->where('status', 'active'),
        'idx_active_users'
    );

    // Custom index method (e.g., GIN for JSONB columns)
    $table->conditionalIndex(['metadata'], method: 'gin');
});

Parameters:

  • $columns: Array of columns with optional ordering
    • String value: column name (e.g., 'code' or 'points DESC')
    • Key-value: 'column' => 'ASC' or 'column' => 'DESC'
  • $where: Optional WHERE clause - raw string or Laravel-style closure
  • $name: Optional index name (auto-generated as {table}_{columns}_idx if null)
  • $method: Index method - btree (default), hash, gist, gin, brin

Default Partitions

Catch rows that don't match any defined partition:

->withDefaultPartition('others')

Check Constraints

->check('positive_amount', 'amount > 0')
->check('valid_status', "status IN ('pending', 'completed')")

Partition Pruning

->enablePartitionPruning()      // Enable query optimization
->detachConcurrently()          // Non-blocking detach operations

Custom Tablespaces

->tablespace('fast_ssd')

Custom Partition Expressions

->partitionByExpression("DATE_TRUNC('week', event_time AT TIME ZONE timezone)")
->partitionByYear('order_date')
->partitionByMonth('created_at')
->partitionByDay('logged_at')

Multi-Column Partitioning

Partition::create('sales', function($table) {
    $table->id();
    $table->string('region');
    $table->integer('year');
    $table->decimal('amount');
})
->by(['region', 'year'])
->addRangePartition('sales_us_2024', ['US', 2024], ['US', 2025])
->generate();

Configuration

// config/partition-manager.php
return [
    'default_connection' => env('DB_CONNECTION', 'pgsql'),

    'defaults' => [
        'enable_partition_pruning' => true,
        'detach_concurrently' => true,
        'analyze_after_create' => true,
        'vacuum_after_drop' => true,
    ],

    'naming' => [
        'prefix' => '',
        'suffix' => '',
        'separator' => '_',
        'date_format' => 'Y_m',
        'day_format' => 'Y_m_d',
    ],

    'schemas' => [
        'auto_create' => [],
        'default' => null,
        'mappings' => [],
    ],

    'logging' => [
        'enabled' => env('PARTITION_LOGGING', true),
        'channel' => env('PARTITION_LOG_CHANNEL', 'daily'),
    ],
];

Service Classes

The package provides standalone service classes for specific partition operations:

PartitionStats

Get statistics and health information about partitions:

use Uzbek\LaravelPartitionManager\Services\PartitionStats;

// Get partition statistics (row count, size)
$stats = PartitionStats::get('orders');
// Returns: partition_name, row_count, size_bytes, size_pretty

// Get partition boundaries
$boundaries = PartitionStats::boundaries('orders');
// Returns: partition_name, partition_type, from_value, to_value

// Estimate row counts
$rowCount = PartitionStats::estimateRowCount('orders');
$totalRows = PartitionStats::estimateTotalRowCount('orders');

// Health check
$health = PartitionStats::healthCheck('orders');
// Returns: gaps, overlaps, missing_indexes, orphan_data

// Find specific issues
$gaps = PartitionStats::findGaps('orders');
$overlaps = PartitionStats::findOverlaps('orders');
$missingIndexes = PartitionStats::findMissingIndexes('orders');

// Analyze query pruning
$pruning = PartitionStats::explainPruning(
    "SELECT * FROM orders WHERE created_at >= '2024-01-01'"
);
// Returns: partitions_scanned, total_partitions, pruning_effective, plan

// Tree visualization
$tree = PartitionStats::getTree('orders');
echo PartitionStats::printTree('orders');
// Output:
// orders
// ├── orders_2024_01 [2024-01-01 → 2024-02-01]
// ├── orders_2024_02 [2024-02-01 → 2024-03-01]
// └── orders_2024_03 [2024-03-01 → 2024-04-01]

PartitionMaintenance

Perform maintenance operations on partitions:

use Uzbek\LaravelPartitionManager\Services\PartitionMaintenance;
use Uzbek\LaravelPartitionManager\Builders\SubPartitionBuilder;

// Vacuum partition (reclaim storage)
PartitionMaintenance::vacuum('orders_2024_01');
PartitionMaintenance::vacuum('orders_2024_01', full: true);
PartitionMaintenance::vacuum('orders_2024_01', analyze: true);

// Vacuum all partitions of a table
PartitionMaintenance::vacuumAll('orders');
PartitionMaintenance::vacuumAll('orders', full: true);

// Analyze partition (update statistics)
PartitionMaintenance::analyze('orders_2024_01');
PartitionMaintenance::analyzeAll('orders');

// Reindex partition
PartitionMaintenance::reindex('orders_2024_01');
PartitionMaintenance::reindex('orders_2024_01', concurrently: true);
PartitionMaintenance::reindexAll('orders');

// Rebalance hash partitions (change modulus)
PartitionMaintenance::rebalanceHash('events', newModulus: 16);
PartitionMaintenance::rebalanceHash('events', newModulus: 16, schema: 'partitions');

// Add sub-partitions to an existing partition
PartitionMaintenance::addSubPartitions(
    'orders',
    'orders_active',
    SubPartitionBuilder::hash('user_id')->addHashPartitions('shard_', 8)
);

// Get partition expression for an existing partition
$expr = PartitionMaintenance::getPartitionExpression('orders', 'orders_active');
// Returns: "FOR VALUES IN ('active', 'pending')"

// Get commands for parallel execution via jobs/CLI
$commands = PartitionMaintenance::getParallelVacuumCommands('orders');
$commands = PartitionMaintenance::getParallelReindexCommands('orders', concurrently: true);
$commands = PartitionMaintenance::getParallelAnalyzeCommands('orders');

// Dry run mode - returns SQL without executing
$queries = PartitionMaintenance::dryRun(function() {
    PartitionMaintenance::vacuum('orders_2024_01');
    PartitionMaintenance::analyze('orders_2024_01');
});

PartitionConsolidator

Merge multiple partitions into larger ones:

use Uzbek\LaravelPartitionManager\Services\PartitionConsolidator;

// Merge specific partitions
PartitionConsolidator::merge(
    'orders',
    ['orders_2024_01', 'orders_2024_02', 'orders_2024_03'],
    'orders_2024_q1',
    '2024-01-01',
    '2024-04-01'
);

// Consolidate monthly partitions into yearly
PartitionConsolidator::monthlyToYearly('orders', 2024, 'orders_m');

// Consolidate daily partitions into weekly
PartitionConsolidator::dailyToWeekly('logs', '2024-01-01', 'logs_d');

// Consolidate daily partitions into monthly
PartitionConsolidator::dailyToMonthly('logs', 2024, 1, 'logs_d');

// Consolidate weekly partitions into monthly
PartitionConsolidator::weeklyToMonthly('events', 2024, 1, 'events_w');

// Merge all partitions within a date range
PartitionConsolidator::range(
    'orders',
    '2024-01-01',
    '2024-07-01',
    'orders_2024_h1'
);

PartitionSplitter

Split partitions into smaller granularity:

use Uzbek\LaravelPartitionManager\Services\PartitionSplitter;

// Split yearly partition into monthly
PartitionSplitter::yearlyToMonthly('orders', 'orders_y2024', 2024);

// Split yearly partition into weekly
PartitionSplitter::yearlyToWeekly('events', 'events_y2024', 2024);

// Split monthly partition into daily
PartitionSplitter::monthlyToDaily('logs', 'logs_m2024_01', 2024, 1);

// Split monthly partition into weekly
PartitionSplitter::monthlyToWeekly('events', 'events_m2024_01', 2024, 1);

// Custom split with specific ranges
PartitionSplitter::custom('orders', 'orders_q1', [
    'orders_jan' => ['from' => '2024-01-01', 'to' => '2024-02-01'],
    'orders_feb' => ['from' => '2024-02-01', 'to' => '2024-03-01'],
    'orders_mar' => ['from' => '2024-03-01', 'to' => '2024-04-01'],
]);

PartitionRotation

Manage partition lifecycle with rotation policies:

use Uzbek\LaravelPartitionManager\Services\PartitionRotation;

// Ensure future partitions exist (column and interval are auto-detected if omitted)
$created = PartitionRotation::ensureFuture('orders', count: 3);
$created = PartitionRotation::ensureFuture('orders', 3, 'created_at', 'monthly');

// Rotate old partitions (keep only the most recent N partitions)
$dropped = PartitionRotation::rotate('logs', keep: 12);

// Add monthly partitions for an entire year
// Year accepts: int, string ("2025"), or DateTimeInterface
PartitionRotation::addMonthlyForYear('orders', 'created_at', 2025);
PartitionRotation::addMonthlyForYear('orders', 'created_at', '2025');

PartitionIndex

Manage indexes on partitions:

use Uzbek\LaravelPartitionManager\Services\PartitionIndex;

// Create index on partition
PartitionIndex::create('orders_2024_01', 'idx_orders_customer', ['customer_id']);

// Create unique index
PartitionIndex::create('orders_2024_01', 'idx_orders_ref', ['reference'], unique: true);

// Create index with specific method (btree, hash, gist, gin, brin)
PartitionIndex::create('logs_2024_01', 'idx_logs_message', ['message'], method: 'gin');

// Create index concurrently (non-blocking)
PartitionIndex::createConcurrently('orders_2024_01', 'idx_customer', ['customer_id']);

// Drop index
PartitionIndex::drop('idx_orders_customer');
PartitionIndex::drop('idx_orders_customer', cascade: true);
PartitionIndex::dropConcurrently('idx_orders_customer');

// List indexes on partition
$indexes = PartitionIndex::list('orders_2024_01');

PartitionExport

Export partition data to files:

use Uzbek\LaravelPartitionManager\Services\PartitionExport;

// Export to SQL file
PartitionExport::toSql('orders_2024_01', '/backup/orders_2024_01.sql');

// Export to compressed SQL
PartitionExport::toCompressedSql('orders_2024_01', '/backup/orders_2024_01.sql.gz');

// Export to CSV
PartitionExport::toCsv('orders_2024_01', '/backup/orders_2024_01.csv');

// Get export command (without executing)
$command = PartitionExport::getExportCommand('orders_2024_01', '/backup/orders.sql');

API Reference

Partition (Static Helper)

Partition::create(string $table, Closure $callback): PostgresPartitionBuilder
Partition::for(string $table): QuickPartitionBuilder

// Quick generation
Partition::monthly(string $table, string $column, int $count = 12): void
Partition::yearly(string $table, string $column, int $count = 5): void
Partition::daily(string $table, string $column, int $count = 30): void
Partition::weekly(string $table, string $column, int $count = 12): void
Partition::quarterly(string $table, string $column, int $count = 8): void

// Queries
Partition::isPartitioned(string $table): bool
Partition::getPartitions(string $table): array
Partition::partitionExists(string $table, string $partitionName): bool
Partition::dropIfExists(string $table, bool $withSchema = false): void
Partition::dropSchemaIfEmpty(string $schema): void
Partition::dropSchemaIfExists(string $schema): void

// Attach partitions (for all partition types)
Partition::attachPartition(string $table, string $partitionName, mixed $from, mixed $to): void  // RANGE
Partition::attachListPartition(string $table, string $partitionName, array $values): void       // LIST
Partition::attachHashPartition(string $table, string $partitionName, int $modulus, int $remainder): void  // HASH

// Detach partition
Partition::detachPartition(string $table, string $partitionName, bool $concurrently = false): void

PartitionType Enum

use Uzbek\LaravelPartitionManager\Enums\PartitionType;

PartitionType::RANGE    // 'RANGE'
PartitionType::LIST     // 'LIST'
PartitionType::HASH     // 'HASH'

// Helper methods
$type->isRange(): bool
$type->isList(): bool
$type->isHash(): bool

// PostgreSQL strategy codes
PartitionType::PG_STRATEGY_RANGE  // 'r'
PartitionType::PG_STRATEGY_LIST   // 'l'
PartitionType::PG_STRATEGY_HASH   // 'h'

// Create from PostgreSQL strategy code
PartitionType::fromPgStrategy('r')  // Returns PartitionType::RANGE

PostgresPartitionBuilder

// Configuration
->setBlueprint(Blueprint $blueprint): self
->connection(string $connection): self
->partition(PartitionType|string $type): self  // Optional: auto-detected from add*Partition calls
->range(): self   // Optional: auto-detected
->list(): self    // Optional: auto-detected
->hash(): self    // Optional: auto-detected
->by(string|array $columns): self
->partitionByExpression(string $expression): self

// Adding single partitions (all support optional subPartitions parameter)
->addPartition(PartitionDefinition $partition): self
->addRangePartition(string $name, mixed $from, mixed $to, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addListPartition(string $name, array $values, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addHashPartition(string $name, int $modulus, int $remainder, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self

// Adding multiple partitions at once (non-terminal)
// All prefix parameters support % as placeholder for table name
// All date parameters accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->addListPartitions(array $values, ?string $schema = null): self
// Examples: ['new', 'void', 'used'] or [true => '%_active', false => '%_inactive']
->addHashPartitions(int $modulus, ?string $prefix = null, ?string $schema = null): self
// Example: addHashPartitions(4, '%_shard_') creates: orders_shard_0, orders_shard_1, etc.
->addMonthlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addYearlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addWeeklyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addDailyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->dateRange(DateRangeBuilder $builder): self

// Terminal methods (execute immediately, includes generate())
// All accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->monthly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->yearly(int $count = 5, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->daily(int $count = 30, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->weekly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->quarterly(int $count = 8, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void

// Schema management
->schema(string $schema): self
->schemaFor(string $partitionType, string $schema): self
->schemasFor(array $schemas): self

// Sub-partitions
->withSubPartitions(string $partitionName, SubPartitionBuilder $builder): self
->addSubPartitionToAll(AbstractSubPartitionBuilder $builder): self  // Apply same sub-partition to all partitions

// Advanced options
->withDefaultPartition(string $name = 'default'): self
->tablespace(string $tablespace): self
->check(string $name, string $expression): self
->enablePartitionPruning(bool $enable = true): self
->detachConcurrently(bool $enable = true): self

// Execution
->generate(): void

// Runtime operations
->attachPartition(string $tableName, string $partitionName, mixed $from, mixed $to): self
->detachPartition(string $partitionName, ?bool $concurrently = null): self
->dropPartition(string $partitionName): self
->analyze(): self
->vacuum(bool $full = false): self

QuickPartitionBuilder

QuickPartitionBuilder::table(string $table): self

->by(string $column): self
->schema(string $schema): self
->connection(string $connection): self

// Range partitions
// All accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->monthly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->yearly(int $count = 5, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->daily(int $count = 30, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->weekly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->quarterly(int $count = 8, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void

// List and hash
->byList(string $column, array $partitions): void
->byHash(string $column, int $count = 4): void

DateRangeBuilder

DateRangeBuilder::monthly(): self
DateRangeBuilder::yearly(): self
DateRangeBuilder::daily(): self
DateRangeBuilder::weekly(): self
DateRangeBuilder::quarterly(): self

// Date parameters accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->from(int|string|DateTimeInterface $date): self
->to(int|string|DateTimeInterface $date): self
->count(int $count): self
->interval(string $interval): self
->nameFormat(string $format): self
->schema(string $schema): self
->build(string $prefix = ''): array

SubPartitionBuilder

SubPartitionBuilder::list(string $column): ListSubPartitionBuilder
SubPartitionBuilder::range(string $column): RangeSubPartitionBuilder
SubPartitionBuilder::hash(string $column): HashSubPartitionBuilder

// Common methods
->for(string $baseName): self  // Set base name for auto-generating partition names
->table(string $tableName): self  // Set table name for column type lookups (auto-set when used via PostgresPartitionBuilder)
->schema(string $schema): self
->add(SubPartition $partition): self
->getBaseName(): ?string
->getTableName(): ?string
->getPartitionType(): PartitionType
->getPartitionColumn(): string
->toArray(?string $defaultSchema = null): array

// ListSubPartitionBuilder
->addListPartition(string $name, array $values, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addListPartitions(array $values, ?string $schema = null): self
// Examples:
//   ->addListPartitions(['new', 'void', 'used'])  // auto-generated names using baseName
//   ->addListPartitions(['new' => '%_new', 'void' => '%_void'])  // % = baseName placeholder
//   ->addListPartitions(['false' => 'inactive', 'true' => 'active'])  // casts based on column type (auto-detected)

// RangeSubPartitionBuilder
// All date parameters accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->addRangePartition(string $name, mixed $from, mixed $to, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addYearlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addMonthlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addWeeklyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addDailyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
// If prefix is null, auto-generates using baseName: {baseName}_y, {baseName}_m, {baseName}_w, {baseName}_d

// HashSubPartitionBuilder
->addHashPartition(string $name, int $modulus, int $remainder, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addHashPartitions(int $modulus, ?string $prefix = null, ?string $schema = null): self
// If prefix is null, auto-generates using baseName: {baseName}_p

Value Objects

// RangePartition
RangePartition::range(string $name)
    ->withRange(mixed $from, mixed $to): self
    ->withSchema(string $schema): self
    ->withSubPartitions(SubPartitionBuilder $builder): self

// ListPartition
ListPartition::list(string $name)
    ->withValues(array $values): self
    ->withValue(mixed $value): self
    ->withSchema(string $schema): self

// HashPartition
HashPartition::hash(string $name)
    ->withHash(int $modulus, int $remainder): self
    ->withSchema(string $schema): self

// Sub-partition value objects (all support nested sub-partitions)
RangeSubPartition::create(string $name)
    ->withRange(mixed $from, mixed $to): self
    ->withSchema(string $schema): self
    ->withTablespace(string $tablespace): self
    ->withSubPartitions(AbstractSubPartitionBuilder $builder): self

ListSubPartition::create(string $name)
    ->withValues(array $values): self
    ->withSchema(string $schema): self
    ->withTablespace(string $tablespace): self
    ->withSubPartitions(AbstractSubPartitionBuilder $builder): self

HashSubPartition::create(string $name)
    ->withHash(int $modulus, int $remainder): self
    ->withSchema(string $schema): self
    ->withTablespace(string $tablespace): self
    ->withSubPartitions(AbstractSubPartitionBuilder $builder): self

// Common SubPartition methods
->getName(): string
->getSchema(): ?string
->getTablespace(): ?string
->hasSubPartitions(): bool
->getSubPartitions(): ?AbstractSubPartitionBuilder
->toArray(): array

PartitionManager Service

// Query
->getPartitions(string $table, ?string $connection = null): array
->getPartitionInfo(string $table, string $partitionName, ?string $connection = null): ?object
->isPartitioned(string $table, ?string $connection = null): bool
->getPartitionStrategy(string $table, ?string $connection = null): ?PartitionType
->getPartitionColumns(string $table, ?string $connection = null): array
->getTableSize(string $table, ?string $connection = null): string
->getPartitionCount(string $table, ?string $connection = null): int
->getOldestPartition(string $table, ?string $connection = null): ?object
->getNewestPartition(string $table, ?string $connection = null): ?object

// Maintenance
->analyzePartition(string $partitionName, ?string $connection = null): void
->vacuumPartition(string $partitionName, bool $full = false, ?string $connection = null): void
->dropOldPartitions(string $table, DateTime $before, ?string $connection = null): array

License

MIT

uzbek/laravel-partition-manager 适用场景与选型建议

uzbek/laravel-partition-manager 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 34 次下载、GitHub Stars 达 2, 最近一次更新时间为 2025 年 12 月 03 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「database」 「postgresql」 「laravel」 「Partition」 「table-partitioning」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 uzbek/laravel-partition-manager 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 uzbek/laravel-partition-manager 我们能提供哪些服务?
定制开发 / 二次开发

基于 uzbek/laravel-partition-manager 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 34
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 2
  • 点击次数: 37
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 2
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-12-03