PHPackages                             monkeyscloud/monkeyslegion-entity - 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. monkeyscloud/monkeyslegion-entity

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

monkeyscloud/monkeyslegion-entity
=================================

Attribute-based data-mapper, entity scanner, hydration, and metadata layer for MonkeysLegion v2.

2.0.7(1mo ago)12.4k↓11.9%17MITPHPPHP ^8.4

Since Jul 23Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/MonkeysCloud/MonkeysLegion-Entity)[ Packagist](https://packagist.org/packages/monkeyscloud/monkeyslegion-entity)[ RSS](/packages/monkeyscloud-monkeyslegion-entity/feed)WikiDiscussions main Synced 2d ago

READMEChangelog (1)Dependencies (6)Versions (11)Used By (7)

MonkeysLegion Entity
====================

[](#monkeyslegion-entity)

[![Latest Stable Version](https://camo.githubusercontent.com/3bec2abff964fe5b1b8eb4ff346d45818038669c240886297a8c97e093f37071/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6f6e6b657973636c6f75642f6d6f6e6b6579736c6567696f6e2d656e746974792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/monkeyscloud/monkeyslegion-entity)[![License](https://camo.githubusercontent.com/f8c6d8311a578c002f2c7dd1ecaacae1c44e5e43ad673412726e5057c7863135/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6d6f6e6b657973636c6f75642f6d6f6e6b6579736c6567696f6e2d656e746974792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/monkeyscloud/monkeyslegion-entity)

**MonkeysLegion Entity v2** is a high-performance, attribute-first data-mapper and metadata layer for PHP 8.4+. It delivers Laravel/Symfony feature parity **plus 7 cross-ecosystem innovations** that no other PHP framework offers, while hydrating at **473K entities/sec** with zero-reflection metadata lookups after boot.

Key Features
------------

[](#key-features)

### Core (Laravel/Symfony Parity)

[](#core-laravelsymfony-parity)

- **Attribute-based mapping** — `#[Entity]`, `#[Field]`, `#[Id]`, `#[Column]`
- **Cast pipeline** — `#[Cast]` with backed enums, DateTimeImmutable, scalars, and custom `CastInterface`
- **Mass-assignment protection** — `#[Fillable]` / `#[Guarded]` with whitelist/blacklist modes
- **Serialization control** — `#[Hidden]` excludes fields from `toArray()` / `toJson()`
- **Soft deletes** — `#[SoftDeletes]` with configurable column name
- **Auto timestamps** — `#[Timestamps]` auto-injects `created_at` / `updated_at`
- **Database indexes** — `#[Index]` at class or property level (composite, unique)
- **Lifecycle observers** — `#[ObservedBy]` with DI container resolution
- **Relationships** — `#[OneToMany]`, `#[ManyToOne]`, `#[ManyToMany]`, `#[OneToOne]`, `#[JoinTable]`
- **UUID support** — `#[Uuid]` with built-in v4 generator
- **Change tracking** — `ChangeTracker` for dirty checking and efficient UPDATEs
- **Entity scanning** — auto-discover entities from directories

### Cross-Ecosystem Exclusives (First in PHP)

[](#cross-ecosystem-exclusives-first-in-php)

AttributeInspired ByWhat It Does`#[Virtual]`Ecto / PrismaComputed properties excluded from persistence`#[QueryFilter]`EF CoreGlobal query filters (multi-tenancy, archival)`#[Changeset]`EctoContextual mass-assignment per operation`#[Subscribe]`TypeORM / DjangoGlobal entity subscribers for cross-cutting concerns`#[AuditTrail]`EF CoreShadow audit columns not in the PHP model`#[Immutable]`DDD / KotlinBlocks UPDATE/DELETE after INSERT`#[Versioned]`JPA / HibernateOne-attribute optimistic locking### Performance

[](#performance)

OperationThroughputMetadata cold parse1.35msMetadata cache hit**71.8M ops/sec**Hydrate entity**473K ops/sec**Extract entity**656K ops/sec**toArray()**1.34M ops/sec**toJson()**1.17M ops/sec**Installation
------------

[](#installation)

```
composer require monkeyscloud/monkeyslegion-entity
```

**Requires PHP 8.4+**

---

Entity Examples
---------------

[](#entity-examples)

### Basic Entity

[](#basic-entity)

```
use MonkeysLegion\Entity\Attributes\Entity;
use MonkeysLegion\Entity\Attributes\Field;
use MonkeysLegion\Entity\Attributes\Id;
use MonkeysLegion\Entity\Attributes\Timestamps;
use MonkeysLegion\Entity\Attributes\SoftDeletes;
use MonkeysLegion\Entity\Attributes\Index;
use MonkeysLegion\Entity\Attributes\Fillable;
use MonkeysLegion\Entity\Attributes\Guarded;
use MonkeysLegion\Entity\Attributes\Hidden;

#[Entity(table: 'users')]
#[Timestamps]
#[SoftDeletes]
#[Index(columns: ['email'], unique: true)]
class User
{
    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'string', length: 255)]
    #[Fillable]
    #[Index(unique: true)]
    public string $email;

    #[Field(type: 'string', length: 255)]
    #[Fillable]
    public string $name;

    #[Field(type: 'string', length: 255)]
    #[Hidden]
    public string $password_hash;

    #[Field(type: 'string', length: 50)]
    #[Guarded]
    public string $role = 'user';

    #[Field(type: 'datetime', nullable: true)]
    public ?\DateTimeImmutable $deleted_at = null;

    #[Field(type: 'datetime')]
    public private(set) \DateTimeImmutable $created_at;

    #[Field(type: 'datetime')]
    public private(set) \DateTimeImmutable $updated_at;
}
```

### Entity with Backed Enums and Casts

[](#entity-with-backed-enums-and-casts)

```
use MonkeysLegion\Entity\Attributes\Cast;
use MonkeysLegion\Entity\Attributes\Versioned;

enum OrderStatus: string
{
    case Draft     = 'draft';
    case Pending   = 'pending';
    case Shipped   = 'shipped';
    case Delivered = 'delivered';
}

#[Entity(table: 'orders')]
#[Timestamps]
class Order
{
    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'unsignedBigInt')]
    #[Fillable]
    public int $user_id;

    #[Field(type: 'string', length: 50)]
    #[Cast(OrderStatus::class)]
    #[Fillable]
    public OrderStatus $status = OrderStatus::Draft;

    #[Field(type: 'decimal', precision: 10, scale: 2)]
    #[Fillable]
    public string $total;

    #[Field(type: 'json', nullable: true)]
    #[Cast('array')]
    #[Fillable]
    public array $metadata = [];

    #[Versioned]
    #[Field(type: 'integer')]
    public private(set) int $version = 1;
}
```

### Entity with Virtual Computed Fields (PHP 8.4 Property Hooks)

[](#entity-with-virtual-computed-fields-php-84-property-hooks)

```
use MonkeysLegion\Entity\Attributes\Virtual;

#[Entity(table: 'invoices')]
#[Timestamps]
class Invoice
{
    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'decimal', precision: 10, scale: 2)]
    #[Fillable]
    public string $subtotal;

    #[Field(type: 'decimal', precision: 5, scale: 2)]
    #[Fillable]
    public string $tax_rate;

    /** Computed field — not persisted to the database */
    #[Virtual]
    public string $tax_amount {
        get => bcmul($this->subtotal, $this->tax_rate, 2);
    }

    /** Computed field — not persisted to the database */
    #[Virtual]
    public string $total {
        get => bcadd($this->subtotal, $this->tax_amount, 2);
    }
}
```

### Immutable Entity (Financial Transactions)

[](#immutable-entity-financial-transactions)

```
use MonkeysLegion\Entity\Attributes\Immutable;
use MonkeysLegion\Entity\Attributes\AuditTrail;

#[Entity(table: 'transactions')]
#[Immutable]
#[AuditTrail]
#[Timestamps]
class Transaction
{
    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'unsignedBigInt')]
    public int $account_id;

    #[Field(type: 'decimal', precision: 12, scale: 2)]
    public string $amount;

    #[Field(type: 'string', length: 3)]
    public string $currency;

    #[Field(type: 'string', length: 100)]
    public string $description;

    // AuditTrail shadow columns exist in DB but NOT here:
    // created_by, updated_by, created_ip, updated_ip
}
```

### Entity with Contextual Changesets

[](#entity-with-contextual-changesets)

```
use MonkeysLegion\Entity\Attributes\Changeset;

#[Entity(table: 'profiles')]
class Profile
{
    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'string', length: 255)]
    public string $display_name;

    #[Field(type: 'text', nullable: true)]
    public ?string $bio = null;

    #[Field(type: 'string', length: 500, nullable: true)]
    public ?string $avatar_url = null;

    #[Field(type: 'string', length: 100)]
    public string $timezone = 'UTC';

    /**
     * Registration: only display_name is writable.
     * @return list
     */
    #[Changeset(context: 'registration')]
    public static function registrationRules(): array
    {
        return ['display_name'];
    }

    /**
     * Profile settings: all personal fields writable.
     * @return list
     */
    #[Changeset(context: 'settings')]
    public static function settingsRules(): array
    {
        return ['display_name', 'bio', 'avatar_url', 'timezone'];
    }
}
```

### Entity with Global Query Filters (Multi-Tenancy)

[](#entity-with-global-query-filters-multi-tenancy)

```
use MonkeysLegion\Entity\Attributes\QueryFilter;

#[Entity(table: 'documents')]
#[SoftDeletes]
#[QueryFilter(method: 'filterByTenant')]
#[QueryFilter(method: 'filterActive')]
class Document
{
    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'unsignedBigInt')]
    public int $tenant_id;

    #[Field(type: 'string', length: 255)]
    #[Fillable]
    public string $title;

    #[Field(type: 'boolean')]
    public bool $is_active = true;

    public static function filterByTenant(object $qb): void
    {
        $qb->where('tenant_id', '=', TenantContext::current()->id);
    }

    public static function filterActive(object $qb): void
    {
        $qb->where('is_active', '=', true);
    }
}
```

### Entity with Relationships

[](#entity-with-relationships)

```
#[Entity(table: 'posts')]
#[Timestamps]
class Post
{
    #[Id]
    #[Field(type: 'unsignedBigInt', autoIncrement: true)]
    public private(set) int $id;

    #[Field(type: 'string', length: 255)]
    #[Fillable]
    public string $title;

    #[Field(type: 'text')]
    #[Fillable]
    public string $body;

    #[ManyToOne(targetEntity: User::class, inversedBy: 'posts')]
    public User $author;

    #[OneToMany(targetEntity: Comment::class, mappedBy: 'post')]
    public array $comments = [];

    #[ManyToMany(targetEntity: Tag::class, inversedBy: 'posts')]
    #[JoinTable(name: 'post_tags', joinColumn: 'post_id', inverseColumn: 'tag_id')]
    public array $tags = [];
}
```

### UUID Entity

[](#uuid-entity)

```
use MonkeysLegion\Entity\Attributes\Uuid;
use MonkeysLegion\Entity\Utils\Uuid as UuidUtil;

#[Entity(table: 'events')]
class Event
{
    #[Id]
    #[Uuid]
    #[Field(type: 'uuid')]
    public private(set) string $id;

    #[Field(type: 'string', length: 255)]
    #[Fillable]
    public string $name;

    #[Field(type: 'json')]
    #[Cast('array')]
    public array $payload = [];

    public function __construct()
    {
        $this->id = UuidUtil::v4();
    }
}
```

---

Hydration &amp; Extraction
--------------------------

[](#hydration--extraction)

```
use MonkeysLegion\Entity\Hydrator;

// Hydrate from a database row (array or stdClass)
$user = Hydrator::hydrate(User::class, [
    'id'            => 1,
    'email'         => 'jorge@example.com',
    'name'          => 'Jorge',
    'password_hash' => '$2y$10$...',
    'role'          => 'admin',
]);

// Extract for persistence (includes all fields)
$data = Hydrator::extract($user);
// → ['id' => 1, 'email' => 'jorge@...', ..., 'password_hash' => '$2y$...']

// Extract for INSERT with auto timestamps
$data = Hydrator::extract($user, forInsert: true);
// → includes created_at and updated_at automatically

// Serialize for API response (respects #[Hidden])
$array = Hydrator::toArray($user);
// → password_hash is EXCLUDED

$json = Hydrator::toJson($user);
// → {"id":1,"email":"jorge@example.com","name":"Jorge","role":"admin"}
```

Mass Assignment
---------------

[](#mass-assignment)

```
use MonkeysLegion\Entity\Security\MassAssignmentGuard;

// Whitelist mode (when #[Fillable] is used)
$user = new User();
MassAssignmentGuard::fill($user, $request->all());
// Only 'email' and 'name' are assigned — 'role' is blocked

// Silent mode — skip disallowed fields without throwing
MassAssignmentGuard::fill($user, $request->all(), silent: true);

// Changeset context — different rules per operation
MassAssignmentGuard::fill($profile, $data, context: 'registration');
// Only 'display_name' allowed

MassAssignmentGuard::fill($profile, $data, context: 'settings');
// 'display_name', 'bio', 'avatar_url', 'timezone' allowed
```

Change Tracking
---------------

[](#change-tracking)

```
use MonkeysLegion\Entity\Support\ChangeTracker;

$tracker = new ChangeTracker();

// Snapshot original values after hydration
$user = Hydrator::hydrate(User::class, $row);
$tracker->track($user);

// Modify the entity
$user->name = 'New Name';

// Check what changed
$tracker->isDirty($user);           // true
$tracker->getDirty($user);          // ['name' => 'New Name']
$tracker->getOriginal($user, 'name'); // 'Old Name'
```

Metadata Registry
-----------------

[](#metadata-registry)

```
use MonkeysLegion\Entity\Metadata\MetadataRegistry;

// Zero-reflection after first call (71.8M ops/sec cache hits)
$meta = MetadataRegistry::for(User::class);

$meta->table;              // 'users'
$meta->primaryKey;         // 'id'
$meta->timestamps;         // true
$meta->softDeletes;        // true
$meta->immutable;          // false
$meta->isVersioned;        // false
$meta->fillable;           // ['email', 'name']
$meta->guarded;            // ['role']
$meta->hidden;             // ['password_hash']
$meta->casts;              // []
$meta->queryFilters;       // []
$meta->changesets;         // ['registration' => [...], 'profile_update' => [...]]
$meta->persistableFields(); // all fields except #[Virtual]
```

Observers &amp; Subscribers
---------------------------

[](#observers--subscribers)

```
use MonkeysLegion\Entity\Observers\EntityObserver;
use MonkeysLegion\Entity\Attributes\ObservedBy;
use MonkeysLegion\Entity\Attributes\Subscribe;

// Per-entity observer
class UserObserver extends EntityObserver
{
    public function creating(object $entity): void
    {
        $entity->password_hash = password_hash($entity->password_hash, PASSWORD_ARGON2ID);
    }

    public function deleting(object $entity): void
    {
        // Prevent deletion of admin users
        if ($entity->role === 'admin') {
            throw new \RuntimeException('Cannot delete admin users');
        }
    }
}

#[Entity(table: 'users')]
#[ObservedBy(UserObserver::class)]
class User { /* ... */ }

// Global subscriber — handles multiple entity types
#[Subscribe(entities: [Order::class, Transaction::class])]
class AuditSubscriber
{
    public function afterInsert(object $entity, EntityEvent $event): void
    {
        AuditLog::record('created', get_class($entity), $event->changes);
    }
}
```

Entity Scanner
--------------

[](#entity-scanner)

```
use MonkeysLegion\Entity\Scanner\EntityScanner;

$scanner = new EntityScanner();
$entities = $scanner->scanDir(__DIR__ . '/src/Entities');

foreach ($entities as $meta) {
    echo "{$meta->className} → {$meta->table}\n";
    echo "  Fields: " . implode(', ', array_keys($meta->fields)) . "\n";
}
```

Custom Casts
------------

[](#custom-casts)

```
use MonkeysLegion\Entity\Contracts\CastInterface;

class MoneyCast implements CastInterface
{
    public function get(mixed $value, string $attribute, object $entity): mixed
    {
        // Convert cents (int) to dollars (string)
        return number_format((int) $value / 100, 2, '.', '');
    }

    public function set(mixed $value, string $attribute, object $entity): mixed
    {
        // Convert dollars (string) to cents (int)
        return (int) round((float) $value * 100);
    }
}

#[Entity(table: 'products')]
class Product
{
    #[Field(type: 'integer')]
    #[Cast(MoneyCast::class)]
    #[Fillable]
    public string $price; // stored as cents, accessed as "29.99"
}
```

---

Architecture
------------

[](#architecture)

```
src/
├── Attributes/          20 attribute classes
│   ├── Entity, Field, Id, Column, Uuid
│   ├── Hidden, Fillable, Guarded, Cast
│   ├── SoftDeletes, Timestamps, Index
│   ├── Virtual, Immutable, Versioned
│   ├── QueryFilter, Changeset, Subscribe, AuditTrail
│   ├── ObservedBy
│   └── OneToMany, ManyToOne, ManyToMany, OneToOne, JoinTable
├── Contracts/           CastInterface
├── Exceptions/          MassAssignment, ImmutableEntity, OptimisticLock
├── Metadata/            EntityMetadata, FieldMetadata, IndexMetadata, MetadataRegistry
├── Observers/           EntityObserver, LifecycleDispatcher
├── Scanner/             EntityScanner
├── Security/            MassAssignmentGuard
├── Support/             ChangeTracker, EntityEvent
├── Utils/               Uuid
└── Hydrator.php         Core hydration/extraction/serialization

```

Requirements
------------

[](#requirements)

- PHP 8.4+
- `psr/container ^2.0` (optional, for DI-aware observer resolution)

License
-------

[](#license)

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

© 2026 MonkeysCloud Team

###  Health Score

51

—

FairBetter than 95% of packages

Maintenance89

Actively maintained with recent releases

Popularity24

Limited adoption so far

Community22

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 78.6% 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

Every ~48 days

Recently: every ~65 days

Total

7

Last Release

56d ago

Major Versions

1.1.0 → 2.0.02026-04-10

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/2913369?v=4)[Jorge Peraza](/maintainers/yorchperaza)[@yorchperaza](https://github.com/yorchperaza)

---

Top Contributors

[![yorchperaza](https://avatars.githubusercontent.com/u/2913369?v=4)](https://github.com/yorchperaza "yorchperaza (22 commits)")[![Amanar-Marouane](https://avatars.githubusercontent.com/u/155680356?v=4)](https://github.com/Amanar-Marouane "Amanar-Marouane (4 commits)")[![Copilot](https://avatars.githubusercontent.com/in/1143301?v=4)](https://github.com/Copilot "Copilot (2 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/monkeyscloud-monkeyslegion-entity/health.svg)

```
[![Health](https://phpackages.com/badges/monkeyscloud-monkeyslegion-entity/health.svg)](https://phpackages.com/packages/monkeyscloud-monkeyslegion-entity)
```

###  Alternatives

[api-platform/core

Build a fully-featured hypermedia or GraphQL API in minutes!

2.6k51.2M339](/packages/api-platform-core)[getgrav/grav

Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS

15.6k86.4k1](/packages/getgrav-grav)[kimai/kimai

Kimai - Time Tracking

4.8k9.0k1](/packages/kimai-kimai)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

564576.7k53](/packages/ecotone-ecotone)[api-platform/state

API Platform state interfaces

274.9M136](/packages/api-platform-state)[wikimedia/parsoid

Parsoid, a bidirectional parser between wikitext and HTML5

187557.3k3](/packages/wikimedia-parsoid)

PHPackages © 2026

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