Takes over a real legacy database and produces Doctrine entities from it — then does it again six months later without discarding the work done since.
Warning
Ormeau connects to the databases whose credentials you give it and writes files describing their schema. It only ever reads the catalogue, in a read-only session enforced by the server, but the target database and the account used remain your choice. A layer describes a customer's database schema: it is not to be versioned or passed around lightly. Preview release, provided without warranty under the Apache 2.0 licence — see Status for what works.
The point is not to generate entities from a clean database — a script does
that, and Doctrine itself did before dropping doctrine:mapping:import. The
point is to take over a real legacy database: T_ prefixes, foreign keys never
declared, tables without a primary key, booleans stored as char(1), generated
columns. And to do it twice, six months apart, without overwriting the work done
on the entities in between.
This tool comes from field experience: taking over an existing database to
derive Doctrine entities from it is a recurring task on client work, and nothing
has covered it since doctrine:mapping:import was removed.
Doctrine removed its reverse engineering: doctrine:mapping:import is gone from
the bundle and DatabaseDriver left with ORM 3. Nothing official remains, and
the ecosystem alternatives are either abandoned or stop at a literal
transposition: they return $clientId as an integer where an association to
Client is needed.
Download the archive for your platform from the latest release, unpack it, run it. Nothing else to install: no runtime, no system driver.
$ export ORMEAU_DSN="postgres://app:secret@srv:5432/gescom"
$ ormeau extraire --sortie gescom.calque.json
gescom.calque.json : 10 table(s), 32 colonne(s), 0 anomalie(s)
empreinte sha256:f422f6d3e5eb455a91b096bd513bd5d8e595bd4e88aa588ef25d241993e201a1The string goes through ORMEAU_DSN rather than --dsn, which ps would show.
A connection can also be given as components, which avoids escaping a password
inside a URL. The password has no flag: it would be visible in ps and in the
shell history.
$ export ORMEAU_MDP=secret
$ ormeau extraire --sgbd postgres --hote srv --utilisateur app --base gescom --sortie gescom.calque.jsonWithout --base, every database on the server is extracted and --sortie
designates a directory:
$ ormeau extraire --sgbd postgres --hote srv --utilisateur app --sortie calques/
calques/gescom.calque.json : 10 table(s), 32 colonne(s), 0 anomalie(s)
calques/facturation.calque.json : 24 table(s), 187 colonne(s), 0 anomalie(s)Verifying a downloaded archive — the binaries being neither signed nor notarized, SmartScreen and Gatekeeper will complain on first launch:
$ gh attestation verify ormeau_v0.7.2_linux_amd64.tar.gz --repo sprimault/ormeauThrough a container, giving it the caller's identity: the image runs as an unprivileged user and would not write to a Linux volume otherwise.
$ docker run --rm --user "$(id -u):$(id -g)" \
-e ORMEAU_DSN -v "$PWD:/sortie" \
ghcr.io/sprimault/ormeau:v0.7.2 extraire --sortie /sortie/gescom.calque.jsonWith the layer in hand, inference runs offline — the database is no longer needed:
$ ormeau inferer gescom.calque.json
gescom.decisions.yaml écrit, entièrement en commentaire : rien n'est appliqué tant que
vous n'avez pas décommenté. Il porte les renommages que l'outil propose.
gescom.logique.json : 12 entité(s), 31 association(s), 2 énumération(s), 1 trait(s)
2 avertissement(s) :
prefixe_detecte public préfixe T_ commun aux 12 tables, conservé ; prefixes_a_retirer le retirerait des noms de classes
table_sans_cle_primaire public.t_log_import aucune clé primaire : Doctrine refusera cette entité en l'état
Par code :
prefixe_detecte 1
table_sans_cle_primaire 1You uncomment what suits you in gescom.decisions.yaml, run it again, and your
rulings replay on every pass: the command line never rewrites an existing file.
The interface does regenerate it when you save, and asks first if it was edited
by hand.
Between releases, build from a clone: see
Getting set up. go install does not work, as
the embedded interface is not versioned.
The Symfony bundle is its own Composer package on Packagist,
sprimault/ormeau-doctrine, published from a read-only mirror of php/:
$ composer require --dev sprimault/ormeau-doctrineIt requires PHP 8.1, Symfony 5.4 to 8 and Doctrine ORM 2.14 to 3. Symfony Flex
registers the bundle for dev and test; without Flex, add
Ormeau\Doctrine\OrmeauDoctrineBundle::class => ['dev' => true, 'test' => true]
to config/bundles.php.
$ bin/console ormeau:generer gescom.logique.json
Cible détectée : PHP 8.4, Doctrine ORM 3.7, DBAL 4.4
Base : gescom
créé src/Entity/Enum/StatutClient.php
créé src/Entity/Base/ClientBase.php
créé src/Entity/Client.phpBase/, Enum/ and Trait/ belong to the tool and are rewritten on every run.
Client.php is created once and never touched again: your business methods go
there. It can be moved into a subdirectory, src/Entity/Ventes/Client.php with
its namespace: the next run finds it by its base class, and the other classes
refer to it where it now lives. When it no longer matches the layer — a renamed
table, a newly declared subclass —, the command names the file, the line and
the expected attribute.
--repertoire writes somewhere other than src/Entity, and --cible-orm=2
or 3 targets an ORM version other than the installed one. The PHP type of
some columns also depends on DBAL, which ORM 3 accepts in version 3 as in
version 4: a forced ORM version assumes one, which the announcement calls
deduced, and --cible-dbal=3.10 corrects it.
Each file names in its header the database it comes from, read from the layer's
file name: gescom for gescom.logique.json. Another database generated into
the same directory does not rewrite those files; the command names the file and
both databases. Give each database its own directory and namespace, or pass
--remplacer=gescom to explicitly accept overwriting gescom's files — a
renamed layer, a directory taken over. Only what prevents the requested
generation returns 1: a refused overwrite, or a PHP file under the entities
directory that cannot be read, before anything is written. A skipped entity and
a divergence return 0: they say what to rework.
This two-class shape is the price of regenerating without overwriting, and it
changes a few habits. Properties are declared in Base/, which is rewritten:
validation constraints and serialization groups do not go on them. They go in
Client.php, on a redeclared getter — #[Assert\NotBlank] public function getRaisonSociale(): string { return parent::getRaisonSociale(); } —, or in the
mapping files of config/validator/ and config/serializer/, which name the
inherited property. make:entity does not suit these classes: it would add
fields to Client.php, outside what the layer describes. A column is added in
the database, and the entities are regenerated.
Table and column comments become docblocks. The sentences the tool writes are in French, like all of its output, while a comment taken from the database keeps the database's language: a docblock may mix both, and that is expected.
ormeau interface opens your browser on a connection screen, where you type a
host and a login rather than assemble a connection string. The DBMS follows from
the port, and the server then says what it actually is.
$ cd ~/projects/gescom && ormeau interface
Interface sur http://127.0.0.1:53412
Répertoire de travail : /home/steff/projects/gescomThe left-hand tree lists the server's databases, their schemas and their tables; each table expands into its columns. You tick what you want to extract, and the screen flags referenced tables missing from the selection — a foreign key left pointing nowhere makes the association vanish silently.
Unticking a column does not remove it from the layer: it fills
colonnes_ignorees in the decisions file, which drops it from the entity. The
layer keeps everything, and you can change your mind without reopening the
connection.
Extract writes <base>.calque.json as a background task, without blocking
the screen; a running extraction can be cancelled.
The Review tab works offline, on the layer in the working directory: no
connection is needed. It shows the entities generation will produce, and saves
your rulings to <base>.decisions.yaml.
It listens on 127.0.0.1 only, on a port drawn at startup, and the token in the
URL is good for a single use. Files land in the directory shown on screen;
--repertoire points somewhere else, and the screen can change it without a
restart.
A connection can be saved as a profile: give it a name before connecting,
and it is kept once the connection succeeds. A profile carries the working
directory that goes with it, and remembers the password if you ask — encrypted
on the machine, with the limits SECURITY.md spells out.
Profiles, display preferences and review drafts live in the system
configuration directory, never in your project:
docs/emplacements.md says what goes where, and why.
The commands are the same, bar three details: the binary is called
.\ormeau.exe, environment variables are set differently, and --user is
pointless — a Windows volume carries no POSIX permissions.
$env:ORMEAU_MDP = "secret"
.\ormeau.exe extraire --sgbd postgres --hote srv --utilisateur app --base gescom --sortie gescom.calque.json
docker run --rm -e ORMEAU_DSN -v "${PWD}:/sortie" `
ghcr.io/sprimault/ormeau:v0.7.2 extraire --sortie /sortie/gescom.calque.jsonVerifying the checksum of a downloaded archive:
$attendu = (Select-String -Path SHA256SUMS -Pattern windows).Line.Split(" ")[0]
$obtenu = (Get-FileHash ormeau_v0.7.2_windows_amd64.zip -Algorithm SHA256).Hash.ToLower()
if ($attendu -eq $obtenu) { "empreinte OK" } else { "EMPREINTE DIFFERENTE" }A layer (calque): a tracing of the catalogue, in JSON, that judges nothing
and loses nothing. The type name is the server's own, never rebuilt; the default
value is structured, so that DEFAULT 'now()' and DEFAULT now() stay
distinguishable; length and precision are absent rather than zero, so that
decimal(10,0) is not confused with int.
{
"nom": "cli_statut",
"position": 4,
"type_brut": "character varying(20)",
"type_normalise": "texte",
"longueur": 20,
"nullable": false,
"defaut": { "genre": "litteral", "valeur": "ACTIF" }
}Two extractions of the same database differ only by source.extrait_le, the
extraction time, and share the same fingerprint, which excludes it. That is what
makes the diff mode usable, and what allows a layer to be versioned in Git.
gescom.calque.json the observation, extracted from the database
gescom.decisions.yaml your rulings, written once
gescom.logique.json the object model, consumed by the generator
They live in your project and are committed with it. Only the first comes from the database; the other two are recomputed offline, so an inference can be corrected without reopening the connection — including on a layer brought back from a customer site.
decisions.yaml is what makes the second extraction bearable. Six months later
the schema has moved, but your rulings replay on top of it: what you corrected
once is not lost again.
One file per database, never a shared one: two databases may both have a
client table without the same rename suiting either.
The design behind this format — its two levels, its three properties, the split
between Go and PHP — is in docs/architecture.md.
Table with no primary key, composite primary key, undeclared foreign key,
0000-00-00 dates, a boolean stored as char(1) holding O/N, two tables
linked by columns of different types. That is the daily reality of a database
being taken over, and what existing tools handle worst.
The rule: emit a warning, never an exception, never an invention. A partial
logical layer with twenty precise warnings beats a fatal error or a silently
wrong model. Every inferred element carries its origine — contrainte,
verification, cardinalite, nommage or decision — without which the tool
is not auditable.
The tool only ever reads. Connections are read-only, enforced by the server rather than by the code's discipline, with a per-query timeout. The session is read back when it opens: if an intermediary dropped read-only mode on the way, extraction is refused.
The DSN is the only secret handled: it appears neither in logs, nor in error messages, nor in the layer.
Ormeau opens two connections and no others: your database, and 127.0.0.1 for
the local interface. No telemetry, no outbound call, not even an update check —
the code is public, and netstat confirms it during an extraction.
A layer is a customer's database schema — table names, column names,
business comments, and with --echantillonner, once sampling ships, real
values. A layer extracted from a production database never enters a repository,
nor an issue attachment.
make aller-retour starts from the test database,
tests/ddl/postgres.sql, which deliberately gathers
the messy cases. It extracts the layer, generates the entities, lets Doctrine
recreate the schema in an empty database with schema:create, then compares
the recreated database with the original, object by object.
make aller-retour-sqlserver does the same from
tests/ddl/sqlserver.sql. CI runs both on every push
and pull request, under Doctrine ORM 3 with DBAL 4 and under ORM 2.14 with
DBAL 3.
An empty diff is out of reach with Doctrine: a view, a CHECK constraint or a
native enum type is not recreated, whatever the entity says. Each remaining
difference is therefore listed with its reason — a Doctrine or DBAL limit, or a
choice of the tool, such as a collation it refuses to write because it cannot
qualify it — and any other difference fails CI. The authoritative lists are
tests/allerretour/ecarts_test.go for
PostgreSQL and
tests/allerretour/ecarts_sqlserver_test.go
for SQL Server.
PostgreSQL and SQL Server extraction, inference and Doctrine entity generation
work: ormeau extraire then ormeau inferer produce the three files, and
bin/console ormeau:generer writes the entities, their associations,
enumerations and traits, and the inheritance declared in the decisions file,
skipping what Doctrine cannot represent and saying so. The round trip checks it
on both DBMSs, under ORM 2 and 3. Comparing
the database with existing entities (ormeau:synchroniser) is still to come.
The state per phase is in ROADMAP.md.
CI runs the test suite with the race detector, golangci-lint, gofmt,
govulncheck, gosec and a JSON Schema validity check on every push and pull
request. The PHP package goes through PHPUnit, PHPStan and PHP-CS-Fixer, and its
tests run under five combinations of PHP, Symfony, Doctrine ORM and DBAL, from
PHP 8.1 with Symfony 5.4 to PHP 8.4 with Symfony 8. Integration tests run there against
a real database server, never a simulated catalog, and the extraction of the
test database is compared byte for byte with a reference layer.
CI only confirms: every change first goes through the same full validation, on Windows and Linux machines, before it is pushed.
docs/architecture.md— the layer, its two levels, why two languagesdocs/construction.md— cross-compilation, multi-arch images, signingdocs/emplacements.md— where the project files go, and where what concerns only your machine goesschemas/— the public contract, versioned separatelyCONTRIBUTING.md— the rules a pull request is judged against
Bugs, feature requests or questions: open an issue at https://github.com/sprimault/ormeau/issues (French preferred, English welcome).
Security flaws go through the private channel described in
SECURITY.md, never through a public issue.
Ormeau — pronounced roughly or-MOH — is French for a young elm, and for the abalone, a shellfish whose shell is built from stacked layers of nacre. It also happens to begin with ORM, which is convenient for a tool that produces ORM entities.
The pivot format is called a calque, in the tracing-paper sense: a faithful copy of the catalogue, with no interpretation. In linguistics a calque is a structural borrowing from one language into another — English skyscraper becoming French gratte-ciel. That is precisely the operation: carrying the structure of a relational schema over into another language's type system.
Apache 2.0 — see LICENSE.
The entities, layers and decision files Ormeau produces are yours. The licence covers the tool, not its output: nothing it generates enters your project with an obligation attached.