Download the PHP package denisyu-1/articulate without Composer
On this page you can find all versions of the php package denisyu-1/articulate. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download denisyu-1/articulate
More information about denisyu-1/articulate
Files in denisyu-1/articulate
Package articulate
Short Description Context-bounded PHP ORM for domain-driven applications
License Apache-2.0
Homepage https://github.com/articulate-orm/core
Informations about the package articulate
Articulate
Context-bounded ORM for modular PHP applications that share database tables across modules.
Why Articulate?
Most ORMs make the table/entity boundary the modeling boundary: one table, one primary entity class. In modular systems, that turns shared tables into shared domain objects.
A users table may be touched by authentication, administration, billing, public APIs, reporting, and background workers. Those contexts do not need the same fields, relations, invariants, or lifecycle behavior. A single shared User entity gradually becomes a coupling point between modules.
Table width is a storage decision. Domain boundaries aren't. Don't let one dictate the other. How many columns share a table is about storage and locality; how those columns split into contexts is domain modeling. One class per table forces the two to be the same call. Articulate lets them diverge.
Articulate makes the bounded context the modeling boundary. Several small entity classes can map to the same physical table: LoginUser for authentication, AdminUser for administration, BillingCustomer for billing, and read-only projection entities for public APIs.
Articulate still provides the expected ORM foundations: attributes, repositories, relations, migrations, type mapping, identity map, unit of work, lazy loading, and caching. The difference is that these pieces are designed around context-bounded entities from the start.
Badges
What Makes It Different?
- Multiple entity classes can map to one physical table.
- Partial entities can be marked read-only when they intentionally omit required columns.
- Each
EntityManagerowns its identity map and units of work. - Shared-table sibling entities are handled deliberately during writes and cache eviction.
- Schema metadata, migrations, relations, lazy loading, repositories, and type conversion all understand context-bounded entities.
- MySQL and PostgreSQL are first-class targets.
Quick Start
Before / After
Before — one shared entity shape for every context:
After — separate entities per context, same table:
When It Fits
Articulate is a good fit when different bounded contexts need different views of the same data, when adding a relation for one workflow should not affect every other workflow, or when long-running processes need tighter control over tracked entities.
If your application has one stable entity model per table and your current ORM handles that well, Articulate would still fit, just probably will not solve a meaningful problem for you.
Core Concepts
Context-Bounded Entities
Multiple entity classes can point to the same database table, each exposing only the fields and relationships needed for that context. Articulate merges compatible column definitions and validates for conflicts.
Read-Only Entities
Mark a context-bounded entity as read-only when it intentionally omits required columns — for example, a LoginUser that exposes only login and password from a users table that has many more non-nullable columns.
ReadOnlyEntityException is thrown at persist() and remove() — before any SQL is built.
Optimistic Locking
A naive optimistic lock (version tied to one entity class) breaks under context-bounded entities: if only one sibling class bumps/checks the version column, another sibling can silently overwrite changes undetected. Articulate's optimistic locking is fully explicit, per-class, from that class's own attributes only:
#[Version]— property-level, no args. This class's canonical version column: hydrated as a normalintproperty, bumped (version = version + 1) and checked (WHERE version = ?, against the tracked value) on everyUPDATEthrough this class.#[VersionAware(['column', ...])]— class-level. Declares raw column names (typically a sibling's#[Version]column) that this class bumps onUPDATEbut never checks. Use it when a class legitimately writes through a versioned table but shouldn't take on lost-update detection it can't reason about (e.g. a lightweight title-only edit path on a billing entity).- No attribute at all on a class mapping a versioned table means that class touches no version columns — a real gap, and
articulate:validateerrors on it rather than silently tolerating it.
A column may appear in at most one of a class's own #[Version] property or its own #[VersionAware] list — declaring both throws at metadata-build time. #[Version] properties must be typed int; a migration-generated column for one gets DEFAULT 0 automatically. OptimisticLockException doesn't distinguish a stale version from a deleted row — both are "zero rows matched."
Recovering from a conflict. A flush that throws rolls its transaction back and leaves the entities' in-memory #[Version] properties at their pre-flush values — the +1 bump is applied just before post-update callbacks (so a #[PostUpdate] handler sees the value the row now carries) and reverted if the flush never commits. So the failed flush does not poison a retry: re-find() the entity (or resolve the conflict another way) and flush again. There is no "EM is now closed" state to reset. Do not, however, write the same row through two different #[Version]-checking classes in a single flush — the first UPDATE bumps the shared column and the second then conflicts with itself.
Run articulate:validate in CI. The coverage guarantee holds only if it is enforced: there is no runtime check, so a class mapping a versioned table with neither #[Version] nor #[VersionAware] silently drops out of lost-update detection until validate catches it. It errors when an entity class mapping a versioned table doesn't account for every #[Version] column on that table (as its own #[Version] property or in its own #[VersionAware] list), and when a #[VersionAware] column has no canonical #[Version] owner in the group; it reports (as info) a table with more than one distinct #[Version] column across its entity classes.
Memory-Efficient Unit of Work
- Clear entities from memory that are no longer needed within specific operations
- Different units of work can track their own entities independently
- Entity manager combines all unit-of-work changes into minimal database queries during flush
Useful for processing large datasets, complex business operations spanning multiple contexts, and long-running processes with varying entity lifecycles.
Polymorphic Many-To-Many Relations
Use MorphToMany / MorphedByMany when several entity types share one pivot table, such as tagging orders and customers through taggables.
The pivot table uses {name}_type, {name}_id, and the target id column:
Registered morph aliases are used for owning and inverse relation loading. If no alias is registered, Articulate falls back to storing and loading the full entity class name.
When Articulate generates a polymorphic pivot schema, it uses the composite key (taggable_type, taggable_id, tag_id) as the relation identity. A separate technical id column is not required for collection loading or persistence.
Type Mapping System
Built-in mappings: bool ↔ TINYINT(1), int ↔ INT, float ↔ FLOAT, string ↔ VARCHAR(255), DateTimeInterface ↔ DATETIME.
Custom class mappings and TypeConverterInterface for complex types. Priority-based resolution when a class implements multiple interfaces with registered mappings.
Repository Pattern
Custom repositories via #[Entity(repositoryClass: UserRepository::class)] extending AbstractRepository.
Caching
Articulate has three independent cache layers, all using PSR-6 (CacheItemPoolInterface). Pass the same pool instance to share backend, or separate instances for isolation.
Second-Level Cache
Cross-request entity cache. Survives beyond a single EntityManager instance.
Pass any PSR-6 pool to EntityManager. If no dedicated pool is given, it falls back to the result cache pool automatically.
find() checks the identity map first, then the L2 cache, then the database. On flush(), modified and deleted entity entries are evicted automatically — stale data is never served after a write.
Query Result Cache
Cache raw result sets from QueryBuilder queries. Useful for read-heavy queries that don't change often.
- Custom cache key via
resultCacheId, or auto-generated from query shape + parameters - Locked queries (
FOR UPDATE) are never cached - Call
disableResultCache()to opt out per query
Statement Cache
Caches compiled SQL strings (query structure, not results). Eliminates repeated SQL compilation for queries with the same shape but different parameter values.
Transparent — no per-query opt-in needed. Failures are silently ignored so a broken cache backend never breaks queries.
Connection Pooling
Enable PDO persistent connections to reuse open database connections across requests:
Skips TCP handshake and authentication overhead on each request. Pair with a pool-aware cache backend for full cross-request performance.
MySQL Table Options (ENGINE, CHARSET, COLLATE)
Articulate does not append table options like ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=... to generated CREATE TABLE statements. This is intentional.
Storage engine and character set are deployment concerns, not schema concerns. The right values depend on the MySQL version, the hosting environment, and the application's locale requirements — there is no single correct default. Hardcoding them would mean either inheriting outdated assumptions or overriding a deliberate server configuration.
Instead, Articulate delegates to the server's configured defaults:
- ENGINE — InnoDB is the MySQL default since 5.7 and is the only engine that supports foreign keys; Articulate's FK generation already implies it.
- CHARSET / COLLATE — configure once at the server or database level (
CREATE DATABASE ... CHARACTER SET utf8mb4). All tables created in that database inherit the correct charset without per-table repetition.
If per-table overrides are ever needed, the right path is an explicit option on #[Entity], not a framework-wide hardcoded string.
Index Attribute Design
#[Index] takes fields — PHP property names, not column names:
This keeps index definitions coupled to the entity model. When a property is renamed alongside its column, PHP tooling catches the broken reference in fields. Raw column strings would silently diverge.
Expression and prefix indexes (e.g. LOWER(email), title(100)) have no PHP property to reference. If that need arises, a dedicated ExpressionIndex attribute will be introduced as an explicit escape hatch rather than mixing column-string support into Index.
License
Licensed under the Apache License 2.0. See LICENSE.
All versions of articulate with dependencies
symfony/console Version ^8.0
symfony/uid Version ^8.0
psr/cache Version ^3.0
psr/log Version ^3.0
ext-pdo Version *