From cf39a82f8a88eeb6ed0e0f7fd64a1b9b2fae125f Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 24 Oct 2026 13:12:29 -0400 Subject: [PATCH] Explain how the Data Mapper, Repository and Active Record patterns relate New Choosing a Pattern page: the Mapper and the Repository are always there, the ActiveRecord trait is a facade over them, and the choice is where the mapping and the persistence calls live. The duplicated pattern text in architecture-layers and active-record now links to it, and the sidebar positions are unique. --- README.md | 11 +- docs/active-record.md | 30 +--- docs/architecture-layers.md | 79 +--------- docs/auto-discovering-relationship.md | 2 +- docs/cache.md | 2 +- docs/common-traits.md | 2 +- docs/comparison-with-other-orms.md | 3 +- docs/controlling-the-data.md | 2 +- docs/getting-started-model.md | 17 ++- docs/mapper-functions.md | 2 +- docs/model-attribute.md | 2 +- docs/observers.md | 2 +- docs/patterns.md | 143 +++++++++++++++++++ docs/query-build.md | 2 +- docs/query-raw.md | 2 +- docs/querying-the-database.md | 2 +- docs/repository.md | 2 +- docs/softdelete.md | 2 +- docs/tables-without-auto-increment-fields.md | 2 +- docs/the-literal-object.md | 2 +- docs/update-constraints.md | 2 +- docs/updating-the-database.md | 2 +- docs/using-fieldalias.md | 2 +- docs/using-mapper-object.md | 2 +- docs/using-with-recursive-sql-command.md | 2 +- docs/uuid-support.md | 2 +- 26 files changed, 190 insertions(+), 133 deletions(-) create mode 100644 docs/patterns.md diff --git a/README.md b/README.md index 01f3d54..dff31b5 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,13 @@ MicroORM implements **Martin Fowler's enterprise patterns**: - **[Repository](https://martinfowler.com/eaaCatalog/repository.html)**: Mediates between domain and data mapping layers - **[Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html)**: Separates domain objects from database tables -- **[Active Record](https://martinfowler.com/eaaCatalog/activeRecord.html)**: Wraps database rows with domain logic ( - alternative approach) +- **[Active Record](https://martinfowler.com/eaaCatalog/activeRecord.html)**: Wraps database rows with domain logic, + as a facade over the two above -You can choose the pattern that best fits your application: use Repository + Data Mapper for complex domains, or Active -Record for simpler CRUD-focused applications. +They are layers, not alternatives. The **Mapper** (Data Mapper) and the **Repository** are always there; the +`ActiveRecord` trait is a facade that creates them for the model. What you choose is where the mapping lives (a `Mapper` +object or attributes on the model) and where the persistence calls live (a repository or the model itself). See +[Choosing a Pattern](docs/patterns.md). These are the key components: @@ -178,6 +180,7 @@ $result = $repository->getByQuery($query); ## Basics * [Defining the Model](docs/getting-started-model.md) +* [Choosing a Pattern](docs/patterns.md) * [Querying the Database](docs/querying-the-database.md) * [Updating the database](docs/updating-the-database.md) * [Using the Mapper Object](docs/using-mapper-object.md) diff --git a/docs/active-record.md b/docs/active-record.md index bdc5b73..f02db90 100644 --- a/docs/active-record.md +++ b/docs/active-record.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 7 --- # Active Record @@ -25,18 +25,9 @@ business objects whose data requires persistent storage to a database. - Provides methods like `save()`, `delete()`, `get()`, `all()`, etc. - Database operations are directly available on the domain object -**When to use Active Record vs Repository/Data Mapper:** - -| Aspect | Active Record | Repository + Data Mapper | -|--------------------|------------------------------------|-------------------------------| -| **Complexity** | Simple, straightforward | More complex, more layers | -| **Best for** | Simple domains, CRUD-heavy apps | Complex domain logic, DDD | -| **Coupling** | Domain objects know about database | Domain objects are pure | -| **Learning curve** | Lower | Higher | -| **Testability** | Harder (objects tied to DB) | Easier (dependency injection) | - -💡 **Tip**: For simple applications or prototypes, Active Record is perfect. For complex domain logic or when following -Domain-Driven Design principles, prefer Repository + Data Mapper. +Behind the trait is the same `Repository` and `Mapper` used everywhere else in MicroORM: +the trait creates them for the model on the first call. To compare Active Record with +the repository approach, see [Choosing a Pattern](patterns.md). ## How to Use Active Record @@ -266,15 +257,4 @@ The Active Record pattern offers several advantages: ## When to Use Active Record vs. Repository -Both patterns have their place in application development: - -- **Use Active Record when**: - - You prefer a simpler, more direct approach - - Your application has straightforward database operations - - You want to reduce the number of classes in your codebase - -- **Use Repository when**: - - You need more control over database operations - - Your application requires complex queries - - You prefer a more explicit separation between models and database operations - - You're implementing a domain-driven design approach \ No newline at end of file +See [Choosing a Pattern](patterns.md). Both can be used in the same application. diff --git a/docs/architecture-layers.md b/docs/architecture-layers.md index 8421bdb..a49b392 100644 --- a/docs/architecture-layers.md +++ b/docs/architecture-layers.md @@ -1,5 +1,5 @@ --- -sidebar_position: 20 +sidebar_position: 24 --- # Architecture Layers: Infrastructure vs Domain @@ -9,79 +9,9 @@ sidebar_position: 20 The MicroORM is designed with a clear separation between **Infrastructure Layer** (raw data access) and **Domain Layer ** (entity-aware operations). Understanding when to use each layer is crucial for writing maintainable code. -## Architectural Foundation - -This architecture follows **Martin Fowler's Enterprise Application Architecture patterns**, specifically: - -### Repository Pattern - -> *"Mediates between the domain and data mapping layers using a collection-like interface for accessing domain -objects."* -> -> — Martin -> Fowler, [Patterns of Enterprise Application Architecture](https://martinfowler.com/eaaCatalog/repository.html) - -**In MicroORM:** - -- The `Repository` class acts as a collection-like interface -- Isolates domain objects from database access code -- Provides a clean separation between domain and data layers -- Handles entity lifecycle (CRUD operations) - -### Data Mapper Pattern - -> *"A layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data -between the two and also to isolate them from each other."* -> -> — Martin -> Fowler, [Patterns of Enterprise Application Architecture](https://martinfowler.com/eaaCatalog/dataMapper.html) - -**In MicroORM:** - -- The `Mapper` class defines the relationship between entities and database tables -- Translates between domain objects (entities) and database rows -- Keeps domain objects independent of database schema -- Allows field name mapping, transformations, and type conversions - -**Why These Patterns Matter:** - -- ✅ **Testability**: Mock repositories without touching the database -- ✅ **Flexibility**: Change database schema without changing domain objects -- ✅ **Separation of Concerns**: Domain logic stays pure, database logic stays isolated -- ✅ **Maintainability**: Clear boundaries make code easier to understand and modify - -### Active Record Pattern (Alternative Approach) - -> *"An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on -that data."* -> -> — Martin -> Fowler, [Patterns of Enterprise Application Architecture](https://martinfowler.com/eaaCatalog/activeRecord.html) - -**In MicroORM:** - -- The `ActiveRecord` trait provides static methods like `get()`, `save()`, `delete()`, etc. -- Each Active Record instance represents a single database row -- Database operations are directly available on domain objects -- Simpler than Repository/Data Mapper for straightforward CRUD applications - -**Pattern Comparison:** - -``` -Repository + Data Mapper Active Record -├─ Domain objects are pure ├─ Domain objects know about DB -├─ More layers/complexity ├─ Fewer layers/simpler -├─ Better for complex domains ├─ Better for simple domains -├─ Easier to test (DI) ├─ Harder to test (static methods) -└─ More flexible └─ More convenient -``` - -**Choosing Your Pattern:** - -- Use **Active Record** for: Simple apps, prototypes, CRUD-heavy applications -- Use **Repository + Data Mapper** for: Complex domain logic, Domain-Driven Design, enterprise applications - -See [Active Record Documentation](active-record.md) for detailed usage examples. +This page is about using the `Repository` versus building a `Query` directly. For the +Data Mapper, Repository and Active Record patterns, and how to choose between them, see +[Choosing a Pattern](patterns.md). ## The Two Layers @@ -381,6 +311,7 @@ operations where entity transformation is not needed or not desired. ## See Also +- [Choosing a Pattern](patterns.md) - [Repository Documentation](repository.md) - [Querying the Database](querying-the-database.md) - [Query Building](query-build.md) diff --git a/docs/auto-discovering-relationship.md b/docs/auto-discovering-relationship.md index b9e5261..b466fcd 100644 --- a/docs/auto-discovering-relationship.md +++ b/docs/auto-discovering-relationship.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 --- # Auto Discovering Relationship diff --git a/docs/cache.md b/docs/cache.md index ca6bf63..c104285 100644 --- a/docs/cache.md +++ b/docs/cache.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 12 --- # Caching the Results diff --git a/docs/common-traits.md b/docs/common-traits.md index bcd709f..370a5fd 100644 --- a/docs/common-traits.md +++ b/docs/common-traits.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 13 --- # Common Traits for Timestamp Fields diff --git a/docs/comparison-with-other-orms.md b/docs/comparison-with-other-orms.md index f9fea7d..d9377da 100644 --- a/docs/comparison-with-other-orms.md +++ b/docs/comparison-with-other-orms.md @@ -1,5 +1,5 @@ --- -sidebar_position: 21 +sidebar_position: 25 --- # MicroORM vs Other ORMs @@ -1154,6 +1154,7 @@ features. ## See Also +- [Choosing a Pattern](patterns.md) - [Architecture Layers](architecture-layers.md) - [Active Record](active-record.md) - [Repository Documentation](repository.md) diff --git a/docs/controlling-the-data.md b/docs/controlling-the-data.md index 8a72133..72d1983 100644 --- a/docs/controlling-the-data.md +++ b/docs/controlling-the-data.md @@ -1,5 +1,5 @@ --- -sidebar_position: 13 +sidebar_position: 15 --- # Controlling the data diff --git a/docs/getting-started-model.md b/docs/getting-started-model.md index 3878e66..54354b4 100644 --- a/docs/getting-started-model.md +++ b/docs/getting-started-model.md @@ -94,17 +94,16 @@ _See also: [Controlling the data](controlling-the-data.md) for advanced mapping_ ### When to use each Mapper or Attribute? -The `Mapper` class is more flexible and can be used in any PHP version. Use cases: +Both produce the same `Mapper`. The difference is where the mapping lives: -* You have a legacy class and cannot change it -* You don't want change the Model class -* You can't change the Model class +* **Attributes** put the mapping on the model. It is the simplest option when you own the + class. +* **A `Mapper` object** keeps the mapping outside the model, so the model has no + database knowledge. Use it for legacy or third-party classes you cannot change, or + when the domain model must stay free of database concerns. -The `Attributes` is more simple and can be used in PHP 8.3 or later. Use cases: - -* You want a more simple way to define the Model -* You want to use the latest PHP features -* You can have a more simple way to define the Model +See [Choosing a Pattern](patterns.md) for how this combines with the Repository and +Active Record. ### Example diff --git a/docs/mapper-functions.md b/docs/mapper-functions.md index e90c4bf..1ed2bce 100644 --- a/docs/mapper-functions.md +++ b/docs/mapper-functions.md @@ -1,5 +1,5 @@ --- -sidebar_position: 14 +sidebar_position: 17 --- # Mapper Functions diff --git a/docs/model-attribute.md b/docs/model-attribute.md index 03a93c7..15915e5 100644 --- a/docs/model-attribute.md +++ b/docs/model-attribute.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 --- # The Model Attributes diff --git a/docs/observers.md b/docs/observers.md index 2307ab1..53170fa 100644 --- a/docs/observers.md +++ b/docs/observers.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 14 --- # Observers diff --git a/docs/patterns.md b/docs/patterns.md new file mode 100644 index 0000000..a5959d6 --- /dev/null +++ b/docs/patterns.md @@ -0,0 +1,143 @@ +--- +sidebar_position: 2 +--- + +# Choosing a Pattern + +MicroORM implements three of Martin Fowler's enterprise patterns: +[Data Mapper](https://martinfowler.com/eaaCatalog/dataMapper.html), +[Repository](https://martinfowler.com/eaaCatalog/repository.html) and +[Active Record](https://martinfowler.com/eaaCatalog/activeRecord.html). + +They are not three alternatives. They are layers, and every way of using MicroORM goes +through all of them: + +```text + Repository + Mapper object Repository + attributes ActiveRecord trait + (pure Data Mapper) (pragmatic Data Mapper) (facade on the model) + │ │ │ + │ │ creates internally + │ │ │ + └────────────────────────────┼────────────────────────────┘ + ▼ + Repository + │ + ▼ + Mapper ◄── built from a Mapper object or from the + │ #[TableAttribute] / #[FieldAttribute] attributes + ▼ + DatabaseExecutor ──► DB +``` + +- The **Mapper** is the Data Mapper. It knows how a model's properties translate to a + table's columns. It is always there. +- The **Repository** does the reading and writing, using the Mapper. It is always there + too: the `ActiveRecord` trait creates one for the model the first time it is used. + +So the choice is not *which* pattern, but two separate questions: + +1. **Where does the mapping live?** In a separate `Mapper` object, or as attributes on + the model. +2. **Where do the persistence calls live?** In a repository (`$repository->save($model)`), + or on the model itself (`$model->save()`). + +## The three combinations + +### Repository + Mapper object (pure Data Mapper) + +The model has no knowledge of the database. The mapping is defined outside it: + +```php +class User +{ + public ?int $id = null; + public ?string $name = null; +} + +$mapper = new Mapper(User::class, 'users', 'id'); +$repository = new Repository($executor, $mapper); + +$user = $repository->get(1); +$repository->save($user); +``` + +Use it when the model must stay free of database concerns: Domain-Driven Design, legacy +or third-party classes you cannot change, or one class mapped to more than one table. +See [Using the Mapper Object](using-mapper-object.md). + +### Repository + attributes (pragmatic Data Mapper) + +The mapping is declared on the model with attributes, and the repository still does the +persistence: + +```php +#[TableAttribute(tableName: 'users')] +class User +{ + #[FieldAttribute(primaryKey: true)] + public ?int $id = null; + + #[FieldAttribute] + public ?string $name = null; +} + +$repository = new Repository($executor, User::class); // builds the Mapper from the attributes + +$user = $repository->get(1); +$repository->save($user); +``` + +The model now carries mapping metadata, but it still has no persistence methods, and +your code depends on the repository, which is easy to replace in a test. This is the +most common choice. See [Model Attributes](model-attribute.md) and +[The Repository Class](repository.md). + +### ActiveRecord trait + +The model gets the persistence methods itself: + +```php +#[TableAttribute(tableName: 'users')] +class User +{ + use ActiveRecord; + + #[FieldAttribute(primaryKey: true)] + public ?int $id = null; + + #[FieldAttribute] + public ?string $name = null; +} + +User::initialize($executor); + +$user = User::get(1); +$user->save(); +``` + +There is no repository class to write. Behind the trait is the same `Repository` and the +same `Mapper`, so everything else in this documentation (literals, soft delete, observers, +mapper functions) works the same way. By default the Mapper comes from the attributes; +override `discoverClass()` to supply a `Mapper` object instead. See +[Active Record](active-record.md). + +## Which one to choose + +| | Repository + Mapper object | Repository + attributes | ActiveRecord trait | +|-------------------------------------|----------------------------|-------------------------|-------------------------------| +| **Model knows the database schema** | No | Yes (attributes) | Yes (attributes) | +| **Model knows how to persist** | No | No | Yes | +| **Classes to write** | Model + mapper setup | Model | Model | +| **Replace persistence in tests** | Easy (inject a repository) | Easy (inject a repository) | Harder (static methods) | +| **Map a class you cannot change** | Yes | No | No | +| **Best for** | DDD, legacy and third-party models | Most applications | Simple CRUD, prototypes | + +You do not have to pick one for the whole application. All three use the same +`Repository` and `Mapper`, so they can live side by side: an `ActiveRecord` model for a +simple lookup table, a repository for the entity with the business rules. + +## Repository or Query? + +A separate question is whether to go through the `Repository` at all, or build a `Query` +and read raw rows. That is about entities versus arrays, not about the patterns on this +page. See [Architecture Layers: Infrastructure vs Domain](architecture-layers.md). diff --git a/docs/query-build.md b/docs/query-build.md index aad61b3..e8b9d9e 100644 --- a/docs/query-build.md +++ b/docs/query-build.md @@ -1,5 +1,5 @@ --- -sidebar_position: 18 +sidebar_position: 22 --- # Building SQL Queries diff --git a/docs/query-raw.md b/docs/query-raw.md index 7f0cfb5..bdfd8b3 100644 --- a/docs/query-raw.md +++ b/docs/query-raw.md @@ -1,5 +1,5 @@ --- -sidebar_position: 19 +sidebar_position: 23 --- # QueryRaw diff --git a/docs/querying-the-database.md b/docs/querying-the-database.md index 1e090e5..f574947 100644 --- a/docs/querying-the-database.md +++ b/docs/querying-the-database.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 --- # Querying the database diff --git a/docs/repository.md b/docs/repository.md index c72b97e..2cbf312 100644 --- a/docs/repository.md +++ b/docs/repository.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 8 --- # The Repository class diff --git a/docs/softdelete.md b/docs/softdelete.md index 59c64b9..adbe809 100644 --- a/docs/softdelete.md +++ b/docs/softdelete.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 11 --- # Soft Deletes diff --git a/docs/tables-without-auto-increment-fields.md b/docs/tables-without-auto-increment-fields.md index e23856f..9ea56f5 100644 --- a/docs/tables-without-auto-increment-fields.md +++ b/docs/tables-without-auto-increment-fields.md @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 19 --- # Tables without auto increments fields diff --git a/docs/the-literal-object.md b/docs/the-literal-object.md index 50a0846..41dffe3 100644 --- a/docs/the-literal-object.md +++ b/docs/the-literal-object.md @@ -1,5 +1,5 @@ --- -sidebar_position: 9 +sidebar_position: 10 --- # The Literal Object diff --git a/docs/update-constraints.md b/docs/update-constraints.md index e593c46..44f81da 100644 --- a/docs/update-constraints.md +++ b/docs/update-constraints.md @@ -1,5 +1,5 @@ --- -sidebar_position: 17 +sidebar_position: 21 --- # Update Constraints diff --git a/docs/updating-the-database.md b/docs/updating-the-database.md index e4159da..08c879a 100644 --- a/docs/updating-the-database.md +++ b/docs/updating-the-database.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 --- # Updating the Database diff --git a/docs/using-fieldalias.md b/docs/using-fieldalias.md index 4665e09..8606cfe 100644 --- a/docs/using-fieldalias.md +++ b/docs/using-fieldalias.md @@ -1,5 +1,5 @@ --- -sidebar_position: 14 +sidebar_position: 18 --- # Using FieldAlias diff --git a/docs/using-mapper-object.md b/docs/using-mapper-object.md index 6ab0b13..b561632 100644 --- a/docs/using-mapper-object.md +++ b/docs/using-mapper-object.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 --- # Using Mapper Object diff --git a/docs/using-with-recursive-sql-command.md b/docs/using-with-recursive-sql-command.md index ac5c0bd..ff59f96 100644 --- a/docs/using-with-recursive-sql-command.md +++ b/docs/using-with-recursive-sql-command.md @@ -1,5 +1,5 @@ --- -sidebar_position: 16 +sidebar_position: 20 --- # Using With Recursive SQL Command diff --git a/docs/uuid-support.md b/docs/uuid-support.md index 7a3a81a..4947cd7 100644 --- a/docs/uuid-support.md +++ b/docs/uuid-support.md @@ -1,5 +1,5 @@ --- -sidebar_position: 13 +sidebar_position: 16 --- # UUID Support