diff --git a/composer.json b/composer.json index b3956a887564..93d6b029d8c1 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "ext-xml": "*", "doctrine/event-manager": "~1.0", "doctrine/persistence": "~1.0", - "fig/link-util": "^1.0", "twig/twig": "^1.41|^2.10", "psr/cache": "~1.0", "psr/container": "^1.0", diff --git a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php index 0ca519ee7242..63bfa2eba026 100644 --- a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php @@ -11,9 +11,9 @@ namespace Symfony\Bridge\Twig\Extension; -use Fig\Link\GenericLinkProvider; -use Fig\Link\Link; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\WebLink\GenericLinkProvider; +use Symfony\Component\WebLink\Link; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php index 332165571c9a..1739c1ee9183 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php @@ -11,11 +11,11 @@ namespace Symfony\Bridge\Twig\Tests\Extension; -use Fig\Link\Link; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\WebLinkExtension; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\WebLink\Link; /** * @author Kévin Dunglas diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index a21b3c288cf8..bddfe8bee55b 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -42,7 +42,7 @@ "symfony/console": "^3.4|^4.0|^5.0", "symfony/var-dumper": "^3.4|^4.0|^5.0", "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/web-link": "^3.4|^4.0|^5.0", + "symfony/web-link": "^4.4|^5.0", "symfony/workflow": "^4.3|^5.0" }, "conflict": { diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 9cb7a58f6e85..14e62f074b24 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -12,9 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; use Doctrine\Common\Persistence\ManagerRegistry; -use Fig\Link\GenericLinkProvider; -use Fig\Link\Link; use Psr\Container\ContainerInterface; +use Psr\Link\LinkInterface; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; @@ -33,6 +32,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\GenericLinkProvider; /** * Common features needed in controllers. @@ -420,7 +420,7 @@ protected function dispatchMessage($message, array $stamps = []): Envelope * * @final */ - protected function addLink(Request $request, Link $link) + protected function addLink(Request $request, LinkInterface $link) { if (!class_exists(AddLinkHeaderListener::class)) { throw new \LogicException('You can not use the "addLink" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index aa03b2d13fab..fd064bb15b1a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; -use Fig\Link\Link; use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\DependencyInjection\Container; @@ -29,6 +28,7 @@ use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\WebLink\Link; abstract class ControllerTraitTest extends TestCase { diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 5e3a699e3dc1..152f1b65a5a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -31,7 +31,6 @@ }, "require-dev": { "doctrine/cache": "~1.0", - "fig/link-util": "^1.0", "symfony/asset": "^3.4|^4.0|^5.0", "symfony/browser-kit": "^4.3|^5.0", "symfony/console": "^4.3|^5.0", @@ -58,7 +57,7 @@ "symfony/workflow": "^4.3|^5.0", "symfony/yaml": "^3.4|^4.0|^5.0", "symfony/property-info": "^3.4|^4.0|^5.0", - "symfony/web-link": "^3.4|^4.0|^5.0", + "symfony/web-link": "^4.4|^5.0", "doctrine/annotations": "~1.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0", "twig/twig": "~1.34|~2.4" diff --git a/src/Symfony/Component/WebLink/CHANGELOG.md b/src/Symfony/Component/WebLink/CHANGELOG.md index 2204282c26ca..28dad5abdd74 100644 --- a/src/Symfony/Component/WebLink/CHANGELOG.md +++ b/src/Symfony/Component/WebLink/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.4.0 +----- + + * implement PSR-13 directly + 3.3.0 ----- diff --git a/src/Symfony/Component/WebLink/GenericLinkProvider.php b/src/Symfony/Component/WebLink/GenericLinkProvider.php new file mode 100644 index 000000000000..5c8b2e71144d --- /dev/null +++ b/src/Symfony/Component/WebLink/GenericLinkProvider.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\WebLink; + +use Psr\Link\EvolvableLinkProviderInterface; +use Psr\Link\LinkInterface; + +class GenericLinkProvider implements EvolvableLinkProviderInterface +{ + /** + * @var LinkInterface[] + */ + private $links = []; + + /** + * @param LinkInterface[] $links + */ + public function __construct(array $links = []) + { + $that = $this; + + foreach ($links as $link) { + $that = $that->withLink($link); + } + + $this->links = $that->links; + } + + /** + * {@inheritdoc} + */ + public function getLinks(): array + { + return array_values($this->links); + } + + /** + * {@inheritdoc} + */ + public function getLinksByRel($rel): array + { + $links = []; + + foreach ($this->links as $link) { + if (\in_array($rel, $link->getRels())) { + $links[] = $link; + } + } + + return $links; + } + + /** + * {@inheritdoc} + */ + public function withLink(LinkInterface $link) + { + $that = clone $this; + $that->links[spl_object_id($link)] = $link; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withoutLink(LinkInterface $link) + { + $that = clone $this; + unset($that->links[spl_object_id($link)]); + + return $that; + } +} diff --git a/src/Symfony/Component/WebLink/Link.php b/src/Symfony/Component/WebLink/Link.php new file mode 100644 index 000000000000..a7d034c1fae8 --- /dev/null +++ b/src/Symfony/Component/WebLink/Link.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\WebLink; + +use Psr\Link\EvolvableLinkInterface; + +class Link implements EvolvableLinkInterface +{ + // Relations defined in https://www.w3.org/TR/html5/links.html#links and applicable on link elements + public const REL_ALTERNATE = 'alternate'; + public const REL_AUTHOR = 'author'; + public const REL_HELP = 'help'; + public const REL_ICON = 'icon'; + public const REL_LICENSE = 'license'; + public const REL_SEARCH = 'search'; + public const REL_STYLESHEET = 'stylesheet'; + public const REL_NEXT = 'next'; + public const REL_PREV = 'prev'; + + // Relation defined in https://www.w3.org/TR/preload/ + public const REL_PRELOAD = 'preload'; + + // Relations defined in https://www.w3.org/TR/resource-hints/ + public const REL_DNS_PREFETCH = 'dns-prefetch'; + public const REL_PRECONNECT = 'preconnect'; + public const REL_PREFETCH = 'prefetch'; + public const REL_PRERENDER = 'prerender'; + + // Extra relations + public const REL_MERCURE = 'mercure'; + + private $href = ''; + + /** + * @var string[] + */ + private $rel = []; + + /** + * @var string[] + */ + private $attributes = []; + + public function __construct(string $rel = null, string $href = '') + { + if (null !== $rel) { + $this->rel[$rel] = $rel; + } + $this->href = $href; + } + + /** + * {@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_values($this->rel); + } + + /** + * {@inheritdoc} + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * {@inheritdoc} + */ + public function withHref($href) + { + $that = clone $this; + $that->href = $href; + $that->templated = $this->hrefIsTemplated($href); + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withRel($rel) + { + $that = clone $this; + $that->rel[$rel] = $rel; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withoutRel($rel) + { + $that = clone $this; + unset($that->rel[$rel]); + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withAttribute($attribute, $value) + { + $that = clone $this; + $that->attributes[$attribute] = $value; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withoutAttribute($attribute) + { + $that = clone $this; + unset($that->attributes[$attribute]); + + return $that; + } + + private function hrefIsTemplated(string $href): bool + { + return false !== strpos($href, '{') || false !== strpos($href, '}'); + } +} diff --git a/src/Symfony/Component/WebLink/Tests/EventListener/AddLinkHeaderListenerTest.php b/src/Symfony/Component/WebLink/Tests/EventListener/AddLinkHeaderListenerTest.php index a1b4a2078775..ddbbef7c263c 100644 --- a/src/Symfony/Component/WebLink/Tests/EventListener/AddLinkHeaderListenerTest.php +++ b/src/Symfony/Component/WebLink/Tests/EventListener/AddLinkHeaderListenerTest.php @@ -11,8 +11,6 @@ namespace Symfony\Component\WebLink\Tests\EventListener; -use Fig\Link\GenericLinkProvider; -use Fig\Link\Link; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -20,6 +18,8 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\GenericLinkProvider; +use Symfony\Component\WebLink\Link; /** * @author Kévin Dunglas diff --git a/src/Symfony/Component/WebLink/Tests/GenericLinkProviderTest.php b/src/Symfony/Component/WebLink/Tests/GenericLinkProviderTest.php new file mode 100644 index 000000000000..b4176fe7f7b5 --- /dev/null +++ b/src/Symfony/Component/WebLink/Tests/GenericLinkProviderTest.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\WebLink\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\WebLink\GenericLinkProvider; +use Symfony\Component\WebLink\Link; + +/** + * Test case borrowed from https://github.com/php-fig/link/. + */ +class GenericLinkProviderTest extends TestCase +{ + public function testCanAddLinksByMethod() + { + $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 testCanAddLinksByConstructor() + { + $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 testCanGetLinksByRel() + { + $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->assertNotContains($link1, $links); + } + + public function testCanRemoveLinks() + { + $link = (new Link()) + ->withHref('http://www.google.com') + ->withRel('next') + ->withAttribute('me', 'you') + ; + + $provider = (new GenericLinkProvider()) + ->withLink($link) + ->withoutLink($link); + + $this->assertNotContains($link, $provider->getLinks()); + } +} diff --git a/src/Symfony/Component/WebLink/Tests/HttpHeaderSerializerTest.php b/src/Symfony/Component/WebLink/Tests/HttpHeaderSerializerTest.php index 5347528ae02e..fa50645a7251 100644 --- a/src/Symfony/Component/WebLink/Tests/HttpHeaderSerializerTest.php +++ b/src/Symfony/Component/WebLink/Tests/HttpHeaderSerializerTest.php @@ -11,9 +11,9 @@ namespace Symfony\Component\WebLink\Tests; -use Fig\Link\Link; use PHPUnit\Framework\TestCase; use Symfony\Component\WebLink\HttpHeaderSerializer; +use Symfony\Component\WebLink\Link; class HttpHeaderSerializerTest extends TestCase { diff --git a/src/Symfony/Component/WebLink/Tests/LinkTest.php b/src/Symfony/Component/WebLink/Tests/LinkTest.php new file mode 100644 index 000000000000..979bbb8b4f67 --- /dev/null +++ b/src/Symfony/Component/WebLink/Tests/LinkTest.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\WebLink\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\WebLink\Link; + +/** + * Test case borrowed from https://github.com/php-fig/link/. + */ +class LinkTest extends TestCase +{ + public function testCanSetAndRetrieveValues() + { + $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 testCanRemoveValues() + { + $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->assertArrayNotHasKey('me', $link->getAttributes()); + } + + public function testMultipleRels() + { + $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 testConstructor() + { + $link = new Link('next', 'http://www.google.com'); + + $this->assertEquals('http://www.google.com', $link->getHref()); + $this->assertContains('next', $link->getRels()); + } + + /** + * @dataProvider templatedHrefProvider + */ + public function testTemplated(string $href) + { + $link = (new Link()) + ->withHref($href); + + $this->assertTrue($link->isTemplated()); + } + + /** + * @dataProvider notTemplatedHrefProvider + */ + public function testNotTemplated(string $href) + { + $link = (new Link()) + ->withHref($href); + + $this->assertFalse($link->isTemplated()); + } + + public function templatedHrefProvider() + { + return [ + ['http://www.google.com/{param}/foo'], + ['http://www.google.com/foo?q={param}'], + ]; + } + + public function notTemplatedHrefProvider() + { + return [ + ['http://www.google.com/foo'], + ['/foo/bar/baz'], + ]; + } +} diff --git a/src/Symfony/Component/WebLink/composer.json b/src/Symfony/Component/WebLink/composer.json index ba0042356571..87eccfbffa08 100644 --- a/src/Symfony/Component/WebLink/composer.json +++ b/src/Symfony/Component/WebLink/composer.json @@ -15,10 +15,13 @@ "homepage": "https://symfony.com/contributors" } ], + "provide": { + "psr/link-implementation": "1.0" + }, "require": { "php": "^7.1.3", - "fig/link-util": "^1.0", - "psr/link": "^1.0" + "psr/link": "^1.0", + "symfony/polyfill-php72": "^1.5" }, "suggest": { "symfony/http-kernel": ""