From 0dcc2512b890e4fc696c7e8c6ce22501bc8bc06f Mon Sep 17 00:00:00 2001
From: wizonesolutions <wizonesolutions@739994.no-reply.drupal.org>
Date: Sat, 18 Jan 2020 19:52:11 +0100
Subject: [PATCH] Issue #3105191 by wizonesolutions, liquidcms: Add API call to
 create PDFs

---
 fillpdf.services.yml                          |   3 +
 src/Controller/HandlePdfController.php        |  69 ++++--------
 src/Entity/FillPdfForm.php                    |  16 +++
 src/FillPdfLinkManipulatorInterface.php       |   2 +-
 src/Form/FillPdfFormForm.php                  |  16 +--
 src/Service/BackendProxy.php                  | 105 ++++++++++++++++++
 src/Service/BackendProxyInterface.php         |  38 +++++++
 src/Service/FillPdfLinkManipulator.php        |   6 +-
 .../Functional/FillPdfFormDeleteFormTest.php  |   2 +-
 .../FillPdfFormDuplicateFormTest.php          |   2 +-
 .../Functional/FillPdfFormImportFormTest.php  |   2 +-
 tests/src/Functional/FillPdfTestBase.php      |   4 +-
 .../src/Functional/FillPdfUploadTestBase.php  |  19 +++-
 tests/src/Functional/LinkManipulatorTest.php  |   6 +-
 tests/src/Functional/PdfPopulationTest.php    |  58 +++++++++-
 tests/src/Functional/UninstallTest.php        |   3 +-
 16 files changed, 277 insertions(+), 74 deletions(-)
 create mode 100644 src/Service/BackendProxy.php
 create mode 100644 src/Service/BackendProxyInterface.php

diff --git a/fillpdf.services.yml b/fillpdf.services.yml
index 7cac75c..b8912c3 100644
--- a/fillpdf.services.yml
+++ b/fillpdf.services.yml
@@ -53,3 +53,6 @@ services:
     class: Drupal\fillpdf\ShellManager
     arguments: ['@config.factory']
 
+  fillpdf.backend_proxy:
+    class: Drupal\fillpdf\Service\BackendProxy
+    arguments: ['@fillpdf.token_resolver', '@plugin.manager.fillpdf.pdf_backend', '@config.factory']
diff --git a/src/Controller/HandlePdfController.php b/src/Controller/HandlePdfController.php
index e476ab6..784b7f9 100644
--- a/src/Controller/HandlePdfController.php
+++ b/src/Controller/HandlePdfController.php
@@ -10,6 +10,7 @@ use Drupal\file\Entity\File;
 use Drupal\fillpdf\FillPdfContextManagerInterface;
 use Drupal\fillpdf\FillPdfFormInterface;
 use Drupal\fillpdf\FillPdfLinkManipulatorInterface;
+use Drupal\fillpdf\Service\BackendProxyInterface;
 use Drupal\fillpdf\TokenResolverInterface;
 use Drupal\fillpdf\Component\Helper\FillPdfMappingHelper;
 use Drupal\fillpdf\Entity\FillPdfForm;
@@ -67,6 +68,13 @@ class HandlePdfController extends ControllerBase {
    */
   protected $actionManager;
 
+  /**
+   * The backend proxy.
+   *
+   * @var \Drupal\fillpdf\Service\BackendProxyInterface
+   */
+  protected $backendProxy;
+
   /**
    * Constructs a FillPdfBackendManager object.
    *
@@ -82,6 +90,8 @@ class HandlePdfController extends ControllerBase {
    *   The FillPDF backend manager.
    * @param \Drupal\fillpdf\Plugin\FillPdfActionPluginManager $action_manager
    *   The FillPDF action manager.
+   * @param \Drupal\fillpdf\Service\BackendProxyInterface $backend_proxy
+   *   The backend proxy.
    */
   public function __construct(
     FillPdfLinkManipulatorInterface $link_manipulator,
@@ -89,7 +99,8 @@ class HandlePdfController extends ControllerBase {
     TokenResolverInterface $token_resolver,
     RequestStack $request_stack,
     PdfBackendManager $backend_manager,
-    FillPdfActionPluginManager $action_manager
+    FillPdfActionPluginManager $action_manager,
+    BackendProxyInterface $backend_proxy
   ) {
     $this->linkManipulator = $link_manipulator;
     $this->contextManager = $context_manager;
@@ -97,6 +108,7 @@ class HandlePdfController extends ControllerBase {
     $this->requestStack = $request_stack;
     $this->backendManager = $backend_manager;
     $this->actionManager = $action_manager;
+    $this->backendProxy = $backend_proxy;
   }
 
   /**
@@ -109,7 +121,8 @@ class HandlePdfController extends ControllerBase {
       $container->get('fillpdf.token_resolver'),
       $container->get('request_stack'),
       $container->get('plugin.manager.fillpdf.pdf_backend'),
-      $container->get('plugin.manager.fillpdf_action.processor')
+      $container->get('plugin.manager.fillpdf_action.processor'),
+      $container->get('fillpdf.backend_proxy')
     );
   }
 
@@ -120,66 +133,28 @@ class HandlePdfController extends ControllerBase {
    *   The action plugin's response object.
    *
    * @throws \InvalidArgumentException
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
    *   If one of the passed arguments is missing or does not pass the
    *   validation.
    */
   public function populatePdf() {
-    $config = $this->config('fillpdf.settings');
-
     $context = $this->linkManipulator->parseRequest($this->requestStack->getCurrentRequest());
-    $entities = $this->contextManager->loadEntities($context);
-
     $fillpdf_form = FillPdfForm::load($context['fid']);
-    $form_replacements = FillPdfMappingHelper::parseReplacements($fillpdf_form->replacements->value);
-
-    // Populate mappings array.
-    $field_mappings = [];
-    foreach ($fillpdf_form->getFormFields() as $pdf_key => $field) {
-      if ($context['sample']) {
-        $field_mappings[$pdf_key] = new TextFieldMapping($pdf_key);
-      }
-      else {
-        $options = [];
-        // Our pdftk backend doesn't support image stamping, so at least for
-        // this backend we already know which type of content we can expect.
-        $options['content'] = $config->get('backend') == 'pdftk' ? 'text' : '';
-
-        // Prepare transformations with field-level replacements taking
-        // precedence over form-level replacements.
-        $options['replacements'] = FillPdfMappingHelper::parseReplacements($field->replacements->value) + $form_replacements;
+    $entities = $this->contextManager->loadEntities($context);
 
-        // Add prefix and suffix.
-        $options['prefix'] = $field->prefix->value;
-        $options['suffix'] = $field->suffix->value;
+    $populated_pdf = $this->backendProxy->merge($fillpdf_form, $entities, $context);
 
-        // Resolve tokens.
-        $text = count($field->value) ? $field->value->value : '';
-        $field_mappings[$pdf_key] = $this->tokenResolver->replace($text, $entities, $options);
-      }
+    if (empty($populated_pdf)) {
+      $this->messenger()->addError($this->t('Merging the FillPDF Form failed.'));
+      return new RedirectResponse(Url::fromRoute('<front>')->toString());
     }
 
     // Generate the filename of downloaded PDF from title of the PDF set in
     // admin/structure/fillpdf/%fid.
     $filename = $this->buildFilename($fillpdf_form->title->value, $entities);
 
-    // Now load the backend plugin.
-    $backend = $this->backendManager->createInstance($config->get('backend'), $config->get());
-
-    // @todo: Emit event (or call alter hook?) before populating PDF.
-    // Rename fillpdf_merge_fields_alter() to fillpdf_populate_fields_alter().
-    $template_file = File::load($fillpdf_form->file->target_id);
-    /** @var \Drupal\fillpdf\FillPdfBackendPluginInterface $backend */
-    $populated_pdf = $backend->mergeFile($template_file, $field_mappings, $context);
-
-    if (empty($populated_pdf)) {
-      $this->messenger()->addError($this->t('Merging the FillPDF Form failed.'));
-      return new RedirectResponse(Url::fromRoute('<front>')->toString());
-    }
-
     // @todo: When Rules integration ported, emit an event or whatever.
-    $action_response = $this->handlePopulatedPdf($fillpdf_form, $populated_pdf, $context, $filename, $entities);
-
-    return $action_response;
+    return $this->handlePopulatedPdf($fillpdf_form, $populated_pdf, $context, $filename, $entities);
   }
 
   /**
diff --git a/src/Entity/FillPdfForm.php b/src/Entity/FillPdfForm.php
index 70977c0..fb9da6d 100644
--- a/src/Entity/FillPdfForm.php
+++ b/src/Entity/FillPdfForm.php
@@ -50,6 +50,22 @@ use Drupal\fillpdf\Service\FillPdfAdminFormHelper;
  */
 class FillPdfForm extends ContentEntityBase implements FillPdfFormInterface {
 
+  /**
+   * Load a FillPDF Form.
+   *
+   * @param int $id
+   *   The ID of the form.
+   *
+   * @return \Drupal\fillpdf\FillPdfFormInterface|null
+   *   The FillPDF Form, or NULL if the ID didn't match any.
+   */
+  public static function load($id) {
+    /** @var \Drupal\fillpdf\FillPdfFormInterface $fillpdf_form */
+    $fillpdf_form = parent::load($id);
+
+    return $fillpdf_form;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/src/FillPdfLinkManipulatorInterface.php b/src/FillPdfLinkManipulatorInterface.php
index cedd944..b17f670 100644
--- a/src/FillPdfLinkManipulatorInterface.php
+++ b/src/FillPdfLinkManipulatorInterface.php
@@ -48,7 +48,7 @@ interface FillPdfLinkManipulatorInterface {
    *
    * @param \Drupal\Core\Url $link
    *   The valid URL containing the FillPDF generation metadata.
-   *   e.g. 'http://example.com/fillpdf?entities[]=node:1&entities[]=contact:7'.
+   *   e.g. 'http://example.com/fillpdf?entity_ids[]=node:1&entity_ids[]=contact:7'.
    *
    * @return array
    *   An associative array representing the request context and containing the
diff --git a/src/Form/FillPdfFormForm.php b/src/Form/FillPdfFormForm.php
index 0c8f9c6..92037e7 100644
--- a/src/Form/FillPdfFormForm.php
+++ b/src/Form/FillPdfFormForm.php
@@ -281,20 +281,20 @@ class FillPdfFormForm extends ContentEntityForm {
       '#type' => 'item',
       '#title' => $this->t('Sample PDF'),
       '#description' => $this->l(
-        $this->t('See which fields are which in this PDF.'),
-        $this->linkManipulator->generateLink([
-          'fid' => $fid,
-          'sample' => TRUE,
-        ])) . '<br />' .
-        $this->t('If you have set a custom path on this PDF, the sample will be saved there silently.'),
+          $this->t('See which fields are which in this PDF.'),
+          $this->linkManipulator->generateLink([
+            'fid' => $fid,
+            'sample' => TRUE,
+          ])) . '<br />' . $this->t('If you have set a custom path on this PDF, the sample will be saved there silently.'),
       '#weight' => $pdf_info_weight++,
     ];
     $form['pdf_info']['form_id'] = [
       '#type' => 'item',
       '#title' => $this->t('Form info'),
-      '#description' => $this->t("Form ID: [@fid].  Populate this form with entity IDs, such as @path<br/>", [
+      '#description' => $this->t('Form ID: [@fid].  Populate this form with entity IDs, such as %path . For more usage examples, see <a href="@documentation">the documentation</a>.', [
         '@fid' => $fid,
-        '@path' => "/fillpdf?fid={$fid}&entity_type=node&entity_id=10",
+        '%path' => "/fillpdf?fid={$fid}&entity_ids[]=node:10&entity_ids[]=user:7",
+        '@documentation' => 'https://www.drupal.org/docs/8/modules/fillpdf/usage#makelink',
       ]),
       '#weight' => $pdf_info_weight,
     ];
diff --git a/src/Service/BackendProxy.php b/src/Service/BackendProxy.php
new file mode 100644
index 0000000..b7899ed
--- /dev/null
+++ b/src/Service/BackendProxy.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\fillpdf\Service;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\file\Entity\File;
+use Drupal\fillpdf\Component\Helper\FillPdfMappingHelper;
+use Drupal\fillpdf\FieldMapping\TextFieldMapping;
+use Drupal\fillpdf\FillPdfFormInterface;
+use Drupal\fillpdf\Plugin\PdfBackendManager;
+use Drupal\fillpdf\TokenResolverInterface;
+
+/**
+ * BackendProxy service.
+ */
+class BackendProxy implements BackendProxyInterface {
+
+  /**
+   * The fillpdf.token_resolver service.
+   *
+   * @var \Drupal\fillpdf\TokenResolverInterface
+   */
+  protected $tokenResolver;
+
+  /**
+   * The plugin.manager.fillpdf.pdf_backend service.
+   *
+   * @var \Drupal\fillpdf\Plugin\PdfBackendManager
+   */
+  protected $backendManager;
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructs a BackendProxy object.
+   *
+   * @param \Drupal\fillpdf\TokenResolverInterface $tokenResolver
+   *   The fillpdf.token_resolver service.
+   * @param \Drupal\fillpdf\Plugin\PdfBackendManager $backendManager
+   *   The plugin.manager.fillpdf.pdf_backend service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The configuration factory.
+   */
+  public function __construct(TokenResolverInterface $tokenResolver, PdfBackendManager $backendManager, ConfigFactoryInterface $configFactory) {
+    $this->tokenResolver = $tokenResolver;
+    $this->backendManager = $backendManager;
+    $this->configFactory = $configFactory;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   */
+  public function merge(FillPdfFormInterface $fillPdfForm, array $entities, array $mergeOptions = []): string {
+    $config = $this->configFactory->get('fillpdf.settings');
+
+    $form_replacements = FillPdfMappingHelper::parseReplacements($fillPdfForm->replacements->value);
+
+    $mergeOptions += [
+      'sample' => FALSE,
+      'flatten' => TRUE,
+    ];
+
+    // Populate mappings array.
+    $field_mappings = [];
+    foreach ($fillPdfForm->getFormFields() as $pdf_key => $field) {
+      if ($mergeOptions['sample']) {
+        $field_mappings[$pdf_key] = new TextFieldMapping($pdf_key);
+      }
+      else {
+        $options = [];
+        // Our pdftk backend doesn't support image stamping, so at least for
+        // this backend we already know which type of content we can expect.
+        $options['content'] = $config->get('backend') === 'pdftk' ? 'text' : '';
+
+        // Prepare transformations with field-level replacements taking
+        // precedence over form-level replacements.
+        $options['replacements'] = FillPdfMappingHelper::parseReplacements($field->replacements->value) + $form_replacements;
+
+        // Add prefix and suffix.
+        $options['prefix'] = $field->prefix->value;
+        $options['suffix'] = $field->suffix->value;
+
+        // Resolve tokens.
+        $text = count($field->value) ? $field->value->value : '';
+        $field_mappings[$pdf_key] = $this->tokenResolver->replace($text, $entities, $options);
+      }
+    }
+
+    // Now load the backend plugin.
+    /** @var \Drupal\fillpdf\FillPdfBackendPluginInterface|\Drupal\fillpdf\Plugin\PdfBackendInterface $backend */
+    $backend = $this->backendManager->createInstance($config->get('backend'), $config->get());
+
+    // @todo: Emit event (or call alter hook?) before populating PDF.
+    // Rename fillpdf_merge_fields_alter() to fillpdf_populate_fields_alter().
+    $template_file = File::load($fillPdfForm->file->target_id);
+    return $backend->mergeFile($template_file, $field_mappings, $mergeOptions);
+  }
+
+}
diff --git a/src/Service/BackendProxyInterface.php b/src/Service/BackendProxyInterface.php
new file mode 100644
index 0000000..41a236c
--- /dev/null
+++ b/src/Service/BackendProxyInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\fillpdf\Service;
+
+use Drupal\fillpdf\FillPdfFormInterface;
+
+/**
+ * The backend proxy allows backend-agnostic PDF operations.
+ *
+ * It uses the backend configured on the site to operate on the PDF. Currently,
+ * only merging is supported. Other modules can use this to integrate with
+ * FillPDF more simply and without having to make sub-requests.
+ */
+interface BackendProxyInterface {
+
+  /**
+   * Merge data into a PDF using the supplied form configuration and entities.
+   *
+   * @param \Drupal\fillpdf\FillPdfFormInterface $fillPdfForm
+   *   The form configuration to use. Will be processed the same way as the
+   *   fillpdf.populate_pdf route, including replacements, token mappings, etc.
+   * @param \Drupal\Core\Entity\EntityInterface[][] $entities
+   *   The entity data to use. The entities should be keyed by entity type.
+   *   Under each key, there should be an array of entities keyed by their IDs.
+   * @param array $mergeOptions
+   *   Configure how the merge should work. Valid keys are:
+   *     - sample: (boolean, default: FALSE) whether to output a sample PDF
+   *     - flatten: (boolean, default: TRUE) whether the merged PDF should have
+   *       its fields made permanent and no longer editable.
+   *   It is safe to pass in a FillPDF $context array. Merge options are a
+   *   subset of that.
+   *
+   * @return string
+   *   The merged PDF data.
+   */
+  public function merge(FillPdfFormInterface $fillPdfForm, array $entities, array $mergeOptions = []): string;
+
+}
diff --git a/src/Service/FillPdfLinkManipulator.php b/src/Service/FillPdfLinkManipulator.php
index 7499c71..e39f3bc 100644
--- a/src/Service/FillPdfLinkManipulator.php
+++ b/src/Service/FillPdfLinkManipulator.php
@@ -66,12 +66,12 @@ class FillPdfLinkManipulator implements FillPdfLinkManipulatorInterface {
     }
 
     if (empty($query['fid'])) {
-      throw new \InvalidArgumentException('No FillPdfForm was specified in the query string, so failing.');
+      throw new \InvalidArgumentException('No FillPDF Form was specified in the query string, so failing.');
     }
 
     $fillpdf_form = FillPdfForm::load($query['fid']);
     if (!$fillpdf_form) {
-      throw new \InvalidArgumentException("The requested FillPdfForm doesn't exist, so failing.");
+      throw new \InvalidArgumentException("The requested FillPDF Form doesn't exist, so failing.");
     }
 
     // Set the fid, merging in evaluated boolean flags.
@@ -175,7 +175,7 @@ class FillPdfLinkManipulator implements FillPdfLinkManipulatorInterface {
    */
   public function generateLink(array $parameters) {
     if (!isset($parameters['fid'])) {
-      throw new \InvalidArgumentException("The $parameters argument must contain the fid key (the FillPdfForm's ID).");
+      throw new \InvalidArgumentException("The \$parameters argument must contain the fid key (the FillPDF Form's ID).");
     }
 
     $query = [
diff --git a/tests/src/Functional/FillPdfFormDeleteFormTest.php b/tests/src/Functional/FillPdfFormDeleteFormTest.php
index 0ab7173..8387fc0 100644
--- a/tests/src/Functional/FillPdfFormDeleteFormTest.php
+++ b/tests/src/Functional/FillPdfFormDeleteFormTest.php
@@ -17,7 +17,7 @@ class FillPdfFormDeleteFormTest extends BrowserTestBase {
   use TestFillPdfTrait;
 
   static public $modules = ['fillpdf_test'];
-  protected $profile = 'minimal';
+  protected $defaultTheme = 'stark';
 
   /**
    * {@inheritdoc}
diff --git a/tests/src/Functional/FillPdfFormDuplicateFormTest.php b/tests/src/Functional/FillPdfFormDuplicateFormTest.php
index 6eb38ce..4365952 100644
--- a/tests/src/Functional/FillPdfFormDuplicateFormTest.php
+++ b/tests/src/Functional/FillPdfFormDuplicateFormTest.php
@@ -17,7 +17,7 @@ class FillPdfFormDuplicateFormTest extends BrowserTestBase {
   use TestFillPdfTrait;
 
   static public $modules = ['fillpdf_test'];
-  protected $profile = 'minimal';
+  protected $defaultTheme = 'stark';
 
   /**
    * {@inheritdoc}
diff --git a/tests/src/Functional/FillPdfFormImportFormTest.php b/tests/src/Functional/FillPdfFormImportFormTest.php
index 045bbcc..bff0cc5 100644
--- a/tests/src/Functional/FillPdfFormImportFormTest.php
+++ b/tests/src/Functional/FillPdfFormImportFormTest.php
@@ -15,7 +15,7 @@ class FillPdfFormImportFormTest extends BrowserTestBase {
   use TestFillPdfTrait;
 
   static public $modules = ['fillpdf_test'];
-  protected $profile = 'minimal';
+  protected $defaultTheme = 'stark';
 
   /**
    * {@inheritdoc}
diff --git a/tests/src/Functional/FillPdfTestBase.php b/tests/src/Functional/FillPdfTestBase.php
index fcf6dae..c097733 100644
--- a/tests/src/Functional/FillPdfTestBase.php
+++ b/tests/src/Functional/FillPdfTestBase.php
@@ -16,11 +16,9 @@ abstract class FillPdfTestBase extends FileFieldTestBase {
   use TestImageFieldTrait;
 
   /**
-   * The profile to install as a basis for testing.
-   *
    * @var string
    */
-  protected $profile = 'minimal';
+  protected $defaultTheme = 'stark';
 
   /**
    * Modules to enable.
diff --git a/tests/src/Functional/FillPdfUploadTestBase.php b/tests/src/Functional/FillPdfUploadTestBase.php
index 2a12821..8a1f679 100644
--- a/tests/src/Functional/FillPdfUploadTestBase.php
+++ b/tests/src/Functional/FillPdfUploadTestBase.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\fillpdf\Functional;
 
+use Drupal;
+use Drupal\Component\Version\Constraint;
 use Drupal\file\Entity\File;
 use Drupal\fillpdf\Component\Utility\FillPdf;
 use Drupal\Component\Render\FormattableMarkup;
@@ -90,9 +92,16 @@ abstract class FillPdfUploadTestBase  extends FillPdfTestBase {
 
     // Whether submitted or just uploaded, at least temporarily the file should
     // exist now both as an object and physically on the disk.
+    /** @var \Drupal\file\FileInterface $new_file */
     $new_file = File::load($this->getLastFileId());
     $new_filename = $new_file->getFilename();
-    $this->assertFileExists($new_file);
+    if (version_compare(Drupal::VERSION, '8.8.0', '<')) {
+      // @todo: REMOVE when Drupal 8.7.x is no longer supported.
+      $this->assertFileExists($new_file);
+    }
+    else {
+      $this->assertFileExists($new_file->getFileUri());
+    }
     $this->assertLessThan((int) $new_file->id(), $previous_file_id);
 
     // If the same file was previously uploaded, it should have a "_0" appendix.
@@ -112,7 +121,13 @@ abstract class FillPdfUploadTestBase  extends FillPdfTestBase {
         // Now remove the PDF file again. The temporary file should now be
         // removed both from the disk and the database.
         $this->drupalPostForm(NULL, NULL, self::OP_REMOVE);
-        $this->assertFileNotExists($new_file);
+        if (version_compare(Drupal::VERSION, '8.8.0', '<')) {
+          // @todo: REMOVE when Drupal 8.7.x is no longer supported.
+          $this->assertFileNotExists($new_file);
+        }
+        else {
+          $this->assertFileNotExists($new_file->getFileUri());
+        }
         // @todo Simplify once Core bug gets fixed.
         //   See: https://www.drupal.org/project/drupal/issues/3043127.
         $this->assertFileEntryNotExists($new_file, NULL);
diff --git a/tests/src/Functional/LinkManipulatorTest.php b/tests/src/Functional/LinkManipulatorTest.php
index ac21a19..e682ff1 100644
--- a/tests/src/Functional/LinkManipulatorTest.php
+++ b/tests/src/Functional/LinkManipulatorTest.php
@@ -18,7 +18,7 @@ class LinkManipulatorTest extends BrowserTestBase {
   use TestFillPdfTrait;
 
   static public $modules = ['fillpdf_test'];
-  protected $profile = 'minimal';
+  protected $defaultTheme = 'stark';
 
   /**
    * The FillPDF link manipulator service.
@@ -58,7 +58,7 @@ class LinkManipulatorTest extends BrowserTestBase {
     $this->drupalGet($fillpdf_route);
     // Ensure the exception is converted to an error and access is denied.
     $this->assertSession()->statusCodeEquals(403);
-    $this->assertSession()->pageTextContains("No FillPdfForm was specified in the query string, so failing.");
+    $this->assertSession()->pageTextContains("No FillPDF Form was specified in the query string, so failing.");
 
     // Hit the generation route with a non-existing fid set.
     $fillpdf_route = Url::fromRoute('fillpdf.populate_pdf', [], [
@@ -69,7 +69,7 @@ class LinkManipulatorTest extends BrowserTestBase {
     $this->drupalGet($fillpdf_route);
     // Ensure the exception is converted to an error and access is denied.
     $this->assertSession()->statusCodeEquals(403);
-    $this->assertSession()->pageTextContains("The requested FillPdfForm doesn't exist, so failing.");
+    $this->assertSession()->pageTextContains("The requested FillPDF Form doesn't exist, so failing.");
   }
 
   /**
diff --git a/tests/src/Functional/PdfPopulationTest.php b/tests/src/Functional/PdfPopulationTest.php
index 21d5106..1425cb2 100644
--- a/tests/src/Functional/PdfPopulationTest.php
+++ b/tests/src/Functional/PdfPopulationTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\Tests\fillpdf\Functional;
 
-use Drupal\Tests\taxonomy\Functional\TaxonomyTestTrait;
 use Drupal\file\Entity\File;
 use Drupal\fillpdf\Component\Utility\FillPdf;
 use Drupal\fillpdf\Entity\FillPdfForm;
@@ -11,6 +10,14 @@ use Drupal\fillpdf\FieldMapping\TextFieldMapping;
 use Drupal\fillpdf_test\Plugin\FillPdfBackend\TestFillPdfBackend;
 use Drupal\user\Entity\Role;
 
+// When 8.7.x is fully EOL, this can be removed.
+use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
+
+if (!trait_exists('\Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait')) {
+  class_alias('\Drupal\Tests\taxonomy\Functional\TaxonomyTestTrait', '\Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait');
+}
+
+
 /**
  * Tests Core entity population and image stamping.
  *
@@ -19,7 +26,6 @@ use Drupal\user\Entity\Role;
 class PdfPopulationTest extends FillPdfTestBase {
 
   use TaxonomyTestTrait;
-
   /**
    * Modules to enable.
    *
@@ -242,6 +248,53 @@ class PdfPopulationTest extends FillPdfTestBase {
     self::assertInstanceOf(ImageFieldMapping::class, $merge_state['field_mapping']['Image1'], 'Field "Image1" was mapped to an ImageFieldMapping object.');
   }
 
+  /**
+   * Tests that merging with the backend proxy works.
+   */
+  public function testProxyMerge() {
+    $this->uploadTestPdf('fillpdf_test_v3.pdf');
+    $fillpdf_form = FillPdfForm::load($this->getLatestFillPdfForm());
+
+    // Instantiate the backend proxy (which uses the configured backend).
+    /** @var \Drupal\fillpdf\Service\BackendProxyInterface $merge_proxy */
+    $merge_proxy = $this->container->get('fillpdf.backend_proxy');
+
+    $original_pdf = file_get_contents($this->getTestPdfPath('fillpdf_test_v3.pdf'));
+
+    $this->mapFillPdfFieldsToEntityFields('node', $fillpdf_form->getFormFields());
+
+    // Create a node to populate the FillPdf Form.
+    // The content of this node is not important; we just need an entity to
+    // pass.
+    $node = $this->createNode([
+      'title' => 'Hello & how are you?',
+      'type' => 'article',
+      'body' => [
+        [
+          'value' => "<p>PDF form fields don't accept <em>any</em> HTML.</p>",
+          'format' => 'restricted_html',
+        ],
+      ],
+    ]);
+    $entities['node'] = [$node->id() => $node];
+
+    // Test merging via the proxy.
+    $merged_pdf = $merge_proxy->merge($fillpdf_form, $entities);
+    self::assertEquals($original_pdf, $merged_pdf);
+
+    $merge_state = $this->container->get('state')
+      ->get('fillpdf_test.last_populated_metadata');
+    self::assertInternalType('array', $merge_state, 'Test backend was used.');
+    self::assertArrayHasKey('field_mapping', $merge_state, 'field_mapping key from test backend is present.');
+    self::assertArrayHasKey('context', $merge_state, 'context key from test backend is present.');
+
+    // These are not that important. They just work because of other tests.
+    // We're just testing that token replacement works in general, not the
+    // details of it. We have other tests for that.
+    self::assertEquals('Hello & how are you doing?', $merge_state['field_mapping']['fields']['TextField1']);
+    self::assertEquals("PDF form fields don't accept any HTML.\n", $merge_state['field_mapping']['fields']['TextField2']);
+  }
+
   /**
    * Maps FillPdf fields to entity fields.
    *
@@ -259,6 +312,7 @@ class PdfPopulationTest extends FillPdfTestBase {
       switch ($pdf_key) {
         case 'ImageField':
         case 'Button2':
+        case 'TestButton':
           $field->value = "[$token_type:field_fillpdf_test_image]";
           break;
 
diff --git a/tests/src/Functional/UninstallTest.php b/tests/src/Functional/UninstallTest.php
index 63da225..703a8a2 100644
--- a/tests/src/Functional/UninstallTest.php
+++ b/tests/src/Functional/UninstallTest.php
@@ -16,8 +16,7 @@ class UninstallTest extends BrowserTestBase {
   use TestFillPdfTrait;
 
   static public $modules = ['fillpdf_test'];
-
-  protected $profile = 'minimal';
+  protected $defaultTheme = 'stark';
 
   /**
    * A user with administrative permissions.
-- 
GitLab