Skip to content

Commit

Permalink
Merge pull request #24 from discoverygarden/feature/deferred-search-a…
Browse files Browse the repository at this point in the history
…pi-resolution

DDST-271: Feature/deferred search api resolution
  • Loading branch information
nchiasson-dgi authored Jul 11, 2024
2 parents 0232a2e + 184b392 commit 767a4ae
Show file tree
Hide file tree
Showing 31 changed files with 943 additions and 52 deletions.
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;

/**
* 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 */
);
}

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

namespace Drupal\dgi_image_discovery\Controller;

use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Http\Exception\CacheableHttpException;
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');

try {
return $plugin->resolve($node, $style);
}
catch (CacheableHttpException $e) {
return (new CacheableResponse($e->getMessage(), $e->getStatusCode()))
->addCacheableDependency($e);
}
});

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

0 comments on commit 767a4ae

Please sign in to comment.