diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1e3b7ef7f..c731eb3dc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -104,6 +104,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' -exec php -l {} \;
benchmark:
name: Benchmark
diff --git a/README.md b/README.md
index 0aa46dc5f..789ab4f3c 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ PHPantom focuses on deep type intelligence. Here's how it compares:
| Closure parameter inference | β
| π§ | π§ | π§ | π§ |
| Laravel | β
| β | π§ | β | π§ |
| Blade templates | π§ | β | β
| β | π§ |
+| Symfony / Twig / Doctrine | β
| π§ | π§ | π§ | π§© |
| Other frameworks4 | π§ | π§ | π§ | π§ | π§© |
| **Refactoring** | | | | | |
| Rename | β
| π | π | β
| β
|
@@ -60,14 +61,14 @@ PHPantom focuses on deep type intelligence. Here's how it compares:
1 Completion, hover, signature help, go-to-definition, find references, diagnostics, document symbols.
2 Auto-import, go-to implementation / type-definition, smart select, folding ranges, formatting, code lens, inlay hints, type hierarchy, document links.
3 Implement interface methods, extract method/function, extract/inline variable, generate constructor, generate getter/setter.
-4 CakePHP, non-Composer WordPress, Symfony, Behat, PHPUnit, and Prophecy, and Twig.
+4 CakePHP, non-Composer WordPress, Behat, PHPUnit, and Prophecy.
5 Convert between arrow functions and closures, and switch statements to match expressions.
Performance measured on a production codebase: 21K PHP files, 1.5M lines of code (vendor + application). Time to ready is CPU time consumed until full type intelligence is available on a cold start (first index); tools with a disk cache launch faster on subsequent starts.
> [!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
@@ -77,6 +78,7 @@ Performance measured on a production codebase: 21K PHP files, 1.5M lines of code
- **Conditional return types.** PHPStan-style conditional `@return` types resolve to the concrete branch at each call site.
- **Type aliases and shapes.** `@phpstan-type`, `@phpstan-import-type`, and `object{...}` shapes all resolve through to completions.
- **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 0efd65239..f085a10ca 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": []
}
}
},
@@ -147,6 +172,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 5cc37501a..faf998237 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -63,10 +63,12 @@ src/
βββ composer.rs # composer.json / PSR-4 autoload parsing
βββ names.rs # Name resolution (FQN, use-map, namespace)
βββ reference_index.rs # Workspace-wide reference index for find-references / rename
-βββ reference_counts.rs # Background-computed member reference counts for the declaration inlay hints
+βββ reference_counts.rs # Bounded exact member-reference cache for declaration hints and lenses
β
β # 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)
@@ -98,6 +100,7 @@ src/
β # LSP features (one module each)
βββ hover/ # Hover: symbol-map dispatch, type/signature/docblock formatting
βββ definition/ # Go-to-definition (resolve, member, variable/, implementation, type_definition)
+βββ resource_navigation.rs # Schema-free PHP class/member indexing and navigation in YAML/XML
βββ references/, rename/, highlight/
βββ signature_help.rs, semantic_tokens.rs, inlay_hints.rs, folding.rs, code_lens.rs
βββ document_symbols.rs, document_links.rs, workspace_symbols.rs, formatting.rs
@@ -117,7 +120,8 @@ src/
βββ fix.rs # `fix` CLI subcommand (automated code fixes)
βββ 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
@@ -141,6 +145,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)
@@ -877,12 +905,14 @@ When the user invokes "Find All References", PHPantom scans all user files for o
Before scanning, `ensure_workspace_indexed` ensures all user files have symbol maps:
1. **Phase 1: fqn_uri_index files (user only)** β files already known from `update_ast` calls. Vendor and stub URIs are skipped.
-2. **Phase 2: `.gitignore`-aware workspace walk** β uses the `ignore` crate's `WalkBuilder` to recursively discover PHP files under the workspace root, respecting `.gitignore` rules (including nested and global gitignore files). This automatically skips generated/cached directories like `storage/framework/views/` (Laravel blade cache), `var/cache/` (Symfony), and `node_modules/`. The vendor directory is always skipped regardless of `.gitignore` content. Hidden directories are skipped by default.
+2. **Phase 2: `.gitignore`-aware workspace walk** β uses the `ignore` crate's `WalkBuilder` to recursively discover PHP plus YAML/XML resource files under the workspace root, respecting `.gitignore` rules (including nested and global gitignore files). This automatically skips generated/cached directories like `storage/framework/views/` (Laravel blade cache), `var/cache/` (Symfony), and `node_modules/`. The vendor directory is always skipped regardless of `.gitignore` content. Hidden directories are skipped by default.
-Both phases parse files in parallel using `std::thread::scope`. The work is split into chunks (one per CPU core) and each thread reads a file from disk and calls `update_ast`, which acquires write locks briefly to store results while the expensive parsing step runs without any locks held. Batches of 2 or fewer files skip threading overhead.
+PHP files are parsed in parallel using `std::thread::scope`. The work is split into chunks (one per CPU core) and each thread reads a file from disk and calls `update_ast`, which acquires write locks briefly to store results while the expensive parsing step runs without any locks held. Batches of 2 or fewer files skip threading overhead. YAML/XML files take the lightweight schema-free scanner and publish synthetic class/member symbol maps into the same reference index.
Parsed files stay cached in `uri_classes_index`, `symbol_maps`, `file_imports`, and `file_namespaces` after the scan completes. There is no post-scan eviction; keeping the entries means subsequent operations (a second find-references call, go-to-definition on a cross-file symbol) benefit from the work already done.
+The workspace reference index keeps its primary map deliberately coarse: it stores candidate URIs and occurrence counts, not a second copy of every source position. CodeLens can therefore answer a conclusive zero without a semantic scan. The first nonzero member query resolves every member receiver in each candidate file while one forward-walked variable scope is active, packs the target class atoms by symbol-span index, and retains that compact per-file semantic layer for later member names. Candidate files are filled in parallel; edits evict their own layer, and signature changes clear layers whose receiver types may have changed. Exact locations remain bounded behind the 50,000-location annotation cache. Refresh-capable clients receive the lens after the background result is ready; other clients retain lazy `codeLens/resolve` as a compatibility path.
+
### Cross-file scanning
The `user_file_symbol_maps()` helper snapshots all symbol maps whose URI does not fall under the vendor directory or the internal stub scheme. With `Arc`, the snapshot is a vector of cheap reference-count increments rather than deep clones. Four scanners use this snapshot:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index e330c52c1..d922bd1d5 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- **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.
+- **Reference CodeLens.** PHP declarations show clickable exact reference counts. Declarations with no indexed uses are answered immediately, while semantic member locations are cached in a bounded background index so opening a large file does not fan out into an expensive resolve request per lens. Clients that support CodeLens refresh receive only ready, fully resolved member lenses. Contributed by @sidux.
+- **Call Hierarchy.** PHP functions and methods expose standard incoming and outgoing call navigation by reusing the existing references and definition pipelines. 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.
+- **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.
- **`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.
### Changed
@@ -53,6 +59,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`@phpstan-assert` narrows a property of the receiver, not just an argument.** A tag naming a path through `$this` (`@phpstan-assert bool $this->resolved`) was ignored, because the subject was looked for among the call's arguments and such a call often has none. The lazy-initialiser idiom (`if ($this->resolved === null) { $this->resolve(); } return $this->resolved;`) therefore reported the nullable property as the return value. The tag's `$this` now stands for whatever the call was made on, so it narrows through a variable receiver as well.
- **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 CodeLens stays responsive in large projects.** PHP member receivers are resolved once per file in parallel and kept in a compact semantic index, so later lens batches filter exact references without reopening or walking source files. The worker drains a request burst before sending one editor refresh, and clients without lens refresh compute only the lens they resolve. Contributed by @sidux.
+- **Reference CodeLens stays responsive when a project has many framework resources or repeated member accesses.** Symfony and Doctrine class/member links now use an incremental inverted index instead of scanning every YAML/XML reference for each PHP declaration. Exact member searches also reuse one parsed PHP syntax tree per candidate file, avoiding repeated reparses for common methods. Contributed by @sidux.
+- **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.
+- **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.
- **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.
- **A global function written with a leading backslash is the same function.** `\get_class($x) === Foo::class` and `\is_a($x, Foo::class)` narrowed nothing, and `if (!\class_exists('Vendor\Optional\Config')) { return; }` read as an un-negated guard, so it protected the `return;` instead of everything after it and the guarded class was reported as not found. A class named in such a guard with escaped backslashes (`'Vendor\\Optional\\Config'`) is now matched against the reference it guards, too.
@@ -166,6 +176,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Laravel and Carbon macros registered with `mixin()`.** A `Str::mixin(new StrMixin())` or `Collection::mixin(CollectionMixin::class)` call contributes one macro per public method of the mixin, taking the signature of the closure that method returns, so those methods complete, hover, resolve, and type-check. Carbon's trait-based `mixin()` is read the way Carbon reads it, where the trait's methods become methods on the target directly. Contributed by @shuvroroy (#256) and @calebdw.
- **Larastan's `model-property` is checked and completed.** The pseudo-type is resolved against the model's known properties during argument checking, so a string literal that names no property is flagged, and typing inside such an argument completes the model's property names. Contributed by @calebdw.
+#### Symfony and Doctrine
+
+- **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.
+- **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 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. Namespace and resource-path refactors also update matching framework configuration. Contributed by @sidux.
+
#### Diagnostics
- **Two new diagnostics: illegal `readonly` writes and self-contradicting docblocks.** A write to a `readonly` property from anywhere PHP forbids one, and a `@param` or `@return` tag that contradicts the nullability of the declaration it documents, are now reported where you write them rather than when the code runs. Every form the readonly write can take is checked, including the ones that are easy to overlook (`unset()`, a `foreach` or destructuring target, taking a reference), and the writes the language allows are left alone.
@@ -234,6 +254,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- **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.
- **The deprecation pass parses a file once, not once per subject.** Deprecation checking recently became precise about which member accesses can share a resolved type, at the cost of resolving many more distinct subjects per file. Each of those resolutions re-parsed the file from scratch, and every `$this` inside a closure body paid for a fresh parse of its own, so diagnostics on a file with many closures ran several times slower than before. The pass now reuses one parsed AST and one chain-resolution cache across the whole file, the same way the unknown-member pass always has, making it faster than it was before the precision fix.
- **Go-to-definition on a Blade echo delimiter agrees with its hover.** `{{ }}` compiles to a call to `e()`, which is not written anywhere in the template, and hovering the `{{`/`}}` itself already reflected that by describing the implicit `e()` call. Ctrl+Click on the same character disagreed: it fell through to the underlying PHP expression and landed on whatever the delimiter happened to sit next to, such as `route(...)` in `{{ route('pages.index') }}`. It now targets `e()` too.
- **A method chain no longer resolves against another file's `use` import.** The cache that reuses a shared chain prefix (`Pen::make()` in `Pen::make()->write()`) keyed its entries by the chain's text alone, with nothing to tell two files apart. A background scan that walks many files under one cache activation, such as Find References or the reference-count computation behind the inlay hints, could resolve a chain in one file against a same-named class a different file imports under the same alias (`use A\Pen;` in one, `use B\Pen;` in another), undercounting or overcounting references depending on which file the cache was populated from first. Each file's chains are now cached separately.
@@ -564,6 +586,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Docblock navigation lands on the right name in types that mix a `*` wildcard with a non-ASCII name.** In a type such as `@return Map`, go-to-definition, find references, and rename measured every name after the accented one against the wrong bytes, so clicking `User` resolved nothing (or the wrong symbol). The PHPStan `*` wildcard is now read directly by the type grammar rather than rewritten to `mixed` beforehand, which removes the byte-offset bookkeeping that was corrupting the positions.
- **Formatting a short method chain starting with `new X(...)` no longer breaks it across lines unnecessarily.** When the constructor call's own arguments were long enough to wrap, the formatter also forced a short trailing chain like `(new Foo(...))->bar()` onto separate lines even though it would have fit on one. Upstream fix from mago 1.44.0.
- **`@method` and `@property` tags on an implemented interface are now always applied.** A class that declared no docblock of its own missed the magic methods and properties its interfaces declared, so they did not complete, hover, or resolve, and calls to them were reported as unknown members. Tags on an interface (and on the interfaces it extends) are now picked up regardless of what the implementing class documents.
+- **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.
- **Find References, Rename, and Go to Implementation no longer look stalled during startup indexing.** A search started while the background index is still parsing the workspace waits for that index to finish, since acting on a partial index would silently miss results. That wait now shows in the request's own progress bar as "Waiting for workspace index" alongside the index's live file counts, instead of sitting at "Resolvingβ¦" with no indication of what it is waiting for.
- **Type narrowing against `@phpstan-assert`/`@psalm-assert` no longer leaks memory.** Evaluating a narrowing call such as `Assert::isInstanceOf($x, Foo::class)` or a custom function/method with the same annotations allocated a small amount of memory that was never freed. This ran on every conditional touched during completion, hover, diagnostics, and go-to-definition, so memory held by a long-running editor session grew slowly but permanently the more the project was edited. Fixed by no longer leaking the allocation.
- **Renaming a namespace no longer corrupts group `use` statements.** Renaming a namespace segment that is imported with a group `use` (e.g. `use App\Old\{Foo, Bar};`) previously rewrote the group's shared prefix and then also spliced the new prefix into each member name, producing invalid PHP like `use App\New\{App\New\Foo, Bar};`. The member names are left untouched now, since the prefix rewrite alone already updates the whole statement correctly.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index e8a070cab..1aaaccd56 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
@@ -36,7 +37,7 @@ Note that clippy runs twice, once for library code and once including test code.
- Use `create_test_backend()` from `tests/common/mod.rs` for same-file tests
- Use `create_psr4_workspace()` for cross-file / PSR-4 tests
- Test the happy path, edge cases, and interactions with existing features
-- When adding a feature, update `examples/php/demo.php` with working examples (and verify with `php -l examples/php/demo.php`). 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 `examples/php/demo.php` with working examples (and verify with `php -l examples/php/demo.php`). Put 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 3eac6b620..89e8ab501 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -52,6 +52,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 d83d15045..fdc1cdc99 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -22,6 +22,7 @@ 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.
+- **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.
diff --git a/docs/todo.md b/docs/todo.md
index 52b13c904..18cde2b44 100644
--- a/docs/todo.md
+++ b/docs/todo.md
@@ -143,7 +143,6 @@ unlikely to move the needle for most users.
| F12 | [IntelliJ / PHPStorm plugin](todo/lsp-features.md#f12-intellij-phpstorm-plugin) | High | Medium-High |
| F13 | [Homebrew formula](todo/lsp-features.md#f13-homebrew-formula) | Medium | Low |
| F17 | [Wire class move to `workspace/willRenameFiles`](todo/lsp-features.md#f17-wire-class-move-to-workspacewillrenamefiles) | Medium | Medium |
-| F5 | [Call hierarchy](todo/lsp-features.md#f5-call-hierarchy) (incoming/outgoing calls) | Medium | Medium |
| F2 | [Partial result streaming via `$/progress`](todo/lsp-features.md#f2-partial-result-streaming-via-progress) | Medium | Medium-High |
| F7 | [Evaluatable expression support (DAP integration)](todo/lsp-features.md#f7-evaluatable-expression-support-dap-integration) | Low-Medium | Low |
| F15 | [Go-to-declaration](todo/lsp-features.md#f15-go-to-declaration) | Low-Medium | Low |
diff --git a/docs/todo/lsp-features.md b/docs/todo/lsp-features.md
index 323f78aec..3244ca5cb 100644
--- a/docs/todo/lsp-features.md
+++ b/docs/todo/lsp-features.md
@@ -89,55 +89,6 @@ developer arrive before vendor matches, even within a single phase.
4. If no token was provided, fall back to the current behaviour: collect
everything, return once.
----
-
-## F5. Call hierarchy
-
-**Impact: Medium Β· Complexity: Medium**
-
-Implement `callHierarchy/incomingCalls` and
-`callHierarchy/outgoingCalls` to answer "who calls this function?" and
-"what does this function call?"
-
-### Incoming calls (who calls this)
-
-Given a function or method, find all call sites across the project.
-This is conceptually similar to Find References but filtered to call
-expressions and structured as a tree (each caller is itself a callable
-with a location).
-
-The existing Find References infrastructure
-(`find_references_in_file`, cross-file scanning) provides the core
-search. The call hierarchy handler wraps the results into
-`CallHierarchyIncomingCall` items, grouping by containing function.
-
-### Outgoing calls (what does this call)
-
-Given a function or method, walk its AST body and collect all call
-expressions (function calls, method calls, static calls, `new`
-expressions). Resolve each callee to its declaration location.
-
-This is a single-file AST walk with cross-file resolution for each
-callee, similar to what go-to-definition already does.
-
-### Prepare
-
-`callHierarchy/prepare` returns a `CallHierarchyItem` for the symbol
-at the cursor. This is straightforward: resolve the symbol, return its
-name, kind, URI, range, and selection range.
-
-### Dependencies
-
-Call hierarchy benefits significantly from a full project index.
-Without an index, incoming calls can only be found via the existing
-classmap + PSR-4 scan approach (same as Find References). Now that
-full background indexing is available, the lookup can become a
-simple index query instead of relying on the scan-based approach that
-Find References uses on its own.
-
-**References:**
-- Phpactor: call hierarchy via its references index.
-
## F7. Evaluatable expression support (DAP integration)
**Impact: Low-Medium Β· Complexity: Low**
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/call_hierarchy.rs b/src/call_hierarchy.rs
new file mode 100644
index 000000000..b26b5dd22
--- /dev/null
+++ b/src/call_hierarchy.rs
@@ -0,0 +1,409 @@
+//! PHP call hierarchy support built on the existing definition and reference
+//! pipelines.
+//!
+//! The hierarchy stores only stable declaration coordinates in LSP item data.
+//! Incoming calls reuse Find References; outgoing calls reuse Go to Definition
+//! for call-like symbol spans inside the callable body. This keeps call
+//! hierarchy aligned with every improvement made to the shared type engine.
+
+use std::collections::HashMap;
+
+use tower_lsp::lsp_types::{
+ CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, Location, Position,
+ Range, SymbolKind as LspSymbolKind, Url,
+};
+
+use crate::Backend;
+use crate::symbol_map::{SymbolKind, SymbolMap};
+use crate::text_position::{offset_to_position, position_to_offset};
+use crate::types::{ClassInfo, FunctionInfo, MethodInfo};
+
+#[derive(Clone)]
+struct PhpCallable {
+ item: CallHierarchyItem,
+ body: Option<(u32, u32)>,
+}
+
+impl Backend {
+ pub(crate) fn prepare_call_hierarchy_impl(
+ &self,
+ uri: &str,
+ content: &str,
+ position: Position,
+ ) -> Option> {
+ let offset = position_to_offset(content, position);
+ if let Some(callable) = self.php_callable_at(uri, content, offset) {
+ return Some(vec![callable.item]);
+ }
+
+ self.resolve_definition(uri, content, position)
+ .into_iter()
+ .find_map(|location| self.php_callable_at_location(&location))
+ .map(|callable| vec![callable.item])
+ }
+
+ pub(crate) fn incoming_calls_impl(
+ &self,
+ item: &CallHierarchyItem,
+ ) -> Option> {
+ let event_calls = self.symfony_event_incoming_calls(item);
+ let Some(target) = self.php_callable_from_item(item) else {
+ return event_calls;
+ };
+ let content = self.get_file_content(target.item.uri.as_str())?;
+ let references = self
+ .find_references(
+ target.item.uri.as_str(),
+ &content,
+ target.item.selection_range.start,
+ false,
+ )
+ .unwrap_or_default();
+
+ let mut grouped: HashMap)> = HashMap::new();
+ for reference in references {
+ let Some(caller) = self.php_callable_at_location(&reference) else {
+ continue;
+ };
+ let key = php_item_key(&caller.item);
+ grouped
+ .entry(key)
+ .and_modify(|(_, ranges)| push_unique_range(ranges, reference.range))
+ .or_insert_with(|| (caller.item, vec![reference.range]));
+ }
+
+ let mut calls: Vec<_> = grouped
+ .into_values()
+ .map(|(from, from_ranges)| CallHierarchyIncomingCall { from, from_ranges })
+ .collect();
+ calls.extend(event_calls.unwrap_or_default());
+ calls.sort_by_key(|left| php_item_key(&left.from));
+ calls.dedup_by(|left, right| {
+ left.from == right.from && left.from_ranges == right.from_ranges
+ });
+ Some(calls)
+ }
+
+ pub(crate) fn outgoing_calls_impl(
+ &self,
+ item: &CallHierarchyItem,
+ ) -> Option> {
+ let event_calls = self.symfony_event_outgoing_calls(item);
+ let Some(callable) = self.php_callable_from_item(item) else {
+ return event_calls;
+ };
+ let Some((body_start, body_end)) = callable.body else {
+ return event_calls.or_else(|| Some(Vec::new()));
+ };
+ let uri = callable.item.uri.as_str();
+ let content = self.get_file_content(uri)?;
+ let symbol_map = self.symbol_maps.read().get(uri).cloned()?;
+
+ let mut grouped: HashMap)> = HashMap::new();
+ for span in symbol_map.spans.iter().filter(|span| {
+ span.start >= body_start
+ && span.start <= body_end
+ && matches!(
+ span.kind,
+ SymbolKind::FunctionCall {
+ is_definition: false,
+ ..
+ } | SymbolKind::MemberAccess {
+ is_method_call: true,
+ ..
+ }
+ )
+ }) {
+ let position = offset_to_position(&content, span.start as usize);
+ let from_range = Range::new(position, offset_to_position(&content, span.end as usize));
+ for location in self.resolve_definition(uri, &content, position) {
+ let Some(callee) = self.php_callable_at_location(&location) else {
+ continue;
+ };
+ let key = php_item_key(&callee.item);
+ grouped
+ .entry(key)
+ .and_modify(|(_, ranges)| push_unique_range(ranges, from_range))
+ .or_insert_with(|| (callee.item, vec![from_range]));
+ }
+ }
+
+ let mut calls: Vec<_> = grouped
+ .into_values()
+ .map(|(to, from_ranges)| CallHierarchyOutgoingCall { to, from_ranges })
+ .collect();
+ calls.extend(event_calls.unwrap_or_default());
+ calls.sort_by_key(|left| php_item_key(&left.to));
+ calls.dedup_by(|left, right| left.to == right.to && left.from_ranges == right.from_ranges);
+ Some(calls)
+ }
+
+ fn php_callable_from_item(&self, item: &CallHierarchyItem) -> Option {
+ let data = item.data.as_ref()?;
+ if data.get("kind")?.as_str()? != "php" {
+ return None;
+ }
+ let offset = data.get("offset")?.as_u64()? as u32;
+ let content = self.get_file_content(item.uri.as_str())?;
+ self.php_callable_at(item.uri.as_str(), &content, offset)
+ }
+
+ fn php_callable_at_location(&self, location: &Location) -> Option {
+ let uri = location.uri.as_str();
+ let content = self.get_file_content(uri)?;
+ let offset = position_to_offset(&content, location.range.start);
+ self.php_callable_at(uri, &content, offset)
+ }
+
+ pub(crate) fn call_hierarchy_item_at_location(
+ &self,
+ location: &Location,
+ ) -> Option {
+ self.php_callable_at_location(location)
+ .map(|callable| callable.item)
+ }
+
+ fn php_callable_at(&self, uri: &str, content: &str, offset: u32) -> Option {
+ let symbol_map = self.symbol_maps.read().get(uri).cloned()?;
+ let classes = self
+ .symbols
+ .uri_classes_index
+ .read()
+ .get(uri)
+ .cloned()
+ .unwrap_or_default();
+
+ for class in &classes {
+ if let Some(callable) = method_callable_at(uri, content, &symbol_map, class, offset) {
+ return Some(callable);
+ }
+ }
+
+ let function_names = self
+ .symbols
+ .uri_globals_index
+ .read()
+ .get(uri)
+ .map(|(functions, _)| functions.clone())
+ .unwrap_or_default();
+ let functions = self.symbols.global_functions.read();
+ for fqn in function_names {
+ let Some((declaring_uri, function)) = functions.get(&fqn) else {
+ continue;
+ };
+ if declaring_uri == uri
+ && let Some(callable) =
+ function_callable_at(uri, content, &symbol_map, &fqn, function, offset)
+ {
+ return Some(callable);
+ }
+ }
+ None
+ }
+}
+
+fn method_callable_at(
+ uri: &str,
+ content: &str,
+ symbol_map: &SymbolMap,
+ class: &ClassInfo,
+ offset: u32,
+) -> Option {
+ for (index, method) in class.methods.iter().enumerate() {
+ if method.is_virtual || method.name_offset == 0 {
+ continue;
+ }
+ let upper = class
+ .methods
+ .iter()
+ .skip(index + 1)
+ .filter(|next| next.name_offset > method.name_offset)
+ .map(|next| next.name_offset)
+ .min()
+ .unwrap_or(class.end_offset);
+ let body = declaration_body(symbol_map, method.name_offset, upper);
+ let name_end = method.name_offset.saturating_add(method.name.len() as u32);
+ let contains = (method.name_offset..=name_end).contains(&offset)
+ || body.is_some_and(|(start, end)| start <= offset && offset <= end);
+ if contains {
+ return build_method_callable(uri, content, class, method, body);
+ }
+ }
+ None
+}
+
+fn function_callable_at(
+ uri: &str,
+ content: &str,
+ symbol_map: &SymbolMap,
+ fqn: &str,
+ function: &FunctionInfo,
+ offset: u32,
+) -> Option {
+ if function.name_offset == 0 {
+ return None;
+ }
+ let body = declaration_body(symbol_map, function.name_offset, content.len() as u32);
+ let name_end = function
+ .name_offset
+ .saturating_add(function.name.len() as u32);
+ if !(function.name_offset..=name_end).contains(&offset)
+ && !body.is_some_and(|(start, end)| start <= offset && offset <= end)
+ {
+ return None;
+ }
+ build_function_callable(uri, content, fqn, function, body)
+}
+
+fn declaration_body(symbol_map: &SymbolMap, name_offset: u32, upper: u32) -> Option<(u32, u32)> {
+ symbol_map
+ .scopes
+ .iter()
+ .copied()
+ .filter(|(start, _)| *start > name_offset && *start < upper)
+ .min_by_key(|(start, _)| *start)
+}
+
+fn build_method_callable(
+ uri: &str,
+ content: &str,
+ class: &ClassInfo,
+ method: &MethodInfo,
+ body: Option<(u32, u32)>,
+) -> Option {
+ let uri = Url::parse(uri).ok()?;
+ let selection_range = offset_range(content, method.name_offset, method.name.len() as u32);
+ let range = Range::new(
+ selection_range.start,
+ body.map_or(selection_range.end, |(_, end)| {
+ offset_to_position(content, end as usize)
+ }),
+ );
+ let class_fqn = class.fqn().to_string();
+ Some(PhpCallable {
+ item: CallHierarchyItem {
+ name: method.name.to_string(),
+ kind: LspSymbolKind::METHOD,
+ tags: None,
+ detail: Some(class_fqn.clone()),
+ uri,
+ range,
+ selection_range,
+ data: Some(serde_json::json!({
+ "kind": "php",
+ "owner": class_fqn,
+ "method": method.name.as_str(),
+ "offset": method.name_offset,
+ })),
+ },
+ body,
+ })
+}
+
+fn build_function_callable(
+ uri: &str,
+ content: &str,
+ fqn: &str,
+ function: &FunctionInfo,
+ body: Option<(u32, u32)>,
+) -> Option {
+ let uri = Url::parse(uri).ok()?;
+ let selection_range = offset_range(content, function.name_offset, function.name.len() as u32);
+ let range = Range::new(
+ selection_range.start,
+ body.map_or(selection_range.end, |(_, end)| {
+ offset_to_position(content, end as usize)
+ }),
+ );
+ Some(PhpCallable {
+ item: CallHierarchyItem {
+ name: function.name.to_string(),
+ kind: LspSymbolKind::FUNCTION,
+ tags: None,
+ detail: function.namespace.clone(),
+ uri,
+ range,
+ selection_range,
+ data: Some(serde_json::json!({
+ "kind": "php",
+ "function": fqn,
+ "offset": function.name_offset,
+ })),
+ },
+ body,
+ })
+}
+
+fn offset_range(content: &str, start: u32, len: u32) -> Range {
+ Range::new(
+ offset_to_position(content, start as usize),
+ offset_to_position(content, start.saturating_add(len) as usize),
+ )
+}
+
+fn php_item_key(item: &CallHierarchyItem) -> String {
+ format!(
+ "{}:{}:{}:{}",
+ item.uri, item.selection_range.start.line, item.selection_range.start.character, item.name
+ )
+}
+
+fn push_unique_range(ranges: &mut Vec, range: Range) {
+ if !ranges.contains(&range) {
+ ranges.push(range);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const URI: &str = "file:///call_hierarchy.php";
+
+ fn parse(content: &str) -> Backend {
+ let backend = Backend::new_test();
+ backend
+ .open_files
+ .write()
+ .insert(URI.to_string(), std::sync::Arc::new(content.to_string()));
+ backend.update_ast(URI, content);
+ backend
+ }
+
+ #[test]
+ fn prepares_methods_and_resolves_outgoing_calls() {
+ let content = r#"leaf(); }
+}
+"#;
+ let backend = parse(content);
+ let run = backend
+ .prepare_call_hierarchy_impl(URI, content, Position::new(3, 22))
+ .unwrap()
+ .remove(0);
+ let outgoing = backend.outgoing_calls_impl(&run).unwrap();
+ assert_eq!(outgoing.len(), 1);
+ assert_eq!(outgoing[0].to.name, "leaf");
+ assert_eq!(outgoing[0].from_ranges.len(), 1);
+ }
+
+ #[test]
+ fn resolves_incoming_calls_through_find_references() {
+ let content = r#"leaf(); }
+}
+"#;
+ let backend = parse(content);
+ let leaf = backend
+ .prepare_call_hierarchy_impl(URI, content, Position::new(2, 22))
+ .unwrap()
+ .remove(0);
+ let incoming = backend.incoming_calls_impl(&leaf).unwrap();
+ assert_eq!(incoming.len(), 1);
+ assert_eq!(incoming[0].from.name, "run");
+ }
+}
diff --git a/src/code_actions/mod.rs b/src/code_actions/mod.rs
index cf31e82ad..d41ddd661 100644
--- a/src/code_actions/mod.rs
+++ b/src/code_actions/mod.rs
@@ -110,6 +110,7 @@ mod replace_deprecated;
mod replace_fqcn;
mod simplify_null;
mod sort_use_statements;
+mod symfony_template;
mod update_docblock;
use std::collections::HashMap;
@@ -254,6 +255,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 9061c1d85..aa9a3f81b 100644
--- a/src/code_lens.rs
+++ b/src/code_lens.rs
@@ -1,14 +1,24 @@
//! Code Lens (`textDocument/codeLens`) support.
//!
-//! Shows override/implement annotations linking to the prototype declaration.
+//! Shows reference counts plus clickable inheritance, implementation, Symfony,
+//! and Doctrine relationship annotations.
+use std::collections::HashSet;
use tower_lsp::lsp_types::*;
use crate::Backend;
use crate::atom::Atom;
+use crate::class_lookup::find_class_at_offset;
use crate::definition::member::MemberKind;
+use crate::framework::{FrameworkReferenceKind, SymfonySymbolKind};
+use crate::reference_index::ReferenceIndexKey;
+use crate::references::{
+ doctrine_repository_matches_entity_convention, looks_like_doctrine_repository,
+};
+use crate::symbol_map::SymbolKind;
use crate::text_position::offset_to_position;
-use crate::types::{ClassInfo, ClassLikeKind, MAX_INHERITANCE_DEPTH};
+use crate::types::{ClassInfo, ClassLikeKind, MAX_INHERITANCE_DEPTH, MethodInfo, Visibility};
+use crate::util::short_name;
fn line_indent(content: &str, byte_offset: usize) -> u32 {
let line_start = content[..byte_offset]
@@ -36,23 +46,40 @@ struct Prototype {
impl Backend {
/// Handle a `textDocument/codeLens` request.
///
- /// Returns a code lens for each method in the file that overrides
- /// a parent class method or implements an interface method.
+ /// Returns reference, inheritance, implementation, and indexed framework
+ /// relationship lenses for PHP declarations.
pub fn handle_code_lens(&self, uri: &str, content: &str) -> Option> {
let classes = {
let map = self.symbols.uri_classes_index.read();
- map.get(uri)?.clone()
+ map.get(uri).cloned().unwrap_or_default()
};
- let mut lenses = Vec::new();
+ let mut lenses = self.symfony_event_lenses(&classes, uri, content);
+ let mut seen = HashSet::new();
+ let ctx = self.file_context(uri);
+ let class_loader = self.class_loader(&ctx);
for class in &classes {
let class_fqn = class.fqn();
+ if let Some(lens) = self.build_declaration_reference_lens(
+ uri,
+ content,
+ self.class_declaration_name_offset(uri, class),
+ &ReferenceIndexKey::class(&class_fqn),
+ ) {
+ lenses.push(lens);
+ }
if let Some(lens) = self.build_covers_lens(class, uri, content) {
lenses.push(lens);
}
-
+ self.push_framework_class_lenses(
+ uri,
+ content,
+ class,
+ &class_loader,
+ (&mut lenses, &mut seen),
+ );
for method in &class.methods {
if method.name_offset == 0
|| method.is_virtual
@@ -75,6 +102,19 @@ impl Backend {
};
let proto = self.find_prototype(class, &class_fqn, &method.name, uri, content);
+ if !method.name.starts_with("__")
+ && proto.is_none()
+ && let Some(lens) = self.build_member_reference_lens(
+ uri,
+ content,
+ method.name_offset,
+ class_fqn,
+ method.name,
+ method.is_static,
+ )
+ {
+ lenses.push(lens);
+ }
if let Some(proto) = proto {
let icon = if proto.is_interface { "β" } else { "β" };
let title = format!("{} {}::{}", icon, proto.ancestor_name, method.name);
@@ -86,15 +126,125 @@ 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,
+ content,
+ class,
+ method,
+ (&mut lenses, &mut seen),
+ );
+ }
+
+ let mut hierarchy = HashSet::new();
+ hierarchy.insert(class_fqn.to_string());
+ hierarchy.extend(self.class_hierarchy_names(class));
+ for property in &class.properties {
+ if property.name_offset == 0 {
+ continue;
+ }
+
+ 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,
+ content,
+ property.name_offset,
+ class_fqn,
+ crate::atom::atom(member_name),
+ property.is_static,
+ ) {
+ lenses.push(lens);
+ }
+ }
+
+ let locations =
+ self.framework_property_reference_locations(&property.name, Some(&hierarchy));
+ if !locations.is_empty() {
+ self.push_locations_lens(
+ uri,
+ offset_to_position(content, property.name_offset as usize),
+ format!(
+ "Symfony form/validation: {} {}",
+ locations.len(),
+ if locations.len() == 1 { "ref" } else { "refs" }
+ ),
+ locations,
+ &mut lenses,
+ &mut seen,
+ );
+ }
+ }
+
+ for constant in &class.constants {
+ if constant.name_offset == 0 || constant.visibility == Visibility::Private {
+ continue;
+ }
+ if let Some(lens) = self.build_member_reference_lens(
+ uri,
+ content,
+ constant.name_offset,
+ class_fqn,
+ constant.name,
+ true,
+ ) {
+ lenses.push(lens);
+ }
+ }
+ }
+
+ if let Some(symbol_map) = self.symbol_maps.read().get(uri).cloned() {
+ for span in &symbol_map.spans {
+ let key = match &span.kind {
+ SymbolKind::FunctionCall {
+ name,
+ is_definition: true,
+ ..
+ } => self.function_reference_key(uri, span.start, name),
+ SymbolKind::ConstantReference {
+ name,
+ is_definition: true,
+ } => ReferenceIndexKey::Constant(self.constant_fqn_at(uri, span.start, name)),
+ _ => continue,
+ };
+ if let Some(lens) =
+ self.build_declaration_reference_lens(uri, content, span.start, &key)
+ {
+ lenses.push(lens);
}
}
}
+ self.push_symfony_route_attribute_lenses(uri, content, &classes, &mut lenses, &mut seen);
+ self.push_doctrine_get_repository_lenses(
+ uri,
+ content,
+ &ctx,
+ &class_loader,
+ &mut lenses,
+ &mut seen,
+ );
+ self.push_symfony_resource_lenses(uri, content, &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 {
@@ -102,6 +252,258 @@ impl Backend {
}
}
+ /// Build a declaration reference lens from the candidate index.
+ ///
+ /// A zero count is returned fully resolved because semantic filtering can
+ /// only remove candidates. Non-zero declarations take the LSP's lazy
+ /// resolve path, which computes exact locations only when the client asks.
+ fn build_declaration_reference_lens(
+ &self,
+ origin_uri: &str,
+ content: &str,
+ declaration_offset: u32,
+ key: &ReferenceIndexKey,
+ ) -> Option {
+ if declaration_offset == 0 {
+ return None;
+ }
+
+ let candidate_count = self.indexed_reference_count(key)?;
+ let origin_url = Url::parse(origin_uri).ok()?;
+ let position = offset_to_position(content, declaration_offset as usize);
+ let range = Range::new(
+ Position::new(position.line, 0),
+ Position::new(position.line, 0),
+ );
+ if candidate_count == 0 {
+ return Some(CodeLens {
+ range,
+ command: Some(Self::reference_lens_command(
+ origin_url,
+ position,
+ Vec::new(),
+ )),
+ data: None,
+ });
+ }
+
+ Some(CodeLens {
+ range,
+ command: None,
+ data: Some(serde_json::json!({
+ "kind": "phpReferences",
+ "uri": origin_uri,
+ "position": position,
+ })),
+ })
+ }
+
+ fn build_member_reference_lens(
+ &self,
+ origin_uri: &str,
+ content: &str,
+ declaration_offset: u32,
+ class_fqn: Atom,
+ member: Atom,
+ is_static: bool,
+ ) -> Option {
+ if declaration_offset == 0 {
+ return None;
+ }
+ let member_name = member.to_string();
+ let candidate_count = self.indexed_reference_count_for_keys(&[
+ ReferenceIndexKey::Member {
+ name: member_name.clone(),
+ is_static,
+ },
+ ReferenceIndexKey::Member {
+ name: member_name,
+ is_static: !is_static,
+ },
+ ])?;
+ let origin_url = Url::parse(origin_uri).ok()?;
+ let position = offset_to_position(content, declaration_offset as usize);
+ let range = Range::new(
+ Position::new(position.line, 0),
+ Position::new(position.line, 0),
+ );
+ if candidate_count == 0 {
+ return Some(CodeLens {
+ range,
+ command: Some(Self::reference_lens_command(
+ origin_url,
+ position,
+ Vec::new(),
+ )),
+ data: None,
+ });
+ }
+
+ let supports_refresh = self
+ .supports_code_lens_refresh
+ .load(std::sync::atomic::Ordering::Acquire);
+ let cached_locations = if supports_refresh {
+ self.member_ref_locations_cached(
+ origin_uri,
+ declaration_offset,
+ class_fqn,
+ member,
+ is_static,
+ )
+ } else {
+ self.member_ref_locations_ready(
+ origin_uri,
+ declaration_offset,
+ class_fqn,
+ member,
+ is_static,
+ )
+ };
+ if let Some(locations) = cached_locations {
+ return Some(CodeLens {
+ range,
+ command: Some(Self::reference_lens_command(
+ origin_url, position, locations,
+ )),
+ data: None,
+ });
+ }
+
+ // Clients with refresh support can re-pull once the shared background
+ // worker fills the exact cache. Omitting the cold lens avoids an
+ // eager resolve burst merely to obtain titles for the viewport.
+ if supports_refresh {
+ return None;
+ }
+
+ Some(CodeLens {
+ range,
+ command: None,
+ data: Some(serde_json::json!({
+ "kind": "phpMemberReferences",
+ "uri": origin_uri,
+ "position": position,
+ "offset": declaration_offset,
+ "classFqn": class_fqn.as_str(),
+ "member": member.as_str(),
+ "isStatic": is_static,
+ })),
+ })
+ }
+
+ fn class_declaration_name_offset(&self, uri: &str, class: &ClassInfo) -> u32 {
+ let maps = self.symbol_maps.read();
+ let Some(map) = maps.get(uri) else {
+ return class.keyword_offset;
+ };
+ map.spans
+ .iter()
+ .find(|span| {
+ matches!(
+ &span.kind,
+ SymbolKind::ClassDeclaration { name } if *name == class.name
+ ) && span.start >= class.decl_start_offset
+ && span.start <= class.start_offset
+ })
+ .map(|span| span.start)
+ .unwrap_or(class.keyword_offset)
+ }
+
+ fn reference_lens_command(
+ origin_uri: Url,
+ origin_position: Position,
+ locations: Vec,
+ ) -> Command {
+ let count = locations.len();
+ Command {
+ title: format!(
+ "{count} {}",
+ if count == 1 {
+ "reference"
+ } else {
+ "references"
+ }
+ ),
+ command: "editor.action.showReferences".to_string(),
+ arguments: Some(vec![
+ serde_json::json!(origin_uri),
+ serde_json::json!(origin_position),
+ serde_json::json!(locations),
+ ]),
+ }
+ }
+
+ pub(crate) fn resolve_code_lens_item(&self, mut lens: CodeLens) -> CodeLens {
+ if lens.command.is_some() {
+ return lens;
+ }
+ let Some(data) = lens.data.as_ref() else {
+ return lens;
+ };
+ let Some(kind) = data.get("kind").and_then(serde_json::Value::as_str) else {
+ return lens;
+ };
+ let Some(uri) = data.get("uri").and_then(serde_json::Value::as_str) else {
+ return lens;
+ };
+ let Some(position) = data
+ .get("position")
+ .cloned()
+ .and_then(|value| serde_json::from_value::(value).ok())
+ else {
+ return lens;
+ };
+ let locations = match kind {
+ "phpReferences" => {
+ let Some(content) = self.get_file_content(uri) else {
+ return lens;
+ };
+ let Some(locations) =
+ self.find_references_from_workspace_index(uri, &content, position, false)
+ else {
+ return lens;
+ };
+ locations
+ }
+ "phpMemberReferences" => {
+ let Some(offset) = data
+ .get("offset")
+ .and_then(serde_json::Value::as_u64)
+ .and_then(|offset| u32::try_from(offset).ok())
+ else {
+ return lens;
+ };
+ let Some(class_fqn) = data.get("classFqn").and_then(serde_json::Value::as_str)
+ else {
+ return lens;
+ };
+ let Some(member) = data.get("member").and_then(serde_json::Value::as_str) else {
+ return lens;
+ };
+ let Some(is_static) = data.get("isStatic").and_then(serde_json::Value::as_bool)
+ else {
+ return lens;
+ };
+ self.resolve_member_ref_locations(
+ uri,
+ offset,
+ crate::atom::atom(class_fqn),
+ crate::atom::atom(member),
+ is_static,
+ )
+ }
+ _ => return lens,
+ };
+ let Ok(origin_uri) = Url::parse(uri) else {
+ return lens;
+ };
+
+ lens.command = Some(Self::reference_lens_command(
+ origin_uri, position, locations,
+ ));
+ lens
+ }
+
/// Build the "which tests cover this class" lens for a class
/// declaration, from the test classes whose PHPUnit coverage metadata
/// (`@covers` / `@uses` / `#[CoversClass]` and friends) names it.
@@ -202,6 +604,530 @@ impl Backend {
})
}
+ fn push_symfony_resource_lenses(
+ &self,
+ uri: &str,
+ content: &str,
+ 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,
+ offset_to_position(content, 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,
+ offset_to_position(content, 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 = offset_to_position(content, 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,
+ 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(content, 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,
+ content: &str,
+ class: &ClassInfo,
+ method: &MethodInfo,
+ output: (&mut Vec, &mut HashSet),
+ ) {
+ let (lenses, seen) = output;
+ let pos = offset_to_position(content, method.name_offset as usize);
+ let mut hierarchy = HashSet::new();
+ hierarchy.insert(class.fqn().to_string());
+ for fqn in self.class_hierarchy_names(class) {
+ hierarchy.insert(fqn);
+ }
+
+ let route_locations =
+ self.framework_member_reference_locations(&method.name, Some(&hierarchy));
+ if route_locations.is_empty() {
+ return;
+ }
+
+ 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_symfony_route_attribute_lenses(
+ &self,
+ uri: &str,
+ content: &str,
+ classes: &[std::sync::Arc],
+ lenses: &mut Vec,
+ seen: &mut HashSet,
+ ) {
+ let declarations = code_lens_declarations(content, classes);
+ if declarations.is_empty() {
+ return;
+ }
+
+ for attr in route_attributes(content) {
+ let Some(decl) = declarations
+ .iter()
+ .filter(|decl| decl.offset > attr.end)
+ .min_by_key(|decl| decl.offset)
+ 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,
+ ctx: &crate::types::FileContext,
+ class_loader: &dyn Fn(&str) -> Option>,
+ lenses: &mut Vec,
+ seen: &mut HashSet,
+ ) {
+ for call in get_repository_calls(content) {
+ let Some(entity_fqn) = class_expr_arg_to_fqn(
+ &call.first_arg,
+ &ctx.use_map,
+ &ctx.namespace,
+ &ctx.classes,
+ call.offset as u32,
+ ) 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 = offset_to_position(content, 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::new();
+ let mut current = class.clone();
+ for _ in 0..MAX_INHERITANCE_DEPTH {
+ let Some(parent_name) = current.parent_class else {
+ break;
+ };
+ let parent_fqn = parent_name.to_string();
+ if !out
+ .iter()
+ .any(|known: &String| known.eq_ignore_ascii_case(&parent_fqn))
+ {
+ out.push(parent_fqn.clone());
+ }
+ let Some(parent) = self.find_or_load_class(&parent_name) else {
+ break;
+ };
+ current = ClassInfo::clone(&parent);
+ }
+ out
+ }
+
/// Search the inheritance hierarchy for the closest ancestor that
/// declares a method with the given name.
///
@@ -457,6 +1383,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(
@@ -492,3 +1436,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(content: &str, class: &ClassInfo) -> Option {
+ let offset = if class.keyword_offset > 0 {
+ class.keyword_offset
+ } else {
+ class.decl_start_offset
+ };
+ if offset == 0 || offset as usize > content.len() {
+ None
+ } else {
+ Some(offset_to_position(content, offset as usize))
+ }
+}
+
+fn code_lens_declarations(
+ content: &str,
+ classes: &[std::sync::Arc],
+) -> Vec {
+ let mut declarations = Vec::new();
+ for class in classes {
+ if let Some(position) = class_lens_position(content, 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: offset_to_position(content, 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: &[std::sync::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 853fb6489..51ce42879 100644
--- a/src/completion/handler/mod.rs
+++ b/src/completion/handler/mod.rs
@@ -197,6 +197,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