Imported from Viostream/drupal-viostream-plugin (
AGENTS.md). Install upstream withnpx skills add Viostream/drupal-viostream-plugin. Copyright stays with the author.
AGENTS.md — Coding Agent Guide for govcms-viostream-plugin
Drupal module providing Viostream video integration (field formatter, field widget, text filter, CKEditor 5 plugin, API client service, admin settings form, media browser controller). Targets Drupal 10 and 11 only (^10 || ^11). PHP >=8.1.
Build & Test Commands
No local PHP/Composer — all commands run via Docker.
# Run the full test suite (145 tests):
docker run --rm -v "$(pwd)":/app -w /app php:8.3-cli php vendor/bin/phpunit 2>&1
# Run a single test file:
docker run --rm -v "$(pwd)":/app -w /app php:8.3-cli php vendor/bin/phpunit tests/src/Unit/ViostreamClientTest.php 2>&1
# Run a single test method:
docker run --rm -v "$(pwd)":/app -w /app php:8.3-cli php vendor/bin/phpunit --filter testMethodName 2>&1
# Run with coverage (installs pcov each time; container is ephemeral):
docker run --rm -v "$(pwd)":/app -w /app php:8.3-cli sh -c \
"pecl install pcov && php -dextension=pcov.so -dpcov.enabled=1 vendor/bin/phpunit --coverage-text 2>&1"
# Install dependencies (requires --ignore-platform-reqs because gd is missing):
docker run --rm -v "$(pwd)":/app -w /app composer:2 install --ignore-platform-reqs
No linting, static analysis, or CI pipeline is configured. No composer test script exists.
Frontend JS Rebuild Workflow
Whenever you make changes to the Viostream CKEditor 5 plugin source JavaScript (js/ckeditor5_plugins/viostreamVideo/src/), you must re-run the JS build before committing:
cd js/ckeditor5_plugins/viostreamVideo
npm install # if not already
npm run build
This regenerates build/viostreamVideo.js, which is what Drupal and CKEditor load.
Critical: Docker builds DO NOT automatically rebuild the plugin JS. If you do not run the build step, your code/bugfixes will not appear in the browser, and you risk committing stale or broken plugin code. Always bundle before commit and verify the change is live!
Pre-commit Build Hook (Automatic Safety)
A pre-commit git hook at .git/hooks/pre-commit automatically runs the JS build and blocks any commit if build/viostreamVideo.js is out of date. If the build changes the bundle, you must git add js/ckeditor5_plugins/viostreamVideo/build/viostreamVideo.js and commit again. This guarantees you can never commit stale or mismatched frontend code.
Project Layout
src/
Client/ViostreamClient.php # API client service (Guzzle + Basic auth)
Controller/ViostreamMediaBrowserController.php
Form/ViostreamSettingsForm.php # ConfigFormBase
Plugin/
CKEditor5Plugin/ViostreamVideo.php
Field/FieldFormatter/ViostreamFormatter.php
Field/FieldWidget/ViostreamBrowserWidget.php
Filter/ViostreamVideoFilter.php
tests/
bootstrap.php # PSR-4 namespaces + Drupal container stubs
src/Unit/ # All test files (one per source class)
js/ # Browser JS (ES5) + CKEditor 5 plugin (built)
Code Style
PHP — Drupal Coding Standards
Follow Drupal coding standards. Key conventions observed in this codebase:
- Boolean/null constants: Always uppercase:
TRUE,FALSE,NULL. - Indentation: 2 spaces (PHP, YAML, JS). No tabs.
- Braces: Opening brace on same line for functions/methods, new line for classes.
- Line length: Soft limit ~80 chars. No hard enforcement.
- String quotes: Single quotes unless interpolation is needed.
Naming Conventions
- Classes: PascalCase, prefixed with
Viostream(e.g.,ViostreamClient). - Methods: camelCase (e.g.,
getMediaDetail,extractVideoId). - Properties: camelCase (e.g.,
$httpClient,$viostreamClient). - Constructor parameters: snake_case per Drupal convention (e.g.,
$http_client,$config_factory). - Local variables: snake_case (e.g.,
$video_id,$access_key,$embed_url). - Constants: UPPER_SNAKE_CASE (e.g.,
API_BASE_URL). - Config keys: snake_case (e.g.,
'access_key').
Imports (use statements)
Single block, no blank lines between groups. Order: Drupal core, then module classes, then third-party (Guzzle, Symfony, Psr). Alphabetical within each group.
Type Declarations
- Properties: Declared without native types; use
@vardocblock annotation. Alwaysprotectedvisibility. - Return types: Generally omitted from method signatures (Drupal 10 convention). Exceptions for interface-required signatures.
- Parameters: Typed when practical (
array,string, interface types). Nullable via default= NULL, not?Type. - No constructor property promotion. Assign explicitly in the constructor body.
PHPDoc
Every class, property, and non-trivial method must have a docblock.
- Class: Short description. Optional longer paragraph.
- Property: Description line + blank line +
@var \Full\Qualified\ClassName. - Method: Description + blank line +
@param/@returnwith FQN types and 2-space-indented descriptions. - Overrides: Use
{@inheritdoc}only, no extra description. - Plugin annotations: Standard Drupal
@FieldFormatter,@FieldWidget,@Filterannotations with@Translation()for labels.
Error Handling
- The API client returns
NULLon failure, never throws exceptions to callers. - Guzzle exceptions are caught internally and logged via Drupal's logger (
$this->logger->error(...)). - Log messages use Drupal placeholders:
'@status','@endpoint','@message'. - Controllers return
JsonResponsewith HTTP status codes (403, 404, 500) on error.
Dependency Injection
- Constructor injection everywhere. No service locator calls except
\Drupal::httpClient()in the AJAX callback. - Plugins implement
ContainerFactoryPluginInterface::create(). - Controllers/forms use
ContainerInjectionInterface::create(). - Services defined in
viostream.services.yml.
JavaScript (js/)
- ES5 only:
var, namedfunctionexpressions,+concatenation. No arrow functions or template literals. - Drupal behavior pattern:
Drupal.behaviors.name = { attach: function(context) { ... } }. - IIFE wrapper:
(function (Drupal, drupalSettings) { 'use strict'; ... })(Drupal, drupalSettings);. - Initialization guard via
datasetattributes to prevent double-binding. - XSS: use
escapeHtml()/escapeAttr()helpers, never rawinnerHTMLwith user data.
Testing Conventions
Structure
- Pure PHPUnit unit tests — extend
PHPUnit\Framework\TestCase, NOT Drupal'sUnitTestCase. - Test namespace:
Drupal\Tests\viostream\Unit. - One test class per source class, named
{ClassName}Test.php. - Class annotations:
@coversDefaultClass+@group viostream. - Method annotations:
@covers ::methodNameon every test. - All test methods return
voidwith explicit return type.
setUp Pattern
protected function setUp(): void {
parent::setUp();
$this->dependency = $this->createMock(InterfaceClass::class);
$this->sut = new ClassUnderTest($this->dependency);
// For classes using $this->t():
$translation = $this->createMock(TranslationInterface::class);
$translation->method('translateString')
->willReturnCallback(fn(TranslatableMarkup $m) => $m->getUntranslatedString());
$this->sut->setStringTranslation($translation);
}
Mocking
$this->createMock()for dependencies with expectations;$this->createStub()for simple return values.- Config mocks: use
willReturnMap([['key', $value], ...]). - Field item mocks: configure
__getviawillReturnMap, NOT property assignment (mock intercepts__set). - FieldItemListInterface mocks: configure Iterator methods (
rewind,valid,current,key,next) andoffsetExists(TRUE). - Container services: swap via
\Drupal::getContainer()->set('service_name', $mock).
Assertions
Prefer assertSame() (strict) over assertEquals(). Use assertStringContainsString(), assertArrayHasKey(), assertCount(), assertNull(), assertInstanceOf() as appropriate.
Protected Methods
Test via reflection helper:
protected function callProtected(string $method, ...$args): mixed {
$ref = new \ReflectionMethod($this->sut, $method);
$ref->setAccessible(TRUE);
return $ref->invoke($this->sut, ...$args);
}
Data Providers
public static methods returning associative arrays with descriptive string keys for test case names.
Commit Messages
Follow Conventional Commits format:
<type>: <short description>
[optional body]
Types
- feat: A new feature or user-facing functionality.
- fix: A bug fix.
- test: Adding or updating tests (no production code change).
- docs: Documentation only changes.
- refactor: Code change that neither fixes a bug nor adds a feature.
- style: Formatting, whitespace, coding standards (no logic change).
- perf: Performance improvement.
- chore: Maintenance tasks, dependency updates, tooling changes.
- ci: CI/CD pipeline changes.
- build: Build system or external dependency changes.
Rules
- Use lowercase for the type and description.
- Keep the subject line under 72 characters.
- Use imperative mood in the description (e.g., "add" not "added" or "adds").
- Do not end the subject line with a period.
- Separate subject from body with a blank line if a body is needed.
- Reference issue numbers in the body when applicable (e.g.,
Closes #42).
Important Notes
- LSP "Undefined type" errors for Drupal core classes are expected — the module is developed standalone without Drupal core in the workspace. They are not real errors.
- PHPUnit deprecation warnings about intersection types in mock declarations are cosmetic (PHPUnit 11.x).
- The
ViostreamSettingsFormconstructor does NOT callparent::__construct()— theconfigFactoryproperty must be injected via reflection in tests. - Coverage target: 90%+ (currently 99.05%).