Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/_utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
import React, { type ReactNode } from 'react';
import ComponentFileSummaryGeneric, {
type ComponentFileSummaryDescription,
type ComponentFileSummaryProps,
} from '@site/src/components/ComponentFileSummary';
import { MDXProvider } from '@mdx-js/react';
Expand All @@ -38,7 +39,7 @@ export const fillDefaultProps = (props: ComponentFileSummaryProps): ComponentFil
...props,
});

const normaliseDescription = (Value: ReactNode | string): null | JSX.Element => {
const normaliseDescription = (Value: ComponentFileSummaryDescription): null | React.JSX.Element => {
if (typeof Value === 'boolean' || !Value) {
return null;
}
Expand Down Expand Up @@ -70,7 +71,7 @@ export const getDescription = ({
description = null,
extraDescription = null,
children = null,
}: ComponentFileSummaryProps, defaultDescription?: ReactNode | string): null | ReactNode | JSX.Element => {
}: ComponentFileSummaryProps, defaultDescription?: ComponentFileSummaryDescription): null | ReactNode | JSX.Element => {
if (children) {
const Description = normaliseDescription(children);
return (
Expand Down
17 changes: 17 additions & 0 deletions docs/apis/_files/composer-json.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- markdownlint-disable first-line-heading -->
The `composer.json` file allows your plugin to be distributed and installed via [Composer](https://getcomposer.org/).

Moodle uses the [moodle/composer-installer](https://github.com/moodle/composer-installer) package to install Moodle plugins from Composer into the correct locations within the Moodle directory structure.

The most important fields for Moodle plugin compatibility are:

- `type`: the Moodle plugin type in the format `moodle-[plugintype]` (for example, `moodle-block`)
- `name`: the Composer package name, where the package component follows the format `moodle-[plugintype]_[pluginname]` (for example, `myplugin/moodle-block_myblock`)
- `require.moodle/moodle`: the supported Moodle version range for your plugin
- `require.moodle/composer-installer`: a production dependency so Moodle package types are installed into the correct locations

You can also declare dependencies on other Moodle plugins in `require`.

Composer runtime dependencies are only guaranteed when the plugin is installed via Composer. For now, avoid introducing Composer-only runtime dependencies that would break non-Composer plugin installs.

See the [Composer guide](../../guides/composer/index.md) for further information.
47 changes: 47 additions & 0 deletions docs/apis/_files/composer-json.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright (c) Moodle Pty Ltd.
*
* Moodle is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Moodle is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Moodle. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import { ComponentFileSummary, type ComponentFileSummaryProps } from '../../_utils';
import DefaultDescription from './composer-json.mdx';

const defaultExample = `{
"name": "myplugin/moodle-block_myblock",
"description": "A description of my Moodle block plugin",
"type": "moodle-block",
"require": {
"moodle/moodle": "^5.2",
"moodle/composer-installer": "*",
"abgreeve/moodle-block_stash": "^5.2"
},
"license": "GPL-3.0-or-later"
}`;

export default function ComposerJSON(props: ComponentFileSummaryProps): React.JSX.Element {
return (
<ComponentFileSummary
filepath="/composer.json"
filetype="json"
summary="Allows the plugin to be distributed and installed via Composer"
examplePurpose="Basic Composer package definition for a block plugin"
defaultDescription={DefaultDescription}
defaultExample={defaultExample}
showLicense={false}
showFileHeader={false}
{...props}
/>
);
}
2 changes: 2 additions & 0 deletions docs/apis/_files/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import AmdDir from './amd-dir';
import BackupDir from './backup-dir';
import ComposerJSON from './composer-json';
import CLIDir from './cli-dir';
import Changes from './changes';
import ClassesDir from './classes-dir';
Expand Down Expand Up @@ -49,6 +50,7 @@ import YUIDir from './yui-dir';
export {
AmdDir,
BackupDir,
ComposerJSON,
CLIDir,
Changes,
ClassesDir,
Expand Down
5 changes: 5 additions & 0 deletions docs/apis/commonfiles/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ tags:
import {
AmdDir,
BackupDir,
ComposerJSON,
CLIDir,
Changes,
ClassesDir,
Expand Down Expand Up @@ -140,6 +141,10 @@ import extraLangDescription from '../_files/lang-extra.md';

<ThirdpartylibsXML />

### composer.json

<ComposerJSON />

### readme_moodle.txt

<ReadmeMoodleTXT />
Expand Down
1 change: 1 addition & 0 deletions docs/guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Learn about key Moodle features for developers through our Developer Guides. The

- Introduction to [JavaScript](./guides/javascript/index.md) in Moodle
- Learn about how Moodle uses [Templates](./guides/templates/index.md) to render content
- Distribute and install Moodle plugins using [Composer](./guides/composer/index.md)
164 changes: 164 additions & 0 deletions docs/guides/composer/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
title: Composer support for plugins
tags:
- Composer
- Plugins
- Package management
description: How to make your Moodle plugin available via Composer, and developer tips for working with Composer-based Moodle sites.
---

<Since version="5.2" issueNumber="MDL-87473" />

Moodle 5.2 introduced native support for distributing and installing Moodle plugins using [Composer](https://getcomposer.org/), the PHP package manager. Plugin developers can publish their plugins to [Packagist](https://packagist.org/) and site administrators can install them with a simple `composer require` command.

The [moodle/composer-installer](https://github.com/moodle/composer-installer) Composer plugin handles placing each Moodle plugin package into the correct location within the Moodle directory structure, based on the plugin type declared in the package's `composer.json`.

## Making your plugin available via Composer

### Adding a composer.json

Add a `composer.json` file to the root of your plugin directory with at minimum the following fields:

- `name`: the Composer package name, where the package component follows the format `moodle-[plugintype]_[pluginname]` (for example, `myplugin/moodle-block_myblock`)
- `type`: the Moodle plugin type in the format `moodle-[plugintype]` (for example, `moodle-block`)
- `require`: the Moodle version requirement, to communicate which versions your plugin supports

In `require`, include a production dependency on `moodle/composer-installer`.

```json title="composer.json"
{
"name": "myplugin/moodle-block_myblock",
"description": "A description of my Moodle block plugin",
"type": "moodle-block",
"require": {
"moodle/moodle": "^5.2",
"moodle/composer-installer": "*"
},
"license": "GPL-3.0-or-later"
}
```

The `type` field is what the `moodle/composer-installer` package uses to determine the installation path. For example, a package with `"type": "moodle-block"` will be installed into `blocks/myblock/` within the Moodle directory.

:::tip Vendor name

The vendor prefix in the `name` field (for example, `myplugin`) is your Packagist vendor name and is independent of any Moodle conventions. The package name component (`moodle-block_myblock`) follows the Moodle convention.

:::

### Publishing to Packagist

Once your plugin has a valid `composer.json` and is in a public Git repository, you can publish it to [Packagist](https://packagist.org/):

1. Visit [https://packagist.org/packages/submit](https://packagist.org/packages/submit) and submit your repository URL.
2. Set up a [GitHub webhook](https://packagist.org/about#how-to-update-packages) to keep Packagist in sync whenever you push to your repository.

Once published, site administrators can install your plugin with:

```bash
composer require myplugin/moodle-block_myblock
```

The installer will automatically place the plugin into `blocks/myblock/` within their Moodle directory.

## Development tips

### Creating a development Moodle site

The [moodle/seed](https://github.com/moodle/seed) project provides a quick way to spin up a new Moodle site using Composer. This is particularly useful for plugin developers who want a reproducible development environment.

```bash
composer create-project moodle/seed [yourlocation]
```

The Moodle scaffolding tool will guide you through the initial site configuration. Within your `[yourlocation]` directory you will find:

- a `composer.json` and `composer.lock`
- a `vendor/` directory
- a `moodle/` directory containing your Moodle installation

To target a specific version of Moodle:

```bash
cd [yourlocation]
composer require "moodle/moodle:~5.2.0"
```

To install a plugin from Packagist:

```bash
cd [yourlocation]
composer require myplugin/moodle-block_myblock
```

### Developing a plugin with a local path repository

When working on your plugin locally, you can tell Composer to use your local checkout instead of downloading from Packagist by adding a `path` repository to your site's `composer.json`:

```json title="composer.json (Moodle site root)"
{
"repositories": [
{
"type": "path",
"url": "/path/to/your/local/moodle-block_myblock",
"options": {
"symlink": false
}
}
],
"require": {
"moodle/composer-installer": "*",
"myplugin/moodle-block_myblock": "*"
}
}
```

Do not use symlinked plugin paths with Moodle. Many PHP entry points in plugins (for example, `view.php` and `index.php`) include `config.php` using relative paths like `require_once('../../config.php')`. With symlinked plugins, PHP resolves the symlink target first, which can make those relative includes resolve outside your Moodle site.

Set `"symlink": false` so Composer mirrors (copies) the plugin into the Moodle tree instead of symlinking it. This avoids relative include path issues.

When developing with a mirrored path repository, re-run `composer update myplugin/moodle-block_myblock` (or remove and re-require the package) after local changes so the copied plugin is refreshed.

:::note

For local path repositories, use `"*"` or `"@dev"` in your `require` constraint so Composer can resolve your local development version regardless of tagged releases.

:::

### Declaring dependencies

:::caution Current recommendation

Moodle currently supports plugins installed both with Composer and without Composer. Composer-declared runtime dependencies are only guaranteed to be installed when the plugin itself is installed via Composer.

For now, avoid introducing Composer-only runtime dependencies that would break non-Composer plugin installs.

:::

When adding Composer metadata to your plugin, these dependency rules apply:

- You can declare dependencies on other Moodle plugins as Composer package requirements.
- You should declare a production dependency on `moodle/composer-installer` in `require` (not `require-dev`) so Moodle package types are installed into the correct locations.

```json title="composer.json with Moodle package dependencies"
{
"name": "myplugin/moodle-block_myblock",
"type": "moodle-block",
"require": {
"moodle/moodle": "^5.2",
"moodle/composer-installer": "*",
"abgreeve/moodle-block_stash": "^5.2"
},
"license": "GPL-3.0-or-later"
}
```

If your plugin has required Moodle plugin dependencies, continue to declare them in `version.php` too, so dependency checks also work for non-Composer installation workflows.

## See also

- [`composer.json`](../../apis/commonfiles/index.mdx#composerjson) — common file reference for Moodle plugins
- [moodle/composer-installer](https://github.com/moodle/composer-installer) — the Composer plugin that installs Moodle packages into the correct directory
- [moodle/seed](https://github.com/moodle/seed) — a Composer project template for spinning up a new Moodle site
- [Packagist](https://packagist.org/) — the main Composer package repository
- [Composer documentation](https://getcomposer.org/doc/) — full Composer reference
40 changes: 30 additions & 10 deletions src/components/ComponentFileSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
*/

/* eslint-disable react/no-unused-prop-types */
import React, { type ReactNode } from 'react';
import React, { type ComponentType, type ReactElement, type ReactNode } from 'react';
import Chip from '@mui/material/Chip';
import Tooltip from '@mui/material/Tooltip';
import Grid from '@mui/material/Grid';
import Details from '@theme/Details';
import { MDXProvider } from '@mdx-js/react';

const getBadge = (title, description, colour = 'info'): JSX.Element => (
type BadgeColour = 'info' | 'success' | 'warning' | 'error';

const getBadge = (title, description, colour: BadgeColour = 'info'): React.JSX.Element => (
<Grid item key={title}>
<Tooltip title={description}>
<Chip
Expand All @@ -40,7 +42,7 @@ function getBadges({
deprecated = false,
refreshedDuringUpgrade = false,
refreshedDuringPurge = false,
}): Array<typeof Grid> {
}): React.JSX.Element[] {
const badges = [];
if (refreshedDuringUpgrade) {
// This file is re-read during an upgrade and configuration will be re-applied.
Expand Down Expand Up @@ -109,14 +111,30 @@ function getExamples(props) {
return null;
}

export type ComponentFileSummaryDescription = string | ReactElement | ComponentType;

const normaliseDescription = (value?: ComponentFileSummaryDescription): null | ReactNode => {
if (typeof value === 'boolean' || !value) {
return null;
}

if (typeof value === 'string' || React.isValidElement(value)) {
return value;
}

const Description = value;

return <Description />;
};

export interface ComponentFileSummaryProps {
description?: string | ReactNode,
defaultDescription?: string | ReactNode,
description?: ComponentFileSummaryDescription,
defaultDescription?: ComponentFileSummaryDescription,
defaultExample?: string | ReactNode,
example?: string | ReactNode | JSX.Element,
example?: string | ReactNode,
exampleFilepath?: string,
examplePurpose?: string,
extraDescription?: string,
extraDescription?: ComponentFileSummaryDescription,
filepath?: string,
filetype?: string,
modulename?: string,
Expand All @@ -132,7 +150,7 @@ export interface ComponentFileSummaryProps {
refreshedDuringPurge?: boolean,
}

export default function ComponentFileSummary(props: ComponentFileSummaryProps): JSX.Element {
export default function ComponentFileSummary(props: ComponentFileSummaryProps): React.JSX.Element {
const {
filepath,
summary,
Expand All @@ -141,10 +159,12 @@ export default function ComponentFileSummary(props: ComponentFileSummaryProps):
const badges = getBadges(props);

const description = (() => {
if (props.description) {
const content = normaliseDescription(props.description);

if (content) {
return (
<Grid item xs={12}>
{props.description}
{content}
</Grid>
);
}
Expand Down
Loading