Contensio logo

FAQ

Common questions about building plugins — answered.

General

Do I need to fork Contensio to build a plugin?

No. Ever. A plugin is a separate Composer package that Contensio loads at runtime. You never touch core files.

What PHP version is required?

PHP 8.3+. Contensio itself requires 8.3, so your plugin must too.

Can a plugin depend on other Composer packages?

Yes — declare them in your composer.json require block. They'll be installed alongside your plugin. If your plugin ends up installed via ZIP upload, the admin's installer walks the tree and pulls deps automatically (when that feature lands — currently Composer install is the cleaner path for dep-heavy plugins).

Can a plugin depend on another plugin?

Yes, via standard Composer require. But be aware — if the user hasn't installed/enabled your dependency, your plugin won't work. Check in your service provider's boot() and emit a clear error.

Hooks & UI

How do I add a new hook point to core?

Currently, new hook points are added to core itself — open an issue or PR at github.com/contensio/contensio. A plugin-contributed hooks API is on the roadmap for 1.x but not in 1.0.

My hook callback isn't running.

Check:

  1. The plugin is enabled (not just installed).
  2. The Hook line is in boot(), not register().
  3. You're using Contensio\Cms\Support\Hook — not a different Hook class.
  4. The hook key name is exactly right — login.after_form, not login.afterForm.

Can I run JavaScript when a hook fires?

Not directly — hooks are server-side, they render HTML strings. But your HTML can include a <script> tag, or reference an asset you've published, and it runs when the page loads.

Routes & views

My plugin's route returns 404.

Three possibilities:

  1. Your service provider isn't loading routes/web.php — add $this->loadRoutesFrom(...) in boot().
  2. The plugin is disabled.
  3. Route caching — run php artisan route:clear (Contensio clears it automatically on enable/disable, but if you edit routes.php while the plugin is running, you might hit a stale cache).

Can my plugin override a core view?

Yes, via Laravel's view-hints mechanism. In your service provider:

$this->loadViewsFrom(__DIR__ . '/../resources/views/overrides', 'cms');

This registers your path under the cms namespace, so view('cms::admin.login') tries your override first. Use this sparingly — it's fragile across core updates.

What if two plugins try to override the same core view?

Last-registered wins. Load order depends on plugin discovery order — unreliable. If two plugins need to contribute to the same view, they should both use hooks, not overrides.

Database & migrations

Can I drop a column in an update migration?

Yes. Ship a new migration with Schema::table('awesome_items', fn ($t) => $t->dropColumn('foo'));. It'll run when the user updates your plugin.

My plugin's down() migration deleted user data on disable!

down() migrations don't run on disable — only if the user manually calls php artisan migrate:rollback. But yes, be careful what you put in down(). Most plugins can leave it as a no-op (Schema::dropIfExists(...)) since disabling doesn't trigger it anyway.

How do I migrate data (not just schema) between versions?

Write a migration that does the data change. Laravel allows any SQL or Eloquent calls inside a migration's up().

Settings & config

Where should I store plugin settings?

Use Contensio's settings table — it has a module column perfect for scoping:

Setting::updateOrCreate(
    ['module' => 'awesome', 'setting_key' => 'api_key'],
    ['value' => $newKey, 'updated_at' => now()]
);

This way your settings live alongside core settings, and your plugin's uninstall cleanup can DELETE FROM settings WHERE module = 'awesome' in one query.

Can my plugin read/write core settings?

Read: yes. Write: don't, unless the user explicitly asked for it. Core settings are user intent — don't mutate them from a plugin.

Multilingual

How does my plugin handle translations?

Two options:

  1. UI strings (buttons, labels) — load under a namespace:

    $this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'awesome');
    

    Then: __('awesome::awesome.save').

  2. Content (rich per-locale text) — create a translations table keyed by language_id, matching Contensio's core pattern (content_translations, menu_translations, etc.).

Can my plugin add a new language to the site?

No — languages are managed by admins in Configuration → Languages. A plugin shouldn't add a language without explicit user action.

Publishing & distribution

Can I ship a plugin as a private package?

Yes. Use Private Packagist, Satis, or a GitHub repo with a deploy key. Contensio doesn't care where the code comes from.

Can I charge for a plugin?

The license is yours to decide, as long as it's compatible with AGPL-3.0. Copyleft licenses technically require source distribution; how you monetize that is up to you (support, hosting, updates).

Should I use a monorepo or separate repos per plugin?

Separate repos, one per plugin. Packagist auto-sync is per-repo. Monorepos work but add complexity (Composer path repositories, manual tagging).

Testing

Is there a way to test my plugin in isolation?

The easiest path: create a fresh Laravel app, composer require your plugin (pointing at a local path during development), enable it, and test manually.

A plugin test-harness package is on the roadmap — for now, integration tests against a live Contensio install are the pragmatic approach.

Still stuck?