glitchr / omnilex
Omnilex: one contract for the sources of law - texts, codes and case law, searched with filters (jurisdiction, date, subject, type), read in the version in force at a date, their links followed, what is new listed - from Légifrance, Judilibre, EUR-Lex and the French administrative courts' open data;
Requires
- php: >=8.2
- symfony/http-client-contracts: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0
- 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/http-client: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
Suggests
- omnilex/eurlex: EUR-Lex: EU law and the case law of the Court of Justice, from the Publications Office's open endpoint
- omnilex/judilibre: Judilibre (PISTE): the decisions of the Cour de cassation and of the judicial courts
- omnilex/justice-administrative: The open data of the Conseil d'État, the administrative courts of appeal and the administrative tribunals
- omnilex/legifrance: Légifrance (PISTE): codes, laws, decrees, the Journal officiel, as in force at a date
- symfony/cache: In a Symfony application: the pool (cache.app) where Omnilex\Bridge\Symfony\CacheTokenStore keeps the PISTE tokens
- symfony/config: In a Symfony application: the configuration of Omnilex\Bridge\Symfony\OmnilexBundle
- symfony/dependency-injection: In a Symfony application: the services of Omnilex\Bridge\Symfony\OmnilexBundle
- symfony/http-kernel: In a Symfony application: Omnilex\Bridge\Symfony\OmnilexBundle itself
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-09 03:19:11 UTC
README
One contract for the sources of law - texts, codes, case law: searched with filters (jurisdiction, date, subject, type), read in the version in force at a given date, their links followed between decisions and texts, and what is new since a date listed for a legal watch.
$article = $legifrance->article('LEGITEXT000006070987', 'L36-11', new DateTimeImmutable('2018-01-01')); $article->content; // the article as it read on 1 January 2018 $article->version->from; // ... the day that wording started to apply $gdpr = $eurlex->text('32016R0679', new DateTimeImmutable('2018-06-01')); $gdpr->version->id; // 02016R0679-20160504: the consolidated version of that day $decision = $eurlex->decision('ECLI:EU:C:2014:317'); $decision->citations; // interprets 31995L0046, cites 62012CJ0473... $new = $administratif->recent(new DateTimeImmutable('-7 days'), new Query(jurisdictions: ['CE']));
This package holds the contract (Source\*Interface, Source\SourceFactory, Registry), the
models (Text, Article, Version, Decision, Court, Citation, Reference, Query,
Results, Capabilities), the identifiers (Identifier: ECLI, CELEX, ELI, NOR, numéro de
pourvoi, Légifrance's own - normalised and validated without the network), OAuth client
credentials for PISTE (Auth\ClientCredentials) and a bridge for Symfony. It needs no framework:
it requires nothing but symfony/http-client-contracts, each source package
symfony/http-client. Each source is a package of its own:
| Package | Source | Access |
|---|---|---|
omnilex/legifrance |
Légifrance: codes, laws, decrees, the Journal officiel, as in force at a date; case law | PISTE account (free), OAuth client credentials |
omnilex/judilibre |
Judilibre: the decisions of the Cour de cassation and of the judicial courts, pseudonymised | PISTE account (free), OAuth client credentials or API key |
omnilex/eurlex |
EUR-Lex: EU acts in their consolidated versions, the case law of the Court of Justice, their links | none (the Publications Office's open endpoint) |
omnilex/justice-administrative |
The Conseil d'État, the administrative courts of appeal and tribunals: decisions, pseudonymised | none |
Capability interfaces
A source implements what its service does, and only that:
| Interface | Method | |
|---|---|---|
SearchInterface |
search(Query): Results |
filters: jurisdiction, dates, subject, type, kind, in force at a date |
TextReaderInterface |
text($id, $at): ?Text |
a text as in force on a day |
ArticleReaderInterface |
article($id, $number, $at): ?Article |
one article as in force on a day |
DecisionReaderInterface |
decision($id): ?Decision |
a court's decision |
CitationsInterface |
citations($id): Citation[] |
the links between decisions and texts, both ways |
RecentInterface |
recent($since, ?Query): Results |
what is new since a day |
capabilities() says the rest: the kinds of documents held, the identifiers read, the search
criteria applied. What a source cannot do is a NotSupportedException - a search criterion it
cannot apply is refused, never silently dropped.
Three answers, never confused
- Unknown is
null(or an empty page). - Down, refused or slowed is an
UnavailableException(RateLimitedException,AuthenticationException): never read as "not found", never cached, never "nothing new". - Not possible with this source is a
NotSupportedException.
Pseudonymised decisions
Judilibre and the administrative courts publish their decisions with the names of the persons
removed. Omnilex gives these texts as published (Decision::$pseudonymised) and never tries to
put a name back on a person; an application built on it must not either.
Scope
Omnilex reads sources of law. Legal scholarship (articles, theses) is read with
glitchr/omnischolar (omnischolar/hal on the
domain shs.droit); public registers (companies, VAT numbers) with glitchr/omnistate.
Documentation
- Installation and first calls
- Sources: what each can and cannot do
- Models
- Identifiers: ECLI, CELEX, ELI, NOR, pourvoi, Légifrance
- The law at a date: versions
- Authentication: PISTE and OAuth client credentials
- Symfony
- The Docker harness
Plain PHP
composer require glitchr/omnilex omnilex/eurlex omnilex/justice-administrative
use Omnilex\Eurlex\EurlexSourceFactory; use Omnilex\JusticeAdministrative\JusticeAdministrativeSourceFactory; use Omnilex\Registry; use Symfony\Component\HttpClient\HttpClient; $http = HttpClient::create(); // or the application's client; a MockHttpClient in a test $registry = new Registry([new EurlexSourceFactory($http), new JusticeAdministrativeSourceFactory($http)], [ 'eurlex' => ['factory' => 'eurlex', 'options' => ['language' => 'fr']], 'administratif' => ['factory' => 'justice-administrative'], ]); $eurlex = $registry->articles('eurlex'); // typed: an ArticleReaderInterface $administratif = $registry->recent('administratif');
No bundle, no container: a factory per source package, the registry built by hand. docs/installation.md opens on a whole script that runs as it is, against the real service.
Symfony
Omnilex\Bridge\Symfony\OmnilexBundle does that wiring in a Symfony application
(docs/symfony.md); its components (symfony/config,
symfony/dependency-injection, symfony/http-kernel, a cache pool for the PISTE tokens) are not
required by this package.
omnilex: sources: legifrance: { factory: legifrance, options: { client_id: '%env(PISTE_CLIENT_ID)%', client_secret: '%env(PISTE_CLIENT_SECRET)%' } } judilibre: { factory: judilibre, options: { client_id: '%env(PISTE_CLIENT_ID)%', client_secret: '%env(PISTE_CLIENT_SECRET)%' } } eurlex: { factory: eurlex, options: { language: fr } } administratif: { factory: justice-administrative }
public function __construct(ArticleReaderInterface $legifrance, DecisionReaderInterface $judilibre, RecentInterface $administratif) {}
Docker: every source, for real
cd docker && cp .env.dist .env docker compose run --rm omnilex sources docker compose run --rm omnilex text eurlex 32016R0679 --at 2018-06-01 docker compose run --rm omnilex recent administratif 2026-10-01 --jurisdiction CE docker compose run --rm omnilex bare # plain PHP: no bundle, no container, and what PHP loaded docker compose run --rm omnilex test
License: MIT since 2026-10-09; earlier versions remain published under LGPL-3.0-or-later.