PHPackages                             matatirosoln/doctrine-odata-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. matatirosoln/doctrine-odata-bundle

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

matatirosoln/doctrine-odata-bundle
==================================

Symfony Bundle for matatirosoln/doctrine-odata-driver — auto-configures the OData DBAL driver including metadata caching.

0.0.2(today)00MITPHPPHP ^8.4

Since Jun 9Pushed todayCompare

[ Source](https://github.com/matatirosolutions/doctrine-odata-bundle)[ Packagist](https://packagist.org/packages/matatirosoln/doctrine-odata-bundle)[ RSS](/packages/matatirosoln-doctrine-odata-bundle/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (11)Versions (3)Used By (0)

doctrine-odata-bundle
=====================

[](#doctrine-odata-bundle)

Symfony bundle for [`matatirosoln/doctrine-odata-driver`](https://github.com/matatirosolutions/doctrine-odata-driver).

Integrates the OData DBAL driver into a Symfony application with automatic metadata caching and a set of FileMaker-specific services for scripts, container fields, and value lists.

---

Requirements
------------

[](#requirements)

DependencyVersionPHP8.4+Symfony7.x or 8.x`matatirosoln/doctrine-odata-driver`^0.0.1`doctrine/doctrine-bundle`^2.12---

Installation
------------

[](#installation)

```
composer require matatirosoln/doctrine-odata-bundle
```

Symfony Flex will register the bundle automatically. Without Flex, add it to `config/bundles.php`:

```
Matatirosoln\DoctrineOdataBundle\DoctrineOdataBundle::class => ['all' => true],
```

---

Configuration
-------------

[](#configuration)

### 1. DBAL connection — `config/packages/doctrine.yaml`

[](#1-dbal-connection--configpackagesdoctrineyaml)

```
doctrine:
    dbal:
        driver_class: Matatirosoln\DoctrineOdataDriver\Driver\ODataDriver
        host: '%env(ODATA_HOST)%'
        dbname: '%env(ODATA_DATABASE)%'
        user: '%env(ODATA_USER)%'
        password: '%env(ODATA_PASSWORD)%'
        options:
            ssl: true
            quote_guids: true   # required for FileMaker — keeps GUID literals quoted in $filter
            metadata_ttl: 3600  # should match the TTL in doctrine_odata.yaml
```

Set the corresponding environment variables in `.env.local`:

```
ODATA_HOST=your-filemaker-server.example.com
ODATA_DATABASE=YourDatabaseName
ODATA_USER=your-odata-user
ODATA_PASSWORD=your-password
```

### 2. Bundle configuration — `config/packages/doctrine_odata.yaml`

[](#2-bundle-configuration--configpackagesdoctrine_odatayaml)

```
doctrine_odata:
    connection: default         # name of the Doctrine DBAL connection using ODataDriver
    metadata_cache:
        pool: cache.app         # any Symfony PSR-6 cache pool service ID
        ttl: 3600               # seconds; 0 = no expiry
```

All keys are optional — the defaults shown above apply if omitted:

```
doctrine_odata: ~
```

---

Metadata caching
----------------

[](#metadata-caching)

The OData `$metadata` endpoint describes the schema of the database and is required for every request. Without caching it would be fetched on every page load. The bundle wires a [DBAL middleware](https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/architecture.html#middlewares) that persists the parsed metadata to the configured Symfony cache pool so that it is only fetched from the server when the cache entry expires.

The middleware is a no-op for any non-OData DBAL connection, so it is safe to use in applications with multiple connections.

---

Services
--------

[](#services)

The bundle registers the following services, all of which are autowireable by their class name.

### ScriptService

[](#scriptservice)

Runs a FileMaker script via the OData `Script.{name}` endpoint.

```
use Matatirosoln\DoctrineOdataBundle\Service\ScriptService;

class MyController
{
    public function __construct(private readonly ScriptService $scripts) {}

    public function run(): void
    {
        // Plain string parameter
        $result = $this->scripts->run('SendWelcomeEmail', 'user@example.com');

        // Array parameter — automatically JSON-encoded
        $result = $this->scripts->run('ProcessOrder', [
            'orderId' => 'abc-123',
            'notify'  => true,
        ]);

        // No parameter
        $result = $this->scripts->run('NightlyCleanup');
    }
}
```

`run()` returns the parsed JSON response body as an array. It throws a `ScriptException` if FileMaker returns a non-zero script result code:

```
use Matatirosoln\DoctrineOdataBundle\Exception\ScriptException;

try {
    $this->scripts->run('ValidateRecord', $id);
} catch (ScriptException $e) {
    // $e->scriptName    — the script that was called
    // $e->scriptCode    — the non-zero result code returned by FileMaker
    // $e->resultParameter — the raw result parameter string from FileMaker
}
```

### ContainerService

[](#containerservice)

Uploads and downloads binary data to/from FileMaker container fields.

In OData responses, a container field's value is a URL pointing to the binary content. Pass that URL directly to `download()` or `downloadToStream()`.

```
use Matatirosoln\DoctrineOdataBundle\Service\ContainerService;
use Matatirosoln\SqlToOdata\Support\KeyValue;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/document')]
class DocumentController extends AbstractController
{
    public function __construct(private readonly ContainerService $containers) {}

    // Download the raw bytes of a container field and return them as a response
    #[Route('/{id}/download', name: 'document_download')]
    public function download(string $id, Document $document): Response
    {
        $content = $this->containers->download($document->attachment);

        return new Response($content, Response::HTTP_OK, [
            'Content-Type'        => 'application/pdf',
            'Content-Disposition' => 'attachment; filename="document.pdf"',
        ]);
    }

    // Stream a large file directly to the browser without loading it into memory
    #[Route('/{id}/stream', name: 'document_stream')]
    public function stream(string $id, Document $document): StreamedResponse
    {
        return new StreamedResponse(function () use ($document) {
            $this->containers->downloadToStream($document->attachment, fopen('php://output', 'wb'));
        }, Response::HTTP_OK, [
            'Content-Type'        => 'application/pdf',
            'Content-Disposition' => 'inline; filename="document.pdf"',
        ]);
    }

    // Upload binary content from a request to a container field
    #[Route('/{id}/upload', name: 'document_upload', methods: ['POST'])]
    public function upload(string $id, Request $request): Response
    {
        $content  = $request->getContent();
        $keyValue = new KeyValue($id, quoted: true);

        $this->containers->upload('Document', $keyValue, 'Attachment', $content, 'application/pdf');

        return new Response(null, Response::HTTP_NO_CONTENT);
    }

    // Upload directly from a file path (MIME type auto-detected if omitted)
    #[Route('/{id}/upload-file', name: 'document_upload_file', methods: ['POST'])]
    public function uploadFile(string $id, string $filePath): Response
    {
        $keyValue = new KeyValue($id, quoted: true);
        $this->containers->uploadFile('Document', $keyValue, 'Attachment', $filePath);

        return new Response(null, Response::HTTP_NO_CONTENT);
    }
}
```

### ValueListService

[](#valuelistservice)

Fetches FileMaker value lists and returns them in the `['Display Label' => 'stored_value']` format expected by Symfony's `ChoiceType`.

> **Requires FileMaker 22 or later.** The `FileMaker_ValueList_{name}` OData endpoints used by this service were introduced in FileMaker 22. Earlier versions expose value list names in `$metadata` but do not serve the actual entries via OData.

```
use Matatirosoln\DoctrineOdataBundle\Service\ValueListService;

class MyFormType extends AbstractType
{
    public function __construct(private readonly ValueListService $valueLists) {}

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('status', ChoiceType::class, [
            'choices' => $this->valueLists->get('Status'),
            // returns e.g. ['Active' => 'Active', 'Inactive' => 'Inactive']
        ]);

        $builder->add('assignee', ChoiceType::class, [
            'choices' => $this->valueLists->choices('Users'),
            // dynamic list: ['Alice Smith' => 'uuid-...', 'Bob Jones' => 'uuid-...']
        ]);
    }
}
```

#### Available methods

[](#available-methods)

MethodDescription`names(): list`All value list names from `$metadata` — no HTTP request`get(string $name): array`Entries for a single list`choices(string $name): array`Alias for `get()``all(): array`All lists keyed by name`clearCache(?string $name): void`Invalidate one list or all lists#### Caching

[](#caching)

Value lists are cached at two layers to minimise OData requests:

1. **Request cache** — an in-memory array on the service instance. Within a single request, a list is never fetched more than once regardless of how many times it is accessed.
2. **Session cache** — when a Symfony session is active, fetched lists are stored in the session and reused across page loads for the lifetime of that session. This avoids N × OData calls on every form reload without requiring a shared application cache that is difficult to invalidate.

The session cache degrades gracefully to request-only caching for API requests and CLI commands that have no session.

Calling `clearCache()` removes the named list (or all lists) from both layers simultaneously. The session cache is also automatically cleared when the user's session is destroyed (e.g. on logout).

### EntityGeneratorService

[](#entitygeneratorservice)

Generates Doctrine entity and repository PHP source files from the OData `$metadata` schema. Intended primarily for use via the CLI command below, but available as an injectable service for programmatic use.

```
use Matatirosoln\DoctrineOdataBundle\Service\EntityGeneratorService;

$plan = $this->generator->plan('Users', 'Users\User');

// Inspect the plan before writing
echo $plan->entityClass;   // App\Entity\Users\User
echo $plan->entityCode;    // rendered PHP source

// Write files to disk
$this->generator->writeFile($plan->entityFile, $plan->entityCode);
$this->generator->writeFile($plan->repositoryFile, $plan->repositoryCode);
```

---

Console commands
----------------

[](#console-commands)

### `doctrine:odata:entity:generate`

[](#doctrineodataentitygenerate)

Generates a Doctrine entity class and its repository from an OData entity set.

```
bin/console doctrine:odata:entity:generate
```

ArgumentDescription`entity-set`The OData entity set name (as it appears in `$metadata`)`class-name`PHP class name, optionally with sub-namespace using backslashes**Examples:**

```
# Generate App\Entity\User and App\Repository\UserRepository
bin/console doctrine:odata:entity:generate User User

# Generate App\Entity\Users\User in a sub-namespace
bin/console doctrine:odata:entity:generate Users "Users\User"
```

The command:

- Displays a summary of what will be generated and asks for confirmation before writing
- Prompts before overwriting any file that already exists
- Creates `src/Entity/` and `src/Repository/` subdirectories as needed
- Infers the base namespace (`App\` or similar) from the project's `composer.json` PSR-4 autoload mapping
- Maps all OData EDM types to the correct PHP types and `Doctrine\DBAL\Types\Types` constants
- Generates properties using PHP 8.4 property hooks with `trim()` applied to string setters
- Marks the primary key `public private(set)` with constructor injection
- Marks `Edm.Stream`/`Edm.Binary` (container) fields with a doc comment pointing to `ContainerService`

**Example output** for a `User` entity set with fields `__pk_UserID`, `Name`, `City`:

```
#[ORM\Table(name: 'User')]
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
    #[ORM\Id]
    #[ORM\Column(name: '__pk_UserID', type: Types::GUID)]
    public private(set) string $id {
        get => $this->id;
        set => $this->id = $value;
    }

    #[ORM\Column(name: 'Name', type: Types::STRING, length: 255)]
    public string $name {
        get => $this->name;
        set => $this->name = trim($value);
    }

    #[ORM\Column(name: 'City', type: Types::STRING, length: 255)]
    public string $city {
        get => $this->city;
        set => $this->city = trim($value);
    }

    public function __construct(string $id)
    {
        $this->id = $id;
    }
}
```

---

Exceptions
----------

[](#exceptions)

All bundle exceptions extend `RuntimeException` and carry structured context:

ExceptionThrown byExtra properties`ConnectionException`All services—`ScriptException``ScriptService``$scriptName`, `$scriptCode`, `$resultParameter``ConnectionException` is thrown when a service cannot resolve a DBAL connection backed by `ODataDriver`. This typically means the `connection` key in `doctrine_odata.yaml` does not match the name of the configured OData DBAL connection.

---

Licence
-------

[](#licence)

MIT

Contact
-------

[](#contact)

Steve Winter — Matatiro Solutions Ltd —

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/9415101?v=4)[Matatiro Solutions](/maintainers/matatirosolutions)[@matatirosolutions](https://github.com/matatirosolutions)

---

Top Contributors

[![steveWinter](https://avatars.githubusercontent.com/u/1171712?v=4)](https://github.com/steveWinter "steveWinter (2 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/matatirosoln-doctrine-odata-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/matatirosoln-doctrine-odata-bundle/health.svg)](https://phpackages.com/packages/matatirosoln-doctrine-odata-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M370](/packages/easycorp-easyadmin-bundle)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.4M506](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
