Download the PHP package antevemus/aspecification without Composer

On this page you can find all versions of the php package antevemus/aspecification. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package aspecification

Antevemus ASpecification

πŸ‡ΊπŸ‡Έ English  |  πŸ‡§πŸ‡· PortuguΓͺs (Brasil)

CI PHP Version Latest Version Architecture Upstream: Domian Synergy: ALinq

Enterprise Specification Pattern Framework for PHP 8.2+ (PHP 8.4 only for the optional ALinq integration)
Independent, unofficial PHP reimplementation and evolution of the acclaimed Java Domian framework, grounded in the seminal paper Specifications by Eric Evans and Martin Fowler. Enhanced with natural-language fluent chaining, zero-exception Notification Pattern with rich diagnostic telemetry, relational database Dynamic Rule Engine, multi-DBMS SQL Query Visitor (15 drivers, 10 SQL dialects), Adianti Framework TCriteria Builder, Directed Acyclic Graph (DAG) partitioned repositories, hybrid persistence, high-concurrency IPC primitives, a specification-driven state machine, use cases with a result shared between both, concurrent resolution of the external data I/O-bound rules depend on, a repository over PDO whose queries are specifications compiled to SQL (with lifecycle columns, batch and set-based operations and relationships loaded in batch), and domain events emitted when an entity starts or stops satisfying a specification, on a write or by the mere passage of time.


πŸ›οΈ Origins & Theoretical Foundation

Antevemus ASpecification was engineered on solid software craftsmanship principles and Domain-Driven Design (DDD):

  1. The Seminal Paper: Based on the classic paper The Specifications Pattern (PDF) by Eric Evans and Martin Fowler (2002), which formalized encapsulating business predicates and domain constraints into first-class composable objects for:

    • Object Validation: Verify whether candidate entities satisfy domain constraints.
    • Selection & Filtering: Query entities from repositories without leaking SQL or persistence concerns into the domain layer.
    • Constraint Satisfaction: Specify preconditions required to instantiate or transition entities between domain lifecycle states.
  2. The Upstream Origin: Domian (Java): This library is an independent, unofficial PHP port and modern evolution of the Domian (Domain-Driven Design for Java) framework, originally authored by Eirik Torske and BjΓΈrn Nordlund under the Apache License, Version 2.0. It is not affiliated with, maintained by or endorsed by the Domian authors: the design, public API and semantics follow Domian's public sources, and the PHP code was written by Antevemus. Domian pioneered unifying Boolean algebra, Venn diagram set theory, collection-scoped specifications, Directed Acyclic Graph (DAG) partitioned repositories, and concurrent synchronization.

🌟 Core Features


🎯 What Sets Us Apart

While most PHP specification libraries stop at primitive Boolean checks (isSatisfiedBy: bool) and the original Java Domian framework relied heavily on runtime reflection and memory-only collections, Antevemus ASpecification was designed for the rigorous demands of enterprise microservices, high-throughput APIs, and mission-critical legacy modernization:

Capability / Feature Domian (Original Java) Typical PHP Specification Libs Antevemus ASpecification
Notification Pattern (Zero Exceptions) ❌ Boolean only ❌ Boolean only or control-flow exceptions βœ… evaluate(), aggregated SpecificationFailure, error codes & rationales
Operational Severity Triage ❌ None ❌ None βœ… BLOCK (fatal), WARN (operational alert), LOG (audit trail)
Multi-DBMS SQL Query Visitor ❌ None ❌ Rare / single engine only βœ… 15 drivers, 10 dialects (PostgreSQL, MySQL, SQL Server, Oracle, Firebird, SQLite, Db2, Informix, DuckDB, ANSI)
Adianti Framework Database Bridge ❌ Not applicable ❌ None βœ… Native TCriteria & TFilter, automatic De Morgan inversion, strict precedence
ALinq Synergy & Functional LINQ ❌ None ❌ None βœ… Direct AST compiler for short-circuit LINQ predicates & ALinqBridge
Deep Nested Property Resolution ⚠️ Strict reflection ⚠️ Public getters only βœ… Dot-notation (PropertyAccess, shared with ALinq) for deep objects and associative arrays
Database-Driven Dynamic Rule Engine ❌ None ❌ None βœ… Dynamic compilation from relational catalog tables (RuleDefinition)
Document Matrix Requirement Algebra ❌ None ❌ None βœ… ALL, ANY, ONE_OF_SET aggregation modes with rich verdict reports
Fluent Chaining & Natural Domain DSL ⚠️ Basic ⚠️ Partial βœ… Natural English syntax (Spec::specify()->where()->and()->or()) + helpers (prop(), is(), not())
Venn Set Subsumption & Remainder βœ… Complete ❌ Absent in almost all libs βœ… isGeneralizationOf, isSpecialCaseOf, isDisjointWith, remainderUnsatisfiedBy
DAG Partitioning $O(1)$ βœ… Supported ❌ Extremely rare βœ… Partitioned repositories with early disjoint branch pruning
State Machine With Explained Denials ❌ None ❌ None βœ… Guards are specifications: a rejection carries code, message and severity; outbox, idempotency and persisted deadlines
Concurrency & Locking ⚠️ Java Threads / Locks ❌ Unsupported βœ… Synchronized repositories (reentrant SemaphoreSynchronizer, Fiber-aware) SysVSemaphoreSynchronizer on SysV IPC between processes, and atomic flock file locks
Modern Architecture ⚠️ Java 6/7 Generics ⚠️ PHP 7.x legacy βœ… PHP 8.2+ native (Enums, First-class callables, Readonly, 8 segregated traits)

πŸ“‹ Requirements

Upgrading from 1.x? Read UPGRADE-2.0.md first: .bin files of the PHP-native serializer and rule catalogs with Portuguese values are converted on 1.14.x, before the upgrade.


πŸš€ Installation

Install via Composer:


πŸ’‘ Practical Examples

1. Natural Language Fluent Chaining

Inspired by Domian Java's syntax, combine business rules fluently and expressively:


2. Zero-Exception Notification Pattern & Diagnostic Telemetry

In enterprise workflows, REST APIs, and UI forms, use evaluate() to accumulate all validation failures without interrupting execution:

Evaluation errors and what to show. When a rule cannot be evaluated (a specification, a handler or a guard throws), isError is true, the failure carries the generic message SpecificationResult::EVALUATION_ERROR_MESSAGE with the rule's code, and the exception stays on $result->exception, never in the failures. The library does not log for you. Recommended practice: send $result->exception to your logger, taking care not to log more than needed (exception messages can hold credentials, personal data or SQL); show users only $failure->message, which is either your business message or the generic one; and never show metadata, which is diagnostic and may hold the evaluated value (metadata['actual']).


3. Partial Satisfaction (remainderUnsatisfiedBy)

Pinpoint at runtime precisely which sub-rules failed for a given candidate:


4. Directed Acyclic Graph (DAG) Partitioned Repositories

Partition collections into logical branches indexed by specification predicates:


5. Decoupled Persistence & Hybrid L1/L2 Cache Tiering

The document is JSON whatever the serializer, and every entity carries its persistence metadata (__persistence_metadata: read and write counters, first and last read and write) next to its data, so getEntityMetaData() answers the same in a new process (since v2.0.0).


6. High-Precision Benchmarking & Telemetry


7. Dynamic Rule Engine & Relational Database Catalogs (Module 11)

Compile dynamic rules and mandatory document matrices directly from relational database tables (e.g. PostgreSQL, MySQL):

Vocabulary note. The rule engine is English end to end: getters (getCode(), getIntValue(), getViolationMessage(), getLegalBasis()), named parameters (scope:, scenario:), fromArray() keys and catalog columns (code, rule_type, violation_action, product_code, valid_from...), context keys (product_code, plan_code, reference_date), failure metadata (action, rule_code, legal_basis...) and the action values block/warn/log. The Portuguese vocabulary of v1.6, legacy from v1.7.0 to v1.14.x, is refused since v2.0.0 with LegacyVocabularyException, never ignored: a Portuguese key or action value in a catalog, a context key, a table role, a handler's metadata key. UPGRADE-2.0.md has the key table and the SQL that migrates a catalog; PdoRuleCatalog still reads tables created with the old schema through two presets (below), which map names only.

The catalog can live in relational tables: PdoRuleCatalog reads the tables of the reference DDL resources/sql/rule_catalog.ansi.sql (business_rule, document_requirement_group, document_requirement) in portable, parameterized ANSI SQL and answers exactly like the in-memory catalog. Table and column names are configurable ($tables, $columns: canonical column => real column):


8. SQL Query Visitor & Multi-DBMS (WHERE Parameterization)

Translate pure domain specification trees directly into parameterized, injection-proof WHERE clauses with automatic identifier quoting and native dialect functions:


9. TCriteria Builder (Adianti Framework Database Bridge - Module 13)

Translate pure domain specifications into native Adianti\Database\TCriteria and TFilter instances, applying De Morgan's Laws for nested negations and maintaining strict Boolean precedence:


10. ALinq Synergy & Fluent LINQ Collections (Module 14)

Native high-performance interoperability with the Antevemus.AlinqCollection framework:

10.1 Deep Property Resolution with Dot-Notation (PropertyAccess)

Evaluate nested properties on objects (with getters or Boolean methods) and multidimensional associative arrays without reflection bottlenecks. Since v2.0.0 the resolution lives in antevemus/adeep-property-access, shared with ALinq (one accessor, one contract test); Helpers\PropertyAccessor remains as a deprecated facade until v3.0:

Each segment resolves in this order, first match wins: array or ArrayAccess key; public method getX(), x(), isX(), hasX() without required parameters (never __call(), never a name starting with __); public initialized property or __get guarded by __isset; public property; and, last, a private or protected property marked #[Specifiable] by the author of the class (since v1.6.1). The property name may come from a request (a filter or an ordering built by a screen), so private state is reachable by name only when its author opts in. An unmarked non-public property behaves as a property that does not exist: the property specification is an evaluation error, PropertyAccess::hasProperty() answers false, and a delta (updateWithDelta()) that names it throws InvalidArgumentException unless the entity has a public setter for it.

#[Specifiable] on a class covers every property declared by that class (not those of its parent nor of its subclasses). #[Antevemus\ALinq\Attributes\Specifiable] and #[Antevemus\ADeepPropertyAccess\Attributes\Specifiable] are accepted as the same marker, by name.

10.2 AST Compiler to LINQ Predicates (ALinqSpecificationVisitor)

Compile specification trees into a zero-overhead native Closure(mixed $candidate): bool using short-circuit logical operators (&&, ||, !):

10.3 Fluent In-Memory Repositories with ALinqBridge

Connect repositories and iterables directly to LINQ pipelines (sorting, pagination, grouping, and aggregations):

10.4 Generator-Based Streaming & Lazy Evaluation (ALinqLazyCollection) with O(1) Memory

Process massive, infinite, or file-backed data streams with constant memory footprint by pairing specifications with lazy generators:


11. Declarative Attributes Engine (#[AssertSpec], #[ValidateRule]) (Module 15)

In PHP 8.2+, annotate Data Transfer Objects (DTOs), Form Requests, Value Objects, and Domain Entities directly with specifications:

12. Tautology, Contradiction & Simplification

Every specification answers two structural questions, isTautology() and isContradiction(). A true is a proof from the shape of the tree; a false means "not proven" (there is no SAT solving, and evaluate() is untouched). The rule engine can report the catalog rules that can never pass or never fail:

simplify() (since 1.9.0) returns an equivalent, smaller specification. It applies, bottom-up until nothing changes: identity (A ∧ true = A, A ∨ false = A), annihilation (A ∧ false = false, A ∨ true = true), double negation, idempotence (A ∧ A = A, by equals()), flattening of nested and/or, the contradictions and tautologies proven above (collapsed to alwaysFalse()/alwaysTrue()) and subsumption (A ∧ B = A and A ∨ B = B when A is a special case of B). The input is never modified, and simplifying a simplified tree returns it unchanged. A node annotated with because()/withCode() is a unit: it is simplified inside but never removed, merged or replaced by a constant, so the verdict and the annotated messages stay the same.

Two limits, both stated in the docblocks. The equivalence holds for non-null candidates, the same premise as isTautology()/isContradiction(): AND/OR/NOT reject null whatever their operands, and the node left after simplification may not (alwaysTrue()->and(alwaysTrue()) rejects null, alwaysTrue() accepts it). And it is an equivalence of truth value only: removing a redundant branch may remove one that would have raised an exception, and the default messages of unannotated nodes may change; only annotated messages and codes are guaranteed. Nothing simplifies by itself: the SQL, TCriteria and ALinq visitors compile the tree as given, and the rule engine simplifies only when asked.

13. Declarative Method Calls (MethodParameterizedSpecification)

where() reads a property; whereMethod() / Spec::calling() call a public method of the candidate with arguments declared as data and apply a specification to the result. Unlike must(closure), the method name and the arguments are data: the specification has structural equals(), a rule name and can be described in a catalog. A missing method is an evaluation error, never a silent false.

The SQL and TCriteria visitors refuse a method call as a non-translatable leaf (like must()); the ALinq visitor compiles it.

14. Spec-Driven State Machine (Modules 17 and 18)

Antevemus\ASpecification\StateMachine is a state machine in which every guard is an ISpecification: when a transition is denied, the result says why, with the code, message, property and severity of the specification that blocked it. The API is event first (send(event), the target is a consequence), the definition is data (stable key and integer version, built fluently or loaded from JSON), transition() is pure (it computes the new snapshot, the transitions taken and the effects, and executes nothing), and MachineRunner is the single write port (snapshot, history and outbox in one transaction). The snippets of this section build on each other.

14.1 A Minimal Machine: Guards Are Specifications

An event that no transition of the current state answers is unhandled (no change, no exception). When several transitions answer the same event, they are tried in declaration order and the first whose guard passes wins; a guard that throws becomes an ERROR failure marked evaluation_error.

14.2 Parallel Regions: a Pending Flag That Never Loses the Progress

A machine has one or more orthogonal regions, each with its own current state. A pending item modelled as a region of its own (none ⇄ open) can be registered and resolved from any step without a "pending" state in the main flow, so resolving it never has to guess where the process was. inState('region.state') reads another region, and a guard expression can carry its own message and code:

Guards read the snapshot before the event (the order of the regions never changes the result), and an event is all or nothing: if any region rejects it, no region moves. The machine is done when every region that declares a final state is in one; a region without final states (the pending flag) does not hold the machine open, and a done machine answers every event as unhandled.

14.3 can() and nextEvents(): a Disabled Button With Its Reason

can() is transition() itself, with no side effect; nextEvents() tests every event the current states answer (as an external event with an empty payload), so an action panel can show a disabled button together with the reason. Continuing with the pending item still open:

14.4 The Definition Is Data: JSON and the Validator

Guards and actions are referenced by name and parameters, never by closures, so a definition round-trips through arrays and JSON without loss and has a deterministic fingerprint (SHA-256 of its canonical form) to detect a definition changed without a new version. The validator runs at load time: it refuses the definition (one exception listing every error) for a region without initial state, an unknown target or inState path, a guard without factory (or an action without handler, when the known action names are given), a final state with outgoing transitions; it warns, without blocking, about unreachable states, non-final states without exit, overlapping or shadowed guards and guards that always pass. The document requirement below (pending β†’ delivered β†’ validated, or expired after 30 days) is the RequirementState the 1.0.0 changelog announced, now expressed as a machine:

14.5 The Runner: One Transaction, Idempotency, Outbox and Persisted Deadlines

MachineRunner::send() loads the instance and the definition version it was started with, checks the idempotency key, computes the transition, and writes the new snapshot (compare-and-set on its version: a concurrent change throws ConcurrencyConflictException and is never retried silently), one history row per region moved and the effects in the outbox, in one transaction. Effects (the named actions: exit β†’ transition β†’ entry) are delivered after the commit by an EffectDispatcher, each with its own idempotency key, in write order per instance; a failing handler never undoes the transition. A deadline is persisted (due_at), never a timer in memory: a cron asks for the due ones and delivers them through the same runner, and a late or repeated delivery is harmless.

EffectHandlerRegistry::registerSync() declares the rare effect that must be able to veto: it runs inside the transaction, before the commit, and an exception rolls the whole step back. Each instance stays pinned to the definition version it started with; moving it to a new version is explicit (registerMigration() + migrate()).

14.6 PDO Persistence and the Reference DDL

MachinePersistence::pdo() gives the same ports over the tables of resources/sql/state_machine.ansi.sql: portable SQL, CREATE TABLE IF NOT EXISTS (accepted by PostgreSQL, MySQL/MariaDB and SQLite), a primary key on every table, UNIQUE (instance_id, idempotency_key), instants as fixed-width UTC text. The due deadlines are searched with a specification compiled to SQL by the SQL visitor. The InMemory and PDO stores run the same test battery (SQLite in memory in the suite).

JSON columns. snapshot, payload, params, reason and transition_result are JSON stored as VARCHAR(4000), the portable size that also fits Oracle. A larger event payload needs the column widened to TEXT/JSONB/CLOB in your copy of the DDL. If the PDO connection is already inside a transaction, the runner joins it and the owner of that transaction commits or rolls back.

Upgrading from 1.8. Since 1.9.0 state_machine_region has a nullable paused_remaining column (the balance of a paused deadline, example 14.7). CREATE TABLE IF NOT EXISTS does not add a column to an existing table, so a database created with the 1.8 script needs, once, before the first deadline pause: ALTER TABLE state_machine_region ADD COLUMN paused_remaining INTEGER;. Until then, every definition that never pauses a deadline keeps working on the old table; the first pause fails and its step is rolled back.

14.7 Deadlines From the Context: an SLA That Pauses While a Pending Item Is Open

A deadline has exactly one source: a fixed duration (deadline('P3D', 'EXPIRE'), as in 14.5), a duration read from the context (afterFrom: 'context.path', an ISO-8601 string or a DateInterval), or the due instant itself read from the context (atFrom: 'context.path', a DateTimeInterface or an ISO-8601 text with date, time and offset). The path is read with the dot notation of the PropertyAccessor when the state is entered, from the context given to that call, so the computation stays pure. A missing or invalid source never becomes a silently empty deadline: the event that enters the state is rejected with SM_DEADLINE_SOURCE_INVALID (an ERROR that names the path, never the value), and so is a start, which then writes nothing. pausedWhile takes a guard expression (a 'region.state' string is a shorthand for inState), evaluated after every accepted step: when it starts to hold, the deadline pauses (no due instant, so no cron ever finds it, and the remaining whole seconds are kept); when it stops holding, the deadline resumes from that balance. Re-entering the state restarts the deadline and drops the balance. Below, the SLA is a region of its own (an absolute instant from the contract), paused while a pending flag (the parallel region of example 14.2) is open, and each stage lasts what the client's table says:

deliverDue() sends a null context unless given a contextProvider (it receives the due deadline and the loaded instance): a deadline transition entering a state whose deadline reads the context needs the instance's context, as here. A pause condition is any guard expression; one that throws rejects the step. The validator refuses a pausedWhile with an unknown guard or an inState path that does not exist, and warns when it references the deadline's own region (the pause would never end). The snapshot keeps the balance (pausedRemaining(), paused_remaining in its array form only while some region is paused, so a 1.8 snapshot loads unchanged), and migrate() refuses a migration that would leave a paused balance in a state whose deadline declares no pausedWhile, since it could never resume.

Deliberately out of scope (each can be modelled without it): actors, invoke/spawn (asynchronous work is an external job that sends an event back), eventless (always) transitions and raise (the caller sends the next event), nested states inside a region and history states (a parallel region, like the pending flag above, covers the real cases), and a machine-owned context (assign): the context is the caller's domain object. The history is a trail; the snapshot is the source of truth (no event sourcing).

15. Use Cases With a Result Shared With the Machine (Module 19)

A use case is one class with one method, execute(object $request): UseCaseResult. UseCaseRunner runs it through the middlewares, validates the request first (its #[ValidateRule]/#[AssertSpec] attributes plus any specification registered for its class) and only an ERROR failure blocks: WARNING and INFO come back with the success. The status is a closed domain enum (SUCCESS, CREATED, ACCEPTED, NO_CHANGE, INVALID, REJECTED, NOT_FOUND, CONFLICT, FORBIDDEN, FAILED); success is derived from it alone, and HTTP exists only in one edge method, toHttpStatus(). An unhandled exception becomes FAILED with a generic message and goes to an optional IUseCaseErrorReporter: no message, class, file, line or stack trace ever reaches the result. No mediator, container or handler discovery is embedded.

15.1 IUseCase, UseCaseRunner and TransactionMiddleware

TransactionMiddleware commits only a success and rolls back a failure status or an exception (which it rethrows for the runner to turn into FAILED). UseCaseResult and the machine's TransitionResult implement the same Results\IOutcome (isSuccess(), getReason()), and UseCaseResult::fromTransition() converts one into the other.

15.2 A Machine Event as a Use Case

StateMachineEventUseCase (or Spec::machineEventUseCase()) exposes one event of a machine as a use case: the request is a MachineEventRequest (instance id, occurredAt, payload, optional idempotency key, actor and expected version), and the outcome is the runner's result converted: accepted β†’ SUCCESS (payload = the new snapshot), rejected β†’ REJECTED with the guards' codes, unhandled β†’ CONFLICT, a stale version β†’ CONFLICT with SM_CONCURRENCY_CONFLICT, an unknown instance β†’ NOT_FOUND. With the quote machine of 14.2:

16. Concurrent Data Resolution for I/O-Bound Specifications (Module 21)

A rule that depends on data held elsewhere (a credit bureau score, a fraud check, the balance in the core banking) does not call the service. Spec::external() declares the data it needs: a source name, parameters read from the candidate (dot paths), and an ordinary specification that decides on the value, with its own because()/withCode(). A DataResolver collects the declarations of the whole tree, fetches the data concurrently (curl_multi, no package dependency, works under PHP-FPM) and only then evaluates the tree, synchronously, as every specification is evaluated. Sources are registered by name: HttpDataSource (fixed base URL, a path template with {placeholders}, headers, its own time limit, a cost and a cache TTL) and CallableDataSource (any synchronous callable: a repository, an SDK, a gRPC client).

The policies: concurrent() (the default) fetches every request of the tree in one stage and reports every failure, as an evaluation outside the resolver does; tiered() (an explicit choice, when calls cost money) fetches by cost, cheapest first, and after each stage evaluates the tree in three-valued logic (true, false, unknown): once the verdict can no longer change (an and with a false operand, an or whose left operand is true) the more expensive sources are not called; sequential() fetches nothing beforehand and each leaf fetches when the evaluation reaches it. The verdict is the same under every policy and outside the resolver; under tiered(), a branch that was not fetched because it could not change the verdict reports no failure, so the list of failures can be shorter than with concurrent(). An or stops early only on its left operand: in the core an error on the left operand aborts the disjunction, so an unknown left operand cannot be skipped. Two leaves asking the same data (same source, same parameters) cause one call. A failed fetch is unknown, never a decision on its own.

Errors follow the 1.8.0 rule: a fetch that fails (an HTTP status outside 2xx, a transport error, the time limit of the source or of the whole resolution with ResolutionPolicy::tiered(timeout: 3.0), a parser that throws, an unknown source) is an evaluation error of the leaf: a failure with the generic message, evaluation_error = true, and a DataFetchException in $result->exception with the status code, never the response body. There is no retry. A parameter value is percent-encoded into the URL template, so /, ?, #, .. or a whole URL in a document number cannot change the host or the path of the call; redirects are not followed. A cache between resolutions is optional (new DataResolver($registry, new InMemoryDataCache()), with the TTL of each source; failures are never cached). ext-curl is a suggest: without it an HttpDataSource is refused when registered and a CallableDataSource keeps working. The SQL, TCriteria and ALinq visitors refuse the leaf with their "non translatable" exception. State machine guards stay pure: a use case resolves the data before sending the event. Concurrency for non-HTTP sources (Fibers/Revolt) is a planned satellite package.

17. Repository Over PDO (Module 22)

PdoRepository is the IRepository of a database table: a query is a specification compiled to SQL by the SQL visitor (any of its dialects), a write is a prepared INSERT/UPDATE/DELETE, and iterate() streams the cursor. The author of the entity maps it to the table, with attributes or with a builder; a mapped property is read and written by reflection even when it is private (the mapping is the author's opt-in). The library generates no DDL; relationships are declared on the entity and loaded explicitly, in batch (example 17.3).

Optimistic locking: with a #[Version] counter, the INSERT writes 1 and every UPDATE increments it and only matches the row while it still holds the version the entity was read with.

The rules that matter, and the builder for a class that cannot carry attributes:

17.1. Lifecycle: Timestamps, Soft Delete, Hooks and the SQL Log (Module 25)

The bookkeeping an application usually writes by hand around a table, as options of the mapping and of the repository: creation and modification timestamps, a removal that marks the row instead of deleting it, callables around each write, a log of every statement, booleans stored as integers or as text and the time zone of columns that hold local time without an offset (the Adianti tables, for one). Without them nothing changes.

Removal marks the row; every read leaves the marked rows out, and two views see them:

Legacy flags stored as text, such as the CHAR(1) 'Y'/'N' columns of the Adianti tables, are read strictly:

17.2. Batch Writes and Set-Based Operations (Module 25)

Writes and reads over many rows that do not build one entity at a time: a multi-row INSERT, an UPDATE by specification, a projection into arrays, grouped aggregates computed by the database and a subquery over another repository.

17.3. Relationships: References, Compositions and Eager Loading (Module 26)

Relationships are declared on the entity and loaded explicitly, never lazily and without proxies. A reference (#[ManyToOne]) is an aggregation: it is never saved with the entity. A collection (#[OneToMany]) is read-only unless cascade: true makes it a composition, saved and removed with its parent. An association (#[ManyToMany]) goes through a join table: saving the owner writes only the links. The repositories of the related classes are given with withRelated().

Eager loading is a view, with(); each relation costs one query per batch of entities, never one per entity:

A collection that was not loaded is never taken as empty, and only a loaded one has orphans:

An association is never a composition: the targets live on their own, and a loaded collection only adds and removes links:

A collection built by hand can be declared loaded, and iterate() loads relations page by page:

18. Domain Events From Specifications (Module 23)

A trigger binds a specification to domain events: when an entity starts to satisfy it, the trigger emits its enter event; when it stops, its exit event. TriggeringRepository decorates any IRepository: on put()/update()/updateWithDelta() it reads the stored state of the entity by its identity, writes, evaluates every trigger on the state before and after, and publishes the crossings; on remove()/removeBy() it emits the exits. The production publisher writes to an outbox in the same transaction as the write, and DomainEventDispatcher delivers it after the commit, as the effects of the state machine.

An event can move a state machine (example 14): MachineEventBridge is a ready-made handler that sends it to a MachineRunner as an internal event carrying the domain event's idempotency key, so a redelivery returns the original result and does not move the instance again.

Over the PdoRepository of example 17 the row and its events commit together when the repository and the outbox share the connection and the transaction manager. The decorator then reads the "before" with Spec::property('entityId', Spec::equalTo($id)), compiled to SQL, without configuration:

18.1. Domain Events From the Passage of Time (Module 24)

"Unpaid and due more than 30 days ago" turns true at midnight while nobody writes the invoice: only the clock moved, so the write-time triggers of example 18 never see it. A time-based trigger builds its specification from "now"; a state store keeps, for every trigger and entity, the last known answer (satisfied, evaluated_at); a sweeper, run by your cron with the instant injected, compares the two, records each crossing and publishes it through the same publisher and outbox, with the same kind of idempotency key. TriggeringRepository keeps the same store on every write, so a write and a sweep never publish the same crossing twice.

⚠️ Introducing a trigger over existing data? Run baseline() first. An entity without a recorded state counts as "not satisfied", so the first sweep publishes an enter for every invoice that is already overdue. That is the truth (they are inside and nobody was told), but rarely what a mailing wants. baseline($now) records the current state of every trigger for every entity and publishes nothing; the following sweeps publish only new crossings.

In production the store is a table, domain_event_trigger_state, in the same resources/sql/domain_events.ansi.sql (running the script again on a 1.11 database only adds it). Give the store, the outbox and the repository the same connection and transaction manager, so that the state of a crossing and its event commit together:


πŸ—οΈ Directory Structure


πŸ§ͺ Code Quality & Automated Tests

The library features comprehensive unit and integration test coverage with zero regressions:

Report below as printed by the runner, with its Portuguese labels translated; the duration depends on the machine. Module 22 also prints its benchmark (10 000 rows in SQLite) and, without the ASPEC_PDO_* variables, that its PostgreSQL and MySQL runs were skipped, Module 24 its sweeps over 1 000 and 10 000 rows, Module 25 its batch, updateWhere(), null/subquery and null/Char parity runs and Module 26 its eager loading benchmarks (1 000 parents Γ— 10 children, 1 000 owners Γ— 5 targets) and the peak memory of iterate() with relations over 2 000 and 10 000 rows; those lines are omitted here:


πŸ—ΊοΈ Roadmap & Next Steps

Antevemus ASpecification is actively maintained and continuously evolving. For the full strategic roadmap and timeline:

πŸ‘‰ View the complete ROADMAP.md

Key highlights & upcoming roadmap:

  1. English DocBlock Internationalization (Shipped in v1.1.0)
  2. PHP 8.4 Declarative Attributes (#[AssertSpec], #[ValidateRule]) (Shipped in v1.1.0)
  3. ALinq Lazy Streaming Pipeline & O(1) RAM Evaluation (Shipped in v1.1.0)
  4. Review-Driven Hardening: Error State, Strict Typing, Rule Binding & Document Matrix (Shipped in v1.2.0)
  5. README Promises I: Every Documented Example Runs (Shipped in v1.3.0) 5a. Domian Parity: Subsumption Algebra, Partition DAG, Synchronized Repositories, Entities & Utilities (Shipped in v1.4.4)
  6. README Promises II: Inter-Process, Unicode, Lazy Sources, Relational Catalog & IN (Shipped in v1.5.0)
  7. Announced-but-Unshipped Backlog I: ULID / UUID v7 Identities & Failure Severity (Shipped in v1.5.0)
  8. PHP-Native Serializer Retirement, Step 1: Deprecation (Shipped in v1.5.0)
  9. MethodParameterizedSpecification: Declarative Method Calls (Shipped in v1.6.0)
  10. Tautology & Contradiction Detection (Shipped in v1.6.0)
  11. Announced-but-Unshipped Backlog II: TTL, Partition DAG Index & Three SQL Dialects (Shipped in v1.6.0) 11a. Specification-Driven State Machine (the reserved RequirementState, now expressible as a machine) (Shipped in v1.8.0) 11b. Use Cases With a Result Shared With the State Machine (Shipped in v1.8.0)
  12. Specification simplify() (Shipped in v1.9.0)
  13. PHP-Native Serializer Retirement, Step 2: Removal & JSON-Only Envelope (persisted per-entity metadata) (Shipped in v2.0.0)
  14. Shared Property Accessor with ALinq (antevemus/adeep-property-access) (Shipped in v2.0.0)
  15. Concurrent Data Resolution for I/O-Bound Specifications (core shipped in v1.10.0) 15b. Fibers/Revolt Adapter for Non-HTTP Sources (satellite package antevemus/aspecification-async)
  16. Distributed Specification Cache (PSR-6 / PSR-16 / Redis)
  17. GraphQL AST & OpenAPI 3.1 Query Compilers
  18. Doctrine ORM & Laravel Eloquent Query Visitors
  19. Specification-Driven Domain Event Triggers (Shipped in v1.11.0) 19b. Time-Based Domain Event Triggers (a scheduled sweeper for crossings without a write) (Shipped in v1.12.0)
  20. AI-Assisted Specification Synthesizer
  21. PDO-Backed Repository (the Domian HibernateRepository) (Shipped in v1.11.0) 21b. PdoRepository Follow-ups (lifecycle columns and soft delete, hooks, SQL log, batch insert, bulk update by specification, projections and aggregates, subqueries, ManyToOne/OneToMany relationships with eager loading in batch) (Shipped in v1.13.0) 21c. PdoRepository Follow-ups II (ManyToMany, marking a hand-built collection as loaded, iterate() with relations over unbuffered MySQL, CHAR(1) booleans) (Shipped in v1.14.0)
  22. Accent-Insensitive Text Comparison (an explicit option that ignores case and accents, with the same answer in memory and in SQL)
  23. Write-Behind Hybrid Repository (the Domian INMEMORY_AND_FILE mode: memory now, disk on persist())

βš–οΈ Upstream Attribution, Licensing & Acknowledgments

Antevemus ASpecification expresses its deepest respect and gratitude to the pioneer authors who established the theoretical and practical foundations of this pattern:

Apache License 2.0 Compliance

The upstream Domian framework is distributed under the terms of the Apache License, Version 2.0 (Copyright 2006-2010 the original author or authors, as stated in the Domian sources; developers Eirik Torske and BjΓΈrn Nordlund). In full compliance with Section 4 of said license:


πŸ“š References & Bibliography


πŸ“„ License

Distributed under the MIT license. See THIRD_PARTY_NOTICES.md for more information.


πŸ‘¨β€πŸ’» Authors & Corporate Maintenance

Heliton Junior (CTO)


All versions of aspecification with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
ext-json Version *
ext-mbstring Version *
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package antevemus/aspecification contains the following files

Loading the files please wait ...