PHPackages                             hongxunpan/eloquent-projection - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. hongxunpan/eloquent-projection

ActiveLibrary[Database &amp; ORM](/categories/database)

hongxunpan/eloquent-projection
==============================

Eloquent read-model projection helpers for append/hidden/override style output shaping

0.1.0(4w ago)011MITPHPPHP ^8.0.20

Since May 13Pushed 4w agoCompare

[ Source](https://github.com/HongXunPan/eloquent-projection)[ Packagist](https://packagist.org/packages/hongxunpan/eloquent-projection)[ Docs](https://github.com/HongXunPan/eloquent-projection)[ RSS](/packages/hongxunpan-eloquent-projection/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (1)Versions (2)Used By (0)

Eloquent Projection
===================

[](#eloquent-projection)

[中文文档](./README.zh-CN.md)

`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
---------------------------------

[](#representative-business-scenarios)

### Administrative lists with conditional field exposure

[](#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_masked` for regular operators;
- privileged operators may inspect the raw `mobile_phone` value;
- 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

[](#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

[](#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

[](#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
----------------------------

[](#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

[](#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

[](#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

[](#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

[](#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

[](#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--after-reducing-repetitive-service-layer-code)

### Before

[](#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

[](#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
------------

[](#installation)

```
composer require hongxunpan/eloquent-projection
```

Compatibility
-------------

[](#compatibility)

Current target compatibility:

- PHP: `^8.0.20`
- `illuminate/database`: `^9.52 || ^10.0 || ^11.0 || ^12.0`

Core Concepts
-------------

[](#core-concepts)

### `FieldOverrideRule`

[](#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`: whether `targetField` should be appended automatically;
- `hideSourceField`: whether `sourceField` should be hidden automatically.

### `ProjectionProfile`

[](#projectionprofile)

A declarative configuration object that contains projection rules only.

- `append`
- `hidden`
- `overrides`

`ProjectionProfile` is intentionally configuration-only and does not execute projection logic.

### `ProjectionApplier`

[](#projectionapplier)

Executes a `ProjectionProfile` against models, collections, or relationships.

Error Handling
--------------

[](#error-handling)

The package keeps its public entry classes at the root namespace:

- `HongXunPan\EloquentProjection\ProjectionApplier`
- `HongXunPan\EloquentProjection\ProjectionProfile`
- `HongXunPan\EloquentProjection\FieldOverrideRule`

At the same time, it exposes a lightweight package-level exception boundary under:

- `HongXunPan\EloquentProjection\Exception\ExceptionInterface`
- `HongXunPan\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
-------------------

[](#parameter-reference)

### `ProjectionProfile(array $append = [], array $hidden = [], array $overrides = [])`

[](#projectionprofilearray-append---array-hidden---array-overrides--)

ParameterRole in the packageEffect on output`$append`Declares accessor or derived fields that should be appended before serializationThe listed fields are included in serialized output`$hidden`Declares fields that should be hidden before serializationThe listed fields are removed from serialized output`$overrides`Provides a list of field override rulesMay 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)`

[](#fieldoverriderulestring-targetfield-string-sourcefield-bool-enabled--true-bool-appendtargetfield--false-bool-hidesourcefield--false)

ParameterRole in the packageEffect on source / target fields`$targetField`Final field name exposed to consumersReceives the output value after projection`$sourceField`Original attribute used as the value sourceSupplies 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 setupEnsures the target field participates in serialization`$hideSourceField`Automatically hides the source field during projection setupKeeps the source field out of serialized output while still allowing it to be used internally> Current behavior note: `$enabled` controls only the value override step. `appendTargetField` and `hideSourceField` are resolved during projection setup regardless of whether `$enabled` is `true` or `false`.

### Method Parameters

[](#method-parameters)

Method parameterMeaning`$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
--------------------------

[](#how-field-processing-works)

The package applies projection in the following order:

1. Start with the profile-level `append` and `hidden` definitions.
2. Merge `appendTargetField` and `hideSourceField` contributions from each override rule.
3. Apply `append()` and `makeHidden()` before serialization.
4. For `projectModel()` / `projectList()`, serialize into arrays; for `applyModel()` / `applyList()` / `applyRelation()`, keep working on model instances.
5. For each rule whose `$enabled` is `true`, copy the value from `sourceField` into `targetField`.

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
---------------------------

[](#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;
- `appendTargetField = true`
    - ensures `mobile_phone_masked` appears in serialized output;
- `hideSourceField = true`
    - ensures `mobile_phone` is not exposed directly in serialized output.

So the final output behavior is:

- when permission is **not granted**: output keeps `mobile_phone_masked`, while `mobile_phone` stays hidden;
- when permission **is granted**: output still hides `mobile_phone`, but `mobile_phone_masked` is overwritten with the raw `mobile_phone` value.

API Overview
------------

[](#api-overview)

MethodBehaviorTypical use case`applyModel(Model $model, ProjectionProfile $profile): void`Mutates the given model instance in placeThe same model instance continues through the read pipeline`applyList(iterable $list, ProjectionProfile $profile): void`Mutates a list of models in placePaginated or aggregated model lists`applyRelation(Model|iterable $target, string $relationName, ProjectionProfile $profile): void`Applies a profile to a loaded relationship pathRelationship-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 instanceDetail responses or controlled one-off serialization`projectList(iterable $list, ProjectionProfile $profile): array`Returns an array list of projected modelsCandidate lists or array-oriented downstream consumersUsage Examples
--------------

[](#usage-examples)

### Apply a projection to a loaded relationship

[](#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

[](#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

[](#project-a-list-into-arrays)

```
return ProjectionApplier::projectList($candidates, $profile);
```

Design Boundaries
-----------------

[](#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
-------------------------------

[](#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
-------

[](#license)

MIT

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance94

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity28

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

28d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/37c4ffcd9218f6b9722875da1ec37f20b0118651687de5798972f919bd2c1eb4?d=identicon)[HongXunPan](/maintainers/HongXunPan)

---

Top Contributors

[![HongXunPan](https://avatars.githubusercontent.com/u/25816508?v=4)](https://github.com/HongXunPan "HongXunPan (1 commits)")

---

Tags

laraveleloquentserializationprojectionread model

### Embed Badge

![Health badge](/badges/hongxunpan-eloquent-projection/health.svg)

```
[![Health](https://phpackages.com/badges/hongxunpan-eloquent-projection/health.svg)](https://phpackages.com/packages/hongxunpan-eloquent-projection)
```

###  Alternatives

[kirschbaum-development/eloquent-power-joins

The Laravel magic applied to joins.

1.6k29.9M42](/packages/kirschbaum-development-eloquent-power-joins)[mongodb/laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel

7.1k8.0M84](/packages/mongodb-laravel-mongodb)[spatie/laravel-sluggable

Generate slugs when saving Eloquent models

1.5k12.4M291](/packages/spatie-laravel-sluggable)[watson/validating

Eloquent model validating trait.

9743.4M53](/packages/watson-validating)[cybercog/laravel-love

Make Laravel Eloquent models reactable with any type of emotions in a minutes!

1.2k322.4k1](/packages/cybercog-laravel-love)[spiritix/lada-cache

A Redis based, automated and scalable database caching layer for Laravel

592452.8k2](/packages/spiritix-lada-cache)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
