Skip to content

Commit d67bdd1

Browse files
committed
Extensions: Plugin structure + commands improvements
1 parent ac5a583 commit d67bdd1

File tree

4 files changed

+112
-64
lines changed

4 files changed

+112
-64
lines changed

docs/.vuepress/placeholders.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ module.exports = {
1414
"mynamespace": { title: "Replace this with your actual namespace." },
1515
"mytablename": { title: "Replace this with your database table name, without any prefix." },
1616
"path/to/my-project": { title: "Replace this with your project path." },
17+
"package/name": { title: "Replace this with the name of your published package." },
1718
};

docs/5.x/extend/commands.md

+20-11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
sidebarDepth: 2
3+
---
4+
15
# Console Commands
26

37
Plugins and modules can add additional [console commands](../reference/cli.md)
@@ -11,10 +15,9 @@ For the most part, writing console commands for Craft is identical to writing co
1115

1216
## Module Setup
1317

14-
If you are adding console commands to a custom module, make sure that your module class defines its root `controllerNamespace` for console requests:
18+
Plugins are [automatically configured](plugin-guide.md#automatic-defaults) to locate web and console controllers. Modules, on the other hand, must explicitly define their root `controllerNamespace` for both request types:
1519

16-
```php{14,15}
17-
<?php
20+
```php{13-17}
1821
namespace acme;
1922
2023
use Craft;
@@ -23,25 +26,25 @@ class Module extends \yii\base\Module
2326
{
2427
public function init()
2528
{
26-
// Define a custom alias named after the namespace
29+
// Define a custom alias named after the namespace:
2730
Craft::setAlias('@acme', __DIR__);
2831
29-
// Set the controllerNamespace based on whether this is a console or web request
32+
// Set the controllerNamespace based on whether this is a console or web request:
3033
if (Craft::$app->getRequest()->getIsConsoleRequest()) {
3134
$this->controllerNamespace = 'acme\\console\\controllers';
3235
} else {
3336
$this->controllerNamespace = 'acme\\controllers';
3437
}
3538
39+
// Call init() *after* setting the `controllerNamespace` so Yii doesn’t try and set it:
3640
parent::init();
3741
38-
// Custom initialization code goes here...
42+
// Additional custom initialization code goes here...
3943
}
4044
}
4145
```
4246

43-
You’ll also need to make sure your module is getting [bootstrapped](guide:runtime-bootstrapping)
44-
from `config/app.php` (or `config/app.console.php`):
47+
You’ll also need to make sure your module is getting [bootstrapped](guide:runtime-bootstrapping) on every request from `config/app.php` (or `config/app.console.php`):
4548

4649
```php{2}
4750
return [
@@ -59,8 +62,6 @@ Any classes within the folder corresponding to your [`controllerNamespace` setti
5962
Create `GreetController.php` in `modules/console/controllers/`, with this content:
6063

6164
```php
62-
<?php
63-
6465
namespace modules\console\controllers;
6566

6667
use craft\console\Controller;
@@ -112,7 +113,7 @@ class GreetController extends Controller
112113

113114
### Running Actions
114115

115-
Supposing your module ID or plugin handle was `acme`, you would access your controller like this:
116+
Supposing your [module ID](module-guide.md#preparation) or plugin handle was `acme`, you would access your controller like this:
116117

117118
```bash
118119
# Run the "default action":
@@ -128,6 +129,14 @@ php craft acme/greet/developer --who="Marvin"
128129
# -> Hello, Marvin!
129130
```
130131

132+
### Options
133+
134+
In the example, we’ve declared a public class property, and set up a mapping for the single _action ID_. Any [option](guide:tutorial-console#options) can be set using bash’s familiar `--kebab-case-option-name` syntax.
135+
136+
::: tip
137+
Use the [`optionAliases()` method](guide:tutorial-console#options-aliases) to define shorthand options like `-m`.
138+
:::
139+
131140
### Arguments
132141

133142
Actions can declare arguments that will be processed from the command’s input. Arguments are separated by spaces, and the values are processed according to their declared types:

docs/5.x/extend/plugin-guide.md

+91-53
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Create a `composer.json` file at the root of your plugin directory, and use this
100100
}
101101
```
102102

103-
Replace:
103+
Review and replace the following values:
104104

105105
- `package/name` with your package name.
106106
- `Developer Name` with your name, or the organization name that the plugin should be attributed to.
@@ -111,24 +111,22 @@ Replace:
111111
- `namespace\\prefix\\` with your namespace prefix. (Use double-backslashes because this is JSON, and note this must end with `\\`.)
112112
- `Plugin Name` with your plugin name.
113113
- `my-plugin-handle` with your plugin handle.
114-
- `MIT` with `proprietary` if you plan to use [Craft License](https://craftcms.github.io/license/) (see [Choose a License](plugin-store.md#choose-a-license) on the “Publishing to the Plugin Store” page).
114+
- `MIT` with `proprietary` if you plan to use [Craft License](https://craftcms.github.io/license/). Read more about [choosing a license](plugin-store.md#choose-a-license).
115115

116-
In addition to `name` and `handle` (which are both required), there are a few other things you can include in that `extra` object:
116+
In addition to `name` and `handle` (both of which are required), there are a few other things you can include under the `extra` key:
117117

118-
- `class` The [Plugin class](#the-plugin-class) name. If not set, the installer will look for a `Plugin.php` file at each of the `autoload` path roots.
119-
- `description` – The plugin description. If not set, the main `description` property will be used.
120-
- `developer` The developer name. If not set, the first author’s `name` will be used (via the `authors` property).
121-
- `developerUrl` The developer URL. If not set, the `homepage` property will be used, or the first author’s `homepage` (via the `authors` property).
122-
- `developerEmail` The support email. If not set, the `support.email` property will be used.
123-
- `documentationUrl` The plugin’s documentation URL. If not set, the `support.docs` property will be used.
118+
- `class` The [main plugin class](#the-plugin-class) name. If not set, the installer will look for a `Plugin.php` file at the root of each `autoload` path.
119+
- `description` — A brief description of your plugin’s purpose. If not set, the main `description` property will be used.
120+
- `developer` The developer name. If not set, the first author’s `name` will be used (via the `authors` property).
121+
- `developerUrl` The developer URL. If not set, the `homepage` property will be used, or the first author’s `homepage` (via the `authors` property).
122+
- `developerEmail` The support email. If not set, the `support.email` property will be used.
123+
- `documentationUrl` The plugin’s documentation URL. If not set, the `support.docs` property will be used.
124124

125-
::: warning
126-
If you’re updating a Craft 2 plugin, make sure to remove the `composer/installers` dependency if it has one.
127-
:::
125+
Some of these values are displayed in the Craft control panel (in <Journey path="Settings, Plugins" />—[see below](#plugin-icons) for an example) and used to auto-populate a handful of fields when you begin the Plugin Store submission process. We encourage you to provide a more thorough description—or update its details any time thereafter—via [Craft Console](kb:what-is-craft-console)—descriptions and URLs are _not_ synchronized again.
128126

129127
## The Plugin Class
130128

131-
The `src/Plugin.php` file is your plugin’s entry point for the system. It will get instantiated at the beginning of every request. Its `init()` method is the best place to register event listeners, and any other steps it needs to take to initialize itself.
129+
The `src/Plugin.php` file is your plugin’s entry point for the system. Craft instantiates a singleton of your plugin class at the beginning of every request, [invoking its `init()` method](#initialization). This is the best place to register [event listeners](events.md), and perform any other setup steps.
132130

133131
Use this template as a starting point for your `Plugin.php` file:
134132

@@ -146,6 +144,17 @@ class Plugin extends \craft\base\Plugin
146144
}
147145
```
148146

147+
::: warning
148+
Don’t move or rename this class or file after [publishing](plugin-store.md) a plugin. Craft stores references to its fully-resolved class name in configuration, and
149+
:::
150+
151+
### Automatic Defaults
152+
153+
Plugins are automatically given a few key features to simplify setup and provide a consistent developer experience:
154+
155+
- An [alias](../configure.md#aliases) is registered, corresponding to each autoloading namespace. If your plugin’s root namespace was `acmelabs\mousetrap`, Craft would create `@acmelabs\mousetrap`.
156+
- The plugin’s `controllerNamespace` is configured such that web requests are served by [controllers](controllers.md) in a `controllers/` directory adjacent to the main plugin file, and [console commands](commands.md) are resolved using classes in `console/controllers/`.
157+
149158
### Initialization
150159

151160
Most initialization logic belongs in your plugin’s `init()` method. However, there are some situations in which parts of the application aren’t ready yet (like another plugin)—in particular, creating [element queries](../development/element-queries.md) or causing the [Twig environment](../development/twig.md) to be loaded prematurely can result in race conditions and incomplete initialization.
@@ -161,7 +170,10 @@ class Plugin extends \craft\base\Plugin
161170
{
162171
public function init(): void
163172
{
164-
// ...
173+
// Always let the parent init() method run, first:
174+
parent::init();
175+
176+
// Set up critical components + features...
165177

166178
// Defer some setup tasks until Craft is fully initialized:
167179
Craft::$app->onInit(function() {
@@ -179,13 +191,55 @@ If Craft has already fully initialized, your callback will be invoked immediatel
179191

180192
Conversely, there are cases in which attaching [event listeners](events.md) in `onInit()` may be _too late_—by the time your callback is invoked, those events may have already happened.
181193

194+
### Accessing your Plugin
195+
196+
When you need a reference to your main plugin class (say, from a [controller](controllers.md) or [service](services.md)), use the static instance getter:
197+
198+
```php
199+
use mynamespace\Plugin as MyPlugin;
200+
201+
$pluginInstance = MyPlugin::getInstance();
202+
```
203+
204+
Each time you call this, the same “singleton” will be returned, so anything you’ve memoized on the class (say, as private properties) will be preserved.
205+
206+
::: warning
207+
You should never need to create additional instances of your plugin!
208+
:::
209+
210+
### Components and Getters
211+
212+
As your plugin’s capabilities grow, you may want to organize functionality into [services](services.md). Yii is able to resolve any services you configure via your plugin’s [setComponents()](yii2:yii\base\Component::setComponents()) method, but some [IDE](README.md#ide)s are aided by [`@property` docblock tags](https://docs.phpdoc.org/guide/references/phpdoc/tags/property.html) or explicit [component getters](services.md#component-getters).
213+
182214
## Loading your plugin into a Craft project
183215

184-
To get Craft to see your plugin, you will need to install it as a Composer dependency of your Craft project. There are multiple ways to do that:
216+
When you scaffold a new plugin with the [generator](#scaffolding), Craft takes care of connecting the project’s `composer.json` to the new local directory using a [path repository](#path-repository). You are free to continue local development like this, indefinitely—but it’s important to understand how other developers might install your plugin, later!
217+
218+
Once published, other developers can [install your plugin via Packagist](#packagist). These steps are identical for any plugin in the [Plugin Store](plugin-store.md).
219+
220+
### Packagist
221+
222+
If you’re ready to [publicly release](plugin-store.md) your plugin, register it as a new Composer package on [Packagist](https://packagist.org/). Other developers can install it like any other package, by passing its name to Composer’s `require` command:
223+
224+
```bash
225+
# go to the project directory
226+
cd /path/to/my-project
227+
228+
# require the plugin package
229+
composer require package/name
230+
```
231+
232+
DDEV users have access to this shortcut when interacting with Composer:
233+
234+
```bash
235+
ddev composer require package/name
236+
```
237+
238+
<See path="plugin-store.md" label="Publishing to the Plugin Store" description="Get your plugin ready to publish in the official Plugin Store!" />
185239

186240
### Path Repository
187241

188-
During development, the easiest way to work on your plugin is with a [path repository][path], which will tell Composer to symlink your plugin into the `vendor/` folder right alongside other dependencies.
242+
During development, the easiest way to work on your plugin is with a [path repository][path]. This allows Composer to _symbolically link_ your plugin into the `vendor/` folder, right alongside other dependencies.
189243

190244
To set it up, open your Craft project’s `composer.json` file and make the following changes:
191245

@@ -200,59 +254,52 @@ To set it up, open your Craft project’s `composer.json` file and make the foll
200254
"repositories": [
201255
{
202256
"type": "path",
203-
"url": "../my-plugin"
257+
"url": "./local/my-plugin"
204258
}
205259
]
206260
}
207261
```
208262

263+
Set the `url` value to the absolute or relative path to your plugin’s source directory. The `./local/my-plugin` example above assumes that the plugin was cloned into a directory named `local/`, which exists alongside `composer.json`.
264+
209265
::: tip
210-
Set the `url` value to the absolute or relative path to your plugin’s source directory. (The `../my-plugin` example value assumes that the plugin lives in a folder alongside the project’s folder.)
266+
If you are using a containerized development environment like DDEV, Composer may not be able to see directories outside the current project! You can either clone another copy of the repository into the project, or configure a new mount by creating `./config/docker-compose.plugin-mount.yml`:
267+
268+
```yml
269+
services:
270+
web:
271+
volumes:
272+
- "$HOME/Developer/my-plugin:/home/shared/my-plugin"
273+
```
274+
275+
This will be merged with DDEV’s main web container config; you can then replace the path repository’s `url` with the absolute path `/home/shared/my-plugin`.
211276
:::
212277

213-
In your terminal, go to your Craft project and tell Composer to require your plugin. (Use the same package name you gave your plugin in its `composer.json` file.)
278+
In a terminal, go to your Craft project and require the plugin using the same package name you gave your plugin in its `composer.json` file:
214279

215280
```bash
216-
# go to the project directory
217-
cd /path/to/my-project
218-
219-
# require the plugin package
220281
composer require package/name
221282
```
222283

223-
Composer’s installation log should indicate that the package was installed via a symlink:
284+
Composer’s output should show that the package was installed via a symlink:
224285

225286
```
226-
- Installing package/name (X.Y.Z): Symlinking from ../my-plugin
287+
- Installing package/name (X.Y.Z): Symlinking from ./local/my-plugin
227288
```
228289

229290
::: warning
230-
One caveat of `path` Composer repositories is that Composer may ignore `path`-based dependencies when you run `composer update`. So any time you change anything in `composer.json`, such as your plugin’s dependency requirements or its plugin information, you might need to completely remove and re-require your plugin in your project for those changes to take effect.
291+
One caveat of `path` Composer repositories is that Composer may ignore `path`-based dependencies when you run `composer update`. So any time you change anything in a plugin’s `composer.json` (like its dependencies or [extras](#composer-json)), you may need to completely remove and re-require your plugin for those changes to take effect:
231292

232293
```bash
233-
# go to the project directory
234-
cd /path/to/my-project
235-
236-
# remove the plugin package
294+
# Remove the plugin package from your project:
237295
composer remove package/name
238296
239-
# re-require the plugin package
240-
composer require package/name
297+
# Re-require the plugin package, and allow updates to dependencies:
298+
composer require package/name -w
241299
```
242-
243300
:::
244301

245-
### Packagist
246-
247-
If you’re ready to publicly release your plugin, register it as a new Composer package on [Packagist](https://packagist.org/). Then you can install it like any other package, by just passing its package name to Composer’s `require` command.
248-
249-
```bash
250-
# go to the project directory
251-
cd /path/to/my-project
252-
253-
# require the plugin package
254-
composer require package/name
255-
```
302+
This same process can be used to fork, clone, and contribute to any open-source or source-available plugin!
256303

257304
## Plugin Icons
258305

@@ -262,19 +309,10 @@ Plugins can provide an icon, which will be visible on the **Settings** → **Plu
262309
<img src="../images/plugin-index.png" alt="Screenshot of control panel Settings → Plugins">
263310
</BrowserShot>
264311

265-
266312
Plugin icons must be square SVG files, saved as `icon.svg` at the root of your plugin’s source directory (e.g `src/`).
267313

268314
If your plugin has a [control panel section](cp-section.md), you can also give its global nav item a custom icon by saving an `icon-mask.svg` file in the root of your plugin’s source directory. Note that this icon cannot contain strokes, and will always be displayed in a solid color (respecting alpha transparency).
269315

270-
[yii modules]: https://www.yiiframework.com/doc/guide/2.0/en/structure-modules
271-
[models]: https://www.yiiframework.com/doc/guide/2.0/en/structure-models
272-
[active record classes]: https://www.yiiframework.com/doc/guide/2.0/en/db-active-record
273-
[controllers]: https://www.yiiframework.com/doc/guide/2.0/en/structure-controllers
274-
[application components]: https://www.yiiframework.com/doc/guide/2.0/en/structure-application-components
275316
[package name]: https://getcomposer.org/doc/04-schema.md#name
276-
[two hardest things]: https://twitter.com/codinghorror/status/506010907021828096
277317
[psr-4]: https://www.php-fig.org/psr/psr-4/
278-
[yii alias]: https://www.yiiframework.com/doc/guide/2.0/en/concept-aliases
279-
[component configs]: https://www.yiiframework.com/doc/guide/2.0/en/structure-application-components
280318
[path]: https://getcomposer.org/doc/05-repositories.md#path

docs/5.x/images/plugin-index.png

140 KB
Loading

0 commit comments

Comments
 (0)