From ebb965d5d52ff4ac06b5933a2e719d978c4255a9 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Mon, 4 Nov 2024 10:43:03 +0100 Subject: [PATCH 01/34] Added Endpoint Discovery --- config.php | 5 ++ doc/.vuepress/config.js | 1 + doc/getting-started/endpoint.md | 43 +++++++++++++++ doc/package.json | 6 +-- src/Controller/ODCFF.php | 6 ++- src/Controller/PBIDS.php | 6 ++- src/Controller/Transaction.php | 11 ++-- src/Endpoint.php | 57 ++++++++++++++++++++ src/Entity.php | 9 +++- src/Model.php | 10 ++-- src/PathSegment/Batch/JSON.php | 12 +++-- src/PathSegment/OpenAPI.php | 4 +- src/ServiceProvider.php | 76 +++++++++++++++++++-------- src/Transaction/MultipartDocument.php | 9 ++-- 14 files changed, 208 insertions(+), 47 deletions(-) create mode 100644 doc/getting-started/endpoint.md create mode 100644 src/Endpoint.php diff --git a/config.php b/config.php index 1882a98d5..f9a088785 100644 --- a/config.php +++ b/config.php @@ -103,4 +103,9 @@ * Configuration for OpenAPI schema generation */ 'openapi' => [], + + /** + * Configuration for multiple service endpoints + */ + 'endpoints' => [], ]; diff --git a/doc/.vuepress/config.js b/doc/.vuepress/config.js index 023ac1cad..befc2ffdd 100644 --- a/doc/.vuepress/config.js +++ b/doc/.vuepress/config.js @@ -134,6 +134,7 @@ module.exports = { children: [ 'getting-started/', 'getting-started/configuration', + 'getting-started/endpoint', 'getting-started/facade', 'getting-started/routing', 'getting-started/authentication', diff --git a/doc/getting-started/endpoint.md b/doc/getting-started/endpoint.md new file mode 100644 index 000000000..64ba2aa76 --- /dev/null +++ b/doc/getting-started/endpoint.md @@ -0,0 +1,43 @@ +# Service Endpoints + +At this point we assume you already published the `lodata.php` config file to your project. + +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. + +Each of your modules could register its own service endpoint with an `\Flat3\Lodata\Endpoint` like this: + +```php +/** + * At the end of `config/lodata.php` + */ +'endpoints' => [ + 'projects' ⇒ \App\Projects\ProjectEndpoint::class, +], +``` + +With that configuration a separate `$metadata` service file will be available via `https://://projects/$metadata`. + +If the `endpoints` array stays empty (the default), only one global service endpoint is created. + +## Selective Discovery + +With endpoints, you can now discover all your entities and annotations in a separate class via the `discover` function. + +```php +use App\Model\Contact; +use Flat3\Lodata\Model; + +/** + * Discovers Schema and Annotations of the `$metadata` file for + * the service. + */ +public function discover(Model $model): Model +{ + // register all of your $metadata capabilities + $model->discover(Contact::class); + … + return $model; +} +``` + +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. diff --git a/doc/package.json b/doc/package.json index 9605bbc15..3e96e0185 100644 --- a/doc/package.json +++ b/doc/package.json @@ -5,9 +5,9 @@ }, "devDependencies": { "@kawarimidoll/vuepress-plugin-tailwind": "^2.0.0", - "@vuepress/plugin-back-to-top": "^1.9.5", - "@vuepress/plugin-nprogress": "^1.9.5", - "vuepress": "^1.9.5", + "@vuepress/plugin-back-to-top": "^1.9.10", + "@vuepress/plugin-nprogress": "^1.9.10", + "vuepress": "^1.9.10", "vuepress-plugin-container": "^2.1.5", "vuepress-plugin-sitemap": "^2.3.1" } diff --git a/src/Controller/ODCFF.php b/src/Controller/ODCFF.php index 5c1b0f3e3..6ae19860a 100644 --- a/src/Controller/ODCFF.php +++ b/src/Controller/ODCFF.php @@ -8,7 +8,8 @@ use Flat3\Lodata\Exception\Protocol\NotFoundException; use Flat3\Lodata\Facades\Lodata; use Flat3\Lodata\Helper\Constants; -use Flat3\Lodata\ServiceProvider; +use Flat3\Lodata\Endpoint; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Http\Response; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\App; @@ -27,6 +28,7 @@ class ODCFF extends Controller * Generate an ODCFF response for the provided entity set identifier * @param string $identifier Identifier * @return Response Client response + * @throws BindingResolutionException */ public function get(string $identifier): Response { @@ -165,7 +167,7 @@ public function get(string $identifier): Response $formula = $mashupDoc->createElement('Formula'); $formulaContent = $mashupDoc->createCDATASection(sprintf( 'let Source = OData.Feed("%1$s", null, [Implementation="2.0"]), %2$s_table = Source{[Name="%2$s",Signature="table"]}[Data] in %2$s_table', - ServiceProvider::endpoint(), + app()->make(Endpoint::class)->endpoint(), $resourceId, )); $formula->appendChild($formulaContent); diff --git a/src/Controller/PBIDS.php b/src/Controller/PBIDS.php index b9273b744..69e46ce30 100644 --- a/src/Controller/PBIDS.php +++ b/src/Controller/PBIDS.php @@ -6,8 +6,9 @@ use Flat3\Lodata\Helper\Constants; use Flat3\Lodata\Helper\JSON; -use Flat3\Lodata\ServiceProvider; +use Flat3\Lodata\Endpoint; use Flat3\Lodata\Transaction\MediaType; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Http\Response; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\App; @@ -23,6 +24,7 @@ class PBIDS extends Controller /** * Generate a PowerBI data source discovery file * @return Response Client response + * @throws BindingResolutionException */ public function get(): Response { @@ -42,7 +44,7 @@ public function get(): Response 'details' => [ 'protocol' => 'odata', 'address' => [ - 'url' => ServiceProvider::endpoint(), + 'url' => app()->make(Endpoint::class)->endpoint(), ], ], ], diff --git a/src/Controller/Transaction.php b/src/Controller/Transaction.php index 2a46b093c..40faf4dce 100644 --- a/src/Controller/Transaction.php +++ b/src/Controller/Transaction.php @@ -30,9 +30,9 @@ use Flat3\Lodata\Interfaces\ResponseInterface; use Flat3\Lodata\Interfaces\TransactionInterface; use Flat3\Lodata\NavigationProperty; +use Flat3\Lodata\Endpoint; use Flat3\Lodata\Operation; use Flat3\Lodata\PathSegment; -use Flat3\Lodata\ServiceProvider; use Flat3\Lodata\Singleton; use Flat3\Lodata\Transaction\IEEE754Compatible; use Flat3\Lodata\Transaction\MediaType; @@ -58,6 +58,7 @@ use Flat3\Lodata\Transaction\ParameterList; use Flat3\Lodata\Transaction\Version; use Flat3\Lodata\Type\Collection; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Arr; use Illuminate\Support\Facades\App; use Illuminate\Support\Str; @@ -784,7 +785,7 @@ public function getPath(): string */ public function getRequestPath(): string { - $route = ServiceProvider::route(); + $route = app()->make(Endpoint::class)->route(); return Str::substr($this->request->path(), strlen($route)); } @@ -947,20 +948,22 @@ private function getSystemQueryOptions(bool $prefixed = true): array * Get the service document context URL * https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_ServiceDocument * @return string Context URL + * @throws BindingResolutionException */ public function getContextUrl(): string { - return ServiceProvider::endpoint().'$metadata'; + return self::getResourceUrl().'$metadata'; } /** * Get the service document resource URL * https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_ServiceDocument * @return string Resource URL + * @throws BindingResolutionException */ public static function getResourceUrl(): string { - return ServiceProvider::endpoint(); + return app()->make(Endpoint::class)->endpoint(); } /** diff --git a/src/Endpoint.php b/src/Endpoint.php new file mode 100644 index 000000000..b8fb6a0aa --- /dev/null +++ b/src/Endpoint.php @@ -0,0 +1,57 @@ + of the Flat3\Lodata\Model. + */ + protected $serviceUri; + + /** + * @var string $route the route prefix configured in 'lodata.prefix' + */ + protected $route; + + /** + * @var string $endpoint the full url to the ODataService endpoint + */ + protected $endpoint; + + public function __construct(string $serviceUri) + { + $this->serviceUri = rtrim($serviceUri, '/'); + + $prefix = rtrim(config('lodata.prefix'), '/'); + $this->route = ('' === $serviceUri) + ? $prefix + : $prefix . '/' . $this->serviceUri; + + $this->endpoint = url($this->route) . '/'; + } + + /** + * @return string the path within the odata URI space, like in + * https://:///$metadata + */ + public function endpoint(): string + { + return $this->endpoint; + } + + public function route(): string + { + return $this->route; + } + + /** + * Discovers Schema and Annotations of the `$metadata` file for + * the service. + */ + public function discover(Model $model): Model + { + // override this function to register all of your $metadata capabilities + return $model; + } +} \ No newline at end of file diff --git a/src/Entity.php b/src/Entity.php index 07c2392b0..efb76f975 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -22,6 +22,7 @@ use Flat3\Lodata\Interfaces\EntitySet\UpdateInterface; use Flat3\Lodata\Interfaces\PipeInterface; use Flat3\Lodata\Transaction\MetadataContainer; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -153,6 +154,9 @@ public function offsetSet($offset, $value): void } } + /** + * @throws BindingResolutionException + */ public static function pipe( Transaction $transaction, string $currentSegment, @@ -174,8 +178,9 @@ public static function pipe( } $entityId = $id->getValue(); - if (Str::startsWith($entityId, ServiceProvider::endpoint())) { - $entityId = Str::substr($entityId, strlen(ServiceProvider::endpoint())); + $endpoint = app()->make(Endpoint::class)->endpoint(); + if (Str::startsWith($entityId, $endpoint)) { + $entityId = Str::substr($entityId, strlen($endpoint)); } return EntitySet::pipe($transaction, $entityId); diff --git a/src/Model.php b/src/Model.php index 172dc31f4..c58be6661 100644 --- a/src/Model.php +++ b/src/Model.php @@ -17,6 +17,7 @@ use Flat3\Lodata\Interfaces\ResourceInterface; use Flat3\Lodata\Interfaces\ServiceInterface; use Flat3\Lodata\Traits\HasAnnotations; +use Illuminate\Contracts\Container\BindingResolutionException; /** * Model @@ -339,10 +340,11 @@ public function discover($discoverable): self /** * Get the REST endpoint of this OData model * @return string REST endpoint + * @throws BindingResolutionException */ public function getEndpoint(): string { - return ServiceProvider::endpoint(); + return app()->make(Endpoint::class)->endpoint(); } /** @@ -351,7 +353,7 @@ public function getEndpoint(): string */ public function getPbidsUrl(): string { - return ServiceProvider::endpoint().'_lodata/odata.pbids'; + return $this->getEndpoint().'_lodata/odata.pbids'; } /** @@ -361,7 +363,7 @@ public function getPbidsUrl(): string */ public function getOdcUrl(string $set): string { - return sprintf('%s_lodata/%s.odc', ServiceProvider::endpoint(), $set); + return sprintf('%s_lodata/%s.odc', $this->getEndpoint(), $set); } /** @@ -370,6 +372,6 @@ public function getOdcUrl(string $set): string */ public function getOpenApiUrl(): string { - return ServiceProvider::endpoint().'openapi.json'; + return $this->getEndpoint().'openapi.json'; } } diff --git a/src/PathSegment/Batch/JSON.php b/src/PathSegment/Batch/JSON.php index 2cd53ed6c..048b90238 100644 --- a/src/PathSegment/Batch/JSON.php +++ b/src/PathSegment/Batch/JSON.php @@ -16,9 +16,10 @@ use Flat3\Lodata\Interfaces\JsonInterface; use Flat3\Lodata\Interfaces\ResourceInterface; use Flat3\Lodata\Interfaces\ResponseInterface; +use Flat3\Lodata\Endpoint; use Flat3\Lodata\PathSegment\Batch; -use Flat3\Lodata\ServiceProvider; use Flat3\Lodata\Transaction\MediaType; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -34,6 +35,10 @@ class JSON extends Batch implements JsonInterface, ResponseInterface */ protected $requests = []; + /** + * @throws BindingResolutionException + * @throws \JsonException + */ public function emitJson(Transaction $transaction): void { $transaction->outputJsonObjectStart(); @@ -54,10 +59,11 @@ public function emitJson(Transaction $transaction): void $requestURI = $requestData['url']; + $endpoint = app()->make(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( - ServiceProvider::endpoint(), + $endpoint, $requestURI, Url::HTTP_URL_REPLACE ); @@ -69,7 +75,7 @@ public function emitJson(Transaction $transaction): void default: $uri = Url::http_build_url( - ServiceProvider::endpoint(), + $endpoint, $requestURI, Url::HTTP_URL_JOIN_PATH | Url::HTTP_URL_JOIN_QUERY ); diff --git a/src/PathSegment/OpenAPI.php b/src/PathSegment/OpenAPI.php index 4ddc8c726..5df9f3215 100644 --- a/src/PathSegment/OpenAPI.php +++ b/src/PathSegment/OpenAPI.php @@ -34,10 +34,10 @@ use Flat3\Lodata\Interfaces\ResponseInterface; use Flat3\Lodata\Model; use Flat3\Lodata\NavigationProperty; +use Flat3\Lodata\Endpoint; use Flat3\Lodata\Operation; use Flat3\Lodata\PrimitiveType; use Flat3\Lodata\Property; -use Flat3\Lodata\ServiceProvider; use Flat3\Lodata\Singleton; use Flat3\Lodata\Transaction\MediaType; use Flat3\Lodata\Transaction\Option\Count; @@ -409,7 +409,7 @@ public function emitJson(Transaction $transaction): void $queryObject->tags = [__('lodata::Batch requests')]; - $route = ServiceProvider::route(); + $route = app()->make(Endpoint::class)->route(); $requestBody = [ 'required' => true, diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 958c05255..511b8b071 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -21,29 +21,14 @@ /** * Service Provider + * + * https://:///$metadata + * * @link https://laravel.com/docs/8.x/providers * @package Flat3\Lodata */ class ServiceProvider extends \Illuminate\Support\ServiceProvider { - /** - * Get the endpoint of the OData service document - * @return string - */ - public static function endpoint(): string - { - return url(self::route()).'/'; - } - - /** - * Get the configured route prefix - * @return string - */ - public static function route(): string - { - return rtrim(config('lodata.prefix'), '/'); - } - /** * Service provider registration method */ @@ -59,14 +44,61 @@ public function boot() { if ($this->app->runningInConsole()) { $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); + $this->bootServices(new Endpoint('')); } + else { + // Let’s examine the request path + $segments = explode('/', request()->path()); + + // we only kick off operation when path prefix is configured in lodata.php + // as all requests share the same root configuration + if ($segments[0] === config('lodata.prefix')) { + + // next look up the configured service endpoints + $serviceUris = config('lodata.endpoints', []); + + $service = null; + if (0 === sizeof($serviceUris)) { + // when no locators are defined, fallback to global mode; this will + // ensure compatibility with prior versions of this package + $service = new Endpoint(''); + } + else if (array_key_exists($segments[1], $serviceUris)) { + $clazz = $serviceUris[$segments[1]]; + $service = new $clazz($segments[1]); + } + else { + // when no service definition is configured for the path segment, + // we abort with an error condition; typically a dev working on + // setting up his project + abort('No odata service endpoint defined for path ' . $segments[1]); + } + + $this->bootServices($service); + } + } + } + + private function bootServices($service): void + { + // register the $service, which is a singleton, with the container; this allows us + // to fulfill all old ServiceProvider::route() and ServiceProvider::endpoint() + // calls with app()->make(ODataService::class)->route() or + // app()->make(ODataService::class)->endpoint() + $this->app->instance(Endpoint::class, $service); $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); - $this->app->singleton(Model::class, function () { - return new Model(); - }); + // next instantiate and discover the global Model + $model = $service->discover(new Model()); + assert($model instanceof Model); + + // and register it with the container + $this->app->instance(Model::class, $model); + // I don't get why you are doing this twice? You never load the model + // via app()->make('lodata.model') or app()->make(Model::class). What + // am I missing here? $this->app->bind('lodata.model', function ($app) { return $app->make(Model::class); }); @@ -83,7 +115,7 @@ public function boot() return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); }); - $route = self::route(); + $route = $service->route(); $middleware = config('lodata.middleware', []); Route::get("{$route}/_lodata/odata.pbids", [PBIDS::class, 'get']); diff --git a/src/Transaction/MultipartDocument.php b/src/Transaction/MultipartDocument.php index 1728bcbb8..948452858 100644 --- a/src/Transaction/MultipartDocument.php +++ b/src/Transaction/MultipartDocument.php @@ -6,7 +6,8 @@ use Flat3\Lodata\Helper\Constants; use Flat3\Lodata\Helper\Url; -use Flat3\Lodata\ServiceProvider; +use Flat3\Lodata\Endpoint; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -153,6 +154,7 @@ public function parseDocuments(): self /** * Convert this document to a Request * @return Request + * @throws BindingResolutionException */ public function toRequest(): Request { @@ -161,10 +163,11 @@ public function toRequest(): Request list($method, $requestURI, $httpVersion) = array_pad(explode(' ', $requestLine), 3, ''); + $endpoint = app()->make(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( - ServiceProvider::endpoint(), + $endpoint, $requestURI, Url::HTTP_URL_REPLACE ); @@ -176,7 +179,7 @@ public function toRequest(): Request default: $uri = Url::http_build_url( - ServiceProvider::endpoint(), + $endpoint, $requestURI, Url::HTTP_URL_JOIN_PATH | Url::HTTP_URL_JOIN_QUERY ); From f17f1999a04eec1b41fff6cf3bc336437d270de8 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Fri, 14 Mar 2025 06:06:35 +0100 Subject: [PATCH 02/34] Tiny documentation tweak --- src/ServiceProvider.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 511b8b071..4f54a376b 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -96,9 +96,7 @@ private function bootServices($service): void // and register it with the container $this->app->instance(Model::class, $model); - // I don't get why you are doing this twice? You never load the model - // via app()->make('lodata.model') or app()->make(Model::class). What - // am I missing here? + // register alias $this->app->bind('lodata.model', function ($app) { return $app->make(Model::class); }); From 0926673e04111c4977598f9667a61cde24fb99b3 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Sat, 12 Apr 2025 14:53:40 +0200 Subject: [PATCH 03/34] Add possibility to use arbitrary Annotations on methods --- src/Drivers/EloquentEntitySet.php | 43 +++++++++---------- src/Interfaces/AnnotationFactoryInterface.php | 13 ++++++ 2 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 src/Interfaces/AnnotationFactoryInterface.php diff --git a/src/Drivers/EloquentEntitySet.php b/src/Drivers/EloquentEntitySet.php index 618676c6b..7b527dd64 100644 --- a/src/Drivers/EloquentEntitySet.php +++ b/src/Drivers/EloquentEntitySet.php @@ -33,6 +33,7 @@ use Flat3\Lodata\Helper\JSON; use Flat3\Lodata\Helper\PropertyValue; use Flat3\Lodata\Helper\PropertyValues; +use Flat3\Lodata\Interfaces\AnnotationFactoryInterface; use Flat3\Lodata\Interfaces\EntitySet\ComputeInterface; use Flat3\Lodata\Interfaces\EntitySet\CountInterface; use Flat3\Lodata\Interfaces\EntitySet\CreateInterface; @@ -866,29 +867,27 @@ public function discover(): self } /** @var ReflectionMethod $reflectionMethod */ - foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) { - /** @var LodataRelationship $relationshipInstance */ - $relationshipInstance = Discovery::getFirstMethodAttributeInstance( - $reflectionMethod, - LodataRelationship::class - ); - - if (!$relationshipInstance) { - continue; - } - - $relationshipMethod = $reflectionMethod->getName(); - - try { - $this->discoverRelationship( - $relationshipMethod, - $relationshipInstance->getName(), - $relationshipInstance->getDescription(), - $relationshipInstance->isNullable() - ); - } catch (ConfigurationException $e) { + foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) + foreach ($reflectionMethod->getAttributes() as $attribute) { + + $instance = $attribute->newInstance(); + if ($instance instanceof LodataRelationship) { + $relationshipMethod = $reflectionMethod->getName(); + + try { + $this->discoverRelationship( + $relationshipMethod, + $instance->getName(), + $instance->getDescription(), + $instance->isNullable() + ); + } catch (ConfigurationException $e) { + } + } + else if ($instance instanceof AnnotationFactoryInterface) { + $this->addAnnotation($instance->toAnnotation()); + } } - } return $this; } diff --git a/src/Interfaces/AnnotationFactoryInterface.php b/src/Interfaces/AnnotationFactoryInterface.php new file mode 100644 index 000000000..44c490ed4 --- /dev/null +++ b/src/Interfaces/AnnotationFactoryInterface.php @@ -0,0 +1,13 @@ + Date: Tue, 15 Apr 2025 15:52:58 +0200 Subject: [PATCH 04/34] Integrate alias into $metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added missing type PropertyPath - added Identifier to Record - added missing Type attribute to Record - added alias to Identifier – including dirty hack to derive it from given namespace - added addReference to Lodata Facade to allow adding additional vocabularies to the model - added alias for all auto registered References --- .gitignore | 1 + src/Annotation.php | 13 +++++++++ src/Annotation/Capabilities/V1/Reference.php | 1 + src/Annotation/Core/V1/Reference.php | 1 + src/Annotation/Record.php | 28 ++++++++++++++++++++ src/Annotation/Reference.php | 15 +++++++++++ src/Facades/Lodata.php | 1 + src/Helper/Identifier.php | 15 ++++++++++- src/Type/PropertyPath.php | 11 ++++++++ 9 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/Type/PropertyPath.php diff --git a/.gitignore b/.gitignore index 98496a821..f54c0aa16 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ vendor/ .phpunit.result.cache .phpunit.cache/test-results .phpdoc/ +.idea .idea/dataSources.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml diff --git a/src/Annotation.php b/src/Annotation.php index c74343082..bd8c80c63 100644 --- a/src/Annotation.php +++ b/src/Annotation.php @@ -13,6 +13,7 @@ use Flat3\Lodata\Type\Byte; use Flat3\Lodata\Type\Collection; use Flat3\Lodata\Type\Enum; +use Flat3\Lodata\Type\PropertyPath; use Flat3\Lodata\Type\String_; use SimpleXMLElement; @@ -58,6 +59,10 @@ public function appendJsonValue($value) case $value instanceof Record: $record = (object) []; + if (method_exists($value, 'getTypeName') && $value->getTypeName()) { + $record->{'@type'} = $value->getTypeName(); + } + /** @var PropertyValue $propertyValue */ foreach ($value as $propertyValue) { $record->{$propertyValue->getProperty()->getName()} = $this->appendJsonValue($propertyValue->getPrimitive()); @@ -109,6 +114,10 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) $element->addAttribute('Int', $value->toUrl()); break; + case $value instanceof PropertyPath: + $element->addAttribute('PropertyPath', $value->get()); + break; + case $value instanceof String_: $element->addAttribute('String', $value->get()); break; @@ -147,6 +156,10 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) protected function appendXmlRecord(SimpleXMLElement $element, Record $record) { $recordElement = $element->addChild('Record'); + $identifier = $record->getIdentifier(); + if (!is_null($identifier)) { + $recordElement->addAttribute('Type', $identifier->getQualifiedName()); + } /** @var PropertyValue $propertyValue */ foreach ($record as $propertyValue) { diff --git a/src/Annotation/Capabilities/V1/Reference.php b/src/Annotation/Capabilities/V1/Reference.php index 4715c36ce..7858abcd2 100644 --- a/src/Annotation/Capabilities/V1/Reference.php +++ b/src/Annotation/Capabilities/V1/Reference.php @@ -12,4 +12,5 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1'; protected $namespace = 'Org.OData.Capabilities.V1'; + protected $alias = 'Capabilities'; } \ No newline at end of file diff --git a/src/Annotation/Core/V1/Reference.php b/src/Annotation/Core/V1/Reference.php index 744f1c578..f0d9a18a4 100644 --- a/src/Annotation/Core/V1/Reference.php +++ b/src/Annotation/Core/V1/Reference.php @@ -12,4 +12,5 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1'; protected $namespace = 'Org.OData.Core.V1'; + protected $alias = 'Core'; } \ No newline at end of file diff --git a/src/Annotation/Record.php b/src/Annotation/Record.php index 65b3f4ab9..8544382b3 100644 --- a/src/Annotation/Record.php +++ b/src/Annotation/Record.php @@ -4,6 +4,7 @@ namespace Flat3\Lodata\Annotation; +use Flat3\Lodata\Helper\Identifier; use Flat3\Lodata\Helper\ObjectArray; use Flat3\Lodata\Interfaces\TypeInterface; use Flat3\Lodata\Traits\HasComplexType; @@ -16,4 +17,31 @@ class Record extends ObjectArray implements TypeInterface { use HasComplexType; + + /** + * Resource identifier + * @var Identifier $identifier + */ + protected $identifier; + + /** + * Get the identifier + * @return Identifier Identifier + */ + public function getIdentifier(): ?Identifier + { + return $this->identifier; + } + + /** + * Set the identifier + * @param string|Identifier $identifier Identifier + * @return $this + */ + public function setIdentifier($identifier): Record + { + $this->identifier = $identifier instanceof Identifier ? $identifier : new Identifier($identifier); + + return $this; + } } \ No newline at end of file diff --git a/src/Annotation/Reference.php b/src/Annotation/Reference.php index 0ebbafd3b..ee1700761 100644 --- a/src/Annotation/Reference.php +++ b/src/Annotation/Reference.php @@ -33,6 +33,21 @@ class Reference */ protected $alias; + public function getUri(): string + { + return $this->uri; + } + + public function getNamespace(): string + { + return $this->namespace; + } + + public function getAlias(): string + { + return is_null($this->alias) ? $this->namespace : $this->alias; + } + /** * Append this reference to the provided XML element * @param SimpleXMLElement $schema Schema diff --git a/src/Facades/Lodata.php b/src/Facades/Lodata.php index 260e299e2..f2966fdee 100644 --- a/src/Facades/Lodata.php +++ b/src/Facades/Lodata.php @@ -46,6 +46,7 @@ * @method static ComplexType getComplexType(Identifier|string $name) Get a complex type from the model * @method static Singleton getSingleton(Identifier|string $name) Get a singleton from the model * @method static IdentifierInterface add(IdentifierInterface $item) Add a named resource or type to the model + * @method static Model addReference(Reference $reference) Add a reference to an external CSDL document * @method static Model drop(Identifier|string $key) Drop a named resource or type from the model * @method static EntityContainer getEntityContainer() Get the entity container * @method static string getNamespace() Get the namespace of this model diff --git a/src/Helper/Identifier.php b/src/Helper/Identifier.php index 1dc517d79..34c902213 100644 --- a/src/Helper/Identifier.php +++ b/src/Helper/Identifier.php @@ -26,6 +26,11 @@ final class Identifier */ private $namespace; + /** + * @var string $alias + */ + private $alias; + public function __construct(string $identifier) { if (!Str::contains($identifier, '.')) { @@ -38,6 +43,14 @@ public function __construct(string $identifier) $this->name = Str::afterLast($identifier, '.'); $this->namespace = Str::beforeLast($identifier, '.'); + + // NB dirty hack to derive alias from namespace + if (preg_match('/\.([^.]+)\.V1$/', $this->namespace, $matches)) { + $this->alias = $matches[1]; + } + else { + $this->alias = $this->namespace; + } } /** @@ -98,7 +111,7 @@ public function setNamespace(string $namespace): self */ public function getQualifiedName(): string { - return $this->namespace.'.'.$this->name; + return $this->alias.'.'.$this->name; } /** diff --git a/src/Type/PropertyPath.php b/src/Type/PropertyPath.php new file mode 100644 index 000000000..7bce9a374 --- /dev/null +++ b/src/Type/PropertyPath.php @@ -0,0 +1,11 @@ + Date: Tue, 15 Apr 2025 16:28:21 +0200 Subject: [PATCH 05/34] Integrating Endpoint Handling --- src/Endpoint.php | 2 +- src/ServiceProvider.php | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Endpoint.php b/src/Endpoint.php index b8fb6a0aa..3d2362145 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -21,7 +21,7 @@ class Endpoint public function __construct(string $serviceUri) { - $this->serviceUri = rtrim($serviceUri, '/'); + $this->serviceUri = trim($serviceUri, '/'); $prefix = rtrim(config('lodata.prefix'), '/'); $this->route = ('' === $serviceUri) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 4f54a376b..088ff6154 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -14,7 +14,6 @@ use Flat3\Lodata\Helper\Flysystem; use Flat3\Lodata\Helper\DBAL; use Flat3\Lodata\Helper\Symfony; -use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Kernel; @@ -51,13 +50,12 @@ public function boot() $segments = explode('/', request()->path()); // we only kick off operation when path prefix is configured in lodata.php - // as all requests share the same root configuration + // and bypass all other routes for performance if ($segments[0] === config('lodata.prefix')) { // next look up the configured service endpoints $serviceUris = config('lodata.endpoints', []); - $service = null; if (0 === sizeof($serviceUris)) { // when no locators are defined, fallback to global mode; this will // ensure compatibility with prior versions of this package @@ -68,10 +66,9 @@ public function boot() $service = new $clazz($segments[1]); } else { - // when no service definition is configured for the path segment, - // we abort with an error condition; typically a dev working on - // setting up his project - abort('No odata service endpoint defined for path ' . $segments[1]); + // when no service definition could be found for the path segment, + // we assume global scope + $service = new Endpoint(''); } $this->bootServices($service); @@ -87,6 +84,10 @@ private function bootServices($service): void // app()->make(ODataService::class)->endpoint() $this->app->instance(Endpoint::class, $service); + $this->app->bind(DBAL::class, function (Application $app, array $args) { + return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); + }); + $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); // next instantiate and discover the global Model @@ -109,10 +110,6 @@ private function bootServices($service): void return class_exists('League\Flysystem\Adapter\Local') ? new Flysystem\Flysystem1() : new Flysystem\Flysystem3(); }); - $this->app->bind(DBAL::class, function (Application $app, array $args) { - return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); - }); - $route = $service->route(); $middleware = config('lodata.middleware', []); From ef5482dc8d692300372d0c402defa8263d0d012b Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Tue, 15 Apr 2025 17:24:00 +0200 Subject: [PATCH 06/34] Qualify model with Endpoint->getNamespace() --- src/Drivers/EloquentEntitySet.php | 4 +++- src/Endpoint.php | 16 ++++++++++++++++ src/Model.php | 2 +- src/PathSegment/Metadata/XML.php | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Drivers/EloquentEntitySet.php b/src/Drivers/EloquentEntitySet.php index 7b527dd64..6b4c2e728 100644 --- a/src/Drivers/EloquentEntitySet.php +++ b/src/Drivers/EloquentEntitySet.php @@ -20,6 +20,7 @@ use Flat3\Lodata\Drivers\SQL\SQLOrderBy; use Flat3\Lodata\Drivers\SQL\SQLSchema; use Flat3\Lodata\Drivers\SQL\SQLWhere; +use Flat3\Lodata\Endpoint; use Flat3\Lodata\Entity; use Flat3\Lodata\EntitySet; use Flat3\Lodata\EntityType; @@ -115,7 +116,8 @@ public function __construct(string $model, ?EntityType $entityType = null) $name = self::convertClassName($model); if (!$entityType) { - $entityType = new EntityType(EntityType::convertClassName($model)); + $identifier = app(Endpoint::class)->getNamespace().'.'.EntityType::convertClassName($model); + $entityType = new EntityType($identifier); } parent::__construct($name, $entityType); diff --git a/src/Endpoint.php b/src/Endpoint.php index 3d2362145..115a4f9fe 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -46,6 +46,22 @@ public function route(): string } /** + * This method is intended to be overridden by subclasses. + * + * The value of the function will be presented in the Schema Namespace attribute, + * https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_Schema + * + * @return string + */ + public function getNamespace(): string + { + // override this function to set Schema Namespace attribute + return config('lodata.namespace'); + } + + /** + * This method is intended to be overridden by subclasses. + * * Discovers Schema and Annotations of the `$metadata` file for * the service. */ diff --git a/src/Model.php b/src/Model.php index c58be6661..c17e527b0 100644 --- a/src/Model.php +++ b/src/Model.php @@ -199,7 +199,7 @@ public function getTypeDefinition(string $name): ?Type */ public static function getNamespace(): string { - return config('lodata.namespace'); + return app(Endpoint::class)->getNamespace(); } /** diff --git a/src/PathSegment/Metadata/XML.php b/src/PathSegment/Metadata/XML.php index b5eefa111..bad294fbb 100644 --- a/src/PathSegment/Metadata/XML.php +++ b/src/PathSegment/Metadata/XML.php @@ -239,14 +239,14 @@ public function emitStream(Transaction $transaction): void case $resource instanceof Singleton: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#_Toc38530395 $resourceElement = $entityContainer->addChild('Singleton'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); $resourceElement->addAttribute('Type', $resource->getType()->getIdentifier()->getQualifiedName()); break; case $resource instanceof EntitySet: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_EntitySet $resourceElement = $entityContainer->addChild('EntitySet'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); $resourceElement->addAttribute( 'EntityType', $resource->getType()->getIdentifier()->getQualifiedName() From b4b68a34ebecc935cdafed2bc03ce233a474c91f Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 18:05:53 +0200 Subject: [PATCH 07/34] Back to Endpoint discovery only --- src/Annotation.php | 13 ------ src/Annotation/Capabilities/V1/Reference.php | 1 - src/Annotation/Core/V1/Reference.php | 1 - src/Annotation/Record.php | 28 ------------ src/Annotation/Reference.php | 15 ------- src/Drivers/EloquentEntitySet.php | 47 ++++++++++---------- src/Facades/Lodata.php | 1 - src/Helper/Identifier.php | 15 +------ src/PathSegment/Metadata/XML.php | 4 +- 9 files changed, 26 insertions(+), 99 deletions(-) diff --git a/src/Annotation.php b/src/Annotation.php index bd8c80c63..c74343082 100644 --- a/src/Annotation.php +++ b/src/Annotation.php @@ -13,7 +13,6 @@ use Flat3\Lodata\Type\Byte; use Flat3\Lodata\Type\Collection; use Flat3\Lodata\Type\Enum; -use Flat3\Lodata\Type\PropertyPath; use Flat3\Lodata\Type\String_; use SimpleXMLElement; @@ -59,10 +58,6 @@ public function appendJsonValue($value) case $value instanceof Record: $record = (object) []; - if (method_exists($value, 'getTypeName') && $value->getTypeName()) { - $record->{'@type'} = $value->getTypeName(); - } - /** @var PropertyValue $propertyValue */ foreach ($value as $propertyValue) { $record->{$propertyValue->getProperty()->getName()} = $this->appendJsonValue($propertyValue->getPrimitive()); @@ -114,10 +109,6 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) $element->addAttribute('Int', $value->toUrl()); break; - case $value instanceof PropertyPath: - $element->addAttribute('PropertyPath', $value->get()); - break; - case $value instanceof String_: $element->addAttribute('String', $value->get()); break; @@ -156,10 +147,6 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) protected function appendXmlRecord(SimpleXMLElement $element, Record $record) { $recordElement = $element->addChild('Record'); - $identifier = $record->getIdentifier(); - if (!is_null($identifier)) { - $recordElement->addAttribute('Type', $identifier->getQualifiedName()); - } /** @var PropertyValue $propertyValue */ foreach ($record as $propertyValue) { diff --git a/src/Annotation/Capabilities/V1/Reference.php b/src/Annotation/Capabilities/V1/Reference.php index 7858abcd2..4715c36ce 100644 --- a/src/Annotation/Capabilities/V1/Reference.php +++ b/src/Annotation/Capabilities/V1/Reference.php @@ -12,5 +12,4 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1'; protected $namespace = 'Org.OData.Capabilities.V1'; - protected $alias = 'Capabilities'; } \ No newline at end of file diff --git a/src/Annotation/Core/V1/Reference.php b/src/Annotation/Core/V1/Reference.php index f0d9a18a4..744f1c578 100644 --- a/src/Annotation/Core/V1/Reference.php +++ b/src/Annotation/Core/V1/Reference.php @@ -12,5 +12,4 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1'; protected $namespace = 'Org.OData.Core.V1'; - protected $alias = 'Core'; } \ No newline at end of file diff --git a/src/Annotation/Record.php b/src/Annotation/Record.php index 8544382b3..65b3f4ab9 100644 --- a/src/Annotation/Record.php +++ b/src/Annotation/Record.php @@ -4,7 +4,6 @@ namespace Flat3\Lodata\Annotation; -use Flat3\Lodata\Helper\Identifier; use Flat3\Lodata\Helper\ObjectArray; use Flat3\Lodata\Interfaces\TypeInterface; use Flat3\Lodata\Traits\HasComplexType; @@ -17,31 +16,4 @@ class Record extends ObjectArray implements TypeInterface { use HasComplexType; - - /** - * Resource identifier - * @var Identifier $identifier - */ - protected $identifier; - - /** - * Get the identifier - * @return Identifier Identifier - */ - public function getIdentifier(): ?Identifier - { - return $this->identifier; - } - - /** - * Set the identifier - * @param string|Identifier $identifier Identifier - * @return $this - */ - public function setIdentifier($identifier): Record - { - $this->identifier = $identifier instanceof Identifier ? $identifier : new Identifier($identifier); - - return $this; - } } \ No newline at end of file diff --git a/src/Annotation/Reference.php b/src/Annotation/Reference.php index ee1700761..0ebbafd3b 100644 --- a/src/Annotation/Reference.php +++ b/src/Annotation/Reference.php @@ -33,21 +33,6 @@ class Reference */ protected $alias; - public function getUri(): string - { - return $this->uri; - } - - public function getNamespace(): string - { - return $this->namespace; - } - - public function getAlias(): string - { - return is_null($this->alias) ? $this->namespace : $this->alias; - } - /** * Append this reference to the provided XML element * @param SimpleXMLElement $schema Schema diff --git a/src/Drivers/EloquentEntitySet.php b/src/Drivers/EloquentEntitySet.php index 6b4c2e728..618676c6b 100644 --- a/src/Drivers/EloquentEntitySet.php +++ b/src/Drivers/EloquentEntitySet.php @@ -20,7 +20,6 @@ use Flat3\Lodata\Drivers\SQL\SQLOrderBy; use Flat3\Lodata\Drivers\SQL\SQLSchema; use Flat3\Lodata\Drivers\SQL\SQLWhere; -use Flat3\Lodata\Endpoint; use Flat3\Lodata\Entity; use Flat3\Lodata\EntitySet; use Flat3\Lodata\EntityType; @@ -34,7 +33,6 @@ use Flat3\Lodata\Helper\JSON; use Flat3\Lodata\Helper\PropertyValue; use Flat3\Lodata\Helper\PropertyValues; -use Flat3\Lodata\Interfaces\AnnotationFactoryInterface; use Flat3\Lodata\Interfaces\EntitySet\ComputeInterface; use Flat3\Lodata\Interfaces\EntitySet\CountInterface; use Flat3\Lodata\Interfaces\EntitySet\CreateInterface; @@ -116,8 +114,7 @@ public function __construct(string $model, ?EntityType $entityType = null) $name = self::convertClassName($model); if (!$entityType) { - $identifier = app(Endpoint::class)->getNamespace().'.'.EntityType::convertClassName($model); - $entityType = new EntityType($identifier); + $entityType = new EntityType(EntityType::convertClassName($model)); } parent::__construct($name, $entityType); @@ -869,28 +866,30 @@ public function discover(): self } /** @var ReflectionMethod $reflectionMethod */ - foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) - foreach ($reflectionMethod->getAttributes() as $attribute) { - - $instance = $attribute->newInstance(); - if ($instance instanceof LodataRelationship) { - $relationshipMethod = $reflectionMethod->getName(); - - try { - $this->discoverRelationship( - $relationshipMethod, - $instance->getName(), - $instance->getDescription(), - $instance->isNullable() - ); - } catch (ConfigurationException $e) { - } - } - else if ($instance instanceof AnnotationFactoryInterface) { - $this->addAnnotation($instance->toAnnotation()); - } + foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) { + /** @var LodataRelationship $relationshipInstance */ + $relationshipInstance = Discovery::getFirstMethodAttributeInstance( + $reflectionMethod, + LodataRelationship::class + ); + + if (!$relationshipInstance) { + continue; } + $relationshipMethod = $reflectionMethod->getName(); + + try { + $this->discoverRelationship( + $relationshipMethod, + $relationshipInstance->getName(), + $relationshipInstance->getDescription(), + $relationshipInstance->isNullable() + ); + } catch (ConfigurationException $e) { + } + } + return $this; } } diff --git a/src/Facades/Lodata.php b/src/Facades/Lodata.php index f2966fdee..260e299e2 100644 --- a/src/Facades/Lodata.php +++ b/src/Facades/Lodata.php @@ -46,7 +46,6 @@ * @method static ComplexType getComplexType(Identifier|string $name) Get a complex type from the model * @method static Singleton getSingleton(Identifier|string $name) Get a singleton from the model * @method static IdentifierInterface add(IdentifierInterface $item) Add a named resource or type to the model - * @method static Model addReference(Reference $reference) Add a reference to an external CSDL document * @method static Model drop(Identifier|string $key) Drop a named resource or type from the model * @method static EntityContainer getEntityContainer() Get the entity container * @method static string getNamespace() Get the namespace of this model diff --git a/src/Helper/Identifier.php b/src/Helper/Identifier.php index 34c902213..1dc517d79 100644 --- a/src/Helper/Identifier.php +++ b/src/Helper/Identifier.php @@ -26,11 +26,6 @@ final class Identifier */ private $namespace; - /** - * @var string $alias - */ - private $alias; - public function __construct(string $identifier) { if (!Str::contains($identifier, '.')) { @@ -43,14 +38,6 @@ public function __construct(string $identifier) $this->name = Str::afterLast($identifier, '.'); $this->namespace = Str::beforeLast($identifier, '.'); - - // NB dirty hack to derive alias from namespace - if (preg_match('/\.([^.]+)\.V1$/', $this->namespace, $matches)) { - $this->alias = $matches[1]; - } - else { - $this->alias = $this->namespace; - } } /** @@ -111,7 +98,7 @@ public function setNamespace(string $namespace): self */ public function getQualifiedName(): string { - return $this->alias.'.'.$this->name; + return $this->namespace.'.'.$this->name; } /** diff --git a/src/PathSegment/Metadata/XML.php b/src/PathSegment/Metadata/XML.php index bad294fbb..b5eefa111 100644 --- a/src/PathSegment/Metadata/XML.php +++ b/src/PathSegment/Metadata/XML.php @@ -239,14 +239,14 @@ public function emitStream(Transaction $transaction): void case $resource instanceof Singleton: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#_Toc38530395 $resourceElement = $entityContainer->addChild('Singleton'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); $resourceElement->addAttribute('Type', $resource->getType()->getIdentifier()->getQualifiedName()); break; case $resource instanceof EntitySet: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_EntitySet $resourceElement = $entityContainer->addChild('EntitySet'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); $resourceElement->addAttribute( 'EntityType', $resource->getType()->getIdentifier()->getQualifiedName() From eaf19600086354f6ecea2cf4edb4afb6245ee0ae Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 18:06:53 +0200 Subject: [PATCH 08/34] Back to Endpoint discovery only (2/2) --- src/Interfaces/AnnotationFactoryInterface.php | 13 ------------- src/Type/PropertyPath.php | 11 ----------- 2 files changed, 24 deletions(-) delete mode 100644 src/Interfaces/AnnotationFactoryInterface.php delete mode 100644 src/Type/PropertyPath.php diff --git a/src/Interfaces/AnnotationFactoryInterface.php b/src/Interfaces/AnnotationFactoryInterface.php deleted file mode 100644 index 44c490ed4..000000000 --- a/src/Interfaces/AnnotationFactoryInterface.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Wed, 16 Apr 2025 19:00:46 +0200 Subject: [PATCH 09/34] Introduced Interface for ServiceEndpoints --- src/Endpoint.php | 40 +++-------- src/Interfaces/ServiceEndpointInterface.php | 74 +++++++++++++++++++++ src/Model.php | 2 +- src/PathSegment/Metadata.php | 5 ++ src/PathSegment/Metadata/CachedXML.php | 37 +++++++++++ 5 files changed, 126 insertions(+), 32 deletions(-) create mode 100644 src/Interfaces/ServiceEndpointInterface.php create mode 100644 src/PathSegment/Metadata/CachedXML.php diff --git a/src/Endpoint.php b/src/Endpoint.php index 115a4f9fe..75b7930be 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -2,21 +2,14 @@ namespace Flat3\Lodata; -class Endpoint +use Flat3\Lodata\Interfaces\ServiceEndpointInterface; + +class Endpoint implements ServiceEndpointInterface { - /** - * @var string $serviceUri the <service-uri> of the Flat3\Lodata\Model. - */ protected $serviceUri; - /** - * @var string $route the route prefix configured in 'lodata.prefix' - */ protected $route; - /** - * @var string $endpoint the full url to the ODataService endpoint - */ protected $endpoint; public function __construct(string $serviceUri) @@ -31,10 +24,6 @@ public function __construct(string $serviceUri) $this->endpoint = url($this->route) . '/'; } - /** - * @return string the path within the odata URI space, like in - * https://:///$metadata - */ public function endpoint(): string { return $this->endpoint; @@ -45,29 +34,18 @@ public function route(): string return $this->route; } - /** - * This method is intended to be overridden by subclasses. - * - * The value of the function will be presented in the Schema Namespace attribute, - * https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_Schema - * - * @return string - */ - public function getNamespace(): string + public function namespace(): string { - // override this function to set Schema Namespace attribute return config('lodata.namespace'); } - /** - * This method is intended to be overridden by subclasses. - * - * Discovers Schema and Annotations of the `$metadata` file for - * the service. - */ + public function cachedMetadataXMLPath(): ?string + { + return null; + } + public function discover(Model $model): Model { - // override this function to register all of your $metadata capabilities return $model; } } \ No newline at end of file diff --git a/src/Interfaces/ServiceEndpointInterface.php b/src/Interfaces/ServiceEndpointInterface.php new file mode 100644 index 000000000..568953b60 --- /dev/null +++ b/src/Interfaces/ServiceEndpointInterface.php @@ -0,0 +1,74 @@ +:///$metadata + * + * @return string The relative OData endpoint path + */ + public function endpoint(): string; + + /** + * Returns the full request route to this service endpoint. + * + * This typically resolves to the route path used by Laravel to handle + * incoming requests for this specific service instance. + * + * @return string The full HTTP route to the endpoint + */ + public function route(): string; + + /** + * Returns the XML namespace used in the `$metadata` document. + * + * This value is injected as the `Namespace` attribute of the element + * in the OData CSDL document, and must be globally unique per service. + * + * @see https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_Schema + * + * @return string The schema namespace for the service + */ + public function namespace(): string; + + /** + * Returns the absolute filesystem path to a statically annotated `$metadata` file. + * + * This method can be overridden to provide a custom pre-generated CSDL XML file + * for the OData metadata endpoint. If a path is returned, it will be used as-is + * instead of dynamically generating the schema from model definitions. + * Return `null` to fall back to automatic schema generation. + * + * @return string|null Full path to a static $metadata XML file, or null for dynamic generation + */ + public function cachedMetadataXMLPath(): ?string; + + /** + * Builds or enriches the model used for schema generation and metadata discovery. + * + * This method should populate the provided `Model` instance with entity sets, + * types, annotations, and operations that define the service schema. It is + * invoked during the metadata bootstrapping process when an OData service request + * is processed. + * + * @param Model $model The schema model to populate + * @return Model The enriched model instance representing the OData service + */ + public function discover(Model $model): Model; +} \ No newline at end of file diff --git a/src/Model.php b/src/Model.php index c17e527b0..ce8881d20 100644 --- a/src/Model.php +++ b/src/Model.php @@ -199,7 +199,7 @@ public function getTypeDefinition(string $name): ?Type */ public static function getNamespace(): string { - return app(Endpoint::class)->getNamespace(); + return app(Endpoint::class)->namespace(); } /** diff --git a/src/PathSegment/Metadata.php b/src/PathSegment/Metadata.php index 3f9dcf967..b49ef1f81 100644 --- a/src/PathSegment/Metadata.php +++ b/src/PathSegment/Metadata.php @@ -5,10 +5,12 @@ namespace Flat3\Lodata\PathSegment; use Flat3\Lodata\Controller\Transaction; +use Flat3\Lodata\Endpoint; use Flat3\Lodata\Exception\Internal\PathNotHandledException; use Flat3\Lodata\Exception\Protocol\BadRequestException; use Flat3\Lodata\Interfaces\PipeInterface; use Flat3\Lodata\Interfaces\ResponseInterface; +use Flat3\Lodata\PathSegment\Metadata\CachedXML; use Flat3\Lodata\Transaction\MediaType; use Flat3\Lodata\Transaction\MediaTypes; use Illuminate\Http\Request; @@ -45,6 +47,9 @@ public static function pipe( return new Metadata\JSON(); default: + if (app(Endpoint::class)->cachedMetadataXMLPath()) { + return new CachedXML(); + } return new Metadata\XML(); } } diff --git a/src/PathSegment/Metadata/CachedXML.php b/src/PathSegment/Metadata/CachedXML.php new file mode 100644 index 000000000..dcd7fe260 --- /dev/null +++ b/src/PathSegment/Metadata/CachedXML.php @@ -0,0 +1,37 @@ +sendContentType((new MediaType)->parse(MediaType::xml)); + + return $transaction->getResponse()->setCallback(function () use ($transaction) { + $this->emitStream($transaction); + }); + } + + public function emitStream(Transaction $transaction): void + { + $path = app(Endpoint::class)->cachedMetadataXMLPath(); + if (!file_exists($path)) { + throw new RuntimeException("Metadata file not found: {$path}"); + } + $content = file_get_contents($path); + if (false === $content) { + throw new RuntimeException("Failed to read metadata file: {$path}"); + } + $transaction->sendOutput($content); + } +} \ No newline at end of file From 57550f134123451156a4a60efc0fcbfd1bc6be07 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 19:37:25 +0200 Subject: [PATCH 10/34] Update documentation --- doc/getting-started/endpoint.md | 317 ++++++++++++++++++++++++++++++-- 1 file changed, 301 insertions(+), 16 deletions(-) diff --git a/doc/getting-started/endpoint.md b/doc/getting-started/endpoint.md index 64ba2aa76..68695a431 100644 --- a/doc/getting-started/endpoint.md +++ b/doc/getting-started/endpoint.md @@ -1,43 +1,328 @@ + # Service Endpoints -At this point we assume you already published the `lodata.php` config file to your project. +> **Prerequisite**: You’ve already published the `lodata.php` config file into your Laravel project using `php artisan vendor:publish`. + +## Overview + +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. + +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. -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. +## Defining Multiple Endpoints -Each of your modules could register its own service endpoint with an `\Flat3\Lodata\Endpoint` like this: +You can define service endpoints by registering them in your `config/lodata.php` configuration file: ```php /** - * At the end of `config/lodata.php` + * At the end of config/lodata.php */ 'endpoints' => [ - 'projects' ⇒ \App\Projects\ProjectEndpoint::class, + 'projects' => \App\Projects\ProjectEndpoint::class, ], ``` -With that configuration a separate `$metadata` service file will be available via `https://://projects/$metadata`. +With this configuration, a separate `$metadata` document becomes available at: + +``` +https://://projects/$metadata +``` -If the `endpoints` array stays empty (the default), only one global service endpoint is created. +If the `endpoints` array is left empty (the default), only a single global endpoint is created under the configured `lodata.prefix`. -## Selective Discovery +## Endpoint Discovery -With endpoints, you can now discover all your entities and annotations in a separate class via the `discover` function. +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. + +This gives you fine-grained control over what each endpoint exposes. ```php -use App\Model\Contact; +use App\Models\Contact; use Flat3\Lodata\Model; /** - * Discovers Schema and Annotations of the `$metadata` file for - * the service. + * Discover schema elements and annotations for the service endpoint. */ public function discover(Model $model): Model { - // register all of your $metadata capabilities - $model->discover(Contact::class); - … + // Register all exposed entity sets or types + $model->discover(Contact::class); + // Add more types or annotations here... + return $model; } ``` -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. +### Performance Benefit + +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. + +> ✅ This optimization also applies to the **default (global) service endpoint** — its `discover()` method is likewise only evaluated on-demand during OData requests. + +This design keeps your application performant, especially in modular or multi-endpoint setups, by avoiding unnecessary processing for unrelated HTTP traffic. + +## Serving Pre-Generated $metadata Files + +In addition to dynamic schema generation, you can optionally serve a **pre-generated `$metadata.xml` file**. This is especially useful when: + +- You want to include **custom annotations** that are not easily represented in PHP code. +- You have **external tools** that generate the schema. +- You prefer **fine-tuned control** over the metadata document. + +To enable this, implement the `cachedMetadataXMLPath()` method in your endpoint class: + +```php +public function cachedMetadataXMLPath(): ?string +{ + return base_path('odata/metadata-projects.xml'); +} +``` + +If this method returns a valid file path, `lodata` will serve this file directly when `$metadata` is requested, bypassing the `discover()` logic. + +If it returns `null` (default), the schema will be generated dynamically from the `discover()` method. + +## Summary + +| Feature | Dynamic (`discover`) | Static (`cachedMetadataXMLPath`) | +|--------------------------|----------------------|-----------------------------------| +| Schema definition | In PHP | In XML file | +| Supports annotations | Basic | Full (manual control) | +| Performance optimized | Yes | Yes | +| Best for | Laravel-native setup | SAP integration, fine-tuned CSDL | + +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. + +## Sample: Defining a `ProjectEndpoint` + +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. + +### Step 1: Define the Custom Endpoint Class + +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. + +Here’s a minimal yet complete example: + +```php + element of $metadata. + */ + public function namespace(): string + { + return 'ProjectService'; + } + + /** + * Optionally return a static metadata XML file. + * If null, dynamic discovery via discover() is used. + */ + public function cachedMetadataXMLPath(): ?string + { + return resource_path('meta/ProjectService.xml'); + } + + /** + * Register entities and types to expose through this endpoint. + */ + public function discover(Model $model): Model + { + $model->discover(Project::class); + + return $model; + } +} +``` + +> ✅ **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. + +### Step 2: Register the Endpoint and Define Its URI Prefix + +In your `config/lodata.php`, register the custom endpoint under the `endpoints` array: + +```php +'endpoints' => [ + 'projects' => \App\Endpoints\ProjectEndpoint::class, +], +``` + +> 🧩 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`. + +This results in: + +- `$metadata` available at: + `https://://projects/$metadata` + +- Entity sets exposed through: + `https://://projects/Projects` + +This convention gives you **clear, readable URLs** and enables **modular, multi-service APIs** without extra routing configuration. + +### Step 3: Serve Dynamic or Static Metadata + +The framework will: + +- Call `cachedMetadataXMLPath()` first. + If a file path is returned and the file exists, it will serve that file directly. +- Otherwise, it will fall back to the `discover()` method to dynamically register entities, types, and annotations. + +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. + +## ✅ What You Get + +With just a few lines of configuration, you now have: + +- A **cleanly separated OData service** for the `Project` module. +- **Independent metadata** for documentation and integration. +- A fast and **on-demand schema bootstrapping** process. +- Full **control over discoverability** and **extensibility**. + +You can now repeat this pattern for other domains (e.g., `contacts`, `finance`, `hr`) to keep your OData services modular, testable, and scalable. + +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: + +## How Everything Connects + +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. + +### Flow Overview + +``` +[ config/lodata.php ] → [ ProjectEndpoint class ] + │ │ + ▼ ▼ + 'projects' => ProjectEndpoint::class ──► defines: + - namespace() + - discover() + - cachedMetadataXMLPath() + + │ │ + ▼ ▼ + URI: /odata/projects/$metadata OData Schema (XML or dynamic) +``` + +### The Building Blocks + +| Component | Purpose | +|-----------------------------------|-------------------------------------------------------------------------| +| **`config/lodata.php`** | Registers all endpoints and defines the URI prefix for each one | +| **Key: `'projects'`** | Becomes part of the URL: `/odata/projects/` | +| **`ProjectEndpoint` class** | Defines what the endpoint serves and how | +| **`namespace()`** | Injects the `` into `$metadata` | +| **`discover(Model $model)`** | Dynamically registers entities like `Project::class` | +| **`cachedMetadataXMLPath()`** | Optionally returns a pre-generated CSDL XML file | +| **OData request** | Triggers loading of this endpoint’s metadata and data | + +## Example: Request Lifecycle + +Let’s break down how the enhanced flow would look for an actual **entity set access**, such as + +``` +GET /odata/projects/Costcenters +``` + +This is about a **data request** for a specific entity set. Here's how the full lifecycle plays out. From config to response. + +### Enhanced Flow for `/odata/projects/Costcenters` + +``` + ┌─────────────────────────────────────────────────────┐ + │ HTTP GET /odata/projects/Costcenters │ + └─────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ [Routing Layer] matches +│ config/lodata.php │── 'projects' key +│ │ +│ 'projects' => ProjectEndpoint::class, │ +└────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ New ProjectEndpoint instance │ + └──────────────────────────────────────┘ + │ + (cachedMetadataXMLPath() not used here) + │ + ▼ + ┌───────────────────────────────────────────────┐ + │ discover(Model $model) is invoked │ + │ → model->discover(Project::class) │ + │ → model->discover(Costcenter::class) │ + └───────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Lodata resolves the URI segment: │ + │ `Costcenters` │ + └──────────────────────────────────────┘ + │ + (via the registered EntitySet name for Costcenter) + │ + ▼ + ┌───────────────────────────────────────────────┐ + │ Query engine builds and executes the query │ + │ using the underlying Eloquent model │ + └───────────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────────────────┐ + │ Response is serialized into JSON or XML │ + │ according to Accept header │ + └────────────────────────────────────────────┘ + │ + ▼ + 🔁 JSON (default) or Atom/XML payload with Costcenter entities + +``` + +### What Must Be in Place for This to Work + +| Requirement | Description | +|-----------------------------------------------|-----------------------------------------------------------------------------| +| `ProjectEndpoint::discover()` | Must register `Costcenter::class` via `$model->discover(...)` | +| `Costcenter` model | Can be a **standard Laravel Eloquent model** – no special base class needed | +| `EntitySet` name | Must match the URI segment: `Costcenters` | +| URI case sensitivity | Lodata uses the identifier names → ensure entity names match URI segments | +| Accept header | Optional – defaults to JSON if none is provided | + +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: + +## What Modular Service Endpoints Enable + +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: + +- **Modular APIs** — Define multiple endpoints, each exposing only the entities and operations relevant to a specific domain (e.g., `projects`, `contacts`, `finance`). +- **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. +- **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. +- **Schema isolation** — Maintain clean separation between domains, clients, or API versions. For example: + - `/odata/projects/$metadata` → `ProjectService` schema + - `/odata/finance/$metadata` → `FinanceService` schema +- **Mix and match discovery strategies** — Use dynamic schema generation via Eloquent models or inject precise, curated metadata with static CSDL files. +- **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. + +### ✅ In Short + +Modular service endpoints allow you to: + +- Keep your domains cleanly separated +- Scale your API by feature, client, or team +- Provide tailored metadata per endpoint +- Mix dynamic discovery with pre-defined XML schemas +- Integrate smoothly into your Laravel app — no magic, just configuration and conventions + +They’re not just a convenience, they’re a foundation for **clean, scalable, and maintainable OData APIs**. From b7affa9a2fa47ff104bdd283069ec4a165e5c530 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 19:51:42 +0200 Subject: [PATCH 11/34] Alias declaration more Laravel-like --- src/ServiceProvider.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 088ff6154..fccacadff 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -98,9 +98,7 @@ private function bootServices($service): void $this->app->instance(Model::class, $model); // register alias - $this->app->bind('lodata.model', function ($app) { - return $app->make(Model::class); - }); + $this->app->alias(Model::class, 'lodata.model'); $this->app->bind(Response::class, function () { return Kernel::VERSION_ID < 60000 ? new Symfony\Response5() : new Symfony\Response6(); From 7bfba22b391e10d15176104b71f225958d5e67f6 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 23 Apr 2025 11:10:05 +0200 Subject: [PATCH 12/34] removed .idea from .gitignore; resolving Endpoint unified --- .gitignore | 1 - src/Controller/ODCFF.php | 2 +- src/Controller/PBIDS.php | 2 +- src/Controller/Transaction.php | 4 ++-- src/Entity.php | 2 +- src/Model.php | 2 +- src/PathSegment/Batch/JSON.php | 2 +- src/PathSegment/OpenAPI.php | 2 +- src/Transaction/MultipartDocument.php | 2 +- 9 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index f54c0aa16..98496a821 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ vendor/ .phpunit.result.cache .phpunit.cache/test-results .phpdoc/ -.idea .idea/dataSources.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml diff --git a/src/Controller/ODCFF.php b/src/Controller/ODCFF.php index 6ae19860a..e42fbf473 100644 --- a/src/Controller/ODCFF.php +++ b/src/Controller/ODCFF.php @@ -167,7 +167,7 @@ public function get(string $identifier): Response $formula = $mashupDoc->createElement('Formula'); $formulaContent = $mashupDoc->createCDATASection(sprintf( 'let Source = OData.Feed("%1$s", null, [Implementation="2.0"]), %2$s_table = Source{[Name="%2$s",Signature="table"]}[Data] in %2$s_table', - app()->make(Endpoint::class)->endpoint(), + app(Endpoint::class)->endpoint(), $resourceId, )); $formula->appendChild($formulaContent); diff --git a/src/Controller/PBIDS.php b/src/Controller/PBIDS.php index 69e46ce30..61f9178d6 100644 --- a/src/Controller/PBIDS.php +++ b/src/Controller/PBIDS.php @@ -44,7 +44,7 @@ public function get(): Response 'details' => [ 'protocol' => 'odata', 'address' => [ - 'url' => app()->make(Endpoint::class)->endpoint(), + 'url' => app(Endpoint::class)->endpoint(), ], ], ], diff --git a/src/Controller/Transaction.php b/src/Controller/Transaction.php index 40faf4dce..a6367624f 100644 --- a/src/Controller/Transaction.php +++ b/src/Controller/Transaction.php @@ -785,7 +785,7 @@ public function getPath(): string */ public function getRequestPath(): string { - $route = app()->make(Endpoint::class)->route(); + $route = app(Endpoint::class)->route(); return Str::substr($this->request->path(), strlen($route)); } @@ -963,7 +963,7 @@ public function getContextUrl(): string */ public static function getResourceUrl(): string { - return app()->make(Endpoint::class)->endpoint(); + return app(Endpoint::class)->endpoint(); } /** diff --git a/src/Entity.php b/src/Entity.php index efb76f975..8c1067407 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -178,7 +178,7 @@ public static function pipe( } $entityId = $id->getValue(); - $endpoint = app()->make(Endpoint::class)->endpoint(); + $endpoint = app(Endpoint::class)->endpoint(); if (Str::startsWith($entityId, $endpoint)) { $entityId = Str::substr($entityId, strlen($endpoint)); } diff --git a/src/Model.php b/src/Model.php index ce8881d20..1c1bdd81e 100644 --- a/src/Model.php +++ b/src/Model.php @@ -344,7 +344,7 @@ public function discover($discoverable): self */ public function getEndpoint(): string { - return app()->make(Endpoint::class)->endpoint(); + return app(Endpoint::class)->endpoint(); } /** diff --git a/src/PathSegment/Batch/JSON.php b/src/PathSegment/Batch/JSON.php index 048b90238..9c012d0f1 100644 --- a/src/PathSegment/Batch/JSON.php +++ b/src/PathSegment/Batch/JSON.php @@ -59,7 +59,7 @@ public function emitJson(Transaction $transaction): void $requestURI = $requestData['url']; - $endpoint = app()->make(Endpoint::class)->endpoint(); + $endpoint = app(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( diff --git a/src/PathSegment/OpenAPI.php b/src/PathSegment/OpenAPI.php index 5df9f3215..b1657d9ab 100644 --- a/src/PathSegment/OpenAPI.php +++ b/src/PathSegment/OpenAPI.php @@ -409,7 +409,7 @@ public function emitJson(Transaction $transaction): void $queryObject->tags = [__('lodata::Batch requests')]; - $route = app()->make(Endpoint::class)->route(); + $route = app(Endpoint::class)->route(); $requestBody = [ 'required' => true, diff --git a/src/Transaction/MultipartDocument.php b/src/Transaction/MultipartDocument.php index 948452858..c87c3a1d2 100644 --- a/src/Transaction/MultipartDocument.php +++ b/src/Transaction/MultipartDocument.php @@ -163,7 +163,7 @@ public function toRequest(): Request list($method, $requestURI, $httpVersion) = array_pad(explode(' ', $requestLine), 3, ''); - $endpoint = app()->make(Endpoint::class)->endpoint(); + $endpoint = app(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( From 6b584daa562f490459a40687d1531a1e6a85a4a8 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Thu, 29 May 2025 13:40:07 +0200 Subject: [PATCH 13/34] Answer HEAD Requests on the Service URI This is important to allow Token Handling for Clients like OpenUi5 --- src/PathSegment/Service.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PathSegment/Service.php b/src/PathSegment/Service.php index 19372e3f4..f3ed110a2 100644 --- a/src/PathSegment/Service.php +++ b/src/PathSegment/Service.php @@ -21,6 +21,11 @@ class Service implements JsonInterface, ResponseInterface { public function response(Transaction $transaction, ?ContextInterface $context = null): Response { + if (Request::METHOD_HEAD === $transaction->getMethod()) { + return $transaction->getResponse()->setCallback(function () use ($transaction) { + $transaction->sendOutput(''); + }); + } $transaction->assertMethod(Request::METHOD_GET); return $transaction->getResponse()->setCallback(function () use ($transaction) { From 84088bf83ce52a0711072a666fbd59dc198ef0ba Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Mon, 23 Jun 2025 06:08:32 +0200 Subject: [PATCH 14/34] Fix: Prevent 'Undefined array key 1' when global Endpoint is requested When multiple Endpoints are declared, and the frontend requests the global Endpoint (with only one path segment), accessing $segments[1] caused an 'Undefined array key' warning. This commit adds a count($segments) check to avoid invalid index access and preserve compatibility with global Endpoint logic. --- src/ServiceProvider.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index fccacadff..78bdedc12 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -56,9 +56,10 @@ public function boot() // next look up the configured service endpoints $serviceUris = config('lodata.endpoints', []); - if (0 === sizeof($serviceUris)) { - // when no locators are defined, fallback to global mode; this will - // ensure compatibility with prior versions of this package + if (0 === sizeof($serviceUris) || count($segments) === 1) { + // when no locators are defined, or the global locator ist requested, + // enter global mode; this will ensure compatibility with prior + // versions of this package $service = new Endpoint(''); } else if (array_key_exists($segments[1], $serviceUris)) { From fb8bb0155293bfcdaa16e4ad40ec86f59ed40160 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Sun, 13 Jul 2025 11:33:24 +0200 Subject: [PATCH 15/34] Fix: Add serviceUri() to ServiceEndpointInterface and clarify endpoint routing docs - Introduced new method serviceUri(): string to explicitly declare the logical URI segment for each OData endpoint - Updated ServiceEndpointInterface PHPDoc to reflect modular, config-driven routing - Clarified semantics of endpoint() and route() to match actual behavior in Endpoint base class - Aligns terminology with ServiceProvider::boot() logic for endpoint resolution --- src/Endpoint.php | 8 ++- src/Interfaces/ServiceEndpointInterface.php | 61 ++++++++++++++++----- src/ServiceProvider.php | 8 +++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/Endpoint.php b/src/Endpoint.php index 75b7930be..a7e6eed44 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -17,13 +17,19 @@ public function __construct(string $serviceUri) $this->serviceUri = trim($serviceUri, '/'); $prefix = rtrim(config('lodata.prefix'), '/'); - $this->route = ('' === $serviceUri) + + $this->route = ('' === $this->serviceUri) ? $prefix : $prefix . '/' . $this->serviceUri; $this->endpoint = url($this->route) . '/'; } + public function serviceUri(): string + { + return $this->serviceUri; + } + public function endpoint(): string { return $this->endpoint; diff --git a/src/Interfaces/ServiceEndpointInterface.php b/src/Interfaces/ServiceEndpointInterface.php index 568953b60..3c4c7ba06 100644 --- a/src/Interfaces/ServiceEndpointInterface.php +++ b/src/Interfaces/ServiceEndpointInterface.php @@ -5,33 +5,68 @@ use Flat3\Lodata\Model; /** - * Interface for defining a custom OData service endpoint. + * Interface for defining a modular OData service endpoint in Laravel. * - * Implementers can use this interface to expose a specific service under a custom path, - * define its namespace, route behavior, and optionally provide a statically generated - * $metadata document. + * Implementers of this interface represent individually addressable OData services. + * Each mounted under its own URI segment and backed by a schema model. + * + * This enables clean separation of business domains and supports multi-endpoint + * discovery for modular application design. + * + * Configuration versus Declaration + * -------------------------------- + * The public URI segment used to expose a service is NOT determined by the + * implementing class itself, but by the service map in `config/lodata.php`: + * + * ```php + * 'endpoints' => [ + * 'users' => \App\OData\UsersEndpoint::class, + * 'budgets' => \App\OData\BudgetsEndpoint::class, + * ] + * ``` + * + * This keeps the routing surface under application control, and avoids + * conflicts when two modules declare the same internal segment. + * + * To implement an endpoint: + * - Extend `Flat3\Lodata\Endpoint` or implement this interface directly + * - Register the class in `config/lodata.php` under a unique segment key + * - Define the `discover()` method to expose entities via OData */ interface ServiceEndpointInterface { + /** + * Returns the ServiceURI segment name for this OData endpoint. + * + * This value is used in routing and metadata resolution. It Must be globally unique. + * + * @return string The segment (path identifier) of the endpoint + */ + public function serviceUri(): string; /** - * Returns the relative endpoint identifier within the OData service URI space. + * Returns the fully qualified URL for this OData endpoint. * - * This is the part that appears between the configured Lodata prefix and - * the `$metadata` segment, e.g.: - * https://:///$metadata + * This includes the application host, port, and the configured segment, + * https://:///, + * Example: https://example.com/odata/users/ * - * @return string The relative OData endpoint path + * This URL forms the base of the OData service space, and is used for navigation links + * and metadata discovery. + * + * @return string The full URL of the service endpoint */ public function endpoint(): string; /** - * Returns the full request route to this service endpoint. + * Returns the internal Laravel route path for this OData service endpoint. + * + * This is the relative URI path that Laravel uses to match incoming requests, + * typically composed of the configured Lodata prefix and the service segment. * - * This typically resolves to the route path used by Laravel to handle - * incoming requests for this specific service instance. + * Example: "odata/users" * - * @return string The full HTTP route to the endpoint + * @return string Relative route path for the endpoint */ public function route(): string; diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 78bdedc12..b7400387e 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,6 +4,7 @@ namespace Flat3\Lodata; +use RuntimeException; use Composer\InstalledVersions; use Flat3\Lodata\Controller\Monitor; use Flat3\Lodata\Controller\OData; @@ -14,6 +15,7 @@ use Flat3\Lodata\Helper\Flysystem; use Flat3\Lodata\Helper\DBAL; use Flat3\Lodata\Helper\Symfony; +use Flat3\Lodata\Interfaces\ServiceEndpointInterface; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Kernel; @@ -64,6 +66,12 @@ public function boot() } else if (array_key_exists($segments[1], $serviceUris)) { $clazz = $serviceUris[$segments[1]]; + if (!class_exists($clazz)) { + throw new RuntimeException(sprintf('Endpoint class `%s` does not exist', $clazz)); + } + if (!is_subclass_of($clazz, ServiceEndpointInterface::class)) { + throw new RuntimeException(sprintf('Endpoint class `%s` must implement Flat3\\Lodata\\Interfaces\\ServiceEndpointInterface', $clazz)); + } $service = new $clazz($segments[1]); } else { From ff1949e23eff028356aa9132d6d267639a7f81cf Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Tue, 22 Jul 2025 13:43:23 +0200 Subject: [PATCH 16/34] chore: simplify ServiceProvider and externalize multi-endpoint support - remove "extra.laravel" from composer.json to disable auto-registration - always boot default Endpoint in ServiceProvider for consistent behavior - drop request path inspection logic for segmented endpoint resolution ==> apps requiring multiple OData endpoints should now implement and register their own ServiceProvider (see docs for example) --- composer.json | 3 - composer.lock | 1487 ++++++++++++++----------------- doc/getting-started/README.md | 11 + doc/getting-started/endpoint.md | 129 +++ src/ServiceProvider.php | 42 +- 5 files changed, 793 insertions(+), 879 deletions(-) diff --git a/composer.json b/composer.json index e1f0cd752..ebb956fa0 100644 --- a/composer.json +++ b/composer.json @@ -39,9 +39,6 @@ }, "extra": { "laravel": { - "providers": [ - "Flat3\\Lodata\\ServiceProvider" - ], "aliases": { "Lodata": "Flat3\\Lodata\\Facades\\Lodata" } diff --git a/composer.lock b/composer.lock index a9b7af5a7..b40afe69c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ac082c3ab51cf432a3c722d7e9aebc53", + "content-hash": "b2aba9ce0072f6841102a5b89c739a97", "packages": [ { "name": "brick/math", - "version": "0.12.3", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.3" + "source": "https://github.com/brick/math/tree/0.13.1" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-02-28T13:11:00+00:00" + "time": "2025-03-29T13:50:30+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -212,34 +212,34 @@ }, { "name": "doctrine/dbal", - "version": "4.2.3", + "version": "4.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e" + "reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e", - "reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ac336c95ea9e13433d56ca81c308b39db0e1a2a7", + "reference": "ac336c95ea9e13433d56ca81c308b39db0e1a2a7", "shasum": "" }, "require": { - "doctrine/deprecations": "^0.5.3|^1", - "php": "^8.1", + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/coding-standard": "13.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.2", - "phpstan/phpstan": "2.1.1", - "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-phpunit": "2.0.6", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "10.5.39", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", + "phpunit/phpunit": "11.5.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", "symfony/cache": "^6.3.8|^7.0", "symfony/console": "^5.4|^6.3|^7.0" }, @@ -298,7 +298,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.2.3" + "source": "https://github.com/doctrine/dbal/tree/4.3.1" }, "funding": [ { @@ -314,30 +314,33 @@ "type": "tidelift" } ], - "time": "2025-03-07T18:29:05+00:00" + "time": "2025-07-22T10:09:51+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.4", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "1.4.10 || 2.0.3", + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -357,9 +360,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" }, - "time": "2024-12-07T21:18:45+00:00" + "time": "2025-04-07T20:06:18+00:00" }, { "name": "doctrine/inflector", @@ -596,16 +599,16 @@ }, { "name": "egulias/email-validator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "b115554301161fa21467629f1e1391c1936de517" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", - "reference": "b115554301161fa21467629f1e1391c1936de517", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { @@ -651,7 +654,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -659,7 +662,7 @@ "type": "github" } ], - "time": "2024-12-27T00:36:43+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { "name": "fruitcake/php-cors", @@ -796,16 +799,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { @@ -902,7 +905,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, "funding": [ { @@ -918,20 +921,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { @@ -985,7 +988,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.2.0" }, "funding": [ { @@ -1001,20 +1004,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-03-27T13:27:01+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "shasum": "" }, "require": { @@ -1101,7 +1104,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.7.1" }, "funding": [ { @@ -1117,7 +1120,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-03-27T12:30:47+00:00" }, { "name": "guzzlehttp/uri-template", @@ -1207,20 +1210,20 @@ }, { "name": "laravel/framework", - "version": "v12.1.1", + "version": "v12.20.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "9be5738f1ca1530055bb9d6db81f909a7ed34842" + "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/9be5738f1ca1530055bb9d6db81f909a7ed34842", - "reference": "9be5738f1ca1530055bb9d6db81f909a7ed34842", + "url": "https://api.github.com/repos/laravel/framework/zipball/1b9a00f8caf5503c92aa436279172beae1a484ff", + "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12", + "brick/math": "^0.11|^0.12|^0.13", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -1237,7 +1240,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.6", + "league/commonmark": "^2.7", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -1325,11 +1328,11 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "orchestra/testbench-core": "^10.0.0", - "pda/pheanstalk": "^5.0.6", + "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "predis/predis": "^2.3", + "predis/predis": "^2.3|^3.0", "resend/resend-php": "^0.10.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", @@ -1361,7 +1364,7 @@ "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", @@ -1418,20 +1421,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-03-05T15:31:19+00:00" + "time": "2025-07-08T15:02:21+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.5", + "version": "v0.3.6", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" + "reference": "86a8b692e8661d0fb308cec64f3d176821323077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", + "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077", "shasum": "" }, "require": { @@ -1475,22 +1478,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.5" + "source": "https://github.com/laravel/prompts/tree/v0.3.6" }, - "time": "2025-02-11T13:34:40+00:00" + "time": "2025-07-07T14:17:42+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.3", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f" + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f379c13663245f7aa4512a7869f62eb14095f23f", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", "shasum": "" }, "require": { @@ -1538,20 +1541,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-02-11T15:03:05+00:00" + "time": "2025-03-19T13:51:03+00:00" }, { "name": "league/commonmark", - "version": "2.6.1", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", "shasum": "" }, "require": { @@ -1580,7 +1583,7 @@ "symfony/process": "^5.4 | ^6.0 | ^7.0", "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", - "vimeo/psalm": "^4.24.0 || ^5.0.0" + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, "suggest": { "symfony/yaml": "v2.3+ required if using the Front Matter extension" @@ -1588,7 +1591,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.8-dev" } }, "autoload": { @@ -1645,7 +1648,7 @@ "type": "tidelift" } ], - "time": "2024-12-29T14:10:59+00:00" + "time": "2025-07-20T12:47:49+00:00" }, { "name": "league/config", @@ -1731,16 +1734,16 @@ }, { "name": "league/flysystem", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", "shasum": "" }, "require": { @@ -1764,13 +1767,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -1808,22 +1811,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" }, - "time": "2024-10-08T08:58:34+00:00" + "time": "2025-06-25T13:29:59+00:00" }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", "shasum": "" }, "require": { @@ -1857,9 +1860,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-05-21T10:34:19+00:00" }, { "name": "league/mime-type-detection", @@ -2093,16 +2096,16 @@ }, { "name": "monolog/monolog", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { @@ -2180,7 +2183,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.8.1" + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" }, "funding": [ { @@ -2192,20 +2195,20 @@ "type": "tidelift" } ], - "time": "2024-12-05T17:15:07+00:00" + "time": "2025-03-24T10:02:05+00:00" }, { "name": "nesbot/carbon", - "version": "3.8.6", + "version": "3.10.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd" + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd", - "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", "shasum": "" }, "require": { @@ -2213,9 +2216,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -2223,14 +2226,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.57.2", + "friendsofphp/php-cs-fixer": "^3.75.0", "kylekatarnls/multi-tester": "^2.5.3", - "ondrejmirtes/better-reflection": "^6.25.0.4", "phpmd/phpmd": "^2.15.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.11.2", - "phpunit/phpunit": "^10.5.20", - "squizlabs/php_codesniffer": "^3.9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" }, "bin": [ "bin/carbon" @@ -2298,7 +2300,7 @@ "type": "tidelift" } ], - "time": "2025-02-20T17:33:38+00:00" + "time": "2025-06-21T15:19:35+00:00" }, { "name": "nette/schema", @@ -2364,16 +2366,16 @@ }, { "name": "nette/utils", - "version": "v4.0.5", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", "shasum": "" }, "require": { @@ -2444,37 +2446,37 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.5" + "source": "https://github.com/nette/utils/tree/v4.0.7" }, - "time": "2024-08-07T15:39:19+00:00" + "time": "2025-06-03T04:55:08+00:00" }, { "name": "nunomaduro/termwind", - "version": "v2.3.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda" + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda", - "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.1.8" + "symfony/console": "^7.2.6" }, "require-dev": { - "illuminate/console": "^11.33.2", - "laravel/pint": "^1.18.2", + "illuminate/console": "^11.44.7", + "laravel/pint": "^1.22.0", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0", - "phpstan/phpstan": "^1.12.11", - "phpstan/phpstan-strict-rules": "^1.6.1", - "symfony/var-dumper": "^7.1.8", + "pestphp/pest": "^2.36.0 || ^3.8.2", + "phpstan/phpstan": "^1.12.25", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.2.6", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -2517,7 +2519,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.0" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" }, "funding": [ { @@ -2533,7 +2535,7 @@ "type": "github" } ], - "time": "2024-11-21T10:39:51+00:00" + "time": "2025-05-08T08:14:37+00:00" }, { "name": "phpoption/phpoption", @@ -3117,16 +3119,16 @@ }, { "name": "ramsey/collection", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { @@ -3187,27 +3189,26 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.1.0" + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "time": "2025-03-02T04:48:29+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { "name": "ramsey/uuid", - "version": "4.7.6", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", - "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", - "ext-json": "*", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -3215,26 +3216,23 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "captainhook/captainhook": "^5.10", + "captainhook/captainhook": "^5.25", "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "doctrine/annotations": "^1.8", - "ergebnis/composer-normalize": "^2.15", - "mockery/mockery": "^1.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.2", - "php-mock/php-mock-mockery": "^1.3", - "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^8.5 || ^9", - "ramsey/composer-repl": "^1.4", - "slevomat/coding-standard": "^8.4", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.9" + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", @@ -3269,23 +3267,13 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.6" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" - } - ], - "time": "2024-04-27T21:32:50+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "symfony/clock", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", @@ -3339,7 +3327,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.2.0" + "source": "https://github.com/symfony/clock/tree/v7.3.0" }, "funding": [ { @@ -3359,23 +3347,24 @@ }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -3432,7 +3421,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.3.1" }, "funding": [ { @@ -3448,11 +3437,11 @@ "type": "tidelift" } ], - "time": "2024-12-11T03:49:26+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/css-selector", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -3497,7 +3486,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + "source": "https://github.com/symfony/css-selector/tree/v7.3.0" }, "funding": [ { @@ -3517,16 +3506,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -3539,7 +3528,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3564,7 +3553,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3580,20 +3569,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/error-handler", - "version": "v7.2.4", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "aabf79938aa795350c07ce6464dd1985607d95d5" + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5", - "reference": "aabf79938aa795350c07ce6464dd1985607d95d5", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", "shasum": "" }, "require": { @@ -3606,9 +3595,11 @@ "symfony/http-kernel": "<6.4" }, "require-dev": { + "symfony/console": "^6.4|^7.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0" + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -3639,7 +3630,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.4" + "source": "https://github.com/symfony/error-handler/tree/v7.3.1" }, "funding": [ { @@ -3655,20 +3646,20 @@ "type": "tidelift" } ], - "time": "2025-02-02T20:27:07+00:00" + "time": "2025-06-13T07:48:40+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", "shasum": "" }, "require": { @@ -3719,7 +3710,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" }, "funding": [ { @@ -3735,20 +3726,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-04-22T09:11:45+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -3762,7 +3753,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3795,7 +3786,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -3811,20 +3802,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/finder", - "version": "v7.2.2", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", "shasum": "" }, "require": { @@ -3859,7 +3850,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.2" + "source": "https://github.com/symfony/finder/tree/v7.3.0" }, "funding": [ { @@ -3875,20 +3866,20 @@ "type": "tidelift" } ], - "time": "2024-12-30T19:00:17+00:00" + "time": "2024-12-30T19:00:26+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.2.3", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0" + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9", + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9", "shasum": "" }, "require": { @@ -3905,6 +3896,7 @@ "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", "symfony/expression-language": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", @@ -3937,7 +3929,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.2.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.1" }, "funding": [ { @@ -3953,20 +3945,20 @@ "type": "tidelift" } ], - "time": "2025-01-17T10:56:55+00:00" + "time": "2025-06-23T15:07:14+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.2.4", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9f1103734c5789798fefb90e91de4586039003ed" + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed", - "reference": "9f1103734c5789798fefb90e91de4586039003ed", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", "shasum": "" }, "require": { @@ -3974,8 +3966,8 @@ "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -4051,7 +4043,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.2.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.1" }, "funding": [ { @@ -4067,20 +4059,20 @@ "type": "tidelift" } ], - "time": "2025-02-26T11:01:22+00:00" + "time": "2025-06-28T08:24:55+00:00" }, { "name": "symfony/mailer", - "version": "v7.2.3", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3" + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3", - "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", "shasum": "" }, "require": { @@ -4131,7 +4123,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.2.3" + "source": "https://github.com/symfony/mailer/tree/v7.3.1" }, "funding": [ { @@ -4147,20 +4139,20 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/mime", - "version": "v7.2.4", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" + "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", + "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", "shasum": "" }, "require": { @@ -4215,7 +4207,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.4" + "source": "https://github.com/symfony/mime/tree/v7.3.0" }, "funding": [ { @@ -4231,11 +4223,11 @@ "type": "tidelift" } ], - "time": "2025-02-19T08:51:20+00:00" + "time": "2025-02-19T08:51:26+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4294,7 +4286,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -4314,16 +4306,16 @@ }, { "name": "symfony/polyfill-iconv", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956" + "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956", - "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/5f3b930437ae03ae5dff61269024d8ea1b3774aa", + "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa", "shasum": "" }, "require": { @@ -4374,7 +4366,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.32.0" }, "funding": [ { @@ -4390,11 +4382,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-17T14:58:18+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -4452,7 +4444,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -4472,16 +4464,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { @@ -4535,7 +4527,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" }, "funding": [ { @@ -4551,11 +4543,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4616,7 +4608,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -4636,19 +4628,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -4696,7 +4689,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -4712,20 +4705,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -4776,7 +4769,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -4792,11 +4785,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -4852,7 +4845,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -4872,7 +4865,7 @@ }, { "name": "symfony/polyfill-uuid", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -4931,7 +4924,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" }, "funding": [ { @@ -4951,16 +4944,16 @@ }, { "name": "symfony/process", - "version": "v7.2.4", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", "shasum": "" }, "require": { @@ -4992,7 +4985,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.4" + "source": "https://github.com/symfony/process/tree/v7.3.0" }, "funding": [ { @@ -5008,20 +5001,20 @@ "type": "tidelift" } ], - "time": "2025-02-05T08:33:46+00:00" + "time": "2025-04-17T09:11:12+00:00" }, { "name": "symfony/routing", - "version": "v7.2.3", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996" + "reference": "8e213820c5fea844ecea29203d2a308019007c15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996", - "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996", + "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", + "reference": "8e213820c5fea844ecea29203d2a308019007c15", "shasum": "" }, "require": { @@ -5073,7 +5066,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.2.3" + "source": "https://github.com/symfony/routing/tree/v7.3.0" }, "funding": [ { @@ -5089,20 +5082,20 @@ "type": "tidelift" } ], - "time": "2025-01-17T10:56:55+00:00" + "time": "2025-05-24T20:43:28+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -5120,7 +5113,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5156,7 +5149,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -5172,20 +5165,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", "shasum": "" }, "require": { @@ -5243,7 +5236,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.3.0" }, "funding": [ { @@ -5259,20 +5252,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2025-04-20T20:19:01+00:00" }, { "name": "symfony/translation", - "version": "v7.2.4", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" + "reference": "241d5ac4910d256660238a7ecf250deba4c73063" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", + "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063", "shasum": "" }, "require": { @@ -5282,6 +5275,7 @@ "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { + "nikic/php-parser": "<5.0", "symfony/config": "<6.4", "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", @@ -5295,7 +5289,7 @@ "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "nikic/php-parser": "^4.18|^5.0", + "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", "symfony/config": "^6.4|^7.0", "symfony/console": "^6.4|^7.0", @@ -5338,7 +5332,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.4" + "source": "https://github.com/symfony/translation/tree/v7.3.1" }, "funding": [ { @@ -5354,20 +5348,20 @@ "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", "shasum": "" }, "require": { @@ -5380,7 +5374,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -5416,7 +5410,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" }, "funding": [ { @@ -5432,20 +5426,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-27T08:32:26+00:00" }, { "name": "symfony/uid", - "version": "v7.2.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", "shasum": "" }, "require": { @@ -5490,7 +5484,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.2.0" + "source": "https://github.com/symfony/uid/tree/v7.3.1" }, "funding": [ { @@ -5506,24 +5500,25 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.2.3", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -5573,7 +5568,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.1" }, "funding": [ { @@ -5589,7 +5584,7 @@ "type": "tidelift" } ], - "time": "2025-01-17T11:39:41+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -5648,16 +5643,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "shasum": "" }, "require": { @@ -5716,7 +5711,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" }, "funding": [ { @@ -5728,7 +5723,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:52:34+00:00" + "time": "2025-04-30T23:37:27+00:00" }, { "name": "voku/portable-ascii", @@ -5998,16 +5993,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.8.3", + "version": "v7.8.4", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "a585c346ddf1bec22e51e20b5387607905604a71" + "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/a585c346ddf1bec22e51e20b5387607905604a71", - "reference": "a585c346ddf1bec22e51e20b5387607905604a71", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/130a9bf0e269ee5f5b320108f794ad03e275cad4", + "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4", "shasum": "" }, "require": { @@ -6016,26 +6011,26 @@ "ext-reflection": "*", "ext-simplexml": "*", "fidry/cpu-core-counter": "^1.2.0", - "jean85/pretty-package-versions": "^2.1.0", + "jean85/pretty-package-versions": "^2.1.1", "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "phpunit/php-code-coverage": "^11.0.9 || ^12.0.4", - "phpunit/php-file-iterator": "^5.1.0 || ^6", - "phpunit/php-timer": "^7.0.1 || ^8", - "phpunit/phpunit": "^11.5.11 || ^12.0.6", - "sebastian/environment": "^7.2.0 || ^8", - "symfony/console": "^6.4.17 || ^7.2.1", - "symfony/process": "^6.4.19 || ^7.2.4" + "phpunit/php-code-coverage": "^11.0.10", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-timer": "^7.0.1", + "phpunit/phpunit": "^11.5.24", + "sebastian/environment": "^7.2.1", + "symfony/console": "^6.4.22 || ^7.3.0", + "symfony/process": "^6.4.20 || ^7.3.0" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.6", - "phpstan/phpstan-deprecation-rules": "^2.0.1", - "phpstan/phpstan-phpunit": "^2.0.4", - "phpstan/phpstan-strict-rules": "^2.0.3", - "squizlabs/php_codesniffer": "^3.11.3", - "symfony/filesystem": "^6.4.13 || ^7.2.0" + "phpstan/phpstan": "^2.1.17", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.6", + "phpstan/phpstan-strict-rules": "^2.0.4", + "squizlabs/php_codesniffer": "^3.13.2", + "symfony/filesystem": "^6.4.13 || ^7.3.0" }, "bin": [ "bin/paratest", @@ -6075,7 +6070,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.8.3" + "source": "https://github.com/paratestphp/paratest/tree/v7.8.4" }, "funding": [ { @@ -6087,7 +6082,7 @@ "type": "paypal" } ], - "time": "2025-03-05T08:29:11+00:00" + "time": "2025-06-23T06:07:21+00:00" }, { "name": "composer/semver", @@ -6349,16 +6344,16 @@ }, { "name": "filp/whoops", - "version": "2.17.0", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e" + "reference": "59a123a3d459c5a23055802237cb317f609867e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/075bc0c26631110584175de6523ab3f1652eb28e", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", "shasum": "" }, "require": { @@ -6408,7 +6403,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.17.0" + "source": "https://github.com/filp/whoops/tree/2.18.3" }, "funding": [ { @@ -6416,24 +6411,24 @@ "type": "github" } ], - "time": "2025-01-25T12:00:00+00:00" + "time": "2025-06-16T00:02:10+00:00" }, { "name": "hamcrest/hamcrest-php", - "version": "v2.0.1", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" + "php": "^7.4|^8.0" }, "replace": { "cordoval/hamcrest-php": "*", @@ -6441,8 +6436,8 @@ "kodova/hamcrest-php": "*" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { @@ -6465,22 +6460,22 @@ ], "support": { "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2020-07-09T08:09:16+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { "name": "jean85/pretty-package-versions", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10" + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", - "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { @@ -6490,8 +6485,9 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", @@ -6524,22 +6520,22 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" }, - "time": "2024-11-18T16:19:46+00:00" + "time": "2025-03-19T14:43:43+00:00" }, { "name": "laravel/pail", - "version": "v1.2.2", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2" + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2", - "reference": "f31f4980f52be17c4667f3eafe034e6826787db2", + "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", "shasum": "" }, "require": { @@ -6559,7 +6555,7 @@ "orchestra/testbench-core": "^8.13|^9.0|^10.0", "pestphp/pest": "^2.20|^3.0", "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.12.27", "symfony/var-dumper": "^6.3|^7.0" }, "type": "library", @@ -6595,6 +6591,7 @@ "description": "Easily delve into your Laravel application's log files directly from the command line.", "homepage": "https://github.com/laravel/pail", "keywords": [ + "dev", "laravel", "logs", "php", @@ -6604,7 +6601,7 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-01-28T15:15:15+00:00" + "time": "2025-06-05T13:55:57+00:00" }, { "name": "laravel/tinker", @@ -6674,16 +6671,16 @@ }, { "name": "league/csv", - "version": "9.22.0", + "version": "9.24.1", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "afc109aa11f3086b8be8dfffa04ac31480b36b76" + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/afc109aa11f3086b8be8dfffa04ac31480b36b76", - "reference": "afc109aa11f3086b8be8dfffa04ac31480b36b76", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8", + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8", "shasum": "" }, "require": { @@ -6693,14 +6690,14 @@ "require-dev": { "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^3.69.0", - "phpbench/phpbench": "^1.4.0", - "phpstan/phpstan": "^1.12.18", + "friendsofphp/php-cs-fixer": "^3.75.0", + "phpbench/phpbench": "^1.4.1", + "phpstan/phpstan": "^1.12.27", "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-phpunit": "^1.4.2", "phpstan/phpstan-strict-rules": "^1.6.2", - "phpunit/phpunit": "^10.5.16 || ^11.5.7", - "symfony/var-dumper": "^6.4.8 || ^7.2.3" + "phpunit/phpunit": "^10.5.16 || ^11.5.22", + "symfony/var-dumper": "^6.4.8 || ^7.3.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", @@ -6761,7 +6758,7 @@ "type": "github" } ], - "time": "2025-02-28T10:00:39+00:00" + "time": "2025-06-25T14:53:51+00:00" }, { "name": "m6web/redis-mock", @@ -6897,39 +6894,38 @@ }, { "name": "mongodb/mongodb", - "version": "1.16.1", + "version": "1.21.1", "source": { "type": "git", "url": "https://github.com/mongodb/mongo-php-library.git", - "reference": "72d80889eb7567c0da4e7d4ddbdcf66dfea90ac3" + "reference": "37bc8df3a67ddf8380704a5ba5dbd00e92ec1f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/72d80889eb7567c0da4e7d4ddbdcf66dfea90ac3", - "reference": "72d80889eb7567c0da4e7d4ddbdcf66dfea90ac3", + "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/37bc8df3a67ddf8380704a5ba5dbd00e92ec1f6a", + "reference": "37bc8df3a67ddf8380704a5ba5dbd00e92ec1f6a", "shasum": "" }, "require": { - "ext-hash": "*", - "ext-json": "*", - "ext-mongodb": "^1.16.0", - "jean85/pretty-package-versions": "^2.0.1", - "php": "^7.2 || ^8.0", - "symfony/polyfill-php73": "^1.27", - "symfony/polyfill-php80": "^1.27", - "symfony/polyfill-php81": "^1.27" + "composer-runtime-api": "^2.0", + "ext-mongodb": "^1.21.0", + "php": "^8.1", + "psr/log": "^1.1.4|^2|^3" + }, + "replace": { + "mongodb/builder": "*" }, "require-dev": { - "doctrine/coding-standard": "^11.1", - "rector/rector": "^0.16.0", + "doctrine/coding-standard": "^12.0", + "phpunit/phpunit": "^10.5.35", + "rector/rector": "^1.2", "squizlabs/php_codesniffer": "^3.7", - "symfony/phpunit-bridge": "^5.2", - "vimeo/psalm": "^4.28" + "vimeo/psalm": "6.5.*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.16.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -6952,6 +6948,10 @@ { "name": "Jeremy Mikola", "email": "jmikola@gmail.com" + }, + { + "name": "Jérôme Tamarelle", + "email": "jerome.tamarelle@mongodb.com" } ], "description": "MongoDB driver library", @@ -6964,22 +6964,22 @@ ], "support": { "issues": "https://github.com/mongodb/mongo-php-library/issues", - "source": "https://github.com/mongodb/mongo-php-library/tree/1.16.1" + "source": "https://github.com/mongodb/mongo-php-library/tree/1.21.1" }, - "time": "2023-09-26T15:44:10+00:00" + "time": "2025-02-28T17:24:20+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -7018,7 +7018,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -7026,20 +7026,20 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "shasum": "" }, "require": { @@ -7082,44 +7082,45 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.6.1", + "version": "v8.8.2", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "86f003c132143d5a2ab214e19933946409e0cae7" + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/86f003c132143d5a2ab214e19933946409e0cae7", - "reference": "86f003c132143d5a2ab214e19933946409e0cae7", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", "shasum": "" }, "require": { - "filp/whoops": "^2.16.0", - "nunomaduro/termwind": "^2.3.0", + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", "php": "^8.2.0", - "symfony/console": "^7.2.1" + "symfony/console": "^7.3.0" }, "conflict": { - "laravel/framework": "<11.39.1 || >=13.0.0", - "phpunit/phpunit": "<11.5.3 || >=12.0.0" + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" }, "require-dev": { - "larastan/larastan": "^2.9.12", - "laravel/framework": "^11.39.1", - "laravel/pint": "^1.20.0", - "laravel/sail": "^1.40.0", - "laravel/sanctum": "^4.0.7", - "laravel/tinker": "^2.10.0", - "orchestra/testbench-core": "^9.9.2", - "pestphp/pest": "^3.7.3", - "sebastian/environment": "^6.1.0 || ^7.2.0" + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", "extra": { @@ -7182,42 +7183,43 @@ "type": "patreon" } ], - "time": "2025-01-23T13:41:43+00:00" + "time": "2025-06-25T02:12:12+00:00" }, { "name": "orchestra/canvas", - "version": "v10.0.1", + "version": "v10.0.2", "source": { "type": "git", "url": "https://github.com/orchestral/canvas.git", - "reference": "8665e96c254350484ded1cdf158765767abc7075" + "reference": "94f732350e5c6d7136ff7b0fd05a90079dd77deb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/canvas/zipball/8665e96c254350484ded1cdf158765767abc7075", - "reference": "8665e96c254350484ded1cdf158765767abc7075", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/94f732350e5c6d7136ff7b0fd05a90079dd77deb", + "reference": "94f732350e5c6d7136ff7b0fd05a90079dd77deb", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "composer/semver": "^3.0", - "illuminate/console": "^12.0", - "illuminate/database": "^12.0", - "illuminate/filesystem": "^12.0", - "illuminate/support": "^12.0", - "orchestra/canvas-core": "^10.0", - "orchestra/testbench-core": "^10.0", + "illuminate/console": "^12.3.0", + "illuminate/database": "^12.3.0", + "illuminate/filesystem": "^12.3.0", + "illuminate/support": "^12.3.0", + "orchestra/canvas-core": "^10.0.1", + "orchestra/sidekick": "^1.1.0", + "orchestra/testbench-core": "^10.1.0", "php": "^8.2", "symfony/polyfill-php83": "^1.31", - "symfony/yaml": "^7.0.3" + "symfony/yaml": "^7.2.0" }, "require-dev": { - "laravel/framework": "^12.0", - "laravel/pint": "^1.20", - "mockery/mockery": "^1.6.10", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^11.5.7", - "spatie/laravel-ray": "^1.39.1" + "laravel/framework": "^12.3.0", + "laravel/pint": "^1.21", + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.8", + "phpunit/phpunit": "^11.5.13", + "spatie/laravel-ray": "^1.40.1" }, "bin": [ "canvas" @@ -7252,9 +7254,9 @@ "description": "Code Generators for Laravel Applications and Packages", "support": { "issues": "https://github.com/orchestral/canvas/issues", - "source": "https://github.com/orchestral/canvas/tree/v10.0.1" + "source": "https://github.com/orchestral/canvas/tree/v10.0.2" }, - "time": "2025-02-15T11:42:39+00:00" + "time": "2025-04-05T16:01:25+00:00" }, { "name": "orchestra/canvas-core", @@ -7324,33 +7326,38 @@ }, { "name": "orchestra/sidekick", - "version": "v1.0.5", + "version": "v1.2.13", "source": { "type": "git", "url": "https://github.com/orchestral/sidekick.git", - "reference": "3e4a497ff3f9d9ab920fa92629d40abbbd07c5ce" + "reference": "aa41994f872cc49a420da42f50886605c0d85f15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/sidekick/zipball/3e4a497ff3f9d9ab920fa92629d40abbbd07c5ce", - "reference": "3e4a497ff3f9d9ab920fa92629d40abbbd07c5ce", + "url": "https://api.github.com/repos/orchestral/sidekick/zipball/aa41994f872cc49a420da42f50886605c0d85f15", + "reference": "aa41994f872cc49a420da42f50886605c0d85f15", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", - "php": "^8.0", - "symfony/polyfill-php83": "^1.31" + "php": "^8.1", + "symfony/polyfill-php83": "^1.32" }, "require-dev": { - "laravel/framework": "^9.52.16|^10.48.28|^11.42.1|^12.0|^13.0", + "fakerphp/faker": "^1.21", + "laravel/framework": "^10.48.29|^11.44.7|^12.1.1|^13.0", "laravel/pint": "^1.4", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^9.6|^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^8.37.0|^9.14.0|^10.0|^11.0", + "phpstan/phpstan": "^2.1.14", + "phpunit/phpunit": "^10.0|^11.0|^12.0", "symfony/process": "^6.0|^7.0" }, "type": "library", "autoload": { "files": [ + "src/Eloquent/functions.php", + "src/Http/functions.php", "src/functions.php" ], "psr-4": { @@ -7370,33 +7377,33 @@ "description": "Packages Toolkit Utilities and Helpers for Laravel", "support": { "issues": "https://github.com/orchestral/sidekick/issues", - "source": "https://github.com/orchestral/sidekick/tree/v1.0.5" + "source": "https://github.com/orchestral/sidekick/tree/v1.2.13" }, - "time": "2025-03-03T11:08:57+00:00" + "time": "2025-06-23T05:09:50+00:00" }, { "name": "orchestra/testbench", - "version": "v10.1.0", + "version": "v10.4.0", "source": { "type": "git", "url": "https://github.com/orchestral/testbench.git", - "reference": "51de04ba056871733baa5192be16fa83043eb23e" + "reference": "36674005fb1b5cddfd953b8c440507394af8695d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench/zipball/51de04ba056871733baa5192be16fa83043eb23e", - "reference": "51de04ba056871733baa5192be16fa83043eb23e", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/36674005fb1b5cddfd953b8c440507394af8695d", + "reference": "36674005fb1b5cddfd953b8c440507394af8695d", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "fakerphp/faker": "^1.23", - "laravel/framework": "^12.1.1", + "laravel/framework": "^12.8.0", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^10.1.0", - "orchestra/workbench": "^10.0.1", + "orchestra/testbench-core": "^10.4.0", + "orchestra/workbench": "^10.0.6", "php": "^8.2", - "phpunit/phpunit": "^11.5.3", + "phpunit/phpunit": "^11.5.3|^12.0.1", "symfony/process": "^7.2", "symfony/yaml": "^7.2", "vlucas/phpdotenv": "^5.6.1" @@ -7425,48 +7432,47 @@ ], "support": { "issues": "https://github.com/orchestral/testbench/issues", - "source": "https://github.com/orchestral/testbench/tree/v10.1.0" + "source": "https://github.com/orchestral/testbench/tree/v10.4.0" }, - "time": "2025-03-06T11:07:16+00:00" + "time": "2025-06-08T23:29:04+00:00" }, { "name": "orchestra/testbench-core", - "version": "v10.1.0", + "version": "v10.4.0", "source": { "type": "git", "url": "https://github.com/orchestral/testbench-core.git", - "reference": "1ce476ab753a235958fb0b16ea78ca8fe815d47d" + "reference": "d1c45a7be15c4d99fb7d48685b038dd39acc7b84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/1ce476ab753a235958fb0b16ea78ca8fe815d47d", - "reference": "1ce476ab753a235958fb0b16ea78ca8fe815d47d", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/d1c45a7be15c4d99fb7d48685b038dd39acc7b84", + "reference": "d1c45a7be15c4d99fb7d48685b038dd39acc7b84", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", - "orchestra/sidekick": "^1.0.5", + "orchestra/sidekick": "~1.1.16|^1.2.12", "php": "^8.2", "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-php83": "^1.31", - "symfony/polyfill-php84": "^1.31" + "symfony/polyfill-php83": "^1.32" }, "conflict": { "brianium/paratest": "<7.3.0|>=8.0.0", - "laravel/framework": "<12.1.1|>=13.0.0", - "laravel/serializable-closure": "<1.3.0|>=3.0.0", + "laravel/framework": "<12.8.0|>=13.0.0", + "laravel/serializable-closure": "<1.3.0|>=2.0.0 <2.0.3|>=3.0.0", "nunomaduro/collision": "<8.0.0|>=9.0.0", - "phpunit/phpunit": "<10.5.35|>=11.0.0 <11.5.3|12.0.0|>=12.1.0" + "phpunit/phpunit": "<10.5.35|>=11.0.0 <11.5.3|12.0.0|>=12.3.0" }, "require-dev": { "fakerphp/faker": "^1.24", - "laravel/framework": "^12.1.1", - "laravel/pint": "^1.21", - "laravel/serializable-closure": "^1.3|^2.0", + "laravel/framework": "^12.8.0", + "laravel/pint": "^1.22", + "laravel/serializable-closure": "^1.3|^2.0.4", "mockery/mockery": "^1.6.10", - "phpstan/phpstan": "^2.1", + "phpstan/phpstan": "^2.1.14", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "spatie/laravel-ray": "^1.39.1", + "spatie/laravel-ray": "^1.40.2", "symfony/process": "^7.2.0", "symfony/yaml": "^7.2.0", "vlucas/phpdotenv": "^5.6.1" @@ -7475,7 +7481,7 @@ "brianium/paratest": "Allow using parallel testing (^7.3).", "ext-pcntl": "Required to use all features of the console signal trapping.", "fakerphp/faker": "Allow using Faker for testing (^1.23).", - "laravel/framework": "Required for testing (^12.1.1).", + "laravel/framework": "Required for testing (^12.8.0).", "mockery/mockery": "Allow using Mockery for testing (^1.6).", "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^8.0).", "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^10.0).", @@ -7521,20 +7527,20 @@ "issues": "https://github.com/orchestral/testbench/issues", "source": "https://github.com/orchestral/testbench-core" }, - "time": "2025-03-06T10:17:18+00:00" + "time": "2025-06-08T04:36:36+00:00" }, { "name": "orchestra/workbench", - "version": "v10.0.1", + "version": "v10.0.6", "source": { "type": "git", "url": "https://github.com/orchestral/workbench.git", - "reference": "e2647e8d9d77c152421b31e8ecc967b11e55fda8" + "reference": "4e8a5a68200971ddb9ce4abf26488838bf5c0812" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/workbench/zipball/e2647e8d9d77c152421b31e8ecc967b11e55fda8", - "reference": "e2647e8d9d77c152421b31e8ecc967b11e55fda8", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/4e8a5a68200971ddb9ce4abf26488838bf5c0812", + "reference": "4e8a5a68200971ddb9ce4abf26488838bf5c0812", "shasum": "" }, "require": { @@ -7544,21 +7550,20 @@ "laravel/pail": "^1.2.2", "laravel/tinker": "^2.10.1", "nunomaduro/collision": "^8.6", - "orchestra/canvas": "^10.0.1", - "orchestra/sidekick": "^1.0.5", - "orchestra/testbench-core": "^10.1.0", + "orchestra/canvas": "^10.0.2", + "orchestra/sidekick": "^1.1.0", + "orchestra/testbench-core": "^10.2.1", "php": "^8.2", "symfony/polyfill-php83": "^1.31", - "symfony/polyfill-php84": "^1.31", "symfony/process": "^7.2", "symfony/yaml": "^7.2" }, "require-dev": { - "laravel/pint": "^1.21", - "mockery/mockery": "^1.6.10", - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^11.5.3", - "spatie/laravel-ray": "^1.39.1" + "laravel/pint": "^1.21.2", + "mockery/mockery": "^1.6.12", + "phpstan/phpstan": "^2.1.8", + "phpunit/phpunit": "^11.5.3|^12.0.1", + "spatie/laravel-ray": "^1.40.1" }, "suggest": { "ext-pcntl": "Required to use all features of the console signal trapping." @@ -7588,9 +7593,9 @@ ], "support": { "issues": "https://github.com/orchestral/workbench/issues", - "source": "https://github.com/orchestral/workbench/tree/v10.0.1" + "source": "https://github.com/orchestral/workbench/tree/v10.0.6" }, - "time": "2025-03-06T10:48:12+00:00" + "time": "2025-04-13T01:07:44+00:00" }, { "name": "phar-io/manifest", @@ -7712,16 +7717,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.21", + "version": "1.12.28", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "14276fdef70575106a3392a4ed553c06a984df28" + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/14276fdef70575106a3392a4ed553c06a984df28", - "reference": "14276fdef70575106a3392a4ed553c06a984df28", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", "shasum": "" }, "require": { @@ -7766,20 +7771,20 @@ "type": "github" } ], - "time": "2025-03-09T09:24:50+00:00" + "time": "2025-07-17T17:15:39+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.9", + "version": "11.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", "shasum": "" }, "require": { @@ -7836,15 +7841,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2025-02-25T13:26:39+00:00" + "time": "2025-06-18T08:56:18+00:00" }, { "name": "phpunit/php-file-iterator", @@ -8093,16 +8110,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.12", + "version": "11.5.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d42785840519401ed2113292263795eb4c0f95da" + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d42785840519401ed2113292263795eb4c0f95da", - "reference": "d42785840519401ed2113292263795eb4c0f95da", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/446d43867314781df7e9adf79c3ec7464956fd8f", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f", "shasum": "" }, "require": { @@ -8112,24 +8129,24 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.0", + "myclabs/deep-copy": "^1.13.3", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.9", + "phpunit/php-code-coverage": "^11.0.10", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.2", + "sebastian/code-unit": "^3.0.3", "sebastian/comparator": "^6.3.1", "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/exporter": "^6.3.0", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.0", + "sebastian/type": "^5.1.2", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" }, @@ -8174,7 +8191,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.27" }, "funding": [ { @@ -8185,25 +8202,33 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-03-07T07:31:03+00:00" + "time": "2025-07-11T04:10:06+00:00" }, { "name": "predis/predis", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" + "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", - "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "url": "https://api.github.com/repos/predis/predis/zipball/f49e13ee3a2a825631562aa0223ac922ec5d058b", + "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b", "shasum": "" }, "require": { @@ -8212,6 +8237,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.3", "phpstan/phpstan": "^1.9", + "phpunit/phpcov": "^6.0 || ^8.0", "phpunit/phpunit": "^8.0 || ^9.4" }, "suggest": { @@ -8234,7 +8260,7 @@ "role": "Maintainer" } ], - "description": "A flexible and feature-complete Redis client for PHP.", + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", "homepage": "http://github.com/predis/predis", "keywords": [ "nosql", @@ -8243,7 +8269,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.3.0" + "source": "https://github.com/predis/predis/tree/v2.4.0" }, "funding": [ { @@ -8251,20 +8277,20 @@ "type": "github" } ], - "time": "2024-11-21T20:00:02+00:00" + "time": "2025-04-30T15:16:02+00:00" }, { "name": "psy/psysh", - "version": "v0.12.7", + "version": "v0.12.9", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" + "reference": "1b801844becfe648985372cb4b12ad6840245ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", + "reference": "1b801844becfe648985372cb4b12ad6840245ace", "shasum": "" }, "require": { @@ -8328,9 +8354,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" }, - "time": "2024-12-10T01:58:33+00:00" + "time": "2025-06-23T02:35:06+00:00" }, { "name": "sebastian/cli-parser", @@ -8391,16 +8417,16 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { @@ -8436,7 +8462,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -8444,7 +8470,7 @@ "type": "github" } ], - "time": "2024-12-12T09:59:06+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -8709,23 +8735,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -8761,15 +8787,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", @@ -9149,16 +9187,16 @@ }, { "name": "sebastian/type", - "version": "5.1.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", "shasum": "" }, "require": { @@ -9194,7 +9232,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" }, "funding": [ { @@ -9202,7 +9240,7 @@ "type": "github" } ], - "time": "2024-09-17T13:12:04+00:00" + "time": "2025-03-18T13:35:50+00:00" }, { "name": "sebastian/version", @@ -9264,12 +9302,12 @@ "source": { "type": "git", "url": "https://github.com/spatie/phpunit-snapshot-assertions.git", - "reference": "2082100271eb006d4c9a5fd8efcf5fcb4fb236e9" + "reference": "0c720353252a41ba37bc92d6ce1464e79889a5f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/phpunit-snapshot-assertions/zipball/2082100271eb006d4c9a5fd8efcf5fcb4fb236e9", - "reference": "2082100271eb006d4c9a5fd8efcf5fcb4fb236e9", + "url": "https://api.github.com/repos/spatie/phpunit-snapshot-assertions/zipball/0c720353252a41ba37bc92d6ce1464e79889a5f1", + "reference": "0c720353252a41ba37bc92d6ce1464e79889a5f1", "shasum": "" }, "require": { @@ -9334,7 +9372,7 @@ "type": "custom" } ], - "time": "2025-02-10T09:21:04+00:00" + "time": "2025-07-02T23:46:10+00:00" }, { "name": "staabm/side-effects-detector", @@ -9493,246 +9531,18 @@ ], "time": "2025-02-25T21:36:38+00:00" }, - { - "name": "symfony/polyfill-php73", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php84", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", - "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php84\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T12:04:04+00:00" - }, { "name": "symfony/property-access", - "version": "v7.2.3", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "b28732e315d81fbec787f838034de7d6c9b2b902" + "reference": "518d15c8cca726ebe665dcd7154074584cf862e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/b28732e315d81fbec787f838034de7d6c9b2b902", - "reference": "b28732e315d81fbec787f838034de7d6c9b2b902", + "url": "https://api.github.com/repos/symfony/property-access/zipball/518d15c8cca726ebe665dcd7154074584cf862e8", + "reference": "518d15c8cca726ebe665dcd7154074584cf862e8", "shasum": "" }, "require": { @@ -9779,7 +9589,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.2.3" + "source": "https://github.com/symfony/property-access/tree/v7.3.1" }, "funding": [ { @@ -9795,26 +9605,27 @@ "type": "tidelift" } ], - "time": "2025-01-17T10:56:55+00:00" + "time": "2025-06-24T04:04:43+00:00" }, { "name": "symfony/property-info", - "version": "v7.2.3", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "dedb118fd588a92f226b390250b384d25f4192fe" + "reference": "90586acbf2a6dd13bee4f09f09111c8bd4773970" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/dedb118fd588a92f226b390250b384d25f4192fe", - "reference": "dedb118fd588a92f226b390250b384d25f4192fe", + "url": "https://api.github.com/repos/symfony/property-info/zipball/90586acbf2a6dd13bee4f09f09111c8bd4773970", + "reference": "90586acbf2a6dd13bee4f09f09111c8bd4773970", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/string": "^6.4|^7.0", - "symfony/type-info": "~7.1.9|^7.2.2" + "symfony/type-info": "~7.2.8|^7.3.1" }, "conflict": { "phpdocumentor/reflection-docblock": "<5.2", @@ -9864,7 +9675,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.2.3" + "source": "https://github.com/symfony/property-info/tree/v7.3.1" }, "funding": [ { @@ -9880,20 +9691,20 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/serializer", - "version": "v7.2.4", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "d3e6cd13f035e1061647f0144b5623a1e7e775ba" + "reference": "feaf837cedbbc8287986602223175d3fd639922d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/d3e6cd13f035e1061647f0144b5623a1e7e775ba", - "reference": "d3e6cd13f035e1061647f0144b5623a1e7e775ba", + "url": "https://api.github.com/repos/symfony/serializer/zipball/feaf837cedbbc8287986602223175d3fd639922d", + "reference": "feaf837cedbbc8287986602223175d3fd639922d", "shasum": "" }, "require": { @@ -9962,7 +9773,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v7.2.4" + "source": "https://github.com/symfony/serializer/tree/v7.3.1" }, "funding": [ { @@ -9978,28 +9789,32 @@ "type": "tidelift" } ], - "time": "2025-02-24T10:49:57+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/type-info", - "version": "v7.2.4", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "269344575181c326781382ed53f7262feae3c6a4" + "reference": "5fa6e25e4195e73ce9e457b521ac5e61ec271150" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/269344575181c326781382ed53f7262feae3c6a4", - "reference": "269344575181c326781382ed53f7262feae3c6a4", + "url": "https://api.github.com/repos/symfony/type-info/zipball/5fa6e25e4195e73ce9e457b521ac5e61ec271150", + "reference": "5fa6e25e4195e73ce9e457b521ac5e61ec271150", "shasum": "" }, "require": { "php": ">=8.2", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" }, "require-dev": { - "phpstan/phpdoc-parser": "^1.0|^2.0" + "phpstan/phpdoc-parser": "^1.30|^2.0" }, "type": "library", "autoload": { @@ -10037,7 +9852,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.2.4" + "source": "https://github.com/symfony/type-info/tree/v7.3.1" }, "funding": [ { @@ -10053,20 +9868,20 @@ "type": "tidelift" } ], - "time": "2025-02-25T15:19:41+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/yaml", - "version": "v7.2.3", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec" + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec", - "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", "shasum": "" }, "require": { @@ -10109,7 +9924,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.2.3" + "source": "https://github.com/symfony/yaml/tree/v7.3.1" }, "funding": [ { @@ -10125,7 +9940,7 @@ "type": "tidelift" } ], - "time": "2025-01-07T12:55:42+00:00" + "time": "2025-06-03T06:57:57+00:00" }, { "name": "theseer/tokenizer", diff --git a/doc/getting-started/README.md b/doc/getting-started/README.md index 11f28509b..e48169155 100644 --- a/doc/getting-started/README.md +++ b/doc/getting-started/README.md @@ -22,6 +22,17 @@ Accessing that endpoint in a browser or an API client such as [Postman](https:// the [Service Document](https://docs.oasis-open.org/odata/odata/v4.01/os/part1-protocol/odata-v4.01-os-part1-protocol.html#sec_ServiceDocumentRequest) that describes the services available at this endpoint. This will show an empty array of services at the moment. +### Register the Service Provider + +Lodata no longer registers its Laravel service provider automatically. To enable OData in your Laravel app, +you must manually register the provider in your `bootstrap/providers.php` file: + +```php +Flat3\Lodata\ServiceProvider::class, +``` + +> **Note:** If you're using a framework that integrates Lodata, this step may already be handled for you. In that case, you should skip registering the service provider manually. + ## Step 2: Discovery The first thing we'll try is exposing the data managed by an Eloquent model. diff --git a/doc/getting-started/endpoint.md b/doc/getting-started/endpoint.md index 68695a431..c37586201 100644 --- a/doc/getting-started/endpoint.md +++ b/doc/getting-started/endpoint.md @@ -9,6 +9,135 @@ By default, `flat3/lodata` exposes a **single global service endpoint**. However 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. +## Replacing the ServiceProvider + +To enable multiple service endpoints you need to replace `\Flat3\Lodata\ServiceProvider` with your own implementation that inspects the request path and boots the appropriate endpoint based on your configuration. + +Take this sample implementation: + +```php +mergeConfigFrom(__DIR__.'/../config.php', 'lodata'); + } + + public function boot() + { + if ($this->app->runningInConsole()) { + $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); + $this->bootServices(new Endpoint('')); + } + else { + $segments = explode('/', request()->path()); + + if ($segments[0] === config('lodata.prefix')) { + + $serviceUris = config('lodata.endpoints', []); + + if (0 === sizeof($serviceUris) || count($segments) === 1) { + $service = new Endpoint(''); + } + else if (array_key_exists($segments[1], $serviceUris)) { + $clazz = $serviceUris[$segments[1]]; + if (!class_exists($clazz)) { + throw new RuntimeException(sprintf('Endpoint class `%s` does not exist', $clazz)); + } + if (!is_subclass_of($clazz, ServiceEndpointInterface::class)) { + throw new RuntimeException(sprintf('Endpoint class `%s` must implement Flat3\\Lodata\\Interfaces\\ServiceEndpointInterface', $clazz)); + } + $service = new $clazz($segments[1]); + } + else { + $service = new Endpoint(''); + } + + $this->bootServices($service); + } + } + } + + private function bootServices(Endpoint $service): void + { + $this->app->instance(Endpoint::class, $service); + + $this->app->bind(DBAL::class, function (Application $app, array $args) { + return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); + }); + + $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); + + $model = $service->discover(new Model()); + assert($model instanceof Model); + + $this->app->instance(Model::class, $model); + + $this->app->alias(Model::class, 'lodata.model'); + + $this->app->bind(Response::class, function () { + return Kernel::VERSION_ID < 60000 ? new Symfony\Response5() : new Symfony\Response6(); + }); + + $this->app->bind(Filesystem::class, function () { + return class_exists('League\Flysystem\Adapter\Local') ? new Flysystem\Flysystem1() : new Flysystem\Flysystem3(); + }); + + $route = $service->route(); + $middleware = config('lodata.middleware', []); + + Route::get("{$route}/_lodata/odata.pbids", [PBIDS::class, 'get']); + Route::get("{$route}/_lodata/{identifier}.odc", [ODCFF::class, 'get']); + Route::resource("{$route}/_lodata/monitor", Monitor::class); + Route::any("{$route}{path}", [OData::class, 'handle'])->where('path', '(.*)')->middleware($middleware); + } +} +``` + +Register your new provider in `bootstrap/providers.php` instead of the original one. + +```php + [ + 'projects' => \App\Endpoints\ProjectEndpoint::class, + 'hr' => \App\Endpoints\HrEndpoint::class, +], +``` + +This setup enables segmented endpoint support for your Laravel app, while keeping you in full control of the boot logic and route behavior. + ## Defining Multiple Endpoints You can define service endpoints by registering them in your `config/lodata.php` configuration file: diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index b7400387e..c7692d700 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,7 +4,6 @@ namespace Flat3\Lodata; -use RuntimeException; use Composer\InstalledVersions; use Flat3\Lodata\Controller\Monitor; use Flat3\Lodata\Controller\OData; @@ -15,7 +14,6 @@ use Flat3\Lodata\Helper\Flysystem; use Flat3\Lodata\Helper\DBAL; use Flat3\Lodata\Helper\Symfony; -use Flat3\Lodata\Interfaces\ServiceEndpointInterface; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Kernel; @@ -45,47 +43,11 @@ public function boot() { if ($this->app->runningInConsole()) { $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); - $this->bootServices(new Endpoint('')); - } - else { - // Let’s examine the request path - $segments = explode('/', request()->path()); - - // we only kick off operation when path prefix is configured in lodata.php - // and bypass all other routes for performance - if ($segments[0] === config('lodata.prefix')) { - - // next look up the configured service endpoints - $serviceUris = config('lodata.endpoints', []); - - if (0 === sizeof($serviceUris) || count($segments) === 1) { - // when no locators are defined, or the global locator ist requested, - // enter global mode; this will ensure compatibility with prior - // versions of this package - $service = new Endpoint(''); - } - else if (array_key_exists($segments[1], $serviceUris)) { - $clazz = $serviceUris[$segments[1]]; - if (!class_exists($clazz)) { - throw new RuntimeException(sprintf('Endpoint class `%s` does not exist', $clazz)); - } - if (!is_subclass_of($clazz, ServiceEndpointInterface::class)) { - throw new RuntimeException(sprintf('Endpoint class `%s` must implement Flat3\\Lodata\\Interfaces\\ServiceEndpointInterface', $clazz)); - } - $service = new $clazz($segments[1]); - } - else { - // when no service definition could be found for the path segment, - // we assume global scope - $service = new Endpoint(''); - } - - $this->bootServices($service); - } } + $this->bootServices(new Endpoint('')); } - private function bootServices($service): void + private function bootServices(Endpoint $service): void { // register the $service, which is a singleton, with the container; this allows us // to fulfill all old ServiceProvider::route() and ServiceProvider::endpoint() From d5f9a33173d9c04ef88d963ec8a798c38e8e0c21 Mon Sep 17 00:00:00 2001 From: Chris Lloyd Date: Mon, 2 Dec 2024 11:34:32 +0000 Subject: [PATCH 17/34] WIP Rebased --- .github/workflows/tests.yml | 38 +++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b584366ce..e5bedc663 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,16 +18,30 @@ jobs: php: "8.4" - laravel: 12 php: "8.3" + - laravel: 10 + php: "8.2" - - laravel: 11 + - laravel: 9 php: "8.2" + - laravel: 9 + php: "8.1" + - laravel: 9 + php: "8.0" + + - laravel: 8 + php: "8.1" + - laravel: 8 + php: "8.0" + - laravel: 8 + php: "7.4" + - laravel: 8 + php: "7.3" name: PHP ${{ matrix.php }} / Laravel ${{ matrix.laravel }} steps: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mongodb-1.21.0 - name: Composer run: | rm composer.lock @@ -35,7 +49,7 @@ jobs: - name: Test env: CREATE_SNAPSHOTS: false - run: composer test -- --exclude-group redis --exclude-group mongo + run: composer test -- --exclude-group redis,mongo sqlsrv: runs-on: ubuntu-22.04 @@ -46,7 +60,7 @@ jobs: run: docker run --detach -p 1433:1433 -e SA_PASSWORD=Your_password123 -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server - uses: shivammathur/setup-php@v2 with: - extensions: pdo_sqlsrv, odbc, pdo_odbc, mongodb-1.21.0 + extensions: pdo_sqlsrv odbc pdo_odbc - name: Composer run: composer update - name: Wait for containers @@ -55,7 +69,7 @@ jobs: env: CREATE_SNAPSHOTS: false DATABASE_URL: sqlsrv://sa:Your_password123@localhost:1433/msdb - run: composer test -- --group sql --group eloquent + run: composer test -- --group sql,eloquent postgres: runs-on: ubuntu-22.04 @@ -66,7 +80,7 @@ jobs: run: docker run --detach -p 5432:5432 -e POSTGRES_PASSWORD=my-secret-pw postgres:latest postgres -c shared_buffers=256MB -c max_connections=2000 - uses: shivammathur/setup-php@v2 with: - extensions: pdo_pgsql, mongodb-1.21.0 + extensions: pdo_pgsql - name: Composer run: composer update - name: Wait for containers @@ -76,7 +90,7 @@ jobs: CREATE_SNAPSHOTS: false DATABASE_URL: pgsql://postgres:my-secret-pw@localhost:5432/postgres?charset=utf8 run: - composer test -- --group sql --group eloquent + composer test -- --group sql,eloquent mysql: runs-on: ubuntu-22.04 @@ -87,7 +101,7 @@ jobs: run: docker run --detach -p 3306:3306 -e MYSQL_ROOT_PASSWORD=my-secret-pw -e MYSQL_DATABASE=testing mysql:latest mysqld --max-connections=8000 - uses: shivammathur/setup-php@v2 with: - extensions: pdo_mysql, mongodb-1.21.0 + extensions: pdo_mysql - name: Composer run: composer update - name: Wait for containers @@ -96,7 +110,7 @@ jobs: env: CREATE_SNAPSHOTS: false DATABASE_URL: mysql://root:my-secret-pw@127.0.0.1:3306/testing - run: composer test -- --group sql --group eloquent + run: composer test -- --group sql,eloquent mongo: runs-on: ubuntu-22.04 @@ -107,7 +121,7 @@ jobs: run: docker run --detach -p 27017:27017 mongo - uses: shivammathur/setup-php@v2 with: - extensions: mongodb-1.21.0 + extensions: mongodb - name: Composer run: composer update - name: Wait for containers @@ -124,7 +138,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - extensions: redis, mongodb-1.21.0 + extensions: redis - name: Composer run: composer update - name: Test @@ -142,7 +156,7 @@ jobs: run: echo "GIT_COMMITTED_AT=$(git log -1 --pretty=format:%ct)" >> $GITHUB_ENV - uses: shivammathur/setup-php@v2 with: - extensions: pdo_pgsql, mongodb-1.21.0 + extensions: pdo_pgsql, mongodb - name: Containers run: | docker run --detach -p 5432:5432 -e POSTGRES_PASSWORD=my-secret-pw postgres:latest postgres -c shared_buffers=256MB -c max_connections=2000 From f1fd518fe79dffddfa8776892398566e6b93aa2b Mon Sep 17 00:00:00 2001 From: Chris Lloyd Date: Sun, 16 Feb 2025 09:45:01 +0000 Subject: [PATCH 18/34] WIP rebased --- src/ServiceProvider.php | 56 ++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index c7692d700..958c05255 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -14,20 +14,36 @@ use Flat3\Lodata\Helper\Flysystem; use Flat3\Lodata\Helper\DBAL; use Flat3\Lodata\Helper\Symfony; +use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Kernel; /** * Service Provider - * - * https://:///$metadata - * * @link https://laravel.com/docs/8.x/providers * @package Flat3\Lodata */ class ServiceProvider extends \Illuminate\Support\ServiceProvider { + /** + * Get the endpoint of the OData service document + * @return string + */ + public static function endpoint(): string + { + return url(self::route()).'/'; + } + + /** + * Get the configured route prefix + * @return string + */ + public static function route(): string + { + return rtrim(config('lodata.prefix'), '/'); + } + /** * Service provider registration method */ @@ -44,32 +60,16 @@ public function boot() if ($this->app->runningInConsole()) { $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); } - $this->bootServices(new Endpoint('')); - } - - private function bootServices(Endpoint $service): void - { - // register the $service, which is a singleton, with the container; this allows us - // to fulfill all old ServiceProvider::route() and ServiceProvider::endpoint() - // calls with app()->make(ODataService::class)->route() or - // app()->make(ODataService::class)->endpoint() - $this->app->instance(Endpoint::class, $service); - - $this->app->bind(DBAL::class, function (Application $app, array $args) { - return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); - }); $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); - // next instantiate and discover the global Model - $model = $service->discover(new Model()); - assert($model instanceof Model); - - // and register it with the container - $this->app->instance(Model::class, $model); + $this->app->singleton(Model::class, function () { + return new Model(); + }); - // register alias - $this->app->alias(Model::class, 'lodata.model'); + $this->app->bind('lodata.model', function ($app) { + return $app->make(Model::class); + }); $this->app->bind(Response::class, function () { return Kernel::VERSION_ID < 60000 ? new Symfony\Response5() : new Symfony\Response6(); @@ -79,7 +79,11 @@ private function bootServices(Endpoint $service): void return class_exists('League\Flysystem\Adapter\Local') ? new Flysystem\Flysystem1() : new Flysystem\Flysystem3(); }); - $route = $service->route(); + $this->app->bind(DBAL::class, function (Application $app, array $args) { + return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); + }); + + $route = self::route(); $middleware = config('lodata.middleware', []); Route::get("{$route}/_lodata/odata.pbids", [PBIDS::class, 'get']); From b83c119ccf6a2f75fae0fc38c6616f9af713d87c Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Mon, 4 Nov 2024 10:43:03 +0100 Subject: [PATCH 19/34] Added Endpoint Discovery --- doc/getting-started/endpoint.md | 446 +------------------------- src/Controller/ODCFF.php | 2 +- src/Controller/PBIDS.php | 2 +- src/Controller/Transaction.php | 4 +- src/Endpoint.php | 42 +-- src/Entity.php | 2 +- src/Model.php | 4 +- src/PathSegment/Batch/JSON.php | 2 +- src/PathSegment/OpenAPI.php | 2 +- src/ServiceProvider.php | 76 +++-- src/Transaction/MultipartDocument.php | 2 +- 11 files changed, 101 insertions(+), 483 deletions(-) diff --git a/doc/getting-started/endpoint.md b/doc/getting-started/endpoint.md index c37586201..64ba2aa76 100644 --- a/doc/getting-started/endpoint.md +++ b/doc/getting-started/endpoint.md @@ -1,457 +1,43 @@ - # Service Endpoints -> **Prerequisite**: You’ve already published the `lodata.php` config file into your Laravel project using `php artisan vendor:publish`. - -## Overview - -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. - -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. - -## Replacing the ServiceProvider - -To enable multiple service endpoints you need to replace `\Flat3\Lodata\ServiceProvider` with your own implementation that inspects the request path and boots the appropriate endpoint based on your configuration. - -Take this sample implementation: - -```php -mergeConfigFrom(__DIR__.'/../config.php', 'lodata'); - } - - public function boot() - { - if ($this->app->runningInConsole()) { - $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); - $this->bootServices(new Endpoint('')); - } - else { - $segments = explode('/', request()->path()); - - if ($segments[0] === config('lodata.prefix')) { - - $serviceUris = config('lodata.endpoints', []); - - if (0 === sizeof($serviceUris) || count($segments) === 1) { - $service = new Endpoint(''); - } - else if (array_key_exists($segments[1], $serviceUris)) { - $clazz = $serviceUris[$segments[1]]; - if (!class_exists($clazz)) { - throw new RuntimeException(sprintf('Endpoint class `%s` does not exist', $clazz)); - } - if (!is_subclass_of($clazz, ServiceEndpointInterface::class)) { - throw new RuntimeException(sprintf('Endpoint class `%s` must implement Flat3\\Lodata\\Interfaces\\ServiceEndpointInterface', $clazz)); - } - $service = new $clazz($segments[1]); - } - else { - $service = new Endpoint(''); - } - - $this->bootServices($service); - } - } - } - - private function bootServices(Endpoint $service): void - { - $this->app->instance(Endpoint::class, $service); - - $this->app->bind(DBAL::class, function (Application $app, array $args) { - return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); - }); - - $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); - - $model = $service->discover(new Model()); - assert($model instanceof Model); - - $this->app->instance(Model::class, $model); - - $this->app->alias(Model::class, 'lodata.model'); - - $this->app->bind(Response::class, function () { - return Kernel::VERSION_ID < 60000 ? new Symfony\Response5() : new Symfony\Response6(); - }); - - $this->app->bind(Filesystem::class, function () { - return class_exists('League\Flysystem\Adapter\Local') ? new Flysystem\Flysystem1() : new Flysystem\Flysystem3(); - }); - - $route = $service->route(); - $middleware = config('lodata.middleware', []); - - Route::get("{$route}/_lodata/odata.pbids", [PBIDS::class, 'get']); - Route::get("{$route}/_lodata/{identifier}.odc", [ODCFF::class, 'get']); - Route::resource("{$route}/_lodata/monitor", Monitor::class); - Route::any("{$route}{path}", [OData::class, 'handle'])->where('path', '(.*)')->middleware($middleware); - } -} -``` - -Register your new provider in `bootstrap/providers.php` instead of the original one. - -```php - [ - 'projects' => \App\Endpoints\ProjectEndpoint::class, - 'hr' => \App\Endpoints\HrEndpoint::class, -], -``` - -This setup enables segmented endpoint support for your Laravel app, while keeping you in full control of the boot logic and route behavior. - -## Defining Multiple Endpoints - -You can define service endpoints by registering them in your `config/lodata.php` configuration file: +Each of your modules could register its own service endpoint with an `\Flat3\Lodata\Endpoint` like this: ```php /** - * At the end of config/lodata.php + * At the end of `config/lodata.php` */ 'endpoints' => [ - 'projects' => \App\Projects\ProjectEndpoint::class, + 'projects' ⇒ \App\Projects\ProjectEndpoint::class, ], ``` -With this configuration, a separate `$metadata` document becomes available at: +With that configuration a separate `$metadata` service file will be available via `https://://projects/$metadata`. -``` -https://://projects/$metadata -``` - -If the `endpoints` array is left empty (the default), only a single global endpoint is created under the configured `lodata.prefix`. +If the `endpoints` array stays empty (the default), only one global service endpoint is created. -## Endpoint Discovery +## Selective Discovery -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. - -This gives you fine-grained control over what each endpoint exposes. +With endpoints, you can now discover all your entities and annotations in a separate class via the `discover` function. ```php -use App\Models\Contact; +use App\Model\Contact; use Flat3\Lodata\Model; /** - * Discover schema elements and annotations for the service endpoint. + * Discovers Schema and Annotations of the `$metadata` file for + * the service. */ public function discover(Model $model): Model { - // Register all exposed entity sets or types - $model->discover(Contact::class); - // Add more types or annotations here... - + // register all of your $metadata capabilities + $model->discover(Contact::class); + … return $model; } ``` -### Performance Benefit - -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. - -> ✅ This optimization also applies to the **default (global) service endpoint** — its `discover()` method is likewise only evaluated on-demand during OData requests. - -This design keeps your application performant, especially in modular or multi-endpoint setups, by avoiding unnecessary processing for unrelated HTTP traffic. - -## Serving Pre-Generated $metadata Files - -In addition to dynamic schema generation, you can optionally serve a **pre-generated `$metadata.xml` file**. This is especially useful when: - -- You want to include **custom annotations** that are not easily represented in PHP code. -- You have **external tools** that generate the schema. -- You prefer **fine-tuned control** over the metadata document. - -To enable this, implement the `cachedMetadataXMLPath()` method in your endpoint class: - -```php -public function cachedMetadataXMLPath(): ?string -{ - return base_path('odata/metadata-projects.xml'); -} -``` - -If this method returns a valid file path, `lodata` will serve this file directly when `$metadata` is requested, bypassing the `discover()` logic. - -If it returns `null` (default), the schema will be generated dynamically from the `discover()` method. - -## Summary - -| Feature | Dynamic (`discover`) | Static (`cachedMetadataXMLPath`) | -|--------------------------|----------------------|-----------------------------------| -| Schema definition | In PHP | In XML file | -| Supports annotations | Basic | Full (manual control) | -| Performance optimized | Yes | Yes | -| Best for | Laravel-native setup | SAP integration, fine-tuned CSDL | - -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. - -## Sample: Defining a `ProjectEndpoint` - -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. - -### Step 1: Define the Custom Endpoint Class - -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. - -Here’s a minimal yet complete example: - -```php - element of $metadata. - */ - public function namespace(): string - { - return 'ProjectService'; - } - - /** - * Optionally return a static metadata XML file. - * If null, dynamic discovery via discover() is used. - */ - public function cachedMetadataXMLPath(): ?string - { - return resource_path('meta/ProjectService.xml'); - } - - /** - * Register entities and types to expose through this endpoint. - */ - public function discover(Model $model): Model - { - $model->discover(Project::class); - - return $model; - } -} -``` - -> ✅ **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. - -### Step 2: Register the Endpoint and Define Its URI Prefix - -In your `config/lodata.php`, register the custom endpoint under the `endpoints` array: - -```php -'endpoints' => [ - 'projects' => \App\Endpoints\ProjectEndpoint::class, -], -``` - -> 🧩 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`. - -This results in: - -- `$metadata` available at: - `https://://projects/$metadata` - -- Entity sets exposed through: - `https://://projects/Projects` - -This convention gives you **clear, readable URLs** and enables **modular, multi-service APIs** without extra routing configuration. - -### Step 3: Serve Dynamic or Static Metadata - -The framework will: - -- Call `cachedMetadataXMLPath()` first. - If a file path is returned and the file exists, it will serve that file directly. -- Otherwise, it will fall back to the `discover()` method to dynamically register entities, types, and annotations. - -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. - -## ✅ What You Get - -With just a few lines of configuration, you now have: - -- A **cleanly separated OData service** for the `Project` module. -- **Independent metadata** for documentation and integration. -- A fast and **on-demand schema bootstrapping** process. -- Full **control over discoverability** and **extensibility**. - -You can now repeat this pattern for other domains (e.g., `contacts`, `finance`, `hr`) to keep your OData services modular, testable, and scalable. - -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: - -## How Everything Connects - -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. - -### Flow Overview - -``` -[ config/lodata.php ] → [ ProjectEndpoint class ] - │ │ - ▼ ▼ - 'projects' => ProjectEndpoint::class ──► defines: - - namespace() - - discover() - - cachedMetadataXMLPath() - - │ │ - ▼ ▼ - URI: /odata/projects/$metadata OData Schema (XML or dynamic) -``` - -### The Building Blocks - -| Component | Purpose | -|-----------------------------------|-------------------------------------------------------------------------| -| **`config/lodata.php`** | Registers all endpoints and defines the URI prefix for each one | -| **Key: `'projects'`** | Becomes part of the URL: `/odata/projects/` | -| **`ProjectEndpoint` class** | Defines what the endpoint serves and how | -| **`namespace()`** | Injects the `` into `$metadata` | -| **`discover(Model $model)`** | Dynamically registers entities like `Project::class` | -| **`cachedMetadataXMLPath()`** | Optionally returns a pre-generated CSDL XML file | -| **OData request** | Triggers loading of this endpoint’s metadata and data | - -## Example: Request Lifecycle - -Let’s break down how the enhanced flow would look for an actual **entity set access**, such as - -``` -GET /odata/projects/Costcenters -``` - -This is about a **data request** for a specific entity set. Here's how the full lifecycle plays out. From config to response. - -### Enhanced Flow for `/odata/projects/Costcenters` - -``` - ┌─────────────────────────────────────────────────────┐ - │ HTTP GET /odata/projects/Costcenters │ - └─────────────────────────────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────┐ [Routing Layer] matches -│ config/lodata.php │── 'projects' key -│ │ -│ 'projects' => ProjectEndpoint::class, │ -└────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────────────────────────┐ - │ New ProjectEndpoint instance │ - └──────────────────────────────────────┘ - │ - (cachedMetadataXMLPath() not used here) - │ - ▼ - ┌───────────────────────────────────────────────┐ - │ discover(Model $model) is invoked │ - │ → model->discover(Project::class) │ - │ → model->discover(Costcenter::class) │ - └───────────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────────────────────────┐ - │ Lodata resolves the URI segment: │ - │ `Costcenters` │ - └──────────────────────────────────────┘ - │ - (via the registered EntitySet name for Costcenter) - │ - ▼ - ┌───────────────────────────────────────────────┐ - │ Query engine builds and executes the query │ - │ using the underlying Eloquent model │ - └───────────────────────────────────────────────┘ - │ - ▼ - ┌────────────────────────────────────────────┐ - │ Response is serialized into JSON or XML │ - │ according to Accept header │ - └────────────────────────────────────────────┘ - │ - ▼ - 🔁 JSON (default) or Atom/XML payload with Costcenter entities - -``` - -### What Must Be in Place for This to Work - -| Requirement | Description | -|-----------------------------------------------|-----------------------------------------------------------------------------| -| `ProjectEndpoint::discover()` | Must register `Costcenter::class` via `$model->discover(...)` | -| `Costcenter` model | Can be a **standard Laravel Eloquent model** – no special base class needed | -| `EntitySet` name | Must match the URI segment: `Costcenters` | -| URI case sensitivity | Lodata uses the identifier names → ensure entity names match URI segments | -| Accept header | Optional – defaults to JSON if none is provided | - -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: - -## What Modular Service Endpoints Enable - -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: - -- **Modular APIs** — Define multiple endpoints, each exposing only the entities and operations relevant to a specific domain (e.g., `projects`, `contacts`, `finance`). -- **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. -- **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. -- **Schema isolation** — Maintain clean separation between domains, clients, or API versions. For example: - - `/odata/projects/$metadata` → `ProjectService` schema - - `/odata/finance/$metadata` → `FinanceService` schema -- **Mix and match discovery strategies** — Use dynamic schema generation via Eloquent models or inject precise, curated metadata with static CSDL files. -- **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. - -### ✅ In Short - -Modular service endpoints allow you to: - -- Keep your domains cleanly separated -- Scale your API by feature, client, or team -- Provide tailored metadata per endpoint -- Mix dynamic discovery with pre-defined XML schemas -- Integrate smoothly into your Laravel app — no magic, just configuration and conventions - -They’re not just a convenience, they’re a foundation for **clean, scalable, and maintainable OData APIs**. +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. diff --git a/src/Controller/ODCFF.php b/src/Controller/ODCFF.php index e42fbf473..6ae19860a 100644 --- a/src/Controller/ODCFF.php +++ b/src/Controller/ODCFF.php @@ -167,7 +167,7 @@ public function get(string $identifier): Response $formula = $mashupDoc->createElement('Formula'); $formulaContent = $mashupDoc->createCDATASection(sprintf( 'let Source = OData.Feed("%1$s", null, [Implementation="2.0"]), %2$s_table = Source{[Name="%2$s",Signature="table"]}[Data] in %2$s_table', - app(Endpoint::class)->endpoint(), + app()->make(Endpoint::class)->endpoint(), $resourceId, )); $formula->appendChild($formulaContent); diff --git a/src/Controller/PBIDS.php b/src/Controller/PBIDS.php index 61f9178d6..69e46ce30 100644 --- a/src/Controller/PBIDS.php +++ b/src/Controller/PBIDS.php @@ -44,7 +44,7 @@ public function get(): Response 'details' => [ 'protocol' => 'odata', 'address' => [ - 'url' => app(Endpoint::class)->endpoint(), + 'url' => app()->make(Endpoint::class)->endpoint(), ], ], ], diff --git a/src/Controller/Transaction.php b/src/Controller/Transaction.php index a6367624f..40faf4dce 100644 --- a/src/Controller/Transaction.php +++ b/src/Controller/Transaction.php @@ -785,7 +785,7 @@ public function getPath(): string */ public function getRequestPath(): string { - $route = app(Endpoint::class)->route(); + $route = app()->make(Endpoint::class)->route(); return Str::substr($this->request->path(), strlen($route)); } @@ -963,7 +963,7 @@ public function getContextUrl(): string */ public static function getResourceUrl(): string { - return app(Endpoint::class)->endpoint(); + return app()->make(Endpoint::class)->endpoint(); } /** diff --git a/src/Endpoint.php b/src/Endpoint.php index a7e6eed44..b8fb6a0aa 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -2,34 +2,39 @@ namespace Flat3\Lodata; -use Flat3\Lodata\Interfaces\ServiceEndpointInterface; - -class Endpoint implements ServiceEndpointInterface +class Endpoint { + /** + * @var string $serviceUri the <service-uri> of the Flat3\Lodata\Model. + */ protected $serviceUri; + /** + * @var string $route the route prefix configured in 'lodata.prefix' + */ protected $route; + /** + * @var string $endpoint the full url to the ODataService endpoint + */ protected $endpoint; public function __construct(string $serviceUri) { - $this->serviceUri = trim($serviceUri, '/'); + $this->serviceUri = rtrim($serviceUri, '/'); $prefix = rtrim(config('lodata.prefix'), '/'); - - $this->route = ('' === $this->serviceUri) + $this->route = ('' === $serviceUri) ? $prefix : $prefix . '/' . $this->serviceUri; $this->endpoint = url($this->route) . '/'; } - public function serviceUri(): string - { - return $this->serviceUri; - } - + /** + * @return string the path within the odata URI space, like in + * https://:///$metadata + */ public function endpoint(): string { return $this->endpoint; @@ -40,18 +45,13 @@ public function route(): string return $this->route; } - public function namespace(): string - { - return config('lodata.namespace'); - } - - public function cachedMetadataXMLPath(): ?string - { - return null; - } - + /** + * Discovers Schema and Annotations of the `$metadata` file for + * the service. + */ public function discover(Model $model): Model { + // override this function to register all of your $metadata capabilities return $model; } } \ No newline at end of file diff --git a/src/Entity.php b/src/Entity.php index 8c1067407..efb76f975 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -178,7 +178,7 @@ public static function pipe( } $entityId = $id->getValue(); - $endpoint = app(Endpoint::class)->endpoint(); + $endpoint = app()->make(Endpoint::class)->endpoint(); if (Str::startsWith($entityId, $endpoint)) { $entityId = Str::substr($entityId, strlen($endpoint)); } diff --git a/src/Model.php b/src/Model.php index 1c1bdd81e..c58be6661 100644 --- a/src/Model.php +++ b/src/Model.php @@ -199,7 +199,7 @@ public function getTypeDefinition(string $name): ?Type */ public static function getNamespace(): string { - return app(Endpoint::class)->namespace(); + return config('lodata.namespace'); } /** @@ -344,7 +344,7 @@ public function discover($discoverable): self */ public function getEndpoint(): string { - return app(Endpoint::class)->endpoint(); + return app()->make(Endpoint::class)->endpoint(); } /** diff --git a/src/PathSegment/Batch/JSON.php b/src/PathSegment/Batch/JSON.php index 9c012d0f1..048b90238 100644 --- a/src/PathSegment/Batch/JSON.php +++ b/src/PathSegment/Batch/JSON.php @@ -59,7 +59,7 @@ public function emitJson(Transaction $transaction): void $requestURI = $requestData['url']; - $endpoint = app(Endpoint::class)->endpoint(); + $endpoint = app()->make(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( diff --git a/src/PathSegment/OpenAPI.php b/src/PathSegment/OpenAPI.php index b1657d9ab..5df9f3215 100644 --- a/src/PathSegment/OpenAPI.php +++ b/src/PathSegment/OpenAPI.php @@ -409,7 +409,7 @@ public function emitJson(Transaction $transaction): void $queryObject->tags = [__('lodata::Batch requests')]; - $route = app(Endpoint::class)->route(); + $route = app()->make(Endpoint::class)->route(); $requestBody = [ 'required' => true, diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 958c05255..511b8b071 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -21,29 +21,14 @@ /** * Service Provider + * + * https://:///$metadata + * * @link https://laravel.com/docs/8.x/providers * @package Flat3\Lodata */ class ServiceProvider extends \Illuminate\Support\ServiceProvider { - /** - * Get the endpoint of the OData service document - * @return string - */ - public static function endpoint(): string - { - return url(self::route()).'/'; - } - - /** - * Get the configured route prefix - * @return string - */ - public static function route(): string - { - return rtrim(config('lodata.prefix'), '/'); - } - /** * Service provider registration method */ @@ -59,14 +44,61 @@ public function boot() { if ($this->app->runningInConsole()) { $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); + $this->bootServices(new Endpoint('')); } + else { + // Let’s examine the request path + $segments = explode('/', request()->path()); + + // we only kick off operation when path prefix is configured in lodata.php + // as all requests share the same root configuration + if ($segments[0] === config('lodata.prefix')) { + + // next look up the configured service endpoints + $serviceUris = config('lodata.endpoints', []); + + $service = null; + if (0 === sizeof($serviceUris)) { + // when no locators are defined, fallback to global mode; this will + // ensure compatibility with prior versions of this package + $service = new Endpoint(''); + } + else if (array_key_exists($segments[1], $serviceUris)) { + $clazz = $serviceUris[$segments[1]]; + $service = new $clazz($segments[1]); + } + else { + // when no service definition is configured for the path segment, + // we abort with an error condition; typically a dev working on + // setting up his project + abort('No odata service endpoint defined for path ' . $segments[1]); + } + + $this->bootServices($service); + } + } + } + + private function bootServices($service): void + { + // register the $service, which is a singleton, with the container; this allows us + // to fulfill all old ServiceProvider::route() and ServiceProvider::endpoint() + // calls with app()->make(ODataService::class)->route() or + // app()->make(ODataService::class)->endpoint() + $this->app->instance(Endpoint::class, $service); $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); - $this->app->singleton(Model::class, function () { - return new Model(); - }); + // next instantiate and discover the global Model + $model = $service->discover(new Model()); + assert($model instanceof Model); + + // and register it with the container + $this->app->instance(Model::class, $model); + // I don't get why you are doing this twice? You never load the model + // via app()->make('lodata.model') or app()->make(Model::class). What + // am I missing here? $this->app->bind('lodata.model', function ($app) { return $app->make(Model::class); }); @@ -83,7 +115,7 @@ public function boot() return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); }); - $route = self::route(); + $route = $service->route(); $middleware = config('lodata.middleware', []); Route::get("{$route}/_lodata/odata.pbids", [PBIDS::class, 'get']); diff --git a/src/Transaction/MultipartDocument.php b/src/Transaction/MultipartDocument.php index c87c3a1d2..948452858 100644 --- a/src/Transaction/MultipartDocument.php +++ b/src/Transaction/MultipartDocument.php @@ -163,7 +163,7 @@ public function toRequest(): Request list($method, $requestURI, $httpVersion) = array_pad(explode(' ', $requestLine), 3, ''); - $endpoint = app(Endpoint::class)->endpoint(); + $endpoint = app()->make(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( From c1e8b8f5a7e21f655717fe685f44de595c6adf89 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Fri, 14 Mar 2025 06:06:35 +0100 Subject: [PATCH 20/34] Tiny documentation tweak --- src/ServiceProvider.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 511b8b071..4f54a376b 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -96,9 +96,7 @@ private function bootServices($service): void // and register it with the container $this->app->instance(Model::class, $model); - // I don't get why you are doing this twice? You never load the model - // via app()->make('lodata.model') or app()->make(Model::class). What - // am I missing here? + // register alias $this->app->bind('lodata.model', function ($app) { return $app->make(Model::class); }); From b41166f7ae700493e28337b15cf3ce9fdf82b6d6 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Sat, 12 Apr 2025 14:53:40 +0200 Subject: [PATCH 21/34] Add possibility to use arbitrary Annotations on methods --- src/Drivers/EloquentEntitySet.php | 43 +++++++++---------- src/Interfaces/AnnotationFactoryInterface.php | 13 ++++++ 2 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 src/Interfaces/AnnotationFactoryInterface.php diff --git a/src/Drivers/EloquentEntitySet.php b/src/Drivers/EloquentEntitySet.php index 618676c6b..7b527dd64 100644 --- a/src/Drivers/EloquentEntitySet.php +++ b/src/Drivers/EloquentEntitySet.php @@ -33,6 +33,7 @@ use Flat3\Lodata\Helper\JSON; use Flat3\Lodata\Helper\PropertyValue; use Flat3\Lodata\Helper\PropertyValues; +use Flat3\Lodata\Interfaces\AnnotationFactoryInterface; use Flat3\Lodata\Interfaces\EntitySet\ComputeInterface; use Flat3\Lodata\Interfaces\EntitySet\CountInterface; use Flat3\Lodata\Interfaces\EntitySet\CreateInterface; @@ -866,29 +867,27 @@ public function discover(): self } /** @var ReflectionMethod $reflectionMethod */ - foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) { - /** @var LodataRelationship $relationshipInstance */ - $relationshipInstance = Discovery::getFirstMethodAttributeInstance( - $reflectionMethod, - LodataRelationship::class - ); - - if (!$relationshipInstance) { - continue; - } - - $relationshipMethod = $reflectionMethod->getName(); - - try { - $this->discoverRelationship( - $relationshipMethod, - $relationshipInstance->getName(), - $relationshipInstance->getDescription(), - $relationshipInstance->isNullable() - ); - } catch (ConfigurationException $e) { + foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) + foreach ($reflectionMethod->getAttributes() as $attribute) { + + $instance = $attribute->newInstance(); + if ($instance instanceof LodataRelationship) { + $relationshipMethod = $reflectionMethod->getName(); + + try { + $this->discoverRelationship( + $relationshipMethod, + $instance->getName(), + $instance->getDescription(), + $instance->isNullable() + ); + } catch (ConfigurationException $e) { + } + } + else if ($instance instanceof AnnotationFactoryInterface) { + $this->addAnnotation($instance->toAnnotation()); + } } - } return $this; } diff --git a/src/Interfaces/AnnotationFactoryInterface.php b/src/Interfaces/AnnotationFactoryInterface.php new file mode 100644 index 000000000..44c490ed4 --- /dev/null +++ b/src/Interfaces/AnnotationFactoryInterface.php @@ -0,0 +1,13 @@ + Date: Tue, 15 Apr 2025 15:52:58 +0200 Subject: [PATCH 22/34] Integrate alias into $metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added missing type PropertyPath - added Identifier to Record - added missing Type attribute to Record - added alias to Identifier – including dirty hack to derive it from given namespace - added addReference to Lodata Facade to allow adding additional vocabularies to the model - added alias for all auto registered References --- .gitignore | 1 + src/Annotation.php | 13 +++++++++ src/Annotation/Capabilities/V1/Reference.php | 1 + src/Annotation/Core/V1/Reference.php | 1 + src/Annotation/Record.php | 28 ++++++++++++++++++++ src/Annotation/Reference.php | 15 +++++++++++ src/Facades/Lodata.php | 1 + src/Helper/Identifier.php | 15 ++++++++++- src/Type/PropertyPath.php | 11 ++++++++ 9 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/Type/PropertyPath.php diff --git a/.gitignore b/.gitignore index 98496a821..f54c0aa16 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ vendor/ .phpunit.result.cache .phpunit.cache/test-results .phpdoc/ +.idea .idea/dataSources.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml diff --git a/src/Annotation.php b/src/Annotation.php index c74343082..bd8c80c63 100644 --- a/src/Annotation.php +++ b/src/Annotation.php @@ -13,6 +13,7 @@ use Flat3\Lodata\Type\Byte; use Flat3\Lodata\Type\Collection; use Flat3\Lodata\Type\Enum; +use Flat3\Lodata\Type\PropertyPath; use Flat3\Lodata\Type\String_; use SimpleXMLElement; @@ -58,6 +59,10 @@ public function appendJsonValue($value) case $value instanceof Record: $record = (object) []; + if (method_exists($value, 'getTypeName') && $value->getTypeName()) { + $record->{'@type'} = $value->getTypeName(); + } + /** @var PropertyValue $propertyValue */ foreach ($value as $propertyValue) { $record->{$propertyValue->getProperty()->getName()} = $this->appendJsonValue($propertyValue->getPrimitive()); @@ -109,6 +114,10 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) $element->addAttribute('Int', $value->toUrl()); break; + case $value instanceof PropertyPath: + $element->addAttribute('PropertyPath', $value->get()); + break; + case $value instanceof String_: $element->addAttribute('String', $value->get()); break; @@ -147,6 +156,10 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) protected function appendXmlRecord(SimpleXMLElement $element, Record $record) { $recordElement = $element->addChild('Record'); + $identifier = $record->getIdentifier(); + if (!is_null($identifier)) { + $recordElement->addAttribute('Type', $identifier->getQualifiedName()); + } /** @var PropertyValue $propertyValue */ foreach ($record as $propertyValue) { diff --git a/src/Annotation/Capabilities/V1/Reference.php b/src/Annotation/Capabilities/V1/Reference.php index 4715c36ce..7858abcd2 100644 --- a/src/Annotation/Capabilities/V1/Reference.php +++ b/src/Annotation/Capabilities/V1/Reference.php @@ -12,4 +12,5 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1'; protected $namespace = 'Org.OData.Capabilities.V1'; + protected $alias = 'Capabilities'; } \ No newline at end of file diff --git a/src/Annotation/Core/V1/Reference.php b/src/Annotation/Core/V1/Reference.php index 744f1c578..f0d9a18a4 100644 --- a/src/Annotation/Core/V1/Reference.php +++ b/src/Annotation/Core/V1/Reference.php @@ -12,4 +12,5 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1'; protected $namespace = 'Org.OData.Core.V1'; + protected $alias = 'Core'; } \ No newline at end of file diff --git a/src/Annotation/Record.php b/src/Annotation/Record.php index 65b3f4ab9..8544382b3 100644 --- a/src/Annotation/Record.php +++ b/src/Annotation/Record.php @@ -4,6 +4,7 @@ namespace Flat3\Lodata\Annotation; +use Flat3\Lodata\Helper\Identifier; use Flat3\Lodata\Helper\ObjectArray; use Flat3\Lodata\Interfaces\TypeInterface; use Flat3\Lodata\Traits\HasComplexType; @@ -16,4 +17,31 @@ class Record extends ObjectArray implements TypeInterface { use HasComplexType; + + /** + * Resource identifier + * @var Identifier $identifier + */ + protected $identifier; + + /** + * Get the identifier + * @return Identifier Identifier + */ + public function getIdentifier(): ?Identifier + { + return $this->identifier; + } + + /** + * Set the identifier + * @param string|Identifier $identifier Identifier + * @return $this + */ + public function setIdentifier($identifier): Record + { + $this->identifier = $identifier instanceof Identifier ? $identifier : new Identifier($identifier); + + return $this; + } } \ No newline at end of file diff --git a/src/Annotation/Reference.php b/src/Annotation/Reference.php index 0ebbafd3b..ee1700761 100644 --- a/src/Annotation/Reference.php +++ b/src/Annotation/Reference.php @@ -33,6 +33,21 @@ class Reference */ protected $alias; + public function getUri(): string + { + return $this->uri; + } + + public function getNamespace(): string + { + return $this->namespace; + } + + public function getAlias(): string + { + return is_null($this->alias) ? $this->namespace : $this->alias; + } + /** * Append this reference to the provided XML element * @param SimpleXMLElement $schema Schema diff --git a/src/Facades/Lodata.php b/src/Facades/Lodata.php index 260e299e2..f2966fdee 100644 --- a/src/Facades/Lodata.php +++ b/src/Facades/Lodata.php @@ -46,6 +46,7 @@ * @method static ComplexType getComplexType(Identifier|string $name) Get a complex type from the model * @method static Singleton getSingleton(Identifier|string $name) Get a singleton from the model * @method static IdentifierInterface add(IdentifierInterface $item) Add a named resource or type to the model + * @method static Model addReference(Reference $reference) Add a reference to an external CSDL document * @method static Model drop(Identifier|string $key) Drop a named resource or type from the model * @method static EntityContainer getEntityContainer() Get the entity container * @method static string getNamespace() Get the namespace of this model diff --git a/src/Helper/Identifier.php b/src/Helper/Identifier.php index 1dc517d79..34c902213 100644 --- a/src/Helper/Identifier.php +++ b/src/Helper/Identifier.php @@ -26,6 +26,11 @@ final class Identifier */ private $namespace; + /** + * @var string $alias + */ + private $alias; + public function __construct(string $identifier) { if (!Str::contains($identifier, '.')) { @@ -38,6 +43,14 @@ public function __construct(string $identifier) $this->name = Str::afterLast($identifier, '.'); $this->namespace = Str::beforeLast($identifier, '.'); + + // NB dirty hack to derive alias from namespace + if (preg_match('/\.([^.]+)\.V1$/', $this->namespace, $matches)) { + $this->alias = $matches[1]; + } + else { + $this->alias = $this->namespace; + } } /** @@ -98,7 +111,7 @@ public function setNamespace(string $namespace): self */ public function getQualifiedName(): string { - return $this->namespace.'.'.$this->name; + return $this->alias.'.'.$this->name; } /** diff --git a/src/Type/PropertyPath.php b/src/Type/PropertyPath.php new file mode 100644 index 000000000..7bce9a374 --- /dev/null +++ b/src/Type/PropertyPath.php @@ -0,0 +1,11 @@ + Date: Tue, 15 Apr 2025 16:28:21 +0200 Subject: [PATCH 23/34] Integrating Endpoint Handling --- src/Endpoint.php | 2 +- src/ServiceProvider.php | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Endpoint.php b/src/Endpoint.php index b8fb6a0aa..3d2362145 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -21,7 +21,7 @@ class Endpoint public function __construct(string $serviceUri) { - $this->serviceUri = rtrim($serviceUri, '/'); + $this->serviceUri = trim($serviceUri, '/'); $prefix = rtrim(config('lodata.prefix'), '/'); $this->route = ('' === $serviceUri) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 4f54a376b..088ff6154 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -14,7 +14,6 @@ use Flat3\Lodata\Helper\Flysystem; use Flat3\Lodata\Helper\DBAL; use Flat3\Lodata\Helper\Symfony; -use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Kernel; @@ -51,13 +50,12 @@ public function boot() $segments = explode('/', request()->path()); // we only kick off operation when path prefix is configured in lodata.php - // as all requests share the same root configuration + // and bypass all other routes for performance if ($segments[0] === config('lodata.prefix')) { // next look up the configured service endpoints $serviceUris = config('lodata.endpoints', []); - $service = null; if (0 === sizeof($serviceUris)) { // when no locators are defined, fallback to global mode; this will // ensure compatibility with prior versions of this package @@ -68,10 +66,9 @@ public function boot() $service = new $clazz($segments[1]); } else { - // when no service definition is configured for the path segment, - // we abort with an error condition; typically a dev working on - // setting up his project - abort('No odata service endpoint defined for path ' . $segments[1]); + // when no service definition could be found for the path segment, + // we assume global scope + $service = new Endpoint(''); } $this->bootServices($service); @@ -87,6 +84,10 @@ private function bootServices($service): void // app()->make(ODataService::class)->endpoint() $this->app->instance(Endpoint::class, $service); + $this->app->bind(DBAL::class, function (Application $app, array $args) { + return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); + }); + $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); // next instantiate and discover the global Model @@ -109,10 +110,6 @@ private function bootServices($service): void return class_exists('League\Flysystem\Adapter\Local') ? new Flysystem\Flysystem1() : new Flysystem\Flysystem3(); }); - $this->app->bind(DBAL::class, function (Application $app, array $args) { - return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); - }); - $route = $service->route(); $middleware = config('lodata.middleware', []); From 4faa16a1b07d93edb51c29d9b86af6e493c903bb Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Tue, 15 Apr 2025 17:24:00 +0200 Subject: [PATCH 24/34] Qualify model with Endpoint->getNamespace() --- src/Drivers/EloquentEntitySet.php | 4 +++- src/Endpoint.php | 16 ++++++++++++++++ src/Model.php | 2 +- src/PathSegment/Metadata/XML.php | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Drivers/EloquentEntitySet.php b/src/Drivers/EloquentEntitySet.php index 7b527dd64..6b4c2e728 100644 --- a/src/Drivers/EloquentEntitySet.php +++ b/src/Drivers/EloquentEntitySet.php @@ -20,6 +20,7 @@ use Flat3\Lodata\Drivers\SQL\SQLOrderBy; use Flat3\Lodata\Drivers\SQL\SQLSchema; use Flat3\Lodata\Drivers\SQL\SQLWhere; +use Flat3\Lodata\Endpoint; use Flat3\Lodata\Entity; use Flat3\Lodata\EntitySet; use Flat3\Lodata\EntityType; @@ -115,7 +116,8 @@ public function __construct(string $model, ?EntityType $entityType = null) $name = self::convertClassName($model); if (!$entityType) { - $entityType = new EntityType(EntityType::convertClassName($model)); + $identifier = app(Endpoint::class)->getNamespace().'.'.EntityType::convertClassName($model); + $entityType = new EntityType($identifier); } parent::__construct($name, $entityType); diff --git a/src/Endpoint.php b/src/Endpoint.php index 3d2362145..115a4f9fe 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -46,6 +46,22 @@ public function route(): string } /** + * This method is intended to be overridden by subclasses. + * + * The value of the function will be presented in the Schema Namespace attribute, + * https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_Schema + * + * @return string + */ + public function getNamespace(): string + { + // override this function to set Schema Namespace attribute + return config('lodata.namespace'); + } + + /** + * This method is intended to be overridden by subclasses. + * * Discovers Schema and Annotations of the `$metadata` file for * the service. */ diff --git a/src/Model.php b/src/Model.php index c58be6661..c17e527b0 100644 --- a/src/Model.php +++ b/src/Model.php @@ -199,7 +199,7 @@ public function getTypeDefinition(string $name): ?Type */ public static function getNamespace(): string { - return config('lodata.namespace'); + return app(Endpoint::class)->getNamespace(); } /** diff --git a/src/PathSegment/Metadata/XML.php b/src/PathSegment/Metadata/XML.php index b5eefa111..bad294fbb 100644 --- a/src/PathSegment/Metadata/XML.php +++ b/src/PathSegment/Metadata/XML.php @@ -239,14 +239,14 @@ public function emitStream(Transaction $transaction): void case $resource instanceof Singleton: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#_Toc38530395 $resourceElement = $entityContainer->addChild('Singleton'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); $resourceElement->addAttribute('Type', $resource->getType()->getIdentifier()->getQualifiedName()); break; case $resource instanceof EntitySet: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_EntitySet $resourceElement = $entityContainer->addChild('EntitySet'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); $resourceElement->addAttribute( 'EntityType', $resource->getType()->getIdentifier()->getQualifiedName() From bedc6e9b8fac8fbe1ea5f1a3247e1f21939b1a0e Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 18:05:53 +0200 Subject: [PATCH 25/34] Back to Endpoint discovery only --- src/Annotation.php | 13 ------ src/Annotation/Capabilities/V1/Reference.php | 1 - src/Annotation/Core/V1/Reference.php | 1 - src/Annotation/Record.php | 28 ------------ src/Annotation/Reference.php | 15 ------- src/Drivers/EloquentEntitySet.php | 47 ++++++++++---------- src/Facades/Lodata.php | 1 - src/Helper/Identifier.php | 15 +------ src/PathSegment/Metadata/XML.php | 4 +- 9 files changed, 26 insertions(+), 99 deletions(-) diff --git a/src/Annotation.php b/src/Annotation.php index bd8c80c63..c74343082 100644 --- a/src/Annotation.php +++ b/src/Annotation.php @@ -13,7 +13,6 @@ use Flat3\Lodata\Type\Byte; use Flat3\Lodata\Type\Collection; use Flat3\Lodata\Type\Enum; -use Flat3\Lodata\Type\PropertyPath; use Flat3\Lodata\Type\String_; use SimpleXMLElement; @@ -59,10 +58,6 @@ public function appendJsonValue($value) case $value instanceof Record: $record = (object) []; - if (method_exists($value, 'getTypeName') && $value->getTypeName()) { - $record->{'@type'} = $value->getTypeName(); - } - /** @var PropertyValue $propertyValue */ foreach ($value as $propertyValue) { $record->{$propertyValue->getProperty()->getName()} = $this->appendJsonValue($propertyValue->getPrimitive()); @@ -114,10 +109,6 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) $element->addAttribute('Int', $value->toUrl()); break; - case $value instanceof PropertyPath: - $element->addAttribute('PropertyPath', $value->get()); - break; - case $value instanceof String_: $element->addAttribute('String', $value->get()); break; @@ -156,10 +147,6 @@ protected function appendXmlValue(SimpleXMLElement $element, $value) protected function appendXmlRecord(SimpleXMLElement $element, Record $record) { $recordElement = $element->addChild('Record'); - $identifier = $record->getIdentifier(); - if (!is_null($identifier)) { - $recordElement->addAttribute('Type', $identifier->getQualifiedName()); - } /** @var PropertyValue $propertyValue */ foreach ($record as $propertyValue) { diff --git a/src/Annotation/Capabilities/V1/Reference.php b/src/Annotation/Capabilities/V1/Reference.php index 7858abcd2..4715c36ce 100644 --- a/src/Annotation/Capabilities/V1/Reference.php +++ b/src/Annotation/Capabilities/V1/Reference.php @@ -12,5 +12,4 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1'; protected $namespace = 'Org.OData.Capabilities.V1'; - protected $alias = 'Capabilities'; } \ No newline at end of file diff --git a/src/Annotation/Core/V1/Reference.php b/src/Annotation/Core/V1/Reference.php index f0d9a18a4..744f1c578 100644 --- a/src/Annotation/Core/V1/Reference.php +++ b/src/Annotation/Core/V1/Reference.php @@ -12,5 +12,4 @@ class Reference extends \Flat3\Lodata\Annotation\Reference { protected $uri = 'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1'; protected $namespace = 'Org.OData.Core.V1'; - protected $alias = 'Core'; } \ No newline at end of file diff --git a/src/Annotation/Record.php b/src/Annotation/Record.php index 8544382b3..65b3f4ab9 100644 --- a/src/Annotation/Record.php +++ b/src/Annotation/Record.php @@ -4,7 +4,6 @@ namespace Flat3\Lodata\Annotation; -use Flat3\Lodata\Helper\Identifier; use Flat3\Lodata\Helper\ObjectArray; use Flat3\Lodata\Interfaces\TypeInterface; use Flat3\Lodata\Traits\HasComplexType; @@ -17,31 +16,4 @@ class Record extends ObjectArray implements TypeInterface { use HasComplexType; - - /** - * Resource identifier - * @var Identifier $identifier - */ - protected $identifier; - - /** - * Get the identifier - * @return Identifier Identifier - */ - public function getIdentifier(): ?Identifier - { - return $this->identifier; - } - - /** - * Set the identifier - * @param string|Identifier $identifier Identifier - * @return $this - */ - public function setIdentifier($identifier): Record - { - $this->identifier = $identifier instanceof Identifier ? $identifier : new Identifier($identifier); - - return $this; - } } \ No newline at end of file diff --git a/src/Annotation/Reference.php b/src/Annotation/Reference.php index ee1700761..0ebbafd3b 100644 --- a/src/Annotation/Reference.php +++ b/src/Annotation/Reference.php @@ -33,21 +33,6 @@ class Reference */ protected $alias; - public function getUri(): string - { - return $this->uri; - } - - public function getNamespace(): string - { - return $this->namespace; - } - - public function getAlias(): string - { - return is_null($this->alias) ? $this->namespace : $this->alias; - } - /** * Append this reference to the provided XML element * @param SimpleXMLElement $schema Schema diff --git a/src/Drivers/EloquentEntitySet.php b/src/Drivers/EloquentEntitySet.php index 6b4c2e728..618676c6b 100644 --- a/src/Drivers/EloquentEntitySet.php +++ b/src/Drivers/EloquentEntitySet.php @@ -20,7 +20,6 @@ use Flat3\Lodata\Drivers\SQL\SQLOrderBy; use Flat3\Lodata\Drivers\SQL\SQLSchema; use Flat3\Lodata\Drivers\SQL\SQLWhere; -use Flat3\Lodata\Endpoint; use Flat3\Lodata\Entity; use Flat3\Lodata\EntitySet; use Flat3\Lodata\EntityType; @@ -34,7 +33,6 @@ use Flat3\Lodata\Helper\JSON; use Flat3\Lodata\Helper\PropertyValue; use Flat3\Lodata\Helper\PropertyValues; -use Flat3\Lodata\Interfaces\AnnotationFactoryInterface; use Flat3\Lodata\Interfaces\EntitySet\ComputeInterface; use Flat3\Lodata\Interfaces\EntitySet\CountInterface; use Flat3\Lodata\Interfaces\EntitySet\CreateInterface; @@ -116,8 +114,7 @@ public function __construct(string $model, ?EntityType $entityType = null) $name = self::convertClassName($model); if (!$entityType) { - $identifier = app(Endpoint::class)->getNamespace().'.'.EntityType::convertClassName($model); - $entityType = new EntityType($identifier); + $entityType = new EntityType(EntityType::convertClassName($model)); } parent::__construct($name, $entityType); @@ -869,28 +866,30 @@ public function discover(): self } /** @var ReflectionMethod $reflectionMethod */ - foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) - foreach ($reflectionMethod->getAttributes() as $attribute) { - - $instance = $attribute->newInstance(); - if ($instance instanceof LodataRelationship) { - $relationshipMethod = $reflectionMethod->getName(); - - try { - $this->discoverRelationship( - $relationshipMethod, - $instance->getName(), - $instance->getDescription(), - $instance->isNullable() - ); - } catch (ConfigurationException $e) { - } - } - else if ($instance instanceof AnnotationFactoryInterface) { - $this->addAnnotation($instance->toAnnotation()); - } + foreach (Discovery::getReflectedMethods($this->model) as $reflectionMethod) { + /** @var LodataRelationship $relationshipInstance */ + $relationshipInstance = Discovery::getFirstMethodAttributeInstance( + $reflectionMethod, + LodataRelationship::class + ); + + if (!$relationshipInstance) { + continue; } + $relationshipMethod = $reflectionMethod->getName(); + + try { + $this->discoverRelationship( + $relationshipMethod, + $relationshipInstance->getName(), + $relationshipInstance->getDescription(), + $relationshipInstance->isNullable() + ); + } catch (ConfigurationException $e) { + } + } + return $this; } } diff --git a/src/Facades/Lodata.php b/src/Facades/Lodata.php index f2966fdee..260e299e2 100644 --- a/src/Facades/Lodata.php +++ b/src/Facades/Lodata.php @@ -46,7 +46,6 @@ * @method static ComplexType getComplexType(Identifier|string $name) Get a complex type from the model * @method static Singleton getSingleton(Identifier|string $name) Get a singleton from the model * @method static IdentifierInterface add(IdentifierInterface $item) Add a named resource or type to the model - * @method static Model addReference(Reference $reference) Add a reference to an external CSDL document * @method static Model drop(Identifier|string $key) Drop a named resource or type from the model * @method static EntityContainer getEntityContainer() Get the entity container * @method static string getNamespace() Get the namespace of this model diff --git a/src/Helper/Identifier.php b/src/Helper/Identifier.php index 34c902213..1dc517d79 100644 --- a/src/Helper/Identifier.php +++ b/src/Helper/Identifier.php @@ -26,11 +26,6 @@ final class Identifier */ private $namespace; - /** - * @var string $alias - */ - private $alias; - public function __construct(string $identifier) { if (!Str::contains($identifier, '.')) { @@ -43,14 +38,6 @@ public function __construct(string $identifier) $this->name = Str::afterLast($identifier, '.'); $this->namespace = Str::beforeLast($identifier, '.'); - - // NB dirty hack to derive alias from namespace - if (preg_match('/\.([^.]+)\.V1$/', $this->namespace, $matches)) { - $this->alias = $matches[1]; - } - else { - $this->alias = $this->namespace; - } } /** @@ -111,7 +98,7 @@ public function setNamespace(string $namespace): self */ public function getQualifiedName(): string { - return $this->alias.'.'.$this->name; + return $this->namespace.'.'.$this->name; } /** diff --git a/src/PathSegment/Metadata/XML.php b/src/PathSegment/Metadata/XML.php index bad294fbb..b5eefa111 100644 --- a/src/PathSegment/Metadata/XML.php +++ b/src/PathSegment/Metadata/XML.php @@ -239,14 +239,14 @@ public function emitStream(Transaction $transaction): void case $resource instanceof Singleton: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#_Toc38530395 $resourceElement = $entityContainer->addChild('Singleton'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); $resourceElement->addAttribute('Type', $resource->getType()->getIdentifier()->getQualifiedName()); break; case $resource instanceof EntitySet: // https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_EntitySet $resourceElement = $entityContainer->addChild('EntitySet'); - $resourceElement->addAttribute('Name', $resource->getIdentifier()->getName()); + $resourceElement->addAttribute('Name', $resource->getIdentifier()->getResolvedName($namespace)); $resourceElement->addAttribute( 'EntityType', $resource->getType()->getIdentifier()->getQualifiedName() From a67da0ff115a07006df551ce0ccbd81e7e9ce59d Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 18:06:53 +0200 Subject: [PATCH 26/34] Back to Endpoint discovery only (2/2) --- src/Interfaces/AnnotationFactoryInterface.php | 13 ------------- src/Type/PropertyPath.php | 11 ----------- 2 files changed, 24 deletions(-) delete mode 100644 src/Interfaces/AnnotationFactoryInterface.php delete mode 100644 src/Type/PropertyPath.php diff --git a/src/Interfaces/AnnotationFactoryInterface.php b/src/Interfaces/AnnotationFactoryInterface.php deleted file mode 100644 index 44c490ed4..000000000 --- a/src/Interfaces/AnnotationFactoryInterface.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Wed, 16 Apr 2025 19:00:46 +0200 Subject: [PATCH 27/34] Introduced Interface for ServiceEndpoints --- src/Endpoint.php | 40 +++----------- src/Interfaces/ServiceEndpointInterface.php | 61 +++++---------------- src/Model.php | 2 +- 3 files changed, 23 insertions(+), 80 deletions(-) diff --git a/src/Endpoint.php b/src/Endpoint.php index 115a4f9fe..75b7930be 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -2,21 +2,14 @@ namespace Flat3\Lodata; -class Endpoint +use Flat3\Lodata\Interfaces\ServiceEndpointInterface; + +class Endpoint implements ServiceEndpointInterface { - /** - * @var string $serviceUri the <service-uri> of the Flat3\Lodata\Model. - */ protected $serviceUri; - /** - * @var string $route the route prefix configured in 'lodata.prefix' - */ protected $route; - /** - * @var string $endpoint the full url to the ODataService endpoint - */ protected $endpoint; public function __construct(string $serviceUri) @@ -31,10 +24,6 @@ public function __construct(string $serviceUri) $this->endpoint = url($this->route) . '/'; } - /** - * @return string the path within the odata URI space, like in - * https://:///$metadata - */ public function endpoint(): string { return $this->endpoint; @@ -45,29 +34,18 @@ public function route(): string return $this->route; } - /** - * This method is intended to be overridden by subclasses. - * - * The value of the function will be presented in the Schema Namespace attribute, - * https://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/odata-csdl-xml-v4.01.html#sec_Schema - * - * @return string - */ - public function getNamespace(): string + public function namespace(): string { - // override this function to set Schema Namespace attribute return config('lodata.namespace'); } - /** - * This method is intended to be overridden by subclasses. - * - * Discovers Schema and Annotations of the `$metadata` file for - * the service. - */ + public function cachedMetadataXMLPath(): ?string + { + return null; + } + public function discover(Model $model): Model { - // override this function to register all of your $metadata capabilities return $model; } } \ No newline at end of file diff --git a/src/Interfaces/ServiceEndpointInterface.php b/src/Interfaces/ServiceEndpointInterface.php index 3c4c7ba06..568953b60 100644 --- a/src/Interfaces/ServiceEndpointInterface.php +++ b/src/Interfaces/ServiceEndpointInterface.php @@ -5,68 +5,33 @@ use Flat3\Lodata\Model; /** - * Interface for defining a modular OData service endpoint in Laravel. + * Interface for defining a custom OData service endpoint. * - * Implementers of this interface represent individually addressable OData services. - * Each mounted under its own URI segment and backed by a schema model. - * - * This enables clean separation of business domains and supports multi-endpoint - * discovery for modular application design. - * - * Configuration versus Declaration - * -------------------------------- - * The public URI segment used to expose a service is NOT determined by the - * implementing class itself, but by the service map in `config/lodata.php`: - * - * ```php - * 'endpoints' => [ - * 'users' => \App\OData\UsersEndpoint::class, - * 'budgets' => \App\OData\BudgetsEndpoint::class, - * ] - * ``` - * - * This keeps the routing surface under application control, and avoids - * conflicts when two modules declare the same internal segment. - * - * To implement an endpoint: - * - Extend `Flat3\Lodata\Endpoint` or implement this interface directly - * - Register the class in `config/lodata.php` under a unique segment key - * - Define the `discover()` method to expose entities via OData + * Implementers can use this interface to expose a specific service under a custom path, + * define its namespace, route behavior, and optionally provide a statically generated + * $metadata document. */ interface ServiceEndpointInterface { - /** - * Returns the ServiceURI segment name for this OData endpoint. - * - * This value is used in routing and metadata resolution. It Must be globally unique. - * - * @return string The segment (path identifier) of the endpoint - */ - public function serviceUri(): string; /** - * Returns the fully qualified URL for this OData endpoint. + * Returns the relative endpoint identifier within the OData service URI space. * - * This includes the application host, port, and the configured segment, - * https://:///, - * Example: https://example.com/odata/users/ + * This is the part that appears between the configured Lodata prefix and + * the `$metadata` segment, e.g.: + * https://:///$metadata * - * This URL forms the base of the OData service space, and is used for navigation links - * and metadata discovery. - * - * @return string The full URL of the service endpoint + * @return string The relative OData endpoint path */ public function endpoint(): string; /** - * Returns the internal Laravel route path for this OData service endpoint. - * - * This is the relative URI path that Laravel uses to match incoming requests, - * typically composed of the configured Lodata prefix and the service segment. + * Returns the full request route to this service endpoint. * - * Example: "odata/users" + * This typically resolves to the route path used by Laravel to handle + * incoming requests for this specific service instance. * - * @return string Relative route path for the endpoint + * @return string The full HTTP route to the endpoint */ public function route(): string; diff --git a/src/Model.php b/src/Model.php index c17e527b0..ce8881d20 100644 --- a/src/Model.php +++ b/src/Model.php @@ -199,7 +199,7 @@ public function getTypeDefinition(string $name): ?Type */ public static function getNamespace(): string { - return app(Endpoint::class)->getNamespace(); + return app(Endpoint::class)->namespace(); } /** From 59bdcbef074272f0fca1c04fe48878903f8b690b Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 19:37:25 +0200 Subject: [PATCH 28/34] Update documentation --- doc/getting-started/endpoint.md | 317 ++++++++++++++++++++++++++++++-- 1 file changed, 301 insertions(+), 16 deletions(-) diff --git a/doc/getting-started/endpoint.md b/doc/getting-started/endpoint.md index 64ba2aa76..68695a431 100644 --- a/doc/getting-started/endpoint.md +++ b/doc/getting-started/endpoint.md @@ -1,43 +1,328 @@ + # Service Endpoints -At this point we assume you already published the `lodata.php` config file to your project. +> **Prerequisite**: You’ve already published the `lodata.php` config file into your Laravel project using `php artisan vendor:publish`. + +## Overview + +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. + +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. -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. +## Defining Multiple Endpoints -Each of your modules could register its own service endpoint with an `\Flat3\Lodata\Endpoint` like this: +You can define service endpoints by registering them in your `config/lodata.php` configuration file: ```php /** - * At the end of `config/lodata.php` + * At the end of config/lodata.php */ 'endpoints' => [ - 'projects' ⇒ \App\Projects\ProjectEndpoint::class, + 'projects' => \App\Projects\ProjectEndpoint::class, ], ``` -With that configuration a separate `$metadata` service file will be available via `https://://projects/$metadata`. +With this configuration, a separate `$metadata` document becomes available at: + +``` +https://://projects/$metadata +``` -If the `endpoints` array stays empty (the default), only one global service endpoint is created. +If the `endpoints` array is left empty (the default), only a single global endpoint is created under the configured `lodata.prefix`. -## Selective Discovery +## Endpoint Discovery -With endpoints, you can now discover all your entities and annotations in a separate class via the `discover` function. +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. + +This gives you fine-grained control over what each endpoint exposes. ```php -use App\Model\Contact; +use App\Models\Contact; use Flat3\Lodata\Model; /** - * Discovers Schema and Annotations of the `$metadata` file for - * the service. + * Discover schema elements and annotations for the service endpoint. */ public function discover(Model $model): Model { - // register all of your $metadata capabilities - $model->discover(Contact::class); - … + // Register all exposed entity sets or types + $model->discover(Contact::class); + // Add more types or annotations here... + return $model; } ``` -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. +### Performance Benefit + +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. + +> ✅ This optimization also applies to the **default (global) service endpoint** — its `discover()` method is likewise only evaluated on-demand during OData requests. + +This design keeps your application performant, especially in modular or multi-endpoint setups, by avoiding unnecessary processing for unrelated HTTP traffic. + +## Serving Pre-Generated $metadata Files + +In addition to dynamic schema generation, you can optionally serve a **pre-generated `$metadata.xml` file**. This is especially useful when: + +- You want to include **custom annotations** that are not easily represented in PHP code. +- You have **external tools** that generate the schema. +- You prefer **fine-tuned control** over the metadata document. + +To enable this, implement the `cachedMetadataXMLPath()` method in your endpoint class: + +```php +public function cachedMetadataXMLPath(): ?string +{ + return base_path('odata/metadata-projects.xml'); +} +``` + +If this method returns a valid file path, `lodata` will serve this file directly when `$metadata` is requested, bypassing the `discover()` logic. + +If it returns `null` (default), the schema will be generated dynamically from the `discover()` method. + +## Summary + +| Feature | Dynamic (`discover`) | Static (`cachedMetadataXMLPath`) | +|--------------------------|----------------------|-----------------------------------| +| Schema definition | In PHP | In XML file | +| Supports annotations | Basic | Full (manual control) | +| Performance optimized | Yes | Yes | +| Best for | Laravel-native setup | SAP integration, fine-tuned CSDL | + +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. + +## Sample: Defining a `ProjectEndpoint` + +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. + +### Step 1: Define the Custom Endpoint Class + +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. + +Here’s a minimal yet complete example: + +```php + element of $metadata. + */ + public function namespace(): string + { + return 'ProjectService'; + } + + /** + * Optionally return a static metadata XML file. + * If null, dynamic discovery via discover() is used. + */ + public function cachedMetadataXMLPath(): ?string + { + return resource_path('meta/ProjectService.xml'); + } + + /** + * Register entities and types to expose through this endpoint. + */ + public function discover(Model $model): Model + { + $model->discover(Project::class); + + return $model; + } +} +``` + +> ✅ **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. + +### Step 2: Register the Endpoint and Define Its URI Prefix + +In your `config/lodata.php`, register the custom endpoint under the `endpoints` array: + +```php +'endpoints' => [ + 'projects' => \App\Endpoints\ProjectEndpoint::class, +], +``` + +> 🧩 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`. + +This results in: + +- `$metadata` available at: + `https://://projects/$metadata` + +- Entity sets exposed through: + `https://://projects/Projects` + +This convention gives you **clear, readable URLs** and enables **modular, multi-service APIs** without extra routing configuration. + +### Step 3: Serve Dynamic or Static Metadata + +The framework will: + +- Call `cachedMetadataXMLPath()` first. + If a file path is returned and the file exists, it will serve that file directly. +- Otherwise, it will fall back to the `discover()` method to dynamically register entities, types, and annotations. + +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. + +## ✅ What You Get + +With just a few lines of configuration, you now have: + +- A **cleanly separated OData service** for the `Project` module. +- **Independent metadata** for documentation and integration. +- A fast and **on-demand schema bootstrapping** process. +- Full **control over discoverability** and **extensibility**. + +You can now repeat this pattern for other domains (e.g., `contacts`, `finance`, `hr`) to keep your OData services modular, testable, and scalable. + +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: + +## How Everything Connects + +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. + +### Flow Overview + +``` +[ config/lodata.php ] → [ ProjectEndpoint class ] + │ │ + ▼ ▼ + 'projects' => ProjectEndpoint::class ──► defines: + - namespace() + - discover() + - cachedMetadataXMLPath() + + │ │ + ▼ ▼ + URI: /odata/projects/$metadata OData Schema (XML or dynamic) +``` + +### The Building Blocks + +| Component | Purpose | +|-----------------------------------|-------------------------------------------------------------------------| +| **`config/lodata.php`** | Registers all endpoints and defines the URI prefix for each one | +| **Key: `'projects'`** | Becomes part of the URL: `/odata/projects/` | +| **`ProjectEndpoint` class** | Defines what the endpoint serves and how | +| **`namespace()`** | Injects the `` into `$metadata` | +| **`discover(Model $model)`** | Dynamically registers entities like `Project::class` | +| **`cachedMetadataXMLPath()`** | Optionally returns a pre-generated CSDL XML file | +| **OData request** | Triggers loading of this endpoint’s metadata and data | + +## Example: Request Lifecycle + +Let’s break down how the enhanced flow would look for an actual **entity set access**, such as + +``` +GET /odata/projects/Costcenters +``` + +This is about a **data request** for a specific entity set. Here's how the full lifecycle plays out. From config to response. + +### Enhanced Flow for `/odata/projects/Costcenters` + +``` + ┌─────────────────────────────────────────────────────┐ + │ HTTP GET /odata/projects/Costcenters │ + └─────────────────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ [Routing Layer] matches +│ config/lodata.php │── 'projects' key +│ │ +│ 'projects' => ProjectEndpoint::class, │ +└────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ New ProjectEndpoint instance │ + └──────────────────────────────────────┘ + │ + (cachedMetadataXMLPath() not used here) + │ + ▼ + ┌───────────────────────────────────────────────┐ + │ discover(Model $model) is invoked │ + │ → model->discover(Project::class) │ + │ → model->discover(Costcenter::class) │ + └───────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ Lodata resolves the URI segment: │ + │ `Costcenters` │ + └──────────────────────────────────────┘ + │ + (via the registered EntitySet name for Costcenter) + │ + ▼ + ┌───────────────────────────────────────────────┐ + │ Query engine builds and executes the query │ + │ using the underlying Eloquent model │ + └───────────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────────────────┐ + │ Response is serialized into JSON or XML │ + │ according to Accept header │ + └────────────────────────────────────────────┘ + │ + ▼ + 🔁 JSON (default) or Atom/XML payload with Costcenter entities + +``` + +### What Must Be in Place for This to Work + +| Requirement | Description | +|-----------------------------------------------|-----------------------------------------------------------------------------| +| `ProjectEndpoint::discover()` | Must register `Costcenter::class` via `$model->discover(...)` | +| `Costcenter` model | Can be a **standard Laravel Eloquent model** – no special base class needed | +| `EntitySet` name | Must match the URI segment: `Costcenters` | +| URI case sensitivity | Lodata uses the identifier names → ensure entity names match URI segments | +| Accept header | Optional – defaults to JSON if none is provided | + +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: + +## What Modular Service Endpoints Enable + +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: + +- **Modular APIs** — Define multiple endpoints, each exposing only the entities and operations relevant to a specific domain (e.g., `projects`, `contacts`, `finance`). +- **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. +- **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. +- **Schema isolation** — Maintain clean separation between domains, clients, or API versions. For example: + - `/odata/projects/$metadata` → `ProjectService` schema + - `/odata/finance/$metadata` → `FinanceService` schema +- **Mix and match discovery strategies** — Use dynamic schema generation via Eloquent models or inject precise, curated metadata with static CSDL files. +- **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. + +### ✅ In Short + +Modular service endpoints allow you to: + +- Keep your domains cleanly separated +- Scale your API by feature, client, or team +- Provide tailored metadata per endpoint +- Mix dynamic discovery with pre-defined XML schemas +- Integrate smoothly into your Laravel app — no magic, just configuration and conventions + +They’re not just a convenience, they’re a foundation for **clean, scalable, and maintainable OData APIs**. From 65fbebe96a48ee64909fdab6b2b6da02642eae4b Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 16 Apr 2025 19:51:42 +0200 Subject: [PATCH 29/34] Alias declaration more Laravel-like --- src/ServiceProvider.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 088ff6154..fccacadff 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -98,9 +98,7 @@ private function bootServices($service): void $this->app->instance(Model::class, $model); // register alias - $this->app->bind('lodata.model', function ($app) { - return $app->make(Model::class); - }); + $this->app->alias(Model::class, 'lodata.model'); $this->app->bind(Response::class, function () { return Kernel::VERSION_ID < 60000 ? new Symfony\Response5() : new Symfony\Response6(); From 8dd570452a633dcad50a929754290154c223ae03 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Wed, 23 Apr 2025 11:10:05 +0200 Subject: [PATCH 30/34] removed .idea from .gitignore; resolving Endpoint unified --- .gitignore | 1 - src/Controller/ODCFF.php | 2 +- src/Controller/PBIDS.php | 2 +- src/Controller/Transaction.php | 4 ++-- src/Entity.php | 2 +- src/Model.php | 2 +- src/PathSegment/Batch/JSON.php | 2 +- src/PathSegment/OpenAPI.php | 2 +- src/Transaction/MultipartDocument.php | 2 +- 9 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index f54c0aa16..98496a821 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ vendor/ .phpunit.result.cache .phpunit.cache/test-results .phpdoc/ -.idea .idea/dataSources.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml diff --git a/src/Controller/ODCFF.php b/src/Controller/ODCFF.php index 6ae19860a..e42fbf473 100644 --- a/src/Controller/ODCFF.php +++ b/src/Controller/ODCFF.php @@ -167,7 +167,7 @@ public function get(string $identifier): Response $formula = $mashupDoc->createElement('Formula'); $formulaContent = $mashupDoc->createCDATASection(sprintf( 'let Source = OData.Feed("%1$s", null, [Implementation="2.0"]), %2$s_table = Source{[Name="%2$s",Signature="table"]}[Data] in %2$s_table', - app()->make(Endpoint::class)->endpoint(), + app(Endpoint::class)->endpoint(), $resourceId, )); $formula->appendChild($formulaContent); diff --git a/src/Controller/PBIDS.php b/src/Controller/PBIDS.php index 69e46ce30..61f9178d6 100644 --- a/src/Controller/PBIDS.php +++ b/src/Controller/PBIDS.php @@ -44,7 +44,7 @@ public function get(): Response 'details' => [ 'protocol' => 'odata', 'address' => [ - 'url' => app()->make(Endpoint::class)->endpoint(), + 'url' => app(Endpoint::class)->endpoint(), ], ], ], diff --git a/src/Controller/Transaction.php b/src/Controller/Transaction.php index 40faf4dce..a6367624f 100644 --- a/src/Controller/Transaction.php +++ b/src/Controller/Transaction.php @@ -785,7 +785,7 @@ public function getPath(): string */ public function getRequestPath(): string { - $route = app()->make(Endpoint::class)->route(); + $route = app(Endpoint::class)->route(); return Str::substr($this->request->path(), strlen($route)); } @@ -963,7 +963,7 @@ public function getContextUrl(): string */ public static function getResourceUrl(): string { - return app()->make(Endpoint::class)->endpoint(); + return app(Endpoint::class)->endpoint(); } /** diff --git a/src/Entity.php b/src/Entity.php index efb76f975..8c1067407 100644 --- a/src/Entity.php +++ b/src/Entity.php @@ -178,7 +178,7 @@ public static function pipe( } $entityId = $id->getValue(); - $endpoint = app()->make(Endpoint::class)->endpoint(); + $endpoint = app(Endpoint::class)->endpoint(); if (Str::startsWith($entityId, $endpoint)) { $entityId = Str::substr($entityId, strlen($endpoint)); } diff --git a/src/Model.php b/src/Model.php index ce8881d20..1c1bdd81e 100644 --- a/src/Model.php +++ b/src/Model.php @@ -344,7 +344,7 @@ public function discover($discoverable): self */ public function getEndpoint(): string { - return app()->make(Endpoint::class)->endpoint(); + return app(Endpoint::class)->endpoint(); } /** diff --git a/src/PathSegment/Batch/JSON.php b/src/PathSegment/Batch/JSON.php index 048b90238..9c012d0f1 100644 --- a/src/PathSegment/Batch/JSON.php +++ b/src/PathSegment/Batch/JSON.php @@ -59,7 +59,7 @@ public function emitJson(Transaction $transaction): void $requestURI = $requestData['url']; - $endpoint = app()->make(Endpoint::class)->endpoint(); + $endpoint = app(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( diff --git a/src/PathSegment/OpenAPI.php b/src/PathSegment/OpenAPI.php index 5df9f3215..b1657d9ab 100644 --- a/src/PathSegment/OpenAPI.php +++ b/src/PathSegment/OpenAPI.php @@ -409,7 +409,7 @@ public function emitJson(Transaction $transaction): void $queryObject->tags = [__('lodata::Batch requests')]; - $route = app()->make(Endpoint::class)->route(); + $route = app(Endpoint::class)->route(); $requestBody = [ 'required' => true, diff --git a/src/Transaction/MultipartDocument.php b/src/Transaction/MultipartDocument.php index 948452858..c87c3a1d2 100644 --- a/src/Transaction/MultipartDocument.php +++ b/src/Transaction/MultipartDocument.php @@ -163,7 +163,7 @@ public function toRequest(): Request list($method, $requestURI, $httpVersion) = array_pad(explode(' ', $requestLine), 3, ''); - $endpoint = app()->make(Endpoint::class)->endpoint(); + $endpoint = app(Endpoint::class)->endpoint(); switch (true) { case Str::startsWith($requestURI, '/'): $uri = Url::http_build_url( From 8f5e3b3dded6feed5be75dd8dd6750ac6fd04c4f Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Mon, 23 Jun 2025 06:08:32 +0200 Subject: [PATCH 31/34] Fix: Prevent 'Undefined array key 1' when global Endpoint is requested When multiple Endpoints are declared, and the frontend requests the global Endpoint (with only one path segment), accessing $segments[1] caused an 'Undefined array key' warning. This commit adds a count($segments) check to avoid invalid index access and preserve compatibility with global Endpoint logic. --- src/ServiceProvider.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index fccacadff..78bdedc12 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -56,9 +56,10 @@ public function boot() // next look up the configured service endpoints $serviceUris = config('lodata.endpoints', []); - if (0 === sizeof($serviceUris)) { - // when no locators are defined, fallback to global mode; this will - // ensure compatibility with prior versions of this package + if (0 === sizeof($serviceUris) || count($segments) === 1) { + // when no locators are defined, or the global locator ist requested, + // enter global mode; this will ensure compatibility with prior + // versions of this package $service = new Endpoint(''); } else if (array_key_exists($segments[1], $serviceUris)) { From 508ac1300885bda868624202a1fe503937bb426d Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Sun, 13 Jul 2025 11:33:24 +0200 Subject: [PATCH 32/34] Fix: Add serviceUri() to ServiceEndpointInterface and clarify endpoint routing docs - Introduced new method serviceUri(): string to explicitly declare the logical URI segment for each OData endpoint - Updated ServiceEndpointInterface PHPDoc to reflect modular, config-driven routing - Clarified semantics of endpoint() and route() to match actual behavior in Endpoint base class - Aligns terminology with ServiceProvider::boot() logic for endpoint resolution --- src/Endpoint.php | 8 ++- src/Interfaces/ServiceEndpointInterface.php | 61 ++++++++++++++++----- src/ServiceProvider.php | 8 +++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/Endpoint.php b/src/Endpoint.php index 75b7930be..a7e6eed44 100644 --- a/src/Endpoint.php +++ b/src/Endpoint.php @@ -17,13 +17,19 @@ public function __construct(string $serviceUri) $this->serviceUri = trim($serviceUri, '/'); $prefix = rtrim(config('lodata.prefix'), '/'); - $this->route = ('' === $serviceUri) + + $this->route = ('' === $this->serviceUri) ? $prefix : $prefix . '/' . $this->serviceUri; $this->endpoint = url($this->route) . '/'; } + public function serviceUri(): string + { + return $this->serviceUri; + } + public function endpoint(): string { return $this->endpoint; diff --git a/src/Interfaces/ServiceEndpointInterface.php b/src/Interfaces/ServiceEndpointInterface.php index 568953b60..3c4c7ba06 100644 --- a/src/Interfaces/ServiceEndpointInterface.php +++ b/src/Interfaces/ServiceEndpointInterface.php @@ -5,33 +5,68 @@ use Flat3\Lodata\Model; /** - * Interface for defining a custom OData service endpoint. + * Interface for defining a modular OData service endpoint in Laravel. * - * Implementers can use this interface to expose a specific service under a custom path, - * define its namespace, route behavior, and optionally provide a statically generated - * $metadata document. + * Implementers of this interface represent individually addressable OData services. + * Each mounted under its own URI segment and backed by a schema model. + * + * This enables clean separation of business domains and supports multi-endpoint + * discovery for modular application design. + * + * Configuration versus Declaration + * -------------------------------- + * The public URI segment used to expose a service is NOT determined by the + * implementing class itself, but by the service map in `config/lodata.php`: + * + * ```php + * 'endpoints' => [ + * 'users' => \App\OData\UsersEndpoint::class, + * 'budgets' => \App\OData\BudgetsEndpoint::class, + * ] + * ``` + * + * This keeps the routing surface under application control, and avoids + * conflicts when two modules declare the same internal segment. + * + * To implement an endpoint: + * - Extend `Flat3\Lodata\Endpoint` or implement this interface directly + * - Register the class in `config/lodata.php` under a unique segment key + * - Define the `discover()` method to expose entities via OData */ interface ServiceEndpointInterface { + /** + * Returns the ServiceURI segment name for this OData endpoint. + * + * This value is used in routing and metadata resolution. It Must be globally unique. + * + * @return string The segment (path identifier) of the endpoint + */ + public function serviceUri(): string; /** - * Returns the relative endpoint identifier within the OData service URI space. + * Returns the fully qualified URL for this OData endpoint. * - * This is the part that appears between the configured Lodata prefix and - * the `$metadata` segment, e.g.: - * https://:///$metadata + * This includes the application host, port, and the configured segment, + * https://:///, + * Example: https://example.com/odata/users/ * - * @return string The relative OData endpoint path + * This URL forms the base of the OData service space, and is used for navigation links + * and metadata discovery. + * + * @return string The full URL of the service endpoint */ public function endpoint(): string; /** - * Returns the full request route to this service endpoint. + * Returns the internal Laravel route path for this OData service endpoint. + * + * This is the relative URI path that Laravel uses to match incoming requests, + * typically composed of the configured Lodata prefix and the service segment. * - * This typically resolves to the route path used by Laravel to handle - * incoming requests for this specific service instance. + * Example: "odata/users" * - * @return string The full HTTP route to the endpoint + * @return string Relative route path for the endpoint */ public function route(): string; diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 78bdedc12..b7400387e 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,6 +4,7 @@ namespace Flat3\Lodata; +use RuntimeException; use Composer\InstalledVersions; use Flat3\Lodata\Controller\Monitor; use Flat3\Lodata\Controller\OData; @@ -14,6 +15,7 @@ use Flat3\Lodata\Helper\Flysystem; use Flat3\Lodata\Helper\DBAL; use Flat3\Lodata\Helper\Symfony; +use Flat3\Lodata\Interfaces\ServiceEndpointInterface; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Kernel; @@ -64,6 +66,12 @@ public function boot() } else if (array_key_exists($segments[1], $serviceUris)) { $clazz = $serviceUris[$segments[1]]; + if (!class_exists($clazz)) { + throw new RuntimeException(sprintf('Endpoint class `%s` does not exist', $clazz)); + } + if (!is_subclass_of($clazz, ServiceEndpointInterface::class)) { + throw new RuntimeException(sprintf('Endpoint class `%s` must implement Flat3\\Lodata\\Interfaces\\ServiceEndpointInterface', $clazz)); + } $service = new $clazz($segments[1]); } else { From ce66fd332a51fcc554d15a3e8f318ac2035eb003 Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Tue, 22 Jul 2025 13:43:23 +0200 Subject: [PATCH 33/34] chore: simplify ServiceProvider and externalize multi-endpoint support - remove "extra.laravel" from composer.json to disable auto-registration - always boot default Endpoint in ServiceProvider for consistent behavior - drop request path inspection logic for segmented endpoint resolution ==> apps requiring multiple OData endpoints should now implement and register their own ServiceProvider (see docs for example) --- doc/getting-started/endpoint.md | 129 ++++++++++++++++++++++++++++++++ src/ServiceProvider.php | 42 +---------- 2 files changed, 131 insertions(+), 40 deletions(-) diff --git a/doc/getting-started/endpoint.md b/doc/getting-started/endpoint.md index 68695a431..c37586201 100644 --- a/doc/getting-started/endpoint.md +++ b/doc/getting-started/endpoint.md @@ -9,6 +9,135 @@ By default, `flat3/lodata` exposes a **single global service endpoint**. However 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. +## Replacing the ServiceProvider + +To enable multiple service endpoints you need to replace `\Flat3\Lodata\ServiceProvider` with your own implementation that inspects the request path and boots the appropriate endpoint based on your configuration. + +Take this sample implementation: + +```php +mergeConfigFrom(__DIR__.'/../config.php', 'lodata'); + } + + public function boot() + { + if ($this->app->runningInConsole()) { + $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); + $this->bootServices(new Endpoint('')); + } + else { + $segments = explode('/', request()->path()); + + if ($segments[0] === config('lodata.prefix')) { + + $serviceUris = config('lodata.endpoints', []); + + if (0 === sizeof($serviceUris) || count($segments) === 1) { + $service = new Endpoint(''); + } + else if (array_key_exists($segments[1], $serviceUris)) { + $clazz = $serviceUris[$segments[1]]; + if (!class_exists($clazz)) { + throw new RuntimeException(sprintf('Endpoint class `%s` does not exist', $clazz)); + } + if (!is_subclass_of($clazz, ServiceEndpointInterface::class)) { + throw new RuntimeException(sprintf('Endpoint class `%s` must implement Flat3\\Lodata\\Interfaces\\ServiceEndpointInterface', $clazz)); + } + $service = new $clazz($segments[1]); + } + else { + $service = new Endpoint(''); + } + + $this->bootServices($service); + } + } + } + + private function bootServices(Endpoint $service): void + { + $this->app->instance(Endpoint::class, $service); + + $this->app->bind(DBAL::class, function (Application $app, array $args) { + return version_compare(InstalledVersions::getVersion('doctrine/dbal'), '4.0.0', '>=') ? new DBAL\DBAL4($args['connection']) : new DBAL\DBAL3($args['connection']); + }); + + $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); + + $model = $service->discover(new Model()); + assert($model instanceof Model); + + $this->app->instance(Model::class, $model); + + $this->app->alias(Model::class, 'lodata.model'); + + $this->app->bind(Response::class, function () { + return Kernel::VERSION_ID < 60000 ? new Symfony\Response5() : new Symfony\Response6(); + }); + + $this->app->bind(Filesystem::class, function () { + return class_exists('League\Flysystem\Adapter\Local') ? new Flysystem\Flysystem1() : new Flysystem\Flysystem3(); + }); + + $route = $service->route(); + $middleware = config('lodata.middleware', []); + + Route::get("{$route}/_lodata/odata.pbids", [PBIDS::class, 'get']); + Route::get("{$route}/_lodata/{identifier}.odc", [ODCFF::class, 'get']); + Route::resource("{$route}/_lodata/monitor", Monitor::class); + Route::any("{$route}{path}", [OData::class, 'handle'])->where('path', '(.*)')->middleware($middleware); + } +} +``` + +Register your new provider in `bootstrap/providers.php` instead of the original one. + +```php + [ + 'projects' => \App\Endpoints\ProjectEndpoint::class, + 'hr' => \App\Endpoints\HrEndpoint::class, +], +``` + +This setup enables segmented endpoint support for your Laravel app, while keeping you in full control of the boot logic and route behavior. + ## Defining Multiple Endpoints You can define service endpoints by registering them in your `config/lodata.php` configuration file: diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index b7400387e..c7692d700 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,7 +4,6 @@ namespace Flat3\Lodata; -use RuntimeException; use Composer\InstalledVersions; use Flat3\Lodata\Controller\Monitor; use Flat3\Lodata\Controller\OData; @@ -15,7 +14,6 @@ use Flat3\Lodata\Helper\Flysystem; use Flat3\Lodata\Helper\DBAL; use Flat3\Lodata\Helper\Symfony; -use Flat3\Lodata\Interfaces\ServiceEndpointInterface; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Kernel; @@ -45,47 +43,11 @@ public function boot() { if ($this->app->runningInConsole()) { $this->publishes([__DIR__.'/../config.php' => config_path('lodata.php')], 'config'); - $this->bootServices(new Endpoint('')); - } - else { - // Let’s examine the request path - $segments = explode('/', request()->path()); - - // we only kick off operation when path prefix is configured in lodata.php - // and bypass all other routes for performance - if ($segments[0] === config('lodata.prefix')) { - - // next look up the configured service endpoints - $serviceUris = config('lodata.endpoints', []); - - if (0 === sizeof($serviceUris) || count($segments) === 1) { - // when no locators are defined, or the global locator ist requested, - // enter global mode; this will ensure compatibility with prior - // versions of this package - $service = new Endpoint(''); - } - else if (array_key_exists($segments[1], $serviceUris)) { - $clazz = $serviceUris[$segments[1]]; - if (!class_exists($clazz)) { - throw new RuntimeException(sprintf('Endpoint class `%s` does not exist', $clazz)); - } - if (!is_subclass_of($clazz, ServiceEndpointInterface::class)) { - throw new RuntimeException(sprintf('Endpoint class `%s` must implement Flat3\\Lodata\\Interfaces\\ServiceEndpointInterface', $clazz)); - } - $service = new $clazz($segments[1]); - } - else { - // when no service definition could be found for the path segment, - // we assume global scope - $service = new Endpoint(''); - } - - $this->bootServices($service); - } } + $this->bootServices(new Endpoint('')); } - private function bootServices($service): void + private function bootServices(Endpoint $service): void { // register the $service, which is a singleton, with the container; this allows us // to fulfill all old ServiceProvider::route() and ServiceProvider::endpoint() From fdb8806e05631684037390237b36e9e6874b687e Mon Sep 17 00:00:00 2001 From: Michael Gerzabek Date: Tue, 22 Jul 2025 14:52:35 +0200 Subject: [PATCH 34/34] fix(NavigationProperty): adapt to changes introduced in PR #858 Resolve compatibility issue with `NavigationProperty::generatePropertyValue` after the introduction of targeted entity ID handling in upstream PR https://github.com/flat3/lodata/pull/858. Restores support for local extensions without breaking the new `$requestedTarget` logic. --- src/NavigationProperty.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NavigationProperty.php b/src/NavigationProperty.php index 6cfa33c02..49f25106d 100644 --- a/src/NavigationProperty.php +++ b/src/NavigationProperty.php @@ -228,7 +228,7 @@ protected function requestedTargetId(EntitySet $targetSet, NavigationRequest $na return null; } - $lexer = new Lexer(Str::after((string) $qualifiedId, ServiceProvider::route() . '/')); + $lexer = new Lexer(Str::after((string) $qualifiedId, app(Endpoint::class)->route() . '/')); $entity = $lexer->identifier(); if ($entity !== $targetSet->getName()) { throw new BadRequestException(