Skip to content

Commit 4ae90e5

Browse files
committed
Update documentation
1 parent 61829aa commit 4ae90e5

File tree

1 file changed

+301
-16
lines changed

1 file changed

+301
-16
lines changed

doc/getting-started/endpoint.md

Lines changed: 301 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,328 @@
1+
12
# Service Endpoints
23

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.
411

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
613

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:
815

916
```php
1017
/**
11-
* At the end of `config/lodata.php`
18+
* At the end of config/lodata.php
1219
*/
1320
'endpoints' => [
14-
'projects' \App\Projects\ProjectEndpoint::class,
21+
'projects' => \App\Projects\ProjectEndpoint::class,
1522
],
1623
```
1724

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+
```
1930

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`.
2132

22-
## Selective Discovery
33+
## Endpoint Discovery
2334

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.
2538

2639
```php
27-
use App\Model\Contact;
40+
use App\Models\Contact;
2841
use Flat3\Lodata\Model;
2942

3043
/**
31-
* Discovers Schema and Annotations of the `$metadata` file for
32-
* the service.
44+
* Discover schema elements and annotations for the service endpoint.
3345
*/
3446
public function discover(Model $model): Model
3547
{
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+
3952
return $model;
4053
}
4154
```
4255

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

Comments
 (0)