|
| 1 | + |
1 | 2 | # Service Endpoints
|
2 | 3 |
|
3 |
| -At this point we assume you already published the `lodata.php` config file to your project. |
| 4 | +> **Prerequisite**: You’ve already published the `lodata.php` config file into your Laravel project using `php artisan vendor:publish`. |
| 5 | +
|
| 6 | +## Overview |
| 7 | + |
| 8 | +By default, `flat3/lodata` exposes a **single global service endpoint**. However, for modular applications or domain-driven designs, you may want to expose **multiple, isolated OData service endpoints** — one per module, feature, or bounded context. |
| 9 | + |
| 10 | +This is where **service endpoints** come in. They allow you to split your schema into smaller, focused units, each with its own `$metadata` document and queryable surface. |
4 | 11 |
|
5 |
| -In case you want to distribute different service endpoints with your Laravel app, you can do so by providing one or more service endpoints to the package. This especially comes in handy when following a modularized setup. |
| 12 | +## Defining Multiple Endpoints |
6 | 13 |
|
7 |
| -Each of your modules could register its own service endpoint with an `\Flat3\Lodata\Endpoint` like this: |
| 14 | +You can define service endpoints by registering them in your `config/lodata.php` configuration file: |
8 | 15 |
|
9 | 16 | ```php
|
10 | 17 | /**
|
11 |
| - * At the end of `config/lodata.php` |
| 18 | + * At the end of config/lodata.php |
12 | 19 | */
|
13 | 20 | 'endpoints' => [
|
14 |
| - 'projects' ⇒ \App\Projects\ProjectEndpoint::class, |
| 21 | + 'projects' => \App\Projects\ProjectEndpoint::class, |
15 | 22 | ],
|
16 | 23 | ```
|
17 | 24 |
|
18 |
| -With that configuration a separate `$metadata` service file will be available via `https://<server>:<port>/<lodata.prefix>/projects/$metadata`. |
| 25 | +With this configuration, a separate `$metadata` document becomes available at: |
| 26 | + |
| 27 | +``` |
| 28 | +https://<server>:<port>/<lodata.prefix>/projects/$metadata |
| 29 | +``` |
19 | 30 |
|
20 |
| -If the `endpoints` array stays empty (the default), only one global service endpoint is created. |
| 31 | +If the `endpoints` array is left empty (the default), only a single global endpoint is created under the configured `lodata.prefix`. |
21 | 32 |
|
22 |
| -## Selective Discovery |
| 33 | +## Endpoint Discovery |
23 | 34 |
|
24 |
| -With endpoints, you can now discover all your entities and annotations in a separate class via the `discover` function. |
| 35 | +Each service endpoint class implements the `ServiceEndpointInterface`. This includes a `discover()` method where you define which entities, types, and annotations should be exposed by this endpoint. |
| 36 | + |
| 37 | +This gives you fine-grained control over what each endpoint exposes. |
25 | 38 |
|
26 | 39 | ```php
|
27 |
| -use App\Model\Contact; |
| 40 | +use App\Models\Contact; |
28 | 41 | use Flat3\Lodata\Model;
|
29 | 42 |
|
30 | 43 | /**
|
31 |
| - * Discovers Schema and Annotations of the `$metadata` file for |
32 |
| - * the service. |
| 44 | + * Discover schema elements and annotations for the service endpoint. |
33 | 45 | */
|
34 | 46 | public function discover(Model $model): Model
|
35 | 47 | {
|
36 |
| - // register all of your $metadata capabilities |
37 |
| - $model->discover(Contact::class); |
38 |
| - … |
| 48 | + // Register all exposed entity sets or types |
| 49 | + $model->discover(Contact::class); |
| 50 | + // Add more types or annotations here... |
| 51 | + |
39 | 52 | return $model;
|
40 | 53 | }
|
41 | 54 | ```
|
42 | 55 |
|
43 |
| -Furthermore, the `discover` function will only be executed when serving actual oData routes. This will enhance page speed for routes outside the `config('lodata.prefix')` URI space. |
| 56 | +### Performance Benefit |
| 57 | + |
| 58 | +The `discover()` method is only invoked **when an actual OData request targets the specific service endpoint**. It is **not** triggered for standard Laravel routes outside the OData URI space (such as `/web`, `/api`, or other unrelated routes). This behavior ensures that your application remains lightweight during boot and only loads schema definitions when they are explicitly required. |
| 59 | + |
| 60 | +> ✅ This optimization also applies to the **default (global) service endpoint** — its `discover()` method is likewise only evaluated on-demand during OData requests. |
| 61 | +
|
| 62 | +This design keeps your application performant, especially in modular or multi-endpoint setups, by avoiding unnecessary processing for unrelated HTTP traffic. |
| 63 | + |
| 64 | +## Serving Pre-Generated $metadata Files |
| 65 | + |
| 66 | +In addition to dynamic schema generation, you can optionally serve a **pre-generated `$metadata.xml` file**. This is especially useful when: |
| 67 | + |
| 68 | +- You want to include **custom annotations** that are not easily represented in PHP code. |
| 69 | +- You have **external tools** that generate the schema. |
| 70 | +- You prefer **fine-tuned control** over the metadata document. |
| 71 | + |
| 72 | +To enable this, implement the `cachedMetadataXMLPath()` method in your endpoint class: |
| 73 | + |
| 74 | +```php |
| 75 | +public function cachedMetadataXMLPath(): ?string |
| 76 | +{ |
| 77 | + return base_path('odata/metadata-projects.xml'); |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +If this method returns a valid file path, `lodata` will serve this file directly when `$metadata` is requested, bypassing the `discover()` logic. |
| 82 | + |
| 83 | +If it returns `null` (default), the schema will be generated dynamically from the `discover()` method. |
| 84 | + |
| 85 | +## Summary |
| 86 | + |
| 87 | +| Feature | Dynamic (`discover`) | Static (`cachedMetadataXMLPath`) | |
| 88 | +|--------------------------|----------------------|-----------------------------------| |
| 89 | +| Schema definition | In PHP | In XML file | |
| 90 | +| Supports annotations | Basic | Full (manual control) | |
| 91 | +| Performance optimized | Yes | Yes | |
| 92 | +| Best for | Laravel-native setup | SAP integration, fine-tuned CSDL | |
| 93 | + |
| 94 | +Great! Here's an additional **section for your documentation** that walks readers through the complete sample endpoint implementation, ties it back to the configuration, and shows how it integrates into the actual request flow. |
| 95 | + |
| 96 | +## Sample: Defining a `ProjectEndpoint` |
| 97 | + |
| 98 | +Let’s walk through a concrete example of how to define and use a modular service endpoint in your Laravel app — focused on the **Project** domain. |
| 99 | + |
| 100 | +### Step 1: Define the Custom Endpoint Class |
| 101 | + |
| 102 | +To create a service that reflects the specific logic, scope, and metadata of your Project domain, you extend the `Flat3\Lodata\Endpoint` base class. You’re not required to implement any abstract methods. Instead, you override the ones that make this service distinct. |
| 103 | + |
| 104 | +Here’s a minimal yet complete example: |
| 105 | + |
| 106 | +```php |
| 107 | +<?php |
| 108 | + |
| 109 | +namespace App\Endpoints; |
| 110 | + |
| 111 | +use App\Models\Project; |
| 112 | +use Flat3\Lodata\Endpoint; |
| 113 | +use Flat3\Lodata\Model; |
| 114 | + |
| 115 | +/** |
| 116 | + * OData service endpoint for project-related data. |
| 117 | + * |
| 118 | + * Exposes a modular OData service at /projects with its own metadata namespace. |
| 119 | + */ |
| 120 | +class ProjectEndpoint extends Endpoint |
| 121 | +{ |
| 122 | + /** |
| 123 | + * Define the namespace used in the <Schema> element of $metadata. |
| 124 | + */ |
| 125 | + public function namespace(): string |
| 126 | + { |
| 127 | + return 'ProjectService'; |
| 128 | + } |
| 129 | + |
| 130 | + /** |
| 131 | + * Optionally return a static metadata XML file. |
| 132 | + * If null, dynamic discovery via discover() is used. |
| 133 | + */ |
| 134 | + public function cachedMetadataXMLPath(): ?string |
| 135 | + { |
| 136 | + return resource_path('meta/ProjectService.xml'); |
| 137 | + } |
| 138 | + |
| 139 | + /** |
| 140 | + * Register entities and types to expose through this endpoint. |
| 141 | + */ |
| 142 | + public function discover(Model $model): Model |
| 143 | + { |
| 144 | + $model->discover(Project::class); |
| 145 | + |
| 146 | + return $model; |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +> ✅ **You only override what’s relevant to your endpoint.** This makes it easy to tailor each endpoint to a specific bounded context without unnecessary boilerplate. |
| 152 | +
|
| 153 | +### Step 2: Register the Endpoint and Define Its URI Prefix |
| 154 | + |
| 155 | +In your `config/lodata.php`, register the custom endpoint under the `endpoints` array: |
| 156 | + |
| 157 | +```php |
| 158 | +'endpoints' => [ |
| 159 | + 'projects' => \App\Endpoints\ProjectEndpoint::class, |
| 160 | +], |
| 161 | +``` |
| 162 | + |
| 163 | +> 🧩 The **key** (`projects`) is not just a label — it becomes the **URI prefix** for this endpoint. In this case, all OData requests to `/odata/projects` will be routed to your `ProjectEndpoint`. |
| 164 | +
|
| 165 | +This results in: |
| 166 | + |
| 167 | +- `$metadata` available at: |
| 168 | + `https://<server>:<port>/<lodata.prefix>/projects/$metadata` |
| 169 | + |
| 170 | +- Entity sets exposed through: |
| 171 | + `https://<server>:<port>/<lodata.prefix>/projects/Projects` |
| 172 | + |
| 173 | +This convention gives you **clear, readable URLs** and enables **modular, multi-service APIs** without extra routing configuration. |
| 174 | + |
| 175 | +### Step 3: Serve Dynamic or Static Metadata |
| 176 | + |
| 177 | +The framework will: |
| 178 | + |
| 179 | +- Call `cachedMetadataXMLPath()` first. |
| 180 | + If a file path is returned and the file exists, it will serve that file directly. |
| 181 | +- Otherwise, it will fall back to the `discover()` method to dynamically register entities, types, and annotations. |
| 182 | + |
| 183 | +This hybrid approach gives you **maximum flexibility** — allowing you to combine automated model discovery with the full expressive power of hand-authored metadata if needed. |
| 184 | + |
| 185 | +## ✅ What You Get |
| 186 | + |
| 187 | +With just a few lines of configuration, you now have: |
| 188 | + |
| 189 | +- A **cleanly separated OData service** for the `Project` module. |
| 190 | +- **Independent metadata** for documentation and integration. |
| 191 | +- A fast and **on-demand schema bootstrapping** process. |
| 192 | +- Full **control over discoverability** and **extensibility**. |
| 193 | + |
| 194 | +You can now repeat this pattern for other domains (e.g., `contacts`, `finance`, `hr`) to keep your OData services modular, testable, and scalable. |
| 195 | + |
| 196 | +Perfect! Let’s build on this momentum and add a **visual + narrative section** that ties the whole flow together — showing how all the moving parts interact: |
| 197 | + |
| 198 | +## How Everything Connects |
| 199 | + |
| 200 | +When you define a custom OData service endpoint, you’re essentially configuring a **self-contained API module** with its own URI, schema, metadata, and behavior. Let’s zoom out and see how the elements work together. |
| 201 | + |
| 202 | +### Flow Overview |
| 203 | + |
| 204 | +``` |
| 205 | +[ config/lodata.php ] → [ ProjectEndpoint class ] |
| 206 | + │ │ |
| 207 | + ▼ ▼ |
| 208 | + 'projects' => ProjectEndpoint::class ──► defines: |
| 209 | + - namespace() |
| 210 | + - discover() |
| 211 | + - cachedMetadataXMLPath() |
| 212 | +
|
| 213 | + │ │ |
| 214 | + ▼ ▼ |
| 215 | + URI: /odata/projects/$metadata OData Schema (XML or dynamic) |
| 216 | +``` |
| 217 | + |
| 218 | +### The Building Blocks |
| 219 | + |
| 220 | +| Component | Purpose | |
| 221 | +|-----------------------------------|-------------------------------------------------------------------------| |
| 222 | +| **`config/lodata.php`** | Registers all endpoints and defines the URI prefix for each one | |
| 223 | +| **Key: `'projects'`** | Becomes part of the URL: `/odata/projects/` | |
| 224 | +| **`ProjectEndpoint` class** | Defines what the endpoint serves and how | |
| 225 | +| **`namespace()`** | Injects the `<Schema Namespace="ProjectService" />` into `$metadata` | |
| 226 | +| **`discover(Model $model)`** | Dynamically registers entities like `Project::class` | |
| 227 | +| **`cachedMetadataXMLPath()`** | Optionally returns a pre-generated CSDL XML file | |
| 228 | +| **OData request** | Triggers loading of this endpoint’s metadata and data | |
| 229 | + |
| 230 | +## Example: Request Lifecycle |
| 231 | + |
| 232 | +Let’s break down how the enhanced flow would look for an actual **entity set access**, such as |
| 233 | + |
| 234 | +``` |
| 235 | +GET /odata/projects/Costcenters |
| 236 | +``` |
| 237 | + |
| 238 | +This is about a **data request** for a specific entity set. Here's how the full lifecycle plays out. From config to response. |
| 239 | + |
| 240 | +### Enhanced Flow for `/odata/projects/Costcenters` |
| 241 | + |
| 242 | +``` |
| 243 | + ┌─────────────────────────────────────────────────────┐ |
| 244 | + │ HTTP GET /odata/projects/Costcenters │ |
| 245 | + └─────────────────────────────────────────────────────┘ |
| 246 | + │ |
| 247 | + ▼ |
| 248 | +┌────────────────────────────────────────┐ [Routing Layer] matches |
| 249 | +│ config/lodata.php │── 'projects' key |
| 250 | +│ │ |
| 251 | +│ 'projects' => ProjectEndpoint::class, │ |
| 252 | +└────────────────────────────────────────┘ |
| 253 | + │ |
| 254 | + ▼ |
| 255 | + ┌──────────────────────────────────────┐ |
| 256 | + │ New ProjectEndpoint instance │ |
| 257 | + └──────────────────────────────────────┘ |
| 258 | + │ |
| 259 | + (cachedMetadataXMLPath() not used here) |
| 260 | + │ |
| 261 | + ▼ |
| 262 | + ┌───────────────────────────────────────────────┐ |
| 263 | + │ discover(Model $model) is invoked │ |
| 264 | + │ → model->discover(Project::class) │ |
| 265 | + │ → model->discover(Costcenter::class) │ |
| 266 | + └───────────────────────────────────────────────┘ |
| 267 | + │ |
| 268 | + ▼ |
| 269 | + ┌──────────────────────────────────────┐ |
| 270 | + │ Lodata resolves the URI segment: │ |
| 271 | + │ `Costcenters` │ |
| 272 | + └──────────────────────────────────────┘ |
| 273 | + │ |
| 274 | + (via the registered EntitySet name for Costcenter) |
| 275 | + │ |
| 276 | + ▼ |
| 277 | + ┌───────────────────────────────────────────────┐ |
| 278 | + │ Query engine builds and executes the query │ |
| 279 | + │ using the underlying Eloquent model │ |
| 280 | + └───────────────────────────────────────────────┘ |
| 281 | + │ |
| 282 | + ▼ |
| 283 | + ┌────────────────────────────────────────────┐ |
| 284 | + │ Response is serialized into JSON or XML │ |
| 285 | + │ according to Accept header │ |
| 286 | + └────────────────────────────────────────────┘ |
| 287 | + │ |
| 288 | + ▼ |
| 289 | + 🔁 JSON (default) or Atom/XML payload with Costcenter entities |
| 290 | +
|
| 291 | +``` |
| 292 | + |
| 293 | +### What Must Be in Place for This to Work |
| 294 | + |
| 295 | +| Requirement | Description | |
| 296 | +|-----------------------------------------------|-----------------------------------------------------------------------------| |
| 297 | +| `ProjectEndpoint::discover()` | Must register `Costcenter::class` via `$model->discover(...)` | |
| 298 | +| `Costcenter` model | Can be a **standard Laravel Eloquent model** – no special base class needed | |
| 299 | +| `EntitySet` name | Must match the URI segment: `Costcenters` | |
| 300 | +| URI case sensitivity | Lodata uses the identifier names → ensure entity names match URI segments | |
| 301 | +| Accept header | Optional – defaults to JSON if none is provided | |
| 302 | + |
| 303 | +Absolutely! Here's a fully integrated and refined section that combines both the **"What This Enables"** and **"Summary"** parts into one cohesive, value-driven conclusion: |
| 304 | + |
| 305 | +## What Modular Service Endpoints Enable |
| 306 | + |
| 307 | +Modular service endpoints give you precise control over how your OData APIs are structured, documented, and consumed. With just a small configuration change and a focused endpoint class, you unlock a powerful set of capabilities: |
| 308 | + |
| 309 | +- **Modular APIs** — Define multiple endpoints, each exposing only the entities and operations relevant to a specific domain (e.g., `projects`, `contacts`, `finance`). |
| 310 | +- **Clean, discoverable URLs** — Support intuitive REST-style routes like `/odata/projects/Costcenters?$filter=active eq true`, with automatic support for `$filter`, `$expand`, `$orderby`, and paging. |
| 311 | +- **Endpoint-specific metadata** — Each service exposes its own `$metadata`, either dynamically generated or served from a pre-generated XML file — perfect for integration with clients that require full annotation control. |
| 312 | +- **Schema isolation** — Maintain clean separation between domains, clients, or API versions. For example: |
| 313 | + - `/odata/projects/$metadata` → `ProjectService` schema |
| 314 | + - `/odata/finance/$metadata` → `FinanceService` schema |
| 315 | +- **Mix and match discovery strategies** — Use dynamic schema generation via Eloquent models or inject precise, curated metadata with static CSDL files. |
| 316 | +- **Scalable architecture** — Modular endpoints help you grow from a single-purpose API to a rich multi-domain platform — all while keeping concerns separated and maintainable. |
| 317 | + |
| 318 | +### ✅ In Short |
| 319 | + |
| 320 | +Modular service endpoints allow you to: |
| 321 | + |
| 322 | +- Keep your domains cleanly separated |
| 323 | +- Scale your API by feature, client, or team |
| 324 | +- Provide tailored metadata per endpoint |
| 325 | +- Mix dynamic discovery with pre-defined XML schemas |
| 326 | +- Integrate smoothly into your Laravel app — no magic, just configuration and conventions |
| 327 | + |
| 328 | +They’re not just a convenience, they’re a foundation for **clean, scalable, and maintainable OData APIs**. |
0 commit comments