Skip to content

Pluginfile Routing & File Areas Reference

Reference for how the Moodle adapter (middag-io/middag-php-moodle) routes pluginfile.php requests to file areas through a typed registry and handler contract, instead of a hardcoded switch/case. Backs MDL-008.

Declaring a file area

php
new file_area(
    name: 'attachments',
    description: 'File attachments for items',
    context_level: context_level::SYSTEM,
    handler: attachments_file_area_handler::class,
    supports_preview: true,
)

Extensions declare file areas via get_file_area_definitions(); external plugins add more through the extend_local_middag_file_areas hook.

FieldTypePurpose
namestringArea identifier — the {filearea} segment of the pluginfile.php URL.
descriptionstringHuman-readable label, used by admin/debug tooling.
context_levelcontext_level enumMoodle context level the file area is scoped to.
handlerclass-string<file_area_handler_interface> | nullHandler class; null resolves to default_file_area_handler.
supports_previewboolWhether the file area supports Moodle's built-in file preview UI.

Naming convention

Mirrors the capability naming convention used across the adapter's static-generation pipeline:

OwnerPatternExample
core extensionno prefixattachments
Other internal extensions{slug}_{area}customdocs_templates
External plugins{plugin}_{slug}_{area}middagpro_premium_reports_exports

Handler contract

file_area_handler_interface is Group A — stable public API:

MethodSignatureResponsibility
can_accesscan_access(context $context, string $filearea, int $itemid, string $filepath, string $filename): boolAuthorization — must return false for anything the caller can't legitimately reach.
serveserve(stored_file $file, bool $forcedownload): voidDelivery — must call send_stored_file(), never send_file().

When a file_area definition omits handler, the framework resolves default_file_area_handler in its place.

Request flow

Browser
  -> /pluginfile.php/{contextid}/local_middag/{filearea}/...
  -> local_middag_pluginfile()
  -> resolve handler from the registry (owning extension -> container)
  -> handler::can_access()
  -> handler::serve()

The registry lookup happens at request time on every call — it is not build-time generated, unlike db/services.php for external services. build_statics:fileareas may generate documentation/validation artifacts, but never the routing or resolution itself.

file_support method inventory

MethodPurpose
get_fileFetch a single stored file.
get_area_filesFetch all files in a file area.
create_file_from_stringCreate a stored file from raw string content.
create_file_from_pathnameCreate a stored file from a filesystem path.
create_file_from_storedfileCopy an existing stored file.
delete_fileDelete a single stored file.
delete_area_filesDelete every file in a file area.
get_file_urlBuild a download URL for a stored file.
has_filesCheck whether a file area has any files.
get_area_sizeTotal size (bytes) of a file area.
count_area_filesCount files in a file area.
is_valid_imageValidate that a stored file is a usable image.

url_support::pluginfile(...) builds the full pluginfile.php download URL with the correct signature for a given context/component/filearea/itemid/filepath/filename tuple.

Anti-patterns

Anti-patternWhy it's wrong
can_access() always returning trueAny user can access any file — defeats the purpose of a per-area authorization hook.
Business logic inside the handlerHandlers should delegate to the domain layer, not implement business rules themselves.
send_file() instead of send_stored_file()Bypasses the File API's own headers/cache handling.
Not declaring a file area at alllocal_middag_pluginfile() finds no handler for the area and the request fails.

MIDDAG © 2015-2026