Search by

tally / laravel

MohammedAshraf

Serves the Tally Query Protocol from Eloquent and the Laravel query builder.

0.1.0 2026-10-17 15:04 UTC

This package is auto-updated.

Last update: 2026-10-17 18:41:47 UTC


README

Serves the Tally Query Protocol from Eloquent and the Laravel query builder. A grid class declares the columns, a route macro exposes it, and the adapter compiles filter, sort, group, aggregate, facet and page requests into SQL, so a Laravel app backs a <Tally> grid exactly like tally-server does in Node.

Not published yet.

Install

composer require tally/laravel

Requires PHP 8.4 and Laravel 13. The service provider is discovered automatically.

Grid

use App\Models\Account;
use Illuminate\Database\Eloquent\Builder;
use Tally\Laravel\Grid;

final class AccountsGrid extends Grid
{
    public function query(): Builder
    {
        return Account::query()->whereBelongsTo(auth()->user()->team);
    }

    public function columns(): array
    {
        return [
            ['id' => 'account', 'label' => 'Account'],
            ['id' => 'plan', 'type' => 'enum', 'options' => [['value' => 'free'], ['value' => 'pro'], ['value' => 'team']]],
            ['id' => 'mrr', 'label' => 'MRR', 'type' => 'currency', 'aggregate' => 'sum'],
            ['id' => 'renewal', 'type' => 'date'],
        ];
    }
}

Columns use the same shape as createSchema in tally-core and are sent to the client unchanged. Grids are resolved from the container, so the constructor can take dependencies. Every method below can be overridden:

Method Default Notes
query() An Eloquent or query builder. Its scopes and constraints apply to reads, updates and deletes.
columns() Column definitions.
key() id Key field. It need not be a column.
columnMap() [] Column id to SQL column name, for ['fullName' => 'full_name'].
flags() [] Named closures that receive a query builder and add the where clauses a flag filter means.
types() [] Extra ColumnType instances for custom column types.
dialect() from the driver SqliteDialect, PostgresDialect, MysqlDialect or your own Dialect.
pagination() Pagination::Offset Offset or Cursor. Cursors are opaque and encode the offset.
maxLimit() 1000 Page sizes above this are clamped. Requests without a page return at most this many rows.
mutable() true false refuses PATCH /rows.
today() today ISO date that relative ranges such as last 30 days resolve against.
bins() 20 Histogram bins.
maxTerms() 50 Terms facet size.
maxGroups() 1000 Group summaries are cut to the first this many groups, and pages past them are empty.

Every request is validated against the tally-protocol JSON Schema and then against the grid's columns before any SQL runs.

Semantics

The adapter passes the tally-protocol conformance suite on SQLite, Postgres and MySQL, so filters, sorts, groups, aggregates, facets and pages match the in-memory engine:

  • Empty means NULL, and for text columns also ''. Empty values sort last in both directions and never match comparisons, except neq, notContains and notIn, which keep them.
  • Text comparisons are case insensitive through lower(). SQLite folds ASCII only.
  • Enum columns sort and facet in option order.
  • Date groups bucket to YYYY-MM-DD, ISO Monday weeks, YYYY-MM, YYYY-Qn or YYYY.
  • Search matches the stored value of searchable columns, not the formatted text.
  • Group order follows the sort on the group column when there is one, otherwise option or natural order, newest dates first.
  • Empty values form one group, NULL and '' alike, which sorts last and pages like any other group.

Dialects

A Dialect supplies the few functions SQL engines disagree on: lower, text, float, floor, dayNumber, dateBucket, plus encode and decode for values crossing the driver. The dialect is picked from the connection driver: sqlite, pgsql, mysql and mariadb. Extend Dialect for another engine and return it from dialect().

Routes

use Illuminate\Support\Facades\Route;

Route::middleware('auth')->group(function () {
    Route::tally('/api/accounts', AccountsGrid::class);
});
Route Body Response
GET /schema { version, columns, capabilities, key }
POST /query QueryRequest QueryResponse
PATCH /rows { version, mutations } MutationResult, or 405 when the grid is read only
GET /events 404. Live patches are not supported yet.

The schema route reports live: false. Invalid requests return 400 with { code, message, details } where every detail names the path of a problem.

Mutations

Mutations run in one database transaction and return the canonical rows read back from the database. Deletes and updates are scoped by query(), so a grid never writes to rows it cannot read. Writes go through the query builder, so model events, casts and timestamps do not run. When the transaction fails, every mutation is rolled back, the exception is reported, and the response is { accepted: false, errors: [{ message: 'Mutation failed' }] }.

The adapter can be used without HTTP:

use Tally\Laravel\Adapter;
use Tally\Laravel\Query\QueryRequest;

$response = (new Adapter(new AccountsGrid))->query(QueryRequest::fromArray($input));

Conformance

The JSON Schema files in resources/schemas and the fixtures in tests/Fixtures/conformance.json are generated from tally-protocol by pnpm --filter tally-protocol build, so the PHP suite runs the same cases, with the same expected results, as the TypeScript adapters.

composer check
DB_CONNECTION=pgsql DB_DATABASE=tally DB_USERNAME=postgres vendor/bin/pest
DB_CONNECTION=mysql DB_DATABASE=tally DB_USERNAME=root vendor/bin/pest