Skip to content

Boundary & Whitelist ​

Reference for the Moodle boundary of middag-io/moodle (middag-php-moodle): which code may call native Moodle APIs, what each wrapper is allowed to call, the files a consumer plugin must keep thin, and the current state of the enforcement tooling. Verified against middag-php-moodle main@6aae04d on 2026-11-04.

The rule ​

Only this adapter package calls Moodle APIs. A consumer (domain code, services, extensions, a product) reaches Moodle through the adapter: through a framework port the adapter implements (see Adapter Port Catalog), through a host domain service in Domain/<Subsystem>/, or through a *Support wrapper. The adapter itself never imports proprietary code and never carries a product identity: the component name reaches it at runtime (Config\ComponentContext, Runtime\MoodleHostContext).

Inside the package, direct Moodle calls are concentrated in Support/ (stateless wrappers, see Support Layer). The port implementations (Database/, Logging/, Mail/, Translation/, Filesystem/, Bus/, Http/, Runtime/), the host domain services (Domain/*/*Service), the settings DSL (Settings/), the form renderer (Form/MformRenderer) and the privacy provider (Privacy/) are also inside the boundary.

What counts as a violation ​

A violation is Moodle coupling outside the boundary:

  • an import of a Moodle namespace or type that does not go through the adapter: \core\*, \core_*, \mod_*, another plugin's \local_*, or a stdClass returned by a Moodle API;
  • a Moodle global ($DB, $CFG, $PAGE, $OUTPUT, $USER) used outside the boundary.

Pure PHP functions (intval, array_map, …) do not count.

Per-wrapper allowlist highlights ​

Every *Support class exposes only the Moodle calls it exists to wrap. Anything else against the same Moodle API, from outside the boundary, is the anti-pattern.

WrapperMoodle API it wrapsAnti-pattern
DbSupport$DB (getRecord, getRecords, insertRecord, updateRecord, deleteRecords, execute, delegated transactions, SQL helpers)global $DB in consumer code
CacheSupportcache::make() and the MUC API (get, set, getMany, setMany, delete, deleteMany, purge, getOrSet); CacheSupportPsr16 exposes the same store as PSR-16Calling cache::make() directly
ContextSupport\core\context\* (system(), course(), coursecat(), module(), user(), block(), instanceById())context_system::instance() in consumer code
CustomFieldSupport\core_customfield\handler and its field/data controllers; returns primitivesLeaking core_customfield types to consumers
CompetencySupport\core_competency\api (frameworks, competencies, user competencies, evidence)Calling \core_competency\api directly
TaskSupport\core\task\manager (scheduled and adhoc tasks)Queuing adhoc tasks through the manager directly
TimeSupporttime(), userdate(), usertime(), core_dateFormatting user dates with userdate() in consumer code
LockSupportlock_config / lock factory (closure-based execute())Acquiring locks without release on every path
CheckSupport\core\check\manager / \core\check\resultInstantiating check classes by hand
RouterBridgeSupportMoodle 5.1+ native router (isAvailable(), register(), proxyRequest()); see Routing BridgeManipulating r.php manually
DiBridgeSupport\core\hook\di_configuration, \core\di (outbound only); see DI BridgeFramework code calling \core\di::get() directly

The full class list is in Support Layer.

Two further allowances:

  • Port implementations and host domain services may call debugging() and the already-wrapped auth/capability APIs.
  • Definition classes (Definition/) and host enums express Moodle constants as typed values (for example ContextLevel::toMoodleValue(), CacheMode, CapabilityRisk). These are inert values, not behavior, so they need no wrapper.

Files a consumer plugin must keep thin ​

Moodle's plugin API mandates files at fixed locations of the consumer plugin. This adapter ships none of them. Each may touch Moodle directly because the plugin contract requires it, but must stay a thin adapter or callback that delegates, never business logic.

Consumer filePlugin-API obligationDelegates to (in this adapter)Anti-pattern
settings.phpAdmin settings entry pointSettings\SettingsResolver (see Settings & Config Keys)Defining admin_setting_* objects inline
lib.phpPlugin callbacks (navigation, pluginfile, …)Output\NavbarService, file-area handlers, Runtime\KernelCallback bodies that implement instead of delegate
db/install.php, db/upgrade.phpInstall and upgrade hooksPersistence\UpgradeHelper, Database\Schema\XmldbSchemaAdapterSchema or migration logic inline
classes/external/* (or a legacy external.php)External functionsWebService\AbstractExternal, generated db/services.phpService logic inside the external class; external.php as a router
classes/event/*, classes/task/*, classes/privacy/*Event, task and privacy APIsGenerated event classes, Bus\MoodleAdhocTransport, Privacy\PrivacyProviderA provider or task that implements instead of delegates

Mandatory plugin APIs are thin adapters ​

Moodle requires some APIs of every plugin that stores data or serves pages. The adapter implements each as a thin adapter that delegates to a contract; the consumer supplies the data and the storage.

Plugin APIAdapter pieceWhat the consumer provides
Privacy (GDPR)Privacy\PrivacyProvider implements Moodle's privacy provider and plugin\provider; it never runs SQL itselfA Privacy\Contract\PrivacyRepositoryInterface binding for its own tables, table metadata through PrivacyProvider::registerDataTable(), and optional extension providers
Privacy for third-party storagePrivacy\Contract\PrivacyProviderInterface (@api)Services tagged middag.privacy_provider in the container; PrivacyProvider calls each after the central repository, in the container's tag order, and skips tagged services that do not implement the interface
Backup and restoreDomain\Backup\Contract\BackupStepsProviderInterface (@api): getBackupSteps(), getRestoreSteps()One implementation per integration with its own storage; integrations that only use the central persistence need none. The import repository that rehydrates restored rows is downstream (see Import Repository)
PluginfileDefinition\FileAreaDefinition, Domain\File\Contract\FileAreaHandlerInterfaceThe pluginfile callback and the registry; see File Areas
NavigationOutput\NavbarService, Http\Inertia\InertiaSharedPropsThe lib.php navigation callbacks; see Navigation
Host page for the SPAHttp\Inertia\MoodleInertiaBootstrap, Support\ThemeSupportThe compiled frontend bundle; see Frontend Build & Theme Bridge

PrivacyProvider catches any exception from the repository or an extension provider and returns a partial result instead of failing the privacy request. Its four operations (contexts for a user, export, delete for all users in a context, delete for a user) are covered by tests/Privacy/PrivacyProviderCoverageTest.php, including tagged-provider discovery.

Static entry points ​

lib.php callbacks, Moodle hooks and cron run in procedural contexts that receive no dependency injection. The adapter therefore keeps a static, per-request kernel: Runtime\Kernel is a singleton (Kernel::init(), Kernel::get(), Kernel::handle(), Kernel::shutdown()) that compiles the container once per request. The static kernel assumes one consumer plugin built on this adapter per request: the Kernel instance and ComponentContext are process-wide slots, so two such plugins in one request share them and the last configuration wins. The WordPress adapter makes the opposite choice for the opposite reason (several plugins share one PHP process); see Adapter Port Catalog — host identity.

Facades over the support layer belong to the consumer, not to this package. Support\Moodle is a static aggregator that returns fresh *Support instances; generated facades live in the consumer plugin ({component}\facade\...).

User-facing text ​

All user-facing text resolves through language strings: the framework TranslatorInterface (implemented by Translation\MoodleTranslator, which maps to get_string()) or LangSupport. Hard-coded PHP strings are reserved for technical messages (debugging, error logs).

Enforcement state ​

The decision intends three complementary tools: an import-based check, a global-variable check, and a per-file refactoring exception list. As of main@6aae04d:

CheckState
No consumer-plugin or proprietary namespace, no product table literals in src/coded — tests/AdapterPluginIsolationTest.php (source scan, runs under composer test)
Every class autoloads without a Moodle runtime (documented host-only exclusions)coded — tests/ClassLoadabilityTest.php
Import-based boundary check (dependency-boundary tool over a "Moodle internal" layer)planned — no dependency-boundary tool is configured; Output/NavbarService.php mentions the intended layer in a docblock only
Moodle globals outside the boundary (custom static-analysis rule)planned — .phpstan.neon runs PHPStan level 6 with no custom rules (the intended rule was sketched as NoInternalImportsFromExtensionsRule)
Wrapper files exempted from refactoring rules that fight their repetitive shapeplanned — .php-rector.php skips whole rule classes and tests/phpstan, not individual wrapper files such as DbSupport.php
Consumer plugin files stay thinplanned — code review in the consumer

Why this is a reference page ​

The allowlist grows as Moodle subsystems get wrapped. The durable rule (the boundary, the thin-file obligation, the static entry point) lives in the decision record; per-API detail lives here so the record does not change every time a wrapper is added.


Decision record: MDL-001 in middag-php-moodle/docs/decisions.

MIDDAG © 2015-2026