Verified Commit 014ef571 authored by Dave Long's avatar Dave Long
Browse files

feat: #3066751 Add resolvable_uri property to LinkItem for APIs

By: justafish
By: jibran
By: lauriii
By: duozersk
By: jonnyeom
By: alexpott
By: vsujeetkumar
By: andregp
By: smustgrave
By: wim leers
By: greg__
By: kristen pol
By: nitesh sethia
By: dcam
(cherry picked from commit ff86b9f9)
parent 8dc2b3dd
Loading
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ protected function createEntity() {
      'title' => 'Llama Gabilondo',
      'description' => 'Llama Gabilondo',
      'link' => 'https://nl.wikipedia.org/wiki/Llama',
      'resolvable_uri' => 'https://nl.wikipedia.org/wiki/Llama',
      'weight' => 0,
      'menu_name' => 'main',
    ]);
@@ -117,6 +118,7 @@ protected function getExpectedDocument(): array {
          'bundle' => 'menu_link_content',
          'link' => [
            'uri' => 'https://nl.wikipedia.org/wiki/Llama',
            'resolvable_uri' => 'https://nl.wikipedia.org/wiki/Llama',
            'title' => NULL,
            'options' => [],
          ],
+2 −0
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ public function testNormalizeFieldItem(): void {
          'options' => [
            'query' => 'foo=bar',
          ],
          'resolvable_uri' => 'https://www.drupal.org',
        ],
      ],
      'internal_property_value' => [
@@ -112,6 +113,7 @@ public function testNormalizeFieldItem(): void {
      'options' => [
        'query' => 'foo=bar',
      ],
      'resolvable_uri' => 'https://www.drupal.org',
    ], $result->getNormalization());

    // Verify a field with one public property and one internal only returns the
+92 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\link\Plugin\DataType;

use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\Attribute\DataType;
use Drupal\Core\TypedData\Plugin\DataType\Uri;
use Drupal\Core\Url;

/**
 * Defines a data type for a Link Resolvable URI.
 */
#[DataType(
  id: 'resolvable_uri',
  label: new TranslatableMarkup('Link Resolvable URI'),
)]
class LinkResolvableUriComputed extends Uri implements CacheableDependencyInterface {

  /**
   * The generated URL.
   *
   * @var \Drupal\Core\GeneratedUrl|null
   */
  protected $processed = NULL;

  /**
   * {@inheritdoc}
   */
  public function getValue() {
    if ($this->processed !== NULL) {
      return $this->processed->getGeneratedUrl();
    }
    /** @var \Drupal\link\Plugin\Field\FieldType\LinkItem $item */
    $item = $this->getParent();
    $this->processed = $item->getUrl()->toString(TRUE);

    return $this->processed->getGeneratedUrl();
  }

  /**
   * {@inheritdoc}
   */
  public function setValue($value, $notify = TRUE): void {
    if (!empty($value)) {
      $parsed = UrlHelper::parse($value);
      // If the path is not an external URL then add 'internal:' prefix to make
      // it a valid uri.
      if (strpos($parsed['path'], ':') === FALSE) {
        $parsed['path'] = 'internal:' . $parsed['path'];
      }
      $url = Url::fromUri($parsed['path'], [
        'query' => $parsed['query'],
        'fragment' => $parsed['fragment'],
      ]);
      $this->processed = $url->toString(TRUE);
    }
    else {
      $this->processed = NULL;
    }
    // Notify the parent of any changes.
    if ($notify && isset($this->parent)) {
      $this->parent->onChange($this->name);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $this->getValue();
    return $this->processed->getCacheTags();
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    $this->getValue();
    return $this->processed->getCacheContexts();
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    $this->getValue();
    return $this->processed->getCacheMaxAge();
  }

}
+42 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
namespace Drupal\link\Plugin\Field\FieldType;

use Drupal\Component\Utility\Random;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
@@ -51,6 +52,13 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    $properties['uri'] = DataDefinition::create('uri')
      ->setLabel(new TranslatableMarkup('URI'));

    $properties['resolvable_uri'] = DataDefinition::create('resolvable_uri')
      ->setLabel(new TranslatableMarkup('Resolvable URI'))
      ->setDescription(new TranslatableMarkup('The processed URL for this link suitable for using in anchor href attributes.'))
      ->setComputed(TRUE)
      ->setInternal(FALSE)
      ->setReadOnly(TRUE);

    $properties['title'] = DataDefinition::create('string')
      ->setLabel(new TranslatableMarkup('Link text'));

@@ -172,6 +180,34 @@ public function getUrl() {
    return Url::fromUri($this->uri, (array) $this->options);
  }

  /**
   * {@inheritdoc}
   */
  public function onChange($property_name, $notify = TRUE): void {
    // Make sure that the link item values can be kept in sync with computed
    // property url.
    if ($property_name === 'resolvable_uri') {
      $property = $this->get('resolvable_uri');
      if ($url = $property->getValue()) {
        $parsed = UrlHelper::parse($url);
        // If the path is not an external URL then add 'internal:' prefix to
        // make it a valid uri.
        if (strpos($parsed['path'], ':') === FALSE) {
          $parsed['path'] = 'internal:' . $parsed['path'];
        }
        $this->writePropertyValue('uri', $parsed['path']);
        // Only set the options if we have query parameters or a fragment.
        if (!empty($parsed['query']) || !empty($parsed['fragment'])) {
          $this->writePropertyValue('options', [
            'query' => $parsed['query'],
            'fragment' => $parsed['fragment'],
          ]);
        }
      }
    }
    parent::onChange($property_name, $notify);
  }

  /**
   * {@inheritdoc}
   */
@@ -194,6 +230,12 @@ public function setValue($values, $notify = TRUE) {
      ];
    }
    parent::setValue($values, $notify);
    // Support setting the field item with only url property, but make sure
    // values stay in sync if only url property is passed.
    // NULL is a valid value, so we use array_key_exists().
    if (is_array($values) && array_key_exists('resolvable_uri', $values) && !array_key_exists('uri', $values)) {
      $this->onChange('resolvable_uri', FALSE);
    }
  }

}
+41 −4
Original line number Diff line number Diff line
@@ -91,6 +91,7 @@ public function testLinkItem(): void {
      ],
      'external' => TRUE,
    ], $entity->field_test->first()->getUrl()->getOptions());
    $this->assertEquals($url, $entity->field_test->resolvable_uri);
    $entity->name->value = $this->randomMachineName();
    $entity->save();

@@ -99,6 +100,7 @@ public function testLinkItem(): void {
    $entity = EntityTest::load($id);
    $this->assertInstanceOf(FieldItemListInterface::class, $entity->field_test);
    $this->assertInstanceOf(FieldItemInterface::class, $entity->field_test[0]);
    $this->assertEquals($url, $entity->field_test->resolvable_uri);
    $this->assertEquals($parsed_url['path'], $entity->field_test->uri);
    $this->assertEquals($parsed_url['path'], $entity->field_test[0]->uri);
    $this->assertEquals($title, $entity->field_test->title);
@@ -123,24 +125,26 @@ public function testLinkItem(): void {
    $new_class = $this->randomMachineName();
    $entity->field_test->uri = $new_url;
    $entity->field_test->title = $new_title;
    $entity->field_test->first()->get('options')->set('query', NULL);
    $entity->field_test->first()->get('options')->set('query', []);
    $entity->field_test->first()->get('options')->set('attributes', ['class' => $new_class]);
    $this->assertEquals($new_url, $entity->field_test->uri);
    $this->assertEquals($new_title, $entity->field_test->title);
    $this->assertEquals($new_class, $entity->field_test->options['attributes']['class']);
    $this->assertNull($entity->field_test->options['query']);
    $this->assertEmpty($entity->field_test->options['query']);

    // Read changed entity and assert changed values.
    $entity->save();
    $entity = EntityTest::load($id);
    $this->assertEquals($new_url, $entity->field_test->uri);
    $this->assertEquals($entity->field_test->resolvable_uri, $new_url);
    $this->assertEquals($new_title, $entity->field_test->title);
    $this->assertEquals($new_class, $entity->field_test->options['attributes']['class']);

    // Check that if we only set uri the default values for title and options
    // are also initialized.
    // Check that if we only set uri the default values for url, title, and
    // options are also initialized.
    $entity->field_test = ['uri' => 'internal:/node/add'];
    $this->assertEquals('internal:/node/add', $entity->field_test->uri);
    $this->assertEquals($entity->field_test->resolvable_uri, '/node/add');
    $this->assertNull($entity->field_test->title);
    $this->assertSame([], $entity->field_test->options);

@@ -151,6 +155,7 @@ public function testLinkItem(): void {
      'options' => ['query' => NULL],
    ];
    $this->assertEquals('internal:/node/add', $entity->field_test->uri);
    $this->assertEquals($entity->field_test->resolvable_uri, '/node/add');
    $this->assertNull($entity->field_test->title);
    $this->assertNull($entity->field_test->options['query']);

@@ -178,6 +183,38 @@ public function testLinkItem(): void {
    $entity->field_test_external->generateSampleItems();
    $entity->field_test_internal->generateSampleItems();
    $this->entityValidateAndSave($entity);

    // Test setting up computed property also sets up other values.
    $entity = EntityTest::create();
    $url = 'https://www.drupal.org?test_param=test_value#top';
    $parsed_url = UrlHelper::parse($url);
    $entity->field_test->resolvable_uri = $url;
    $this->assertEquals($parsed_url['path'], $entity->field_test->uri);
    $this->assertEquals([
      'query' => $parsed_url['query'],
      'fragment' => $parsed_url['fragment'],
      'external' => TRUE,
    ], $entity->field_test->first()->getUrl()->getOptions());
    $entity->name->value = $this->randomMachineName();
    $entity->save();

    // Verify that the field value is changed.
    $id = $entity->id();
    $entity = EntityTest::load($id);
    $this->assertInstanceOf(FieldItemListInterface::class, $entity->field_test);
    $this->assertInstanceOf(FieldItemInterface::class, $entity->field_test[0]);
    $this->assertEquals($url, $entity->field_test->resolvable_uri);
    $this->assertEquals($entity->field_test->uri, $parsed_url['path']);
    $this->assertEquals($entity->field_test[0]->uri, $parsed_url['path']);
    $this->assertEquals($entity->field_test->options['query'], $parsed_url['query']);
    $this->assertEquals($entity->field_test->options['fragment'], $parsed_url['fragment']);

    // Check that if we only set url the default values for uri, title, and
    // options are also initialized.
    $entity->field_test = ['resolvable_uri' => '/node/add'];
    $this->assertEquals($entity->field_test->uri, 'internal:/node/add');
    $this->assertEquals($entity->field_test->resolvable_uri, '/node/add');
    $this->assertNull($entity->field_test->title);
  }

}
Loading