diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 443bef768..0989dd51b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -111,6 +111,7 @@ jobs:
- run: find examples/php -name '*.php' -print0 | xargs -0 -n1 php -l
- run: php -d zend.assertions=1 examples/php/scaffolding/assertions.php
- run: php -l examples/laravel/app/Demo.php
+ - run: find examples/symfony/src examples/symfony/config -name '*.php' -print0 | xargs -0 -n1 php -l
benchmark:
name: Benchmark
diff --git a/README.md b/README.md
index 04ef3c7ac..ed23ce9f0 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ PHPantom focuses on deep type intelligence. Here's how it compares:
| Closure parameter inference | ✅ | 🚧 | 🚧 | 🚧 | 🚧 |
| **Frameworks** | | | | | |
| Laravel & Blade | ✅ | ❌ | 🚧 | ❌ | 🚧🧩 |
-| Symfony & Twig | ❌ | ❌ | ❌ | 🚧 | 🧩 |
+| Symfony & Twig | ✅ | ❌ | ❌ | 🚧 | 🧩 |
| Drupal | 🚧 | ❌ | ❌ | ❌ | ✅🧩 |
| Other frameworks | 🚧 | 🚧 | 🚧 | 🚧 | 🧩 |
| **Refactoring** | | | | | |
@@ -64,7 +64,7 @@ Independent, re-runnable type-inference conformance results across twelve PHP en
> [!TIP]
-> **Want to verify?** Open [`examples/php/`](examples/php/) in your editor and trigger completion at the marked locations in `completion.php`. It exercises every type intelligence feature in the table, including edge cases where tools diverge. For Laravel specifically, open [`examples/laravel/`](examples/laravel/) — a standalone project with real Eloquent models, config, routes, views, and translations that exercises Eloquent property resolution, query builder chaining, scopes, custom collections, and go-to-definition for config keys, route names, and translation strings.
+> **Want to verify?** Open [`examples/php/`](examples/php/) in your editor and trigger completion at the marked locations in `completion.php`. It exercises every type intelligence feature in the table, including edge cases where tools diverge. Framework playgrounds live in [`examples/laravel/`](examples/laravel/) and [`examples/symfony/`](examples/symfony/).
## Context-Aware Intelligence
@@ -75,6 +75,8 @@ Independent, re-runnable type-inference conformance results across twelve PHP en
- **Type aliases and shapes.** `@phpstan-type`, `@phpstan-import-type`, and `object{...}` shapes all resolve through to completions.
- **The full modern language.** PHP 8.4 and 8.5 features resolve end-to-end: property hooks, asymmetric visibility, and the pipe operator behave like any other code.
- **Laravel.** Eloquent relationships, scopes, accessors, casts, and Builder chains resolve end-to-end. Macros behave like real methods. Container strings like `app('cache')` resolve to the bound class, `auth()->user()` resolves to your configured model, authorization strings resolve to the gate definition or policy method that declares them, and query string completion on both relation and column names. Blade templates get completion, hover, go-to-definition, and diagnostics through virtual PHP preprocessing. No ide-helper or database access required.
+- **Laravel.** Eloquent relationships, scopes, accessors, casts, and Builder chains resolve end-to-end. Macros behave like real methods. Container strings like `app('cache')` resolve to the bound class, `auth()->user()` resolves to your configured model, authorization strings resolve to the gate definition or policy method that declares them, and query string compleation on both relation and column names. Blade templates get completion, hover, go-to-definition, and diagnostics through virtual PHP preprocessing. No ide-helper or database access required.
+- **Symfony.** Container configuration, routes, Twig templates, translations, events, Messenger, forms, validation mappings, Doctrine metadata, and local configuration schemas participate in completion, navigation, references, rename, diagnostics, and code lenses across PHP, YAML, XML, XLIFF, and Twig.
- **Everything else you'd expect.** Generics, type narrowing, named arguments, destructuring, first-class callables, anonymous classes, `@deprecated` detection, and namespace segment drilling.
## Project Awareness
diff --git a/config-schema.json b/config-schema.json
index 3d745adad..7d646698b 100644
--- a/config-schema.json
+++ b/config-schema.json
@@ -13,6 +13,31 @@
"type": "string",
"description": "Override the detected PHP version (e.g. \"8.3\"). When unset, PHPantom infers from composer.json's platform or require.php.",
"pattern": "^\\d+\\.\\d+(\\.\\d+)?$"
+ },
+ "proxies": {
+ "type": "array",
+ "description": "Generated transparent-proxy discovery rules. Matching subclasses keep their PHP type, while project metadata is attributed to their real parent class.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "paths": {
+ "type": "array",
+ "description": "Workspace-relative PHP files, directories, or glob patterns to scan for generated proxy subclasses.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "marker-interface": {
+ "type": "string",
+ "description": "Fully-qualified interface that a generated subclass must directly implement to be treated as a transparent proxy."
+ }
+ },
+ "required": [
+ "paths",
+ "marker-interface"
+ ]
+ },
+ "default": []
}
}
},
@@ -168,6 +193,267 @@
}
}
},
+ "symfony": {
+ "type": "object",
+ "description": "Symfony runtime metadata recovered statically from compiled containers and configured PHP attributes.",
+ "properties": {
+ "container": {
+ "type": "object",
+ "description": "Controls static compiled-container discovery. Container PHP is read as text and is never executed.",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Read Symfony compiled-container metadata.",
+ "default": true
+ },
+ "environment": {
+ "type": "string",
+ "description": "Cache environment used by automatic var/cache/ discovery.",
+ "default": "dev"
+ },
+ "paths": {
+ "type": "array",
+ "description": "Optional workspace-relative compiled-container files, directories, or glob patterns. The newest useful match wins.",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "events": {
+ "type": "object",
+ "description": "Configures event-name matching and attribute adapters. Exact listener wiring comes from the compiled container.",
+ "properties": {
+ "ignored-prefixes": {
+ "type": "array",
+ "description": "Prefixes removed before event names are compared.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ignored-suffixes": {
+ "type": "array",
+ "description": "Suffixes removed before event names are compared.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "publishers": {
+ "type": "array",
+ "description": "Rules that derive Symfony event publishers from PHP method attributes.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "attribute": {
+ "type": "string",
+ "description": "Fully-qualified publisher attribute class."
+ },
+ "name-argument": {
+ "type": "string",
+ "description": "Named argument containing an explicit event name."
+ },
+ "name-position": {
+ "type": "integer",
+ "description": "Zero-based positional fallback for the explicit event-name argument.",
+ "minimum": 0
+ },
+ "dispatch-argument": {
+ "type": "string",
+ "description": "Named argument containing dispatch enum cases."
+ },
+ "dispatch-position": {
+ "type": "integer",
+ "description": "Zero-based positional fallback for the dispatch argument.",
+ "minimum": 0
+ },
+ "default-dispatch": {
+ "type": "array",
+ "description": "Dispatch names used when the attribute omits its dispatch argument.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "dispatch-cases": {
+ "type": "object",
+ "description": "Map from PHP enum case name to the event-name dispatch segment.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "name-template": {
+ "type": "string",
+ "description": "Derived event-name template. Supports {dispatch}, {class}, {class_snake}, {method}, {method_snake}, {method_suffix}, and {method_suffix_snake}."
+ },
+ "explicit-name-template": {
+ "type": "string",
+ "description": "Template used for explicit names. Supports the same placeholders plus {name}.",
+ "default": "{name}"
+ },
+ "default-methods": {
+ "type": "array",
+ "description": "Method names that omit the method suffix.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "skip": {
+ "type": "array",
+ "description": "Conditions that omit one derived dispatch when another attribute argument is set.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "dispatch": {
+ "type": "string"
+ },
+ "argument": {
+ "type": "string"
+ },
+ "position": {
+ "type": "integer",
+ "minimum": 0
+ }
+ },
+ "required": [
+ "dispatch",
+ "argument"
+ ]
+ }
+ }
+ },
+ "required": [
+ "attribute",
+ "name-template"
+ ]
+ }
+ },
+ "subscribers": {
+ "type": "array",
+ "description": "Optional rules that expose event names written in subscriber method attributes before or without a compiled container.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "attribute": {
+ "type": "string",
+ "description": "Fully-qualified subscriber attribute class."
+ },
+ "name-argument": {
+ "type": "string"
+ },
+ "name-position": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "transport-argument": {
+ "type": "string"
+ },
+ "transport-position": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "transport-cases": {
+ "type": "object",
+ "description": "Map from PHP enum case name to an event-name suffix.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "attribute"
+ ]
+ }
+ }
+ }
+ },
+ "expression-language": {
+ "type": "object",
+ "description": "Maps configured PHP attribute and constructor arguments to Symfony ExpressionLanguage strings and their PHP type contracts.",
+ "properties": {
+ "attributes": {
+ "type": "array",
+ "description": "Method-attribute arguments that contain an expression string or an array of expression strings.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "attribute": {
+ "type": "string",
+ "description": "Fully-qualified PHP method attribute class."
+ },
+ "argument": {
+ "type": "string",
+ "description": "Named argument containing expressions."
+ },
+ "position": {
+ "type": "integer",
+ "description": "Zero-based positional fallback for the expression argument.",
+ "minimum": 0
+ },
+ "method-parameters": {
+ "type": "boolean",
+ "description": "Bind expression roots to method parameters with the same name.",
+ "default": false
+ },
+ "bindings": {
+ "type": "object",
+ "description": "Map expression roots to return, parameter:, or class: type sources.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "attribute"
+ ]
+ }
+ },
+ "constructors": {
+ "type": "array",
+ "description": "Expression object constructors nested inside PHP method attributes.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "class": {
+ "type": "string",
+ "description": "Fully-qualified expression object class."
+ },
+ "argument": {
+ "type": "string",
+ "description": "Named constructor argument containing the expression."
+ },
+ "position": {
+ "type": "integer",
+ "description": "Zero-based positional fallback for the constructor argument.",
+ "minimum": 0
+ },
+ "inside-attribute-prefixes": {
+ "type": "array",
+ "description": "Only match constructors nested in attributes whose FQN starts with one of these prefixes. An empty list matches any attribute.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "method-parameters": {
+ "type": "boolean",
+ "description": "Bind expression roots to method parameters with the same name.",
+ "default": false
+ },
+ "bindings": {
+ "type": "object",
+ "description": "Map expression roots to return, parameter:, or class: type sources.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "class"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
"formatting": {
"type": "object",
"description": "Controls the formatting strategy. PHPantom ships a built-in formatter (PER-CS 2.0 style). Projects with php-cs-fixer or PHP_CodeSniffer in composer.json require-dev automatically use those tools instead. Explicit configuration here always takes priority.",
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 4b324a375..bb56138d4 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -67,6 +67,8 @@ src/
│
│ # Class & type resolution
├── resolution.rs # Multi-phase class/function lookup across files (find_or_load_class)
+├── proxy_metadata.rs # Transparent proxy → real-class relations for metadata consumers
+├── symfony/ # Compiled-container, event, and ExpressionLanguage adapters
├── class_lookup.rs # Subtype checks (is_subtype_of_typed) and class-lookup helpers
├── inheritance/ # Parent/trait/mixin member merging, generics substitution
├── virtual_members/ # Synthesized members: phpdoc.rs (@method/@property/@mixin) + laravel/ (one file per Eloquent/framework feature)
@@ -119,7 +121,8 @@ src/
├── move_cli/ # `move` CLI subcommand (class/namespace refactoring)
├── self_update.rs # Binary self-update
│
-│ # Blade & shared utilities
+│ # Framework resources & shared utilities
+├── framework.rs # Symfony/Doctrine PHP, YAML, XML, XLIFF, and Twig reference index
├── blade/ # Laravel Blade template support (directives, preprocessor, source map)
└── util.rs, text_position.rs, text_scan.rs, atom.rs, call_args.rs, return_collection.rs, toposort.rs, ci_map.rs, process.rs, progress.rs
@@ -143,6 +146,30 @@ diagnostics, hover, go-to-definition, and signature help, not just completion
below). Do not build a second type-resolution path: extend the engine here so
every consumer benefits.
+### Framework Resource Index
+
+**Symfony and Doctrine resources use a lightweight parallel reference index.**
+`framework.rs` scans PHP configurators and attributes alongside YAML, XML,
+XLIFF, and Twig resources. It records classes, members, paths, named framework
+symbols, form and validation properties, Messenger relationships, and local
+`TreeBuilder` keys. Definition, references, rename, highlights, completion,
+diagnostics, and code lenses consume that shared index without parsing
+non-PHP resources as PHP.
+
+### Framework Runtime Metadata
+
+Framework-generated files feed small metadata adapters instead of creating a
+second symbol resolver. `proxy_metadata.rs` is the shared proxy-to-real-class
+relation. `symfony/container.rs` reads compiled containers as text and exposes
+listener registrations and proxied service candidates; it never includes PHP.
+`symfony/events.rs` combines that exact runtime wiring with configured
+attribute rules, then serves go-to-definition, references, and code lenses.
+`symfony/expressions.rs` maps configured attribute and constructor arguments to
+method parameter, return, or fixed-class types, then delegates member chains to
+the shared PHP type engine for navigation and diagnostics.
+Metadata owners are canonicalized through the proxy relation before lookup, so
+all consumers agree on the real class without rewriting normal PHP types.
+
## External Crates
PHPantom uses several crates from the [Mago](https://github.com/carthage-software/mago)
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 86d5bb3b3..35cbbecce 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -27,6 +27,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Reference CodeLens.** A clickable reference count sits above the classes, functions, methods, properties, and constants a PHP file declares, and following one opens the same list Find References gives. This replaces the count that used to be drawn at the end of the declaration line, which could only be read, so the number appears once and in a place you can act on. A declaration nothing in the workspace names is answered from the index straight away, and the receiver of every member access in a candidate file is resolved once and kept in a compact semantic layer, so opening a large file does not turn into one expensive search per lens. Clients that can be asked to re-pull lenses are shown a member's count once it is ready rather than being made to resolve each one in turn. A lens whose count is being worked out reads `- references` and keeps its line, so editing a file does not shuffle every line in it up and down as counts come and go, and an edit only counts again what it can actually have changed: typing inside a method body leaves every count in the file standing, and a burst of keystrokes is answered once rather than once per keystroke. Contributed by @sidux.
- **Implementation CodeLens.** Interfaces and abstract classes, and the methods they declare, show a clickable implementation count, and following one lists every implementation. A method a class inherits unchanged or takes from a trait is listed at the declaration that supplies its body. The count replaces the one that used to be drawn at the end of an interface's or abstract class's declaration line, which could only be read. Contributed by @sidux.
- **Fully-qualified PHP classes navigate from YAML and XML.** Ctrl+Click a class name in any YAML key or value, or any XML attribute or text node, and PHPantom opens its PHP declaration without needing to know that file's schema. `Class::member` references navigate too. The same occurrences feed Find References and declaration CodeLens through the workspace reference index. Unknown and unqualified strings are left alone. Contributed by @sidux.
+- **Generated transparent proxies can be mapped back to their real classes.** Configure opt-in proxy paths and a marker interface under `[[php.proxies]]`; metadata read from YAML or XML then bubbles navigation, references, and member links to the real parent class without changing normal PHP type resolution. Contributed by @sidux.
+- **Symfony forms, validation, and configuration schemas.** Form field names and YAML/XML validation property mappings now complete or navigate against their data-class properties, validation constraint names navigate to constraint classes, and local `TreeBuilder` schemas provide YAML configuration completion, navigation, missing-key diagnostics, references, and code lenses. Contributed by @sidux.
+- **Configured Symfony ExpressionLanguage strings understand PHP members.** Declare the attribute or expression-object argument and map its variables to method parameters, the return type, or a fixed class. Ctrl+Click follows roots, properties, and method chains to PHP declarations, while missing members use the normal `unknown_member` warning. Package-specific names and contracts stay in `.phpantom.toml`. Contributed by @sidux.
+- **Symfony events and Messenger intelligence.** Named events declared by listener attributes or service tags and Messenger buses declared in configuration now complete, navigate, find references, diagnose missing project-local names, and show declaration-side code lenses. Event listener methods and Messenger message-to-handler relationships link directly to their PHP declarations. Contributed by @sidux.
+- **Symfony event publishers and listeners navigate in both directions.** PHPantom reads the final listener wiring from the generated container without executing it, while project-defined publisher attributes and event-name rules stay in `.phpantom.toml`. Event links and lenses follow transparent proxies back to the real class. Contributed by @sidux.
+- **Symfony translation intelligence.** Translation keys declared in YAML, XLIFF, and PHP catalogues now complete and navigate from PHP translator calls, translatable messages, and Twig filters with domain-aware references, diagnostics, and declaration-side code lenses. Contributed by @sidux.
+- **Symfony Twig template intelligence.** Template files now complete and navigate from controller rendering, templated emails, and Twig inheritance or inclusion expressions, with cross-file references and declaration-side code lenses. Missing project templates produce diagnostics with a create-template quick fix. Contributed by @sidux.
+- **Symfony route intelligence.** Route names and path parameters declared with attributes or in YAML, XML, and PHP now complete, navigate, find references, rename, highlight, and show declaration-side code lenses across controllers and Twig templates. Project-local missing route names produce diagnostics. Contributed by @sidux.
+- **Symfony service container intelligence.** Service IDs and parameters declared in YAML, XML, and PHP configuration now complete, navigate, find references, rename, highlight, and show declaration-side code lenses across configuration and PHP usage sites. Project-local missing IDs and parameters produce diagnostics. Contributed by @sidux.
+- **Symfony and Doctrine configuration navigation.** Service declarations, route controllers, and Doctrine mappings in YAML, XML, and Symfony PHP configurators now participate in go-to-definition, find references, rename, document highlights, and PHP code lenses. Renaming a class, namespace, or controller action rewrites each configuration occurrence in the spelling its file uses, and namespace and resource-path refactors also update matching framework configuration. Contributed by @sidux.
- **Headless consumers can export owned semantic records without starting an LSP transport.** The optional `semantic-export` feature accepts caller-supplied PHP documents, resolves them together, and returns deterministic declarations, occurrences, calls, byte ranges, and document diagnostics through batch or streaming APIs. An independent `offline-stubs` feature guarantees that missing stubs do not trigger a build-time download. Contributed by @aaaaaandrew.
- **`analyze` takes more than one path.** `phpantom_lsp analyze app/ lib/Helper.php tests/` scans the union of everything named, mixing directories and single files freely, so a pre-commit hook or a CI step can hand it exactly the paths that changed instead of running the whole project or invoking the binary once per path. Overlapping arguments are reported once, and a path that does not exist still stops the run with exit code 2. Naming no path scans the entire project, as before.
- **Blade directives a project registers itself.** A directive declared with `Blade::directive('priceTag', …)` or `Blade::if('bakeryOpen', …)` in a service provider is now read off that registration, so a template writing it gets the same treatment as one writing a directive Blade ships: the name is offered while it is being typed, and the expression the directive is handed stays real PHP whose types are checked, instead of the whole thing being masked as markup. `Blade::if()` registers four directives rather than one, and all four (`@bakeryOpen`, `@unlessbakeryOpen`, `@elsebakeryOpen`, `@endbakeryOpen`) are recognised as the block they form. A directive registered while the editor is open applies to the templates already open.
@@ -63,6 +73,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- **Symfony PHP resource scanning handles non-ASCII source safely.** Indexing framework references no longer crashes when a multibyte character falls near the bounded call-context scan window. Contributed by @sidux.
+- **Code lenses with several targets keep every destination.** Symfony and Doctrine annotations that point to more than one matching declaration now pass the full location list to the editor instead of opening only the first. Contributed by @sidux.
+- **Whitespace-only PHP strings no longer crash Symfony indexing.** A string containing only spaces could produce an invalid source range while PHPantom scanned Symfony-aware PHP files, terminating the request instead of ignoring the empty value. Contributed by @sidux.
- **Go to Implementation finds a method a class takes from a trait.** A class that implements an interface method through a `use`d trait was left out of the method's implementations, so the trait's method never appeared among them. It is now listed, alongside methods inherited unchanged from a parent class. Contributed by @sidux.
- **A constant defined in terms of other constants has a type.** `const TWO = ONE * 2;` inside a namespace used to have no type, because the `ONE` it names was only looked up in the global namespace. A class constant that divides, concatenates, or spreads other constants had no type either. `self::ONE / self::THREE` is now a `float` (or the exact integer when the division comes out even), `'The value of THREE is ' . self::THREE` is the string `'The value of THREE is 3'`, and `[...parent::KEYS, 'c' => 'c']` is the full array shape.
- **A by-reference parameter a callee sets before returning early reads back with every value it can hold.** With `if ($s === null) { $s = 5; return; } $s = 4;` in the callee, the caller's variable came out of the call as `4`, because only the end of the function body was read and a `return` never gets there. Each `return` now contributes what the parameter holds at that point, so the variable reads as `4|5`.
@@ -429,6 +442,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **A class named `Scalar` or `Numeric` is a class, not a PHPDoc pseudo-type.** `scalar` and `numeric` have no native spelling in PHP, so a project may name a class either of them, and nikic/php-parser does exactly that with `PhpParser\Node\Scalar`. Any capitalised spelling was folded into the pseudo-type instead of being resolved through the file's imports, which left the name unqualified and every check against it failing: passing a `Scalar` to a parameter typed as its own parent was reported as a mismatch, in a native type hint, a `@param`, a `@return`, and a `@implements` type argument alike. The all-lowercase spellings keep their PHPDoc meaning, which is the same rule already applied to `Number`, `Integer`, `Boolean`, `Double`, and `Resource`.
- **An `&&` operand that pins a value to one class outranks a later operand that only lists alternatives.** `$bound instanceof GenericType && ($class === GenericType::class || $bound instanceof TemplateType)` read the two operands as peers and answered `GenericType|TemplateType`, so passing the value on to anything expecting a `GenericType` was reported as a mismatch — even though the first operand alone settles the question. The disjunction can only narrow the value further, never widen it past what was already proven. A disjunction still narrows on its own when nothing in the chain pinned the subject down.
- **Reference and CodeLens annotations reuse the completed workspace index.** Resolving many lenses in a large project no longer starts another full filesystem walk for every declaration. Concurrent annotations share the first indexing pass, while an explicit Find References command still refreshes once so files created without an editor notification remain discoverable. Contributed by @sidux.
+- **Reference CodeLens stays responsive when a project has many framework resources.** Symfony and Doctrine class/member links now use an incremental inverted index instead of scanning every YAML/XML reference for each PHP declaration. Contributed by @sidux.
+- **Doctrine relationship CodeLens stays bounded on large workspaces.** Entity-to-repository pairs are indexed as mapping resources change instead of rescanning every YAML/XML file per lens. Reverse repository lenses use those mappings directly and apply the standard naming convention without repeatedly resolving repository candidates for every indexed class. Contributed by @sidux.
- **`array{…}|null` and `?array{…}` are the same nullable shape.** The two spellings were tracked as separate types, so a branch that wrote one left its shape standing beside the one another branch had already established: a variable holding `array{a: int}|null` and then reassigned `array{a: int, b: string}|null` under an `if` came out as `array{a: int}|null|array{a: int, b: string}`, one alternative per branch it was written in. Hover, and every check reading past the merge, now see the single folded `?array{a: int, b?: string}`, which is what the same code written without the `null` already reported. Contributed by @sidux.
- **Argument checks accept the widenings PHP performs and the types the engine admits it does not know.** Four shapes of correct code were reported as type mismatches: a bounded `int<0, max>` passed to a `float` parameter, even though PHP widens an integer to a float on the way in; a `class-string` passed to `non-empty-string`, even though a string that names a class always has content; an `array-key` passed to `int` or to `string`, which is the key type of an array nobody described rather than a value measured to be two things; and a closure body doing `$a & $b` on untyped parameters, which produces a string from two strings just as readily as an int from two numbers.
- **`get_class($x) === Foo::class` narrows the same subjects `instanceof` does.** The identity check only pinned a plain variable, so `get_class($this->held) === Sub::class` and `get_class($items[0]) === Sub::class` left the subject at its declared type and every member read past the check was reported as missing. A property fetch, an array element, and a call result are all narrowed now, in the `$x::class === Foo::class` spelling as well.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index a205be34f..8bd346977 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -10,7 +10,7 @@ Thanks for your interest in contributing!
## Before Submitting a PR
-All six CI checks must pass with zero warnings and zero failures:
+All CI checks must pass with zero warnings and zero failures:
```bash
cargo test
@@ -19,10 +19,11 @@ cargo fmt --check
find examples/php -name '*.php' -print0 | xargs -0 -n1 php -l
php -d zend.assertions=1 examples/php/scaffolding/assertions.php
php -l examples/laravel/app/Demo.php
+find examples/symfony/src examples/symfony/config -name '*.php' -exec php -l {} \;
phpantom_lsp analyze --project-root examples/laravel --no-colour
```
-Note that clippy runs twice, once for library code and once including test code. The `php -l` checks ensure the `examples/php/` playground remains valid PHP. The `php -d zend.assertions=1` run executes `assertions.php`'s `runDemoAssertions()` to verify that `scaffolding/scaffolding.php`'s stubs actually return what their docblocks claim. The final `php -l` and `phpantom_lsp analyze` runs check `examples/laravel/` for syntax errors and diagnostic regressions. `app/Demo.php` carries three deliberate mistakes: `Artisan::call('does:not-exist')` demonstrates `invalid_laravel_command`, and one `view('welcome', …)` call both leaves out a variable the template declares and passes a misspelled key, demonstrating `missing_view_variable` and `unused_view_variable`. So the analyze run must report exactly `[ERROR] Found 3 errors` on those two lines, not `[OK] No errors`; any other count, or an error on a different line, is a regression.
+Note that clippy runs twice, once for library code and once including test code. The `php -l` checks keep the PHP and framework playgrounds valid. The `php -d zend.assertions=1` run executes `assertions.php`'s `runDemoAssertions()` to verify that `scaffolding/scaffolding.php`'s stubs actually return what their docblocks claim. The final `php -l` and `phpantom_lsp analyze` runs check `examples/laravel/` for syntax errors and diagnostic regressions. `app/Demo.php` carries three deliberate mistakes: `Artisan::call('does:not-exist')` demonstrates `invalid_laravel_command`, and one `view('welcome', …)` call both leaves out a variable the template declares and passes a misspelled key, demonstrating `missing_view_variable` and `unused_view_variable`. So the analyze run must report exactly `[ERROR] Found 3 errors` on those two lines, not `[OK] No errors`; any other count, or an error on a different line, is a regression.
## Code Style
@@ -38,7 +39,7 @@ Note that clippy runs twice, once for library code and once including test code.
- A helper (a request wrapper, a label extractor, a position finder) that three or more test files would define belongs in `tests/integration/common/mod.rs`, not copied into each file; check there before writing one
- Inline `#[cfg(test)]` modules in `src/` are for unit tests of private helpers; a test that drives a `Backend` through a public handler goes in `tests/integration/`
- Test the happy path, edge cases, and interactions with existing features
-- When adding a feature, update the matching demo file under `examples/php/` with working examples (and verify with `find examples/php -name '*.php' -print0 | xargs -0 -n1 php -l`). For Laravel-specific features, also update `examples/laravel/app/Demo.php` (and verify with `php -l examples/laravel/app/Demo.php`).
+- When adding a feature, update the matching demo file under `examples/php/` with working examples (and verify with `find examples/php -name '*.php' -print0 | xargs -0 -n1 php -l`). For Laravel-specific features, also update `examples/laravel/app/Demo.php` (and verify with `php -l examples/laravel/app/Demo.php`). Put other framework-specific examples in the matching `examples//` project and lint its PHP files.
See [BUILDING.md](BUILDING.md) for more on running tests and manual LSP testing.
diff --git a/docs/configuration.md b/docs/configuration.md
index c260811ca..fe9f8b8d8 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -61,6 +61,122 @@ The full schema is at [`config-schema.json`](https://github.com/PHPantom-dev/php
| --------- | ------ | --------------------------- | ----------- |
| `version` | string | Inferred from composer.json | Override the detected PHP version (e.g. `"8.3"`). |
+#### `[[php.proxies]]`
+
+Declare generated transparent-proxy subclasses so metadata found on the
+generated class is attributed to its real parent class. PHPantom scans only
+the listed workspace-relative files, directories, or globs. A class must
+directly implement `marker-interface`; an ordinary subclass in the same path
+is left alone.
+
+```toml
+[[php.proxies]]
+paths = ["var/cache/*/generated-proxies/*.php"]
+marker-interface = 'ProxyManager\Proxy\AccessInterceptorValueHolderInterface'
+```
+
+This does not replace the proxy class in PHP type resolution. It gives project
+metadata features one shared relation to the parent class; YAML/XML navigation
+uses that relation directly.
+
+### `[symfony]`
+
+PHPantom can read Symfony's generated dependency-injection container as text to
+recover final event-listener wiring. It never includes or executes the
+container PHP. By default it checks `var/cache/dev`, tries the newest generated
+container first, and skips stale wrapper files that contain no useful wiring.
+
+#### `[symfony.container]`
+
+| Key | Type | Default | Description |
+| ------------- | -------- | ------- | ----------- |
+| `enabled` | bool | `true` | Read compiled-container metadata. |
+| `environment` | string | `"dev"` | Cache environment used by automatic discovery. |
+| `paths` | string[] | unset | Optional workspace-relative files, directories, or globs. The newest useful match wins. |
+
+#### `[symfony.events]`
+
+The compiled container supplies exact `addListener()` registrations. Publisher
+attributes and their naming convention are package choices, so they are
+declarative rules. This example models an attribute package without adding that
+package to PHPantom:
+
+```toml
+[symfony.container]
+environment = "dev"
+
+[symfony.events]
+ignored-prefixes = ["use_case."]
+ignored-suffixes = [".async"]
+
+[[symfony.events.publishers]]
+attribute = 'Acme\Event\Publish'
+name-argument = "name"
+name-position = 2
+dispatch-argument = "dispatch"
+dispatch-position = 4
+default-dispatch = ["post"]
+dispatch-cases = { PRE = "pre", POST = "post", EXCEPTION = "exception" }
+name-template = "{dispatch}.{class_snake}{method_suffix_snake}"
+explicit-name-template = "{name}"
+default-methods = ["execute", "__invoke"]
+
+[[symfony.events.publishers.skip]]
+dispatch = "post"
+argument = "messageClass"
+position = 5
+
+[[symfony.events.subscribers]]
+attribute = 'Acme\Event\Listen'
+name-argument = "name"
+name-position = 0
+transport-argument = "transport"
+transport-position = 2
+transport-cases = { ASYNC = ".async" }
+```
+
+Argument positions are zero-based fallbacks for positional PHP attribute
+arguments. Named arguments win. Publisher templates support `{dispatch}`,
+`{class}`, `{class_snake}`, `{method}`, `{method_snake}`, `{method_suffix}`,
+`{method_suffix_snake}`, and `{name}`. A `skip` rule omits one derived dispatch
+when another argument is set, such as an event sent to Messenger instead of
+Symfony's event dispatcher.
+
+The result is bidirectional go-to-definition, references, and `Symfony event`
+code lenses between publisher and listener methods. Listener classes that are
+configured transparent proxies use the shared `[[php.proxies]]` relation, so
+the links land on the real class.
+
+#### `[symfony.expression-language]`
+
+Declare which attribute or constructor arguments contain ExpressionLanguage
+strings. PHPantom then uses the normal PHP type engine for member navigation and
+`unknown_member` diagnostics.
+
+```toml
+[[symfony.expression-language.attributes]]
+attribute = 'Acme\Expression\Attribute\Rule'
+argument = "tags"
+position = 3
+method-parameters = true
+
+[[symfony.expression-language.constructors]]
+class = 'Symfony\Component\ExpressionLanguage\Expression'
+position = 0
+inside-attribute-prefixes = [
+ 'Acme\Expression\Attribute\',
+ 'Vendor\Policy\Attribute\',
+]
+bindings = { request = "parameter:0", response = "return" }
+```
+
+An attribute rule accepts one string or an array of strings. Named arguments
+win over the zero-based positional fallback. `method-parameters = true` maps
+each expression root to a same-named method parameter. Explicit `bindings` can
+map a root to `parameter:0`, `parameter:name`, `return`, or `class:FQN`.
+Constructor prefixes keep a shared expression class scoped to attributes that
+use the declared variable contract; an empty prefix list matches any attribute.
+
### `[diagnostics]`
| Key | Type | Default | Description |
diff --git a/docs/index.md b/docs/index.md
index 57e7e4909..d6a7975d7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -23,6 +23,8 @@ You may want to jump to:
- **Deep type intelligence.** Generics, conditional return types, closure parameter inference, array shapes, PHPStan types.
- **Laravel support.** Eloquent relationships, scopes, accessors, casts, Builder chains, macros, Blade templates -- no ide-helper or database access required.
- **Fast.** Ready in 2 seconds on a 30K-file Laravel codebase, cold or warm. No disk cache.
+- **Symfony support.** Navigate and refactor container services, routes, Twig templates, translations, events, Messenger handlers, forms, validation mappings, Doctrine metadata, and configuration schemas across PHP and resource files.
+- **Fast.** 5 seconds to ready on a 21K-file codebase. 360 MB RAM. No disk cache.
- **PHPStan, PHPCS, and Mago integration.** Run external tools on save and surface their diagnostics in the editor.
- **CLI tools.** Batch diagnostics (`analyze`) and automated fixes (`fix`) for CI and bulk cleanup.
- **Refactoring.** Rename, extract method/function/variable/constant/interface, implement interface methods, promote constructor parameters, modernize syntax.
diff --git a/examples/symfony/.gitignore b/examples/symfony/.gitignore
new file mode 100644
index 000000000..cac762f1d
--- /dev/null
+++ b/examples/symfony/.gitignore
@@ -0,0 +1,2 @@
+/vendor/
+/.idea/
diff --git a/examples/symfony/README.md b/examples/symfony/README.md
new file mode 100644
index 000000000..1dadc1c7f
--- /dev/null
+++ b/examples/symfony/README.md
@@ -0,0 +1,29 @@
+# Symfony Demo Project for PHPantom LSP
+
+A standalone editor playground for PHPantom's Symfony and Doctrine
+intelligence. It intentionally uses attributes, YAML, XML, PHP configurators,
+Twig, and translation catalogues together.
+
+## What to try
+
+- Open `src/Controller/DemoController.php` and use completion,
+ go-to-definition, find references, rename, and diagnostics on route names,
+ route parameters, service IDs, parameters, templates, translations, and
+ event names.
+- Open `src/Entity/User.php` to see code lenses for Doctrine mappings, form
+ fields, and validation properties.
+- Open `src/Message/SendWelcomeEmail.php` or its handler to navigate the
+ Messenger relationship in either direction.
+- Open `config/services.php` to navigate between a PHP service declaration and
+ its class. YAML and XML references work the same way.
+- Open `config/packages/acme_demo.yaml` to complete and navigate keys from the
+ local `TreeBuilder` schema.
+
+## Getting started
+
+1. Optionally run `composer install` here to install the real Symfony classes.
+2. Open this directory as a project or workspace folder in your editor.
+3. Trigger completion or go-to-definition inside the example strings.
+
+The files are a language-server playground, not a bootable Symfony
+application, so no kernel or database is required.
diff --git a/examples/symfony/composer.json b/examples/symfony/composer.json
new file mode 100644
index 000000000..09dbe8de3
--- /dev/null
+++ b/examples/symfony/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "phpantom/symfony-demo",
+ "description": "Symfony intelligence playground for PHPantom",
+ "license": "MIT",
+ "type": "project",
+ "require": {
+ "php": ">=8.2",
+ "doctrine/doctrine-bundle": "^2.10",
+ "symfony/form": "^7.0",
+ "symfony/framework-bundle": "^7.0",
+ "symfony/messenger": "^7.0",
+ "symfony/twig-bundle": "^7.0",
+ "symfony/validator": "^7.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "App\\": "src/"
+ }
+ },
+ "config": {
+ "platform": {
+ "php": "8.2.0"
+ },
+ "sort-packages": true
+ }
+}
diff --git a/examples/symfony/composer.lock b/examples/symfony/composer.lock
new file mode 100644
index 000000000..90f3e6cf8
--- /dev/null
+++ b/examples/symfony/composer.lock
@@ -0,0 +1,4250 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "c22dc3058b038d5e4288a91507f9d71f",
+ "packages": [
+ {
+ "name": "doctrine/dbal",
+ "version": "4.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/dbal.git",
+ "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce",
+ "reference": "fb9e0ffe15e1590e24dc61c0c0a23f9a33ee42ce",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.1.5",
+ "php": "^8.2",
+ "psr/cache": "^1|^2|^3",
+ "psr/log": "^1|^2|^3"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "14.0.0",
+ "fig/log-test": "^1",
+ "jetbrains/phpstorm-stubs": "2023.2",
+ "phpstan/phpstan": "2.1.30",
+ "phpstan/phpstan-phpunit": "2.0.7",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "11.5.50",
+ "slevomat/coding-standard": "8.27.1",
+ "squizlabs/php_codesniffer": "4.0.1",
+ "symfony/cache": "^6.3.8|^7.0|^8.0",
+ "symfony/console": "^5.4|^6.3|^7.0|^8.0"
+ },
+ "suggest": {
+ "symfony/console": "For helpful console commands such as SQL execution and import of files."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\DBAL\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ }
+ ],
+ "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
+ "homepage": "https://www.doctrine-project.org/projects/dbal.html",
+ "keywords": [
+ "abstraction",
+ "database",
+ "db2",
+ "dbal",
+ "mariadb",
+ "mssql",
+ "mysql",
+ "oci8",
+ "oracle",
+ "pdo",
+ "pgsql",
+ "postgresql",
+ "queryobject",
+ "sasql",
+ "sql",
+ "sqlite",
+ "sqlserver",
+ "sqlsrv"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/dbal/issues",
+ "source": "https://github.com/doctrine/dbal/tree/4.4.4"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T14:34:40+00:00"
+ },
+ {
+ "name": "doctrine/deprecations",
+ "version": "1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/deprecations.git",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<=7.5 || >=14"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "suggest": {
+ "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Deprecations\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
+ "homepage": "https://www.doctrine-project.org/",
+ "support": {
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
+ },
+ "time": "2026-02-07T07:09:04+00:00"
+ },
+ {
+ "name": "doctrine/doctrine-bundle",
+ "version": "2.19.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/DoctrineBundle.git",
+ "reference": "07b90f707b82981097731c419f546e7ba97fba3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/07b90f707b82981097731c419f546e7ba97fba3c",
+ "reference": "07b90f707b82981097731c419f546e7ba97fba3c",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^3.7.0 || ^4.0",
+ "doctrine/deprecations": "^1.0",
+ "doctrine/persistence": "^3.1 || ^4",
+ "doctrine/sql-formatter": "^1.0.1",
+ "php": "^8.1",
+ "symfony/cache": "^6.4 || ^7.0",
+ "symfony/config": "^6.4 || ^7.0",
+ "symfony/console": "^6.4 || ^7.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0",
+ "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3",
+ "symfony/framework-bundle": "^6.4 || ^7.0",
+ "symfony/service-contracts": "^2.5 || ^3"
+ },
+ "conflict": {
+ "doctrine/annotations": ">=3.0",
+ "doctrine/cache": "< 1.11",
+ "doctrine/orm": "<2.17 || >=4.0",
+ "symfony/var-exporter": "< 6.4.1 || 7.0.0",
+ "twig/twig": "<2.13 || >=3.0 <3.0.4 || >=5"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1 || ^2",
+ "doctrine/cache": "^1.11 || ^2.0",
+ "doctrine/coding-standard": "^14",
+ "doctrine/orm": "^2.17 || ^3.1",
+ "friendsofphp/proxy-manager-lts": "^1.0",
+ "phpstan/phpstan": "2.1.1",
+ "phpstan/phpstan-phpunit": "2.0.3",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^10.5.53 || ^12.3.10",
+ "psr/log": "^1.1.4 || ^2.0 || ^3.0",
+ "symfony/doctrine-messenger": "^6.4 || ^7.0",
+ "symfony/event-dispatcher": "^6.4 || ^7.0",
+ "symfony/expression-language": "^6.4 || ^7.0",
+ "symfony/http-kernel": "^6.4 || ^7.0",
+ "symfony/messenger": "^6.4 || ^7.0",
+ "symfony/property-info": "^6.4 || ^7.0",
+ "symfony/runtime": "^6.4 || ^7.0",
+ "symfony/security-bundle": "^6.4 || ^7.0",
+ "symfony/stopwatch": "^6.4 || ^7.0",
+ "symfony/string": "^6.4 || ^7.0",
+ "symfony/twig-bridge": "^6.4 || ^7.0",
+ "symfony/validator": "^6.4 || ^7.0",
+ "symfony/var-exporter": "^6.4.1 || ^7.0.1",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.0",
+ "symfony/yaml": "^6.4 || ^7.0",
+ "twig/twig": "^2.14.7 || ^3.0.4 || ^4"
+ },
+ "suggest": {
+ "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
+ "ext-pdo": "*",
+ "symfony/web-profiler-bundle": "To use the data collector."
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Bundle\\DoctrineBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ },
+ {
+ "name": "Doctrine Project",
+ "homepage": "https://www.doctrine-project.org/"
+ }
+ ],
+ "description": "Symfony DoctrineBundle",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "database",
+ "dbal",
+ "orm",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/DoctrineBundle/issues",
+ "source": "https://github.com/doctrine/DoctrineBundle/tree/2.19.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-23T14:52:05+00:00"
+ },
+ {
+ "name": "doctrine/event-manager",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/event-manager.git",
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf",
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "conflict": {
+ "doctrine/common": "<2.9"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "phpdocumentor/guides-cli": "^1.4",
+ "phpstan/phpstan": "^2.1.32",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
+ "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
+ "keywords": [
+ "event",
+ "event dispatcher",
+ "event manager",
+ "event system",
+ "events"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/event-manager/issues",
+ "source": "https://github.com/doctrine/event-manager/tree/2.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-29T07:11:08+00:00"
+ },
+ {
+ "name": "doctrine/persistence",
+ "version": "4.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/persistence.git",
+ "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/persistence/zipball/49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b",
+ "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1",
+ "doctrine/event-manager": "^1 || ^2",
+ "php": "^8.1",
+ "psr/cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "phpstan/phpstan": "2.1.30",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^10.5.58 || ^12",
+ "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Persistence\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.",
+ "homepage": "https://www.doctrine-project.org/projects/persistence.html",
+ "keywords": [
+ "mapper",
+ "object",
+ "odm",
+ "orm",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/persistence/issues",
+ "source": "https://github.com/doctrine/persistence/tree/4.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-26T12:12:52+00:00"
+ },
+ {
+ "name": "doctrine/sql-formatter",
+ "version": "1.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/sql-formatter.git",
+ "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806",
+ "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ergebnis/phpunit-slow-test-detector": "^2.20",
+ "phpstan/phpstan": "^2.1.31",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "bin": [
+ "bin/sql-formatter"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\SqlFormatter\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeremy Dorn",
+ "email": "jeremy@jeremydorn.com",
+ "homepage": "https://jeremydorn.com/"
+ }
+ ],
+ "description": "a PHP SQL highlighting library",
+ "homepage": "https://github.com/doctrine/sql-formatter/",
+ "keywords": [
+ "highlight",
+ "sql"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/sql-formatter/issues",
+ "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4"
+ },
+ "time": "2026-02-08T16:21:46+00:00"
+ },
+ {
+ "name": "psr/cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/3.0.0"
+ },
+ "time": "2021-02-03T23:26:27+00:00"
+ },
+ {
+ "name": "psr/clock",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/clock.git",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Clock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for reading the clock.",
+ "homepage": "https://github.com/php-fig/clock",
+ "keywords": [
+ "clock",
+ "now",
+ "psr",
+ "psr-20",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/clock/issues",
+ "source": "https://github.com/php-fig/clock/tree/1.0.0"
+ },
+ "time": "2022-11-25T14:36:26+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/event-dispatcher",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/event-dispatcher.git",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\EventDispatcher\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Standard interfaces for event handling.",
+ "keywords": [
+ "events",
+ "psr",
+ "psr-14"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/event-dispatcher/issues",
+ "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
+ },
+ "time": "2019-01-08T18:20:26+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "symfony/cache",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache.git",
+ "reference": "c1e7abe8e8c9b315d6d8b86446ffd9cf73679303"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/c1e7abe8e8c9b315d6d8b86446ffd9cf73679303",
+ "reference": "c1e7abe8e8c9b315d6d8b86446ffd9cf73679303",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/cache": "^2.0|^3.0",
+ "psr/log": "^1.1|^2|^3",
+ "symfony/cache-contracts": "^3.6",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "<3.6",
+ "ext-redis": "<6.1",
+ "ext-relay": "<0.12.1",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/http-kernel": "<6.4",
+ "symfony/var-dumper": "<6.4"
+ },
+ "provide": {
+ "psr/cache-implementation": "2.0|3.0",
+ "psr/simple-cache-implementation": "1.0|2.0|3.0",
+ "symfony/cache-implementation": "1.1|2.0|3.0"
+ },
+ "require-dev": {
+ "cache/integration-tests": "dev-master",
+ "doctrine/dbal": "^3.6|^4",
+ "predis/predis": "^1.1|^2.0",
+ "psr/simple-cache": "^1.0|^2.0|^3.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Cache\\": ""
+ },
+ "classmap": [
+ "Traits/ValueWrapper.php"
+ ],
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Provides extended PSR-6, PSR-16 (and tags) implementations",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "caching",
+ "psr6"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-29T04:03:42+00:00"
+ },
+ {
+ "name": "symfony/cache-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache-contracts.git",
+ "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2",
+ "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/cache": "^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Cache\\": ""
+ }
+ },
+ "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": "Generic abstractions related to caching",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/clock",
+ "version": "v7.4.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/clock.git",
+ "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111",
+ "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/clock": "^1.0",
+ "symfony/polyfill-php83": "^1.28"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/now.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Clock\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Decouples applications from the system clock",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clock",
+ "psr20",
+ "time"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/clock/tree/v7.4.8"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-24T13:12:05+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "b18e33881ef402ad940f36e85935420624009bf4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/b18e33881ef402ad940f36e85935420624009bf4",
+ "reference": "b18e33881ef402ad940f36e85935420624009bf4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/filesystem": "^7.1|^8.0",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "symfony/finder": "<6.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-22T12:54:40+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "088ec6fe0ef6819cbc301174093b6bfa4ad26930"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/088ec6fe0ef6819cbc301174093b6bfa4ad26930",
+ "reference": "088ec6fe0ef6819cbc301174093b6bfa4ad26930",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^7.2|^8.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<6.4",
+ "symfony/dotenv": "<6.4",
+ "symfony/event-dispatcher": "<6.4",
+ "symfony/lock": "<6.4",
+ "symfony/process": "<6.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-27T13:51:00+00:00"
+ },
+ {
+ "name": "symfony/dependency-injection",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "b7825671c553af46a98c744e23f37f972aee6427"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b7825671c553af46a98c744e23f37f972aee6427",
+ "reference": "b7825671c553af46a98c744e23f37f972aee6427",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^3.6",
+ "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2",
+ "symfony/config": "<6.4",
+ "symfony/finder": "<6.4",
+ "symfony/yaml": "<6.4"
+ },
+ "provide": {
+ "psr/container-implementation": "1.1|2.0",
+ "symfony/service-implementation": "1.1|2.0|3.0"
+ },
+ "require-dev": {
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DependencyInjection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows you to standardize and centralize the way objects are constructed in your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dependency-injection/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-22T08:40:50+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
+ "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "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": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/doctrine-bridge",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/doctrine-bridge.git",
+ "reference": "76ebc0ca1680f766c57cf23d334784ef8e83a5e7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/76ebc0ca1680f766c57cf23d334784ef8e83a5e7",
+ "reference": "76ebc0ca1680f766c57cf23d334784ef8e83a5e7",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/event-manager": "^2",
+ "doctrine/persistence": "^3.1|^4",
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.8",
+ "doctrine/dbal": "<3.6",
+ "doctrine/lexer": "<1.1",
+ "doctrine/orm": "<2.15",
+ "symfony/cache": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/form": "<6.4.6|>=7,<7.0.6",
+ "symfony/http-foundation": "<6.4",
+ "symfony/http-kernel": "<6.4",
+ "symfony/lock": "<6.4",
+ "symfony/messenger": "<6.4",
+ "symfony/property-info": "<6.4",
+ "symfony/security-bundle": "<6.4",
+ "symfony/security-core": "<6.4",
+ "symfony/validator": "<7.4"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.8|^2.0",
+ "doctrine/data-fixtures": "^1.1|^2",
+ "doctrine/dbal": "^3.6|^4",
+ "doctrine/orm": "^2.15|^3",
+ "psr/log": "^1|^2|^3",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/doctrine-messenger": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/form": "^7.2|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/property-info": "^6.4|^7.0|^8.0",
+ "symfony/security-core": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
+ "symfony/type-info": "^7.1.8|^8.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Doctrine\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides integration for Doctrine with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T15:13:06+00:00"
+ },
+ {
+ "name": "symfony/error-handler",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/error-handler.git",
+ "reference": "d49f6a19f326db41ae7103bdc38e3eb35a791261"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/d49f6a19f326db41ae7103bdc38e3eb35a791261",
+ "reference": "d49f6a19f326db41ae7103bdc38e3eb35a791261",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/log": "^1|^2|^3",
+ "symfony/polyfill-php85": "^1.32",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ },
+ "conflict": {
+ "symfony/deprecation-contracts": "<2.5",
+ "symfony/http-kernel": "<6.4"
+ },
+ "require-dev": {
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
+ "symfony/webpack-encore-bundle": "^1.0|^2.0"
+ },
+ "bin": [
+ "Resources/bin/patch-type-declarations"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\ErrorHandler\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "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.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T15:13:06+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/336e7f3b9e95aba04f93ea9143920c2186abfbb9",
+ "reference": "336e7f3b9e95aba04f93ea9143920c2186abfbb9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/event-dispatcher-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<6.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "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.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T15:13:06+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e",
+ "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/event-dispatcher": "^1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "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": "Generic abstractions related to dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/ff16a16bf87fdf264638b8f6995b3515975e3c79",
+ "reference": "ff16a16bf87fdf264638b8f6995b3515975e3c79",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8"
+ },
+ "require-dev": {
+ "symfony/process": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-22T07:36:05+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v7.4.14",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "13b38720174286f55d1761152b575a8d1436fc25"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/13b38720174286f55d1761152b575a8d1436fc25",
+ "reference": "13b38720174286f55d1761152b575a8d1436fc25",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Finds files and directories via an intuitive fluent interface",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v7.4.14"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-27T08:31:18+00:00"
+ },
+ {
+ "name": "symfony/form",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/form.git",
+ "reference": "24687cfe07dbf29912a86e68cb25208805b70097"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/form/zipball/24687cfe07dbf29912a86e68cb25208805b70097",
+ "reference": "24687cfe07dbf29912a86e68cb25208805b70097",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/options-resolver": "^7.3|^8.0",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/console": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/doctrine-bridge": "<6.4",
+ "symfony/error-handler": "<6.4",
+ "symfony/framework-bundle": "<6.4",
+ "symfony/http-kernel": "<6.4",
+ "symfony/intl": "<7.4",
+ "symfony/translation": "<6.4.3|>=7.0,<7.0.3",
+ "symfony/translation-contracts": "<2.5",
+ "symfony/twig-bridge": "<6.4"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0|^2.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/html-sanitizer": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^7.4|^8.0",
+ "symfony/security-core": "^6.4|^7.0|^8.0",
+ "symfony/security-csrf": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4.3|^7.0.3|^8.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4.12|^7.1.5|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Form\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows to easily create, process and reuse HTML forms",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/form/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-28T21:44:23+00:00"
+ },
+ {
+ "name": "symfony/framework-bundle",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/framework-bundle.git",
+ "reference": "a430728797dda13ec60add8afd18e3fea50f5e93"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a430728797dda13ec60add8afd18e3fea50f5e93",
+ "reference": "a430728797dda13ec60add8afd18e3fea50f5e93",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "ext-xml": "*",
+ "php": ">=8.2",
+ "symfony/cache": "^6.4.12|^7.0|^8.0",
+ "symfony/config": "^7.4.4|^8.0.4",
+ "symfony/dependency-injection": "^7.4.15|~8.0.15|^8.1.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/error-handler": "^7.3|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/filesystem": "^7.1|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php85": "^1.32",
+ "symfony/routing": "^7.4|^8.0"
+ },
+ "conflict": {
+ "doctrine/persistence": "<1.3",
+ "phpdocumentor/reflection-docblock": "<5.2|>=7",
+ "phpdocumentor/type-resolver": "<1.5.1",
+ "symfony/asset": "<6.4",
+ "symfony/asset-mapper": "<6.4",
+ "symfony/clock": "<6.4",
+ "symfony/console": "<6.4.43|>=7.0,<7.4.15|>=8.0,<8.0.15",
+ "symfony/dom-crawler": "<6.4",
+ "symfony/dotenv": "<6.4",
+ "symfony/form": "<7.4",
+ "symfony/http-client": "<6.4",
+ "symfony/lock": "<6.4",
+ "symfony/mailer": "<6.4",
+ "symfony/messenger": "<7.4",
+ "symfony/mime": "<6.4.37|>=7.0,<7.4.9|>=8.0,<8.0.9",
+ "symfony/property-access": "<6.4",
+ "symfony/property-info": "<6.4",
+ "symfony/runtime": "<6.4.13|>=7.0,<7.1.6",
+ "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4",
+ "symfony/security-core": "<6.4",
+ "symfony/security-csrf": "<7.2",
+ "symfony/serializer": "<7.2.5",
+ "symfony/stopwatch": "<6.4",
+ "symfony/translation": "<7.3",
+ "symfony/twig-bridge": "<6.4",
+ "symfony/twig-bundle": "<6.4",
+ "symfony/validator": "<6.4",
+ "symfony/web-profiler-bundle": "<6.4",
+ "symfony/webhook": "<7.4",
+ "symfony/workflow": "<7.4"
+ },
+ "require-dev": {
+ "doctrine/persistence": "^1.3|^2|^3",
+ "dragonmantank/cron-expression": "^3.1",
+ "phpdocumentor/reflection-docblock": "^5.2|^6.0",
+ "seld/jsonlint": "^1.10",
+ "symfony/asset": "^6.4|^7.0|^8.0",
+ "symfony/asset-mapper": "^6.4|^7.0|^8.0",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4.43|^7.4.15|^8.0.15",
+ "symfony/css-selector": "^6.4|^7.0|^8.0",
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0",
+ "symfony/dotenv": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/form": "^7.4|^8.0",
+ "symfony/html-sanitizer": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/json-streamer": "^7.3|^8.0",
+ "symfony/lock": "^6.4|^7.0|^8.0",
+ "symfony/mailer": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/mime": "^6.4.37|^7.4.9|^8.0.9",
+ "symfony/notifier": "^6.4|^7.0|^8.0",
+ "symfony/object-mapper": "^7.3|^8.0",
+ "symfony/polyfill-intl-icu": "~1.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-info": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0",
+ "symfony/runtime": "^6.4.13|^7.1.6|^8.0",
+ "symfony/scheduler": "^6.4.4|^7.0.4|^8.0",
+ "symfony/security-bundle": "^6.4|^7.0|^8.0",
+ "symfony/semaphore": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^7.2.5|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/string": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^7.3|^8.0",
+ "symfony/twig-bundle": "^6.4|^7.0|^8.0",
+ "symfony/type-info": "^7.1.8|^8.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/web-link": "^6.4|^7.0|^8.0",
+ "symfony/webhook": "^7.4|^8.0",
+ "symfony/workflow": "^7.4|^8.0",
+ "symfony/yaml": "^7.3|^8.0",
+ "twig/twig": "^3.12|^4.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\FrameworkBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/framework-bundle/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-26T12:33:49+00:00"
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "1f898ee8188adda9417fb52cf8425a8342c254e7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1f898ee8188adda9417fb52cf8425a8342c254e7",
+ "reference": "1f898ee8188adda9417fb52cf8425a8342c254e7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "^1.1"
+ },
+ "conflict": {
+ "doctrine/dbal": "<3.6",
+ "symfony/cache": "<6.4.12|>=7.0,<7.1.5"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^3.6|^4",
+ "predis/predis": "^1.1|^2.0",
+ "symfony/cache": "^6.4.12|^7.1.5|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-29T07:12:33+00:00"
+ },
+ {
+ "name": "symfony/http-kernel",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-kernel.git",
+ "reference": "403275d94f94d5626c3288c599b3b48093ba24f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/403275d94f94d5626c3288c599b3b48093ba24f7",
+ "reference": "403275d94f94d5626c3288c599b3b48093ba24f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/error-handler": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^7.3|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/browser-kit": "<6.4",
+ "symfony/cache": "<6.4",
+ "symfony/config": "<6.4",
+ "symfony/console": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/doctrine-bridge": "<6.4",
+ "symfony/flex": "<2.10",
+ "symfony/form": "<6.4",
+ "symfony/http-client": "<6.4",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/mailer": "<6.4",
+ "symfony/messenger": "<6.4",
+ "symfony/translation": "<6.4",
+ "symfony/translation-contracts": "<2.5",
+ "symfony/twig-bridge": "<6.4",
+ "symfony/validator": "<6.4",
+ "symfony/var-dumper": "<6.4",
+ "twig/twig": "<3.12"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/css-selector": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0",
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/http-client-contracts": "^2.5|^3",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^7.1|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^7.1|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
+ "symfony/translation-contracts": "^2.5|^3",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0",
+ "twig/twig": "^3.12|^4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpKernel\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "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.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-29T11:40:42+00:00"
+ },
+ {
+ "name": "symfony/messenger",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/messenger.git",
+ "reference": "6338eed8f65c593469fdbdbe043d2a7552264403"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/messenger/zipball/6338eed8f65c593469fdbdbe043d2a7552264403",
+ "reference": "6338eed8f65c593469fdbdbe043d2a7552264403",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/log": "^1|^2|^3",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/console": "<7.2",
+ "symfony/event-dispatcher": "<6.4",
+ "symfony/event-dispatcher-contracts": "<2.5",
+ "symfony/framework-bundle": "<6.4",
+ "symfony/http-kernel": "<7.3",
+ "symfony/lock": "<7.4",
+ "symfony/serializer": "<6.4.32|>=7.3,<7.3.10|>=7.4,<7.4.4|>=8.0,<8.0.4"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/console": "^7.2|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^7.3|^8.0",
+ "symfony/lock": "^7.4|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Messenger\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Samuel Roze",
+ "email": "samuel.roze@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps applications send and receive messages to/from other applications or via message queues",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/messenger/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-22T12:46:20+00:00"
+ },
+ {
+ "name": "symfony/options-resolver",
+ "version": "v7.4.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/options-resolver.git",
+ "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
+ "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\OptionsResolver\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an improved replacement for the array_replace PHP function",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "config",
+ "configuration",
+ "options"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/options-resolver/tree/v7.4.8"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-24T13:12:05+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.41.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
+ "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "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 for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-28T08:25:59+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-icu",
+ "version": "v1.38.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-icu.git",
+ "reference": "445c90e341fccda10311019cf82ff73bb7343945"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/445c90e341fccda10311019cf82ff73bb7343945",
+ "reference": "445c90e341fccda10311019cf82ff73bb7343945",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance and support of other locales than \"en\""
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Icu\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ],
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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 for intl's ICU-related data and classes",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "icu",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.38.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-25T11:52:53+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.38.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
+ "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "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 for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-25T13:48:31+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.38.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+ "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "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 for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-27T06:59:30+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php83",
+ "version": "v1.41.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php83.git",
+ "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6",
+ "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6",
+ "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\\Php83\\": ""
+ },
+ "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.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-01T12:47:55+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php85",
+ "version": "v1.41.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php85.git",
+ "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a",
+ "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a",
+ "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\\Php85\\": ""
+ },
+ "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.5+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-01T12:47:55+00:00"
+ },
+ {
+ "name": "symfony/property-access",
+ "version": "v7.4.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-access.git",
+ "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc",
+ "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4"
+ },
+ "require-dev": {
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyAccess\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides functions to read and write from/to an object or array using a simple string notation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "access",
+ "array",
+ "extraction",
+ "index",
+ "injection",
+ "object",
+ "property",
+ "property-path",
+ "reflection"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-access/tree/v7.4.8"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-24T13:12:05+00:00"
+ },
+ {
+ "name": "symfony/property-info",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-info.git",
+ "reference": "fce3f4d9cfeb4ddc674c357b5a4c3a23ccf408ad"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-info/zipball/fce3f4d9cfeb4ddc674c357b5a4c3a23ccf408ad",
+ "reference": "fce3f4d9cfeb4ddc674c357b5a4c3a23ccf408ad",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/string": "^6.4|^7.0|^8.0",
+ "symfony/type-info": "^7.4.7|^8.0.7"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<5.2|>=7",
+ "phpdocumentor/type-resolver": "<1.5.1",
+ "symfony/cache": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/serializer": "<6.4"
+ },
+ "require-dev": {
+ "phpdocumentor/reflection-docblock": "^5.2|^6.0",
+ "phpstan/phpdoc-parser": "^1.0|^2.0",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyInfo\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Extracts information about PHP class' properties using metadata of popular sources",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "doctrine",
+ "phpdoc",
+ "property",
+ "symfony",
+ "type",
+ "validator"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-info/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-28T07:09:44+00:00"
+ },
+ {
+ "name": "symfony/routing",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/routing.git",
+ "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/80c0a93d3f8e7499f716204a1fb38ead942a7a2b",
+ "reference": "80c0a93d3f8e7499f716204a1fb38ead942a7a2b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/config": "<6.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/yaml": "<6.4"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Routing\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Maps an HTTP request to a set of configuration variables",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "router",
+ "routing",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/routing/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T15:13:06+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0",
+ "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "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": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-16T09:55:08+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "e394af32256bf9e7bf80849d95e589167c10097b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b",
+ "reference": "e394af32256bf9e7bf80849d95e589167c10097b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3.0",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.33",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/emoji": "^7.1|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
+ "symfony/translation-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-28T07:33:02+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621",
+ "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "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": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/twig-bridge",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bridge.git",
+ "reference": "c81843850c1c791b7f516db62cb81beaadd8ab22"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/c81843850c1c791b7f516db62cb81beaadd8ab22",
+ "reference": "c81843850c1c791b7f516db62cb81beaadd8ab22",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/translation-contracts": "^2.5|^3",
+ "twig/twig": "^3.21|^4.0"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<5.2|>=7",
+ "phpdocumentor/type-resolver": "<1.5.1",
+ "symfony/console": "<6.4",
+ "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4",
+ "symfony/http-foundation": "<6.4",
+ "symfony/http-kernel": "<6.4",
+ "symfony/mime": "<6.4.37|>7,<7.4.9|>8.0,<8.0.9",
+ "symfony/serializer": "<6.4",
+ "symfony/translation": "<6.4",
+ "symfony/workflow": "<6.4"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "league/html-to-markdown": "^5.0",
+ "phpdocumentor/reflection-docblock": "^5.2|^6.0",
+ "symfony/asset": "^6.4|^7.0|^8.0",
+ "symfony/asset-mapper": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/emoji": "^7.1|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
+ "symfony/html-sanitizer": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^7.3|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4.37|^7.4.9|^8.0.9",
+ "symfony/polyfill-intl-icu": "~1.0",
+ "symfony/property-info": "^6.4|^7.0|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/security-acl": "^2.8|^3.0",
+ "symfony/security-core": "^6.4|^7.0|^8.0",
+ "symfony/security-csrf": "^6.4|^7.0|^8.0",
+ "symfony/security-http": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4.3|^7.0.3|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
+ "symfony/validator": "^6.4|^7.0|^8.0",
+ "symfony/web-link": "^6.4|^7.0|^8.0",
+ "symfony/workflow": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0",
+ "twig/cssinliner-extra": "^3",
+ "twig/inky-extra": "^3",
+ "twig/markdown-extra": "^3"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Twig\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides integration for Twig with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bridge/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T15:27:07+00:00"
+ },
+ {
+ "name": "symfony/twig-bundle",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bundle.git",
+ "reference": "e3d2bea0b594aa50c3482a278a5cd09d83d94517"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/e3d2bea0b594aa50c3482a278a5cd09d83d94517",
+ "reference": "e3d2bea0b594aa50c3482a278a5cd09d83d94517",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "php": ">=8.2",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0",
+ "symfony/twig-bridge": "^7.3|^8.0",
+ "twig/twig": "^3.12|^4.0"
+ },
+ "conflict": {
+ "symfony/framework-bundle": "<6.4",
+ "symfony/translation": "<6.4"
+ },
+ "require-dev": {
+ "symfony/asset": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/form": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0",
+ "symfony/routing": "^6.4|^7.0|^8.0",
+ "symfony/runtime": "^6.4.13|^7.1.6",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4|^7.0|^8.0",
+ "symfony/web-link": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\TwigBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration of Twig into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bundle/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T15:13:06+00:00"
+ },
+ {
+ "name": "symfony/type-info",
+ "version": "v7.4.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/type-info.git",
+ "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/type-info/zipball/cafeedbf157b890e94ac5b83eaed85595106d5d6",
+ "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "phpstan/phpdoc-parser": "<1.30"
+ },
+ "require-dev": {
+ "phpstan/phpdoc-parser": "^1.30|^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\TypeInfo\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mathias Arlaud",
+ "email": "mathias.arlaud@gmail.com"
+ },
+ {
+ "name": "Baptiste LEDUC",
+ "email": "baptiste.leduc@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Extracts PHP types information.",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "PHPStan",
+ "phpdoc",
+ "symfony",
+ "type"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/type-info/tree/v7.4.9"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-22T15:21:55+00:00"
+ },
+ {
+ "name": "symfony/validator",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/validator.git",
+ "reference": "a1a345b72800e05b4366c43e06fbc57754abdd3e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/validator/zipball/a1a345b72800e05b4366c43e06fbc57754abdd3e",
+ "reference": "a1a345b72800e05b4366c43e06fbc57754abdd3e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php83": "^1.27",
+ "symfony/translation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/lexer": "<1.1",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/doctrine-bridge": "<7.0",
+ "symfony/expression-language": "<6.4",
+ "symfony/http-kernel": "<6.4",
+ "symfony/intl": "<6.4",
+ "symfony/property-info": "<6.4",
+ "symfony/translation": "<6.4.3|>=7.0,<7.0.3",
+ "symfony/var-exporter": "<6.4.25|>=7.0,<7.3.3",
+ "symfony/yaml": "<6.4"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/expression-language": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/intl": "^6.4|^7.0|^8.0",
+ "symfony/mime": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/property-info": "^6.4|^7.0|^8.0",
+ "symfony/string": "^6.4|^7.0|^8.0",
+ "symfony/translation": "^6.4.3|^7.0.3|^8.0",
+ "symfony/type-info": "^7.1.8",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Validator\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/",
+ "/Resources/bin/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to validate values",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/validator/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-28T21:44:23+00:00"
+ },
+ {
+ "name": "symfony/var-dumper",
+ "version": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-dumper.git",
+ "reference": "04ba4add636a95ff437af3a5a9499bb1d6c6d4bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/04ba4add636a95ff437af3a5a9499bb1d6c6d4bd",
+ "reference": "04ba4add636a95ff437af3a5a9499bb1d6c6d4bd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/console": "<6.4"
+ },
+ "require-dev": {
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/uid": "^6.4|^7.0|^8.0",
+ "twig/twig": "^3.12|^4.0"
+ },
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions/dump.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\VarDumper\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Provides mechanisms for walking through any arbitrary PHP variable",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "debug",
+ "dump"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v7.4.15"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T15:13:06+00:00"
+ },
+ {
+ "name": "symfony/var-exporter",
+ "version": "v7.4.14",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-exporter.git",
+ "reference": "0118811b1d59f323bf131250b3fb919febfece28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0118811b1d59f323bf131250b3fb919febfece28",
+ "reference": "0118811b1d59f323bf131250b3fb919febfece28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/serializer": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\VarExporter\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "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": "Allows exporting any serializable PHP data structure to plain PHP code",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clone",
+ "construct",
+ "export",
+ "hydrate",
+ "instantiate",
+ "lazy-loading",
+ "proxy",
+ "serialize"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-exporter/tree/v7.4.14"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-27T08:41:53+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.28.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
+ "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3"
+ },
+ "require-dev": {
+ "php-cs-fixer/shim": "^3.0@stable",
+ "phpstan/phpstan": "^2.0@stable",
+ "psr/container": "^1.0|^2.0",
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Resources/core.php",
+ "src/Resources/debug.php",
+ "src/Resources/escaper.php",
+ "src/Resources/string_loader.php"
+ ],
+ "psr-4": {
+ "Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Twig Team",
+ "role": "Contributors"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "templating"
+ ],
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v3.28.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-03T20:44:34+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=8.2"
+ },
+ "platform-dev": {},
+ "platform-overrides": {
+ "php": "8.2.0"
+ },
+ "plugin-api-version": "2.9.0"
+}
diff --git a/examples/symfony/config/doctrine/User.orm.xml b/examples/symfony/config/doctrine/User.orm.xml
new file mode 100644
index 000000000..b24aed284
--- /dev/null
+++ b/examples/symfony/config/doctrine/User.orm.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/examples/symfony/config/packages/acme_demo.yaml b/examples/symfony/config/packages/acme_demo.yaml
new file mode 100644
index 000000000..711b932f8
--- /dev/null
+++ b/examples/symfony/config/packages/acme_demo.yaml
@@ -0,0 +1,4 @@
+acme_demo:
+ api_key: local-development-key
+ mailer:
+ dsn: smtp://localhost
diff --git a/examples/symfony/config/packages/messenger.yaml b/examples/symfony/config/packages/messenger.yaml
new file mode 100644
index 000000000..58774a174
--- /dev/null
+++ b/examples/symfony/config/packages/messenger.yaml
@@ -0,0 +1,6 @@
+framework:
+ messenger:
+ default_bus: command.bus
+ buses:
+ command.bus: ~
+ query.bus: ~
diff --git a/examples/symfony/config/routes.yaml b/examples/symfony/config/routes.yaml
new file mode 100644
index 000000000..d3a2ab6f6
--- /dev/null
+++ b/examples/symfony/config/routes.yaml
@@ -0,0 +1,3 @@
+app_demo_legacy:
+ path: /legacy
+ controller: App\Controller\DemoController::legacy
diff --git a/examples/symfony/config/services.php b/examples/symfony/config/services.php
new file mode 100644
index 000000000..d7d4913c5
--- /dev/null
+++ b/examples/symfony/config/services.php
@@ -0,0 +1,14 @@
+services();
+ $parameters = $container->parameters();
+
+ $services->set('app.welcome_mailer', WelcomeMailer::class);
+ $services->alias('app.mailer', 'app.welcome_mailer');
+ $parameters->set('app.sender_name', 'PHPantom');
+};
diff --git a/examples/symfony/config/services.yaml b/examples/symfony/config/services.yaml
new file mode 100644
index 000000000..76903b276
--- /dev/null
+++ b/examples/symfony/config/services.yaml
@@ -0,0 +1,6 @@
+services:
+ App\:
+ resource: '../src/'
+ App\EventListener\OrderPlacedListener:
+ tags:
+ - { name: kernel.event_listener, event: app.order.placed, method: onOrderPlaced }
diff --git a/examples/symfony/config/validator/User.yaml b/examples/symfony/config/validator/User.yaml
new file mode 100644
index 000000000..3835c3c72
--- /dev/null
+++ b/examples/symfony/config/validator/User.yaml
@@ -0,0 +1,7 @@
+App\Entity\User:
+ properties:
+ email:
+ - NotBlank: ~
+ displayName:
+ - Length:
+ min: 2
diff --git a/examples/symfony/src/Controller/DemoController.php b/examples/symfony/src/Controller/DemoController.php
new file mode 100644
index 000000000..65b14bebf
--- /dev/null
+++ b/examples/symfony/src/Controller/DemoController.php
@@ -0,0 +1,46 @@
+has('app.welcome_mailer');
+ $this->generateUrl('app_demo', ['userId' => $userId]);
+ $title = $this->translator->trans('demo.title');
+ $this->events->dispatch(new OrderPlaced($userId), 'app.order.placed');
+ $this->commandBus->dispatch(new SendWelcomeEmail($userId));
+
+ return $this->render('demo/index.html.twig', [
+ 'sender' => $this->senderName,
+ 'title' => $title,
+ ]);
+ }
+
+ public function legacy(): Response
+ {
+ return new Response('Configured in routes.yaml');
+ }
+}
diff --git a/examples/symfony/src/DependencyInjection/Configuration.php b/examples/symfony/src/DependencyInjection/Configuration.php
new file mode 100644
index 000000000..890d0315b
--- /dev/null
+++ b/examples/symfony/src/DependencyInjection/Configuration.php
@@ -0,0 +1,26 @@
+getRootNode();
+ assert($rootNode instanceof ArrayNodeDefinition);
+ $rootNode
+ ->children()
+ ->scalarNode('api_key')->end()
+ ->arrayNode('mailer')
+ ->children()
+ ->scalarNode('dsn')->end()
+ ->end()
+ ->end();
+
+ return $treeBuilder;
+ }
+}
diff --git a/examples/symfony/src/Entity/User.php b/examples/symfony/src/Entity/User.php
new file mode 100644
index 000000000..e50e3405d
--- /dev/null
+++ b/examples/symfony/src/Entity/User.php
@@ -0,0 +1,21 @@
+email;
+ }
+
+ public function displayName(): string
+ {
+ return $this->displayName;
+ }
+}
diff --git a/examples/symfony/src/Event/OrderPlaced.php b/examples/symfony/src/Event/OrderPlaced.php
new file mode 100644
index 000000000..4e778c31a
--- /dev/null
+++ b/examples/symfony/src/Event/OrderPlaced.php
@@ -0,0 +1,8 @@
+userId;
+ }
+}
diff --git a/examples/symfony/src/Form/RegistrationType.php b/examples/symfony/src/Form/RegistrationType.php
new file mode 100644
index 000000000..bee6785ec
--- /dev/null
+++ b/examples/symfony/src/Form/RegistrationType.php
@@ -0,0 +1,28 @@
+ $options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->add('email')
+ ->add('displayName');
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults(['data_class' => User::class]);
+ }
+}
diff --git a/examples/symfony/src/Message/SendWelcomeEmail.php b/examples/symfony/src/Message/SendWelcomeEmail.php
new file mode 100644
index 000000000..17d5709da
--- /dev/null
+++ b/examples/symfony/src/Message/SendWelcomeEmail.php
@@ -0,0 +1,8 @@
+mailer->send($message->userId);
+ }
+}
diff --git a/examples/symfony/src/Repository/UserRepository.php b/examples/symfony/src/Repository/UserRepository.php
new file mode 100644
index 000000000..32339e0b3
--- /dev/null
+++ b/examples/symfony/src/Repository/UserRepository.php
@@ -0,0 +1,13 @@
+
+ */
+final class UserRepository extends ServiceEntityRepository
+{
+}
diff --git a/examples/symfony/src/Service/WelcomeMailer.php b/examples/symfony/src/Service/WelcomeMailer.php
new file mode 100644
index 000000000..d0c73cc12
--- /dev/null
+++ b/examples/symfony/src/Service/WelcomeMailer.php
@@ -0,0 +1,11 @@
+
+
+ {% block body %}{% endblock %}
+
diff --git a/examples/symfony/templates/demo/_card.html.twig b/examples/symfony/templates/demo/_card.html.twig
new file mode 100644
index 000000000..d549e523b
--- /dev/null
+++ b/examples/symfony/templates/demo/_card.html.twig
@@ -0,0 +1 @@
+{{ 'demo.card'|trans }}
diff --git a/examples/symfony/templates/demo/index.html.twig b/examples/symfony/templates/demo/index.html.twig
new file mode 100644
index 000000000..b05d03f39
--- /dev/null
+++ b/examples/symfony/templates/demo/index.html.twig
@@ -0,0 +1,7 @@
+{% extends 'base.html.twig' %}
+
+{% block body %}
+ {{ 'demo.title'|trans }}
+ {% include 'demo/_card.html.twig' %}
+ {{ sender }}
+{% endblock %}
diff --git a/examples/symfony/translations/messages.en.yaml b/examples/symfony/translations/messages.en.yaml
new file mode 100644
index 000000000..c0ded029f
--- /dev/null
+++ b/examples/symfony/translations/messages.en.yaml
@@ -0,0 +1,3 @@
+demo:
+ title: Symfony intelligence
+ card: Every string links back to its declaration
diff --git a/examples/symfony/translations/validators.en.xlf b/examples/symfony/translations/validators.en.xlf
new file mode 100644
index 000000000..47c66732f
--- /dev/null
+++ b/examples/symfony/translations/validators.en.xlf
@@ -0,0 +1,11 @@
+
+
+
+
+
+ user.invalid_email
+ Please enter a valid email address.
+
+
+
+
diff --git a/src/backend/documents.rs b/src/backend/documents.rs
index 51d33e839..c7415df62 100644
--- a/src/backend/documents.rs
+++ b/src/backend/documents.rs
@@ -29,8 +29,11 @@ impl Backend {
// Resource documents are not PHP source. Build a lightweight symbol
// map so navigation, references, rename, and PHP declaration lenses
// all consume the same indexed occurrences.
- if crate::resource_navigation::is_resource_document(&uri) {
- self.update_resource_symbol_index(&uri, &text);
+ if is_non_php_resource(&uri) {
+ self.index_resource_text(&uri, &text);
+ if crate::framework::is_framework_resource_uri(&uri) {
+ self.schedule_diagnostics(uri.clone());
+ }
self.log(MessageType::INFO, format!("Opened resource file: {}", uri))
.await;
return;
@@ -125,9 +128,15 @@ impl Backend {
// service loop for every keystroke would stall interactive
// requests, and refreshing lenses per keystroke would make the
// client re-pull them faster than it can render them.
- if crate::resource_navigation::is_resource_document(&uri) {
+ if is_non_php_resource(&uri) {
+ // A framework resource carries its own diagnostics (a template
+ // or route that does not exist), which follow its new text.
+ let diagnose = crate::framework::is_framework_resource_uri(&uri).then(|| uri.clone());
if self.sync_ast_updates {
- self.update_resource_symbol_index(&uri, &text);
+ self.index_resource_text(&uri, &text);
+ if let Some(uri) = diagnose {
+ self.schedule_diagnostics(uri);
+ }
return;
}
let backend = self.clone_for_blocking();
@@ -142,12 +151,15 @@ impl Backend {
if !is_latest_text {
return false;
}
- backend.update_resource_symbol_index(&uri, &text);
+ backend.index_resource_text(&uri, &text);
true
})
.await;
if committed == Some(true) {
+ if let Some(uri) = diagnose {
+ refresh_backend.schedule_diagnostics(uri);
+ }
refresh_backend.request_code_lens_refresh().await;
}
});
@@ -234,11 +246,16 @@ impl Backend {
coalesce.last.lock().retain(|k, _| !k.ends_with(&suffix));
}
- if crate::resource_navigation::is_resource_document(&uri) {
- if let Some(content) = self.get_file_content(&uri) {
- self.update_resource_symbol_index(&uri, &content);
- } else {
- self.clear_file_maps(&uri);
+ if is_non_php_resource(&uri) {
+ if crate::resource_navigation::is_resource_document(&uri) {
+ if let Some(content) = self.get_file_content(&uri) {
+ self.update_resource_symbol_index(&uri, &content);
+ } else {
+ self.clear_file_maps(&uri);
+ }
+ }
+ if crate::framework::is_framework_resource_uri(&uri) {
+ self.reindex_framework_uri_from_disk(&uri);
}
} else if let Some(path) = self.workspace_index_path(&uri) {
// A workspace file stays in the index once closed, as the file
@@ -272,6 +289,11 @@ impl Backend {
});
}
} else {
+ if crate::framework::is_framework_php_config_uri(&uri)
+ || self.framework_references.read().contains_key(&uri)
+ {
+ self.reindex_framework_uri_from_disk(&uri);
+ }
self.clear_file_maps(&uri);
}
@@ -284,7 +306,7 @@ impl Backend {
pub(crate) async fn on_did_save(&self, params: DidSaveTextDocumentParams) {
let uri = params.text_document.uri.to_string();
- let is_resource = crate::resource_navigation::is_resource_document(&uri);
+ let is_resource = is_non_php_resource(&uri);
if let Some(text) = params.text {
let text = Arc::new(text);
@@ -292,7 +314,7 @@ impl Backend {
.write()
.insert(uri.clone(), Arc::clone(&text));
if is_resource {
- self.update_resource_symbol_index(&uri, &text);
+ self.index_resource_text(&uri, &text);
} else {
self.update_ast(&uri, &text);
}
@@ -356,4 +378,23 @@ impl Backend {
self.request_code_lens_refresh().await;
}
}
+
+ /// Refresh whichever indexes cover a non-PHP resource document: the
+ /// PHP symbols its YAML/XML names, and its Symfony and Doctrine
+ /// references.
+ fn index_resource_text(&self, uri: &str, text: &str) {
+ if crate::resource_navigation::is_resource_document(uri) {
+ self.update_resource_symbol_index(uri, text);
+ }
+ if crate::framework::is_framework_resource_uri(uri) {
+ self.index_framework_uri_content(uri, text);
+ }
+ }
+}
+
+/// A YAML, XML, or Twig document: indexed for the PHP names and framework
+/// symbols it holds, never parsed as PHP.
+fn is_non_php_resource(uri: &str) -> bool {
+ crate::resource_navigation::is_resource_document(uri)
+ || crate::framework::is_framework_resource_uri(uri)
}
diff --git a/src/backend/startup.rs b/src/backend/startup.rs
index 350ae8add..fa5d0a90b 100644
--- a/src/backend/startup.rs
+++ b/src/backend/startup.rs
@@ -349,6 +349,35 @@ impl Backend {
self.discover_workspace_symbols(&root, php_version, composer_package, Some(&progress))
.await;
+ // Generated transparent proxies live in opt-in cache/build paths
+ // that normal project indexing may ignore. Read their declarations
+ // into the metadata relation index; they do not enter the type
+ // engine or the workspace class map.
+ let proxy_backend = self.clone_for_blocking();
+ let proxy_root = root.clone();
+ let proxy_count = run_blocking_cancel_safe("index_php_proxies", move || {
+ proxy_backend.rebuild_configured_proxy_index(&proxy_root)
+ })
+ .await
+ .unwrap_or(0);
+ if proxy_count > 0 {
+ tracing::info!("PHPantom: indexed {} transparent proxies", proxy_count);
+ }
+
+ // Symfony's generated container records the final event-listener
+ // wiring after compiler passes have run. Read it statically; the
+ // container PHP is never loaded or executed.
+ let symfony_backend = self.clone_for_blocking();
+ let symfony_root = root.clone();
+ let event_count = run_blocking_cancel_safe("index_symfony_metadata", move || {
+ symfony_backend.rebuild_symfony_metadata(&symfony_root)
+ })
+ .await
+ .unwrap_or(0);
+ if event_count > 0 {
+ tracing::info!("PHPantom: indexed {} Symfony event links", event_count);
+ }
+
// Laravel-only startup work. The project classification is
// set by the init pass above from composer.json, so it has to
// run after it: a Symfony workspace must never pay for the
@@ -413,6 +442,14 @@ impl Backend {
}
}
+ let framework_count = self.index_framework_workspace();
+ if framework_count > 0 {
+ tracing::info!(
+ "PHPantom: indexed {} Symfony/Doctrine resource file(s)",
+ framework_count
+ );
+ }
+
if let Some(poller) = poller {
poller.finish().await;
}
diff --git a/src/call_hierarchy.rs b/src/call_hierarchy.rs
index f863e2037..c53720c34 100644
--- a/src/call_hierarchy.rs
+++ b/src/call_hierarchy.rs
@@ -85,8 +85,11 @@ impl Backend {
&self,
item: &CallHierarchyItem,
) -> Option> {
+ let event_calls = self.symfony_event_incoming_calls(item);
let mut cache = FileCache::default();
- let target = self.php_callable_from_item(&mut cache, item)?;
+ let Some(target) = self.php_callable_from_item(&mut cache, item) else {
+ return event_calls;
+ };
let (content, _) = self.cached_file(&mut cache, target.item.uri.as_str())?;
let references = self
.find_references(
@@ -128,6 +131,7 @@ impl Backend {
CallHierarchyIncomingCall { from, from_ranges }
})
.collect();
+ calls.extend(event_calls.unwrap_or_default());
calls.sort_by_cached_key(|call| php_item_key(&call.from));
Some(calls)
}
@@ -136,10 +140,13 @@ impl Backend {
&self,
item: &CallHierarchyItem,
) -> Option> {
+ let event_calls = self.symfony_event_outgoing_calls(item);
let mut cache = FileCache::default();
- let callable = self.php_callable_from_item(&mut cache, item)?;
+ let Some(callable) = self.php_callable_from_item(&mut cache, item) else {
+ return event_calls;
+ };
let Some((body_start, body_end)) = callable.body else {
- return Some(Vec::new());
+ return event_calls.or_else(|| Some(Vec::new()));
};
let uri = callable.item.uri.as_str();
let (content, symbol_map) = self.cached_file(&mut cache, uri)?;
@@ -189,6 +196,7 @@ impl Backend {
CallHierarchyOutgoingCall { to, from_ranges }
})
.collect();
+ calls.extend(event_calls.unwrap_or_default());
calls.sort_by_cached_key(|call| php_item_key(&call.to));
Some(calls)
}
@@ -265,6 +273,14 @@ impl Backend {
self.php_callable_at(uri, &content, &symbol_map, offset)
}
+ pub(crate) fn call_hierarchy_item_at_location(
+ &self,
+ location: &Location,
+ ) -> Option {
+ self.php_callable_at_location(&mut FileCache::default(), location)
+ .map(|callable| callable.item)
+ }
+
/// The innermost function or method whose name token or body contains
/// `offset`.
fn php_callable_at(
diff --git a/src/code_actions/mod.rs b/src/code_actions/mod.rs
index 230b66949..2f4117606 100644
--- a/src/code_actions/mod.rs
+++ b/src/code_actions/mod.rs
@@ -119,6 +119,7 @@ mod replace_deprecated;
mod replace_fqcn;
mod simplify_null;
mod sort_use_statements;
+mod symfony_template;
mod update_docblock;
mod docblock_edit;
@@ -151,6 +152,11 @@ impl Backend {
) -> Vec {
let mut actions = Vec::new();
+ self.collect_create_symfony_template_actions(uri, content, params, &mut actions);
+ if crate::framework::is_framework_resource_uri(uri) {
+ return actions;
+ }
+
// Parse the file once and share the result across every collector
// below. Each collector resolves cursor context by walking the
// AST via `with_parsed_program(content, …)`; without this guard
diff --git a/src/code_actions/symfony_template.rs b/src/code_actions/symfony_template.rs
new file mode 100644
index 000000000..3afb85a1d
--- /dev/null
+++ b/src/code_actions/symfony_template.rs
@@ -0,0 +1,88 @@
+use std::collections::HashSet;
+
+use tower_lsp::lsp_types::{
+ CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CreateFile,
+ CreateFileOptions, DocumentChangeOperation, DocumentChanges, OneOf,
+ OptionalVersionedTextDocumentIdentifier, Position, Range, ResourceOp, TextDocumentEdit,
+ TextEdit, WorkspaceEdit,
+};
+
+use crate::Backend;
+use crate::framework::{FrameworkReferenceKind, SymfonySymbolKind};
+
+impl Backend {
+ pub(super) fn collect_create_symfony_template_actions(
+ &self,
+ uri: &str,
+ content: &str,
+ params: &CodeActionParams,
+ actions: &mut Vec,
+ ) {
+ let mut seen = HashSet::new();
+ for diagnostic in ¶ms.context.diagnostics {
+ if diagnostic.code.as_ref().and_then(|code| match code {
+ tower_lsp::lsp_types::NumberOrString::String(code) => Some(code.as_str()),
+ tower_lsp::lsp_types::NumberOrString::Number(_) => None,
+ }) != Some("unknown_symfony_template")
+ {
+ continue;
+ }
+
+ let Some(reference) =
+ self.framework_reference_at_position(uri, content, diagnostic.range.start)
+ else {
+ continue;
+ };
+ let FrameworkReferenceKind::SymfonySymbol {
+ kind: SymfonySymbolKind::Template,
+ name,
+ declaration: false,
+ } = reference.kind
+ else {
+ continue;
+ };
+ let Some(template_uri) = self.symfony_template_uri(&name) else {
+ continue;
+ };
+ if !seen.insert(template_uri.clone()) {
+ continue;
+ }
+
+ let operations = vec![
+ DocumentChangeOperation::Op(ResourceOp::Create(CreateFile {
+ uri: template_uri.clone(),
+ options: Some(CreateFileOptions {
+ overwrite: Some(false),
+ ignore_if_exists: Some(true),
+ }),
+ annotation_id: None,
+ })),
+ DocumentChangeOperation::Edit(TextDocumentEdit {
+ text_document: OptionalVersionedTextDocumentIdentifier {
+ uri: template_uri,
+ version: None,
+ },
+ edits: vec![OneOf::Left(TextEdit {
+ range: Range {
+ start: Position::new(0, 0),
+ end: Position::new(0, 0),
+ },
+ new_text: format!("{{# {name} #}}\n"),
+ })],
+ }),
+ ];
+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
+ title: format!("Create Twig template '{name}'"),
+ kind: Some(CodeActionKind::QUICKFIX),
+ diagnostics: Some(vec![diagnostic.clone()]),
+ edit: Some(WorkspaceEdit {
+ changes: None,
+ document_changes: Some(DocumentChanges::Operations(operations)),
+ change_annotations: None,
+ }),
+ is_preferred: Some(true),
+ ..Default::default()
+ }));
+ }
+ }
+}
diff --git a/src/code_lens.rs b/src/code_lens.rs
index 249d2bfac..d33214a73 100644
--- a/src/code_lens.rs
+++ b/src/code_lens.rs
@@ -1,21 +1,28 @@
//! Code Lens (`textDocument/codeLens`) support.
//!
-//! Shows reference and implementation counts plus override/implement
-//! annotations.
+//! Shows reference and implementation counts plus override/implement,
+//! Symfony, and Doctrine relationship annotations.
+use std::collections::HashSet;
use std::sync::Arc;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::atom::Atom;
+use crate::class_lookup::find_class_at_offset;
use crate::definition::implementation::member_implementation_providers;
use crate::definition::member::MemberKind;
+use crate::framework::{FrameworkReferenceKind, SymfonySymbolKind};
use crate::inheritance::find_declaring_ancestor;
use crate::reference_index::ReferenceIndexKey;
+use crate::references::{
+ doctrine_repository_matches_entity_convention, looks_like_doctrine_repository,
+};
use crate::symbol_map::{SymbolKind, SymbolMap};
use crate::text_position::{LineIndex, offset_to_position};
-use crate::types::{ClassInfo, ClassLikeKind, Visibility};
+use crate::types::{ClassInfo, ClassLikeKind, MAX_INHERITANCE_DEPTH, MethodInfo, Visibility};
+use crate::util::short_name;
/// Shown while a declaration's references are being counted, so the lens
/// keeps its line instead of vanishing and shifting the file, and reads as
@@ -66,9 +73,10 @@ impl Backend {
/// Handle a `textDocument/codeLens` request.
///
/// Returns reference lenses for PHP declarations, implementation
- /// lenses for interfaces and abstract classes and their methods, and
+ /// lenses for interfaces and abstract classes and their methods,
/// navigation lenses for methods that override or implement an
- /// ancestor declaration.
+ /// ancestor declaration, and indexed Symfony and Doctrine relationship
+ /// lenses.
pub fn handle_code_lens(&self, uri: &str, content: &str) -> Option> {
let classes = {
let map = self.symbols.uri_classes_index.read();
@@ -81,8 +89,9 @@ impl Backend {
// O(offset) on each call.
let index = LineIndex::new(content);
- let mut lenses = Vec::new();
+ let mut lenses = self.symfony_event_lenses(&classes, uri, content);
let class_loader = |name: &str| self.find_or_load_class(name);
+ let mut seen = HashSet::new();
for class in &classes {
let class_fqn = class.fqn();
@@ -113,7 +122,17 @@ impl Backend {
if let Some(lens) = self.build_covers_lens(class, uri, &index) {
lenses.push(lens);
}
-
+ self.push_framework_class_lenses(
+ uri,
+ content,
+ &index,
+ class,
+ &class_loader,
+ (&mut lenses, &mut seen),
+ );
+ // The ancestors a framework route may name a method through, built
+ // on the first method some framework resource mentions.
+ let framework_scope = std::cell::OnceCell::new();
for method in &class.methods {
if method.name_offset == 0
|| method.is_virtual
@@ -177,32 +196,53 @@ impl Backend {
let command = self.build_code_lens_command(title, target_uri, proto.position);
- lenses.push(CodeLens {
- range,
- command: Some(command),
- data: None,
- });
+ push_unique_lens(
+ &mut lenses,
+ &mut seen,
+ CodeLens {
+ range,
+ command: Some(command),
+ data: None,
+ },
+ );
}
+
+ self.push_framework_method_lenses(
+ uri,
+ &index,
+ class,
+ &framework_scope,
+ method,
+ (&mut lenses, &mut seen),
+ );
}
for property in &class.properties {
- if property.name_offset == 0
- || property.is_virtual
- || property.visibility == Visibility::Private
- {
+ if property.name_offset == 0 {
continue;
}
- let member_name = property.name.strip_prefix('$').unwrap_or(&property.name);
- if let Some(lens) = self.build_member_reference_lens(
+ if !property.is_virtual && property.visibility != Visibility::Private {
+ let member_name = property.name.strip_prefix('$').unwrap_or(&property.name);
+ if let Some(lens) = self.build_member_reference_lens(
+ uri,
+ &index,
+ property.name_offset,
+ class_fqn,
+ crate::atom::atom(member_name),
+ property.is_static,
+ ) {
+ lenses.push(lens);
+ }
+ }
+
+ self.push_framework_property_lenses(
uri,
&index,
- property.name_offset,
- class_fqn,
- crate::atom::atom(member_name),
- property.is_static,
- ) {
- lenses.push(lens);
- }
+ class,
+ &framework_scope,
+ property,
+ (&mut lenses, &mut seen),
+ );
}
for constant in &class.constants {
@@ -244,6 +284,33 @@ impl Backend {
}
}
+ self.push_symfony_route_attribute_lenses(
+ uri,
+ content,
+ &index,
+ &classes,
+ &mut lenses,
+ &mut seen,
+ );
+ self.push_doctrine_get_repository_lenses(
+ uri,
+ content,
+ &index,
+ &class_loader,
+ &mut lenses,
+ &mut seen,
+ );
+ self.push_symfony_resource_lenses(uri, content, &index, &mut lenses, &mut seen);
+
+ lenses.sort_by(|a, b| {
+ a.range
+ .start
+ .line
+ .cmp(&b.range.start.line)
+ .then(a.range.start.character.cmp(&b.range.start.character))
+ .then(lens_title(a).cmp(&lens_title(b)))
+ });
+
if lenses.is_empty() {
None
} else {
@@ -713,6 +780,579 @@ impl Backend {
})
}
+ fn push_symfony_resource_lenses(
+ &self,
+ uri: &str,
+ content: &str,
+ index: &LineIndex,
+ lenses: &mut Vec,
+ seen: &mut HashSet,
+ ) {
+ let Some(references) = self.framework_references.read().get(uri).cloned() else {
+ return;
+ };
+
+ for (idx, declaration) in references.iter().enumerate() {
+ if let FrameworkReferenceKind::ConfigKey {
+ path,
+ declaration: true,
+ } = &declaration.kind
+ {
+ let usages = self.framework_config_key_locations(path, false, true);
+ if !usages.is_empty() {
+ self.push_locations_lens(
+ uri,
+ index.position(declaration.start as usize),
+ format!(
+ "Symfony configuration: {} {}",
+ usages.len(),
+ if usages.len() == 1 { "ref" } else { "refs" }
+ ),
+ usages,
+ lenses,
+ seen,
+ );
+ }
+ continue;
+ }
+ if let FrameworkReferenceKind::Translation {
+ domain,
+ name,
+ declaration: true,
+ } = &declaration.kind
+ {
+ let usages = self.framework_translation_locations(domain, name, false, true);
+ if !usages.is_empty() {
+ self.push_locations_lens(
+ uri,
+ index.position(declaration.start as usize),
+ format!(
+ "Symfony translation: {} {}",
+ usages.len(),
+ if usages.len() == 1 { "ref" } else { "refs" }
+ ),
+ usages,
+ lenses,
+ seen,
+ );
+ }
+ continue;
+ }
+ let FrameworkReferenceKind::SymfonySymbol {
+ kind,
+ name,
+ declaration: true,
+ } = &declaration.kind
+ else {
+ continue;
+ };
+ if !matches!(
+ kind,
+ SymfonySymbolKind::Service
+ | SymfonySymbolKind::Parameter
+ | SymfonySymbolKind::Route
+ | SymfonySymbolKind::Template
+ | SymfonySymbolKind::Event
+ | SymfonySymbolKind::MessengerBus
+ ) {
+ continue;
+ }
+
+ let pos = index.position(declaration.start as usize);
+ let usages = self.framework_symfony_symbol_locations(*kind, name, false, true);
+ if !usages.is_empty() {
+ let title = format!(
+ "Symfony {}: {} {}",
+ kind.label(),
+ usages.len(),
+ if usages.len() == 1 { "ref" } else { "refs" }
+ );
+ self.push_locations_lens(uri, pos, title, usages, lenses, seen);
+ }
+
+ if matches!(
+ kind,
+ SymfonySymbolKind::Parameter
+ | SymfonySymbolKind::Template
+ | SymfonySymbolKind::Event
+ | SymfonySymbolKind::MessengerBus
+ ) {
+ continue;
+ }
+ let block_end = references
+ .iter()
+ .skip(idx + 1)
+ .find_map(|candidate| {
+ matches!(
+ &candidate.kind,
+ FrameworkReferenceKind::SymfonySymbol {
+ kind: candidate_kind,
+ declaration: true,
+ ..
+ } if candidate_kind == kind
+ )
+ .then_some(candidate.start)
+ })
+ .unwrap_or(content.len() as u32);
+
+ if *kind == SymfonySymbolKind::Route {
+ if let Some((class_fqn, member_name)) = references.iter().find_map(|candidate| {
+ if candidate.start <= declaration.start || candidate.start >= block_end {
+ return None;
+ }
+ let FrameworkReferenceKind::Method {
+ class_fqn,
+ member_name,
+ } = &candidate.kind
+ else {
+ return None;
+ };
+ Some((class_fqn.as_str(), member_name.as_str()))
+ }) && let Some(location) =
+ self.resolve_framework_member_definition(uri, content, class_fqn, member_name)
+ {
+ self.push_locations_lens(
+ uri,
+ pos,
+ format!(
+ "Symfony controller: {}::{}",
+ short_name(class_fqn),
+ member_name
+ ),
+ vec![location],
+ lenses,
+ seen,
+ );
+ }
+ continue;
+ }
+
+ if let Some(class_fqn) = references.iter().find_map(|candidate| {
+ if candidate.start <= declaration.start || candidate.start >= block_end {
+ return None;
+ }
+ let FrameworkReferenceKind::Class { fqn } = &candidate.kind else {
+ return None;
+ };
+ Some(fqn.as_str())
+ }) && let Some(location) = self.class_location(class_fqn, uri, content)
+ {
+ self.push_locations_lens(
+ uri,
+ pos,
+ format!("Symfony service class: {}", short_name(class_fqn)),
+ vec![location],
+ lenses,
+ seen,
+ );
+ }
+ }
+ }
+
+ fn push_framework_class_lenses(
+ &self,
+ uri: &str,
+ content: &str,
+ index: &LineIndex,
+ class: &ClassInfo,
+ class_loader: &dyn Fn(&str) -> Option>,
+ output: (&mut Vec, &mut HashSet),
+ ) {
+ let (lenses, seen) = output;
+ let class_fqn = class.fqn();
+ let Some(source_pos) = class_lens_position(index, class) else {
+ return;
+ };
+
+ let config_locations = self.framework_class_reference_locations(class_fqn.as_str());
+ if !config_locations.is_empty() {
+ let title = if config_locations.len() == 1 {
+ "Symfony/Doctrine config: 1 ref".to_string()
+ } else {
+ format!("Symfony/Doctrine config: {} refs", config_locations.len())
+ };
+ self.push_locations_lens(
+ uri,
+ source_pos,
+ title,
+ config_locations,
+ &mut *lenses,
+ &mut *seen,
+ );
+ }
+
+ for (message_fqn, handler_fqn) in self.framework_messenger_mappings_for_class(&class_fqn) {
+ let (target, title) = if framework_fqn_eq(&class_fqn, &message_fqn) {
+ (
+ handler_fqn.as_str(),
+ format!("Symfony Messenger handler: {}", short_name(&handler_fqn)),
+ )
+ } else if framework_fqn_eq(&class_fqn, &handler_fqn) {
+ (
+ message_fqn.as_str(),
+ format!("Symfony Messenger message: {}", short_name(&message_fqn)),
+ )
+ } else {
+ continue;
+ };
+ if let Some(location) = self.class_location(target, uri, content) {
+ self.push_locations_lens(
+ uri,
+ source_pos,
+ title,
+ vec![location],
+ &mut *lenses,
+ &mut *seen,
+ );
+ }
+ }
+
+ for repo_fqn in self
+ .doctrine_repository_fqns_for_entity(class_fqn.as_str(), class_loader)
+ .into_iter()
+ .filter(|fqn| !is_builtin_doctrine_repository_fqn(fqn))
+ {
+ if let Some(location) = self.class_location(&repo_fqn, uri, content) {
+ let title = format!("Doctrine repository: {}", short_name(&repo_fqn));
+ self.push_locations_lens(
+ uri,
+ source_pos,
+ title,
+ vec![location],
+ &mut *lenses,
+ &mut *seen,
+ );
+ }
+ }
+
+ for entity_fqn in self.doctrine_entities_for_repository(class_fqn.as_str(), class_loader) {
+ if let Some(location) = self.class_location(&entity_fqn, uri, content) {
+ let title = format!("Doctrine entity: {}", short_name(&entity_fqn));
+ self.push_locations_lens(
+ uri,
+ source_pos,
+ title,
+ vec![location],
+ &mut *lenses,
+ &mut *seen,
+ );
+ }
+ }
+ }
+
+ fn push_framework_method_lenses(
+ &self,
+ uri: &str,
+ index: &LineIndex,
+ class: &ClassInfo,
+ scope: &std::cell::OnceCell,
+ method: &MethodInfo,
+ output: (&mut Vec, &mut HashSet),
+ ) {
+ let (lenses, seen) = output;
+ let method_name = method.name.as_str();
+ if !self.has_framework_member_references(method_name) {
+ return;
+ }
+ let scope = scope.get_or_init(|| self.framework_class_scope(class));
+
+ let route_locations = self.framework_member_reference_locations(method_name, Some(scope));
+ if route_locations.is_empty() {
+ return;
+ }
+ let pos = index.position(method.name_offset as usize);
+
+ let title = if route_locations.len() == 1 {
+ "Symfony config: 1 ref".to_string()
+ } else {
+ format!("Symfony config: {} refs", route_locations.len())
+ };
+ self.push_locations_lens(uri, pos, title, route_locations, lenses, seen);
+ }
+
+ fn push_framework_property_lenses(
+ &self,
+ uri: &str,
+ index: &LineIndex,
+ class: &ClassInfo,
+ scope: &std::cell::OnceCell,
+ property: &crate::types::PropertyInfo,
+ output: (&mut Vec, &mut HashSet),
+ ) {
+ let (lenses, seen) = output;
+ if !self.has_framework_property_references(&property.name) {
+ return;
+ }
+ let scope = scope.get_or_init(|| self.framework_class_scope(class));
+ let locations = self.framework_property_reference_locations(&property.name, Some(scope));
+ if locations.is_empty() {
+ return;
+ }
+ let title = format!(
+ "Symfony form/validation: {} {}",
+ locations.len(),
+ if locations.len() == 1 { "ref" } else { "refs" }
+ );
+ let pos = index.position(property.name_offset as usize);
+ self.push_locations_lens(uri, pos, title, locations, lenses, seen);
+ }
+
+ /// The class and its ancestors: the classes a framework resource may
+ /// name one of its members through.
+ fn framework_class_scope(&self, class: &ClassInfo) -> crate::references::MemberScope {
+ let mut hierarchy = HashSet::new();
+ hierarchy.insert(class.fqn().to_string());
+ hierarchy.extend(self.class_hierarchy_names(class));
+ crate::references::MemberScope::exact(hierarchy)
+ }
+
+ fn push_symfony_route_attribute_lenses(
+ &self,
+ uri: &str,
+ content: &str,
+ index: &LineIndex,
+ classes: &[Arc],
+ lenses: &mut Vec,
+ seen: &mut HashSet,
+ ) {
+ let attributes = route_attributes(content);
+ if attributes.is_empty() {
+ return;
+ }
+ let declarations = code_lens_declarations(index, classes);
+ if declarations.is_empty() {
+ return;
+ }
+
+ for attr in attributes {
+ // `declarations` is sorted by offset, so the first one past the
+ // attribute is the declaration it decorates.
+ let next = declarations.partition_point(|decl| decl.offset <= attr.end);
+ let Some(decl) = declarations.get(next) else {
+ continue;
+ };
+ if decl.offset.saturating_sub(attr.end) > 1024 {
+ continue;
+ }
+ let between = &content[attr.end..decl.offset];
+ if between.contains(';') || between.contains('{') || between.contains('}') {
+ continue;
+ }
+ let Some(title) = route_attribute_lens_title(&attr, decl.kind) else {
+ continue;
+ };
+
+ let source_pos = decl.position;
+ let Ok(parsed_uri) = Url::parse(uri) else {
+ continue;
+ };
+ let location = Location {
+ uri: parsed_uri,
+ range: Range {
+ start: source_pos,
+ end: source_pos,
+ },
+ };
+ self.push_locations_lens(uri, source_pos, title, vec![location], lenses, seen);
+ }
+ }
+
+ fn push_doctrine_get_repository_lenses(
+ &self,
+ uri: &str,
+ content: &str,
+ index: &LineIndex,
+ class_loader: &dyn Fn(&str) -> Option>,
+ lenses: &mut Vec,
+ seen: &mut HashSet,
+ ) {
+ let calls = get_repository_calls(content);
+ if calls.is_empty() {
+ return;
+ }
+ let ctx = self.file_context(uri);
+ for call in calls {
+ let offset = call.offset as u32;
+ let Some(entity_fqn) = class_expr_arg_to_fqn(
+ &call.first_arg,
+ ctx.use_map_at(offset),
+ ctx.namespace_at(offset),
+ &ctx.classes,
+ offset,
+ ) else {
+ continue;
+ };
+ let mut locations = Vec::new();
+ let mut title = None;
+
+ for repo_fqn in self
+ .doctrine_repository_fqns_for_entity(&entity_fqn, class_loader)
+ .into_iter()
+ .filter(|fqn| !is_builtin_doctrine_repository_fqn(fqn))
+ {
+ if let Some(location) = self.class_location(&repo_fqn, uri, content) {
+ title = Some(format!("Doctrine repository: {}", short_name(&repo_fqn)));
+ locations.push(location);
+ break;
+ }
+ }
+
+ if locations.is_empty()
+ && let Some(location) = self.class_location(&entity_fqn, uri, content)
+ {
+ title = Some(format!("Doctrine entity: {}", short_name(&entity_fqn)));
+ locations.push(location);
+ }
+
+ let Some(title) = title else {
+ continue;
+ };
+ let pos = index.position(call.offset);
+ self.push_locations_lens(uri, pos, title, locations, lenses, seen);
+ }
+ }
+
+ fn push_locations_lens(
+ &self,
+ origin_uri: &str,
+ source_pos: Position,
+ title: String,
+ locations: Vec,
+ lenses: &mut Vec,
+ seen: &mut HashSet,
+ ) {
+ if locations.is_empty() {
+ return;
+ }
+ let Ok(origin_url) = Url::parse(origin_uri) else {
+ return;
+ };
+ let range = Range {
+ start: Position {
+ line: source_pos.line,
+ character: 0,
+ },
+ end: Position {
+ line: source_pos.line,
+ character: 0,
+ },
+ };
+ let command =
+ self.build_code_lens_locations_command(title, origin_url, source_pos, locations);
+ push_unique_lens(
+ lenses,
+ seen,
+ CodeLens {
+ range,
+ command: Some(command),
+ data: None,
+ },
+ );
+ }
+
+ fn class_location(
+ &self,
+ fqn: &str,
+ current_uri: &str,
+ current_content: &str,
+ ) -> Option {
+ let class_info = self.find_or_load_class(fqn)?;
+ let class_fqn = class_info.fqn();
+ let (file_uri, file_content) =
+ self.find_class_file_content(&class_fqn, current_uri, current_content)?;
+ let offset = class_info
+ .keyword_offset
+ .max(class_info.decl_start_offset)
+ .min(file_content.len() as u32);
+ if offset == 0 {
+ return None;
+ }
+ let uri = Url::parse(&file_uri).ok()?;
+ let pos = offset_to_position(&file_content, offset as usize);
+ Some(Location {
+ uri,
+ range: Range {
+ start: pos,
+ end: pos,
+ },
+ })
+ }
+
+ fn doctrine_entities_for_repository(
+ &self,
+ repository_fqn: &str,
+ class_loader: &dyn Fn(&str) -> Option>,
+ ) -> Vec {
+ let mut out = self.framework_doctrine_entity_fqns_for_repository(repository_fqn);
+ let repository = normalize_class_name(repository_fqn);
+ if !out.is_empty() {
+ return out;
+ }
+ let Some(repository_class) = class_loader(&repository) else {
+ return out;
+ };
+ if !looks_like_doctrine_repository(&repository_class) {
+ return out;
+ }
+
+ let mut candidates: Vec = Vec::new();
+ {
+ let index = self.symbols.fqn_class_index.read();
+ candidates.extend(index.keys().map(|key| key.to_string()));
+ }
+ {
+ let uri_index = self.symbols.uri_classes_index.read();
+ for classes in uri_index.values() {
+ for class in classes {
+ candidates.push(class.fqn().to_string());
+ }
+ }
+ }
+ candidates.sort();
+ candidates.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
+
+ for entity_fqn in candidates {
+ if entity_fqn.eq_ignore_ascii_case(&repository) {
+ continue;
+ }
+ if !looks_like_doctrine_entity_name(&entity_fqn) {
+ continue;
+ }
+ if doctrine_repository_matches_entity_convention(&entity_fqn, &repository)
+ && !out
+ .iter()
+ .any(|known| known.eq_ignore_ascii_case(&entity_fqn))
+ {
+ out.push(entity_fqn);
+ }
+ }
+
+ out
+ }
+
+ fn class_hierarchy_names(&self, class: &ClassInfo) -> Vec {
+ let mut out: Vec = Vec::new();
+ let mut parent_name = class.parent_class;
+ for _ in 0..MAX_INHERITANCE_DEPTH {
+ let Some(name) = parent_name else {
+ break;
+ };
+ let parent_fqn = name.to_string();
+ if out
+ .iter()
+ .any(|known| known.eq_ignore_ascii_case(&parent_fqn))
+ {
+ break;
+ }
+ out.push(parent_fqn);
+ let Some(parent) = self.find_or_load_class(&name) else {
+ break;
+ };
+ parent_name = parent.parent_class;
+ }
+ out
+ }
+
/// The closest ancestor that declares a method with the given name,
/// as a navigation target.
///
@@ -791,6 +1431,24 @@ impl Backend {
}
}
+ fn build_code_lens_locations_command(
+ &self,
+ title: String,
+ origin_uri: Url,
+ origin_position: Position,
+ locations: Vec,
+ ) -> Command {
+ Command {
+ title,
+ command: "editor.action.showReferences".to_string(),
+ arguments: Some(vec![
+ serde_json::json!(origin_uri),
+ serde_json::json!(origin_position),
+ serde_json::json!(locations),
+ ]),
+ }
+ }
+
/// Build a `Prototype` by locating the method's position in the
/// ancestor's source file.
fn build_prototype(
@@ -821,3 +1479,434 @@ impl Backend {
})
}
}
+
+fn framework_fqn_eq(lhs: &str, rhs: &str) -> bool {
+ lhs.trim_start_matches('\\')
+ .eq_ignore_ascii_case(rhs.trim_start_matches('\\'))
+}
+
+#[derive(Clone, Copy)]
+struct CodeLensDeclaration {
+ offset: usize,
+ position: Position,
+ kind: RouteDeclarationKind,
+}
+
+#[derive(Clone, Copy)]
+enum RouteDeclarationKind {
+ Class,
+ Method,
+}
+
+struct RouteAttribute {
+ end: usize,
+ path: Option,
+ name: Option,
+ methods: Vec,
+}
+
+struct GetRepositoryCall {
+ offset: usize,
+ first_arg: String,
+}
+
+fn class_lens_position(index: &LineIndex, class: &ClassInfo) -> Option {
+ let offset = if class.keyword_offset > 0 {
+ class.keyword_offset
+ } else {
+ class.decl_start_offset
+ };
+ if offset == 0 || offset as usize > index.content().len() {
+ None
+ } else {
+ Some(index.position(offset as usize))
+ }
+}
+
+fn code_lens_declarations(
+ index: &LineIndex,
+ classes: &[Arc],
+) -> Vec {
+ let mut declarations = Vec::new();
+ for class in classes {
+ if let Some(position) = class_lens_position(index, class) {
+ let offset = if class.keyword_offset > 0 {
+ class.keyword_offset
+ } else {
+ class.decl_start_offset
+ };
+ declarations.push(CodeLensDeclaration {
+ offset: offset as usize,
+ position,
+ kind: RouteDeclarationKind::Class,
+ });
+ }
+ for method in &class.methods {
+ if method.name_offset == 0 || method.is_virtual {
+ continue;
+ }
+ declarations.push(CodeLensDeclaration {
+ offset: method.name_offset as usize,
+ position: index.position(method.name_offset as usize),
+ kind: RouteDeclarationKind::Method,
+ });
+ }
+ }
+ declarations.sort_by_key(|decl| decl.offset);
+ declarations
+}
+
+fn route_attributes(content: &str) -> Vec {
+ let mut attributes = Vec::new();
+ let mut search = 0usize;
+ while let Some(rel) = content[search..].find("#[") {
+ let start = search + rel;
+ let Some(end) = find_attribute_end(content, start) else {
+ break;
+ };
+ let attr = &content[start..end];
+ if !is_route_attribute(attr) {
+ search = end;
+ continue;
+ }
+ let args = attr
+ .find('(')
+ .zip(attr.rfind(')'))
+ .and_then(|(open, close)| (close > open).then_some(&attr[open + 1..close]))
+ .unwrap_or("");
+ let path = find_named_string_arg(args, "path").or_else(|| first_string_literal(args));
+ let name = find_named_string_arg(args, "name");
+ let methods = find_methods_arg(args);
+ attributes.push(RouteAttribute {
+ end,
+ path,
+ name,
+ methods,
+ });
+ search = end;
+ }
+ attributes
+}
+
+fn route_attribute_lens_title(
+ attr: &RouteAttribute,
+ decl_kind: RouteDeclarationKind,
+) -> Option {
+ let mut title = String::new();
+ match decl_kind {
+ RouteDeclarationKind::Class => title.push_str("Symfony route prefix"),
+ RouteDeclarationKind::Method => title.push_str("Symfony route"),
+ }
+
+ let mut parts = Vec::new();
+ if !attr.methods.is_empty() {
+ parts.push(attr.methods.join("|"));
+ }
+ if let Some(path) = &attr.path
+ && !path.is_empty()
+ {
+ parts.push(path.clone());
+ }
+ if let Some(name) = &attr.name
+ && !name.is_empty()
+ {
+ parts.push(format!("({name})"));
+ }
+
+ if parts.is_empty() {
+ None
+ } else {
+ title.push_str(": ");
+ title.push_str(&parts.join(" "));
+ Some(title)
+ }
+}
+
+fn find_attribute_end(content: &str, start: usize) -> Option {
+ let bytes = content.as_bytes();
+ let mut i = start + 2;
+ let mut depth = 1usize;
+ let mut quote: Option = None;
+ while i < bytes.len() {
+ let byte = bytes[i];
+ if let Some(q) = quote {
+ if byte == b'\\' {
+ i += 2;
+ continue;
+ }
+ if byte == q {
+ quote = None;
+ }
+ i += 1;
+ continue;
+ }
+ match byte {
+ b'\'' | b'"' => quote = Some(byte),
+ b'[' => depth += 1,
+ b']' => {
+ depth = depth.saturating_sub(1);
+ if depth == 0 {
+ return Some(i + 1);
+ }
+ }
+ _ => {}
+ }
+ i += 1;
+ }
+ None
+}
+
+fn is_route_attribute(attr: &str) -> bool {
+ let lower = attr.to_ascii_lowercase();
+ lower.starts_with("#[route")
+ || lower.starts_with("#[\\symfony\\component\\routing\\attribute\\route")
+ || lower.starts_with("#[symfony\\component\\routing\\attribute\\route")
+ || lower.starts_with("#[\\symfony\\component\\routing\\annotation\\route")
+ || lower.starts_with("#[symfony\\component\\routing\\annotation\\route")
+}
+
+fn find_named_string_arg(args: &str, name: &str) -> Option {
+ let pattern = format!("{name}:");
+ let mut search = 0usize;
+ while let Some(rel) = args[search..].find(&pattern) {
+ let start = search + rel;
+ if start > 0 {
+ let prev = args.as_bytes()[start - 1];
+ if prev == b'_' || prev.is_ascii_alphanumeric() {
+ search = start + pattern.len();
+ continue;
+ }
+ }
+ let value_start = start + pattern.len();
+ return first_string_literal(&args[value_start..]);
+ }
+ None
+}
+
+fn first_string_literal(text: &str) -> Option {
+ let bytes = text.as_bytes();
+ let mut i = 0usize;
+ while i < bytes.len() {
+ let quote = bytes[i];
+ if quote != b'\'' && quote != b'"' {
+ i += 1;
+ continue;
+ }
+ let mut value = String::new();
+ i += 1;
+ while i < bytes.len() {
+ if bytes[i] == b'\\' && i + 1 < bytes.len() {
+ value.push(bytes[i + 1] as char);
+ i += 2;
+ continue;
+ }
+ if bytes[i] == quote {
+ return Some(value);
+ }
+ value.push(bytes[i] as char);
+ i += 1;
+ }
+ return None;
+ }
+ None
+}
+
+fn find_methods_arg(args: &str) -> Vec {
+ let Some(start) = args.find("methods:") else {
+ return Vec::new();
+ };
+ let tail = &args[start + "methods:".len()..];
+ let end = tail.find("]").map(|idx| idx + 1).unwrap_or_else(|| {
+ tail.find(',')
+ .or_else(|| tail.find(')'))
+ .unwrap_or(tail.len())
+ });
+ let segment = &tail[..end];
+ let mut out = Vec::new();
+ let mut search = 0usize;
+ while let Some(method) = first_string_literal(&segment[search..]) {
+ let Some(pos) = segment[search..].find(&method) else {
+ break;
+ };
+ let method_len = method.len();
+ if !out.iter().any(|known: &String| known == &method) {
+ out.push(method);
+ }
+ search += pos + method_len + 1;
+ if search >= segment.len() {
+ break;
+ }
+ }
+ out
+}
+
+fn get_repository_calls(content: &str) -> Vec {
+ let mut calls = Vec::new();
+ let mut search = 0usize;
+ while let Some(rel) = content[search..].find("getRepository") {
+ let name_start = search + rel;
+ let name_end = name_start + "getRepository".len();
+ if name_start > 0 && is_ident_byte(content.as_bytes()[name_start - 1]) {
+ search = name_end;
+ continue;
+ }
+ if content
+ .as_bytes()
+ .get(name_end)
+ .is_some_and(|byte| is_ident_byte(*byte))
+ {
+ search = name_end;
+ continue;
+ }
+ let Some(open) = content[name_end..].find('(').map(|open| name_end + open) else {
+ break;
+ };
+ if !content[name_end..open].trim().is_empty() {
+ search = name_end;
+ continue;
+ }
+ let Some(close) = find_matching_paren(content, open) else {
+ break;
+ };
+ let args = &content[open + 1..close];
+ if let Some(first_arg) = split_first_arg(args) {
+ calls.push(GetRepositoryCall {
+ offset: name_start,
+ first_arg: first_arg.to_string(),
+ });
+ }
+ search = close + 1;
+ }
+ calls
+}
+
+fn class_expr_arg_to_fqn(
+ first_arg: &str,
+ use_map: &std::collections::HashMap,
+ namespace: &Option,
+ local_classes: &[Arc],
+ access_offset: u32,
+) -> Option {
+ let class_expr = first_arg.trim().strip_suffix("::class")?.trim();
+ let class_expr = class_expr.trim_start_matches('\\');
+ if class_expr.is_empty() {
+ return None;
+ }
+ match class_expr {
+ "self" | "static" => {
+ find_class_at_offset(local_classes, access_offset).map(|class| class.fqn().to_string())
+ }
+ "parent" => find_class_at_offset(local_classes, access_offset)
+ .and_then(|class| class.parent_class.map(|parent| parent.to_string())),
+ _ => Some(Backend::resolve_to_fqn(class_expr, use_map, namespace)),
+ }
+}
+
+fn find_matching_paren(content: &str, open: usize) -> Option {
+ let bytes = content.as_bytes();
+ let mut depth = 0usize;
+ let mut quote: Option = None;
+ let mut i = open;
+ while i < bytes.len() {
+ let byte = bytes[i];
+ if let Some(q) = quote {
+ if byte == b'\\' {
+ i += 2;
+ continue;
+ }
+ if byte == q {
+ quote = None;
+ }
+ i += 1;
+ continue;
+ }
+ match byte {
+ b'\'' | b'"' => quote = Some(byte),
+ b'(' => depth += 1,
+ b')' => {
+ depth = depth.saturating_sub(1);
+ if depth == 0 {
+ return Some(i);
+ }
+ }
+ _ => {}
+ }
+ i += 1;
+ }
+ None
+}
+
+fn split_first_arg(args: &str) -> Option<&str> {
+ let bytes = args.as_bytes();
+ let mut quote: Option = None;
+ let mut paren_depth = 0usize;
+ let mut bracket_depth = 0usize;
+ for (i, byte) in bytes.iter().enumerate() {
+ if let Some(q) = quote {
+ if *byte == b'\\' {
+ continue;
+ }
+ if *byte == q {
+ quote = None;
+ }
+ continue;
+ }
+ match *byte {
+ b'\'' | b'"' => quote = Some(*byte),
+ b'(' => paren_depth += 1,
+ b')' => paren_depth = paren_depth.saturating_sub(1),
+ b'[' => bracket_depth += 1,
+ b']' => bracket_depth = bracket_depth.saturating_sub(1),
+ b',' if paren_depth == 0 && bracket_depth == 0 => return Some(args[..i].trim()),
+ _ => {}
+ }
+ }
+ let trimmed = args.trim();
+ if trimmed.is_empty() {
+ None
+ } else {
+ Some(trimmed)
+ }
+}
+
+fn is_ident_byte(byte: u8) -> bool {
+ byte == b'_' || byte.is_ascii_alphanumeric()
+}
+
+fn is_builtin_doctrine_repository_fqn(fqn: &str) -> bool {
+ let normalized = normalize_class_name(fqn);
+ normalized.starts_with("Doctrine\\")
+ || matches!(
+ short_name(&normalized),
+ "ServiceEntityRepository" | "EntityRepository" | "ObjectRepository"
+ )
+}
+
+fn looks_like_doctrine_entity_name(fqn: &str) -> bool {
+ let normalized = normalize_class_name(fqn);
+ normalized.contains("\\Entity\\")
+ || normalized.contains("\\Entities\\")
+ || short_name(&normalized).ends_with("Entity")
+}
+
+fn normalize_class_name(name: &str) -> String {
+ name.trim().trim_start_matches('\\').to_string()
+}
+
+fn push_unique_lens(lenses: &mut Vec, seen: &mut HashSet, lens: CodeLens) {
+ let title = lens_title(&lens);
+ let key = format!(
+ "{}:{}:{}",
+ lens.range.start.line, lens.range.start.character, title
+ );
+ if seen.insert(key) {
+ lenses.push(lens);
+ }
+}
+
+fn lens_title(lens: &CodeLens) -> String {
+ lens.command
+ .as_ref()
+ .map(|command| command.title.clone())
+ .unwrap_or_default()
+}
diff --git a/src/completion/handler/mod.rs b/src/completion/handler/mod.rs
index b6c76b605..88bbc0983 100644
--- a/src/completion/handler/mod.rs
+++ b/src/completion/handler/mod.rs
@@ -192,6 +192,10 @@ impl Backend {
});
if let Some(content) = content {
+ if crate::framework::is_framework_resource_uri(&uri) {
+ return Ok(self.try_symfony_completion(&uri, &content, position));
+ }
+
let response = (|| -> Result