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

DDST-271: Feature/deferred search api resolution #24

Merged
merged 13 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A module to facilitate image discovery for Islandora repository items. Image dis

* contents of a Media field, `field_representative_image` on the node
* an "Islandora thumbnail", i.e., a media that is "Media of" the node (using `field_media_of`) with a Media Use (`field_media_use`) taxonomy term with External URI (`field_external_uri`) equal to "http://pcdm.org/use#ThumbnailImage"
* a first child's Islandora thumbnail media, i.e. the Islandora thumbnail of the node with lowest weight (`field_weight`) that is a Member Of (`field_member_of`) the node in question. If not found on the first direct child, it will look at the first child's first child, and so forth to a depth of 3.
* a first child's Islandora thumbnail media, i.e. the Islandora thumbnail of the node with lowest weight (`field_weight`) that is a Member Of (`field_member_of`) the node in question. If not found on the first direct child, it will look at the first child's first child, and so forth to a depth of 3.


## Requirements
Expand All @@ -24,17 +24,39 @@ further information.
## Usage

This module allows for image discovery on parent aggregate objects such as
collections, compounds and paged objects.
collections, compounds and paged objects in multiple context.

## Configuration
### Search API

Search API can be made to index URLs to the discovered image in multiple ways:

- `deferred`: Create URL to dedicated endpoint which can handle the final image lookup. Given responses here can be aware of Drupal's cache tag invalidations, we can accordingly change what is ultimately served.
- `pre_generated`: Creates URL to styled image directly. May cause stale references to stick in the index, due to changing access control constraints.

This is configurable on the field when it is added to be indexed. Effectively this defaults to `pre_generated` to maintain existing/current behaviour; however, `deferred` should possibly be preferred without other mechanisms to perform bulk reindexing due to changes on other entities. In particular, should there be something such as [Embargo](https://github.com/discoverygarden/embargo) and [Embargo Inheritance](https://github.com/discoverygarden/embargo_inheritance), where an access control statement applied to a parent node is expected to be applied to children. That said, `pre_generated` could be more convenient/efficient when there are no complex access control requirements in play.

#### Deferral mechanism

There are multiple plugins to dereference deferred URLs:

- `redirect`: Issue a redirect to the final derived image destination from our endpoint. Easily enough done; however:
- incurs another round trip
- can cause a race condition if two items being displayed in a set of results happen to reference the same image. Drupal maintains a lock/semaphore around the image derivation: If the second request occurs while the first still has the lock for deriving the image, then the second request will receive an HTTP 503 with `Retry-After` of `3` seconds, but many browsers do not make use of the `Retry-After` header.
- `subrequest`: Perform subrequest to stream the image directly from our endpoint. Can deal with the 503 with `Retry-After`.

The plugin in use is presently controlled with the `DGI_IMAGE_DISCOVERY_DEFERRED_PLUGIN`, which defaults to `subrequest`.

### Views

Views referencing node content can directly make use of a virtual field.

### Adding a "Representative Image" field to your content type

To override the use of the "Islandora" thumbnail, you can add a new field to each of your applicable content types. To do this:

1. In the "Manage fields" page for your content type, choose "Create a new field".
1. In the "Add a new field" list, choose "Media" (if on Drupal < 10.2, this is "Reference > Media")
1. Set the new field's label to "Representative image" so that the machine name of this field is `field_representative_image`. This machine name must be set; you can change the label later if you wish.
1. Set the new field's label to "Representative image" so that the machine name of this field is `field_representative_image`. This machine name must be set; you can change the label later if you wish.
1. On the next page, in the "Type of item to reference" setting, choose "Media" and leave the "Allowed number of values" at 1.
1. On the next page, in the "Media type" checkboxes, choose "Image".
1. Click on "Save settings".
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"license": "GPL-3.0-only",
"require": {
"drupal/search_api": "^1.19"
},
"conflict": {
"drupal/core": "<10.2"
}
}
3 changes: 1 addition & 2 deletions dgi_image_discovery.module
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
* General hook implementations.
*/

use Drupal\dgi_image_discovery\DIDImageItemList;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\dgi_image_discovery\DIDImageItemList;
Comment on lines -8 to +10
Copy link
Contributor Author

@adam-vessey adam-vessey Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Largely inconsequential, but should've been in the pre-existing coding standards commit; however, I spec'd the extensions as php, so it didn't touch the .module... whoops! Anyway.


/**
* Implements hook_entity_base_field_info().
Expand Down
13 changes: 13 additions & 0 deletions dgi_image_discovery.routing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
dgi_image_discovery.deferred_resolution:
path: '/node/{node}/dgi_image_discovery/{style}'
defaults:
_controller: 'dgi_image_discovery.deferred_resolution_controller:resolve'
requirements:
_entity_access: node.view
options:
parameters:
style:
type: entity:image_style
node:
type: entity:node
11 changes: 11 additions & 0 deletions dgi_image_discovery.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ services:
class: '\Drupal\dgi_image_discovery\EventSubscriber\DiscoverRepresentativeImageSubscriber'
tags:
- name: event_subscriber
dgi_image_discovery.deferred_resolution_controller:
class: '\Drupal\dgi_image_discovery\Controller\DeferredResolutionController'
factory: [null, 'create']
arguments:
- '@service_container'
plugin.manager.dgi_image_discovery.url_generator:
class: Drupal\dgi_image_discovery\UrlGeneratorPluginManager
parent: default_plugin_manager
plugin.manager.dgi_image_discovery.url_generator.deferred:
class: Drupal\dgi_image_discovery\DeferredResolutionPluginManager
parent: default_plugin_manager
38 changes: 38 additions & 0 deletions src/Attribute/DeferredResolution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Drupal\dgi_image_discovery\Attribute;

use Drupal\Component\Plugin\Attribute\AttributeBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
* The deferred_resolution attribute.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class DeferredResolution extends AttributeBase {

/**
* Constructs a new DgiImageDiscoveryUrlGenerator instance.
*
* @param string $id
* The plugin ID. There are some implementation bugs that make the plugin
* available only if the ID follows a specific pattern. It must be either
* identical to group or prefixed with the group. E.g. if the group is "foo"
* the ID must be either "foo" or "foo:bar".
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label
* (optional) The human-readable name of the plugin.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description
* (optional) A brief description of the plugin.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $label,
public readonly ?TranslatableMarkup $description = NULL,
public readonly ?string $deriver = NULL,
) {}

}
38 changes: 38 additions & 0 deletions src/Attribute/UrlGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Drupal\dgi_image_discovery\Attribute;

use Drupal\Component\Plugin\Attribute\AttributeBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
* The dgi_image_discovery__url_generator attribute.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class UrlGenerator extends AttributeBase {

/**
* Constructs a new DgiImageDiscoveryUrlGenerator instance.
*
* @param string $id
* The plugin ID. There are some implementation bugs that make the plugin
* available only if the ID follows a specific pattern. It must be either
* identical to group or prefixed with the group. E.g. if the group is "foo"
* the ID must be either "foo" or "foo:bar".
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label
* (optional) The human-readable name of the plugin.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description
* (optional) A brief description of the plugin.
* @param class-string|null $deriver
* (optional) The deriver class.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $label,
public readonly ?TranslatableMarkup $description = NULL,
public readonly ?string $deriver = NULL,
) {}

}
74 changes: 74 additions & 0 deletions src/CacheableBinaryFileResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Drupal\dgi_image_discovery;

use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Cache\CacheableResponseTrait;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

/**
* Cacheable binary file response.
*
* Loosely adapted from
* https://www.drupal.org/project/drupal/issues/3227041#comment-15335922
*/
class CacheableBinaryFileResponse extends BinaryFileResponse implements CacheableResponseInterface {

use CacheableResponseTrait;
use DependencySerializationTrait {
__sleep as traitSleep;
__wakeup as traitWakeup;
}

/**
* Serializable reference to the file.
*
* @var string
*/
protected string $uri;

/**
* {@inheritDoc}
*/
public function setFile(\SplFileInfo|string $file, ?string $contentDisposition = NULL, bool $autoEtag = FALSE, bool $autoLastModified = TRUE): static {
$this->uri = $file instanceof \SplFileInfo ? $file->getPathname() : $file;
return parent::setFile($file, $contentDisposition, $autoEtag, $autoLastModified);
}

/**
* {@inheritDoc}
*/
public function __sleep() {
return array_diff($this->traitSleep(), [
'file',
]);
}

/**
* {@inheritDoc}
*/
public function __wakeup() : void {
$this->traitWakeup();
$this->setFile($this->uri);
}

/**
* Convert a BinaryFileResponse into a CacheableBinaryFileResponse.
*
* @param \Symfony\Component\HttpFoundation\BinaryFileResponse $response
* The response to convert.
*
* @return static
* The converted response.
*/
public static function convert(BinaryFileResponse $response) : static {
return new static(
$response->getFile(),
$response->getStatusCode(),
$response->headers->all(),
/* $public, $contentDisposition, $autoEtag, $autoLastModified all accounted for in headers */
);
}

}
70 changes: 70 additions & 0 deletions src/Controller/DeferredResolutionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Drupal\dgi_image_discovery\Controller;

use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\dgi_image_discovery\DeferredResolutionPluginManagerInterface;
use Drupal\image\ImageStyleInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Deferred image URL resolution controller.
*/
class DeferredResolutionController implements ContainerInjectionInterface {

/**
* Constructor.
*/
public function __construct(
protected DeferredResolutionPluginManagerInterface $deferredResolutionPluginManager,
protected RendererInterface $renderer,
) {}

/**
* {@inheritDoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.dgi_image_discovery.url_generator.deferred'),
$container->get('renderer'),
);
}

/**
* Resolve image for the given node and style.
*
* @param \Drupal\image\ImageStyleInterface $style
* The style of image to get.
* @param \Drupal\node\NodeInterface $node
* The node of which to get an image.
*
* @return \Drupal\Core\Cache\CacheableResponseInterface
* A cacheable response.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function resolve(ImageStyleInterface $style, NodeInterface $node) : CacheableResponseInterface {
$context = new RenderContext();
/** @var \Drupal\Core\Cache\CacheableResponseInterface $response */
$response = $this->renderer->executeInRenderContext($context, function () use ($style, $node) {
// @todo Make plugin configurable?
/** @var \Drupal\dgi_image_discovery\DeferredResolutionInterface $plugin */
$plugin = $this->deferredResolutionPluginManager->createInstance(getenv('DGI_IMAGE_DISCOVERY_DEFERRED_PLUGIN') ?: 'subrequest');

return $plugin->resolve($node, $style);
});

if (!$context->isEmpty()) {
$metadata = $context->pop();
$response->addCacheableDependency($metadata);
}

return $response;

}

}
2 changes: 1 addition & 1 deletion src/DIDImageItemList.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Drupal\dgi_image_discovery;

use Drupal\Core\TypedData\ComputedItemListTrait;
use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;

/**
* Boiler-plate for our computed field.
Expand Down
34 changes: 34 additions & 0 deletions src/DeferredResolutionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Drupal\dgi_image_discovery;

use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\image\ImageStyleInterface;
use Drupal\node\NodeInterface;

/**
* Interface for deferred_resolution plugins.
*/
interface DeferredResolutionInterface {

/**
* Returns the translated plugin label.
*/
public function label(): string;

/**
* Generate URL for the given node/style.
*
* @param \Drupal\node\NodeInterface $node
* The node for which to generate a URL.
* @param \Drupal\image\ImageStyleInterface $style
* The style which the URL should return.
*
* @return \Drupal\Core\Cache\CacheableResponseInterface
* Cacheable resolution response.
*/
public function resolve(NodeInterface $node, ImageStyleInterface $style): CacheableResponseInterface;

}
Loading