# Writing Convoro extensions

An extension adds a feature to a running Convoro site. It ships as a single
`.zip` file, installs from the admin panel or the command line, and can be
upgraded, disabled and removed without touching Convoro itself.

| | |
|---|---|
| [hooks.md](hooks.md) | Every template hook point and what it is given |
| [events.md](events.md) | Every domain event and what it carries |
| [packaging.md](packaging.md) | The `.zip` format, versions, signing, releasing |
| [security.md](security.md) | **What an extension is trusted with. Read this.** |

Two working examples live in [`examples/`](../../examples):
[Hello Hooks](../../examples/hello_hooks) is presentation only and fits in one
file; [Topic Stars](../../examples/topic_stars) owns a table, a route and a
page.

## Before anything else

**An installed extension runs as PHP in the same process as Convoro, with the
same access to the database, the filesystem and members' data. There is no
sandbox.** Installing one means trusting its author with all of that.
[security.md](security.md) explains why it works that way and what is done
about it.

## Start here

```
php tools/convoro ext:scaffold my_thing --name="My Thing" --author="You"
php tools/convoro ext:package my_thing
php tools/convoro ext:install my_thing-1.0.0.zip
```

What that generates already works: it serves a page at `/my_thing`, puts a link
in the header through a hook, and logs when a topic is created through an event
listener. Change it from there.

## Anatomy

```
my_thing/
├── manifest.json                  key, version, what it needs
├── MyThing.php                    the provider: hooks and listeners
├── Routes/front.php               loaded automatically
├── Routes/admin.php               ditto — put RequireAdmin on it yourself
├── Controllers/                   extend the same base class core uses
├── Templates/                     addressed as my_thing::front/index
└── Migrations/                    forwards on install, backwards on uninstall
```

An extension **is** a module. Every built-in feature — forums, members,
messages, search — is built the same way, from the same base class, and the
`modules/` directory is worth reading when you want to know how something is
normally done.

The difference is where it lives and what it is allowed to assume. Core modules
sit in `modules/` and always load. Extensions sit in `extensions/`, are
namespaced `Convoro\Extensions\`, and load only when they are installed and
enabled.

## The provider

```php
namespace Convoro\Extensions\MyThing;

use Convoro\Engine\Module\Module;

final class MyThing extends Module
{
    public function register(): void
    {
        // Runs before any module has booted. Bind services here. Do not
        // reach for another module's work yet — it may not exist.
    }

    public function boot(): void
    {
        // Runs once everything is registered. Hooks and event listeners go
        // here. $this->template(), $this->events() and $this->app are all
        // available.
    }
}
```

Modules boot in dependency order, so anything in `requires` has booted before
you do.

## The three things an extension can do

**Render into a page** with a [hook](hooks.md). Presentation only, and it is
handed the context of where it is rendering — the post, the topic, the member.

**React to something happening** with an [event](events.md). Typed classes,
dispatched by core after the fact. A listener observes; it never vetoes.

**Add its own routes, screens and tables.** Routes files are picked up
automatically, `Templates/` is registered under your key, and `Migrations/`
runs on install and in reverse on an uninstall that was asked to remove data.

## What is available

The container holds `config`, `logger`, `db`, `cache`, `events`, `router`,
`template`, `modules`, `session.driver`, `hasher`, `auth`, `gate` and
`extensions`. Reach them with `$this->app->make('db')`.

Extension controllers extend `Convoro\Engine\Http\Controller`, so `user()`,
`session()`, `isModerator()`, `settings()`, `seo()`, `render()`, `json()`,
`redirect()`, `back()` and `dispatch()` all work exactly as they do in core.

The query builder has `insertGetId()`, `insertBulk()`, `updateAll()`,
`deleteAll()`, `whereNull()`, `whereIn()`, `whereLike()`, `exists()` and
`count()`. There is no `insert()`.

## What is deliberately not possible

- **Overriding another module's template.** Hooks insert; they do not replace.
  Changing core markup is a theming concern, not an extension one.
- **Cancelling an event.** See [events.md](events.md).
- **Disabling a core module.** Turning off `member` is not a feature.

## Two things that will bite you

**Clear the template cache after editing a `.cvr`.** Compiled templates are
cached to disk and are not invalidated by mtime.

```bash
rm -f storage/templates/*.php
```

**Escape everything from the database before it reaches a hook's output.** A
hook returns HTML, so nothing escapes it for you.
