Plugin System (Package Manager)
Every bot feature is written as an independent plugin — exactly
like every app installed on Android has its own Activities,
permissions, and database. Package\PackageManager is the
equivalent of PackageManagerService.
Plugin Structure
plugins/greeter/
├── manifest.php # this plugin's AndroidManifest.xml
├── migrations/
│ ├── migrations.php
│ └── versions/
│ └── Version20260101000000.php
└── src/
├── StartActivity.php
├── ProfileActivity.php
├── RelayActivity.php
├── WelcomeReceiver.php
├── Entity/
│ └── Subscriber.php
└── Repository/
└── SubscriberRepository.php
Writing manifest.php
use Aicrion\Tandroid\Package\Manifest; use Aicrion\Tandroid\Package\Permission; use Greeter\Entity\Subscriber; use Greeter\{ProfileActivity, RelayActivity, StartActivity, WelcomeReceiver}; return Manifest::define( package: 'greeter', version: '1.0.0', activities: [StartActivity::class, ProfileActivity::class, RelayActivity::class], permissions: [Permission::SendMessage, Permission::BroadcastEvents], entities: [Subscriber::class], receivers: [WelcomeReceiver::class], );
| Field | Android equivalent | Role |
|---|---|---|
package |
package="..." |
The plugin's unique identifier; also used to name the migration table |
version |
versionName |
Purely informational; not currently checked for compatibility/auto-update |
activities |
<activity> |
Any Activity not declared here is never registered in the DI Container and can never be navigated to — even with a #[IntentFilter] on it |
permissions |
<uses-permission> |
A declarative list from the Permission enum; currently documentation/audit only, not yet enforced automatically at runtime |
entities |
— | The list of Entity classes; used to build the EntityManager automatically and resolve migration paths |
receivers |
<receiver> |
Classes implementing Broadcast\BroadcastReceiverInterface |
⚠️ Important: the only way to activate an Activity, Receiver, or Entity is to declare it in that same plugin's
manifest.php. Simply having the class exist insrc/isn't enough — this is a deliberate design soPackageManageronly wires autoloading and DI for things that are actually "installed."
The Discovery Cycle
PackageManager::discover() runs on every Kernel::boot():
$pattern = "{plugins_path}/*/manifest.php"; foreach (glob($pattern) as $manifestFile) { $manifest = require $manifestFile; // must return a Manifest }
After discovery, Kernel::boot() continues in this order:
- Runs each plugin's pending migrations (Database and Doctrine).
- Builds the
EntityManagerusing the Entities collected from all plugins. autowires every declared Activity in the DI Container.- Builds the
#[IntentFilter]registry forIntentResolver. - Registers every declared Receiver in
BroadcastDispatcher.
Automatic Autoloading — No Separate composer.json
Plugins aren't installed as Composer dependencies (only their folder
is copied), so Composer has no idea their classes exist.
PackageManager::discover() registers a lightweight PSR-4-style
autoloader for every discovered plugin via spl_autoload_register():
the first namespace segment (which must match the plugin's root
class name exactly, e.g. Greeter) maps directly to that plugin's
src/ folder:
Greeter\StartActivity → plugins/greeter/src/StartActivity.php Greeter\Entity\Subscriber → plugins/greeter/src/Entity/Subscriber.php Greeter\Repository\SubscriberRepository → plugins/greeter/src/Repository/SubscriberRepository.php
This means your plugin's src/ folder structure just needs to match
its internal namespaces under this convention — the exact same one
Composer's PSR-4 uses — without needing to build a separate
composer.json per plugin or run composer dump-autoload.
Installing a New Plugin
Installing a plugin means simply copying its folder into
plugins/:
cp -r /path/to/awesome-plugin plugins/awesome-plugin
On the next request (webhook or polling), Kernel::boot() will:
- discover the new plugin,
- build its tables (automatic migration),
- activate its Activities and Receivers.
No install command, no manual class cache, no SSH required — the same experience as installing an APK where the OS handles the rest.
Removing a Plugin
Delete its folder from plugins/. Its Activities/Receivers will no
longer resolve. Its database tables are not automatically
removed (much like uninstalling an Android app without
adb uninstall -k sometimes keeps app data) — if you need a full
cleanup, write a down() migration for it and run it before
deleting the folder.
Suggested Skeleton for Your Next Plugin
The best starting point is copying plugins/greeter and renaming
the namespace — this plugin demonstrates every core pattern (a
simple Activity, an Activity with navigation, an Activity making a
bot-to-bot call, Entity+Migration, a Receiver) in the smallest
possible form.