{
  "generator": {
    "name": "Symvanta",
    "url": "https://symvanta.com"
  },
  "page": {
    "url": "https://symvanta.com/architecture/symfony",
    "title": "Symfony Architecture: How It Actually Works",
    "description": "symfony/symfony across 500 Louvain clusters with zero dependency cycles: the Notifier and Translation transports lead, mapped by Symvanta.",
    "datePublished": "2026-07-09",
    "dateModified": "2026-08-22"
  },
  "repository": {
    "name": "symfony/symfony",
    "url": "https://github.com/symfony/symfony",
    "commit": "66f06e5",
    "license": "MIT"
  },
  "graph": {
    "modules": 500,
    "dependencyCycles": 0,
    "modularityQ": 0.95,
    "largestCycleFiles": null,
    "mutuallyRecursiveGroups": 144
  },
  "summaryMarkdown": "Symfony is a set of decoupled, independently versioned PHP components (DependencyInjection, HttpKernel, Validator, Form, Console, Serializer, and dozens more) that also compose into a full-stack web framework: the same components ship standalone for any PHP project to pull in one at a time. [Symvanta](https://symvanta.com) indexed the symfony/symfony monorepo and grouped its symbols into Louvain-detected clusters at modularity Q=0.95, the clean separation you would expect from a design rule that lets one component depend only on another component's public interface. The map lists 500 of those clusters and flags a display cap at that number, so a monorepo this size carries more of them than the map prints. The largest, 1,868 symbols anchored on `MockHttpClient`, gathers the Notifier component's 81 transport bridges and the Translation component's four remote providers: each talks to a vendor API through an injected HttpClient, and the same mock client stands in for that HttpClient throughout the repo. Dependency injection follows at 1,768 symbols around `ContainerBuilder`, then the Validator's constraint classes, VarExporter's lazy-object and Redis proxies, and the HttpFoundation request layer that HttpKernel's events and the Security component's tokens both sit on.",
  "subsystems": [
    {
      "name": "Notifier and Translation Transports",
      "descriptionMarkdown": "The Notifier component's 81 transport bridges under [`src/Symfony/Component/Notifier/Bridge/`](https://github.com/symfony/symfony/tree/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Notifier/Bridge) (Slack, Twilio, Telegram, and 78 more), together with the Translation component: message catalogues, file loaders such as `XliffFileLoader`, and the four remote providers Crowdin, Loco, Lokalise, and Phrase. Every class here reaches a vendor API through an injected HttpClient, and `MockHttpClient` is the stand-in each one runs against inside the repo, which is why the cluster gathers on it."
    },
    {
      "name": "Container Definition Management",
      "descriptionMarkdown": "Symfony's dependency injection container: the code that turns service definitions (from YAML, XML, PHP config, or `#[Autoconfigure]` attributes) into a compiled autowired service graph. `ContainerBuilder` is the hub every bundle's `Extension::load()` calls to register a `Definition` or `ChildDefinition`, compiler passes such as `CheckTypeDeclarationsPass` then walk the same object, and `PhpDumper` writes the result out as a compiled PHP class. At 1,768 symbols it is the second largest cluster on the map, and its two heaviest outbound edges (7 calls each) run into the routing cluster and into a smaller FrameworkBundle container-building cluster not drawn above."
    },
    {
      "name": "Validation and Constraints",
      "descriptionMarkdown": "The Validator component: `Constraint` subclasses (`Length`, `Range`, `NotBlank`, `Valid`, and the rest of the 150 files under [`src/Symfony/Component/Validator/Constraints/`](https://github.com/symfony/symfony/tree/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Validator/Constraints)) declare a rule, and a matching class implementing `ConstraintValidatorInterface` runs it against a value. The cluster's hub is the bare `validate()` name, because every one of those validators declares a method with exactly that name."
    },
    {
      "name": "Lazy Object and Redis Proxies",
      "descriptionMarkdown": "VarExporter's lazy-object machinery (`LazyGhostTrait`, `LazyProxyTrait`, and the `initializeLazyObject()` contract every generated proxy implements) plus the Cache component's Redis client proxies under `src/Symfony/Component/Cache/Traits/`. The Redis proxies are what make this the fourth largest cluster: [`Redis6Proxy`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Cache/Traits/Redis6Proxy.php#L25) alone runs from line 25 to line 1266, re-declaring the phpredis surface (`get`, `set`, `eval`, `pipeline`, `getex`, `waitaof`, and hundreds of siblings) method by method, and every one of those methods calls `initializeLazyObject()` before it forwards."
    },
    {
      "name": "Request and Security Context",
      "descriptionMarkdown": "HttpFoundation's request objects and session handlers, the HttpKernel events that carry them (`RequestEvent`, `ResponseEvent`), and the Security component's tokens and users (`UsernamePasswordToken`, `InMemoryUser`). `RequestStack` anchors the cluster because it is where every listener, session handler, and token resolver reads the current request from. Its heaviest outbound edge is 27 calls into the serialization cluster, which is also where `CsrfToken` and the controller-argument metadata classes sit."
    },
    {
      "name": "Serialization and Property Metadata",
      "descriptionMarkdown": "The Serializer component's normalizers and encoders (`ObjectNormalizer`, `JsonEncoder`, `ClassMetadataFactory`, `AttributeMetadata`) and the PropertyInfo extractors they lean on, `ReflectionExtractor` above all. This is the layer that turns an object into an array and back, reading attributes and reflection to decide which properties travel and under what names."
    },
    {
      "name": "Also on the map",
      "descriptionMarkdown": "Four more of the ten drawn modules carry the framework's plumbing. Console Command Execution (hub `CommandTester`) holds the Console component's `Command` class, the `Dotenv` loading around it, and the in-process harnesses `CommandTester`, `ApplicationTester`, and `CommandCompletionTester`, all three shipped as public API. Form Building and Rendering (hub `TransformationFailedException`) holds the Form component's `FormError`, the `DataMapper` that moves values between a form and its underlying object, the `getBuilder()` entry points, and the `renderRow()` and `renderHelp()` calls that drive Twig form themes. Route Collection and Matching (hub `RouteCollection`) adds `RequestContext`, which carries the scheme, host, and base URL a match runs against, and `UrlGenerator`, which walks the same collection in reverse. Event Dispatching (hub `EventDispatcher`) holds the dispatcher itself plus the `FormBuilder`, `FormConfigBuilder`, `Stopwatch`, and `ConstraintViolation` objects listeners pass around."
    }
  ],
  "startReading": {
    "symbols": [
      {
        "name": "MockHttpClient",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/HttpClient/MockHttpClient.php#L27"
      },
      {
        "name": "ContainerBuilder",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/DependencyInjection/ContainerBuilder.php#L59"
      },
      {
        "name": "ConstraintValidatorInterface",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Validator/ConstraintValidatorInterface.php#L19"
      },
      {
        "name": "initializeLazyObject()",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/VarExporter/LazyObjectInterface.php#L26"
      },
      {
        "name": "RequestStack",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/HttpFoundation/RequestStack.php#L22"
      },
      {
        "name": "ObjectNormalizer",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php#L32"
      },
      {
        "name": "CommandTester",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Console/Tester/CommandTester.php#L23"
      },
      {
        "name": "TransformationFailedException",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Form/Exception/TransformationFailedException.php#L19"
      },
      {
        "name": "RouteCollection",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Routing/RouteCollection.php#L30"
      },
      {
        "name": "EventDispatcher",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/EventDispatcher/EventDispatcher.php#L32"
      },
      {
        "name": "FormExtension",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Bridge/Twig/Extension/FormExtension.php#L35"
      },
      {
        "name": "FrameworkExtension",
        "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L207"
      }
    ],
    "endpoints": []
  },
  "requestFlow": [
    {
      "position": 1,
      "symbol": "handle()",
      "sourceUrl": "https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/HttpKernel/HttpKernel.php",
      "markdown": "`handle()` ([`src/Symfony/Component/HttpKernel/HttpKernel.php`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/HttpKernel/HttpKernel.php)) is the entry point every Symfony front controller calls with the incoming `Request`. It pushes that request onto the `RequestStack` and delegates to `handleRaw()`, catching any `Throwable` and routing it to `handleThrowable()` before it can propagate (a caller passing `catch: false` gets the exception instead)."
    },
    {
      "position": 2,
      "symbol": "handleRaw()",
      "sourceUrl": null,
      "markdown": "`handleRaw()` does the actual work: it dispatches a `RequestEvent` (`KernelEvents::REQUEST`), giving listeners like the router a chance to resolve a controller and short-circuit with a `Response` early (redirects, cached responses). If none did, it resolves the controller and its arguments, dispatching `ControllerEvent` and `ControllerArgumentsEvent` so listeners can swap the controller or its arguments before it runs."
    },
    {
      "position": 3,
      "symbol": "ControllerEvent",
      "sourceUrl": null,
      "markdown": "`ControllerEvent`'s `getControllerReflector()` is what `handleRaw()` hands to `argumentResolver->getArguments()`: the reflection of the resolved controller, whose parameter list drives how each argument is resolved before the controller is invoked."
    },
    {
      "position": 4,
      "symbol": null,
      "sourceUrl": null,
      "markdown": "The controller runs and returns a value. If that value is anything other than a `Response`, `handleRaw()` dispatches a `ViewEvent` (`KernelEvents::VIEW`) so a listener can turn it into one; if nothing does, `handleRaw()` throws `ControllerDoesNotReturnResponseException`."
    },
    {
      "position": 5,
      "symbol": null,
      "sourceUrl": null,
      "markdown": "Whichever path produced the `Response`, `handleRaw()` returns it through `filterResponse()`, which dispatches `KernelEvents::RESPONSE` so listeners can modify headers, add cookies, or wrap the response, then calls `finishRequest()` to dispatch `KernelEvents::FINISH_REQUEST` for cleanup. Popping the request back off the `RequestStack` is separate: that happens in `handle()`'s `finally` block, so the previous request context is restored whether the request returned a response or threw."
    },
    {
      "position": 6,
      "symbol": null,
      "sourceUrl": null,
      "markdown": "If `handleRaw()` threw instead, `handleThrowable()` catches it and dispatches `KernelEvents::EXCEPTION` so an exception listener can substitute a proper error `Response`. With one supplied, the same `filterResponse()` and `finishRequest()` steps as the success path run, so an error response goes through identical header and cleanup handling; with no listener supplying one, `handleThrowable()` calls `finishRequest()` and rethrows."
    }
  ],
  "diagrams": [
    {
      "url": "https://symvanta.com/architecture/symfony/module-map.svg",
      "encodingFormat": "image/svg+xml"
    }
  ]
}
