maatify / php-eligibility
Framework-neutral eligibility rules and typed decisions for Maatify applications.
Requires
- php: ^8.4
- ext-pcre: *
- ext-pdo: *
- ext-pdo_mysql: *
- maatify/exceptions: ^1.0
- maatify/persistence: ^1.4
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-18 19:49:16 UTC
README
Maatify Eligibility
Framework-neutral eligibility rules and typed decisions that answer one reusable business question about an external Subject in a supplied Context.
Package Summary
maatify/php-eligibility is a standalone, framework-neutral PHP library that
answers "Is a given Subject eligible in the supplied Context?" for domains
such as products, categories, payment methods, shipping, and promotions, so
those domains do not each re-invent a customer/country restriction subsystem.
Host Input -> Public API -> Domain Service -> Integration Boundary -> Observable Result
The package knows external identities only. It does not know the database model, lifecycle, or implementation of the domains that own those identities.
The canonical behavioral contract for the Pre-Stable v1.0.0-rc.1 Release
Candidate is ELIGIBILITY_PACKAGE_REFERENCE.md.
The package identity is maatify/php-eligibility and its distribution is
available through Packagist.
Status
- Release state: Pre-Stable
v1.0.0-rc.1Release Candidate. - Package identity:
maatify/php-eligibility. - Distribution: Packagist.
- Quality: see Quality Status.
Key Features
- Immutable, typed
Subject,Context,Rule, andEligibilityDecisionvalues with canonical UTF-8 string validation and bytewise canonical order. - Deterministic rule evaluation: ALLOW/DENY effects, active/inactive lifecycle, DENY precedence, ALLOW-list and DENY-only semantics, AND across dimensions, and no inferred cross-dimension or cross-Subject behavior.
- Fully typed decision output:
EligibilityDecision, per-dimensionDimensionOutcome, completeRuleReferencetraces, and stable machine reason codes. - Single and ordered batch evaluation with duplicate-Subject rejection, empty batch support, and a bounded bulk load path (no canonical N+1 query pattern).
- Management lifecycle: create, inspect/list (including inactive Rules),
active-dimension introspection, effect and lifecycle mutations, atomic
replaceDimensionRules(), and idempotent Subject cleanup. - Direct-PDO MySQL-compatible persistence with separate command, management query, and evaluation-read adapters, plus shared Persistence transaction ownership/savepoint mechanics and Eligibility-owned coordination locking; no Host foreign keys or joins.
- Typed package exceptions on the shared
maatify/exceptionshierarchy, with unknown external throwables propagated unchanged.
Requirements
| Requirement | Constraint |
|---|---|
| PHP | ^8.4 |
| PHP extensions | ext-pdo, ext-pdo_mysql, ext-pcre |
| Runtime packages | maatify/exceptions (^1.0), maatify/persistence (^1.4) |
| Database | MySQL-compatible database-server semantics through direct PDO (capability-based; no minimum product version is declared — see Persistence and Schema) |
maatify/persistence ^1.4 is an explicit runtime dependency. v1.4.0 is the
minimum stable line required for the released
SavepointTransactionRunnerInterface and PdoSavepointTransactionRunner.
Eligibility delegates transaction ownership, operation-local savepoints,
cleanup, and original-Throwable preservation to that shared API.
The database contract requires transactional InnoDB-style package-owned table behavior, binary-safe exact-value storage/comparison, the bounded indexed-key capacity of this package's schema, and the uniqueness/index semantics the schema uses. Being labelled MySQL-compatible is not enough; the server must provide those capabilities. MariaDB compatibility is not claimed without executed MariaDB verification.
Installation
Install the Pre-Stable v1.0.0-rc.1 Release Candidate from
Packagist:
composer require maatify/php-eligibility:1.0.0-rc.1
Development access
For development access only, use a local checkout of the current main branch
through a Composer path repository. This development path is separate from the
v1.0.0-rc.1 Release Candidate. From your consumer project:
git clone https://github.com/Maatify/php-eligibility.git .tools/php-eligibility
composer config repositories.php-eligibility '{"type":"path","url":".tools/php-eligibility","options":{"symlink":true}}'
composer require maatify/php-eligibility:dev-main
The dev-main constraint is development-only access to the current main
checkout. For developing the library itself (running the full local parity suite),
clone into a working directory and follow Development and Testing.
Installation (both paths) requires ext-pdo, ext-pdo_mysql, and
ext-pcre. The package performs no automatic setup: the schema asset is an
install asset (schema/eligibility_rules.sql),
not a migration run at install time.
Quick Usage
Evaluation
use Maatify\Eligibility\Management\Query\RuleCriteria; use Maatify\Eligibility\Evaluation\Service\EligibilityEvaluationService; use Maatify\Eligibility\Management\Service\EligibilityManagementService; use Maatify\Eligibility\Evaluation\Decision\DecisionReasonEnum; use Maatify\Eligibility\Rule\Repository\PdoActiveRuleReader; use Maatify\Eligibility\Rule\Repository\PdoRuleCommandRepository; use Maatify\Eligibility\Rule\Repository\PdoRuleManagementQuery; use Maatify\Eligibility\Rule\RuleEffectEnum; use Maatify\Persistence\Pdo\Transaction\PdoSavepointTransactionRunner; use Maatify\Eligibility\Evaluation\Value\Context; use Maatify\Eligibility\Evaluation\Value\ContextDimension; use Maatify\Eligibility\Common\Value\Subject; $pdo = new PDO('mysql:host=127.0.0.1;dbname=app;charset=utf8mb4', 'app', 'secret', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ]); $pdo->exec(file_get_contents(__DIR__ . '/vendor/maatify/php-eligibility/schema/eligibility_rules.sql')); $commandRepository = new PdoRuleCommandRepository($pdo); $managementQuery = new PdoRuleManagementQuery($pdo); $activeRuleReader = new PdoActiveRuleReader($pdo); $transactionRunner = new PdoSavepointTransactionRunner($pdo); $management = new EligibilityManagementService( $commandRepository, $managementQuery, $commandRepository, $transactionRunner, ); $evaluation = new EligibilityEvaluationService($activeRuleReader); $subject = new Subject('product', '150'); $management->createRule(new \Maatify\Eligibility\Management\Command\CreateRuleCommand( $subject, 'country', 'EG', RuleEffectEnum::ALLOW, )); $management->createRule(new \Maatify\Eligibility\Management\Command\CreateRuleCommand( $subject, 'customer_type', 'blocked', RuleEffectEnum::DENY, )); $decision = $evaluation->decide($subject, new Context( ContextDimension::fromStrings('country', 'EG'), ContextDimension::fromStrings('customer_type', 'retail'), )); if ($decision->eligible) { // $decision->reasonCode is DecisionReasonEnum::ELIGIBLE }
Ordered batch evaluation
use Maatify\Eligibility\Common\Value\SubjectCollection; $decisions = $evaluation->decideMany( new SubjectCollection( new Subject('product', '150'), new Subject('product', '151'), ), new Context(ContextDimension::fromStrings('country', 'EG')), ); // Results preserve the supplied Subject order; duplicate Subjects are rejected.
Management lifecycle
use Maatify\Eligibility\Management\Command\ReplaceDimensionRulesCommand; use Maatify\Eligibility\Management\Command\DesiredRule; use Maatify\Eligibility\Management\Command\DesiredRuleCollection; // Atomic replacement of the complete active set for one Subject dimension. $management->replaceDimensionRules(new ReplaceDimensionRulesCommand( $subject, 'country', new DesiredRuleCollection( new DesiredRule('EG', RuleEffectEnum::ALLOW), new DesiredRule('KW', RuleEffectEnum::ALLOW), ), )); // Bounded management reads include inactive Rules. $rules = $management->inspectRules(new RuleCriteria($subject, 'country'));
Public Runtime API
Public surface (see the Package Reference for the full contract):
- Values:
Subject,SubjectCollection,Context,ContextDimension,ContextValue,ContextValueCollection,Rule,RuleIdentity,RuleCollection,RuleEffectEnum,RuleLifecycleEnum,CanonicalString. - Decisions:
EligibilityDecision,DimensionOutcome,DimensionOutcomeCollection,RuleReference,RuleReferenceCollection,DecisionReasonEnum(UNRESTRICTED,ELIGIBLE,DENIED),DimensionReasonEnum. - Evaluation service:
EligibilityEvaluationService/EligibilityEvaluationServiceInterface—decide()anddecideMany(). - Management service:
EligibilityManagementService/EligibilityManagementServiceInterface—createRule(),inspectRule(),inspectRules(),inspectActiveDimensionKeys(),updateRuleEffect(),deactivateRule(),reactivateRule(),replaceDimensionRules(),cleanupSubject(). - Persistence contracts:
RuleCommandRepositoryInterfaceowns command mutations,RuleManagementQueryInterfaceowns bounded management reads, andActiveRuleReaderInterfaceowns active bulk reads for evaluation. The internalRuleMutationSupportInterfaceowns only the coordination lock, complete Subject + dimension mutation read, and coordination cleanup needed by atomic replacement/cleanup. Generic transaction/savepoint mechanics are owned bymaatify/persistence;EligibilityManagementServicereceives itsSavepointTransactionRunnerInterfaceseparately. - PDO adapters:
PdoRuleCommandRepository,PdoRuleManagementQuery,PdoActiveRuleReader, andPdoSavepointTransactionRunnerare constructed from the same PDO connection. The command adapter implements only the Eligibility command and mutation-support contracts; callers wire each responsibility explicitly to the corresponding service dependency. - Commands / queries / results:
CreateRuleCommand,UpdateRuleEffectCommand,DeactivateRuleCommand,ReactivateRuleCommand,DesiredRule,DesiredRuleCollection,ReplaceDimensionRulesCommand,CleanupSubjectCommand,RuleCriteria,ActiveDimensionKeysQuery,ActiveDimensionKeyCollection,SubjectDecisionResult,SubjectDecisionCollection. - Exceptions:
EligibilityExceptionInterface,InvalidEligibilityInputException,RuleNotFoundException,RuleIdentityConflictException,RuleConcurrencyConflictException(backed by the sharedmaatify/exceptionshierarchy).
Critical Runtime Behavior
- No active Rules for any dimension ->
UNRESTRICTED, eligible, with no dimension outcomes. This does not mean the owning domain has enabled the Subject. - Per-dimension evaluation, grouped by dimension key, combined with AND across dimensions. No inference across dimensions or Subjects.
- DENY always wins within a dimension over every matching ALLOW.
- ALLOW Rules create an allow-list: at least one supplied Context value must match an ALLOW, otherwise the dimension fails.
- DENY-only Rules behave as a deny-list: the dimension passes unless a DENY value matches.
- Missing Context values are explicit: an ALLOW dimension absent from the Context fails; a DENY-only dimension absent from the Context passes (fail-open). The trace distinguishes both from supplied non-matching values.
- Lifecycle: create always stores active Rules; inactive Rules stay unique and management-visible but are invisible to evaluation. Deactivate / reactivate / update-effect are idempotent and typed not-found on a missing identity; create against an existing natural identity is a typed conflict.
- Replacement:
replaceDimensionRules()defines the complete desired active set for one Subject + dimension, reuses/reactivates existing identities, creates missing identities, deactivates omitted active Rules (never hard-deletes), preserves omitted inactive Rules, and leaves other dimensions/Subjects untouched. - Batch: duplicate Subjects rejected, empty batch valid, results in accepted input order, single and batch evaluation equivalent, bounded bulk loading with no canonical N+1 path.
Architecture Guarantees
- Canonical identity:
subject_type + subject_id + dimension_key + dimension_valueis the one natural identity, independent of effect and lifecycle. Canonical strings reject null/non-string/coerced input, malformed UTF-8, empty/whitespace-only values, and leading/trailing whitespace without trimming, case conversion, transliteration, or normalization. - Canonical ordering: string comparison and ordering use ascending bytewise ordering over the validated UTF-8 byte sequence, independent of database row order or collation.
- Framework neutrality: direct PDO persistence only; no ORM, no external query builder, no framework runtime requirement, and no Host foreign keys or joins. The Host validates external identities; the package trusts them.
- Transaction ownership:
PdoSavepointTransactionRunnerfrommaatify/persistenceowns the transaction lifecycle when no outer transaction is active. Inside a Host-owned outer transaction it creates an operation-local savepoint, releases it on success, and rolls back to it on failure without committing or fully rolling back the Host transaction. Eligibility still owns mutation coordination, and the runner plus all Eligibility PDO adapters MUST use the same PDO connection. - Concurrency: parallel creates cannot duplicate a natural identity; parallel replacements cannot produce partial or mixed dimension state; reads observe coherent committed states.
Exception and Error Propagation
- Package typified failures use the single marker
Maatify\Eligibility\Exception\EligibilityExceptionInterfaceand the sharedmaatify/exceptionshierarchy. - Duplicate natural identities classify to
RuleIdentityConflictException; missing identities toRuleNotFoundException; unresolved uniqueness or concurrency conditions toRuleConcurrencyConflictException. - Unknown
PDOException/Throwableinstances propagate unchanged; a known semantic condition is converted to a package exception only on documented driver-specific evidence, preserving the original asprevious.
Security and Trust Boundaries
- No repository secrets in baseline CI; integration credentials are temporary and local to the runner/service container only.
- CI actions are pinned to immutable full commit SHAs. The workflow-lint tool
(
actionlintpinned tov1.7.12) is downloaded from its release and verified against the published SHA-256 checksum file before execution; the Composer security audit runs as part of the Composer gate. - The package never reads Host application databases, schemas, or credentials, and never requires Host-side mutations or setup scripts at install time.
Documentation
The Usage Guide contains the capability decision map and helps a consumer choose the smallest suitable public API surface before installing or trying the package.
| Document | Purpose |
|---|---|
| Package Reference | Canonical RC1 contract: identity, Context, Rule, Decision, lifecycle, ordering, persistence, transaction, concurrency, error, batch, and 52-scenario coverage. |
| Usage Guide | Consumer-facing API guide, capability decision map, input/output types, transaction notes, and links to runnable examples. |
| Runnable Examples | Standalone public-API examples for evaluation, batch evaluation, management, replacement, PDO wiring, and typed exception handling. |
| Schema | Persistence contract, tables, bounds, applying/reapplying, and the local MySQL fixture. |
| CHANGELOG | RC1 change history under [1.0.0-rc.1]. |
| Security Policy | Support state, vulnerability reporting, and scope. |
| Contributing Guide | Contribution expectations, local verification, and PR requirements. |
| Code of Conduct | Community rules and reporting. |
Persistence and Schema
The package owns two tables with the maa_eligibility_ prefix:
maa_eligibility_rules— Rules with the exact-string-safeVARBINARYcolumns and a 574-byte natural-identity unique key.maa_eligibility_subject_locks— package-owned coordination rows used by replacement and cleanup to serialize per-Subject mutations.
See schema/README.md for bounds, storage guarantees,
transaction/savepoint behavior, and the local mysql:8.4.11 reproducibility
fixture. The fixture version is not a minimum supported product version.
The runtime composition keeps command, management-query, evaluation-read, and
internal mutation-support responsibilities explicit. A Host constructs the
three PDO adapters and PdoSavepointTransactionRunner from the same PDO
connection, passes the command adapter, management query, mutation-support
capability, and shared transaction runner separately to
EligibilityManagementService, and passes the active-rule reader to
EligibilityEvaluationService. Generic transaction/savepoint mechanics belong
to maatify/persistence; per-Subject coordination locking remains
Eligibility-owned.
Quality Status
- PHPStan level max, zero errors, no baseline and no suppressions.
- Unit, Golden (52 canonical acceptance scenarios with an executable evidence map), real-MySQL Integration, and concurrency/invariant suites.
- Real-service Integration is run on both supported PHP minors with a repeated run for cleanup/repeatability evidence; there is no SQLite or mock substitute.
- Consumer Verification Harness: two clean external-consumer runs using production autoload, public contracts, and the real persistence boundary.
- Fail-closed CI with stable aggregate gates:
ci-quality,ci-tests,ci-integration.
Development and Testing
Prerequisites: PHP ^8.4, Composer, Docker (for the real MySQL fixture).
composer update --no-interaction --prefer-dist --no-progress tools/check-local.sh # Composer, platform, syntax, PHPStan, whitespace, audit, workflow lint, Unit, Golden docker compose -f docker-compose.integration.yml up -d --wait tools/check-local.sh --with-integration # adds real-MySQL Integration + Harness docker compose -f docker-compose.integration.yml down
Generated files such as vendor/, composer.lock (this library does not
track it), and PHPUnit/PHPStan caches are not committed.
License
This package is released under a proprietary Maatify license. See
LICENSE. The Pre-Stable v1.0.0-rc.1 Release Candidate is
distributed through Packagist.
Author
Engineered by Mohamed Abdulalim (@megyptm)
Backend Lead & Technical Architect
https://www.maatify.dev
Built with ❤️ by Maatify.dev — Unified Ecosystem for Modern PHP Libraries