Skip to content
Search
Back to the Work-Shop
Symfony Backoffice Blueprint screenshot 1
Blueprint

Symfony Backoffice Blueprint

A working Symfony back office built on Flow PHP. Read it, run it, take the patterns to your own project.

This is a real Symfony 7.4 application, not a tutorial repository. It manages products, categories, customers, orders, reviews, warehouses and stock for an imaginary shop, and it uses that boring domain as an excuse to show how the hard parts fit together.

Three things it focuses on.

One schema, four jobs

Every model has a schema() method that returns a Flow Type. Nothing else describes the shape of that model. Decorated with a bit of metadata, that one definition is what generates:

  • The PostgreSQL table, with its column types, lengths, defaults, indexes and foreign keys
  • The OpenAPI 3.1 specification you can browse at /api
  • The expected CSV layout on the import page, including generated sample rows
  • Runtime validation of every row on its way out of the database

Add a column in one place and the migration, the API contract, the import format and the validation all follow. There is no second definition to keep in sync.

Data processing that streams

Imports run through a Flow DataFrame. You upload a CSV, it lands in S3 compatible storage, a Messenger job picks it up and runs the pipeline inside a transaction with batched upserts. Row counts and errors are written back to an imports table you can watch from the UI.

Exports and the API stream properly. /export/products.csv, .json and .xml go straight from a server side PostgreSQL cursor to the response without building an array in memory, and so does /api/{resource}/stream. The paginated endpoints use keyset pagination with opaque cursors, not OFFSET.

PostgreSQL, typed properly

There is no ORM, and there is not a single SQL string in src. Every query is built programmatically with the flow-php/postgresql query builder, from a plain count(*) up to the keyset pagination that compares a row constructor against a bound cursor. Columns, joins, predicates and ordering are typed expressions, so renaming a column is a static analysis error instead of a runtime one, and there is nowhere for a concatenated string to sneak in.

Results come back through the model schema, so what you get out of fetchAllInto() is a typed object and not an array of mixed. Static analysis can follow your data from the query all the way to the template.

Migrations are the one place that holds plain SQL, and deliberately so. A migration is something you have to be able to open and change by hand, whether that is a backfill, an index built concurrently or a one-off data fix, so it stays as system queries you control. flow:migrations:diff writes the first draft for you by comparing the database to the catalog, and from there the file is yours.

One PostgreSQL instance runs everything. Data, cache pools, sessions, the Messenger transport and migrations. No Redis, no RabbitMQ.

Static analysis is Mago on PHP 8.5. There are seven suppressions in the entire src tree.

Telemetry from the browser to the query

Most PHP applications stop at Monolog. This one implements OpenTelemetry end to end. Spans are emitted for the HTTP kernel, the controller and its argument resolvers, every SQL query, every Twig template, cache operations, the HTTP client, Messenger consumers and console commands.

The browser is instrumented too. The server renders its trace context into a meta tag, so the documentLoad span from JavaScript ends up in the same trace as the SQL query that rendered the page. One waterfall, front to back.

None of which is much use while you are still writing the feature, so it is wired into the Symfony Profiler as well. Every request gets a Flow Telemetry panel with the span count, the total trace time, a timeline of that request, and the full list of spans with their kind, scope, duration and attributes. The metrics and logs the request emitted are on the same page. You inspect signals in the toolbar while you work, and the same signals go to the collector when you are done.

If you have ever tried to wire OTEL into Symfony by hand, the configuration is the part to read first. Named loggers, meters and tracers with their own scope attributes, a log processing pipeline with severity and attribute filtering, batching, and a failover exporter that writes to disk when the collector is unreachable. It all ships to an OTEL Collector and lands in OpenObserve, which comes up with the rest of the stack.

What's in the repository

  • A complete Symfony 7.4 application, over 200 classes, organised in vertical slices with separated read and write models
  • 364 test methods split into unit and integration suites, plus mutation testing configured at a 97% minimum covered MSI
  • Seventeen Flow PHP packages wired up and actually used, not just installed
  • Docker and nix development environments sharing the same PHP and web server configuration
  • Generated fixtures loaded through ETL pipelines, so you get a populated database with one command
  • compose.yaml with PostgreSQL, the OTEL Collector, OpenObserve and S3 compatible storage
  • Updates as the blueprint evolves, at no extra cost

What it is not

It is a blueprint, not a product. There is no authentication or authorization layer, the HTML is deliberately plain because the front end is not what this teaches, and you should not deploy it as-is. It exists so you can read it, run it, pull it apart, and copy what works into your own codebase.

Contributors

Built in the open.

Join us on GitHub
scroll back to top