正文
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;
}
}
php-fig/link-util 包
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()));
}
}
Rudloff/drupal-link-psr13 包
用法:
// 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