PSR-13 超媒体链接 解读


PSR-13 超媒体链接 解读


正文

mezzio/mezzio-hal 包

<?php

declare(strict_types=1);

namespace Mezzio\Hal;

use InvalidArgumentException;
use Psr\Link\EvolvableLinkInterface;

use function array_filter;
use function array_reduce;
use function get_class;
use function gettype;
use function in_array;
use function is_array;
use function is_object;
use function is_scalar;
use function is_string;
use function method_exists;
use function sprintf;

class Link implements EvolvableLinkInterface
{
    public const AS_COLLECTION = '__FORCE_COLLECTION__';

    /** @var array */
    private $attributes;

    /** @var string[] Link relation types */
    private $relations;

    /** @var string */
    private $uri;

    /** @var bool Whether or not the link is templated */
    private $isTemplated;

    /**
     * @param string|string[] $relation One or more relations represented by this link.
     * @param array $attributes
     * @throws InvalidArgumentException If $relation is neither a string nor an array.
     * @throws InvalidArgumentException If an array $relation is provided, but one or
     *     more values is not a string.
     */
    public function __construct($relation, string $uri = '', bool $isTemplated = false, array $attributes = [])
    {
        $this->relations   = $this->validateRelation($relation);
        $this->uri         = is_string($uri) ? $uri : (string) $uri;
        $this->isTemplated = $isTemplated;
        $this->attributes  = $this->validateAttributes($attributes);
    }

    /**
     * {@inheritDoc}
     */
    public function getHref()
    {
        return $this->uri;
    }

    /**
     * {@inheritDoc}
     */
    public function isTemplated()
    {
        return $this->isTemplated;
    }

    /**
     * {@inheritDoc}
     */
    public function getRels()
    {
        return $this->relations;
    }

    /**
     * {@inheritDoc}
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * {@inheritDoc}
     *
     * @throws InvalidArgumentException If $href is not a string, and not an
     *     object implementing __toString.
     */
    public function withHref($href)
    {
        if (
            ! is_string($href)
            && ! (is_object($href) && method_exists($href, '__toString'))
        ) {
            throw new InvalidArgumentException(sprintf(
                '%s expects a string URI or an object implementing __toString; received %s',
                __METHOD__,
                is_object($href) ? get_class($href) : gettype($href)
            ));
        }
        $new      = clone $this;
        $new->uri = (string) $href;
        return $new;
    }

    /**
     * {@inheritDoc}
     *
     * @throws InvalidArgumentException If $rel is not a string.
     */
    public function withRel($rel)
    {
        if (! is_string($rel) || empty($rel)) {
            throw new InvalidArgumentException(sprintf(
                '%s expects a non-empty string relation type; received %s',
                __METHOD__,
                is_object($rel) ? get_class($rel) : gettype($rel)
            ));
        }

        if (in_array($rel, $this->relations, true)) {
            return $this;
        }

        $new              = clone $this;
        $new->relations[] = $rel;
        return $new;
    }

    /**
     * {@inheritDoc}
     */
    public function withoutRel($rel)
    {
        if (! is_string($rel) || empty($rel)) {
            return $this;
        }

        if (! in_array($rel, $this->relations, true)) {
            return $this;
        }

        $new            = clone $this;
        $new->relations = array_filter($this->relations, function ($value) use ($rel) {
            return $rel !== $value;
        });
        return $new;
    }

    /**
     * {@inheritDoc}
     *
     * @throws InvalidArgumentException If $attribute is not a string or is empty.
     * @throws InvalidArgumentException If $value is neither a scalar nor an array.
     * @throws InvalidArgumentException If $value is an array, but one or more values
     *     is not a string.
     */
    public function withAttribute($attribute, $value)
    {
        $this->validateAttributeName($attribute, __METHOD__);
        $this->validateAttributeValue($value, __METHOD__);

        $new                         = clone $this;
        $new->attributes[$attribute] = $value;
        return $new;
    }

    /**
     * {@inheritDoc}
     */
    public function withoutAttribute($attribute)
    {
        if (! is_string($attribute) || empty($attribute)) {
            return $this;
        }

        if (! isset($this->attributes[$attribute])) {
            return $this;
        }

        $new = clone $this;
        unset($new->attributes[$attribute]);
        return $new;
    }

    /**
     * @param mixed $name
     * @throws InvalidArgumentException If $attribute is not a string or is empty.
     */
    private function validateAttributeName($name, string $context): void
    {
        if (! is_string($name) || empty($name)) {
            throw new InvalidArgumentException(sprintf(
                '%s expects the $name argument to be a non-empty string; received %s',
                $context,
                is_object($name) ? get_class($name) : gettype($name)
            ));
        }
    }

    /**
     * @param mixed $value
     * @throws InvalidArgumentException If $value is neither a scalar nor an array.
     * @throws InvalidArgumentException If $value is an array, but one or more values
     *     is not a string.
     */
    private function validateAttributeValue($value, string $context): void
    {
        if (! is_scalar($value) && ! is_array($value)) {
            throw new InvalidArgumentException(sprintf(
                '%s expects the $value to be a PHP primitive or array of strings; received %s',
                $context,
                is_object($value) ? get_class($value) : gettype($value)
            ));
        }

        if (
            is_array($value) && array_reduce($value, function ($isInvalid, $value) {
                return $isInvalid || ! is_string($value);
            }, false)
        ) {
            throw new InvalidArgumentException(sprintf(
                '%s expects $value to contain an array of strings; one or more values was not a string',
                $context
            ));
        }
    }

    private function validateAttributes(array $attributes): array
    {
        foreach ($attributes as $name => $value) {
            $this->validateAttributeName($name, self::class);
            $this->validateAttributeValue($value, self::class);
        }
        return $attributes;
    }

    /**
     * @param mixed $relation
     * @return string|string[]
     * @throws InvalidArgumentException If $relation is neither a string nor an array.
     * @throws InvalidArgumentException If $relation is an array, but any given value in it is not a string.
     */
    private function validateRelation($relation)
    {
        if (! is_array($relation) && (! is_string($relation) || empty($relation))) {
            throw new InvalidArgumentException(sprintf(
                '$relation argument must be a string or array of strings; received %s',
                is_object($relation) ? get_class($relation) : gettype($relation)
            ));
        }

        if (
            is_array($relation) && false === array_reduce($relation, function ($isString, $value) {
                return $isString === false || is_string($value) || empty($value);
            }, true)
        ) {
            throw new InvalidArgumentException(
                'When passing an array for $relation, each value must be a non-empty string; '
                . 'one or more non-string or empty values were present'
            );
        }

        return is_string($relation) ? [$relation] : $relation;
    }
}

jbboehr/php-psr 中提供了 PSR 的实现实例,其中发现一个 PSR13 的实现实例: php-fig/link-util 包。

Relations.php

<?php

/**
 * Standard relation names.
 *
 * This file is auto-generated.  Do not edit directly.  Edit or re-run `rebuild-rels.php` if necessary.
 */

declare(strict_types=1);

namespace Fig\Link;

/**
 * Standard relation names.
 *
 * This interface provides convenience constants for standard relationships defined by IANA. They are not required,
 * but are useful for avoiding typos and similar such errors.
 *
 * This interface may be referenced directly like so:
 *
 * Relations::REL_UP
 *
 * Or you may implement this interface in your class and then refer to the constants locally:
 *
 * static::REL_UP
 */
interface Relations
{
    /**
     * Refers to a resource that is the subject of the link's context.
     *
     * @see https://tools.ietf.org/html/rfc6903
     */
    const REL_ABOUT = 'about';

    /**
     * Refers to a substitute for this context
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-alternate
     */
    const REL_ALTERNATE = 'alternate';

    /**
     * Used to reference alternative content that uses the AMP profile of the HTML format.
     *
     * @see https://amp.dev/documentation/guides-and-tutorials/learn/spec/amphtml/
     */
    const REL_AMPHTML = 'amphtml';

    /**
     * Refers to an appendix.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_APPENDIX = 'appendix';

    /**
     * Refers to an icon for the context. Synonym for icon.
     *
     * @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html#//apple_ref/doc/uid/TP40002051-CH3-SW3
     */
    const REL_APPLE_TOUCH_ICON = 'apple-touch-icon';

    /**
     * Refers to a launch screen for the context.
     *
     * @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html#//apple_ref/doc/uid/TP40002051-CH3-SW3
     */
    const REL_APPLE_TOUCH_STARTUP_IMAGE = 'apple-touch-startup-image';

    /**
     * Refers to a collection of records, documents, or other materials of historical interest.
     *
     * @see http://www.w3.org/TR/2011/WD-html5-20110113/links.html#rel-archives
     */
    const REL_ARCHIVES = 'archives';

    /**
     * Refers to the context's author.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-author
     */
    const REL_AUTHOR = 'author';

    /**
     * Identifies the entity that blocks access to a resource following receipt of a legal demand.
     *
     * @see https://tools.ietf.org/html/rfc7725
     */
    const REL_BLOCKED_BY = 'blocked-by';

    /**
     * Gives a permanent link to use for bookmarking purposes.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-bookmark
     */
    const REL_BOOKMARK = 'bookmark';

    /**
     * Designates the preferred version of a resource (the IRI and its contents).
     *
     * @see https://tools.ietf.org/html/rfc6596
     */
    const REL_CANONICAL = 'canonical';

    /**
     * Refers to a chapter in a collection of resources.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_CHAPTER = 'chapter';

    /**
     * Indicates that the link target is preferred over the link context for the purpose of permanent citation.
     *
     * @see https://tools.ietf.org/html/rfc8574
     */
    const REL_CITE_AS = 'cite-as';

    /**
     * The target IRI points to a resource which represents the collection resource for the context IRI.
     *
     * @see https://tools.ietf.org/html/rfc6573
     */
    const REL_COLLECTION = 'collection';

    /**
     * Refers to a table of contents.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_CONTENTS = 'contents';

    /**
     * The document linked to was later converted to the document that contains this link relation. For example, an RFC can
     * have a link to the Internet-Draft that became the RFC; in that case, the link relation would be "convertedFrom".
     *
     * This relation is different than "predecessor-version" in that "predecessor-version" is for items in a version control
     * system. It is also different than "previous" in that this relation is used for converted resources, not those that are
     * part of a sequence of resources.
     *
     * @see https://tools.ietf.org/html/rfc7991
     */
    const REL_CONVERTEDFROM = 'convertedFrom';

    /**
     * Refers to a copyright statement that applies to the link's context.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_COPYRIGHT = 'copyright';

    /**
     * The target IRI points to a resource where a submission form can be obtained.
     *
     * @see https://tools.ietf.org/html/rfc6861
     */
    const REL_CREATE_FORM = 'create-form';

    /**
     * Refers to a resource containing the most recent item(s) in a collection of resources.
     *
     * @see https://tools.ietf.org/html/rfc5005
     */
    const REL_CURRENT = 'current';

    /**
     * Refers to a resource providing information about the link's context.
     *
     * @see http://www.w3.org/TR/powder-dr/#assoc-linking
     */
    const REL_DESCRIBEDBY = 'describedby';

    /**
     * The relationship A 'describes' B asserts that resource A provides a description of resource B. There are no constraints
     * on the format or representation of either A or B, neither are there any further constraints on either resource.
     *
     * This link relation type is the inverse of the 'describedby' relation type. While 'describedby' establishes a relation
     * from the described resource back to the resource that describes it, 'describes' established a relation from the
     * describing resource to the resource it describes. If B is 'describedby' A, then A 'describes' B.
     *
     * @see https://tools.ietf.org/html/rfc6892
     */
    const REL_DESCRIBES = 'describes';

    /**
     * Refers to a list of patent disclosures made with respect to material for which 'disclosure' relation is specified.
     *
     * @see https://tools.ietf.org/html/rfc6579
     */
    const REL_DISCLOSURE = 'disclosure';

    /**
     * Used to indicate an origin that will be used to fetch required resources for the link context, and that the user agent
     * ought to resolve as early as possible.
     *
     * @see https://www.w3.org/TR/resource-hints/
     */
    const REL_DNS_PREFETCH = 'dns-prefetch';

    /**
     * Refers to a resource whose available representations are byte-for-byte identical with the corresponding representations
     * of the context IRI.
     *
     * This relation is for static resources. That is, an HTTP GET request on any duplicate will return the same
     * representation. It does not make sense for dynamic or POSTable resources and should not be used for them. 
     *
     * @see https://tools.ietf.org/html/rfc6249
     */
    const REL_DUPLICATE = 'duplicate';

    /**
     * Refers to a resource that can be used to edit the link's context.
     *
     * @see https://tools.ietf.org/html/rfc5023
     */
    const REL_EDIT = 'edit';

    /**
     * The target IRI points to a resource where a submission form for editing associated resource can be obtained.
     *
     * @see https://tools.ietf.org/html/rfc6861
     */
    const REL_EDIT_FORM = 'edit-form';

    /**
     * Refers to a resource that can be used to edit media associated with the link's context.
     *
     * @see https://tools.ietf.org/html/rfc5023
     */
    const REL_EDIT_MEDIA = 'edit-media';

    /**
     * Identifies a related resource that is potentially large and might require special handling.
     *
     * @see https://tools.ietf.org/html/rfc4287
     */
    const REL_ENCLOSURE = 'enclosure';

    /**
     * Refers to a resource that is not part of the same site as the current context.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-external
     */
    const REL_EXTERNAL = 'external';

    /**
     * An IRI that refers to the furthest preceding resource in a series of resources.
     *
     * This relation type registration did not indicate a reference. Originally requested by Mark Nottingham in December 2004. 
     *
     * @see https://tools.ietf.org/html/rfc8288
     */
    const REL_FIRST = 'first';

    /**
     * Refers to a glossary of terms.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_GLOSSARY = 'glossary';

    /**
     * Refers to context-sensitive help.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-help
     */
    const REL_HELP = 'help';

    /**
     * Refers to a resource hosted by the server indicated by the link context.
     *
     * This relation is used in CoRE where links are retrieved as a "/.well-known/core" resource representation, and is the
     * default relation type in the CoRE Link Format.
     *
     * @see https://tools.ietf.org/html/rfc6690
     */
    const REL_HOSTS = 'hosts';

    /**
     * Refers to a hub that enables registration for notification of updates to the context.
     *
     * This relation type was requested by Brett Slatkin.
     *
     * @see https://www.w3.org/TR/websub/
     */
    const REL_HUB = 'hub';

    /**
     * Refers to an icon representing the link's context.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#rel-icon
     */
    const REL_ICON = 'icon';

    /**
     * Refers to an index.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_INDEX = 'index';

    /**
     * refers to a resource associated with a time interval that ends before the beginning of the time interval associated with
     * the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalAfter
     */
    const REL_INTERVALAFTER = 'intervalAfter';

    /**
     * refers to a resource associated with a time interval that begins after the end of the time interval associated with the
     * context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalBefore
     */
    const REL_INTERVALBEFORE = 'intervalBefore';

    /**
     * refers to a resource associated with a time interval that begins after the beginning of the time interval associated
     * with the context resource, and ends before the end of the time interval associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalContains
     */
    const REL_INTERVALCONTAINS = 'intervalContains';

    /**
     * refers to a resource associated with a time interval that begins after the end of the time interval associated with the
     * context resource, or ends before the beginning of the time interval associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalDisjoint
     */
    const REL_INTERVALDISJOINT = 'intervalDisjoint';

    /**
     * refers to a resource associated with a time interval that begins before the beginning of the time interval associated
     * with the context resource, and ends after the end of the time interval associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalDuring
     */
    const REL_INTERVALDURING = 'intervalDuring';

    /**
     * refers to a resource associated with a time interval whose beginning coincides with the beginning of the time interval
     * associated with the context resource, and whose end coincides with the end of the time interval associated with the
     * context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalEquals
     */
    const REL_INTERVALEQUALS = 'intervalEquals';

    /**
     * refers to a resource associated with a time interval that begins after the beginning of the time interval associated
     * with the context resource, and whose end coincides with the end of the time interval associated with the context
     * resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalFinishedBy
     */
    const REL_INTERVALFINISHEDBY = 'intervalFinishedBy';

    /**
     * refers to a resource associated with a time interval that begins before the beginning of the time interval associated
     * with the context resource, and whose end coincides with the end of the time interval associated with the context
     * resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalFinishes
     */
    const REL_INTERVALFINISHES = 'intervalFinishes';

    /**
     * refers to a resource associated with a time interval that begins before or is coincident with the beginning of the time
     * interval associated with the context resource, and ends after or is coincident with the end of the time interval
     * associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalIn
     */
    const REL_INTERVALIN = 'intervalIn';

    /**
     * refers to a resource associated with a time interval whose beginning coincides with the end of the time interval
     * associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalMeets
     */
    const REL_INTERVALMEETS = 'intervalMeets';

    /**
     * refers to a resource associated with a time interval whose end coincides with the beginning of the time interval
     * associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalMetBy
     */
    const REL_INTERVALMETBY = 'intervalMetBy';

    /**
     * refers to a resource associated with a time interval that begins before the beginning of the time interval associated
     * with the context resource, and ends after the beginning of the time interval associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalOverlappedBy
     */
    const REL_INTERVALOVERLAPPEDBY = 'intervalOverlappedBy';

    /**
     * refers to a resource associated with a time interval that begins before the end of the time interval associated with the
     * context resource, and ends after the end of the time interval associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalOverlaps
     */
    const REL_INTERVALOVERLAPS = 'intervalOverlaps';

    /**
     * refers to a resource associated with a time interval whose beginning coincides with the beginning of the time interval
     * associated with the context resource, and ends before the end of the time interval associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalStartedBy
     */
    const REL_INTERVALSTARTEDBY = 'intervalStartedBy';

    /**
     * refers to a resource associated with a time interval whose beginning coincides with the beginning of the time interval
     * associated with the context resource, and ends after the end of the time interval associated with the context resource
     *
     * @see https://www.w3.org/TR/owl-time/#time:intervalStarts
     */
    const REL_INTERVALSTARTS = 'intervalStarts';

    /**
     * The target IRI points to a resource that is a member of the collection represented by the context IRI.
     *
     * @see https://tools.ietf.org/html/rfc6573
     */
    const REL_ITEM = 'item';

    /**
     * An IRI that refers to the furthest following resource in a series of resources.
     *
     * This relation type registration did not indicate a reference. Originally requested by Mark Nottingham in December 2004. 
     *
     * @see https://tools.ietf.org/html/rfc8288
     */
    const REL_LAST = 'last';

    /**
     * Points to a resource containing the latest (e.g., current) version of the context.
     *
     * @see https://tools.ietf.org/html/rfc5829
     */
    const REL_LATEST_VERSION = 'latest-version';

    /**
     * Refers to a license associated with this context.
     *
     * For implications of use in HTML, see: http://www.w3.org/TR/html5/links.html#link-type-license
     *
     * @see https://tools.ietf.org/html/rfc4946
     */
    const REL_LICENSE = 'license';

    /**
     * Refers to further information about the link's context, expressed as a LRDD ("Link-based Resource Descriptor Document")
     * resource. See for information about processing this relation type in host-meta documents. When used elsewhere, it refers
     * to additional links and other metadata. Multiple instances indicate additional LRDD resources. LRDD resources MUST have
     * an "application/xrd+xml" representation, and MAY have others.
     *
     * @see https://tools.ietf.org/html/rfc6415
     */
    const REL_LRDD = 'lrdd';

    /**
     * Links to a manifest file for the context.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-manifest
     */
    const REL_MANIFEST = 'manifest';

    /**
     * Refers to a mask that can be applied to the icon for the context.
     *
     * @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/pinnedTabs/pinnedTabs.html#//apple_ref/doc/uid/TP40002051-CH18-SW1
     */
    const REL_MASK_ICON = 'mask-icon';

    /**
     * Refers to a feed of personalised media recommendations relevant to the link context.
     *
     * @see https://wicg.github.io/media-feeds/#discovery-of-media-feeds
     */
    const REL_MEDIA_FEED = 'media-feed';

    /**
     * The Target IRI points to a Memento, a fixed resource that will not change state anymore.
     *
     * A Memento for an Original Resource is a resource that encapsulates a prior state of the Original Resource.
     *
     * @see https://tools.ietf.org/html/rfc7089
     */
    const REL_MEMENTO = 'memento';

    /**
     * Links to the context's Micropub endpoint.
     *
     * @see https://www.w3.org/TR/micropub/#endpoint-discovery-p-1
     */
    const REL_MICROPUB = 'micropub';

    /**
     * Refers to a module that the user agent is to preemptively fetch and store for use in the current context.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload
     */
    const REL_MODULEPRELOAD = 'modulepreload';

    /**
     * Refers to a resource that can be used to monitor changes in an HTTP resource. 
     *
     * @see https://tools.ietf.org/html/rfc5989
     */
    const REL_MONITOR = 'monitor';

    /**
     * Refers to a resource that can be used to monitor changes in a specified group of HTTP resources. 
     *
     * @see https://tools.ietf.org/html/rfc5989
     */
    const REL_MONITOR_GROUP = 'monitor-group';

    /**
     * Indicates that the link's context is a part of a series, and that the next in the series is the link target. 
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-next
     */
    const REL_NEXT = 'next';

    /**
     * Refers to the immediately following archive resource.
     *
     * @see https://tools.ietf.org/html/rfc5005
     */
    const REL_NEXT_ARCHIVE = 'next-archive';

    /**
     * Indicates that the context’s original author or publisher does not endorse the link target.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-nofollow
     */
    const REL_NOFOLLOW = 'nofollow';

    /**
     * Indicates that any newly created top-level browsing context which results from following the link will not be an
     * auxiliary browsing context.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-noopener
     */
    const REL_NOOPENER = 'noopener';

    /**
     * Indicates that no referrer information is to be leaked when following the link.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer
     */
    const REL_NOREFERRER = 'noreferrer';

    /**
     * Indicates that any newly created top-level browsing context which results from following the link will be an auxiliary
     * browsing context.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-opener
     */
    const REL_OPENER = 'opener';

    /**
     * Refers to an OpenID Authentication server on which the context relies for an assertion that the end user controls an
     * Identifier.
     *
     * @see https://openid.net/specs/openid-authentication-2_0.html#rfc.section.7.3.3
     */
    const REL_OPENID2_LOCAL_ID = 'openid2.local_id';

    /**
     * Refers to a resource which accepts OpenID Authentication protocol messages for the context.
     *
     * @see https://openid.net/specs/openid-authentication-2_0.html#rfc.section.7.3.3
     */
    const REL_OPENID2_PROVIDER = 'openid2.provider';

    /**
     * The Target IRI points to an Original Resource.
     *
     * An Original Resource is a resource that exists or used to exist, and for which access to one of its prior states may be
     * required. 
     *
     * @see https://tools.ietf.org/html/rfc7089
     */
    const REL_ORIGINAL = 'original';

    /**
     * Refers to a P3P privacy policy for the context.
     *
     * @see https://www.w3.org/TR/P3P/#syntax_link
     */
    const REL_P3PV1 = 'P3Pv1';

    /**
     * Indicates a resource where payment is accepted.
     *
     * This relation type registration did not indicate a reference. Requested by Joshua Kinberg and Robert Sayre. It is meant
     * as a general way to facilitate acts of payment, and thus this specification makes no assumptions on the type of payment
     * or transaction protocol. Examples may include a web page where donations are accepted or where goods and services are
     * available for purchase. rel="payment" is not intended to initiate an automated transaction. In Atom documents, a link
     * element with a rel="payment" attribute may exist at the feed/channel level and/or the entry/item level. For example, a
     * rel="payment" link at the feed/channel level may point to a "tip jar" URI, whereas an entry/ item containing a book
     * review may include a rel="payment" link that points to the location where the book may be purchased through an online
     * retailer. 
     *
     * @see https://tools.ietf.org/html/rfc8288
     */
    const REL_PAYMENT = 'payment';

    /**
     * Gives the address of the pingback resource for the link context.
     *
     * @see http://www.hixie.ch/specs/pingback/pingback
     */
    const REL_PINGBACK = 'pingback';

    /**
     * Used to indicate an origin that will be used to fetch required resources for the link context. Initiating an early
     * connection, which includes the DNS lookup, TCP handshake, and optional TLS negotiation, allows the user agent to mask
     * the high latency costs of establishing a connection.
     *
     * @see https://www.w3.org/TR/resource-hints/
     */
    const REL_PRECONNECT = 'preconnect';

    /**
     * Points to a resource containing the predecessor version in the version history. 
     *
     * @see https://tools.ietf.org/html/rfc5829
     */
    const REL_PREDECESSOR_VERSION = 'predecessor-version';

    /**
     * The prefetch link relation type is used to identify a resource that might be required by the next navigation from the
     * link context, and that the user agent ought to fetch, such that the user agent can deliver a faster response once the
     * resource is requested in the future.
     *
     * @see http://www.w3.org/TR/resource-hints/
     */
    const REL_PREFETCH = 'prefetch';

    /**
     * Refers to a resource that should be loaded early in the processing of the link's context, without blocking rendering.
     *
     * Additional target attributes establish the detailed fetch properties of the link.
     *
     * @see http://www.w3.org/TR/preload/
     */
    const REL_PRELOAD = 'preload';

    /**
     * Used to identify a resource that might be required by the next navigation from the link context, and that the user agent
     * ought to fetch and execute, such that the user agent can deliver a faster response once the resource is requested in the
     * future.
     *
     * @see https://www.w3.org/TR/resource-hints/
     */
    const REL_PRERENDER = 'prerender';

    /**
     * Indicates that the link's context is a part of a series, and that the previous in the series is the link target. 
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-prev
     */
    const REL_PREV = 'prev';

    /**
     * Refers to a resource that provides a preview of the link's context.
     *
     * @see https://tools.ietf.org/html/rfc6903
     */
    const REL_PREVIEW = 'preview';

    /**
     * Refers to the previous resource in an ordered series of resources. Synonym for "prev".
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_PREVIOUS = 'previous';

    /**
     * Refers to the immediately preceding archive resource.
     *
     * @see https://tools.ietf.org/html/rfc5005
     */
    const REL_PREV_ARCHIVE = 'prev-archive';

    /**
     * Refers to a privacy policy associated with the link's context.
     *
     * @see https://tools.ietf.org/html/rfc6903
     */
    const REL_PRIVACY_POLICY = 'privacy-policy';

    /**
     * Identifying that a resource representation conforms
     * to a certain profile, without affecting the non-profile semantics
     * of the resource representation.
     *
     * Profile URIs are primarily intended to be used as
     * identifiers, and thus clients SHOULD NOT indiscriminately access
     * profile URIs.
     *
     * @see https://tools.ietf.org/html/rfc6906
     */
    const REL_PROFILE = 'profile';

    /**
     * Links to a publication manifest. A manifest represents structured information about a publication, such as informative
     * metadata, a list of resources, and a default reading order.
     *
     * @see https://www.w3.org/TR/pub-manifest/#link-relation-type-registration
     */
    const REL_PUBLICATION = 'publication';

    /**
     * Identifies a related resource.
     *
     * @see https://tools.ietf.org/html/rfc4287
     */
    const REL_RELATED = 'related';

    /**
     * Identifies the root of RESTCONF API as configured on this HTTP server. The "restconf" relation defines the root of the
     * API defined in RFC8040. Subsequent revisions of RESTCONF will use alternate relation values to support protocol
     * versioning.
     *
     * @see https://tools.ietf.org/html/rfc8040
     */
    const REL_RESTCONF = 'restconf';

    /**
     * Identifies a resource that is a reply to the context of the link. 
     *
     * @see https://tools.ietf.org/html/rfc4685
     */
    const REL_REPLIES = 'replies';

    /**
     * The resource identified by the link target provides an input value to an instance of a rule, where the resource which
     * represents the rule instance is identified by the link context. 
     *
     * @see https://openconnectivity.org/specs/OCF_Core_Optional_Specification_v2.2.0.pdf
     */
    const REL_RULEINPUT = 'ruleinput';

    /**
     * Refers to a resource that can be used to search through the link's context and related resources.
     *
     * @see http://www.opensearch.org/Specifications/OpenSearch/1.1
     */
    const REL_SEARCH = 'search';

    /**
     * Refers to a section in a collection of resources.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_SECTION = 'section';

    /**
     * Conveys an identifier for the link's context. 
     *
     * @see https://tools.ietf.org/html/rfc4287
     */
    const REL_SELF = 'self';

    /**
     * Indicates a URI that can be used to retrieve a service document.
     *
     * When used in an Atom document, this relation type specifies Atom Publishing Protocol service documents by default.
     * Requested by James Snell. 
     *
     * @see https://tools.ietf.org/html/rfc5023
     */
    const REL_SERVICE = 'service';

    /**
     * Identifies service description for the context that is primarily intended for consumption by machines.
     *
     * @see https://tools.ietf.org/html/rfc8631
     */
    const REL_SERVICE_DESC = 'service-desc';

    /**
     * Identifies service documentation for the context that is primarily intended for human consumption.
     *
     * @see https://tools.ietf.org/html/rfc8631
     */
    const REL_SERVICE_DOC = 'service-doc';

    /**
     * Identifies general metadata for the context that is primarily intended for consumption by machines.
     *
     * @see https://tools.ietf.org/html/rfc8631
     */
    const REL_SERVICE_META = 'service-meta';

    /**
     * Refers to a resource that is within a context that is sponsored (such as advertising or another compensation agreement).
     *
     * @see https://webmasters.googleblog.com/2019/09/evolving-nofollow-new-ways-to-identify.html
     */
    const REL_SPONSORED = 'sponsored';

    /**
     * Refers to the first resource in a collection of resources.
     *
     * @see https://www.w3.org/TR/html401
     */
    const REL_START = 'start';

    /**
     * Identifies a resource that represents the context's status.
     *
     * @see https://tools.ietf.org/html/rfc8631
     */
    const REL_STATUS = 'status';

    /**
     * Refers to a stylesheet.
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
     */
    const REL_STYLESHEET = 'stylesheet';

    /**
     * Refers to a resource serving as a subsection in a collection of resources.
     *
     * @see https://www.w3.org/TR/html401/
     */
    const REL_SUBSECTION = 'subsection';

    /**
     * Points to a resource containing the successor version in the version history. 
     *
     * @see https://tools.ietf.org/html/rfc5829
     */
    const REL_SUCCESSOR_VERSION = 'successor-version';

    /**
     * Identifies a resource that provides information about the context's retirement policy. 
     *
     * @see https://tools.ietf.org/html/rfc8594
     */
    const REL_SUNSET = 'sunset';

    /**
     * Gives a tag (identified by the given address) that applies to the current document. 
     *
     * @see https://html.spec.whatwg.org/multipage/links.html#link-type-tag
     */
    const REL_TAG = 'tag';

    /**
     * Refers to the terms of service associated with the link's context.
     *
     * @see https://tools.ietf.org/html/rfc6903
     */
    const REL_TERMS_OF_SERVICE = 'terms-of-service';

    /**
     * The Target IRI points to a TimeGate for an Original Resource.
     *
     * A TimeGate for an Original Resource is a resource that is capable of datetime negotiation to support access to prior
     * states of the Original Resource. 
     *
     * @see https://tools.ietf.org/html/rfc7089
     */
    const REL_TIMEGATE = 'timegate';

    /**
     * The Target IRI points to a TimeMap for an Original Resource.
     *
     * A TimeMap for an Original Resource is a resource from which a list of URIs of Mementos of the Original Resource is
     * available. 
     *
     * @see https://tools.ietf.org/html/rfc7089
     */
    const REL_TIMEMAP = 'timemap';

    /**
     * Refers to a resource identifying the abstract semantic type of which the link's context is considered to be an instance.
     *
     * @see https://tools.ietf.org/html/rfc6903
     */
    const REL_TYPE = 'type';

    /**
     * Refers to a resource that is within a context that is User Generated Content. 
     *
     * @see https://webmasters.googleblog.com/2019/09/evolving-nofollow-new-ways-to-identify.html
     */
    const REL_UGC = 'ugc';

    /**
     * Refers to a parent document in a hierarchy of documents. 
     *
     * This relation type registration did not indicate a reference. Requested by Noah Slater.
     *
     * @see https://tools.ietf.org/html/rfc8288
     */
    const REL_UP = 'up';

    /**
     * Points to a resource containing the version history for the context. 
     *
     * @see https://tools.ietf.org/html/rfc5829
     */
    const REL_VERSION_HISTORY = 'version-history';

    /**
     * Identifies a resource that is the source of the information in the link's context. 
     *
     * @see https://tools.ietf.org/html/rfc4287
     */
    const REL_VIA = 'via';

    /**
     * Identifies a target URI that supports the Webmention protocol. This allows clients that mention a resource in some form
     * of publishing process to contact that endpoint and inform it that this resource has been mentioned.
     *
     * This is a similar "Linkback" mechanism to the ones of Refback, Trackback, and Pingback. It uses a different protocol,
     * though, and thus should be discoverable through its own link relation type.
     *
     * @see http://www.w3.org/TR/webmention/
     */
    const REL_WEBMENTION = 'webmention';

    /**
     * Points to a working copy for this resource.
     *
     * @see https://tools.ietf.org/html/rfc5829
     */
    const REL_WORKING_COPY = 'working-copy';

    /**
     * Points to the versioned resource from which this working copy was obtained. 
     *
     * @see https://tools.ietf.org/html/rfc5829
     */
    const REL_WORKING_COPY_OF = 'working-copy-of';
}

TemplatedHrefTrait.php

<?php


namespace Fig\Link;

trait TemplatedHrefTrait
{
    /**
     * Determines if an href is a templated link or not.
     *
     * @see https://tools.ietf.org/html/rfc6570
     *
     * @param string $href
     *   The href value to check.
     *
     * @return bool
     *   True if the specified href is a templated path, False otherwise.
     */
    private function hrefIsTemplated(string $href): bool
    {
        return str_contains($href, '{') || str_contains($href, '}');
    }
}

LinkTrait.php

<?php

namespace Fig\Link;

use Psr\Link\LinkInterface;

/**
 * Class LinkTrait
 *
 * @inherits LinkInterface
 */
trait LinkTrait
{
    use TemplatedHrefTrait;

    private string $href = '';

    /**
     * The set of rels on this link.
     *
     * Note: Because rels are an exclusive set, we use the keys of the array
     * to store the rels that have been added, not the values. The values
     * are simply boolean true.  A rel is present if the key is set, false
     * otherwise.
     *
     * @var string[]
     */
    private array $rel = [];

    private array $attributes = [];

    /**
     * {@inheritdoc}
     */
    public function getHref(): string
    {
        return $this->href;
    }

    /**
     * {@inheritdoc}
     */
    public function isTemplated(): bool
    {
        return $this->hrefIsTemplated($this->href);
    }

    /**
     * {@inheritdoc}
     */
    public function getRels(): array
    {
        return array_keys($this->rel);
    }

    /**
     * {@inheritdoc}
     */
    public function getAttributes(): array
    {
        return $this->attributes;
    }
}

EvolvableLinkTrait.php

<?php


namespace Fig\Link;

use Psr\Link\EvolvableLinkInterface;

/**
 * Class EvolvableLinkTrait
 *
 * @implements EvolvableLinkInterface
 */
trait EvolvableLinkTrait
{
    use LinkTrait;

    /**
     * {@inheritdoc}
     *
     * @return EvolvableLinkInterface
     */
    public function withHref(\Stringable|string $href): static
    {
        /** @var EvolvableLinkInterface $that */
        $that = clone($this);
        $that->href = $href;

        $that->templated = $this->hrefIsTemplated($href);

        return $that;
    }

    /**
     * {@inheritdoc}
     *
     * @return EvolvableLinkInterface
     */
    public function withRel(string $rel): static
    {
        /** @var EvolvableLinkInterface $that */
        $that = clone($this);
        $that->rel[$rel] = true;
        return $that;
    }

    /**
     * {@inheritdoc}
     *
     * @return EvolvableLinkInterface
     */
    public function withoutRel(string $rel): static
    {
        /** @var EvolvableLinkInterface $that */
        $that = clone($this);
        unset($that->rel[$rel]);
        return $that;
    }

    /**
     * {@inheritdoc}
     *
     * @return EvolvableLinkInterface
     */
    public function withAttribute(string $attribute, string|\Stringable|int|float|bool|array $value): static
    {
        /** @var EvolvableLinkInterface $that */
        $that = clone($this);
        $that->attributes[$attribute] = $value;
        return $that;
    }

    /**
     * {@inheritdoc}
     *
     * @return EvolvableLinkInterface
     */
    public function withoutAttribute(string $attribute): static
    {
        /** @var EvolvableLinkInterface $that */
        $that = clone($this);
        unset($that->attributes[$attribute]);
        return $that;
    }
}

Link.php

<?php


namespace Fig\Link;

use Psr\Link\EvolvableLinkInterface;

class Link implements EvolvableLinkInterface
{
    use EvolvableLinkTrait;

    /**
     * Link constructor.
     *
     * @param string $rel
     *   A single relationship to include on this link.
     * @param string $href
     *   An href for this link.
     */
    public function __construct(string $rel = '', string $href = '')
    {
        if ($rel) {
            $this->rel[$rel] = true;
        }
        $this->href = $href;
    }
}

LinkProviderTrait.php

<?php


namespace Fig\Link;

use Psr\Link\LinkProviderInterface;
use Psr\Link\LinkInterface;

/**
 * Class LinkProviderTrait
 *
 * @implements LinkProviderInterface
 */
trait LinkProviderTrait
{
    /**
     * An array of the links in this provider.
     *
     * The keys of the array MUST be the spl_object_hash() of the object being stored.
     * That helps to ensure uniqueness.
     *
     * @var LinkInterface[]
     */
    private array $links = [];

    /**
     * {@inheritdoc}
     */
    public function getLinks(): iterable
    {
        return $this->links;
    }

    /**
     * {@inheritdoc}
     */
    public function getLinksByRel($rel): iterable
    {
        return array_filter($this->links, fn(LinkInterface $link) => in_array($rel, $link->getRels()));
    }
}

EvolvableLinkProviderTrait.php

<?php

namespace Fig\Link;

use Psr\Link\LinkInterface;
use Psr\Link\EvolvableLinkProviderInterface;

/**
 * Class EvolvableLinkProviderTrait
 *
 * @implements EvolvableLinkProviderInterface
 */
trait EvolvableLinkProviderTrait
{
    use LinkProviderTrait;

    /**
     * {@inheritdoc}
     */
    public function withLink(LinkInterface $link): static
    {
        $that = clone($this);
        $splosh = spl_object_hash($link);
        if (!array_key_exists($splosh, $that->links)) {
            $that->links[$splosh] = $link;
        }
        return $that;
    }

    /**
     * {@inheritdoc}
     */
    public function withoutLink(LinkInterface $link): static
    {
        $that = clone($this);
        $splosh = spl_object_hash($link);
        unset($that->links[$splosh]);
        return $that;
    }
}

GenericLinkProvider.php

<?php

namespace Fig\Link;

use Psr\Link\EvolvableLinkProviderInterface;
use Psr\Link\LinkInterface;

class GenericLinkProvider implements EvolvableLinkProviderInterface
{
    use EvolvableLinkProviderTrait;

    /**
     * Constructs a new link provider.
     *
     * @param LinkInterface[] $links
     *   Optionally, specify an initial set of links for this provider.
     *   Note that the keys of the array will be ignored.
     */
    public function __construct(array $links = [])
    {
        // This block will throw a type error if any item isn't a LinkInterface, by design.
        array_filter($links, fn(LinkInterface $item) => true);

        $hashes = array_map('spl_object_hash', $links);
        $this->links = array_combine($hashes, $links);
    }
}

test类

TemplatedHrefTraitTest.php

<?php

namespace Fig\Link\Tests;


use Fig\Link\Link;
use PHPUnit\Framework\TestCase;

class TemplatedHrefTraitTest extends TestCase
{
    /**
     *
     * @dataProvider templatedHrefProvider
     *
     * @param string $href
     *   The href to check.
     */
    public function test_templated(string $href): void
    {
        $link = (new Link())
            ->withHref($href);

        $this->assertTrue($link->isTemplated());
    }

    /**
     *
     * @dataProvider notTemplatedHrefProvider
     *
     * @param string $href
     *   The href to check.
     */
    public function test_not_templated(string $href): void
    {
        $link = (new Link())
            ->withHref($href);

        $this->assertFalse($link->isTemplated());
    }

    public function templatedHrefProvider(): iterable
    {
        return [
            ['http://www.google.com/{param}/foo'],
            ['http://www.google.com/foo?q={param}'],
        ];
    }

    public function notTemplatedHrefProvider(): iterable
    {
        return [
            ['http://www.google.com/foo'],
            ['/foo/bar/baz'],
        ];
    }
}

LinkTest.php

<?php

namespace Fig\Link\Tests;

use Fig\Link\Link;
use PHPUnit\Framework\TestCase;

class LinkTest extends TestCase
{

    public function test_can_set_and_retrieve_values(): void
    {
        $link = (new Link())
            ->withHref('http://www.google.com')
            ->withRel('next')
            ->withAttribute('me', 'you')
        ;

        $this->assertEquals('http://www.google.com', $link->getHref());
        $this->assertContains('next', $link->getRels());
        $this->assertArrayHasKey('me', $link->getAttributes());
        $this->assertEquals('you', $link->getAttributes()['me']);
    }

    public function test_can_remove_values(): void
    {
        $link = (new Link())
            ->withHref('http://www.google.com')
            ->withRel('next')
            ->withAttribute('me', 'you')
        ;

        $link = $link->withoutAttribute('me')
            ->withoutRel('next');

        $this->assertEquals('http://www.google.com', $link->getHref());
        $this->assertFalse(in_array('next', $link->getRels()));
        $this->assertFalse(array_key_exists('me', $link->getAttributes()));
    }

    public function test_multiple_rels(): void
    {
        $link = (new Link())
            ->withHref('http://www.google.com')
            ->withRel('next')
            ->withRel('reference');

        $this->assertCount(2, $link->getRels());
        $this->assertContains('next', $link->getRels());
        $this->assertContains('reference', $link->getRels());
    }

    public function test_constructor(): void
    {
        $link = new Link('next', 'http://www.google.com');

        $this->assertEquals('http://www.google.com', $link->getHref());
        $this->assertContains('next', $link->getRels());
    }
}

GenericLinkProviderTest.php

<?php

namespace Fig\Link\Tests;

use Fig\Link\GenericLinkProvider;
use Fig\Link\Link;
use PHPUnit\Framework\TestCase;

class GenericLinkProviderTest extends TestCase
{

    public function test_can_add_links_by_method(): void
    {
        $link = (new Link())
            ->withHref('http://www.google.com')
            ->withRel('next')
            ->withAttribute('me', 'you')
        ;

        $provider = (new GenericLinkProvider())
            ->withLink($link);

        $this->assertContains($link, $provider->getLinks());
    }


    public function test_can_add_links_by_constructor(): void
    {
        $link = (new Link())
            ->withHref('http://www.google.com')
            ->withRel('next')
            ->withAttribute('me', 'you')
        ;

        $provider = (new GenericLinkProvider())
            ->withLink($link);

        $this->assertContains($link, $provider->getLinks());
    }

    public function test_can_get_links_by_rel(): void
    {
        $link1 = (new Link())
            ->withHref('http://www.google.com')
            ->withRel('next')
            ->withAttribute('me', 'you')
        ;
        $link2 = (new Link())
            ->withHref('http://www.php-fig.org/')
            ->withRel('home')
            ->withAttribute('me', 'you')
        ;

        $provider = (new GenericLinkProvider())
            ->withLink($link1)
            ->withLink($link2);

        $links = $provider->getLinksByRel('home');
        $this->assertContains($link2, $links);
        $this->assertFalse(in_array($link1, $links));
    }

    public function test_can_remove_links(): void
    {
        $link = (new Link())
            ->withHref('http://www.google.com')
            ->withRel('next')
            ->withAttribute('me', 'you')
        ;

        $provider = (new GenericLinkProvider())
            ->withLink($link)
            ->withoutLink($link);

        $this->assertFalse(in_array($link, $provider->getLinks()));
    }
}

用法:

// Create a link object from a Drupal URL
$link = new \DrupalLinkPsr\Link(
    \Drupal\Core\Url::fromUri('https://example.com/')
);

// Get an array of attributes
$link->getAttributes();

// Get the URL
$link->getHref();

相关类:

Link.php

<?php

namespace DrupalLinkPsr;

use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Url;
use Psr\Link\EvolvableLinkInterface;
use Stringable;

class Link implements EvolvableLinkInterface
{

    /**
     * @param Url $url
     */
    function __construct(private Url $url)
    {

    }

    /**
     * @return string
     */
    public function getHref(): string
    {
        return $this->url->toString();
    }

    /**
     * @return bool
     */
    public function isTemplated(): bool
    {
        return FALSE;
    }

    /**
     * @return array
     */
    public function getRels(): array
    {
        $attributes = $this->getAttributes();
        if (isset($attributes['rel'])) {
            if (is_array($attributes['rel'])) {
                return $attributes['rel'];
            } else {
                return [$attributes['rel']];
            }
        }

        return [];
    }

    /**
     * @return array
     */
    public function getAttributes(): array
    {
        if ($attributes = $this->url->getOption('attributes')) {
            return $attributes;
        }

        return [];
    }

    /**
     * @param Stringable|string $href
     * @return $this
     */
    public function withHref(Stringable|string $href): static
    {
        $that = clone $this;
        $options = $that->url->getOptions();

        if (UrlHelper::isValid($href, true)) {
            $that->url = Url::fromUri($href);
        } else {
            $that->url = Url::fromUserInput($href);
        }

        $that->url->setOptions($options);

        return $that;
    }

    /**
     * @param string $rel
     * @return $this
     */
    public function withRel(string $rel): static
    {
        $rels = $this->getRels();
        $rels[] = $rel;

        return $this->withAttribute('rel', $rels);
    }

    /**
     * @param string $rel
     * @return $this
     */
    public function withoutRel(string $rel): static
    {
        $rels = $this->getRels();
        foreach ($rels as $i => $curRel) {
            if ($curRel == $rel) {
                unset($rels[$i]);
            }
        }

        return $this->withAttribute('rel', $rels);
    }

    /**
     * @param string $attribute
     * @param float|array|bool|Stringable|int|string $value
     * @return $this
     */
    public function withAttribute(string $attribute, float|array|bool|Stringable|int|string $value): static
    {
        $that = clone $this;

        $attributes = $that->getAttributes();
        $attributes[$attribute] = $value;
        $that->url->setOption('attributes', $attributes);

        return $that;
    }

    /**
     * @param string $attribute
     * @return $this
     */
    public function withoutAttribute(string $attribute): static
    {
        $that = clone $this;

        $attributes = $that->getAttributes();
        unset($attributes[$attribute]);
        $that->url->setOption('attributes', $attributes);

        return $that;
    }

    /**
     * @return void
     */
    function __clone()
    {
        $this->url = clone $this->url;
    }
}

Drupal\Core\Url 类:

<?php

namespace Drupal\Core;

use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
use Drupal\Core\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;

// cspell:ignore abempty

/**
 * Defines an object that holds information about a URL.
 *
 * In most cases, these should be created with the following methods:
 * - \Drupal\Core\Url::fromRoute()
 * - \Drupal\Core\Url::fromRouteMatch()
 * - \Drupal\Core\Url::fromUri()
 * - \Drupal\Core\Url::fromUserInput()
 *
 * @see \Drupal\Core\Entity\EntityBase::toUrl()
 */
class Url implements TrustedCallbackInterface {
  use DependencySerializationTrait;

  /**
   * The URL generator.
   *
   * @var \Drupal\Core\Routing\UrlGeneratorInterface
   */
  protected $urlGenerator;

  /**
   * The unrouted URL assembler.
   *
   * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
   */
  protected $urlAssembler;

  /**
   * The access manager.
   *
   * @var \Drupal\Core\Access\AccessManagerInterface
   */
  protected $accessManager;

  /**
   * The route name.
   *
   * @var string
   */
  protected $routeName;

  /**
   * The route parameters.
   *
   * @var array
   */
  protected $routeParameters = [];

  /**
   * The URL options.
   *
   * See \Drupal\Core\Url::fromUri() for details on the options.
   *
   * @var array
   */
  protected $options = [];

  /**
   * Indicates whether this object contains an external URL.
   *
   * @var bool
   */
  protected $external = FALSE;

  /**
   * Indicates whether this URL is for a URI without a Drupal route.
   *
   * @var bool
   */
  protected $unrouted = FALSE;

  /**
   * The non-route URI.
   *
   * Only used if self::$unrouted is TRUE.
   *
   * @var string
   */
  protected $uri;

  /**
   * Stores the internal path, if already requested by getInternalPath().
   *
   * @var string
   */
  protected $internalPath;

  /**
   * Constructs a new Url object.
   *
   * In most cases, use Url::fromRoute() or Url::fromUri() rather than
   * constructing Url objects directly in order to avoid ambiguity and make your
   * code more self-documenting.
   *
   * @param string $route_name
   *   The name of the route
   * @param array $route_parameters
   *   (optional) An associative array of parameter names and values.
   * @param array $options
   *   See \Drupal\Core\Url::fromUri() for details.
   *
   * @see static::fromRoute()
   * @see static::fromUri()
   *
   * @todo Update this documentation for non-routed URIs in
   *   https://www.drupal.org/node/2346787
   */
  public function __construct($route_name, $route_parameters = [], $options = []) {
    $this->routeName = $route_name;
    $this->routeParameters = $route_parameters;
    $this->options = $options;
  }

  /**
   * Creates a new Url object for a URL that has a Drupal route.
   *
   * This method is for URLs that have Drupal routes (that is, most pages
   * generated by Drupal). For non-routed local URIs relative to the base
   * path (like robots.txt) use Url::fromUri() with the base: scheme.
   *
   * @param string $route_name
   *   The name of the route
   * @param array $route_parameters
   *   (optional) An associative array of route parameter names and values.
   * @param array $options
   *   See \Drupal\Core\Url::fromUri() for details.
   *
   * @return static
   *   A new Url object for a routed (internal to Drupal) URL.
   *
   * @see \Drupal\Core\Url::fromUserInput()
   * @see \Drupal\Core\Url::fromUri()
   */
  public static function fromRoute($route_name, $route_parameters = [], $options = []) {
    return new static($route_name, $route_parameters, $options);
  }

  /**
   * Creates a new URL object from a route match.
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The route match.
   *
   * @return static
   */
  public static function fromRouteMatch(RouteMatchInterface $route_match) {
    if ($route_match->getRouteObject()) {
      return new static($route_match->getRouteName(), $route_match->getRawParameters()->all());
    }
    else {
      throw new \InvalidArgumentException('Route required');
    }
  }

  /**
   * Creates a Url object for a relative URI reference submitted by user input.
   *
   * Use this method to create a URL for user-entered paths that may or may not
   * correspond to a valid Drupal route.
   *
   * @param string $user_input
   *   User input for a link or path. The first character must be one of the
   *   following characters:
   *   - '/': A path within the current site. This path might be to a Drupal
   *     route (e.g., '/admin'), to a file (e.g., '/README.txt'), or to
   *     something processed by a non-Drupal script (e.g.,
   *     '/not/a/drupal/page'). If the path matches a Drupal route, then the
   *     URL generation will include Drupal's path processors (e.g.,
   *     language-prefixing and aliasing). Otherwise, the URL generation will
   *     just append the passed-in path to Drupal's base path.
   *   - '?': A query string for the current page or resource.
   *   - '#': A fragment (jump-link) on the current page or resource.
   *   This helps reduce ambiguity for user-entered links and paths, and
   *   supports user interfaces where users may normally use auto-completion
   *   to search for existing resources, but also may type one of these
   *   characters to link to (e.g.) a specific path on the site.
   *   (With regard to the URI specification, the user input is treated as a
   *   @link https://tools.ietf.org/html/rfc3986#section-4.2 relative URI reference @endlink
   *   where the relative part is of type
   *   @link https://tools.ietf.org/html/rfc3986#section-3.3 path-abempty @endlink.)
   * @param array $options
   *   (optional) An array of options. See Url::fromUri() for details.
   *
   * @return static
   *   A new Url object based on user input.
   *
   * @throws \InvalidArgumentException
   *   Thrown when the user input does not begin with one of the following
   *   characters: '/', '?', or '#'.
   */
  public static function fromUserInput($user_input, $options = []) {
    // Ensuring one of these initial characters also enforces that what is
    // passed is a relative URI reference rather than an absolute URI,
    // because these are URI reserved characters that a scheme name may not
    // start with.
    if ((strpos($user_input, '/') !== 0) && (strpos($user_input, '#') !== 0) && (strpos($user_input, '?') !== 0)) {
      throw new \InvalidArgumentException("The user-entered string '$user_input' must begin with a '/', '?', or '#'.");
    }

    // fromUri() requires an absolute URI, so prepend the appropriate scheme
    // name.
    return static::fromUri('internal:' . $user_input, $options);
  }

  /**
   * Creates a new Url object from a URI.
   *
   * This method is for generating URLs for URIs that:
   * - do not have Drupal routes: both external URLs and unrouted local URIs
   *   like base:robots.txt
   * - do have a Drupal route but have a custom scheme to simplify linking.
   *   Currently, there is only the entity: scheme (This allows URIs of the
   *   form entity:{entity_type}/{entity_id}. For example: entity:node/1
   *   resolves to the entity.node.canonical route with a node parameter of 1.)
   *
   * For URLs that have Drupal routes (that is, most pages generated by Drupal),
   * use Url::fromRoute().
   *
   * @param string $uri
   *   The URI of the resource including the scheme. For user input that may
   *   correspond to a Drupal route, use internal: for the scheme. For paths
   *   that are known not to be handled by the Drupal routing system (such as
   *   static files), use base: for the scheme to get a link relative to the
   *   Drupal base path (like the <base> HTML element). For a link to an entity
   *   you may use entity:{entity_type}/{entity_id} URIs. The internal: scheme
   *   should be avoided except when processing actual user input that may or
   *   may not correspond to a Drupal route. Normally use Url::fromRoute() for
   *   code linking to any Drupal page.
   * @param array $options
   *   (optional) An associative array of additional URL options, with the
   *   following elements:
   *   - 'query': An array of query key/value-pairs (without any URL-encoding)
   *     to append to the URL.
   *   - 'fragment': A fragment identifier (named anchor) to append to the URL.
   *     Do not include the leading '#' character.
   *   - 'absolute': Defaults to FALSE. Whether to force the output to be an
   *     absolute link (beginning with http:). Useful for links that will be
   *     displayed outside the site, such as in an RSS feed.
   *   - 'attributes': An associative array of HTML attributes that will be
   *     added to the anchor tag if you use the \Drupal\Core\Link class to make
   *     the link.
   *   - 'language': An optional language object used to look up the alias
   *     for the URL. If $options['language'] is omitted, it defaults to the
   *     current language for the language type LanguageInterface::TYPE_URL.
   *   - 'https': Whether this URL should point to a secure location. If not
   *     defined, the current scheme is used, so the user stays on HTTP or HTTPS
   *     respectively. TRUE enforces HTTPS and FALSE enforces HTTP.
   *
   * @return static
   *   A new Url object with properties depending on the URI scheme. Call the
   *   access() method on this to do access checking.
   *
   * @throws \InvalidArgumentException
   *   Thrown when the passed in path has no scheme.
   *
   * @see \Drupal\Core\Url::fromRoute()
   * @see \Drupal\Core\Url::fromUserInput()
   */
  public static function fromUri($uri, $options = []) {
    // parse_url() incorrectly parses base:number/... as hostname:port/...
    // and not the scheme. Prevent that by prefixing the path with a slash.
    if (preg_match('/^base:\d/', $uri)) {
      $uri = str_replace('base:', 'base:/', $uri);
    }
    $uri_parts = parse_url($uri);
    if ($uri_parts === FALSE) {
      throw new \InvalidArgumentException("The URI '$uri' is malformed.");
    }
    // We support protocol-relative URLs.
    if (strpos($uri, '//') === 0) {
      $uri_parts['scheme'] = '';
    }
    elseif (empty($uri_parts['scheme'])) {
      throw new \InvalidArgumentException("The URI '$uri' is invalid. You must use a valid URI scheme.");
    }
    $uri_parts += ['path' => ''];
    // Discard empty fragment in $options for consistency with parse_url().
    if (isset($options['fragment']) && strlen($options['fragment']) == 0) {
      unset($options['fragment']);
    }
    // Extract query parameters and fragment and merge them into $uri_options,
    // but preserve the original $options for the fallback case.
    $uri_options = $options;
    if (isset($uri_parts['fragment']) && $uri_parts['fragment'] !== '') {
      $uri_options += ['fragment' => $uri_parts['fragment']];
      unset($uri_parts['fragment']);
    }

    if (!empty($uri_parts['query'])) {
      $uri_query = [];
      parse_str($uri_parts['query'], $uri_query);
      $uri_options['query'] = isset($uri_options['query']) ? $uri_options['query'] + $uri_query : $uri_query;
      unset($uri_parts['query']);
    }

    if ($uri_parts['scheme'] === 'entity') {
      $url = static::fromEntityUri($uri_parts, $uri_options, $uri);
    }
    elseif ($uri_parts['scheme'] === 'internal') {
      $url = static::fromInternalUri($uri_parts, $uri_options);
    }
    elseif ($uri_parts['scheme'] === 'route') {
      $url = static::fromRouteUri($uri_parts, $uri_options, $uri);
    }
    else {
      $url = new static($uri, [], $options);
      if ($uri_parts['scheme'] !== 'base') {
        $url->external = TRUE;
        $url->setOption('external', TRUE);
      }
      $url->setUnrouted();
    }

    return $url;
  }

  /**
   * Create a new Url object for entity URIs.
   *
   * @param array $uri_parts
   *   Parts from a URI of the form entity:{entity_type}/{entity_id} as from
   *   parse_url().
   * @param array $options
   *   An array of options, see \Drupal\Core\Url::fromUri() for details.
   * @param string $uri
   *   The original entered URI.
   *
   * @return static
   *   A new Url object for an entity's canonical route.
   *
   * @throws \InvalidArgumentException
   *   Thrown if the entity URI is invalid.
   */
  protected static function fromEntityUri(array $uri_parts, array $options, $uri) {
    [$entity_type_id, $entity_id] = explode('/', $uri_parts['path'], 2);
    if ($uri_parts['scheme'] != 'entity' || $entity_id === '') {
      throw new \InvalidArgumentException("The entity URI '$uri' is invalid. You must specify the entity id in the URL. e.g., entity:node/1 for loading the canonical path to node entity with id 1.");
    }

    return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id], $options);
  }

  /**
   * Creates a new Url object for 'internal:' URIs.
   *
   * Important note: the URI minus the scheme can NOT simply be validated by a
   * \Drupal\Core\Path\PathValidatorInterface implementation. The semantics of
   * the 'internal:' URI scheme are different:
   * - PathValidatorInterface accepts paths without a leading slash (e.g.
   *   'node/add') as well as 2 special paths: '<front>' and '<none>', which are
   *   mapped to the correspondingly named routes.
   * - 'internal:' URIs store paths with a leading slash that represents the
   *   root — i.e. the front page — (e.g. 'internal:/node/add'), and doesn't
   *   have any exceptions.
   *
   * To clarify, a few examples of path plus corresponding 'internal:' URI:
   * - 'node/add' -> 'internal:/node/add'
   * - 'node/add?foo=bar' -> 'internal:/node/add?foo=bar'
   * - 'node/add#kitten' -> 'internal:/node/add#kitten'
   * - '<front>' -> 'internal:/'
   * - '<front>foo=bar' -> 'internal:/?foo=bar'
   * - '<front>#kitten' -> 'internal:/#kitten'
   * - '<none>' -> 'internal:'
   * - '<none>foo=bar' -> 'internal:?foo=bar'
   * - '<none>#kitten' -> 'internal:#kitten'
   *
   * Therefore, when using a PathValidatorInterface to validate 'internal:'
   * URIs, we must map:
   * - 'internal:' (path component is '')  to the special '<none>' path
   * - 'internal:/' (path component is '/') to the special '<front>' path
   * - 'internal:/some-path' (path component is '/some-path') to 'some-path'
   *
   * @param array $uri_parts
   *   Parts from a URI of the form internal:{path} as from parse_url().
   * @param array $options
   *   An array of options, see \Drupal\Core\Url::fromUri() for details.
   *
   * @return static
   *   A new Url object for an 'internal:' URI.
   *
   * @throws \InvalidArgumentException
   *   Thrown when the URI's path component doesn't have a leading slash.
   */
  protected static function fromInternalUri(array $uri_parts, array $options) {
    // Both PathValidator::getUrlIfValidWithoutAccessCheck() and 'base:' URIs
    // only accept/contain paths without a leading slash, unlike 'internal:'
    // URIs, for which the leading slash means "relative to Drupal root" and
    // "relative to Symfony app root" (just like in Symfony/Drupal 8 routes).
    if (empty($uri_parts['path'])) {
      $uri_parts['path'] = '<none>';
    }
    elseif ($uri_parts['path'] === '/') {
      $uri_parts['path'] = '<front>';
    }
    else {
      if ($uri_parts['path'][0] !== '/') {
        throw new \InvalidArgumentException("The internal path component '{$uri_parts['path']}' is invalid. Its path component must have a leading slash, e.g. internal:/foo.");
      }
      // Remove the leading slash.
      $uri_parts['path'] = substr($uri_parts['path'], 1);

      if (UrlHelper::isExternal($uri_parts['path'])) {
        throw new \InvalidArgumentException("The internal path component '{$uri_parts['path']}' is external. You are not allowed to specify an external URL together with internal:/.");
      }
    }

    $url = \Drupal::pathValidator()
      ->getUrlIfValidWithoutAccessCheck($uri_parts['path']) ?: static::fromUri('base:' . $uri_parts['path'], $options);
    // Allow specifying additional options.
    $url->setOptions($options + $url->getOptions());

    return $url;
  }

  /**
   * Creates a new Url object for 'route:' URIs.
   *
   * @param array $uri_parts
   *   Parts from a URI of the form route:{route_name};{route_parameters} as
   *   from parse_url(), where the path is the route name optionally followed by
   *   a ";" followed by route parameters in key=value format with & separators.
   * @param array $options
   *   An array of options, see \Drupal\Core\Url::fromUri() for details.
   * @param string $uri
   *   The original passed in URI.
   *
   * @return static
   *   A new Url object for a 'route:' URI.
   *
   * @throws \InvalidArgumentException
   *   Thrown when the route URI does not have a route name.
   */
  protected static function fromRouteUri(array $uri_parts, array $options, $uri) {
    $route_parts = explode(';', $uri_parts['path'], 2);
    $route_name = $route_parts[0];
    if ($route_name === '') {
      throw new \InvalidArgumentException("The route URI '$uri' is invalid. You must have a route name in the URI. e.g., route:system.admin");
    }
    $route_parameters = [];
    if (!empty($route_parts[1])) {
      parse_str($route_parts[1], $route_parameters);
    }

    return new static($route_name, $route_parameters, $options);
  }

  /**
   * Returns the Url object matching a request.
   *
   * SECURITY NOTE: The request path is not checked to be valid and accessible
   * by the current user to allow storing and reusing Url objects by different
   * users. The 'path.validator' service getUrlIfValid() method should be used
   * instead of this one if validation and access check is desired. Otherwise,
   * 'access_manager' service checkNamedRoute() method should be used on the
   * router name and parameters stored in the Url object returned by this
   * method.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   A request object.
   *
   * @return static
   *   A Url object. Warning: the object is created even if the current user
   *   would get an access denied running the same request via the normal page
   *   flow.
   *
   * @throws \Drupal\Core\Routing\MatchingRouteNotFoundException
   *   Thrown when the request cannot be matched.
   */
  public static function createFromRequest(Request $request) {
    // We use the router without access checks because URL objects might be
    // created and stored for different users.
    $result = \Drupal::service('router.no_access_checks')->matchRequest($request);
    $route_name = $result[RouteObjectInterface::ROUTE_NAME];
    $route_parameters = $result['_raw_variables']->all();
    return new static($route_name, $route_parameters);
  }

  /**
   * Sets this Url to encapsulate an unrouted URI.
   *
   * @return $this
   */
  protected function setUnrouted() {
    $this->unrouted = TRUE;
    // What was passed in as the route name is actually the URI.
    // @todo Consider fixing this in https://www.drupal.org/node/2346787.
    $this->uri = $this->routeName;
    // Set empty route name and parameters.
    $this->routeName = NULL;
    $this->routeParameters = [];
    return $this;
  }

  /**
   * Generates a URI string that represents the data in the Url object.
   *
   * The URI will typically have the scheme of route: even if the object was
   * constructed using an entity: or internal: scheme. An internal: URI that
   * does not match a Drupal route with be returned here with the base: scheme,
   * and external URLs will be returned in their original form.
   *
   * @return string
   *   A URI representation of the Url object data.
   */
  public function toUriString() {
    if ($this->isRouted()) {
      $uri = 'route:' . $this->routeName;
      if ($this->routeParameters) {
        $uri .= ';' . UrlHelper::buildQuery($this->routeParameters);
      }
    }
    else {
      $uri = $this->uri;
    }
    $query = !empty($this->options['query']) ? ('?' . UrlHelper::buildQuery($this->options['query'])) : '';
    $fragment = isset($this->options['fragment']) && strlen($this->options['fragment']) ? '#' . $this->options['fragment'] : '';
    return $uri . $query . $fragment;
  }

  /**
   * Indicates if this Url is external.
   *
   * @return bool
   */
  public function isExternal() {
    return $this->external;
  }

  /**
   * Indicates if this Url has a Drupal route.
   *
   * @return bool
   */
  public function isRouted() {
    return !$this->unrouted;
  }

  /**
   * Returns the route name.
   *
   * @return string
   *
   * @throws \UnexpectedValueException.
   *   If this is a URI with no corresponding route.
   */
  public function getRouteName() {
    if ($this->unrouted) {
      throw new \UnexpectedValueException($this->getUri() . ' has no corresponding route.');
    }

    return $this->routeName;
  }

  /**
   * Returns the route parameters.
   *
   * @return array
   *
   * @throws \UnexpectedValueException.
   *   If this is a URI with no corresponding route.
   */
  public function getRouteParameters() {
    if ($this->unrouted) {
      throw new \UnexpectedValueException('External URLs do not have internal route parameters.');
    }

    return $this->routeParameters;
  }

  /**
   * Sets the route parameters.
   *
   * @param array $parameters
   *   The array of parameters.
   *
   * @return $this
   *
   * @throws \UnexpectedValueException.
   *   If this is a URI with no corresponding route.
   */
  public function setRouteParameters($parameters) {
    if ($this->unrouted) {
      throw new \UnexpectedValueException('External URLs do not have route parameters.');
    }
    $this->routeParameters = $parameters;
    return $this;
  }

  /**
   * Sets a specific route parameter.
   *
   * @param string $key
   *   The key of the route parameter.
   * @param mixed $value
   *   The route parameter.
   *
   * @return $this
   *
   * @throws \UnexpectedValueException.
   *   If this is a URI with no corresponding route.
   */
  public function setRouteParameter($key, $value) {
    if ($this->unrouted) {
      throw new \UnexpectedValueException('External URLs do not have route parameters.');
    }
    $this->routeParameters[$key] = $value;
    return $this;
  }

  /**
   * Returns the URL options.
   *
   * @return array
   *   The array of options. See \Drupal\Core\Url::fromUri() for details on what
   *   it contains.
   */
  public function getOptions() {
    return $this->options;
  }

  /**
   * Gets a specific option.
   *
   * See \Drupal\Core\Url::fromUri() for details on the options.
   *
   * @param string $name
   *   The name of the option.
   *
   * @return mixed
   *   The value for a specific option, or NULL if it does not exist.
   */
  public function getOption($name) {
    if (!isset($this->options[$name])) {
      return NULL;
    }

    return $this->options[$name];
  }

  /**
   * Sets the URL options.
   *
   * @param array $options
   *   The array of options. See \Drupal\Core\Url::fromUri() for details on what
   *   it contains.
   *
   * @return $this
   */
  public function setOptions($options) {
    $this->options = $options;
    return $this;
  }

  /**
   * Sets a specific option.
   *
   * See \Drupal\Core\Url::fromUri() for details on the options.
   *
   * @param string $name
   *   The name of the option.
   * @param mixed $value
   *   The option value.
   *
   * @return $this
   */
  public function setOption($name, $value) {
    $this->options[$name] = $value;
    return $this;
  }

  /**
   * Merges the URL options with any currently set.
   *
   * In the case of conflict with existing options, the new options will replace
   * the existing options.
   *
   * @param array $options
   *   The array of options. See \Drupal\Core\Url::fromUri() for details on what
   *   it contains.
   *
   * @return $this
   */
  public function mergeOptions($options) {
    $this->options = NestedArray::mergeDeep($this->options, $options);
    return $this;
  }

  /**
   * Returns the URI value for this Url object.
   *
   * Only to be used if self::$unrouted is TRUE.
   *
   * @return string
   *   A URI not connected to a route. May be an external URL.
   *
   * @throws \UnexpectedValueException
   *   Thrown when the URI was requested for a routed URL.
   */
  public function getUri() {
    if (!$this->unrouted) {
      throw new \UnexpectedValueException('This URL has a Drupal route, so the canonical form is not a URI.');
    }

    return $this->uri;
  }

  /**
   * Sets the value of the absolute option for this Url.
   *
   * @param bool $absolute
   *   (optional) Whether to make this Url absolute or not. Defaults to TRUE.
   *
   * @return $this
   */
  public function setAbsolute($absolute = TRUE) {
    $this->options['absolute'] = $absolute;
    return $this;
  }

  /**
   * Generates the string URL representation for this Url object.
   *
   * For an external URL, the string will contain the input plus any query
   * string or fragment specified by the options array.
   *
   * If this Url object was constructed from a Drupal route or from an internal
   * URI (URIs using the internal:, base:, or entity: schemes), the returned
   * string will either be a relative URL like /node/1 or an absolute URL like
   * http://example.com/node/1 depending on the options array, plus any
   * specified query string or fragment.
   *
   * @param bool $collect_bubbleable_metadata
   *   (optional) Defaults to FALSE. When TRUE, both the generated URL and its
   *   associated bubbleable metadata are returned.
   *
   * @return string|\Drupal\Core\GeneratedUrl
   *   A string URL.
   *   When $collect_bubbleable_metadata is TRUE, a GeneratedUrl object is
   *   returned, containing the generated URL plus bubbleable metadata.
   */
  public function toString($collect_bubbleable_metadata = FALSE) {
    if ($this->unrouted) {
      return $this->unroutedUrlAssembler()->assemble($this->getUri(), $this->getOptions(), $collect_bubbleable_metadata);
    }

    return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions(), $collect_bubbleable_metadata);
  }

  /**
   * Returns the route information for a render array.
   *
   * @return array
   *   An associative array suitable for a render array.
   */
  public function toRenderArray() {
    $render_array = [
      '#url' => $this,
      '#options' => $this->getOptions(),
    ];
    if (!$this->unrouted) {
      $render_array['#access_callback'] = [get_class(), 'renderAccess'];
    }
    return $render_array;
  }

  /**
   * Returns the internal path (system path) for this route.
   *
   * This path will not include any prefixes, fragments, or query strings.
   *
   * @return string
   *   The internal path for this route.
   *
   * @throws \UnexpectedValueException.
   *   If this is a URI with no corresponding system path.
   */
  public function getInternalPath() {
    if ($this->unrouted) {
      throw new \UnexpectedValueException('Unrouted URIs do not have internal representations.');
    }

    if (!isset($this->internalPath)) {
      $this->internalPath = $this->urlGenerator()->getPathFromRoute($this->getRouteName(), $this->getRouteParameters());
    }
    return $this->internalPath;
  }

  /**
   * Checks this Url object against applicable access check services.
   *
   * Determines whether the route is accessible or not.
   *
   * @param \Drupal\Core\Session\AccountInterface|null $account
   *   (optional) Run access checks for this account. NULL for the current user.
   * @param bool $return_as_object
   *   (optional) Defaults to FALSE.
   *
   * @return bool|\Drupal\Core\Access\AccessResultInterface
   *   The access result. Returns a boolean if $return_as_object is FALSE (this
   *   is the default) and otherwise an AccessResultInterface object.
   *   When a boolean is returned, the result of AccessInterface::isAllowed() is
   *   returned, i.e. TRUE means access is explicitly allowed, FALSE means
   *   access is either explicitly forbidden or "no opinion".
   */
  public function access(AccountInterface $account = NULL, $return_as_object = FALSE) {
    if ($this->isRouted()) {
      return $this->accessManager()->checkNamedRoute($this->getRouteName(), $this->getRouteParameters(), $account, $return_as_object);
    }
    return $return_as_object ? AccessResult::allowed() : TRUE;
  }

  /**
   * Checks a Url render element against applicable access check services.
   *
   * @param array $element
   *   A render element as returned from \Drupal\Core\Url::toRenderArray().
   *
   * @return bool
   *   Returns TRUE if the current user has access to the url, otherwise FALSE.
   */
  public static function renderAccess(array $element) {
    return $element['#url']->access();
  }

  /**
   * @return \Drupal\Core\Access\AccessManagerInterface
   */
  protected function accessManager() {
    if (!isset($this->accessManager)) {
      $this->accessManager = \Drupal::service('access_manager');
    }
    return $this->accessManager;
  }

  /**
   * Gets the URL generator.
   *
   * @return \Drupal\Core\Routing\UrlGeneratorInterface
   *   The URL generator.
   */
  protected function urlGenerator() {
    if (!$this->urlGenerator) {
      $this->urlGenerator = \Drupal::urlGenerator();
    }
    return $this->urlGenerator;
  }

  /**
   * Gets the unrouted URL assembler for non-Drupal URLs.
   *
   * @return \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
   *   The unrouted URL assembler.
   */
  protected function unroutedUrlAssembler() {
    if (!$this->urlAssembler) {
      $this->urlAssembler = \Drupal::service('unrouted_url_assembler');
    }
    return $this->urlAssembler;
  }

  /**
   * Sets the URL generator.
   *
   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
   *   (optional) The URL generator, specify NULL to reset it.
   *
   * @return $this
   */
  public function setUrlGenerator(UrlGeneratorInterface $url_generator = NULL) {
    $this->urlGenerator = $url_generator;
    $this->internalPath = NULL;
    return $this;
  }

  /**
   * Sets the unrouted URL assembler.
   *
   * @param \Drupal\Core\Utility\UnroutedUrlAssemblerInterface $url_assembler
   *   The unrouted URL assembler.
   *
   * @return $this
   */
  public function setUnroutedUrlAssembler(UnroutedUrlAssemblerInterface $url_assembler) {
    $this->urlAssembler = $url_assembler;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['renderAccess'];
  }

}

参考资料

PHP中PSR规范 PSR-13 超媒体链接 https://ibaiyang.github.io/blog/php/2022/04/03/PHP中PSR规范.html#psr-13-超媒体链接

mezzio/mezzio-hal 包 https://github.com/mezzio/mezzio-hal

jbboehr/php-psr 包 https://github.com/jbboehr/php-psr

php-fig/link-util 包 https://github.com/php-fig/link-util

Rudloff/drupal-link-psr13 包 https://github.com/Rudloff/drupal-link-psr13

drupal/core packagist https://packagist.org/packages/drupal/core

drupal/core github https://github.com/drupal/core


返回