Skip to main content
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 into packages/<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 into packages/ next to the others, then let Composer link it:
That creates 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 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:
If the package builds on the platform contracts, require them by constraint in its own composer.json, for example "fapost/foundation": "^0.1".
A Solution or Plugin may depend on fapost/foundation. It must never depend on Core (App\…) directly.
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 pins the tagged version into 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’s composer.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

It writes 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 COMPOSER=composer.local.json:
A plain 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 sections below explain each piece.

The overlay file

Describe the package in composer.overlay.json at Core’s root. The file is git-ignored, and only repositories and require are accepted:
For local development point a 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

The script writes 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 with COMPOSER=composer.local.json, so they read the overlaid manifest:
A plain 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.
The overlay changes nothing in Core’s repository, so your extension must work through public contracts only. Core never checks whether an extension is installed; it binds its own defaults for every contract, and your service provider replaces them through package discovery.

In your extension’s CI

Check out Core, place your package under packages/, write an overlay with a path repository to it, and run:
Then run your package’s tests inside the Core application. Testing against the real Core, rather than a stand-in framework, catches contract drift as soon as Core changes.

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:
For a package added through an overlay, raise the constraint in 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 the landlord 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.
The platform migrate command applies them together with Core’s own, so no extra deploy step is needed:

Reference