Download the PHP package prolaxu/laravel-intentql without Composer

On this page you can find all versions of the php package prolaxu/laravel-intentql. 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 laravel-intentql

IntentQL

Ask your Laravel database a question in plain English. Get a safe, validated query back.

Latest Version

The language model never writes SQL. It only fills in a constrained JSON query plan, which is parsed into a typed AST, validated field by field against the real schema, authorized by a policy, and only then compiled with Laravel's query builder.


Table of contents


The problem this solves

The obvious way to build "ask your database a question" is:

This is unsafe, and no amount of prompt engineering fixes it. The model can hallucinate tables, read columns you never meant to expose, emit a query that scans a billion rows, or be talked into something else entirely by text a user typed. You cannot review SQL you did not write, and you cannot bind values you did not choose.

This package inverts the relationship. The model is a planner, not a generator:

Everything after the planner is deterministic, inspectable and testable. The model returns field names and operators drawn from a schema you handed it; if it returns anything else, the query is rejected before a connection is touched.

Architecture

Each arrow is a class boundary with its own tests. The four boundaries designed to be replaced are LanguageModel, SchemaInspector, QueryCompiler and QueryPolicy.

Installation

The service provider and IntentQL facade are registered through package discovery.

Requires PHP 8.2+ and Laravel 11 or 12.

Configuration

config/intentql.php:

Structural limits (filters, depth, aggregates) reject a plan that exceeds them. max_results clamps instead, so a planner asking for 10,000 rows quietly gets 1,000.

Gemini setup

The provider calls generateContent directly through Laravel's HTTP client — no SDK. It requests application/json at temperature 0, retries on transient failures, and turns transport problems into typed exceptions: AiProviderException for HTTP 429, API errors and unreachable endpoints; InvalidAiResponseException for a blocked, empty or unparsable completion. The API key is redacted from every message.

Asking without naming a data source

When the caller has a single text box and no idea which table the question is about, across() routes the prompt first, then plans against whatever it picked:

Routing is a separate, deliberately cheap call: the planner sees a catalogue of names, labels, descriptions and a field sample, and returns one name. A name that is not in the catalogue is rejected, so routing cannot widen access. With one candidate the call is skipped entirely.

Give a candidate its own restrictions, and they apply once it is chosen:

If nothing fits, the router returns NO_MATCHING_ENTITY and you get an UnsupportedQueryException rather than an answer from the wrong table.

Tune the catalogue under catalog in the config: include_fields and max_fields_per_entity trade routing accuracy against prompt size.

Your first query

ask() and prompt() are the same method; pick whichever reads better. Nothing runs until you call get(), compile() or explain().

get() returns hydrated Eloquent models for a plain query, and plain row objects for a grouped or aggregated one — a GROUP BY result is not a model.

Schema inspection

The package inspects both halves of a model and merges them:

Source What it contributes
Database columns, storage types, nullability, primary key, indexes, foreign keys, enum members
Model casts, $fillable/$guarded, $hidden, table, connection, soft deletes, relations

A cast wins over the storage type, so a tinyint cast to boolean is filtered as a boolean, and a decimal cast column is filtered as a number. Absent a cast, MySQL's tinyint(1) is read as a boolean, since that is how BOOL is stored.

A column cast to a PHP backed enum contributes its members too. The planner is shown the case names, and the matching stored value is bound:

Without this the planner sees a bare enum with no members and correctly refuses to guess. Database-level enum(...) columns work the same way, with label and stored value being identical.

A table that reports no columns raises EntityNotInspectableException rather than producing an empty schema — usually a sign the model points at a database that is not the active connection.

Storage types are normalized to logical types the planner can reason about:

Allowed and denied fields

By default a query may touch any column that is neither in the model's $hidden array nor in security.denied_fields. Narrow it per query:

Restrictions apply twice: the narrowed schema is what the planner sees, and the validator re-checks the returned plan against the same list. A column you deny is invisible upstream and rejected downstream.

When the planner selects no columns at all, the compiler selects the visible columns explicitly rather than emitting select *, so a hidden column cannot leak through the default projection.

Relations

Discovery is conservative: only public, parameterless methods that declare a supported Eloquent relation return type are considered, and no other model method is ever invoked.

Supported: belongsTo, hasOne, hasMany, belongsToMany, hasOneThrough, hasManyThrough, morphOne, morphMany. MorphTo is excluded — its target table is not knowable in advance.

Restrict traversal per query, by exact path:

Or declare it on the model, which disables discovery for that model entirely:

Set relations.auto_discover to false to make relations opt-in globally.

Relation filters compile to whereHas / whereDoesntHave, never to a join the planner controls. Counts work too:

Depth is capped by limits.max_relation_depth (default 2).

Aggregates and HAVING

Supported functions are count, sum, avg, min, max, each checked against the field's logical type — SUM(name) is rejected. Aliases must be plain identifiers, must be unique, and may not collide with a real column.

HAVING is fully implemented, not stubbed. A having condition may only reference an alias the same query declares, and the compiler expands it back into the aggregate expression rather than referencing the alias, since alias references in HAVING are a MySQL extension rather than portable SQL.

An aggregated query may only select columns it also groups by; anything else is rejected, so the result is valid under strict SQL modes.

Nested filters

Filters are a tree, not a flat list:

compiles to

A bare array of conditions is accepted as an implicit and group. Depth and total condition count are both capped.

Operators are a closed set, mapped to the field types they may be used with:

Type Operators
string, uuid, enum = != contains starts_with ends_with in not_in is_null is_not_null
integer, number = != > >= < <= between in not_in is_null is_not_null
boolean = != is_null is_not_null
date, datetime = != > >= < <= between is_null is_not_null + relative dates
json is_null is_not_null

contains(total) fails validation. So does this_month(status).

contains, starts_with and ends_with compile to LIKE ... ESCAPE '!' with !, % and _ escaped in the value, so a user searching for 100% gets a literal match on every supported driver.

Date filters

today, yesterday, this_week, last_week, this_month, last_month, this_year, last_year.

The planner is told never to compute a date itself — it emits the operator, and the compiler resolves it through an injectable clock:

Configure the timezone and the first day of the week under dates. For deterministic tests, bind a frozen clock:

Explain and debug mode

explain() plans, validates and compiles, but never executes. $compiled->toRawSql() gives the statement with bindings inlined, for logs.

The security model

The planner never touches a connection. It receives a JSON description of the columns you allowed and returns JSON. Everything else is the package's own code.

What is enforced, and where:

Check Stage
Field names match ^[A-Za-z_][A-Za-z0-9_]{0,63}$ parsing
Operators and aggregate functions resolve to enum cases parsing
Filter values contain no nested structures or objects parsing
Structural nesting depth is bounded parsing
Every field exists in the narrowed metadata validation
Operator is legal for the field's logical type validation
Value type matches the column validation
Enum values are members of the column's enum validation
Aggregate is legal for the field's type validation
Aliases are unique and do not shadow columns validation
Sort targets a grouped column or a declared alias validation
Relation paths are allowed and within the depth budget validation
Filter count, group-by count, aggregate count, sort count validation
Row limit clamped to max_results normalization
Referenced fields and relations re-checked against constraints policy
Identifiers re-checked immediately before quoting compilation

What never happens:

Prompt injection is contained rather than trusted: the user's text is labelled as data in the message, the system instruction tells the planner to ignore instructions inside it, and — more importantly — even a fully compromised planner can only return field names and operators that survive validation.

Custom AI providers

Three ship in the box: gemini, openai (any chat-completions endpoint, including a self-hosted gateway) and ollama (local models, format: json).

Register your own in a service provider:

Swapping the provider changes nothing else — the engine downstream is provider-agnostic.

A planner can decline. Returning

raises UnsupportedQueryException carrying the typed AiError.

Custom query policy

Schema validation answers "is this query well-formed against the schema?". The policy answers "is this caller allowed to run it?".

QueryContext carries the model, the narrowed metadata, the constraints, the original prompt and the authenticated user. Pass the user with ->actingAs($user).

For row-level scoping, apply a closure to the compiled builder after validation:

Custom compiler

The compiler only ever receives definitions that already passed validation and policy.

Events

Useful for logging prompts, tracking token spend and spotting slow reports.

Schema cache

Introspection does not run on every prompt. Metadata is cached as plain arrays under a versioned key, so a package upgrade can never rehydrate a stale object graph.

Clear it after a migration — a deployment hook is the natural place.

Testing

Use the bundled fake planner rather than faking HTTP:

Queue several payloads to script a sequence, or push a Throwable to simulate a provider failure. For provider-level tests, Http::fake() works normally.

The package's own suite covers metadata normalization, type detection, operator/type compatibility, nested filter groups, aggregate validation, unknown fields and relations, denied fields, result limits, invalid sort fields, aggregate aliases, malformed JSON, HTTP failures, timeouts and 429s, relative dates, compiler output and bindings, injection-shaped values, SQL-shaped prompts, malicious planner output, relation depth and schema caching.

Limitations

Known and deliberate:

Notes on the design

A few deliberate departures from the obvious shape:

License

MIT. See LICENSE.


All versions of laravel-intentql with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
illuminate/contracts Version ^11.0|^12.0
illuminate/database Version ^11.0|^12.0
illuminate/http Version ^11.0|^12.0
illuminate/support Version ^11.0|^12.0
nesbot/carbon Version ^2.72|^3.0
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 prolaxu/laravel-intentql contains the following files

Loading the files please wait ...