hongxunpan/eloquent-projection
最新稳定版本:0.1.0
Composer 安装命令:
composer require hongxunpan/eloquent-projection
包简介
Eloquent read-model projection helpers for append/hidden/override style output shaping
README 文档
README
hongxunpan/eloquent-projection provides a focused projection layer for Eloquent read models. It is intended for scenarios where output shaping has become stable, repetitive, and worth standardizing across services.
Typical use cases include:
- appending derived accessor fields for read-only responses;
- hiding source fields before serialization;
- overriding masked fields with raw values under explicit conditions;
- applying the same output-shaping rules to models, collections, and loaded relationships.
This package intentionally stays narrow in scope. It is not intended for:
- write-path transformation;
- persistence-time mutation;
- DTO / presenter style response composition;
- flattening relationships into custom top-level payloads.
If Eloquent's natural select, with, and toArray() flow already expresses the response cleanly, prefer the native approach over introducing projections.
Representative Business Scenarios
Administrative lists with conditional field exposure
A common scenario is an administrative list that returns a primary resource together with related identity data, while sensitive fields must be exposed differently depending on permissions. For example:
- an event signup list needs
alumniCard.mobile_phone_maskedfor regular operators; - privileged operators may inspect the raw
mobile_phonevalue; - the list should continue to use the same loaded relation structure instead of rebuilding the payload manually.
In this case, applyRelation() allows the service layer to preserve the original relation tree while applying a consistent projection profile to the related model.
Detail responses with stable serialization rules
Some detail endpoints need more than Eloquent's default serialization, but still do not justify introducing a dedicated DTO layer. A typical example is an alumni profile, submission record, or activity detail view that requires:
- appended accessor fields for presentation;
- hidden source fields to avoid leaking raw data;
- field overrides under explicit business conditions.
projectModel() is intended for this class of detail response: precise output shaping without forcing the service layer to hand-assemble arrays field by field.
Nested relationship projection in read pipelines
Read pipelines often traverse multiple relation levels, such as signup.alumniCard or record.user.profile. When output shaping rules need to be applied only to the nested relation, it is usually undesirable to flatten or rebuild the entire root response structure.
applyRelation() supports nested relation paths so that projection can stay local to the relevant read model while the root payload remains natural.
Array-oriented candidate and matching workflows
Some business flows consume arrays rather than mutable model instances. Examples include:
- candidate matching lists;
- review queues exported to downstream processors;
- read-only comparison results assembled from model collections.
For these scenarios, projectList() provides a consistent way to convert a model list into projected arrays without duplicating serialization logic in each service.
Problems This Package Solves
Without a dedicated projection layer, the same read-model shaping logic tends to be reimplemented repeatedly across services. This package is designed to eliminate several recurring problems:
1. Repeated append + hidden + override code
Service methods often accumulate nearly identical code paths that clone models, append accessors, hide raw fields, convert to arrays, and conditionally overwrite output fields. Repetition makes behavior drift likely and increases the cost of maintenance.
2. Projection logic mixed into business services
When output shaping rules live directly inside service classes, the service layer starts handling both business decisions and serialization mechanics. That makes the core business flow harder to read, review, and evolve.
3. Inconsistent handling between model, list, and relation outputs
It is common for a rule to be implemented one way for a detail endpoint, another way for a list endpoint, and a third way for a related model inside another response. A reusable projection kernel keeps these paths aligned.
4. Weak separation between reusable kernel and scenario-specific policy
Most projects need two different layers:
- a reusable execution kernel;
- business-specific profiles that express when and how fields should be exposed.
This package provides the kernel, while leaving scenario-specific policy in the business codebase.
5. High regression risk when serialization rules evolve
When masking or exposure rules change, copy-pasted serialization logic forces updates across multiple services and relation paths. Centralized projection behavior reduces regression risk and makes rule changes easier to audit.
Before / After: Reducing Repetitive Service-Layer Code
Before
Without this package, service methods often end up mixing business flow and serialization mechanics:
$projectionModel = clone $submitRecord; $projectionModel->append(['mobile_phone_masked']); $projectionModel->makeHidden(['mobile_phone']); $result = $projectionModel->toArray(); if ($canViewMobileFull) { $result['mobile_phone_masked'] = $projectionModel->getAttribute('mobile_phone'); } return $result;
Typical problems in the “before” style:
- the same pattern gets copied across detail, list, and relation flows;
- field-shaping concerns compete with business logic in the same service method;
- when masking rules change, multiple call sites must be updated together.
After
With hongxunpan/eloquent-projection, the same intent can be expressed declaratively:
use HongXunPan\EloquentProjection\FieldOverrideRule; use HongXunPan\EloquentProjection\ProjectionApplier; use HongXunPan\EloquentProjection\ProjectionProfile; $profile = new ProjectionProfile( overrides: [ new FieldOverrideRule( 'mobile_phone_masked', 'mobile_phone', $canViewMobileFull, true, true ), ], ); return ProjectionApplier::projectModel($submitRecord, $profile);
What improves in the “after” style:
- the service method expresses what should happen instead of reimplementing how output is shaped;
- the same profile structure can be reused in model, list, and relation scenarios;
- serialization behavior becomes easier to review, evolve, and test.
Installation
composer require hongxunpan/eloquent-projection
Compatibility
Current target compatibility:
- PHP:
^8.0.20 illuminate/database:^9.52 || ^10.0 || ^11.0 || ^12.0
Core Concepts
FieldOverrideRule
Represents a single field override rule.
targetField: the field exposed in the final output;sourceField: the attribute used as the value source;enabled: whether the override is active;appendTargetField: whethertargetFieldshould be appended automatically;hideSourceField: whethersourceFieldshould be hidden automatically.
ProjectionProfile
A declarative configuration object that contains projection rules only.
appendhiddenoverrides
ProjectionProfile is intentionally configuration-only and does not execute projection logic.
ProjectionApplier
Executes a ProjectionProfile against models, collections, or relationships.
Error Handling
The package keeps its public entry classes at the root namespace:
HongXunPan\EloquentProjection\ProjectionApplierHongXunPan\EloquentProjection\ProjectionProfileHongXunPan\EloquentProjection\FieldOverrideRule
At the same time, it exposes a lightweight package-level exception boundary under:
HongXunPan\EloquentProjection\Exception\ExceptionInterfaceHongXunPan\EloquentProjection\Exception\InvalidProjectionArgumentException
Current exception behavior:
- invalid model / iterable input;
- empty relation paths;
- unsupported relation value types;
- invalid override rule items inside
ProjectionProfile::$overrides;
all raise InvalidProjectionArgumentException.
This class extends PHP's native InvalidArgumentException, so consumers may either catch the native SPL type or the package-specific exception boundary, depending on how narrowly they want to scope error handling.
Parameter Reference
ProjectionProfile(array $append = [], array $hidden = [], array $overrides = [])
| Parameter | Role in the package | Effect on output |
|---|---|---|
$append |
Declares accessor or derived fields that should be appended before serialization | The listed fields are included in serialized output |
$hidden |
Declares fields that should be hidden before serialization | The listed fields are removed from serialized output |
$overrides |
Provides a list of field override rules | May extend append/hidden behavior and may overwrite the final exposed value of target fields |
FieldOverrideRule(string $targetField, string $sourceField, bool $enabled = true, bool $appendTargetField = false, bool $hideSourceField = false)
| Parameter | Role in the package | Effect on source / target fields |
|---|---|---|
$targetField |
Final field name exposed to consumers | Receives the output value after projection |
$sourceField |
Original attribute used as the value source | Supplies the raw attribute value used for override |
$enabled |
Controls whether source-to-target value override is executed | true: target value is replaced with source value; false: target keeps its existing serialized value |
$appendTargetField |
Automatically appends the target field during projection setup | Ensures the target field participates in serialization |
$hideSourceField |
Automatically hides the source field during projection setup | Keeps the source field out of serialized output while still allowing it to be used internally |
Current behavior note:
$enabledcontrols only the value override step.appendTargetFieldandhideSourceFieldare resolved during projection setup regardless of whether$enabledistrueorfalse.
Method Parameters
| Method parameter | Meaning |
|---|---|
$model |
A single Eloquent model to mutate or project |
$list |
An iterable collection of Eloquent models |
$target |
A model or iterable that already owns the loaded relation to be projected |
$relationName |
A relation path such as alumniCard or signup.alumniCard |
$profile |
The projection rule set applied by ProjectionApplier |
How Field Processing Works
The package applies projection in the following order:
- Start with the profile-level
appendandhiddendefinitions. - Merge
appendTargetFieldandhideSourceFieldcontributions from each override rule. - Apply
append()andmakeHidden()before serialization. - For
projectModel()/projectList(), serialize into arrays; forapplyModel()/applyList()/applyRelation(), keep working on model instances. - For each rule whose
$enabledistrue, copy the value fromsourceFieldintotargetField.
This means a single projection can both:
- hide the raw source field from the final payload; and
- expose a safer or conditionally upgraded target field for consumers.
Example Parameter Breakdown
For the following rule:
new FieldOverrideRule( 'mobile_phone_masked', 'mobile_phone', $canViewMobileFull, true, true )
The parameters mean:
targetField = 'mobile_phone_masked'- the consumer-facing output field;
sourceField = 'mobile_phone'- the original raw field on the model;
enabled = $canViewMobileFull- if
true, the output field is replaced with the raw phone value; - if
false, the output field keeps its existing masked serialization result;
- if
appendTargetField = true- ensures
mobile_phone_maskedappears in serialized output;
- ensures
hideSourceField = true- ensures
mobile_phoneis not exposed directly in serialized output.
- ensures
So the final output behavior is:
- when permission is not granted: output keeps
mobile_phone_masked, whilemobile_phonestays hidden; - when permission is granted: output still hides
mobile_phone, butmobile_phone_maskedis overwritten with the rawmobile_phonevalue.
API Overview
| Method | Behavior | Typical use case |
|---|---|---|
applyModel(Model $model, ProjectionProfile $profile): void |
Mutates the given model instance in place | The same model instance continues through the read pipeline |
applyList(iterable $list, ProjectionProfile $profile): void |
Mutates a list of models in place | Paginated or aggregated model lists |
applyRelation(Model|iterable $target, string $relationName, ProjectionProfile $profile): void |
Applies a profile to a loaded relationship path | Relationship-level output shaping without rebuilding the root payload |
projectModel(Model $model, ProjectionProfile $profile): array |
Returns a projected array without requiring the caller to keep using the mutated output state of the same instance | Detail responses or controlled one-off serialization |
projectList(iterable $list, ProjectionProfile $profile): array |
Returns an array list of projected models | Candidate lists or array-oriented downstream consumers |
Usage Examples
Apply a projection to a loaded relationship
use HongXunPan\EloquentProjection\FieldOverrideRule; use HongXunPan\EloquentProjection\ProjectionApplier; use HongXunPan\EloquentProjection\ProjectionProfile; $profile = new ProjectionProfile( overrides: [ new FieldOverrideRule( 'mobile_phone_masked', 'mobile_phone', $canViewMobileFull, true, true ), ], ); ProjectionApplier::applyRelation($records, 'alumniCard', $profile);
Project a single model into an array
$profile = new ProjectionProfile( overrides: [ new FieldOverrideRule( 'phone_masked', 'phone', $canViewPhoneFull, true, true ), ], ); return ProjectionApplier::projectModel($user, $profile);
Project a list into arrays
return ProjectionApplier::projectList($candidates, $profile);
Design Boundaries
Keep this package focused on output projection for Eloquent read models.
Avoid using it for:
- write workflows;
- bidirectional transformation APIs that both mutate and return shaped output;
- page-specific response assembly;
- thin helper APIs that only save one extra array access.
Recommended Integration Pattern
This package should remain the reusable kernel. Business projects should keep scenario-specific profiles in their own codebase, for example:
app/Projections/AlumniCard/AlumniCardProjectionProfiles.php
app/Projections/Activity/ActivityProjectionProfiles.php
Prefer organizing profiles by the projected entity rather than by page name, modal name, or one-off endpoint context.
License
MIT
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 3
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-13