ghostcompiler/ghostgrid 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

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

ghostcompiler/ghostgrid

最新稳定版本:v1.0.0

Composer 安装命令:

composer require ghostcompiler/ghostgrid

包简介

Dynamic multi-tenant reseller commerce, hierarchical pricelists, storefront products, orders, services, and action-based provisioning for Laravel.

README 文档

README

Version: 1.0.0

GhostGrid is a Laravel backend package for tenant-based reseller commerce. It gives your app accounts, account hierarchy, providers, product variants, pricelists, storefront products, order snapshots, provisioning tasks, and services.

It is not an admin panel. You build your own panel, API, checkout, billing area, or storefront on top of the package.

Supports Laravel 10, 11, 12, and 13. Laravel 13 is tested on PHP 8.3+.

Install

composer require ghostcompiler/ghostgrid
php artisan gg:init
php artisan migrate

Manual publish:

php artisan vendor:publish --tag=ghostgrid-config
php artisan vendor:publish --tag=ghostgrid-migrations

Setup User Model

Add the trait to app/Models/User.php:

use GhostCompiler\GhostGrid\Support\Concerns\HasGhostGrid;

class User extends Authenticatable
{
    use HasGhostGrid;
}

Now your controllers can use the short helper:

$priceLists = ghostgrid()->priceLists();
$active = ghostgrid()->activePriceList();
$products = ghostgrid()->visibleStorefrontProducts();

In a job, seeder, or test where there is no logged-in user, pass the user:

$priceLists = ghostgrid($user)->priceLists();

You can also use the facade:

use GhostCompiler\GhostGrid\Facades\GhostGrid;

$grid = GhostGrid::forUser(auth()->user());

Mental Model

Your Laravel users table handles login. GhostGrid accounts are commerce identities.

Laravel User
  -> account_users
      -> GhostGrid Account
          -> Price Lists
          -> Storefront Products
          -> Orders
          -> Services

So account_id means accounts.id, not users.id.

The helper hides this most of the time:

$account = ghostgrid()->account();
$accounts = ghostgrid()->accounts();

Quick Start

Create the seller account:

$account = ghostgrid()->createAccount([
    'name' => 'Acme Hosting',
    'type' => 'reseller',
]);

Create a provider record:

$provider = ghostgrid()->createProvider('hetzner', 'Hetzner');

Create a backend product, variant, and provider SKU mapping:

[$product, $variant] = ghostgrid()->createProduct(
    product: [
        'name' => 'Cloud VPS',
        'slug' => 'cloud-vps',
        'type' => 'vps',
    ],
    variant: [
        'name' => 'Hetzner CX32',
        'slug' => 'cx32',
        'billing_cycle' => 'monthly',
        'specs' => ['ram' => '4 GB', 'disk' => '80 GB SSD'],
    ],
    provider: $provider,
    mapping: [
        'provider_sku' => 'cx32',
        'priority' => 100,
        'config' => ['region' => 'fsn1'],
    ],
);

Create and assign a pricelist:

$priceList = ghostgrid()->createPriceList('Default VPS Pricing', [
    [
        'product_variant_id' => $variant->id,
        'billing_cycle' => 'monthly',
        'selling_price' => 1499,
        'setup_fee' => 0,
    ],
]);

Create a frontend/storefront product:

$storefrontProduct = ghostgrid()->createStorefrontProduct([
    'product_variant_id' => $variant->id,
    'price_list_id' => $priceList->id,
    'name' => 'Premium VPS 4GB',
    'slug' => 'premium-vps-4gb',
    'description' => 'Fast cloud server for growing sites',
    'actual_price' => 2000,
    'price' => 1499,
    'popular' => true,
    'visible' => true,
    'featured' => true,
    'features' => [
        ['label' => '4 GB RAM', 'icon' => 'server'],
        ['label' => '80 GB SSD', 'icon' => 'hard-drive'],
        ['label' => 'Free Setup', 'icon' => null],
    ],
]);

Show products on your frontend:

$products = ghostgrid()->visibleStorefrontProducts();

Pricelists

Most apps only need these methods:

// All pricelists owned by accounts linked to the current user.
$priceLists = ghostgrid()->priceLists();

// Active/default assigned pricelist for the current account.
$active = ghostgrid()->activePriceList();

// Pricelists that have an assignment.
$assigned = ghostgrid()->assignedPriceLists();

// Draft pricelists with no assignment.
$drafts = ghostgrid()->unassignedPriceLists();

// Find by ID or name, scoped to the current user.
$priceList = ghostgrid()->priceList('Default VPS Pricing');

Create a draft pricelist without assigning it:

$draft = ghostgrid()->createUnassignedPriceList('Draft Sale Pricing', [
    [
        'product_variant_id' => $variant->id,
        'billing_cycle' => 'monthly',
        'selling_price' => 1299,
    ],
]);

This is the same as:

$draft = ghostgrid()->createPriceList(
    name: 'Draft Sale Pricing',
    items: [
        [
            'product_variant_id' => $variant->id,
            'billing_cycle' => 'monthly',
            'selling_price' => 1299,
        ],
    ],
    assign: false,
);

Assign an existing pricelist later:

$assignment = ghostgrid()->assignPriceList($draft);

Assign it to a specific linked account:

$assignment = ghostgrid()->assignPriceList(
    priceList: $draft,
    account: $clientAccount,
);

Assign with a date window:

$assignment = ghostgrid()->assignPriceList(
    priceList: $draft,
    account: $clientAccount,
    isDefault: true,
    startsAt: '2026-06-01 00:00:00',
    endsAt: '2026-06-30 23:59:59',
);

Master Pricelist Sharing

GhostGrid supports a master pricelist that acts as the source of products a tenant may sell.

When a tenant creates a pricelist from a master:

  • products with tenant price overrides become active
  • products without tenant prices are copied as inactive
  • inactive state is stored on price_list_items.enabled
  • this is not a storefront visibility feature
$tenantPriceList = ghostgrid()->createPriceListFromParent(
    parent: $masterPriceList,
    name: 'Tenant VPS Pricing',
    itemOverrides: [
        $variant->id => [
            'selling_price' => 1499,
            'enabled' => true,
        ],
    ],
);

If a master product has no tenant price, GhostGrid creates it like this:

[
    'product_variant_id' => $variant->id,
    'selling_price' => '1000.00',
    'enabled' => false,
    'metadata' => [
        'inherited_from_parent' => true,
        'tenant_price_set' => false,
    ],
]

When the master pricelist gets new products, sync missing items into the tenant pricelist as inactive rows:

$createdInactiveItems = ghostgrid()->syncPriceListWithParent($tenantPriceList);

Pricing Rules

By default:

  • a child price cannot go below the parent selling_price
  • if parent min_selling_price exists, that value becomes the minimum
  • disabled parent products cannot be sold by child pricelists
  • archived pricelists do not resolve as active pricelists
  • helper methods only mutate records owned by accounts linked to the current user

Allow selling below the parent:

'allow_selling_below_parent' => true,

Storefront Products

Backend variants are the real sellable products. Storefront products are frontend display records.

Example:

Backend variant: Hetzner CX32
Storefront product: Premium VPS 4GB

Create:

$storefrontProduct = ghostgrid()->createStorefrontProduct([
    'product_variant_id' => $variant->id,
    'price_list_id' => $priceList->id,
    'name' => 'Premium VPS 4GB',
    'slug' => 'premium-vps-4gb',
    'description' => 'Fast cloud server for growing sites',
    'actual_price' => 2000,
    'price' => 1499,
    'popular' => true,
    'features' => [
        ['label' => '4 GB RAM', 'icon' => 'server'],
        ['label' => 'Free Setup', 'icon' => null],
    ],
]);

Read visible products:

$products = ghostgrid()->visibleStorefrontProducts();

Hide:

ghostgrid()->hideStorefrontProduct($storefrontProduct);

Orders

Use CreateOrderFromCart after the account has an active assigned pricelist.

use GhostCompiler\GhostGrid\Actions\CreateOrderFromCart;

$order = app(CreateOrderFromCart::class)->handle(
    accountId: ghostgrid()->account()->id,
    cartItems: [
        [
            'product_variant_id' => $variant->id,
            'billing_cycle' => 'monthly',
            'quantity' => 1,
        ],
    ],
);

GhostGrid snapshots at order time:

  • product name
  • variant name
  • unit price
  • setup fee
  • provider ID
  • provider SKU
  • provider mapping config

Payment To Provisioning

After payment succeeds:

$paidOrder = ghostgrid()->markOrderPaid(
    order: $order,
    createProvisioningTasks: true,
    dispatch: true,
);

Create tasks without dispatching:

$tasks = ghostgrid()->createProvisioningTasksForOrder(
    order: $order,
    dispatch: false,
);

Provisioning task creation is idempotent per order item, provider, and action. If your webhook runs twice, duplicate create-tasks are not created.

Provider Adapters

Generate an adapter:

php artisan gg:vendor Hetzner

Register it:

'provider_registry' => [
    'hetzner' => App\GhostGrid\Providers\HetznerProvider::class,
],

Adapters implement:

use GhostCompiler\GhostGrid\DTO\ProvisioningResult;

interface ProvisioningProvider
{
    public function code(): string;
    public function name(): string;
    public function capabilities(): array;
    public function supports(string $action): bool;
    public function execute(string $action, array $payload): ProvisioningResult;
}

Dynamic actions can be anything your adapter supports:

create
suspend
unsuspend
terminate
renew
reboot
shutdown
power_on
reinstall
reset_password
upgrade
downgrade
sync_products

Execute manually:

use GhostCompiler\GhostGrid\Services\ProvisioningManager;

$result = app(ProvisioningManager::class)->execute(
    providerCode: 'hetzner',
    action: 'reboot',
    payload: ['remote_id' => 'server-123'],
);

Services

Provisioning create success creates or updates a services record.

Service lifecycle helpers create provider action tasks:

ghostgrid()->suspendService($service, dispatch: true);
ghostgrid()->unsuspendService($service, dispatch: true);
ghostgrid()->renewService($service, dispatch: true);
ghostgrid()->terminateService($service, dispatch: true);

Update, Archive, Delete

Most helper methods accept a model instance or ID.

ghostgrid()->updateAccount($account, ['name' => 'Updated Hosting']);
ghostgrid()->archiveAccount($account);
ghostgrid()->deleteAccount($account);

ghostgrid()->updatePriceList($priceList, ['name' => 'Published Pricing']);
ghostgrid()->archivePriceList($priceList);
ghostgrid()->deletePriceList($priceList);

ghostgrid()->addPriceListItem($priceList, [
    'product_variant_id' => $variant->id,
    'billing_cycle' => 'monthly',
    'selling_price' => 1600,
]);
ghostgrid()->updatePriceListItem($item, ['selling_price' => 1700]);
ghostgrid()->disablePriceListItem($item);
ghostgrid()->deletePriceListItem($item);

ghostgrid()->hideStorefrontProduct($storefrontProduct);
ghostgrid()->deleteStorefrontProduct($storefrontProduct);

Because GhostGrid does not use database foreign key constraints, your app decides whether hard delete is allowed. For production commerce flows, prefer archive/disable helpers for live records.

ID Strategy

Default ID strategy is UUID.

Set before running migrations:

GHOSTGRID_ID_STRATEGY=uuid
# uuid, ulid, bigint

Rules:

  • uuid: string UUID primary keys
  • ulid: string ULID primary keys
  • bigint: auto-incrementing big integer primary keys
  • relationship columns follow the same strategy
  • relationship columns are indexed

No Foreign Key Constraints

GhostGrid intentionally avoids database foreign key constraints.

Migrations use indexed relationship columns, but never:

foreign()
constrained()
references()
cascadeOnDelete()
nullOnDelete()

This gives reseller systems more flexibility for imports, provider sync, account movement, and recovery. Your app should enforce deletion rules at the application layer.

Config

Published config:

return [
    'id_strategy' => env('GHOSTGRID_ID_STRATEGY', 'uuid'),
    'default_currency' => env('GHOSTGRID_DEFAULT_CURRENCY', 'USD'),
    'allow_selling_below_parent' => env('GHOSTGRID_ALLOW_SELLING_BELOW_PARENT', false),
    'queue_connection' => env('GHOSTGRID_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')),
    'provider_registry' => [],
    'model_overrides' => [],
    'storefront' => [
        'default_visible' => true,
        'default_featured' => false,
    ],
    'enabled_modules' => [
        'accounts' => true,
        'providers' => true,
        'pricing' => true,
        'storefront' => true,
        'orders' => true,
        'provisioning' => true,
        'services' => true,
    ],
];

Internal constants such as order statuses, provisioning statuses, service statuses, and default provisioning actions live in PHP classes, not public config.

Commands

php artisan gg:init
php artisan gg:vendor Hetzner
php artisan gg:sync hetzner
php artisan gg:demo
php artisan gg:check

Factories

GhostGrid ships factories for tests and seeders:

use GhostCompiler\GhostGrid\Models\Account;
use GhostCompiler\GhostGrid\Models\Product;
use GhostCompiler\GhostGrid\Models\ProductVariant;
use GhostCompiler\GhostGrid\Models\PriceList;
use GhostCompiler\GhostGrid\Models\StorefrontProduct;

$account = Account::factory()->create();
$product = Product::factory()->create();
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
$priceList = PriceList::factory()->create(['account_id' => $account->id]);
$storefront = StorefrontProduct::factory()->create([
    'account_id' => $account->id,
    'product_variant_id' => $variant->id,
]);

Advanced Raw Model API

Helpers are recommended for tenant-aware app code. Raw Eloquent models are available for seeders, internal admin tools, or custom workflows.

use GhostCompiler\GhostGrid\Models\Account;
use GhostCompiler\GhostGrid\Models\PriceList;

$account = Account::create(['name' => 'Acme Hosting']);

$priceList = PriceList::create([
    'account_id' => $account->id,
    'name' => 'Default Pricing',
    'currency' => 'USD',
]);

$priceList->items()->create([
    'product_variant_id' => $variant->id,
    'billing_cycle' => 'monthly',
    'selling_price' => 1499,
]);

Helper API Cheatsheet

$grid = ghostgrid();

$grid->createAccount([...]);
$grid->account();
$grid->accounts();
$grid->createChildAccount([...]);
$grid->updateAccount($account, [...]);
$grid->deleteAccount($account);
$grid->archiveAccount($account);

$grid->createProvider('hetzner', 'Hetzner');
$grid->updateProvider($provider, [...]);
$grid->deleteProvider($provider);
$grid->disableProvider($provider);

$grid->createProduct($product, $variant, $provider, $mapping);
$grid->updateProduct($product, [...]);
$grid->deleteProduct($product);
$grid->archiveProduct($product);
$grid->updateProductVariant($variant, [...]);
$grid->deleteProductVariant($variant);
$grid->archiveProductVariant($variant);
$grid->updateProviderMapping($mapping, [...]);
$grid->deleteProviderMapping($mapping);
$grid->disableProviderMapping($mapping);

$grid->priceLists();
$grid->activePriceList();
$grid->assignedPriceLists();
$grid->unassignedPriceLists();
$grid->priceList($idOrName);
$grid->createPriceList('Default', $items);
$grid->createPriceList('Draft', $items, assign: false);
$grid->createUnassignedPriceList('Draft', $items);
$grid->createPriceListFromParent($masterPriceList, 'Tenant Pricing', $overrides);
$grid->syncPriceListWithParent($tenantPriceList);
$grid->assignPriceList($priceList);
$grid->updatePriceList($priceList, [...]);
$grid->deletePriceList($priceList);
$grid->archivePriceList($priceList);
$grid->addPriceListItem($priceList, [...]);
$grid->updatePriceListItem($item, [...]);
$grid->deletePriceListItem($item);
$grid->disablePriceListItem($item);

$grid->createStorefrontProduct([...]);
$grid->visibleStorefrontProducts();
$grid->updateStorefrontProduct($storefrontProduct, [...]);
$grid->deleteStorefrontProduct($storefrontProduct);
$grid->hideStorefrontProduct($storefrontProduct);

$grid->markOrderPaid($order, createProvisioningTasks: true, dispatch: true);
$grid->createProvisioningTasksForOrder($order);
$grid->suspendService($service);
$grid->unsuspendService($service);
$grid->renewService($service);
$grid->terminateService($service);

Docs Website

Open the static docs:

docs/index.html
docs/docs.html

GitHub Pages:

https://ghostcompiler.github.io/ghostgrid/

Testing

composer validate --strict
composer test

The repository includes GitHub Actions coverage for Laravel 10, 11, 12, and 13.

Security Notes

  • Helper methods are tenant-aware and only mutate records owned by accounts linked to the current user.
  • Raw models/actions are intentionally available for advanced/internal app code, so protect them with your own policies/controllers.
  • Provider credentials should be stored encrypted by your Laravel app.
  • Keep provider adapter payloads minimal and avoid logging secrets.

Roadmap

  • More optional helper methods for checkout flows
  • More first-party provider examples
  • More docs recipes for billing portals and reseller dashboards
  • Additional test coverage for custom model overrides

ghostcompiler/ghostgrid 适用场景与选型建议

ghostcompiler/ghostgrid 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 05 月 03 日, 在 PHP 生态内属于活跃度较高的组件。

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

围绕 ghostcompiler/ghostgrid 我们能提供哪些服务?
定制开发 / 二次开发

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

BUG 修复 & 性能优化

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

项目外包 & 长期维护

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

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

统计信息

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

GitHub 信息

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

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-03