Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation for "developing registration agency plugin in 3.4+" #1110

Open
ewhanson opened this issue Jun 2, 2023 · 1 comment
Open

Comments

@ewhanson
Copy link

ewhanson commented Jun 2, 2023

The way DOIs work in OJS/OMP/OPS in general was massively overhauled in 3.4, and the way registration agency plugins are treated and interact with the rest of the application as it relates to DOIs has also significantly changed from 3.3. Detailed documentation on what's new, what's needed, and why will be necessary for new developers to understand how DOIs work within the registration agency plugin ecosystem and to be able to write new registration agency plugins.

A draft of all the details related to changes/requirements for registration agency plugins can be found below. 👇


DOI Registration Agency Plugins in 3.4

The following outlines the major architectural changes to how DOIs work with plugins in OJS/OMP/OPS 3.4 and the elements necessary to create a registration agency plugin for 3.4. A plugin that interacts with the DOI functionality is now referred to as a "registration agency plugin" or agency plugin for short.

Implementation Requirements

When creating a new registration agency plugin, there are two different aspects to consider: 1) the functionality that is enforced via code (e.g. required interfaces, inherited methods, etc.) and 2) functionality that is not enforced via code (e.g. functions/hooks that are dictated by convention and won't raise an error in your IDE).

Generic plugin implementing IDoiRegistrationAgency details

The main plugin must be a GenericPlugin and implement the IDoiRegistrationAgency interface

setEnabled()

Base implementation defined in LazyLoadPlugin::setEnabled()

A registration agency plugin can be set as the configured agency plugin at the context level. When the plugin is disabled, if the plugin is currently set as the configured agency plugin, it should be removed, e.g.

if ($context->getData(Context::SETTING_CONFIGURED_REGISTRATION_AGENCY) === $this->getName()) {
    $context->setData(Context::SETTING_CONFIGURED_REGISTRATION_AGENCY, Context::SETTING_NO_REGISTRATION_AGENCY);
    $contextDao->updateObject($context);
}

exportSubmission()

The export methods are enforced by IDoiRegistrationAgency but the shape of the array returned is not. It should return array with the following shape:

array{temporaryFileId: int, xmlErrors: array{string}}

The actual exporting logic is handled via the export plugin and this method returns results to the API. The function should look something like:

/**
 * @param \APP\submission\Submission[] $submissions
 *
 */
public function exportSubmissions(array $submissions, Context $context): array
{
    $exportPlugin = $this->_getExportPlugin();
    $xmlErrors = [];

    $items = [];

    foreach ($submissions as $submission) {
        $items[] = $submission;
        foreach ($submission->getGalleys() as $galley) {
            $items[] = $galley;
        }
    }

    $temporaryFileId = $exportPlugin->exportAsDownload($context, $items, null, $xmlErrors);
    return ['temporaryFileId' => $temporaryFileId, 'xmlErrors' => $xmlErrors];
}

depositSubmission()

The deposit methods are enforced by IDORegistrationAgency but the shape of the array returned is not. It should return an array with the following shape:

array{hasError: bool, responseMessage: string}

The actual depositing logic is handled via the export plugin and this method returns results to the API. The function should look something like:

/**
 * @param \APP\submission\Submission[] $submissions
 */
public function depositSubmissions(array $submissions, Context $context): array
{
    $exportPlugin = $this->_getExportPlugin();
    $responseMessage = '';

    $items = [];

    foreach ($submissions as $submission) {
        $items[] = $submission;
        foreach ($submission->getGalleys() as $galley) {
            $items[] = $galley;
        }
    }

    $status = $exportPlugin->exportAndDeposit($context, $items, $responseMessage);
    return [
        'hasErrors' => !$status,
        'responseMessage' => $responseMessage
    ];
    }

addAsRegistrationAgencyOption()

Enforced by IDoiRegistrationAgency. Should do the following:

/** @var Collection<int,IDoiRegistrationAgency> $enabledRegistrationAgencies */
$enabledRegistrationAgencies = &$args[0];
$enabledRegistrationAgencies->add($this);

isPluginConfigured()

Enforced by IDoiRegistrationAgency. Any agency-specific requirements for the plugin can be checked here. Will prevent exporting/depositing if the plugin is not correctly configured.

The following snippet can get the required settings from the plugin setting to include as part of the configuration check.

$settingsObject = $this->getSettingsObject();

/** @var PKPSchemaService $schemaService */
$schemaService = Services::get('schema');
$requiredProps = $schemaService->getRequiredProps($settingsObject::class);

foreach ($requiredProps as $requiredProp) {
    $settingValue = $this->getSetting($context->getId(), $requiredProp);
    if (empty($settingValue)) {
        return false;
    }
}

getErrorMessageKey() & getRegisteredMessageKey()

Enforced by IDoiRegistrationAgency.

Should return null if not used. If a key is defined, registration or error messages can be saved/associated with individual DOI submissions. If used, the items will need to be added to the DOI schema via the Schema::get::doi hook as well as managed throughout the DOI registration lifecycle e.g. removing error messages when successfully deposited, marked registered, etc. Most of the time this should happen in-place when the status is set, except for the DOI being marked registered. The Doi::markRegistered hook should be used in this case to modify the params to be saved (see hooks and plugin initialization below).

getAllowedDoiTypes()

Enforced by IDoiRegistrationAgency. Should return an array of DOI types that are allowed to be registered with this registration agency. E.g.

return [
    Repo::doi()::TYPE_PUBLICATION,
    Repo::doi()::TYPE_REPRESENTATION,
    Repo::doi()::TYPE_ISSUE,
];

Plugin Initialization and Hooks

Plugin initialization (registration of hooks, etc) should be abstracted out to a private pluginInitialization() function that can be called from the plugin's register() function, getExportPlugin() function (see below), or when attempting to get the export plugin via a CLI operation where plugin registration doesn't happen, e.g.

/**
 * @return AgencyExportPlugin
 */
private function _getExportPlugin()
{
    if (empty($this->_exportPlugin)) {
        $pluginCategory = 'importexport';
        $pluginPathName = 'AgencyExportPlugin';
        $this->_exportPlugin = PluginRegistry::getPlugin($pluginCategory, $pluginPathName);
        // If being run from CLI, there is no context, so plugin initialization would not have been fired
        if ($this->_exportPlugin === null && !isset($_SERVER['SERVER_NAME'])) {
            $this->_pluginInitialization();
            $this->_exportPlugin = PluginRegistry::getPlugin($pluginCategory, $pluginPathName);
        }
    }
    return $this->_exportPlugin;
}

pluginInitialization() should instantiate the export plugin, add it to the plugin registry, and add any hook callbacks.

Required hooks:

  • DoiSettingsForm::setEnabledRegistrationAgencies: Callback should be addAsRegistrationAgency() method from IDoiRegistrationAgency
  • DoiSetupSettingsForm::getObjectTypes: Which DOI types can be assigned to which plugins are tracked by the settings form and need to be added in the expected way. The callback should look as follows:
    /**
     * Adds self to "allowed" list of pub object types that can be assigned DOIs for this registration agency.
     *
     * @param string $hookName DoiSetupSettingsForm::getObjectTypes
     * @param array $args [
     *
     *      @option array &$objectTypeOptions
     * ]
     */
    public function addAllowedObjectTypes(string $hookName, array $args): bool
    {
        $objectTypeOptions = &$args[0];
        $allowedTypes = $this->getAllowedDoiTypes();
      
        $objectTypeOptions = array_map(function ($option) use ($allowedTypes) {
            if (in_array($option['value'], $allowedTypes)) {
                $option['allowedBy'][] = $this->getName();
            }
            return $option;
        }, $objectTypeOptions);
      
        return Hook::CONTINUE;
        }
    • DoiListPanel::setConfig: Adds display name in DoiListPanel for agency. The call back should look as follows:
    /**
     * Includes human-readable name of registration agency for display in conjunction with how/with whom the
     * DOI was registered.
     *
     * @param string $hookName DoiListPanel::setConfig
     * @param array $args [
     *
     *      @option $config array
     * ]
     */
    public function addRegistrationAgencyName(string $hookName, array $args): bool
    {
        $config = &$args[0];
        $config['registrationAgencyNames'][$this->_getExportPlugin()->getName()] = $this->getRegistrationAgencyName();
      
        return HOOK::CONTINUE;
    }

Optional hooks:

  • Context::validate: Can be used to enforce validation rule to restrict the types of DOIs that can be registered with this agency. The system architecture by default assumes all types are allowed and any restrictions must be specified. Example from the Crossref plugin:
/**
 * Add validation rule to Context for restriction of allowed pubObject types for DOI registration.
 *
 * @throws \Exception
 */
public function validateAllowedPubObjectTypes(string $hookName, array $args): bool
{
    $errors = &$args[0];
    $props = $args[2];

    if (!isset($props['enabledDoiTypes'])) {
        return Hook::CONTINUE;
    }

    $contextId = $props['id'];
    if (empty($contextId)) {
        throw new \Exception('A context ID must be present to edit context settings');
    }

    /** @var ContextService $contextService */
    $contextService = Services::get('context');
    $context = $contextService->get($contextId);
    $enabledRegistrationAgency = $context->getConfiguredDoiAgency();
    if (!$enabledRegistrationAgency instanceof $this) {
        return Hook::CONTINUE;
    }

    $allowedTypes = $enabledRegistrationAgency->getAllowedDoiTypes();

    if (!empty(array_diff($props['enabledDoiTypes'], $allowedTypes))) {
        $errors['enabledDoiTypes'] = [__('doi.manager.settings.enabledDoiTypes.error')];
    }

    return Hook::CONTINUE;
}
  • Schema::get::doi: This can be used to add any additional data to be stored alongside DOIs. This can include any agency-specific data, or registration, error messages, e.g.
/**
 * Add properties for registration agency to the DOI entity for storage in the database.
 *
 * @param string $hookName `Schema::get::doi`
 * @param array $args [
 *
 *      @option stdClass $schema
 * ]
 *
 */
public function addToSchema(string $hookName, array $args): bool
{
    $schema = &$args[0];

    $settings = [
        $this->getName() . '_failedMsg',
        $this->getName() . '_successMsg',
    ];

    foreach ($settings as $settingName) {
        $schema->properties->{$settingName} = (object) [
            'type' => 'string',
            'apiSummary' => true,
            'validation' => ['nullable'],
        ];
    }

    return false;
}
  • Doi::markRegistered: If any registration, error, or any other custom messages should be removed when a DOI is marked as registered, that data should be added here, e.g.
/**
 * Adds agency specific info to Repo::doi()->markRegistered()
 *
 * @param string $hookName Doi::markRegistered
 *
 */
public function editMarkRegisteredParams(string $hookName, array $args): bool
{
    $editParams = &$args[0];
    $editParams[$this->getName() . '_failedMsg'] = null;
    $editParams[$this->getName() . '_successMsg'] = null;

    return false;
}

The param names must be the same as those added to the schema.

Plugin settings, getSettingsObject(), and RegistrationAgencySettings

Registration agency plugin settings are integrated with the rest of the DOI settings/options rather than being managed through the plugin settings directly. This is managed by an agency-specific settings class that getSetingsObject() should return, e.g.

public function getSettingsObject(): RegistrationAgencySettings
{
    if (!isset($this->settingsObject)) {
        $this->settingsObject = new AgencySettings($this);
    }

    return $this->settingsObject;
}

where AgencySettings is the plugin-specific class that inherits from RegistrationAgencySettings.

RegistrationAgencySettings manages much of the logic for creating the settings directly, but three abstract methods must be overridden in the child class:

  • getSchema(): Returns a stdClass object in the shape of the settings schema, similar to the rest of the json schema objects. To force the correct structure and return the expected stdClass, associative arrays should be cast to objects, e.g.
return (object) [
    'title' => 'Example Agency Plugin',
    'description' => 'An example plugin used in documenting DOI registration agency plugins',
    'type' => 'object',
    // Any required field should have their string keys added here
    'required' => ['requiredTestSetting'],
    'properties' => (object) [
        'testSetting' => (object) [
            'type': 'string',
            // Optional params must have nullable validation, otherwise validation checks will require a value
            'validation' = ['nullable']
        ],
        'requiredTestSetting' => (object) [
            'type': 'string',
        ],
    ],
]
  • getFields(): Returns an array of form field classes to be injected in the DOI settings UI, e.g.
return [
    new FieldHTML('preamble', [
        'label' => __('label.locale.key.here'),
        'description' => "<p>Example description/instruction text in HTML here</p>",
    ]),
    new FieldText('testSetting', [
        'label' => __('label.locale.key.here'),
        'description' => __('description.locale.key.here'),
        'value' => $this->agencyPlugin->getSetting($context->getId(), 'testSetting')
    ]),
]
  • addValidationChecks(): Any additional checks to be added to the \Illuminate\Validation\Validator instance can be added here, e.g.
protected function addValidationChecks(Validator &validator, $props): void
{
    $validator->after(function (Validator $validator) use ($props) {
        // Add validation checks and errors here
    });
}

Export Plugin

This guide will primarily focus on the difference between existing DOI registration agency export plugins and the new architecture. How the actual XML is created and deposited remains largely unchanged, but some additional parameters and modifications have been made.

The existing export plugin should be included alongside (or a new one should be created). The management of interactions between the system and the registration agency plugin are managed through the generic plugin, but the XML creation and depositing are still handled via inherited/slightly modified methods in the export plugin.

Main plugin and settings delegation

Settings have been delegated to the main plugin, so export plugin methods that deal with settings are passed up to the main plugin. We therefore need access to the main plugin within the export plugin, e.g.

public function __construct(protected IDoiRegistrationAgency|Plugin $agencyPlugin)
{
    parent::__construct();
}

and further proxy the settings up to the main plugin, e.g.

/** Proxy to main plugin class's `getSetting` method */
public function getSetting($contextId, $name)
{
    return $this->agencyPlugin->getSetting($contextId, $name);
}

exportAndDeposit()

This is the pre-existing method for handling exporting and depositing. There is a change in function signature to include a string reference for a response message, $responseMessage, to be passed back up to the API and UI.

exportAndDeposit(Context $context, array $objects, string &$responseMessage, ?bool $noValidation = null): bool

exportXML()

This is also largely unchanged, but the base method, PubObjectsExportPlugin::exportXML() includes an optional final parameter, array &$exportErrors that should be included when calling exportXML(). If an array is not passed in, PubObjectsExportPlugin::exportXML() will attempt to directly return HTML rather than letting the API & UI framework handle it.

depositXML()

The returned array of errors is passed up to the array reference in exportAndDeposit() and further up through the main plugins export functions and to the API. Status updates should be handled according to the new conventions (see below) and any custom status information (including error or registration messages) should be handled here.

exportAsDownload()

This function should create the XML, save it as a temporary file, and return the temporary file ID, which will be used from the frontend to initiate the file download for the user. A simple example of this looks like:

/**
 * Exports and stores XML as a TemporaryFile
 *
 *
 * @throws Exception
 */
public function exportAsDownload(\PKP\context\Context $context, array $objects, string $filter, string $objectsFileNamePart, ?bool $noValidation = null, ?array &$exportErrors = null): ?int
{
    $fileManager = new TemporaryFileManager();

    $exportErrors = [];
    $exportXml = $this->exportXML($objects, $filter, $context, $noValidation, $exportErrors);

    $exportFileName = $this->getExportFileName($this->getExportPath(), $objectsFileNamePart, $context, '.xml');

    $fileManager->writeFile($exportFileName, $exportXml);

    $user = Application::get()->getRequest()->getUser();

    return $fileManager->createTempFileFromExisting($exportFileName, $user->getId());
}

The temporary file ID gets passed back up to the main plugin's export function, and in turn back up to the API and UI.

updateDepositStatus()

Should update the status to one of Doi::STATUS_* constants for each individual DOI associated with an object, e.g. if a submission has a publication DOI and a galley DOI, both should be updated with the new status.

If the new status is Doi::STATUS_REGISTERED, the registration agency name should be associated with the DOI as well. This is used to differentiate items manually marked registered and those deposited with a registration agency. For example:

foreach ($doiIds as $doiId) {
    $doi = Repo::doi()->get($doiId);
    
    // Any other custom items added to the DOI schema by the main plugin can be stored here
    $editParams = ['status' => $status];
    
    if ($status === Doi::STATUS_REGISTERED) {
        $editParams['registrationAgency'] = $this->getName();
    }
    
    Repo::doi()->edit($doi, $editParams);
]
}

markedRegistered()

This method existed previously and its default behaviour looks for a single 'doiId' on the object. If you need to get all doiIds from e.g. a submission, you can override the default behaviour. E.g. in OJS:

public function markRegistered($context, $objects)
{
    foreach ($objects as $object) {
        // Get all DOIs for each object
        // Check if submission or issue
        if ($object instanceof Submission) {
            $doiIds = Repo::doi()->getDoisForSubmission($object->getId());
        } else {
            $doiIds = Repo::doi()->getDoisForIssue($object->getId, true);
        }

        foreach ($doiIds as $doiId) {
            Repo::doi()->markRegistered($doiId);
        }
    }
}

Automatic deposit

Automatic depositing is now handled at the application level and all references to it should be removed from the registration agency plugin. Users can enable this from the DOI settings menu.

@ewhanson
Copy link
Author

ewhanson commented Jun 2, 2023

@bozana, here is the draft I was able to put together of documentation for registration agency plugins. I wasn't sure where best to put it and haven't had time to revise it in detail, but thought I'd include it here along with a more general issue to add it to the docs hub eventually. This covers working with the registration agency plugins specifically and assumes familiarity with DOIs in 3.4 in general.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant