承接 webmintydotcom/laravel-feature-requests 相关项目开发

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

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

webmintydotcom/laravel-feature-requests

Composer 安装命令:

composer require webmintydotcom/laravel-feature-requests

包简介

A headless Laravel package providing a Canny/FeatureBase-style feature request portal: posts, votes, comments, tags, statuses, attachments, and an activity log. JSON API only — consumers ship their own UI.

README 文档

README

A Laravel package that adds a Canny/FeatureBase-style feature request portal to your app. Backend only — it exposes a JSON API and you build whatever UI you like (Blade, Inertia, Livewire, a separate SPA).

What you get

Entities

  • Posts (title, body, tags, attachments)
  • Upvotes (one per user)
  • Flat comments with attachments
  • Statuses — admin-editable, DB-driven, one marked default
  • Tags — admin-editable
  • Per-post activity log

Behavior

  • Lock a post → it refuses new votes and comments
  • Pin posts to the top of listings
  • Soft-deletes throughout
  • Rate-limited submissions, votes, and uploads
  • Stream files through an auth'd route or expose public URLs — your choice per disk
  • Events fired after every state change so you can wire your own notifications
  • Bodies are stored as plain text; bind a renderer (Markdown, CommonMark, etc.) to produce HTML on the fly
  • Polymorphic authors — any User model works

Not included. No UI. No notifications. No emails. Those are yours to wire.

Requirements

  • PHP 8.2+
  • Laravel 11, 12, or 13

Installation

composer require webmintydotcom/laravel-feature-requests

# Publishes config, migrations, seeder, and translations.
php artisan feature-requests:install

php artisan migrate
php artisan db:seed --class=FeatureRequestStatusSeeder

Add the seeder to database/seeders/DatabaseSeeder.php if you want it to run on fresh installs.

Define the three gates the package consults — auth logic is yours:

// AppServiceProvider::boot()
use Illuminate\Support\Facades\Gate;

Gate::define('featureRequests.moderate', fn ($user) => $user->isAdmin());
Gate::define('featureRequests.manageStatuses', fn ($user) => $user->isAdmin());
Gate::define('featureRequests.manageTags', fn ($user) => $user->isAdmin());

That's it — GET /feature-requests/posts is now live.

If your setup is non-standard

Custom User model namespace. The default config points at \App\Models\User. If yours lives somewhere else, set this in .env before any config:cache:

FEATURE_REQUESTS_USER_MODEL=Domain\\Users\\User

Or publish the config and edit it directly.

Session-cookie auth (Sanctum SPA, Inertia, Breeze, Jetstream). Default route middleware is ['api', 'auth']. Override it in the published config:

'routes' => [
    'middleware' => ['web', 'auth'],
    'admin_middleware' => ['web', 'auth'],
],

Morph map (recommended). The package stores fully-qualified class names in polymorphic *_type columns. To keep that data intact through future User-model renames, register a morph map:

// AppServiceProvider::boot()
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'user' => \App\Models\User::class,
]);

Configuration

After install, edit config/feature-requests.php. The keys most teams touch:

Key Purpose
routes.prefix URL prefix, default feature-requests
routes.middleware / routes.admin_middleware Middleware stacks for the two route groups
pagination.per_page Default page size, default 25
attachments.disk Laravel filesystem disk
attachments.serve_via 'stream' (package serves via auth'd route) or 'public_url' (returns Storage::url())
attachments.max_size_kb Upload size cap, default 5120 (5 MB)
attachments.mime_whitelist e.g. ['png', 'jpg', 'pdf'] to restrict, null to allow any
rate_limits.* Attempts/decay per endpoint

The config file contains no closures, so php artisan config:cache is safe.

API surface

All routes are JSON. Default prefix is /feature-requests. List/detail endpoints return Laravel paginator JSON (data, links, meta).

Public (auth required)

GET    /posts                            newest first
GET    /posts/{post}                     single post with relations
POST   /posts                            create (throttled)
PATCH  /posts/{post}                     edit  (author or moderator)
DELETE /posts/{post}                     soft-delete (author or moderator)

GET    /posts/{post}/comments            oldest first
POST   /posts/{post}/comments            create (throttled)
PATCH  /comments/{comment}               edit  (author or moderator)
DELETE /comments/{comment}               soft-delete (author or moderator)

POST   /posts/{post}/votes               cast (idempotent, throttled)
DELETE /posts/{post}/votes               retract

POST   /posts/{post}/attachments         upload (throttled)
POST   /comments/{comment}/attachments   upload (throttled)
DELETE /attachments/{attachment}         remove (uploader or moderator)
GET    /attachments/{attachment}         download (stream mode only)

GET    /tags                             full list
GET    /statuses                         ordered list
GET    /posts/{post}/activity            paginated activity log

Admin (gate-protected)

PATCH  /admin/posts/{post}/status        can:featureRequests.moderate
POST   /admin/posts/{post}/pin
DELETE /admin/posts/{post}/pin
POST   /admin/posts/{post}/lock
DELETE /admin/posts/{post}/lock
PATCH  /admin/posts/{post}/tags

POST   /admin/statuses                   can:featureRequests.manageStatuses
PATCH  /admin/statuses/reorder
PATCH  /admin/statuses/{status}
DELETE /admin/statuses/{status}

POST   /admin/tags                       can:featureRequests.manageTags
PATCH  /admin/tags/{tag}
DELETE /admin/tags/{tag}

Customizing IDs (hashids, sqids, etc.)

By default, the package exposes integer primary keys in URLs and API responses ("id": 42, /posts/42). To swap in hashids, sqids, or any other ID scheme, implement IdCodec and bind it in your service provider:

namespace App\Support;

use Webminty\FeatureRequests\Contracts\IdCodec;

final class SqidsIdCodec implements IdCodec
{
    public function __construct(private readonly \Sqids\Sqids $sqids) {}

    public function encode(int $id): string
    {
        return $this->sqids->encode([$id]);
    }

    public function decode(string $value): ?int
    {
        $decoded = $this->sqids->decode($value);

        return count($decoded) === 1 ? $decoded[0] : null;
    }
}
// AppServiceProvider::register()
$this->app->singleton(\Webminty\FeatureRequests\Contracts\IdCodec::class, function () {
    return new \App\Support\SqidsIdCodec(new \Sqids\Sqids(minLength: 8));
});

After that, /feature-requests/posts/42 becomes /feature-requests/posts/Xy3kQa9p and the same encoded form appears as "id" in JSON responses. The package handles encoding on the way out and decoding (with automatic 404 on invalid input) on the way in. Database PKs stay as integers — encoding is API-layer only.

The same hook is used for every route binding: frPost, frComment, frAttachment, frStatus, frTag. (Route params are prefixed fr to avoid colliding with any {post} / {comment} / etc. bindings your host app may already define. The public URL paths are unchanged — /feature-requests/posts/42 still works.) If you generate URLs with the route() helper, use the prefixed names:

route('feature-requests.posts.show', ['frPost' => $post->id]);
route('feature-requests.attachments.show', ['frAttachment' => $attachment->id]);

Customizing the author payload

The author / voter / uploader shape in API responses is produced by AuthorPayloadResolver. Default: ['id' => key, 'name' => $author->name]. To add fields (avatar URL, hashed handle, anything), bind your own:

namespace App\Support;

use Illuminate\Database\Eloquent\Model;
use Webminty\FeatureRequests\Contracts\AuthorPayloadResolver;

final class AvatarAuthorPayloadResolver implements AuthorPayloadResolver
{
    public function resolve(?Model $author): ?array
    {
        if ($author === null) {
            return null;
        }

        return [
            'id' => $author->getKey(),
            'name' => $author->name,
            'avatar' => $author->avatar_url,
        ];
    }
}
// AppServiceProvider::register()
$this->app->singleton(
    \Webminty\FeatureRequests\Contracts\AuthorPayloadResolver::class,
    \App\Support\AvatarAuthorPayloadResolver::class,
);

Rendering post bodies

Bodies are stored as raw text. To turn them into HTML for the body_html field in API responses, bind your renderer to the BodyRenderer contract:

use Webminty\FeatureRequests\Contracts\BodyRenderer;

$this->app->bind(BodyRenderer::class, MyMarkdownRenderer::class);

Your renderer implements render(string $body): string and must return sanitized HTML. The default PlainTextRenderer escapes HTML and converts newlines to <br />.

Events

Dispatched after the DB transaction commits — listen to any of these without touching package internals:

Event Payload
PostCreated, PostUpdated, PostDeleted Post
PostStatusChanged Post, Status $from, Status $to
PostPinned, PostUnpinned, PostLocked, PostUnlocked Post
VoteCast Vote
VoteRetracted Post, Authenticatable $voter
CommentCreated, CommentUpdated, CommentDeleted Comment
AttachmentAdded, AttachmentRemoved Attachment

Example listener wiring:

// EventServiceProvider
protected $listen = [
    \Webminty\FeatureRequests\Events\PostStatusChanged::class => [
        \App\Listeners\NotifyVotersOfStatusChange::class,
    ],
];

Translations

User-facing strings (authorization errors and the two custom-exception responses) are translated through the feature-requests::messages namespace. To localize:

php artisan vendor:publish --tag=feature-requests-translations

Files land in lang/vendor/feature-requests/{locale}/messages.php. Add new locale folders and Laravel resolves them based on app()->getLocale().

Internal RuntimeException messages (misconfiguration, storage failures, race conditions) are intentionally not translated — they exist for developers and logs, not end users.

Client examples

Submit a post:

await fetch('/feature-requests/posts', {
    method: 'POST',
    credentials: 'include',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        Accept: 'application/json',
    },
    body: JSON.stringify({ title: 'Dark mode', body: 'Please add dark mode.' }),
});

Vote / unvote:

await fetch(`/feature-requests/posts/${postId}/votes`, { method: 'POST', credentials: 'include' });
await fetch(`/feature-requests/posts/${postId}/votes`, { method: 'DELETE', credentials: 'include' });

Troubleshooting

  • 403 on every admin route — the package registers false defaults for its three gates. Define them in AppServiceProvider::boot() (see Installation).
  • 404 on GET /attachments/{id}attachments.serve_via is set to 'public_url'. Either switch it to 'stream' or use the url field returned in the API response.
  • Class "App\Models\User" not found at boot — set FEATURE_REQUESTS_USER_MODEL in .env (see "If your setup is non-standard").
  • Reorder request rejected — the order array on PATCH /admin/statuses/reorder must list every status ID exactly once; partial reorders aren't supported.

License

MIT

Built fresh by Webminty.

webmintydotcom/laravel-feature-requests 适用场景与选型建议

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

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

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

围绕 webmintydotcom/laravel-feature-requests 我们能提供哪些服务?
定制开发 / 二次开发

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

BUG 修复 & 性能优化

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

项目外包 & 长期维护

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

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

统计信息

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

GitHub 信息

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

其他信息

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