Quick start
From an empty folder to a running plugin. The path takes about twenty minutes and ends with an API endpoint of your own that you can see in the browser.
Prerequisites
A development checkout of the platform with a running database. A plugin can be developed standalone, but for the first run the checkout is the shorter route.
1. Generate the skeleton
bin/console octibiz:plugin:create AcmeShipping
Options, when the defaults do not fit:
bin/console octibiz:plugin:create AcmeShipping \
--namespace "Acme\Shipping" \
--label "Acme Shipping" \
--dir plugins/ \
--with-entity
The naming rules
The name is the technical name in PascalCase with your own prefix. Everything else derives from it:
| Input | Namespace | Plugin key | Module switch | API segment |
|---|---|---|---|---|
AcmeShipping | Acme\Shipping | acme.shipping | module.acme-shipping | /api/v1/acme-shipping |
The prefix is not a matter of style. It prevents two plugins from claiming the same key or the same address segment. If a plugin claims a segment belonging to the core, the installation aborts hard rather than shadowing the core route.
The name needs at least two word parts; the first is the developer prefix.
2. What you get
plugins/AcmeShipping/
├── composer.json # type: octibiz-plugin + extra.octibiz
├── plugin.json # package manifest
├── CHANGELOG.md # topmost [x.y.z] section == const VERSION
├── README.md
├── src/
│ ├── AcmeShippingPlugin.php # bundle
│ ├── AcmeShippingDescriptor.php # self-description
│ ├── ApiResource/AcmeShippingExampleResource.php
│ ├── State/AcmeShippingExampleProvider.php
│ ├── Security/AcmeShippingPermissionProvider.php
│ ├── Security/AcmeShippingVoter.php
│ ├── Catalog/AcmeShippingStandardDataProvider.php
│ ├── EventListener/AcmeShippingWorkItemCreatedListener.php
│ └── Resources/icon.svg
└── frontend/
├── plugin.manifest.ts
└── locales/de.ts, en.ts
For a third-party plugin you additionally get tests/, phpunit.xml.dist and a CI workflow, plus the development dependency on the SDK package.
3. Register
Two entries, both in the host. Without them the plugin does not exist for the application.
Autoload in the instance composer.json:
{
"autoload": {
"psr-4": {
"Acme\\Shipping\\": "plugins/AcmeShipping/src/"
}
}
}
composer dump-autoload
Bundle in config/plugins.php:
return [
Acme\Shipping\AcmeShippingPlugin::class => ['all' => true],
];
4. Install and enable
bin/console doctrine:migrations:migrate --no-interaction
bin/console octibiz:plugin:install acme.shipping --activate
The order matters: the install command runs no migrations. It records catalogue ownership and creates the module switch, and it creates it disabled. Without --activate every route of the plugin keeps answering with 404.
Why disabled: a plugin switch is fail-closed. If the row is missing or off, the route is not reachable. That is the inverse of the core, whose modules count as enabled without a row.
5. Check that it runs
bin/console octibiz:plugin:list
The row shows version, trust tier, enabled state and the number of catalogue rows held. If it says enabled, the example endpoint answers too:
curl -H "Authorization: Bearer <token>" https://<instance>/api/v1/acme-shipping/examples
6. Remove the generator leftovers
The skeleton ships an example resource with placeholder rows. They must go before the plugin ships. They are visible not only in the OpenAPI description but also in the tool catalogue for AI access, where a model takes the placeholders for real business data.
Replace AcmeShippingExampleResource with your first real resource, or delete it along with its provider.
The most common beginner mistake
A new API prefix is introduced but not added to getRouteSegments() in the descriptor. The consequence is inconspicuous and dangerous: for an undeclared segment no gate applies. The route stays reachable even when the module is switched off.
Every new prefix belongs in the descriptor in the same commit.
Developing standalone
Without a host checkout, the contract surface is available as packages of its own. Both follow the version axis of the plugin contract, not the system version:
{
"require-dev": { "octibiz/plugin-sdk": "~3.15.0" }
}
{
"devDependencies": { "@octibiz/plugin-sdk": "~3.15.0" }
}
The PHP package ships AbstractPlugin, the contract registry, every contract, the frozen event classes and the payload types. That gives you autocompletion, static analysis and standalone unit tests without access to the host sources.
Development dependency only. At runtime the instance provides the real classes; the SDK must never sit in a running installation, or every class would exist twice.
Not included are host classes that appear in no frozen signature: entities, API resources, repositories and the services behind them. For integration tests against the running platform you still need a checkout.
Next
- Anatomy of a plugin — what each file does
- Backend — data model, API, permissions, background processing
- Interface — views, extension points, navigation
- Testing — where tests live and why not in the plugin
- Lifecycle and commands — install, update, remove