serri/alchemist 问题修复 & 功能扩展

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

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

serri/alchemist

Composer 安装命令:

composer require serri/alchemist

包简介

The JSON Revolution for Laravel, a simple, fast, and elegant alternative to Laravel JSON Resource.

README 文档

README

Latest Version Tests License Total Downloads

The JSON Revolution for Laravel, a simple, fast, and elegant alternative to Laravel JSON Resource.

📖 Table of Contents

  1. Philosophy
  2. Requirements
  3. Installation
  4. Fundamentals
  5. Quick Start
  6. Usage & Examples
  7. Custom Ingredients
  8. Error Handling
  9. Testing
  10. Changelog
  11. License

🔮 Philosophy - The Problem with Traditional Laravel Resources

We've all been there:

  • Creating endless resource classes that mostly repeat the same boilerplate.
  • Duplicating code across multiple API responses.
  • Drowning in maintenance when frontend requirements change.
  • Wrestling with nested relationships that bloat your codebase.

The breaking point comes when:

  • Your API evolves and resources multiply.
  • Frontend devs request constant field changes.
  • Your models grow, but your resource classes don't scale.
  • Nested relationships turn into unmaintainable spaghetti.

The Solution: Laravel Alchemist - Formula Approach

One File to Rule Them All

Each model gets a single SomeModelFormula.php where you:

✅ Define all fields as simple strings in arrays.
✅ Manage every API variation in one place.
✅ Update database changes with a single edit.

Relationship Handling Made Simple

  • Reference nested resources by their name only.
  • Each relation maintains its own Formula::class.
  • No more recursive resource nightmares.

Frontend-Friendly Flexibility

  • Instantly modify fields without resource class hopping.
  • Track all API variations through clear formula methods.
  • Never miss a field update again.

Why This Works

  • Less Code: Eliminates 80%+ of resource boilerplate.
  • True Maintainability: All changes flow through controlled formulas.
  • Team Friendly: Frontend can request changes without breaking your flow.

“Laravel Resources grant you the illusion of control – meticulous yet maddening. Laravel Alchemist surrenders this false dominion... and in its place conjures true magic.„

📋 Requirements

Laravel PHP Alchemist
12.x ≥ 8.2 ≥ 1.1
13.x ≥ 8.3 ≥ 1.1

Laravel 11 is supported by Alchemist 1.0.x only: it reached end of security support in March 2026 and is no longer maintained.

🔧 Installation

You may install Alchemist using the Composer package manager:

composer require serri/alchemist

You can publish the Alchemist configuration file config/alchemist.php and the default Formulas/Formula.php using the vendor:publish Artisan command:

php artisan vendor:publish --provider="Serri\Alchemist\Providers\AlchemistServiceProvider"

Or for the configuration file only:

php artisan vendor:publish --tag=alchemist-config

For the default formula class only:

php artisan vendor:publish --tag=alchemist-formula

📖 Fundamentals

To wield this package's magic effectively, you must understand these arcane principles:

The Formulas Directory

  • Your sacred workshop where all model formulas reside
  • Created automatically at app/Formulas/Formula.php when you publish the default formula class as we did in the Installation section:
namespace App\Formulas;

class Formula
{
    const BlankParchment = ['id']; # Default formula.
}

Crafting Your Formulas

Summon a formula class with the generator:

php artisan make:formula PostFormula

# Or scan a model: pre-fills BlankParchment with its exposed fields and
# lists its #[Mutagen] / #[Relation] methods as hints.
php artisan make:formula PostFormula --model=Post

The generator writes to your configured formulas_folder_path (default app/Formulas/), creates the base Formula class if it is missing, and refuses to overwrite an existing formula unless you pass --force.

Linting Your Formulas

Formulas are strings resolved at runtime — so let CI catch the typos before production does:

php artisan formula:lint

Every constant of every formula class is validated against its model using the exact same discovery the brew pipeline uses, including nested specs (validated against the related model, no database needed). Unknown fields fail with a "did you mean" suggestion; the command exits non-zero, so add it next to your test step in CI. Models are matched by convention (PostFormulaApp\Models\Post) or explicitly:

class WeirdlyNamedFormula extends Formula
{
    protected static string $model = \App\Models\Post::class;
}

Use --json for machine-readable output.

Or craft one by hand in app/Formulas/ like so:

namespace App\Formulas;

class UserFormula extends Formula
{
    # Define your transformations here.
    # ex:

    const UserLogin = ['id', 'username', /*...etc.*/];

    // ... other formulas.
}

Key Laws:

  • Each model deserves its own formula class ModelNameFormula.php

  • The BlankParchment remains your fallback option.

Modular Codebases

Fallback resolution searches App\Formulas by convention. Modular / DDD apps can add their own namespaces (searched in order — the first entry is also where make:formula generates classes):

// config/alchemist.php
'formula_namespaces' => [
    'Modules\\Blog\\Formulas',
    'App\\Formulas',
],

Using the package's default Formula

If you did not publish app/Formulas/Formula.php, you can still extend the default Formula provided by the package like this:

namespace App\Formulas;

use Serri\Alchemist\Formulas\Formula;

class UserFormula extends Formula
{
    // Define your transformations here.
}

🪄 Quick Start

1. Model Configuration

To enable formula support, models must use the HasAlchemyFormulas concern.

use Serri\Alchemist\Concerns\HasAlchemyFormulas;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasAlchemyFormulas;

    //
}

2. Exposing Fields

By default, everything included in the $fillable array and the $guarded array is automatically available to formulas.

use Serri\Alchemist\Concerns\HasAlchemyFormulas;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasAlchemyFormulas;

    # Automatically exposed to formulas.
    protected $guarded = ['id'];

    # Automatically exposed to formulas.
    protected $fillable = [
        'title',
        'description',
        'published_at',
    ];
}

Note: Eloquent's default $guarded = ['*'] wildcard is never treated as a field. If you rely on the default, expose your columns through $fillable.

$hidden is respected: fields in a model's $hidden property are never exposed to formulas — matching Eloquent's serialisation contract. A formula referencing one throws with a clear message. Deliberately brewing hidden attributes requires setting alchemist.respect_hidden to false.

3. Exposing Relationships

Relationships must be explicitly marked with the #[Relation] decorator to be available in formulas:

use Serri\Alchemist\Decorators\Relation;

#[Relation] # Exposed to formulas as 'comments'
public function comments(): HasMany
{
    return $this->hasMany(Comment::class);
}

#[Relation(name: 'author_profile')] # Exposed to formulas as 'author_profile'
public function profile(): HasOne
{
    return $this->hasOne(Profile::class);
}

4. Exposing Custom Methods

Model methods require the #[Mutagen] decorator to be accessible in formulas:

use Serri\Alchemist\Decorators\Mutagen;

#[Mutagen] # Exposed to formulas as 'fullName'
public function fullName(): string
{
    return "{$this->first_name} {$this->last_name}";
}

#[Mutagen(name: 'is_verified')] # Exposed to formulas as 'is_verified'
public function isVerified(): bool
{
    return $this->email_verified_at !== null;
}

Keynotes

  • $fillable / $guarded fields are available in formulas by default.

  • Decorators: Only #[Relation] and #[Mutagen] methods are exposed to formulas.

  • The name argument works positionally too: #[Mutagen('is_verified')].

5. Crafting Formulas

Once your models are properly configured, you can define formulas to transform your data. Formulas are defined in classes within the app/Formulas/ directory.

Here is an example:

namespace App\Formulas;

class PostFormula extends Formula
{
    const Author = ['id', 'title', 'author_profile'];

    const WithComments = ['id', 'title', 'comments'];

    const Detailed = ['id', 'title', 'description', 'comments', 'author_profile'];
}

For the profile formula:

namespace App\Formulas;

class ProfileFormula extends Formula
{
    const OnlyName = ['fullName'];

    const AnyOther = ['id', 'username', 'fullName'];
}

🛠️ Usage & Examples

Basic Data Transformation

Pass a formula straight into the brew — no global state, no sequencing:

use App\Models\Post;
use App\Formulas\PostFormula;
use Serri\Alchemist\Facades\Alchemist;

// Eager-load the relations your formula uses!
$posts = Post::with('author.profile')->get();

$transformedData = Alchemist::brew($posts, PostFormula::Author);

...where the formula pins the whole response shape, nested relations included:

class PostFormula extends Formula
{
    const Author = [
        'id',
        'title',
        'author_profile' => ProfileFormula::OnlyName, # Nested spec: shapes the relation inline.
    ];
}

Nested specs recurse to any depth, and any relation listed as a plain string keeps resolving through the related model's own formula.

The classic stateful style remains fully supported — set formulas up front, brew later:

Post::setFormula(PostFormula::Author);
Profile::setFormula(ProfileFormula::OnlyName);

$transformedData = Alchemist::brew($posts);

A per-call formula always wins over setFormula() for that call, so the two styles mix safely.

Results:

[
  [
    'id' => 1,
    'title' => "Post 1",
    'author_profile' => [
      'fullName' => "some author name"
    ]
  ],
  [
    'id' => 2,
    'title' => 'Post 2',
    'author_profile' => [
      'fullName' => "some author name"
    ]
  ],
  [
    'id' => 3,
    'title' => 'Post 3',
    'author_profile' => [
      'fullName' => "some author name"
    ]
  ]
]

Eager loading: brewing a #[Relation] field accesses the relation on each model. Always eager-load (Post::with('comments')) the relations your formula references, or you will trigger N+1 queries.

Key Methods

Method Purpose Example
brew() Transforms a model or collection into an array Alchemist::brew($posts, PostFormula::Author)
brewBatch() Transforms a paginator's items, keeping its metadata Alchemist::brewBatch($paginator, $formula)
brewMixed() Brews a mixed-class collection, per-class formulas Alchemist::brewMixed($feed, $formulaMap)
response() Brews anything straight into a JsonResponse Alchemist::response($paginator, $formula)
setFormula() Assigns the model's active (fallback) formula Post::setFormula(PostFormula::DetailedView)
unsetFormula() Clears it, falling back to BlankParchment Post::unsetFormula()

The $formula argument is optional everywhere: omit it and the model's active formula (or BlankParchment) applies.

Laravel Octane: formulas set via setFormula() are flushed automatically at every request boundary, so state can never leak between requests in long-lived workers. Per-call formulas never touch shared state at all.

Patterns

1. Context-Aware Formulas

$formula = auth()->user()->isAdmin()
    ? PostFormula::AdminView
    : PostFormula::PublicView;

Post::setFormula($formula);

2. Direct Model Transformation

$post = Post::find(1);

return Alchemist::brew($post); // Auto-detects single model

3. Pagination Support

$paginated = Post::paginate(15);

return Alchemist::brewBatch($paginated); // Preserves pagination structure

brewBatch() accepts length-aware, simple (simplePaginate), and cursor (cursorPaginate) paginators alike.

4. Mixed Collections

Feeds, search results, and morph queries mix model classes — brewMixed() brews each element with its own class's formula, order preserved:

$feed = collect([$post, $comment, $anotherPost]);

$brewed = Alchemist::brewMixed($feed, [
    Post::class    => PostFormula::Summary,
    Comment::class => CommentFormula::BodyOnly,
]);
// Classes omitted from the map fall back to their active formula / BlankParchment.

Plain brew() requires a homogeneous collection and now fails fast (pointing at brewMixed()) when given a mixed one.

5. One-Line Controllers

response() brews anything — model, collection, or paginator — straight into a JsonResponse:

public function index()
{
    return Alchemist::response(
        Post::with('comments')->paginate(15),
        PostFormula::WithComments,
    );
}

// Optional status and headers:
return Alchemist::response($post, PostFormula::Detailed, 201, ['X-Custom' => 'header']);

Paginators keep their pagination envelope (data, total, links, ...); everything else returns the brewed array.

6. Sparse Fieldsets from the Request

Let clients narrow the response — with your formula as the ceiling they can never exceed:

use Serri\Alchemist\Support\Sieve;

// GET /posts?fields=id,title&include=comments&fields[comments]=body
public function index(Request $request)
{
    return Alchemist::response(
        Post::with('comments')->paginate(15),
        Sieve::from($request, PostFormula::Detailed),   // Detailed is the allow-list
    );
}
  • fields=id,title narrows top-level fields (fields[self]=... when combined with relation fields).
  • include=comments,comments.post chooses which nested-spec relations survive (dot paths for depth).
  • fields[comments]=body narrows a relation's nested spec, at any depth (fields[comments.post]=id).
  • No parameters → the allow-list verbatim, so it is a drop-in wrapper.
  • Requests outside the allow-list are silently dropped; use Sieve::strict() to throw an InvalidSieveRequestException instead (map it to a 4xx in your exception handler).

Syntax Variations

1. Helper Function (Simplest)

$posts = Post::all();
$transformed = alchemist()->brew($posts);

2. Facade (For static contexts)

use Serri\Alchemist\Facades\Alchemist;

$data = Alchemist::brew($models);

3. Dependency Injection (Recommended for controllers)

use Serri\Alchemist\Services\Alchemist;

class PostController
{
    public function __construct(
        protected Alchemist $alchemist,
    ) {}

    public function index()
    {
        return $this->alchemist->brew(Post::all());
    }
}

All three resolve the same container singleton.

🧪 Custom Ingredients

Ingredients are the strategies the Alchemist uses to resolve each formula field. The built-ins cover $fillable, $guarded, #[Mutagen], and #[Relation] — but you can brew your own.

An ingredient implements Serri\Alchemist\Contracts\IngredientContract:

namespace App\Ingredients;

use Serri\Alchemist\Contracts\IngredientContract;

final class UppercaseIngredient implements IngredientContract
{
    /**
     * The model property that lists the fields this ingredient resolves.
     */
    public static function ingredientName(): string
    {
        return 'shoutable';
    }

    /**
     * Resolve one field on one model.
     */
    public static function infuse(string $ingredient, mixed $brewing): array
    {
        return [
            $ingredient => strtoupper($brewing[$ingredient]),
        ];
    }
}

Register it in config/alchemist.php:

'ingredients' => [
    \Serri\Alchemist\Ingredients\FillableIngredient::class,
    \Serri\Alchemist\Ingredients\GuardedIngredient::class,
    \Serri\Alchemist\Ingredients\MutagenIngredient::class,
    \Serri\Alchemist\Ingredients\RelationIngredient::class,

    \App\Ingredients\UppercaseIngredient::class,
],

Then list the fields on your model:

class Post extends Model
{
    use HasAlchemyFormulas;

    protected array $shoutable = ['title'];
}

Rules of the craft:

  • Order matters: when two ingredients expose the same field name, the one registered later wins.
  • Models without the ingredient's property simply contribute no fields for it — no error.
  • Decorator-driven ingredients (like the built-in Mutagen/Relation) additionally implement a static usesDecorator(): bool returning true, and ingredientName() returns the attribute class to scan for.

🚨 Error Handling

Everything the Alchemist throws extends Serri\Alchemist\Exceptions\AlchemistException, so one catch covers all:

Exception Thrown when
UnknownFormulaFieldException A formula references a field the model does not expose (typo, or forgotten decorator).
UnbrewableInputException The input collection contains non-models, or the model lacks the HasAlchemyFormulas trait.
InvalidConfigurationException The alchemist config is missing or malformed.
InvalidIngredientException A configured ingredient class does not exist or lacks infuse().

🧬 Testing

composer test       # PHPUnit (unit + feature suites)
composer lint       # Pint code style check
composer analyse    # PHPStan level 6

The CI matrix runs the suite on PHP 8.2–8.4 across Laravel 12 and 13.

📆 Changelog

See CHANGELOG.md for release history and upgrade notes.

📜 License

This project is open-source and available under the MIT License.

serri/alchemist 适用场景与选型建议

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

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

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

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

BUG 修复 & 性能优化

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

项目外包 & 长期维护

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

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

统计信息

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

GitHub 信息

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

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-05-21