Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lodata Laravel Package

flat3/lodata

Lodata is an OData v4 server for Laravel that exposes Eloquent models as standards-compliant APIs. It supports $filter, $select, $expand, $orderby, $top and $skip query options, metadata discovery, and easy model configuration for building rich, queryable endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config if you need to change the base path or schema namespace:

composer require flat3/lodata
php artisan vendor:publish --tag=lodata-config   # optional, see install docs for the exact tag

Register a single OData endpoint with the route macro the package provides:

// routes/api.php
Route::odata('odata', \App\Http\Controllers\ODataController::class);

Then create the controller and list the Eloquent models you want exposed:

namespace App\Http\Controllers;

use App\Models\Airport;
use App\Models\Flight;
use Flat3\Lodata\Controller\OData as ODataController;

class OData extends ODataController
{
    public $entitySets = [
        Flight::class,
        Airport::class,
    ];

    // public $namespace = 'App'; // controls the CSDL namespace
}

That is enough to get a read/write OData surface over your models. Verify in this order:

  1. GET /api/odata — service document listing entity sets.
  2. GET /api/odata/$metadata — the CSDL. This is the single most useful debugging endpoint; it tells you exactly what properties, types, keys and navigations the package inferred from your models.
  3. GET /api/odata/Flights?$top=5&$select=id,number,departure — first real query.

If you have many models, php artisan lodata:discover can scan app/Models and scaffold the $entitySets array, which is faster than hand-listing them.

First use case: a read-only reference feed for Power BI / Excel / a JS OData client. Models exposed read-only need no extra work — Laravel handles GET, and you can lock down writes with middleware (->middleware(['auth:sanctum'])) before you broaden exposure.

Implementation Patterns

Model = entity type. The package reflects the database schema for properties, $casts for EDM types, and belongsTo / hasMany / belongsToMany for navigation properties. You rarely define the EDM by hand; you shape it by shaping the model and migrations.

One controller per audience, not one giant controller. Mount separate endpoints — /odata/public and /odata/admin — with different $entitySets and different middleware. It is far easier to reason about exposure and versioning than a single controller with conditional logic. Treat the endpoint prefix as your API version.

Control the surface with $select conventions rather than hiding columns ad hoc. For list endpoints, clients should ask for $select=id,name; the package will only project those columns. For fixed integrations, document the required $select in the consumer.

Query options you will actually use: $filter, $select, $expand, $orderby, $top, $skip, $count, $search, $apply, $compute, $format. $filter is where most of the value is — clients can push predicate logic instead of you building bespoke query endpoints:

GET /api/odata/Flights?$filter=departure gt 2024-01-01T00:00:00Z and destination eq 'LHR'&$orderby=departure desc&$count=true

Relationships: navigation paths (/Flights(1)/Airport, /Airports(2)/Flights) come from the Eloquent relations. $expand=Airport fetches them inline. Use $expand selectively — see gotchas below.

Writes: POST / PATCH / PUT / DELETE map onto Eloquent create/save/delete, and @odata.bind lets you set foreign keys by navigation URL. Deep inserts (creating a parent and children in one POST) work via nested payloads. Gate these behind auth before you ship.

Row-level security lives in the model, not the controller. The package builds Eloquent queries, so global scopes apply. A Flight::addGlobalScope('tenant', fn ($q) => $q->where('tenant_id', auth()->id())) is usually cleaner than trying to inspect the OData path inside middleware. Route-level middleware handles who can reach the endpoint; model scopes handle what rows they see.

Extensions: computed properties, custom functions/actions, custom entity sets backed by non-Eloquent data, and custom primitive types are the supported escape hatches. Reach for a custom entity set when you need a read-only projection (e.g. an aggregated report) that doesn't map cleanly to a table.

Testing: plain HTTP tests work well.

$this->getJson('/api/odata/Flights?$filter=status eq \'active\'&$top=3')
    ->assertOk()
    ->assertJsonStructure(['@odata.context', 'value' => [['id', 'number']]]);

Assert on @odata.context and property names — those are your public contract, and they change when migrations change.

Gotchas and Tips

  • The schema is your API, and migrations change it silently. Adding a column changes $metadata and immediately exposes the field on every query. Make a habit of diffing $metadata after every migration, and treat any diff as a potentially breaking API change.
  • Don't assume $hidden / $visible protect you. The OData surface is derived from the DB schema and casts, not from Eloquent serialization. A password or internal flag column will be exposed unless you exclude it through the package's property configuration or expose a dedicated read model. Check $metadata, not the model file.
  • $metadata is generated per request. For large models it's a meaningful startup cost. If profiling shows it, cache the CSDL and clear caches (php artisan optimize:clear) after schema changes. Under Octane, don't stash the EDM in a static without resetting it per request.
  • $top is unbounded unless you bound it. Set a max top / default page size in config so $top=1000000 doesn't become a support ticket.
  • $count=true issues a second count query. Fine on small sets, expensive on wide filtered sets — measure before enabling it in a dashboard.
  • $expand can N+1. Log the SQL (DB::listen) on an expanded request. If the package isn't eager-loading the relation, add with() via a global scope on the hot paths.
  • Case sensitivity. OData path segments are case-sensitive: /Flights is not /flights. Keep entity set names stable and document them; they are part of your URL contract.
  • Route ordering. The OData route is effectively a catch-all under its prefix. Register it before any competing /{segment} route, or a generic route will shadow it and return 404s that look like package bugs.
  • CSRF. Mount on api middleware. If you mount on web, every POST/PATCH/DELETE from a non-browser client returns 419.
  • CORS for Excel / Power BI. Expose OData-Version, OData-EntityId and Location in your CORS config; some clients fail cryptically on updates if Location isn't readable.
  • $ in URLs. $filter values routinely contain $, ' and spaces. URL-encode from clients; be aware that some proxies/log pipelines mangle $ in query strings. Nginx access-log formats that interpolate $ are a common red herring.
  • DB portability. $filter string functions (contains, startswith, tolower) are translated to SQL. SQLite, MySQL and Postgres differ on LIKE case sensitivity and function availability — test on the production engine, not just the local one.
  • Type mapping surprises. decimal:2 casts come out as strings; floats lose money precision. Carbon casts become Edm.DateTimeOffset — make sure the timezone is what clients expect. Enum casts need checking against the EDM enum you intend.
  • Concurrency on writes. If you expose PATCH/DELETE, decide explicitly between ETag/If-Match optimistic concurrency and last-write-wins. The default is the latter.
  • Debugging order: a 400 is almost always an unknown property or a type mismatch. Hit $metadata first, then retry the query with $select restricted — it isolates filtering from projection problems.
  • Client-side metadata caching. Power BI and Excel cache the CSDL aggressively. After a schema change, restart the connector before assuming the server is wrong.
  • Extension points to remember: custom functions/actions for business operations, custom entity sets for non-table data, and computed properties for derived values — all preferable to bolting custom routes onto the side of the OData endpoint.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo