trismegiste/prolog
A Prolog interpreter written in PHP. Embed logic programming and rule-based reasoning directly in your PHP applications using familiar Prolog syntax, unification, backtracking, and query solving—no external extensions required.
Install with Composer; there are no PHP extensions or system binaries to configure:
composer require trismegiste/prolog
The package is a self-contained Prolog implementation written in PHP: a compiler that turns Prolog source into WAM instructions, and a virtual machine that executes them. Nothing is compiled to native code and nothing shells out — everything runs in the PHP process, which is the key fact that shapes how you use it.
Where to look first. The public surface is small. Skim src/ in this order:
findall, !/0, etc.) actually exist, which is more reliable than reading the README.Start there before you write a single line of application code; the supported-builtin list is the real contract of the package, and it is smaller than ISO Prolog.
First use case. The sweet spot is a small, declarative rule base that would be miserable to express as nested if statements: access-control rules, pricing/discount eligibility, compatibility matrices, workflow state transitions, or term-rewriting/simplification. Write the domain as .pl facts and rules, assert or load them, then query and iterate results.
use Trismegiste\Prolog\Program;
use Trismegiste\Prolog\Solver;
$source = <<<'PL'
parent(john, mary).
parent(mary, sue).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
PL;
$program = new Program();
$program->addString($source); // see src/ for the exact loading method name in your version
$solver = new Solver($program);
$solver->query('ancestor(john, Who)');
while ($solver->nextSolution()) {
$who = $solver->getBinding('Who'); // atom/term for that solution
// ...
}
Class and method names have shifted between tagged versions of this package — confirm the entry points in src/ before pinning anything, and wrap the whole thing in a tiny adapter class of your own so no application code touches the vendor API directly.
Treat Prolog as a pure function, not a service. Instantiate a fresh machine per query. The compiled Program can be built once and reused, but the trail, heap, and choice points must be fresh for each query, otherwise solutions from a previous query bleed into the next one. The clean shape is:
Load domain knowledge from files, not heredocs. Keep .pl sources in resources/prolog/ (or database/prolog/) and load them by path. This lets you:
Bridge Eloquent into facts deliberately. Do not query the database from inside Prolog. Materialize the rows you need into facts up front, bounded by the query you are actually answering:
$facts = Role::query()
->where('tenant_id', $tenant)
->get(['user_id', 'role'])
->map(fn ($r) => sprintf('has_role(%s, %s).', atom($r->user_id), atom($r->role)))
->implode("\n");
Keep an atom() helper that lowercases and quotes anything that is not a safe unquoted atom — IDs with dashes, UUIDs, and mixed case will otherwise be parsed as variables or fail to compile.
Cache the compiled program. Compilation is not free. If the rule base is static, compile once and store the resulting program in the container (or in Laravel's cache) rather than per request. If the rule base depends on request data, compile only the static part and assert the dynamic facts at query time.
Iterate with an explicit bound. Prolog is generative; the VM will happily keep producing solutions or recurse forever. Always decide the termination strategy up front: break after the first solution, take the first n, or collect into a set with a hard cap and a wall-clock guard.
$solutions = [];
$count = 0;
while ($solver->nextSolution() && $count++ < 100) {
$solutions[] = $solver->getBinding('X');
}
Laravel wiring. Register a singleton compiler/loader (not the machine) in a service provider, and resolve a fresh machine per query. Keep the Prolog layer behind a small interface:
interface RuleEngine {
/** @return array<int, array<string, mixed>> */
public function query(string $goal, array $facts = [], int $limit = 25): array;
}
Bind the concrete implementation in the provider. Every call site now depends on RuleEngine, not on trismegiste/prolog — which matters given the package's size and activity level.
Separate facts from rules across sources. Put invariant rules in version-controlled .pl files and dynamic facts in PHP-generated strings. Mixing them makes it impossible to tell whether a bug is in your data or your logic.
Test the Prolog, not the PHP. The valuable tests are golden query tests: a fixed fact base, a fixed goal, and an expected set of solutions (sort before comparing, since solution order depends on clause order). Add one test per builtin you rely on — this is how you discover that a predicate you assumed existed does not.
Use it for analysis, not for write paths. The natural integration is: PHP owns state and persistence, Prolog owns "given these facts, which of these are true / what satisfies this constraint". Never let a Prolog solution write to the database inside the backtracking loop — you will get duplicates on backtracking and partial writes on exceptions. Collect solutions first, act afterwards.
The builtin set is not ISO Prolog. Assume nothing. Verify !/0, findall/3, between/3, is/2, =:=, \==, once/1, assert/1, and retract/1 individually against the registry. Missing findall/3 in particular changes your whole design — you end up collecting solutions in PHP instead.
= vs == vs =:=. Unification, term identity, and arithmetic equality are three different things and the errors are silent (you get false, not an exception). If arithmetic "doesn't work", you almost certainly used = where you needed is/2.
Compile errors surface at load time, not query time. Wrap program construction in try/catch and log the offending source line. A malformed .pl file in resources/ will otherwise produce a 500 with an opaque message on the first request that touches it — validate all .pl files in a test so this never reaches production.
No tail-call optimization you can rely on. Deeply recursive rules (transitive closure over a large graph, list processing over long lists) will blow PHP's memory limit or max_execution_time before Prolog semantics become the problem. Keep fact bases in the hundreds-to-low-thousands range, not tens of thousands, and set explicit limits in your adapter.
Infinite loops are the default failure mode. Left-recursive rules (ancestor(X,Y) :- ancestor(X,Z), parent(Z,Y).) loop forever. Order clauses so recursion comes last, and always guard generative goals with a bound.
Cut (!) is where implementations diverge. Even where supported, cut semantics inside a VM port are a common source of "why did it stop returning solutions". If you need determinism, prefer restructuring the rule base, or break out of the solution loop in PHP instead of relying on cut.
Quoting and escaping. Lowercase identifiers are atoms; uppercase or underscore-prefixed identifiers are variables. UUIDs, emails, and mixed-case names must be quoted. Building Prolog source with sprintf and forgetting this produces rules that compile but never match — or worse, silently unify with a variable. Centralize atom-safe quoting in one helper.
Heredocs and backslashes. PHP heredoc/nowdoc interacts badly with Prolog escapes and $ in atoms. Use <<<'PL' (nowdoc) for static sources loaded inline, or better, read from files.
Debugging is print-based. There is no tracer UI. The reliable technique is to shrink the problem: query the leaf predicates one at a time from a REPL-ish script to confirm the facts loaded, then add one rule layer at a time. Dumping the compiled instruction list is useful for confirming the parser understood your source the way you think it did.
Bindings come back as terms, not PHP types. Numbers and atoms need explicit coercion before they hit your domain layer; do the conversion at the adapter boundary so the rest of the app never sees a Prolog term object.
Concurrency and long-running workers. The machine is mutable state. Under Octane, queue workers, or any singleton-scoped service, never hold a machine instance across requests — that is a guaranteed cross-request leak. Compiler/program singletons are fine; machines are not.
Mind the dependency. At 32 stars this is a small, low-activity package. Pin an exact version in composer.json, do not assume bug fixes are coming, and keep the adapter boundary thin enough that a swap to a different rules engine is a one-file change. If you need to extend it — adding a builtin that calls into PHP — that is the intended extension point, but treat any predicate that reaches into application state as a security boundary: only expose pure functions, never anything that reads the request or the filesystem.
How can I help you explore Laravel packages today?