GTFS Core¶
Imports GTFS feeds into Drupal and gives every file in the specification a real entity, with the relationships between them intact. A route knows its agency and its trips, a trip knows its stops and its shape, a stop knows the routes that serve it.
It is built to be the foundation other things sit on: a website per route, a departure board, a timetable, an API for someone else's app. The importer keeps those entities in sync with the feed, so fields you add yourself survive re-imports rather than being wiped each time the agency publishes.
How the data is stored¶
Everything is one entity type, gtfs_row, with a bundle per GTFS file — routes, trips,
stops, stop_times and the rest. Bundles are declared in code with the #[GTFSFile]
attribute rather than through the UI, so a module can add a file the specification does not
define. GTFS+ does exactly that.
Each import belongs to a feed, and rows are scoped to their feed. A site can import the same agency many times — different versions, different sources — and they stay separate.
Requirements¶
- Drupal 11.3 or later, or Drupal 12
- Timezone Field, Time Field, Currency Field, Entity Reference Revisions, Telephone — installed automatically by Composer
A private file path is required. Feeds are downloaded and extracted under Drupal's
private file system. Set $settings['file_private_path'] in settings.php to a directory
outside the web root before importing. The status report warns if it is missing.
Installation¶
composer require drupal/gtfs
Then enable it as you would any module. See Installing modules if you need the longer version.
Getting started¶
- Add a feed. Go to Administration › GTFS › Feeds (
/admin/gtfs/feed) and add one, pointing at the URL of a GTFS zip archive. - Import it. Use the feed's Import tab and follow the prompts. Large feeds run as a batch across several requests.
- Browse what came in. Administration › GTFS (
/admin/gtfs) lists every file with its rows.
Re-run the import whenever the agency publishes an update. Rows are matched on their GTFS identifiers and updated in place, so anything you added on top of them is preserved.
Exporting a feed¶
A feed can be written back out as a GTFS zip, as the site holds it now, rows edited in
Drupal included. Give a role the Export GTFS feeds permission, then use the feed's
Export tab (/admin/gtfs/feed/{id}/export). The zip is built as a batch and kept in
private files for download until the next export replaces it.
From the command line:
drush gtfs:feed:export my-feed-alias --output=/tmp/feed.zip
Add --validate to run the result through MobilityData's
GTFS validator. It needs Java 17 or later
and the validator's CLI jar, given with --validator-jar or the GTFS_VALIDATOR_JAR
environment variable. The command fails if the validator reports any errors, and leaves
report.html beside the zip.
The export carries what the importer kept. Columns and files the module does not define, such as a licence text in the archive, were never stored, so they are not written back. Columns come out in the order the file is declared rather than the source's order. Optional columns that no row fills in are left out.
REST API¶
Every resource lives under /gtfs/api/v2/feeds/{feed_alias}/, so a request is always
scoped to one feed. A site that has imported the same agency several times keeps those
imports separate: nothing in a response crosses from one feed to another.
Responses carry the GTFS fields flat, alongside a label and a links map of related
resources. Follow links rather than building paths by hand — that is the supported way to
move between resources.
List responses are paginated. Pass ?limit= and ?offset=; read meta.totalCount for the
size of the whole set, meta.dataCount for the size of the page, and meta.links.next and
meta.links.previous to walk it.
/gtfs/api/v2/feeds and /gtfs/api/v2/feeds/{feed_alias} both report what a feed currently is:
its id, last_imported, and its own feed_info.txt row. A client caching responses needs all
of them, because an alias is deliberately stable — promotion changes the id, re-importing the
same feed changes last_imported, and neither moves the alias. The list carries the same fields
as the single-feed form, so comparing feeds costs one request rather than one per feed.
Field values are JSON strings, including numeric ones — stop_lat is "37.7212", not
37.7212, and route_type is "3", not 3. This is consistent across every field and
every resource, so parse on the client where you need numbers. It follows GTFS itself,
which is a CSV format with no types.
Resources are exposed through Drupal's REST module; enable and grant them as you would any other REST resource.
Submodules¶
GTFS Fares (gtfs_fares) — entity types for the fare files: fare attributes, fare
rules, fare products, and the rest of the fares model. Enable it if your feed has them.
Extending¶
Bundles are declared with the #[GTFSFile] attribute on a class extending
Drupal\gtfs\Entity\GTFSRow. Adding a file the specification does not define, or changing
the fields on one that it does, means declaring such a class — see the gtfs_fares
submodule for a worked example, and GTFS+ for a
separate project that does it.
gtfs.api.php documents the hooks. Currently:
hook_gtfs_yasumi_provider_alter()— choose the Yasumi holiday provider that names the dates incalendar_dates.txt, since agencies commonly run a reduced timetable on locally observed holidays. The service and schedules resources put the name beside the date, so a picker can offer "Labor Day" rather than the 7th. The default is the United States, so any agency outside it should implement this hook, passing either a short name understood byYasumi::create()such as'USA', or a fully-qualified class name. An agency with its own service days — a school break, a seasonal timetable — can subclass a provider and name that. A date no provider knows keeps itsholidayasnull, which is the ordinary case.
Development¶
The module ships PHPUnit tests, a phpunit.xml.dist, and a GitLab CI
configuration.
./vendor/bin/phpunit -c phpunit.xml.dist
Run that from this directory. It needs the module's own dev dependencies, which
means composer install here — and because a contrib module does not declare
where Drupal packages come from, that install only resolves on a checkout where
the drupal.org Composer repository is configured. Drupal.org CI supplies it; a
local checkout needs it adding.
The configuration bootstraps core rather than tests/bootstrap.php, because
core's bootstrap is what registers the Drupal\Tests\{module} namespaces that
kernel tests extending EntityKernelTestBase rely on. Its relative path assumes
this module sits in a site's modules directory.
See ROADMAP.md for planned work.
Issues¶
File issues and merge requests at https://www.drupal.org/project/issues/gtfs.
Related modules¶
- GTFS Realtime — trip updates, vehicle positions and service alerts
- GTFS Display — stop, route and system displays for public screens
- GTFS Display Map — route and stop geometry on a map
- GTFS Display Map RT — live vehicles, and an embeddable map
- GTFS Schedule — readable timetables
- GTFS 511 — feed provisioning from the 511 SF Bay API
- GTFS+ — the GTFS+ extension files
Maintainers¶
This project has been sponsored by: