fapost/foundation and fapost/support — and any Solution or Plugin you build —
live in their own git repositories. This page explains how a package is developed
against a Core checkout, and the two ways it reaches an install: as a dependency
Core itself declares, or as an overlay Core never names.
The Core-owned shared packages use the fapost/* vendor and live under the
fapost-lab organization. A Solution or Plugin you
contribute is your own package: it uses your vendor (for example
acme/hr-solution) and any git host you like. Everything below works the same
regardless of vendor — nothing is hard-coded to fapost.
How it works
Each package is a standalone repository. During development its source is checked out intopackages/<dir>, which the Core repository ignores (/packages is in
.gitignore). The checkout directory name is free; the package’s Composer name
is what matters.
Two lanes coexist without ever editing composer.json per environment:
composer.json and composer.lock always describe the production lane, so they
are safe to commit. The local symlinks are created outside Composer’s dependency
resolution, so they never leak into the lock file.
The linker
tools/dev-link-packages.php, exposed as the dev:link Composer script, replaces
the Composer-installed copy of each package under vendor/ with a relative
symlink to its local source. It is wired into post-install-cmd and
post-update-cmd, so it runs automatically after every composer install and
composer update.
It auto-discovers packages by reading each packages/*/composer.json and linking
vendor/<name> from that file’s name — so any vendor works with no changes to
the linker. It is a no-op when packages/ is absent, which is why the same hook
is safe in production and CI.
Set up an existing package for development
Clone the package intopackages/ next to the others, then let Composer link it:
vendor/fapost/foundation as a symlink to
packages/fapost-foundation. A plain composer install re-creates the symlink on
its own.
From here, edits inside packages/fapost-foundation are picked up instantly — no
composer update needed. The checkout is a normal git repository: branch, commit,
and push it independently of Core.
Which lane a package takes
Core’s
composer.json lists only what Core needs to run. An extension that would be
written into it would ship to every install and every fork; an overlay keeps it where
it is wanted.
Add a package Core requires
This lane is for packages Core itself depends on. Assume a new package<vendor>/<name>; the steps are vendor- and host-agnostic.
1
Create the package repository
Scaffold If the package builds on the platform contracts, require them by constraint in
its own
packages/<dir> — any directory name — with its own composer.json
("name": "<vendor>/<name>", PSR-4 autoload, a license), initialise a git
repository, push it, and tag the first release:composer.json, for example "fapost/foundation": "^0.1".2
Declare it in the root composer.json
Add a VCS repository and a version constraint. This is what production resolves
against:Private repositories on a non-GitHub host may need Composer authentication
configured through
composer config or auth.json.3
Resolve and link
composer.lock from the VCS, and
post-update-cmd runs dev:link for you, replacing
vendor/<vendor>/<name> with a symlink to the local checkout. The linker
needs no changes — it discovers the new package automatically from its
composer.json name, whatever the vendor.Packages Core does not require
A Solution, a Plugin or a private extension is added on top of Core with a composer overlay: Core’scomposer.json and composer.lock stay untouched,
the package is installed against Core’s locked versions, and the same mechanism
serves local development, your extension’s CI and an image built from Core.
Quick start: develop a package against a Core checkout
1
Check the package out under packages/
packages/ is ignored by Core’s .gitignore, so the checkout never shows up in Core’s
repository:2
Describe it in composer.overlay.json
At Core’s root (the file is git-ignored too):
3
Install it
composer.local.json and composer.local.lock and installs the package against
Core’s locked versions. dev:link then links vendor/acme/my-extension to your checkout, so
edits are live.4
Keep using the overlaid manifest
Run every further Composer command in Core with A plain
COMPOSER=composer.local.json:composer install returns Core to its state without the package — composer overlay
brings it back.5
Run the package's tests inside Core
Test against the real application with Core’s PHPUnit and the package’s own configuration,
whose bootstrap loads Core’s
vendor/autoload.php:The overlay file
Describe the package incomposer.overlay.json at Core’s root. The file is
git-ignored, and only repositories and require are accepted:
path repository at your checkout under
packages/; in CI or an image use your package’s VCS repository and a version
constraint instead.
Install it
composer.local.json (Core’s manifest plus the overlay) and
composer.local.lock (a copy of Core’s lock), both git-ignored, then installs the
overlay packages against them. A package Core already requires or locks, directly
or transitively, is rejected: an overlay adds packages, it never overrides or
moves Core’s versions. If your package needs a newer version of something Core
locks, the install fails instead of upgrading Core underneath it.
post-update-cmd runs dev:link, so a checkout under packages/ is symlinked into
vendor/ exactly as in the Core-required lane.
Options reach the script after --, because Composer strips the first -- and
drops options it does not know before it:
Working with an overlay
Run further Composer commands withCOMPOSER=composer.local.json, so they read the
overlaid manifest:
composer install or composer update goes back to Core alone and
uninstalls the overlay package from Composer’s point of view — a quick way to
check that Core still runs without your extension. Run composer overlay again to
bring it back. dev:link may still leave a symlink under vendor/ for a
packages/* checkout; it is harmless, because the package is neither autoloaded nor
discovered.
In your extension’s CI
Check out Core, place your package underpackages/, write an overlay with a
path repository to it, and run:
In an image built from Core
An image that adds an extension on top of the Core image runs the script directly: passing--no-scripts to composer overlay would disable the overlay script
itself, because Composer scans all raw arguments. The Core image removes Composer
and runs as www-data, so switch to root for the build step, copy Composer back,
and hand the results back to www-data:
post-autoload-dump runs package discovery, so your service provider is found.
A private package repository needs Composer credentials at build time; pass them as
a build secret (COMPOSER_AUTH), never as a file copied into the image.
Publish a new version
Package changes reach production through tags, not the local checkout:composer.overlay.json and run composer overlay again instead.
With 0.x versions, ^0.1 accepts 0.1.* only; bump the constraint to ^0.2 to
adopt a new minor. Until you re-run composer update, Core keeps the version
pinned in composer.lock — your local symlinked edits are visible to you but do
not change what production installs.
Composer needs git or SSH access to each package repository to resolve it — the
fapost-lab organization for the Core packages, and your own host for your
Solution or Plugin. Make sure your key is authorised, or auth.json is set for
private hosts, before running composer update.Landlord tables of a package
An extension may keep platform-level data of its own, for example plans or usage per tenant, in thelandlord database. The rules:
- Name every table with the package’s own prefix, such as
saas_. - Never write Core’s landlord tables (
tenants,webhook_registry) and never put a foreign key on them. Refer to a tenant by its id and clean up your own rows yourself. - Deliver the migrations with
loadMigrationsFrom()in the package’s service provider. - Declare
protected $connection = 'landlord';in each migration, and keep it pure DDL.